diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/VersionNumber.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/VersionNumber.java
index badf696ec..1afa025e4 100644
--- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/VersionNumber.java
+++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/versioning/VersionNumber.java
@@ -17,8 +17,6 @@
*/
package org.jackhuang.hmcl.util.versioning;
-import org.jackhuang.hmcl.util.StringUtils;
-
import java.math.BigInteger;
import java.util.*;
@@ -29,7 +27,9 @@ import java.util.*;
* Maybe we can migrate to org.jenkins-ci:version-number:1.7?
* @see Specification
*/
-public class VersionNumber implements Comparable {
+public final class VersionNumber implements Comparable {
+
+ public static final Comparator VERSION_COMPARATOR = Comparator.comparing(VersionNumber::asVersion);
public static VersionNumber asVersion(String version) {
Objects.requireNonNull(version);
@@ -41,68 +41,85 @@ public class VersionNumber implements Comparable {
}
public static boolean isIntVersionNumber(String version) {
- if (version.chars().noneMatch(ch -> ch != '.' && (ch < '0' || ch > '9'))
- && !version.contains("..") && StringUtils.isNotBlank(version)) {
- String[] arr = version.split("\\.");
- for (String str : arr)
- if (str.length() > 9)
- // Numbers which are larger than 1e9 cannot be stored as integer.
- return false;
- return true;
- } else {
+ if (version.isEmpty()) {
return false;
}
+
+ int idx = 0;
+ boolean cont = true;
+ do {
+ int dotIndex = version.indexOf('.', idx);
+ if (dotIndex == idx || dotIndex == version.length() - 1) {
+ return false;
+ }
+
+ int endIndex;
+ if (dotIndex < 0) {
+ cont = false;
+ endIndex = version.length();
+ } else {
+ endIndex = dotIndex;
+ }
+
+ if (endIndex - idx > 9)
+ // Numbers which are larger than 10^10 cannot be stored as integer
+ return false;
+
+ for (int i = idx; i < endIndex; i++) {
+ char ch = version.charAt(i);
+ if (ch < '0' || ch > '9')
+ return false;
+ }
+
+ idx = endIndex + 1;
+ } while (cont);
+
+ return true;
}
- private String value;
- private String canonical;
- private ListItem items;
-
private interface Item {
- int INTEGER_ITEM = 0;
- int STRING_ITEM = 1;
- int LIST_ITEM = 2;
+ int LONG_ITEM = 0;
+ int BIGINTEGER_ITEM = 1;
+ int STRING_ITEM = 2;
+ int LIST_ITEM = 3;
int compareTo(Item item);
int getType();
boolean isNull();
+
+ void appendTo(StringBuilder buffer);
}
- /**
- * Represents a numeric item in the version item list.
- */
- private static class IntegerItem
- implements Item {
- private final BigInteger value;
+ private static final class LongItem implements Item {
+ private final long value;
- public static final IntegerItem ZERO = new IntegerItem();
+ public static final LongItem ZERO = new LongItem(0L);
- private IntegerItem() {
- this.value = BigInteger.ZERO;
- }
-
- IntegerItem(String str) {
- this.value = new BigInteger(str);
+ LongItem(long value) {
+ this.value = value;
}
public int getType() {
- return INTEGER_ITEM;
+ return LONG_ITEM;
}
public boolean isNull() {
- return BigInteger.ZERO.equals(value);
+ return value == 0L;
}
public int compareTo(Item item) {
if (item == null) {
- return BigInteger.ZERO.equals(value) ? 0 : 1; // 1.0 == 1, 1.1 > 1
+ return value == 0L ? 0 : 1; // 1.0 == 1, 1.1 > 1
}
switch (item.getType()) {
- case INTEGER_ITEM:
- return value.compareTo(((IntegerItem) item).value);
+ case LONG_ITEM:
+ long itemValue = ((LongItem) item).value;
+ return Long.compare(value, itemValue);
+ case BIGINTEGER_ITEM:
+ return -1;
case STRING_ITEM:
return 1; // 1.1 > 1-sp
@@ -111,10 +128,68 @@ public class VersionNumber implements Comparable {
return 1; // 1.1 > 1-1
default:
- throw new RuntimeException("invalid item: " + item.getClass());
+ throw new AssertionError("invalid item: " + item.getClass());
}
}
+ @Override
+ public void appendTo(StringBuilder buffer) {
+ buffer.append(value);
+ }
+
+ public String toString() {
+ return Long.toString(value);
+ }
+ }
+
+ /**
+ * Represents a numeric item in the version item list.
+ */
+ private static final class BigIntegerItem implements Item {
+ private final BigInteger value;
+
+ BigIntegerItem(String str) {
+ this.value = new BigInteger(str);
+ }
+
+ public int getType() {
+ return BIGINTEGER_ITEM;
+ }
+
+ public boolean isNull() {
+ // Never be 0
+ // return BigInteger.ZERO.equals(value);
+ return false;
+ }
+
+ public int compareTo(Item item) {
+ if (item == null) {
+ // return BigInteger.ZERO.equals(value) ? 0 : 1; // 1.0 == 1, 1.1 > 1
+ return 1;
+ }
+
+ switch (item.getType()) {
+ case LONG_ITEM:
+ return 1;
+ case BIGINTEGER_ITEM:
+ return value.compareTo(((BigIntegerItem) item).value);
+
+ case STRING_ITEM:
+ return 1; // 1.1 > 1-sp
+
+ case LIST_ITEM:
+ return 1; // 1.1 > 1-1
+
+ default:
+ throw new AssertionError("invalid item: " + item.getClass());
+ }
+ }
+
+ @Override
+ public void appendTo(StringBuilder buffer) {
+ buffer.append(value);
+ }
+
public String toString() {
return value.toString();
}
@@ -123,9 +198,8 @@ public class VersionNumber implements Comparable {
/**
* Represents a string in the version item list, usually a qualifier.
*/
- private static class StringItem
- implements Item {
- private String value;
+ private static final class StringItem implements Item {
+ private final String value;
StringItem(String value) {
this.value = value;
@@ -145,7 +219,8 @@ public class VersionNumber implements Comparable {
return 1;
}
switch (item.getType()) {
- case INTEGER_ITEM:
+ case LONG_ITEM:
+ case BIGINTEGER_ITEM:
return -1; // 1.any < 1.1 ?
case STRING_ITEM:
@@ -155,10 +230,15 @@ public class VersionNumber implements Comparable {
return -1; // 1.any < 1-1
default:
- throw new RuntimeException("invalid item: " + item.getClass());
+ throw new AssertionError("invalid item: " + item.getClass());
}
}
+ @Override
+ public void appendTo(StringBuilder buffer) {
+ buffer.append(value);
+ }
+
public String toString() {
return value;
}
@@ -168,14 +248,14 @@ public class VersionNumber implements Comparable {
* Represents a version list item. This class is used both for the global item list and for sub-lists (which start
* with '-(number)' in the version specification).
*/
- private static class ListItem
- extends ArrayList-
- implements Item {
- Character separator;
+ private static final class ListItem extends ArrayList
- implements Item {
+ private final Character separator;
- public ListItem() {}
+ ListItem() {
+ this.separator = null;
+ }
- public ListItem(char separator) {
+ ListItem(char separator) {
this.separator = separator;
}
@@ -184,7 +264,7 @@ public class VersionNumber implements Comparable {
}
public boolean isNull() {
- return (size() == 0);
+ return size() == 0;
}
void normalize() {
@@ -209,7 +289,8 @@ public class VersionNumber implements Comparable {
return first.compareTo(null);
}
switch (item.getType()) {
- case INTEGER_ITEM:
+ case LONG_ITEM:
+ case BIGINTEGER_ITEM:
return -1; // 1-1 < 1.0.x
case STRING_ITEM:
@@ -234,36 +315,46 @@ public class VersionNumber implements Comparable {
return 0;
default:
- throw new RuntimeException("invalid item: " + item.getClass());
+ throw new AssertionError("invalid item: " + item.getClass());
+ }
+ }
+
+ @Override
+ public void appendTo(StringBuilder buffer) {
+ if (separator != null) {
+ buffer.append((char) separator);
+ }
+
+ final int initLength = buffer.length();
+
+ for (Item item : this) {
+ if (buffer.length() > initLength) {
+ if (!(item instanceof ListItem))
+ buffer.append('.');
+ }
+ item.appendTo(buffer);
}
}
public String toString() {
StringBuilder buffer = new StringBuilder();
- for (Item item : this) {
- if (buffer.length() > 0) {
- if (!(item instanceof ListItem))
- buffer.append('.');
- }
- buffer.append(item);
- }
- if (separator != null)
- return separator + buffer.toString();
- else
- return buffer.toString();
+ appendTo(buffer);
+ return buffer.toString();
}
}
- public VersionNumber(String version) {
- parseVersion(version);
- }
+ private static final int MAX_LONGITEM_LENGTH = 18;
- private void parseVersion(String version) {
+ private final String value;
+ public final ListItem items;
+ private final String canonical;
+
+ private VersionNumber(String version) {
this.value = version;
- ListItem list = items = new ListItem();
+ ListItem list = this.items = new ListItem();
- Stack
- stack = new Stack<>();
+ Deque
- stack = new ArrayDeque<>();
stack.push(list);
boolean isDigit = false;
@@ -275,14 +366,14 @@ public class VersionNumber implements Comparable {
if (c == '.') {
if (i == startIndex) {
- list.add(IntegerItem.ZERO);
+ list.add(LongItem.ZERO);
} else {
list.add(parseItem(version.substring(startIndex, i)));
}
startIndex = i + 1;
} else if ("!\"#$%&'()*+,-/:;<=>?@[\\]^_`{|}~".indexOf(c) != -1) {
if (i == startIndex) {
- list.add(IntegerItem.ZERO);
+ list.add(LongItem.ZERO);
} else {
list.add(parseItem(version.substring(startIndex, i)));
}
@@ -290,7 +381,7 @@ public class VersionNumber implements Comparable {
list.add(list = new ListItem(c));
stack.push(list);
- } else if (Character.isDigit(c)) {
+ } else if (c >= '0' && c <= '9') {
if (!isDigit && i > startIndex) {
list.add(parseItem(version.substring(startIndex, i)));
startIndex = i;
@@ -322,11 +413,42 @@ public class VersionNumber implements Comparable {
list.normalize();
}
- canonical = items.toString();
+ this.canonical = items.toString();
+ }
+
+ // For simple version
+ private VersionNumber(String version, ListItem items) {
+ this.value = version;
+ this.items = items;
+ this.canonical = version;
}
private static Item parseItem(String buf) {
- return buf.chars().allMatch(Character::isDigit) ? new IntegerItem(buf) : new StringItem(buf);
+ int numberLength = 0;
+ boolean leadingZero = true;
+ for (int i = 0; i < buf.length(); i++) {
+ char ch = buf.charAt(i);
+ if (ch >= '0' && ch <= '9') {
+ if (ch != '0') {
+ leadingZero = false;
+ }
+
+ if (!leadingZero) {
+ numberLength++;
+ }
+ } else {
+ return new StringItem(buf);
+ }
+ }
+
+ if (numberLength == 0) {
+ return LongItem.ZERO;
+ } else if (numberLength <= MAX_LONGITEM_LENGTH) {
+ // Numbers which are larger than 10^19 cannot be stored as long
+ return new LongItem(Long.parseLong(buf));
+ } else {
+ return new BigIntegerItem(buf);
+ }
}
public int compareTo(String o) {
@@ -364,6 +486,4 @@ public class VersionNumber implements Comparable {
public int hashCode() {
return canonical.hashCode();
}
-
- public static final Comparator VERSION_COMPARATOR = Comparator.comparing(VersionNumber::asVersion);
}
diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/versioning/VersionNumberTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/versioning/VersionNumberTest.java
index 24c1aa649..9ab986c26 100644
--- a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/versioning/VersionNumberTest.java
+++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/versioning/VersionNumberTest.java
@@ -19,105 +19,135 @@ package org.jackhuang.hmcl.util.versioning;
import org.junit.jupiter.api.Test;
-import java.util.Arrays;
-import java.util.Comparator;
-import java.util.List;
+import java.util.*;
+import java.util.function.Supplier;
+import static org.jackhuang.hmcl.util.versioning.VersionNumber.isIntVersionNumber;
+import static org.jackhuang.hmcl.util.versioning.VersionNumber.normalize;
import static org.junit.jupiter.api.Assertions.*;
public class VersionNumberTest {
@Test
public void testCanonical() {
- VersionNumber u, v;
+ assertEquals("3.2", normalize("3.2.0.0"));
+ assertEquals("3.2-5", normalize("3.2.0.0-5"));
+ assertEquals("3.2", normalize("3.2.0.0-0"));
+ assertEquals("3.2", normalize("3.2--------"));
+ assertEquals("3.2", normalize("3.0002"));
+ assertEquals("1.7.2$%%^@&snapshot-3.1.1", normalize("1.7.2$%%^@&snapshot-3.1.1"));
+ assertEquals("1.99999999999999999999", normalize("1.99999999999999999999"));
+ assertEquals("1.99999999999999999999", normalize("1.0099999999999999999999"));
+ assertEquals("1.99999999999999999999", normalize("1.99999999999999999999.0"));
+ assertEquals("1.99999999999999999999", normalize("1.99999999999999999999--------"));
+ }
- v = VersionNumber.asVersion("3.2.0.0");
- assertEquals("3.2", v.getCanonical());
+ @Test
+ public void testIsIntVersion() {
+ assertFalse(isIntVersionNumber(""));
+ assertFalse(isIntVersionNumber(" "));
+ assertFalse(isIntVersionNumber("."));
+ assertFalse(isIntVersionNumber("1."));
+ assertFalse(isIntVersionNumber(".1"));
+ assertFalse(isIntVersionNumber(".1."));
+ assertFalse(isIntVersionNumber("1..8"));
+ assertFalse(isIntVersionNumber("1.8."));
+ assertFalse(isIntVersionNumber(".1.8"));
+ assertFalse(isIntVersionNumber("1.7.10forge1614_FTBInfinity"));
+ assertFalse(isIntVersionNumber("3.2-5"));
+ assertFalse(isIntVersionNumber("1.9999999999"));
- v = VersionNumber.asVersion("3.2.0.0-5");
- assertEquals("3.2-5", v.getCanonical());
+ assertTrue(isIntVersionNumber("0"));
+ assertTrue(isIntVersionNumber("1"));
+ assertTrue(isIntVersionNumber("0.1"));
+ assertTrue(isIntVersionNumber("0.1.0"));
+ assertTrue(isIntVersionNumber("1.8"));
+ assertTrue(isIntVersionNumber("1.12.2"));
+ assertTrue(isIntVersionNumber("1.13.1"));
+ assertTrue(isIntVersionNumber("1.999999999"));
+ assertTrue(isIntVersionNumber("999999999.0"));
+ }
- v = VersionNumber.asVersion("3.2.0.0-0");
- assertEquals("3.2", v.getCanonical());
+ private static void assertLessThan(String s1, String s2) {
+ Supplier messageSupplier = () -> String.format("%s should be less than %s", s1, s2);
- v = VersionNumber.asVersion("3.2--------");
- assertEquals("3.2", v.getCanonical());
+ VersionNumber v1 = VersionNumber.asVersion(s1);
+ VersionNumber v2 = VersionNumber.asVersion(s2);
- v = VersionNumber.asVersion("1.7.2$%%^@&snapshot-3.1.1");
- assertEquals("1.7.2$%%^@&snapshot-3.1.1", v.getCanonical());
+ assertTrue(v1.compareTo(v2) < 0, messageSupplier);
+ assertTrue(v2.compareTo(v1) > 0, messageSupplier);
}
@Test
public void testComparator() {
- VersionNumber u, v;
-
- u = VersionNumber.asVersion("1.7.10forge1614_FTBInfinity");
- v = VersionNumber.asVersion("1.12.2");
- assertTrue(u.compareTo(v) < 0);
-
- u = VersionNumber.asVersion("1.8.0_51");
- v = VersionNumber.asVersion("1.8.0.51");
- assertTrue(u.compareTo(v) < 0);
-
- u = VersionNumber.asVersion("1.8.0_151");
- v = VersionNumber.asVersion("1.8.0_77");
- assertTrue(u.compareTo(v) > 0);
-
- u = VersionNumber.asVersion("1.6.0_22");
- v = VersionNumber.asVersion("1.8.0_11");
- assertTrue(u.compareTo(v) < 0);
-
- u = VersionNumber.asVersion("1.7.0_22");
- v = VersionNumber.asVersion("1.7.99");
- assertTrue(u.compareTo(v) < 0);
-
- u = VersionNumber.asVersion("1.12.2-14.23.5.2760");
- v = VersionNumber.asVersion("1.12.2-14.23.4.2739");
- assertTrue(u.compareTo(v) > 0);
+ assertLessThan("1.7.10forge1614_FTBInfinity", "1.12.2");
+ assertLessThan("1.8.0_51", "1.8.0.51");
+ assertLessThan("1.8.0_77", "1.8.0_151");
+ assertLessThan("1.6.0_22", "1.8.0_11");
+ assertLessThan("1.7.0_22", "1.7.99");
+ assertLessThan("1.12.2-14.23.4.2739", "1.12.2-14.23.5.2760");
+ assertLessThan("1.9", "1.99999999999999999999");
+ assertLessThan("1.99999999999999999999", "1.199999999999999999999");
+ assertLessThan("1.99999999999999999999", "2");
+ assertLessThan("1.99999999999999999999", "2.0");
}
@Test
public void testSorting() {
- List input = Arrays.asList(
- "1.10",
- "1.10.2",
- "1.10.2-All the Mods",
- "1.10.2-AOE",
- "1.10.2-AOE-1.1.5",
- "1.10.2-forge2511-Age_of_Progression",
- "1.10.2-forge2511-AOE-1.1.2",
- "1.10.2-forge2511-ATM-E",
- "1.10.2-forge2511-simple_life_2",
- "1.10.2-forge2511_bxztest",
- "1.10.2-forge2511_Farming_Valley",
- "1.10.2-forge2511中文",
- "1.10.2-FTB_Beyond",
- "1.10.2-LiteLoader1.10.2",
- "1.12.2",
- "1.12.2_Modern_Skyblock-3.4.2",
- "1.13.1",
+ final Comparator comparator = VersionNumber.VERSION_COMPARATOR.thenComparing(String::compareTo);
+ final List input = Collections.unmodifiableList(Arrays.asList(
+ "0",
+ "0.10.0",
"1.6.4",
"1.6.4-Forge9.11.1.1345",
"1.7.10",
- "1.7.10-1614",
- "1.7.10-1614-test",
+ "1.7.10Agrarian_Skies_2",
"1.7.10-F1614-L",
"1.7.10-FL1614_04",
"1.7.10-Forge10.13.4.1614-1.7.10",
"1.7.10-Forge1614",
- "1.7.10-Forge1614.1",
- "1.7.10Agrarian_Skies_2",
- "1.7.10forge1614test",
- "1.7.10forge1614_ATlauncher",
- "1.7.10forge1614_FTBInfinity",
"1.7.10Forge1614_FTBInfinity-2.6.0",
"1.7.10Forge1614_FTBInfinity-3.0.1",
+ "1.7.10-Forge1614.1",
+ "1.7.10forge1614_ATlauncher",
+ "1.7.10forge1614_FTBInfinity",
"1.7.10forge1614_FTBInfinity_server",
+ "1.7.10forge1614test",
+ "1.7.10-1614",
+ "1.7.10-1614-test",
"1.8",
"1.8-forge1577",
"1.8.9",
"1.8.9-forge1902",
- "1.9");
- input.sort(Comparator.comparing(VersionNumber::asVersion));
+ "1.9",
+ "1.10",
+ "1.10.2",
+ "1.10.2-AOE",
+ "1.10.2-AOE-1.1.5",
+ "1.10.2-All the Mods",
+ "1.10.2-FTB_Beyond",
+ "1.10.2-LiteLoader1.10.2",
+ "1.10.2-forge2511-AOE-1.1.2",
+ "1.10.2-forge2511-ATM-E",
+ "1.10.2-forge2511-Age_of_Progression",
+ "1.10.2-forge2511_Farming_Valley",
+ "1.10.2-forge2511_bxztest",
+ "1.10.2-forge2511-simple_life_2",
+ "1.10.2-forge2511中文",
+ "1.12.2",
+ "1.12.2_Modern_Skyblock-3.4.2",
+ "1.13.1",
+ "1.99999999999999999999",
+ "2",
+ "2.0",
+ "2.1"));
+
+ List output = new ArrayList<>(input);
+ output.sort(comparator);
+ assertIterableEquals(input, output);
+
+ Collections.shuffle(output, new Random(0));
+ output.sort(comparator);
+ assertIterableEquals(input, output);
}
}