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); } }