Merge remote-tracking branch 'origin/master'

This commit is contained in:
Him188 2020-09-21 13:22:55 +08:00
commit a434e6f569
5 changed files with 385 additions and 66 deletions

View File

@ -0,0 +1,251 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*
*/
@file:Suppress("MemberVisibilityCanBePrivate")
package net.mamoe.mirai.console.internal.util.semver
import net.mamoe.mirai.console.util.SemVersion
import kotlin.math.max
import kotlin.math.min
internal object RangeTokenReader {
enum class TokenType {
STRING,
/* 左括号 */
LEFT,
/* 右括号 */
RIGHT,
/* || */
OR,
/* && */
AND,
GROUP
}
sealed class Token {
abstract val type: TokenType
abstract val value: String
abstract val position: Int
class LeftBracket(override val position: Int) : Token() {
override val type: TokenType get() = TokenType.LEFT
override val value: String get() = "{"
override fun toString(): String = "LB{"
}
class RightBracket(override val position: Int) : Token() {
override val type: TokenType get() = TokenType.RIGHT
override val value: String get() = "}"
override fun toString(): String = "RB}"
}
class Or(override val position: Int) : Token() {
override val type: TokenType get() = TokenType.OR
override val value: String get() = "||"
override fun toString(): String = "OR||"
}
class And(override val position: Int) : Token() {
override val type: TokenType get() = TokenType.AND
override val value: String get() = "&&"
override fun toString(): String = "AD&&"
}
class Group(val values: List<Token>, override val position: Int) : Token() {
override val type: TokenType get() = TokenType.GROUP
override val value: String get() = ""
}
class Raw(val source: String, val start: Int, val end: Int) : Token() {
override val value: String get() = source.substring(start, end)
override val position: Int
get() = start
override val type: TokenType get() = TokenType.STRING
override fun toString(): String = "R:$value"
}
}
fun parseToTokens(source: String): List<Token> = ArrayList<Token>(
max(source.length / 3, 16)
).apply {
var index = 0
var position = 0
fun flushOld() {
if (position > index) {
val id = index
index = position
for (i in id until position) {
if (!source[i].isWhitespace()) {
add(Token.Raw(source, id, position))
return
}
}
}
}
val iterator = source.indices.iterator()
for (i in iterator) {
position = i
when (source[i]) {
'{' -> {
flushOld()
add(Token.LeftBracket(i))
index = i + 1
}
'|' -> {
if (source.getOrNull(i + 1) == '|') {
flushOld()
add(Token.Or(i))
index = i + 2
iterator.nextInt()
}
}
'&' -> {
if (source.getOrNull(i + 1) == '&') {
flushOld()
add(Token.And(i))
index = i + 2
iterator.nextInt()
}
}
'}' -> {
flushOld()
add(Token.RightBracket(i))
index = i + 1
}
}
}
position = source.length
flushOld()
}
fun collect(source: String, tokens: Iterator<Token>, root: Boolean): List<Token> = ArrayList<Token>().apply {
tokens.forEach { token ->
if (token is Token.LeftBracket) {
add(Token.Group(collect(source, tokens, false), token.position))
} else if (token is Token.RightBracket) {
if (root) {
throw IllegalArgumentException("Syntax error: Unexpected }, ${buildMsg(source, token.position)}")
} else {
return@apply
}
} else add(token)
}
if (!root) {
throw IllegalArgumentException("Syntax error: Excepted }, ${buildMsg(source, source.length)}")
}
}
private fun buildMsg(source: String, position: Int): String {
val ed = min(position + 10, source.length)
val st = max(0, position - 10)
return buildString {
append('`')
if (st != 0) append("...")
append(source, st, ed)
if (ed != source.length) append("...")
append("` at ").append(position)
}
}
fun check(source: String, tokens: Iterator<Token>, group: Token.Group?) {
if (!tokens.hasNext()) {
throw IllegalArgumentException("Syntax error: empty rule, ${buildMsg(source, group?.position ?: 0)}")
}
var type = false
do {
val next = tokens.next()
if (type) {
if (next is Token.Group || next is Token.Raw) {
throw IllegalArgumentException("Syntax error: Except logic but got expression, ${buildMsg(source, next.position)}")
}
} else {
if (next is Token.Or || next is Token.And) {
throw IllegalArgumentException("Syntax error: Except expression but got logic, ${buildMsg(source, next.position)}")
}
if (next is Token.Group) {
check(source, next.values.iterator(), next)
}
}
type = !type
} while (tokens.hasNext())
if (!type) {
throw IllegalArgumentException("Syntax error: Except more expression, ${buildMsg(source, group?.values?.last()?.position ?: source.length)}")
}
}
fun parse(source: String, token: Token): SemVersion.Requirement {
return when (token) {
is Token.Group -> {
if (token.values.size == 1) {
parse(source, token.values.first())
} else {
val logic = token.values.asSequence().map { it.type }.filter {
it == TokenType.OR || it == TokenType.AND
}.toSet()
if (logic.size == 2) {
throw IllegalArgumentException("Syntax error: || and && cannot use in one group, ${buildMsg(source, token.position)}")
}
val rules = token.values.asSequence().filter {
it is Token.Raw || it is Token.Group
}.map { parse(source, it) }.toList()
when (logic.first()) {
TokenType.OR -> {
return object : SemVersion.Requirement {
override fun test(version: SemVersion): Boolean {
rules.forEach { if (it.test(version)) return true }
return false
}
}
}
TokenType.AND -> {
return object : SemVersion.Requirement {
override fun test(version: SemVersion): Boolean {
rules.forEach { if (!it.test(version)) return false }
return true
}
}
}
else -> throw AssertionError()
}
}
}
is Token.Raw -> SemVersionInternal.parseRule(token.value)
else -> throw AssertionError()
}
}
fun StringBuilder.dump(prefix: String, token: Token) {
when (token) {
is Token.LeftBracket -> append("${prefix}LF {\n")
is Token.RightBracket -> append("${prefix}LR }\n")
is Token.Or -> append("${prefix}OR ||\n")
is Token.And -> append("${prefix}AND &&\n")
is Token.Group -> {
append("${prefix}GROUP {\n")
token.values.forEach { dump("$prefix ", it) }
append("${prefix}}\n")
}
is Token.Raw -> append("${prefix}RAW ${token.value}\n")
}
}
}

View File

@ -8,12 +8,9 @@
*
*/
/*
* @author Karlatemp <karlatemp@vip.qq.com> <https://github.com/Karlatemp>
*/
package net.mamoe.mirai.console.internal.util
package net.mamoe.mirai.console.internal.util.semver
import net.mamoe.mirai.console.internal.util.semver.RangeTokenReader.dump
import net.mamoe.mirai.console.util.SemVersion
import kotlin.math.max
import kotlin.math.min
@ -22,10 +19,9 @@ import kotlin.math.min
internal object SemVersionInternal {
private val directVersion = """^[0-9]+(\.[0-9]+)+(|[\-+].+)$""".toRegex()
private val versionSelect = """^[0-9]+(\.[0-9]+)*\.x$""".toRegex()
private val versionRange = """([0-9]+(\.[0-9]+)+(|[\-+].+))\s*\-\s*([0-9]+(\.[0-9]+)+(|[\-+].+))""".toRegex()
private val versionMathRange =
"""\[([0-9]+(\.[0-9]+)+(|[\-+].+))\s*\,\s*([0-9]+(\.[0-9]+)+(|[\-+].+))\]""".toRegex()
private val versionRule = """^((\>\=)|(\<\=)|(\=)|(\>)|(\<))\s*([0-9]+(\.[0-9]+)+(|[\-+].+))$""".toRegex()
"""([\[\(])([0-9]+(\.[0-9]+)+(|[\-+].+))\s*\,\s*([0-9]+(\.[0-9]+)+(|[\-+].+))([\]\)])""".toRegex()
private val versionRule = """^((\>\=)|(\<\=)|(\=)|(\!\=)|(\>)|(\<))\s*([0-9]+(\.[0-9]+)+(|[\-+].+))$""".toRegex()
private val SEM_VERSION_REGEX =
"""^(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$""".toRegex()
@ -69,16 +65,19 @@ internal object SemVersionInternal {
}
}
}
val mainVersion = version.substring(0, mainVersionEnd).parseMainVersion()
return SemVersion(
mainVersion = version.substring(0, mainVersionEnd).parseMainVersion(),
major = mainVersion[0],
minor = mainVersion[1],
patch = mainVersion.getOrNull(2),
identifier = identifier,
metadata = metadata
)
}
@JvmStatic
private fun String.parseRule(): SemVersion.Requirement {
val trimmed = trim()
internal fun parseRule(rule: String): SemVersion.Requirement {
val trimmed = rule.trim()
if (directVersion.matches(trimmed)) {
val parsed = SemVersion.invoke(trimmed)
return object : SemVersion.Requirement {
@ -95,22 +94,40 @@ internal object SemVersionInternal {
override fun test(version: SemVersion): Boolean = regex.matches(version.toString())
}
}
(versionRange.matchEntire(trimmed) ?: versionMathRange.matchEntire(trimmed))?.let { range ->
var start = SemVersion.invoke(range.groupValues[1])
var end = SemVersion.invoke(range.groupValues[4])
versionMathRange.matchEntire(trimmed)?.let { range ->
// 1 mode
// 2 first
// 5 sec
// 8 type
var typeStart = range.groupValues[1][0]
var typeEnd = range.groupValues[8][0]
var start = SemVersion.invoke(range.groupValues[2])
var end = SemVersion.invoke(range.groupValues[5])
if (start > end) {
val c = end
end = start
start = c
val x = typeEnd
typeEnd = typeStart
typeStart = x
}
val a: (SemVersion) -> Boolean = when (typeStart) {
'[', ']' -> ({ start <= it })
'(', ')' -> ({ start < it })
else -> throw AssertionError()
}
val b: (SemVersion) -> Boolean = when (typeEnd) {
'[', ']' -> ({ it <= end })
'(', ')' -> ({ it < end })
else -> throw AssertionError()
}
val compareRange = start..end
return object : SemVersion.Requirement {
override fun test(version: SemVersion): Boolean = version in compareRange
override fun test(version: SemVersion): Boolean = a(version) && b(version)
}
}
versionRule.matchEntire(trimmed)?.let { result ->
val operator = result.groupValues[1]
val version1 = SemVersion.invoke(result.groupValues[7])
val version1 = SemVersion.invoke(result.groupValues[8])
return when (operator) {
">=" -> {
object : SemVersion.Requirement {
@ -137,10 +154,15 @@ internal object SemVersionInternal {
override fun test(version: SemVersion): Boolean = version.compareTo(version1) == 0
}
}
"!=" -> {
object : SemVersion.Requirement {
override fun test(version: SemVersion): Boolean = version.compareTo(version1) != 0
}
}
else -> error("operator=$operator, version=$version1")
}
}
throw IllegalArgumentException("Cannot parse $this")
throw IllegalArgumentException("Cannot parse $rule")
}
private fun SemVersion.Requirement.withRule(rule: String): SemVersion.Requirement {
@ -155,19 +177,16 @@ internal object SemVersionInternal {
if (requirement.isBlank()) {
throw IllegalArgumentException("Invalid requirement: Empty requirement rule.")
}
return requirement.split("||").map {
it.parseRule().withRule(it)
}.let { checks ->
if (checks.size == 1) return checks[0]
object : SemVersion.Requirement {
override fun test(version: SemVersion): Boolean {
checks.forEach { rule ->
if (rule.test(version)) return true
}
return false
}
}.withRule(requirement)
}
val tokens = RangeTokenReader.parseToTokens(requirement)
val collected = RangeTokenReader.collect(requirement, tokens.iterator(), true)
RangeTokenReader.check(requirement, collected.iterator(), null)
return kotlin.runCatching {
RangeTokenReader.parse(requirement, RangeTokenReader.Token.Group(collected, 0)).withRule(requirement)
}.onFailure { error ->
throw IllegalArgumentException("Exception in parsing $requirement\n\n" + buildString {
collected.forEach { dump("", it) }
}, error)
}.getOrThrow()
}
@JvmStatic
@ -176,16 +195,12 @@ internal object SemVersionInternal {
// If $this equals $other (without metadata),
// return same.
if (other.mainVersion.contentEquals(source.mainVersion) && source.identifier == other.identifier) {
return 0
}
fun IntArray.getSafe(index: Int) = getOrElse(index) { 0 }
// Compare main-version
for (index in 0 until (max(source.mainVersion.size, other.mainVersion.size))) {
val result = source.mainVersion.getSafe(index).compareTo(other.mainVersion.getSafe(index))
if (result != 0) return result
}
source.major.compareTo(other.major).takeUnless { it == 0 }?.let { return it }
source.minor.compareTo(other.minor).takeUnless { it == 0 }?.let { return it }
(source.patch ?: 0).compareTo(other.patch ?: 0).takeUnless { it == 0 }?.let { return it }
// If main-versions are same.
var identifier0 = source.identifier
var identifier1 = other.identifier

View File

@ -23,7 +23,7 @@ import kotlinx.serialization.builtins.serializer
import net.mamoe.mirai.console.compiler.common.ResolveContext
import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.PLUGIN_VERSION
import net.mamoe.mirai.console.internal.data.map
import net.mamoe.mirai.console.internal.util.SemVersionInternal
import net.mamoe.mirai.console.internal.util.semver.SemVersionInternal
import net.mamoe.mirai.console.util.SemVersion.Companion.equals
import net.mamoe.mirai.console.util.SemVersion.Requirement
@ -32,10 +32,13 @@ import net.mamoe.mirai.console.util.SemVersion.Requirement
*
* 解析示例:
*
* `1.0.0-M4+c25733b8` 将会解析出三个内容, mainVersion (核心版本号), [identifier] (先行版本号) [metadata] (元数据).
* `1.0.0-M4+c25733b8` 将会解析出下面的内容,
* [major] (主本号), [minor] (次版本号), [patch] (修订号), [identifier] (先行版本号) [metadata] (元数据).
* ```
* SemVersion(
* mainVersion = IntArray [1, 0, 0],
* major = 1,
* minor = 0,
* patch = 0,
* identifier = "M4"
* metadata = "c25733b8"
* )
@ -53,8 +56,12 @@ public data class SemVersion
* @see SemVersion.invoke 字符串解析
*/
internal constructor(
/** 核心版本号, 由主版本号, 次版本号和修订号组成, 其中修订号不一定存在 */
public val mainVersion: IntArray,
/** 主版本号 */
public val major: Int,
/** 次版本号 */
public val minor: Int,
/** 修订号 */
public val patch: Int?,
/** 先行版本号识别符 */
public val identifier: String? = null,
/** 版本号元数据, 不参与版本号对比([compareTo]), 但是参与版本号严格对比([equals]) */
@ -106,21 +113,28 @@ internal constructor(
*
* - `1.0.0-M4` 要求 1.0.0-M4 版本, 且只能是 1.0.0-M4 版本
* - `1.x` 要求 1.x 版本
* - `1.0.0 - 1.2.0` 要求 1.0.0 1.2.0 的任意版本, 注意 `-` 两边必须要有空格
* - `[1.0.0, 1.2.0]` `1.0.0 - 1.2.0` 一致
* - `> 1.0.0-RC` 要求 1.0.0-RC 之后的版本, 不能是 1.0.0-RC
* - `>= 1.0.0-RC` 要求 1.0.0-RC 或之后的版本, 可以是 1.0.0-RC
* - `< 1.0.0-RC` 要求 1.0.0-RC 之前的版本, 不能是 1.0.0-RC
* - `<= 1.0.0-RC` 要求 1.0.0-RC 或之前的版本, 可以是 1.0.0-RC
* - `!= 1.0.0-RC` 要求 除了1.0.0-RC 的任何版本
* - `[1.0.0, 1.2.0]`
* - `(1.0.0, 1.2.0]`
* - `[1.0.0, 1.2.0)`
* - `(1.0.0, 1.2.0)` [数学区间](https://baike.baidu.com/item/%E5%8C%BA%E9%97%B4/1273117)
*
* 对于多个规则, 也允许使用 `||` 拼接.
* 对于多个规则, 允许使用逻辑符号 `{}`, `||`, `&&`
* 例如:
* - `1.x || 2.x || 3.0.0`
* - `<= 0.5.3 || >= 1.0.0`
* - `{> 1.0 && < 1.5} || {> 1.8}`
* - `{> 1.0 && < 1.5} || {> 1.8}`
* - `> 1.0.0 && != 1.2.0`
*
* 特别注意:
* - 依赖规则版本号不需要携带版本号元数据, 元数据不参与依赖需求的检查
* - 如果目标版本号携带有先行版本号, 请不要忘记先行版本号
* - 因为 `()` 已经用于数学区间, 使用 `{}` 替代 `()`
*/
@Throws(IllegalArgumentException::class)
@JvmStatic
@ -151,14 +165,14 @@ internal constructor(
@Transient
private val toString: String by lazy(LazyThreadSafetyMode.NONE) {
buildString {
mainVersion.joinTo(this, ".")
append(major)
append('.').append(minor)
patch?.let { append('.').append(it) }
identifier?.let { identifier ->
append('-')
append(identifier)
append('-').append(identifier)
}
metadata?.let { metadata ->
append('+')
append(metadata)
append('+').append(metadata)
}
}
}
@ -169,7 +183,7 @@ internal constructor(
* [SemVersion] 转为 Kotlin data class 风格的 [String]
*/
public fun toStructuredString(): String {
return "SemVersion(mainVersion=${mainVersion.contentToString()}, identifier=$identifier, metadata=$metadata)"
return "SemVersion(major=$major, minor=$minor, patch=$patch, identifier=$identifier, metadata=$metadata)"
}
override fun equals(other: Any?): Boolean {
@ -182,7 +196,8 @@ internal constructor(
}
override fun hashCode(): Int {
var result = mainVersion.contentHashCode()
var result = major shl minor
result *= (patch ?: 1)
result = 31 * result + (identifier?.hashCode() ?: 0)
result = 31 * result + (metadata?.hashCode() ?: 0)
return result

View File

@ -26,7 +26,7 @@ public object SemVersionRangeRequirementBuilder {
}
override fun toString(): String {
return "(${this@or}) or ($other)"
return "{${this@or}} || {$other}"
}
}
}
@ -48,7 +48,7 @@ public object SemVersionRangeRequirementBuilder {
}
override fun toString(): String {
return "(${this@and}) or ($other)"
return "{${this@and}} && {$other}"
}
}
}
@ -64,7 +64,12 @@ public object SemVersionRangeRequirementBuilder {
@Suppress("NOTHING_TO_INLINE")
@ILoveHim188moeForever
public inline fun custom(rule: SemVersion.Requirement): SemVersion.Requirement = rule
public fun custom(rule: (SemVersion) -> Boolean): SemVersion.Requirement = object : SemVersion.Requirement {
override fun test(version: SemVersion): Boolean = rule(version)
override fun toString(): String {
return "Custom{$rule}"
}
}
/**
* 标注一个 [SemVersionRangeRequirementBuilder] DSL

View File

@ -48,6 +48,12 @@ internal class TestSemVersion {
return this
}
fun assertInvalid(requirement: String) {
kotlin.runCatching {
SemVersion.parseRangeRequirement(requirement)
}.onSuccess { assert(false) { requirement } }
}
fun SemVersion.Requirement.assertFalse(version: String): SemVersion.Requirement {
assert(!test(version)) { version }
return this
@ -59,19 +65,24 @@ internal class TestSemVersion {
.assert("1.0").assert("1.1")
.assert("1.5").assert("1.14514")
.assertFalse("2.33")
SemVersion.parseRangeRequirement("2.0||1.2.x")
SemVersion.parseRangeRequirement("{2.0||1.2.x} && 1.1.0 &&1.2.3")
SemVersion.parseRangeRequirement("2.0 || 1.2.x")
.assert("2.0").assert("2.0.0")
.assertFalse("2.1")
.assert("1.2.5").assert("1.2.0").assertFalse("1.2")
.assertFalse("1.0.0")
SemVersion.parseRangeRequirement("1.0.0 - 114.514.1919")
.assert("1.0.0")
.assert("114.514").assert("114.514.1919")
.assertFalse("0.0.1")
.assertFalse("4444.4444")
SemVersion.parseRangeRequirement("[1.0.0, 19190.0]")
.assert("1.0.0").assertFalse("0.1.0")
.assert("19190.0").assertFalse("19198.10")
SemVersion.parseRangeRequirement("[1.0.0, 2.0.0)")
.assert("1.0.0").assert("1.2.3").assertFalse("2.0.0")
SemVersion.parseRangeRequirement("(2.0.0, 1.0.0]")
.assert("1.0.0").assert("1.2.3").assertFalse("2.0.0")
SemVersion.parseRangeRequirement("(2.0.0, 1.0.0)")
.assertFalse("1.0.0").assert("1.2.3").assertFalse("2.0.0")
SemVersion.parseRangeRequirement("(1.0.0, 2.0.0)")
.assertFalse("1.0.0").assert("1.2.3").assertFalse("2.0.0")
SemVersion.parseRangeRequirement(" >= 1.0.0")
.assert("1.0.0")
.assert("114.514.1919")
@ -79,9 +90,30 @@ internal class TestSemVersion {
.assertFalse("0.98774587")
SemVersion.parseRangeRequirement("> 1.0.0")
.assertFalse("1.0.0")
kotlin.runCatching { SemVersion.parseRangeRequirement("WPOXAXW") }
.onSuccess { assert(false) }
SemVersion.parseRangeRequirement("!= 1.0.0 && != 2.0.0")
.assert("1.2.3").assert("2.1.1")
.assertFalse("1.0").assertFalse("1.0.0")
.assertFalse("2.0").assertFalse("2.0.0")
.assert("2.0.1").assert("1.0.1")
SemVersion.parseRangeRequirement("> 1.0.0 || < 0.9.0")
.assertFalse("1.0.0")
.assert("0.8.0")
.assertFalse("0.9.0")
SemVersion.parseRangeRequirement("{>= 1.0.0 && <= 1.2.3} || {>= 2.0.0 && <= 2.2.3}")
.assertFalse("1.3.0")
.assert("1.0.0").assert("1.2.3")
.assertFalse("0.9.0")
.assert("2.0.0").assert("2.2.3").assertFalse("2.3.4")
assertInvalid("WPOXAXW")
assertInvalid("1.0.0 || 1.0.0 && 1.0.0")
assertInvalid("{")
assertInvalid("}")
assertInvalid("")
assertInvalid("1.2.3 - 3.2.1")
assertInvalid("1.5.78 &&")
assertInvalid("|| 1.0.0")
}
private fun String.check() {
@ -112,8 +144,9 @@ internal class TestSemVersion {
"5.1+68-7".check()
"5.1+68-".check()
}
@Test
internal fun testSemVersionOfficial(){
internal fun testSemVersionOfficial() {
"""
1.0-RC
0.0.4