diff --git a/backend/mirai-console/src/internal/util/semver/RangeTokenReader.kt b/backend/mirai-console/src/internal/util/semver/RangeTokenReader.kt deleted file mode 100644 index 17064132b..000000000 --- a/backend/mirai-console/src/internal/util/semver/RangeTokenReader.kt +++ /dev/null @@ -1,250 +0,0 @@ -/* - * 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 through 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, 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 = ArrayList( - 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, root: Boolean): List = ArrayList().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, 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): RequirementInternal { - 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 : RequirementInternal { - override fun test(version: SemVersion): Boolean { - rules.forEach { if (it.test(version)) return true } - return false - } - } - } - TokenType.AND -> { - return object : RequirementInternal { - 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") - } - } -} \ No newline at end of file diff --git a/backend/mirai-console/src/internal/util/semver/RequirementParser.kt b/backend/mirai-console/src/internal/util/semver/RequirementParser.kt new file mode 100644 index 000000000..011f18954 --- /dev/null +++ b/backend/mirai-console/src/internal/util/semver/RequirementParser.kt @@ -0,0 +1,335 @@ +/* + * 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 through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.console.internal.util.semver + +import kotlin.math.max +import kotlin.math.min + +internal class RequirementParser { + sealed class Token { + open var line: Int = -1 + open var pos: Int = -1 + open var sourcePos: Int = -1 + open lateinit var content: String + + sealed class GroupBod : Token() { + class Left : GroupBod() { + override var content: String + get() = "{" + set(_) {} + } + + class Right : GroupBod() { + override var content: String + get() = "}" + set(_) {} + } + } + + sealed class Logic : Token() { + class And : Logic() { + override var content: String + get() = "&&" + set(_) {} + } + + class Or : Logic() { + override var content: String + get() = "||" + set(_) {} + } + } + + class Content : Token() + class Ending : Token() { + override var content: String + get() = "" + set(_) {} + } + + object Begin : Token() { + override var content: String + get() = "" + set(_) {} + override var line: Int + get() = 0 + set(_) {} + override var pos: Int + get() = 0 + set(_) {} + override var sourcePos: Int + get() = 0 + set(_) {} + } + + override fun toString(): String { + return javaClass.canonicalName.substringAfterLast('.') + " - $content [$line, $pos]" + } + } + + companion object { + const val END = '\u0000' + } + + class TokenReader( + @JvmField val content: String + ) { + @JvmField + var pos: Int = 0 + + @JvmField + var line: Int = 0 + + @JvmField + var posi: Int = 0 + + @JvmField + var latestToken: Token = Token.Begin + + @JvmField + var insertToken: Token? = Token.Begin + fun peekChar(): Char { + if (pos < content.length) + return content[pos] + return END + } + + fun peekNextChar(): Char { + if (pos + 1 < content.length) + return content[pos + 1] + return END + } + + fun nextChar(): Char { + val char = peekChar() + pos++ + if (char == '\n') { + line++ + posi = 0 + } else { + posi++ + } + return char + } + + fun nextToken(): Token { + insertToken?.let { insertToken = null; return it } + return nextToken0().also { latestToken = it } + } + + private fun nextToken0(): Token { + if (pos < content.length) { + while (peekChar().isWhitespace()) { + nextChar() + } + val startIndex = pos + if (startIndex >= content.length) { + return Token.Ending().also { + it.line = line + it.pos = posi + it.sourcePos = content.length + } + } + val pline = line + val ppos = posi + nextChar() + when (content[startIndex]) { + '&' -> { + if (peekChar() == '&') { + return Token.Logic.And().also { + it.pos = ppos + it.line = pline + it.sourcePos = startIndex + nextChar() + } + } + } + '|' -> { + if (peekChar() == '|') { + return Token.Logic.Or().also { + nextChar() + it.pos = ppos + it.line = pline + it.sourcePos = startIndex + } + } + } + '{' -> { + return Token.GroupBod.Left().also { + it.pos = ppos + it.line = pline + it.sourcePos = startIndex + } + } + '}' -> { + return Token.GroupBod.Right().also { + it.pos = ppos + it.line = pline + it.sourcePos = startIndex + } + } + } + while (true) { + when (val c = peekChar()) { + '&', '|' -> { + if (c == peekNextChar()) { + break + } + nextChar() + } + '{', '}' -> { + break + } + END -> break + else -> nextChar() + } + } + val endIndex = pos + return Token.Content().also { + it.content = content.substring(startIndex, endIndex) + it.pos = ppos + it.line = pline + it.sourcePos = startIndex + } + } + return Token.Ending().also { + it.line = line + it.pos = posi + it.sourcePos = content.length + } + } + } + + interface TokensProcessor { + fun process(reader: TokenReader): R + fun processLine(reader: TokenReader): R + fun processLogic(isAnd: Boolean, chunks: Iterable): R + } + + abstract class ProcessorBase : TokensProcessor { + fun Token.ia(reader: TokenReader, msg: String, cause: Throwable? = null): Nothing { + throw IllegalArgumentException("$msg (at [$line, $pos], ${cutSource(reader, sourcePos)})", cause) + } + + fun cutSource(reader: TokenReader, index: Int): String { + val content = reader.content + val s = max(0, index - 10) + val e = min(content.length, index + 10) + return content.substring(s, e) + } + + override fun process(reader: TokenReader): R { + return when (val nextToken = reader.nextToken()) { + is Token.Begin, + is Token.GroupBod.Left -> { + val first = when (val next = reader.nextToken()) { + is Token.Content -> { + processString(reader, next) + } + is Token.GroupBod.Right -> { + nextToken.ia( + reader, if (nextToken is Token.Begin) + "Invalid token `}`" + else "The first token cannot be Group Ending" + ) + } + is Token.Logic -> { + nextToken.ia(reader, "The first token cannot be Token.Logic") + } + is Token.Ending -> { + nextToken.ia( + reader, if (nextToken is Token.Begin) + "Requirement cannot be blank" + else "Except more tokens" + ) + } + is Token.GroupBod.Left -> { + reader.insertToken = next + process(reader) + } + else -> { + next.ia(reader, "Bad token $next") + } + } + // null -> not set + // true -> AND mode + // false-> OR mode + var mode: Boolean? = null + val chunks = arrayListOf(first) + while (true) { + when (val next = reader.nextToken()) { + is Token.Ending, + is Token.GroupBod.Right -> { + val isEndingOfGroup = next is Token.GroupBod.Right + val isStartingOfGroup = nextToken is Token.GroupBod.Left + if (isStartingOfGroup != isEndingOfGroup) { + fun getType(type: Boolean) = if (type) "`}`" else "" + next.ia(reader, "Except ${getType(isStartingOfGroup)} but got ${getType(isEndingOfGroup)}") + } else { + // reader.insertToken = next + break + } + } + is Token.Logic -> { + val stx = next is Token.Logic.And + if (mode == null) mode = stx + else if (mode != stx) { + fun getMode(type: Boolean) = if (type) "`&&`" else "`||`" + next.ia( + reader, "Cannot change logic mode after setting. " + + "Except ${getMode(mode)} but got ${getMode(stx)}" + ) + } + chunks.add(process(reader)) + } + else -> { + next.ia( + reader, "Except ${ + when (mode) { + null -> "`&&` or `||`" + true -> "`&&`" + false -> "`||`" + } + } but get `${next.content}`" + ) + } + } + } + if (mode == null) { + first + } else { + processLogic(mode, chunks) + } + } + is Token.Content -> { + processString(reader, nextToken) + } + is Token.Ending -> { + nextToken.ia(reader, "Except more values.") + } + else -> { + nextToken.ia(reader, "Assert Error: $nextToken") + } + } + } + + abstract fun processString(reader: TokenReader, token: Token.Content): R + + + override fun processLine(reader: TokenReader): R { + return process(reader).also { + val tok = reader.nextToken() + if (reader.nextToken() !is Token.Ending) { + tok.ia(reader, "Token reader stream not done") + } + } + } + } +} diff --git a/backend/mirai-console/src/internal/util/semver/SemVersionInternal.kt b/backend/mirai-console/src/internal/util/semver/SemVersionInternal.kt index 1d9c1af03..bb9f9a947 100644 --- a/backend/mirai-console/src/internal/util/semver/SemVersionInternal.kt +++ b/backend/mirai-console/src/internal/util/semver/SemVersionInternal.kt @@ -9,7 +9,6 @@ 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 @@ -166,21 +165,27 @@ internal object SemVersionInternal { @JvmStatic - fun parseRangeRequirement(requirement: String): RequirementInternal { - if (requirement.isBlank()) { - throw IllegalArgumentException("Invalid requirement: Empty requirement rule.") - } - 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)) - }.onFailure { error -> - throw IllegalArgumentException("Exception in parsing $requirement\n\n" + buildString { - collected.forEach { dump("", it) } - }, error) - }.getOrThrow() - } + fun parseRangeRequirement(requirement: String): RequirementInternal = + object : RequirementParser.ProcessorBase() { + override fun processLogic(isAnd: Boolean, chunks: Iterable): RequirementInternal { + return if (isAnd) object : RequirementInternal { + override fun test(version: SemVersion): Boolean { + return chunks.all { it.test(version) } + } + } else object : RequirementInternal { + override fun test(version: SemVersion): Boolean { + return chunks.any { it.test(version) } + } + } + } + + override fun processString( + reader: RequirementParser.TokenReader, + token: RequirementParser.Token.Content + ): RequirementInternal = kotlin.runCatching { + parseRule(token.content) + }.getOrElse { token.ia(reader, "Error in parsing rule `${token.content}`", it) } + }.processLine(RequirementParser.TokenReader(requirement)) @JvmStatic fun compareInternal(source: SemVersion, other: SemVersion): Int {