Initial commit

master
eater 5 years ago
commit ea8aa9f63c

3
.gitignore vendored

@ -0,0 +1,3 @@
.idea
.gradle
/build/

@ -0,0 +1,53 @@
plugins {
id 'org.jetbrains.kotlin.multiplatform' version '1.3.50'
}
repositories {
mavenCentral()
}
group 'net.cijber'
version '0.0.1'
apply plugin: 'maven-publish'
kotlin {
jvm()
js {
browser {
}
nodejs {
}
}
// For ARM, should be changed to iosArm32 or iosArm64
// For Linux, should be changed to e.g. linuxX64
// For MacOS, should be changed to e.g. macosX64
// For Windows, should be changed to e.g. mingwX64
linuxX64("linux")
sourceSets {
commonMain {
dependencies {
implementation kotlin('stdlib-common')
}
}
commonTest {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
}
}
jvmMain {
dependencies {
implementation kotlin('stdlib-jdk8')
}
}
jvmTest {
dependencies {
implementation kotlin('test')
implementation kotlin('test-junit')
}
}
linuxMain {
}
linuxTest {
}
}
}

@ -0,0 +1 @@
kotlin.code.style=official

Binary file not shown.

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

172
gradlew vendored

@ -0,0 +1,172 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

84
gradlew.bat vendored

@ -0,0 +1,84 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

@ -0,0 +1,4 @@
rootProject.name = 'pubgrub'
enableFeaturePreview('GRADLE_METADATA')

@ -0,0 +1,16 @@
package net.cijber.pubgrub
import net.cijber.pubgrub.packages.PackageSelection
import net.cijber.pubgrub.stubs.PackageId
import net.cijber.pubgrub.stubs.Version
class Assignment<P : PackageId, V : Version<V>>(
selection: PackageSelection<P, V>,
exclusive: Boolean,
val cause: Incompatibility<P, V>? = null,
val decisionLevel: Int = 0,
val index: Int = 0
) : Term<P, V>(selection, exclusive) {
val isDecision: Boolean
get() = cause == null
}

@ -0,0 +1,9 @@
package net.cijber.pubgrub
import net.cijber.pubgrub.stubs.PackageId
import net.cijber.pubgrub.stubs.Version
data class Incompatibility<P : PackageId, V : Version<V>>(val terms: List<Term<P, V>>, val cause: IncompatibilityCause<P, V>) {
val isFailure: Boolean
get() = terms.isEmpty() || (terms.count() == 1 && terms.first().pkg.isRoot);
}

@ -0,0 +1,20 @@
package net.cijber.pubgrub
import net.cijber.pubgrub.stubs.PackageId
import net.cijber.pubgrub.stubs.Version
sealed class IncompatibilityCause<P : PackageId, V : Version<V>> {
enum class SimpleCause {
Root,
Dependency,
UseLatest,
NoVersions,
UnknownSource
}
data class Simple<P : PackageId, V : Version<V>>(val cause: SimpleCause) : IncompatibilityCause<P, V>()
data class ConflictCause<P : PackageId, V : Version<V>>(
val conflict: Incompatibility<P, V>,
val other: Incompatibility<P, V>
) : IncompatibilityCause<P, V>()
}

@ -0,0 +1,105 @@
package net.cijber.pubgrub
import net.cijber.pubgrub.packages.PackageSelection
import net.cijber.pubgrub.stubs.PackageId
import net.cijber.pubgrub.stubs.Version
class PartialSolution<P : PackageId, V : Version<V>> {
private var decisionLevel = 0
private val assignments = mutableListOf<Assignment<P, V>>()
private var backtracking = false
private val decisions = mutableListOf<Assignment<P, V>>()
private val positive = mutableMapOf<P, Term<P, V>>()
private val negative = mutableMapOf<P, Term<P, V>>()
infix fun relation(term: Term<P, V>): SetRelation {
positive.get(term.pkg)?.let {
return it relation term
}
negative.get(term.pkg)?.let {
return it relation term
}
return SetRelation.Overlaps
}
fun derive(pkg: PackageSelection<P, V>, exclusive: Boolean, incompatibility: Incompatibility<P, V>) {
assign(Assignment(pkg, !exclusive, incompatibility, decisionLevel, assignments.count()))
}
private fun assign(assignment: Assignment<P, V>) {
assignments.add(assignment)
register(assignment)
}
private fun register(assignment: Assignment<P, V>) {
val pkg = assignment.pkg
val oldPositive = positive[pkg]
if (oldPositive != null) {
positive.remove(pkg)
oldPositive.intersect(assignment)?.let {
positive[pkg] = it
}
}
val oldNegative = negative[pkg]
val term = if (oldNegative == null) {
assignment
} else {
assignment.intersect(oldNegative)!!
}
if (!term.exclusive) {
negative.remove(pkg)
positive[pkg] = term
} else {
negative[pkg] = term
}
}
fun satisfier(term: Term<P, V>): Assignment<P, V> {
var assignedTerm: Term<P, V>? = null;
for (assignment in assignments) {
if (assignment.pkg != term.pkg) continue
assignedTerm = if (assignedTerm == null) {
assignment
} else {
assignedTerm.intersect(assignment)
}
if (assignedTerm?.satisfies(term) == true) {
return assignment
}
}
error("[BUG] $term is not satisfied")
}
fun backtrack(decisionLevel: Int) {
backtracking = true
val packages = mutableSetOf<P>()
while (assignments.lastOrNull()?.decisionLevel?.let { it > decisionLevel } == true) {
val removed = assignments.removeAt(assignments.lastIndex)
packages.add(removed.pkg)
if (removed.isDecision) {
decisions.remove(removed)
}
}
for (pkg in packages) {
positive.remove(pkg)
negative.remove(pkg)
}
for (assignment in assignments) {
if (packages.contains(assignment.pkg)) {
register(assignment)
}
}
}
}

@ -0,0 +1,9 @@
package net.cijber.pubgrub
enum class SetRelation {
Disjoint,
Overlaps,
Equal,
Superset,
Subset;
}

@ -0,0 +1,164 @@
package net.cijber.pubgrub
import net.cijber.pubgrub.packages.PackageSelection
import net.cijber.pubgrub.packages.PackageVersion
import net.cijber.pubgrub.stubs.PackageId
import net.cijber.pubgrub.stubs.PackageRetriever
import net.cijber.pubgrub.stubs.Version
import net.cijber.pubgrub.version.VersionRange
class Solver<P : PackageId, V : Version<V>>(
val packageRetriever: PackageRetriever<P, V>,
private val root: PackageVersion<P, V>
) {
private val incompatibilities: MutableMap<P, MutableList<Incompatibility<P, V>>> = mutableMapOf(
root.pkg to mutableListOf(
Incompatibility(
listOf(Term(PackageSelection(root.pkg, VersionRange.single(root.version)), true)),
IncompatibilityCause.Simple(IncompatibilityCause.SimpleCause.Root)
)
)
)
private val solution = PartialSolution<P, V>()
suspend fun solve() {
var next: P? = root.pkg
while (next != null) {
propagate(next)
next = choosePackageVersion()
}
}
private fun propagate(pkg: P) {
val changed = mutableSetOf(pkg)
while (changed.isNotEmpty()) {
val currentPkg = changed.first()
changed.remove(currentPkg)
for (incompat in incompatibilities.getOrPut(currentPkg, ::mutableListOf).reversed()) {
val result = propagateIncompatibility(incompat)
if (result == PropagationResult.Conflict) {
val rootCause = resolveConflict(incompat)
changed.clear()
@Suppress("UNCHECKED_CAST")
changed.add((propagateIncompatibility(rootCause) as? PropagationResult.Unsatisfied<P>)!!.pkg)
break
}
if (result is PropagationResult.Unsatisfied<*>) {
@Suppress("UNCHECKED_CAST")
changed.add(result.pkg as P)
}
}
}
}
private fun resolveConflict(incompat: Incompatibility<P, V>): Incompatibility<P, V> {
var workingIncompatibility = incompat
var newIncompatibility = false
while (!incompat.isFailure) {
var mostRecentTerm: Term<P, V>? = null
var mostRecentSatisfier: Assignment<P, V>? = null
var difference: Term<P, V>? = null
var previousSatisfierLevel = 1
for (term in workingIncompatibility.terms) {
val satisfier = solution.satisfier(term)
if (mostRecentSatisfier == null) {
mostRecentTerm = term
mostRecentSatisfier = satisfier
} else if (mostRecentSatisfier.index < satisfier.index) {
previousSatisfierLevel = maxOf(previousSatisfierLevel, mostRecentSatisfier.decisionLevel)
mostRecentTerm = term;
mostRecentSatisfier = satisfier;
difference = null;
} else {
previousSatisfierLevel = maxOf(previousSatisfierLevel, satisfier.decisionLevel)
}
if (mostRecentTerm == term) {
difference = mostRecentSatisfier.difference(mostRecentTerm)
if (difference != null) {
previousSatisfierLevel = maxOf(
previousSatisfierLevel,
solution.satisfier(difference.inverse).decisionLevel
)
}
}
}
if (previousSatisfierLevel < mostRecentSatisfier!!.decisionLevel ||
mostRecentSatisfier.cause == null
) {
solution.backtrack(previousSatisfierLevel);
if (newIncompatibility) {
addIncompatibility(workingIncompatibility)
}
return workingIncompatibility
}
val newTerms = workingIncompatibility.terms.filter { it != mostRecentTerm }.toMutableList()
mostRecentSatisfier.cause?.let {
newTerms.addAll(it.terms.filter { term -> term.pkg != mostRecentSatisfier.pkg })
}
if (difference != null) {
newTerms.add(difference.inverse)
}
workingIncompatibility = Incompatibility(
newTerms,
IncompatibilityCause.ConflictCause(workingIncompatibility, mostRecentSatisfier.cause!!)
)
}
error("Failed")
}
private fun addIncompatibility(incompat: Incompatibility<P, V>) {
for (term in incompat.terms) {
incompatibilities.getOrPut(term.pkg, { mutableListOf() }).add(incompat)
}
}
private fun propagateIncompatibility(incompatibility: Incompatibility<P, V>): PropagationResult {
val unsatisfied = run {
var unsatisfied: Term<P, V>? = null;
for (term in incompatibility.terms) {
when (val relation = solution.relation(term)) {
SetRelation.Disjoint -> return PropagationResult.None
SetRelation.Overlaps, SetRelation.Subset, SetRelation.Superset -> {
if (unsatisfied != null) {
return PropagationResult.None
}
unsatisfied = term
}
}
}
unsatisfied
} ?: return PropagationResult.Conflict
solution.derive(unsatisfied.pkgSelection, !unsatisfied.exclusive, incompatibility)
return PropagationResult.Unsatisfied(unsatisfied.pkg)
}
private sealed class PropagationResult {
object None : PropagationResult()
object Conflict : PropagationResult()
class Unsatisfied<P>(val pkg: P) : PropagationResult()
}
private fun choosePackageVersion(): P? {
return null
}
}

@ -0,0 +1,63 @@
package net.cijber.pubgrub
import net.cijber.pubgrub.packages.PackageSelection
import net.cijber.pubgrub.stubs.PackageId
import net.cijber.pubgrub.stubs.Version
import net.cijber.pubgrub.version.VersionConstraint
open class Term<P : PackageId, V : Version<V>>(val pkgSelection: PackageSelection<P, V>, val exclusive: Boolean) {
val constraint
get() = pkgSelection.constraint
val pkg
get() = pkgSelection.pkg
val inverse by lazy {
Term(pkgSelection, !exclusive)
}
infix fun relation(rhs: Term<P, V>): SetRelation {
if (rhs.pkg !== pkg) {
throw RuntimeException("Relations between terms can only be compared if they have the same package id")
}
val rhsConstraint = rhs.constraint
if (!rhs.exclusive) {
if (exclusive) {
if (rhsConstraint.allowsAll(constraint))
return SetRelation.Subset
}
}
return SetRelation.Disjoint
}
fun intersect(rhs: Term<P, V>): Term<P, V>? {
if (rhs.pkg !== pkg) {
throw RuntimeException("Relations between terms can only be compared if they have the same package id")
}
return if (exclusive != rhs.exclusive) {
val positive = if (!exclusive) this else rhs
val negative = if (!exclusive) rhs else this
nonEmptyTerm(positive.constraint.difference(negative.constraint), false)
} else if (!exclusive) {
nonEmptyTerm(constraint.intersect(rhs.constraint), false)
} else {
nonEmptyTerm(constraint.union(rhs.constraint), true)
}
}
private fun nonEmptyTerm(constraint: VersionConstraint<V>, exclusive: Boolean): Term<P, V>? {
if (constraint.isEmpty) return null
return Term(PackageSelection(pkg, constraint), exclusive)
}
fun satisfies(term: Term<P, V>): Boolean = term.pkg == pkg &&
(relation(term) == SetRelation.Subset || relation(term) == SetRelation.Equal)
fun difference(rhs: Term<P, V>) = intersect(rhs.inverse)
}

@ -0,0 +1,13 @@
package net.cijber.pubgrub.packages
import net.cijber.pubgrub.stubs.PackageId
import net.cijber.pubgrub.stubs.Version
import net.cijber.pubgrub.version.VersionConstraint
data class PackageSelection<P : PackageId, V : Version<V>>(
val pkg: P,
val constraint: VersionConstraint<V>
) {
fun intersect(rhs: PackageSelection<P, V>): PackageSelection<P, V> =
PackageSelection(pkg, constraint.intersect(rhs.constraint))
}

@ -0,0 +1,6 @@
package net.cijber.pubgrub.packages
import net.cijber.pubgrub.stubs.PackageId
import net.cijber.pubgrub.stubs.Version
data class PackageVersion<P : PackageId, V : Version<V>>(val pkg: P, val version: V)

@ -0,0 +1,5 @@
package net.cijber.pubgrub.stubs
interface PackageId {
val isRoot: Boolean
}

@ -0,0 +1,8 @@
package net.cijber.pubgrub.stubs
import net.cijber.pubgrub.Term
interface PackageRetriever<P: PackageId, V: Version<V>> {
suspend fun getIncompatibilitiesForVersion(pkg: P, version: V): Iterable<Term<P, V>>
suspend fun getVersions(pkg: P): Iterable<V>
}

@ -0,0 +1,3 @@
package net.cijber.pubgrub.stubs
interface Version<V> : Comparable<V>

@ -0,0 +1,81 @@
package net.cijber.pubgrub.version
import net.cijber.pubgrub.stubs.Version
data class VersionBorder<V : Version<V>>(val version: V, val inclusive: Boolean, val location: Location) :
Comparable<VersionBorder<V>> {
enum class Location {
Top,
Bottom;
}
fun above(otherVersion: V, inside: Boolean = true): Boolean =
when (otherVersion.compareTo(version)) {
-1 -> true
0 -> inside == inclusive
else -> false
}
fun beneath(otherVersion: V, inside: Boolean = true): Boolean =
when (otherVersion.compareTo(version)) {
1 -> true
0 -> inside == inclusive
else -> false
}
override operator fun compareTo(other: VersionBorder<V>): Int {
val diff = version.compareTo(other.version)
if (diff == 0) {
if (other.inclusive == inclusive) {
return 0
}
if (inclusive) {
if (location == Location.Top) {
return 1
}
return -1
}
if (location == Location.Bottom) {
return -1
}
return 1
}
return diff
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as VersionBorder<*>
if (version != other.version) return false
if (inclusive != other.inclusive) return false
return true
}
override fun hashCode(): Int {
var result = version.hashCode()
result = 31 * result + inclusive.hashCode()
return result
}
fun reversed(): VersionBorder<V> {
return VersionBorder(version, !inclusive, if (location == Location.Top) Location.Bottom else Location.Top)
}
companion object {
fun <V : Version<V>> top(version: V, inclusive: Boolean = true) =
VersionBorder(version, inclusive, Location.Top)
fun <V : Version<V>> bottom(version: V, inclusive: Boolean = true) =
VersionBorder(version, inclusive, Location.Bottom)
}
}

@ -0,0 +1,14 @@
package net.cijber.pubgrub.version
import net.cijber.pubgrub.stubs.Version
interface VersionConstraint<V : Version<V>> {
fun allows(version: V): Boolean
fun allowsAll(rhs: VersionConstraint<V>): Boolean
fun allowsAny(rhs: VersionConstraint<V>): Boolean
fun intersect(rhs: VersionConstraint<V>): VersionConstraint<V>
fun difference(rhs: VersionConstraint<V>): VersionConstraint<V>
fun union(rhs: VersionConstraint<V>): VersionConstraint<V>
val isAny: Boolean
val isEmpty: Boolean
}

@ -0,0 +1,14 @@
package net.cijber.pubgrub.version
import net.cijber.pubgrub.stubs.Version
class VersionEmpty<V : Version<V>> : VersionConstraint<V> {
override val isAny: Boolean = false
override val isEmpty: Boolean = true
override fun allows(version: V): Boolean = false
override fun allowsAll(rhs: VersionConstraint<V>): Boolean = rhs.isEmpty
override fun allowsAny(rhs: VersionConstraint<V>): Boolean = false
override fun union(rhs: VersionConstraint<V>): VersionConstraint<V> = rhs
override fun intersect(rhs: VersionConstraint<V>): VersionConstraint<V> = this
override fun difference(rhs: VersionConstraint<V>): VersionConstraint<V> = this
}

@ -0,0 +1,241 @@
package net.cijber.pubgrub.version
import net.cijber.pubgrub.SetRelation
import net.cijber.pubgrub.stubs.Version
open class VersionRange<V : Version<V>>(
val bottom: VersionBorder<V>?,
val top: VersionBorder<V>?
) :
VersionConstraint<V> {
override val isEmpty: Boolean = false
override val isAny: Boolean = bottom == null && top == null
infix fun relation(rhs: VersionRange<V>): SetRelation {
val topDiff = when {
top != null && rhs.top != null -> top.compareTo(rhs.top)
top != null -> -1
rhs.top != null -> 1
else -> 0
}
val bottomDiff = when {
bottom != null && rhs.bottom != null -> bottom.compareTo(rhs.bottom)
bottom != null -> 1
rhs.bottom != null -> -1
else -> 0
}
if (topDiff == 0 && bottomDiff == 0) {
return SetRelation.Equal
}
if (topDiff == 1 && bottomDiff == -1) {
return SetRelation.Superset
}
if (topDiff <= 0 && bottomDiff >= 0) {
return SetRelation.Subset
}
val topBottomDiff = when {
top != null && rhs.bottom != null -> top.compareTo(rhs.bottom)
else -> 1
}
val bottomTopDiff = when {
bottom != null && rhs.top != null -> bottom.compareTo(rhs.top)
else -> -1
}
if (bottomTopDiff == 1 || topBottomDiff == -1) {
return SetRelation.Disjoint
}
return SetRelation.Overlaps
}
override fun allowsAll(rhs: VersionConstraint<V>): Boolean {
if (rhs.isEmpty)
return true
if (rhs.isAny)
return false
if (rhs is VersionRange) {
return relation(rhs) in setOf(SetRelation.Equal, SetRelation.Superset)
}
if (rhs is VersionUnion) {
return rhs.constraints.all(this::allowsAll)
}
error("Unknown VersionConstraint: $rhs")
}
override fun allowsAny(rhs: VersionConstraint<V>): Boolean {
if (rhs.isEmpty || rhs.isAny)
return true
if (rhs is VersionRange) {
return relation(rhs) != SetRelation.Disjoint
}
if (rhs is VersionUnion) {
return rhs.constraints.any(this::allowsAny)
}
error("Unknown VersionConstraint: $rhs")
}
override fun allows(version: V): Boolean {
return (bottom?.beneath(version, true) ?: true) &&
(top?.above(version, true) ?: true)
}
override fun intersect(rhs: VersionConstraint<V>): VersionConstraint<V> {
if (rhs.isEmpty) {
return rhs
}
if (rhs.isAny) {
return this
}
if (rhs is VersionUnion) {
return rhs.intersect(this)
}
if (rhs is VersionRange) {
if (relation(rhs) != SetRelation.Disjoint) {
return VersionRange(
when {
rhs.bottom != null && bottom != null -> maxOf(rhs.bottom, bottom)
else -> bottom ?: rhs.bottom
},
when {
rhs.top != null && top != null -> minOf(rhs.top, top)
else -> top ?: rhs.top
}
)
}
return VersionEmpty()
}
error("Unkown VersionConstraint: $rhs")
}
override fun union(rhs: VersionConstraint<V>): VersionConstraint<V> {
if (rhs is VersionRange) {
return when (relation(rhs)) {
SetRelation.Superset, SetRelation.Equal -> this
SetRelation.Subset -> rhs
SetRelation.Overlaps -> VersionRange(
when {
rhs.bottom != null && bottom != null -> minOf(rhs.bottom, bottom)
else -> null
},
when {
rhs.top != null && top != null -> maxOf(rhs.top, top)
else -> null
}
)
SetRelation.Disjoint -> VersionUnion(listOf(this, rhs))
}
}
if (rhs is VersionEmpty) {
return this
}
if (rhs is VersionUnion) {
return VersionUnion(rhs.constraints + listOf(this))
}
error("Unknown VersionConstraint given: $rhs")
}
override fun difference(rhs: VersionConstraint<V>): VersionConstraint<V> {
if (rhs.isEmpty) {
return this
}
if (rhs.isAny) {
return VersionEmpty()
}
if (rhs is VersionUnion) {
val ranges = mutableListOf(this)
for (constraint in rhs.constraints) {
val rel = constraint relation ranges.last()
if (rel == SetRelation.Disjoint) {
continue
}
val last = ranges.dropLast(1).first()
val res = last.difference(constraint)
if (res.isEmpty) {
break
}
if (res is VersionUnion) {
ranges.addAll(res.constraints)
}
if (res is VersionRange) {
ranges.add(res)
}
}
return when (ranges.count()) {
0 -> VersionEmpty()
1 -> ranges.first()
else -> VersionUnion(ranges)
}
}
if (rhs is VersionRange) {
return when (rhs relation this) {
SetRelation.Disjoint -> this
SetRelation.Equal, SetRelation.Superset -> VersionEmpty()
SetRelation.Overlaps -> VersionRange(
when {
bottom != null && rhs.bottom != null && rhs.bottom < bottom -> rhs.top
else -> bottom
},
when {
top != null && rhs.top != null && rhs.top > top -> rhs.bottom
else -> top
}
)
SetRelation.Subset -> {
if (top == rhs.top) {
return VersionRange(bottom, rhs.bottom?.reversed())
}
if (bottom == rhs.bottom) {
return VersionRange(rhs.top?.reversed(), top)
}
return VersionUnion(
listOf(
VersionRange(bottom, rhs.bottom?.reversed()),
VersionRange(rhs.top?.reversed(), top)
)
)
}
}
}
error("Unknown VersionConstraint given: $rhs")
}
companion object {
fun <V : Version<V>> single(version: V) =
VersionRange(VersionBorder.bottom(version), VersionBorder.top(version))
}
}

@ -0,0 +1,91 @@
package net.cijber.pubgrub.version
import net.cijber.pubgrub.stubs.Version
///
/// TODO: could be optimized to be sorted by version, which would make the function O((N+M)-1)
///
class VersionUnion<V : Version<V>>(val constraints: List<VersionRange<V>>) : VersionConstraint<V> {
override val isEmpty: Boolean
get() = constraints.isEmpty()
override val isAny: Boolean
get() = false
override fun allows(version: V): Boolean {
return constraints.any {
it.allows(version)
}
}
override fun allowsAll(rhs: VersionConstraint<V>): Boolean {
return constraints.all {
it.allowsAll(rhs)
}
}
override fun allowsAny(rhs: VersionConstraint<V>): Boolean {
return constraints.any {
it.allowsAny(rhs)
}
}
override fun intersect(rhs: VersionConstraint<V>): VersionConstraint<V> {
if (rhs.isAny) {
return this
}
if (rhs.isEmpty) {
return rhs
}
val found = constraints
.flatMap {
when (val diff = it.intersect(rhs)) {
is VersionRange -> listOf(diff)
is VersionUnion -> diff.constraints
else -> listOf()
}
}
return when (found.count()) {
0 -> VersionEmpty()
1 -> found.first()
else -> VersionUnion(found)
}
}
override fun difference(rhs: VersionConstraint<V>): VersionConstraint<V> {
if (rhs.isAny) {
return VersionEmpty()
}
if (rhs.isEmpty) {
return this
}
val found = constraints
.flatMap {
when (val diff = it.difference(rhs)) {
is VersionRange -> listOf(diff)
is VersionUnion -> diff.constraints
else -> listOf()
}
}
return when (found.count()) {
0 -> VersionEmpty()
1 -> found.first()
else -> VersionUnion(found)
}
}
override fun union(rhs: VersionConstraint<V>): VersionConstraint<V> {
if (rhs !is VersionUnion) {
return rhs.union(this)
}
return VersionUnion(rhs.constraints + this.constraints)
}
}

@ -0,0 +1,77 @@
package net.cijber.pubgrub.commonTest
import net.cijber.pubgrub.commonTest.impl.VersionImpl
import net.cijber.pubgrub.version.VersionBorder
import net.cijber.pubgrub.version.VersionRange
import net.cijber.pubgrub.version.VersionUnion
import kotlin.test.Asserter
import kotlin.test.DefaultAsserter
import kotlin.test.Test
class VersionConstraintTest : Asserter by DefaultAsserter {
@Test
fun difference() {
val a = VersionUnion(
listOf(
VersionRange(null, VersionBorder.top(VersionImpl(4))),
VersionRange(VersionBorder.bottom(VersionImpl(6)), null)
)
)
val b = VersionRange(
VersionBorder.top(VersionImpl(3)),
VersionBorder.top(VersionImpl(7))
)
val diff = a.difference(b)
assertTrue("diff is VersionUnion", diff is VersionUnion)
if (diff !is VersionUnion) {
error("")
}
val first = diff.constraints.first()
val last = diff.constraints.last()
assertEquals("First has no bottom", null, first.bottom)
assertEquals("First top at 3", VersionImpl(3), first.top?.version)
assertEquals("Last has no top", null, last.top)
assertEquals("Last bottom at 7", VersionImpl(7), last.bottom?.version)
}
@Test
fun intersection() {
val a = VersionUnion(
listOf(
VersionRange(null, VersionBorder.top(VersionImpl(4))),
VersionRange(VersionBorder.bottom(VersionImpl(6)), null)
)
)
val b = VersionRange(
VersionBorder.bottom(VersionImpl(3)),
VersionBorder.top(VersionImpl(7))
)
val diff = a.intersect(b)
assertTrue("diff is VersionUnion: $diff", diff is VersionUnion)
if (diff !is VersionUnion) {
error("")
}
val first = diff.constraints.first()
val last = diff.constraints.last()
assertEquals("First bottom at 3", VersionImpl(3), first.bottom?.version)
assertEquals("First top at 4", VersionImpl(4), first.top?.version)
assertEquals("Last bottom at 6", VersionImpl(6), last.bottom?.version)
assertEquals("Last top at 7", VersionImpl(7), last.top?.version)
}
}

@ -0,0 +1,9 @@
package net.cijber.pubgrub.commonTest.impl
import net.cijber.pubgrub.stubs.Version
data class VersionImpl(val number: Int) : Version<VersionImpl> {
override fun compareTo(other: VersionImpl): Int {
return compareValues(number, other.number)
}
}
Loading…
Cancel
Save