diff --git a/rpm/src/main/java/org/eclipse/packager/rpm/RpmVersion.java b/rpm/src/main/java/org/eclipse/packager/rpm/RpmVersion.java index acea09c..bb8539d 100644 --- a/rpm/src/main/java/org/eclipse/packager/rpm/RpmVersion.java +++ b/rpm/src/main/java/org/eclipse/packager/rpm/RpmVersion.java @@ -16,7 +16,7 @@ import java.util.Objects; import java.util.Optional; -public class RpmVersion { +public class RpmVersion implements Comparable { private final Optional epoch; private final String version; @@ -93,4 +93,132 @@ public static RpmVersion valueOf(final String version) { return new RpmVersion(epoch, ver, rel); } + + public static int compare(final String a, final String b) { + if (a.equals(b)) { + return 0; + } + + final RpmVersionScanner scanner1 = new RpmVersionScanner(a); + final RpmVersionScanner scanner2 = new RpmVersionScanner(b); + + while (scanner1.hasNext() || scanner2.hasNext()) { + if (scanner1.hasNextTilde() || scanner2.hasNextTilde()) { + if (!scanner1.hasNextTilde()) { + return 1; + } + + if (!scanner2.hasNextTilde()) { + return -1; + } + + scanner1.next(); + scanner2.next(); + continue; + } + + if (scanner1.hasNextCarat() || scanner2.hasNextCarat()) { + if (!scanner1.hasNext()) { + return -1; + } + + if (!scanner2.hasNext()) { + return 1; + } + + if (!scanner1.hasNextCarat()) { + return 1; + } + + if (!scanner2.hasNextCarat()) { + return -1; + } + + scanner1.next(); + scanner2.next(); + continue; + } + + if (scanner1.hasNextAlpha() && scanner2.hasNextAlpha()) { + final CharSequence one = scanner1.next(); + final CharSequence two = scanner2.next(); + final int i = CharSequence.compare(one, two); + + if (i != 0) { + return i < 0 ? -1 : 1; + } + } else { + final boolean digit1 = scanner1.hasNextDigit(); + final boolean digit2 = scanner2.hasNextDigit(); + + if (digit1 && digit2) { + final CharSequence one = scanner1.next(); + final CharSequence two = scanner2.next(); + final int oneLength = one.length(); + final int twoLength = two.length(); + + if (oneLength > twoLength) { + return 1; + } + + if (twoLength > oneLength) { + return -1; + } + + final int i = CharSequence.compare(one, two); + + if (i != 0) { + return i < 0 ? -1 : 1; + } + } else if (digit1) { + return 1; + } else if (digit2) { + return -1; + } else if (scanner1.hasNext()) { + return 1; + } else { + return -1; + } + } + } + + return 0; + } + + @Override + public boolean equals(final Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + + final RpmVersion that = (RpmVersion) o; + return Objects.equals(this.epoch, that.epoch) && Objects.equals(this.version, that.version) && Objects.equals(this.release, that.release); + } + + @Override + public int hashCode() { + return Objects.hash(this.epoch, this.version, this.release); + } + + @Override + public int compareTo(final RpmVersion that) { + // RPM currently treats no epoch as 0 + final int i = Integer.compare(epoch.orElse(0), that.epoch.orElse(0)); + + if (i != 0) { + return i; + } + + final int j = compare(this.version, that.version); + + if (j != 0) { + return j; + } + + if (this.release.isPresent() || that.release.isPresent()) { + return this.release.map(rel -> that.release.map(otherRel -> compare(rel, otherRel)).orElse(1)).orElse(-1); + } + + return 0; + } } diff --git a/rpm/src/main/java/org/eclipse/packager/rpm/RpmVersionScanner.java b/rpm/src/main/java/org/eclipse/packager/rpm/RpmVersionScanner.java new file mode 100644 index 0000000..09686bd --- /dev/null +++ b/rpm/src/main/java/org/eclipse/packager/rpm/RpmVersionScanner.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2015, 2019 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.eclipse.packager.rpm; + +import java.nio.CharBuffer; +import java.util.BitSet; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Objects; + +final class RpmVersionScanner implements Iterator { + private static final char TILDE_CHAR = '~'; + + private static final String TILDE_STRING = "~"; + + private static final char CARAT_CHAR = '^'; + + private static final String CARAT_STRING = "^"; + + private static final BitSet ALPHA = new BitSet(128); + + static { + ALPHA.set('A', 'Z'); + ALPHA.set('a', 'z'); + } + + private static final BitSet DIGIT = new BitSet(128); + + static { + DIGIT.set('0', '9'); + } + + private static final BitSet SIGNIFICANT = new BitSet(128); + + static { + SIGNIFICANT.or(ALPHA); + SIGNIFICANT.or(DIGIT); + SIGNIFICANT.set(TILDE_CHAR); + SIGNIFICANT.set(CARAT_CHAR); + } + + private final CharBuffer buf; + + private int position; + + public RpmVersionScanner(final CharSequence input) { + this.buf = CharBuffer.wrap(Objects.requireNonNull(input)); + } + + @Override + public boolean hasNext() { + skipInsignificantChars(); + return (position < buf.length()); + } + + public boolean hasNextAlpha() { + return hasNext(ALPHA); + } + + public boolean hasNextDigit() { + return hasNext(DIGIT); + } + + public boolean hasNextTilde() { + return hasNext(TILDE_CHAR); + } + + public boolean hasNextCarat() { + return hasNext(CARAT_CHAR); + } + + @Override + public CharSequence next() { + if (position >= buf.length()) { + throw new NoSuchElementException(); + } + + skipInsignificantChars(); + + final char c = buf.charAt(position); + + if (c == TILDE_CHAR) { + position++; + return TILDE_STRING; + } + + if (c == CARAT_CHAR) { + position++; + return CARAT_STRING; + } + + return DIGIT.get(c) ? nextDigit() : nextAlpha(); + } + + private CharSequence nextAlpha() { + return next(ALPHA); + } + + private CharSequence nextDigit() { + return next(DIGIT); + } + + private void skipInsignificantChars() { + while (position < buf.length() && !SIGNIFICANT.get(buf.charAt(position))) { + position++; + } + } + + private boolean hasNext(BitSet bitSet) { + return (hasNext() && bitSet.get(buf.charAt(position))); + } + + private boolean hasNext(char c) { + return (hasNext() && buf.charAt(position) == c); + } + + private int skipLeadingZeros() { + int start = position; + + while (start + 1 < buf.length() && buf.charAt(start) == '0' && DIGIT.get(buf.charAt(start + 1))) { + start++; + } + + return start; + } + + private CharBuffer next(BitSet bitSet) { + skipInsignificantChars(); + + final int start = skipLeadingZeros(); + + while (position < buf.length() && bitSet.get(buf.charAt(position))) { + position++; + } + + return buf.subSequence(start, position); + } +} diff --git a/rpm/src/test/java/org/eclipse/packager/rpm/RpmVersionScannerTest.java b/rpm/src/test/java/org/eclipse/packager/rpm/RpmVersionScannerTest.java new file mode 100644 index 0000000..bf38d4f --- /dev/null +++ b/rpm/src/test/java/org/eclipse/packager/rpm/RpmVersionScannerTest.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2015, 2019 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.eclipse.packager.rpm; + +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class RpmVersionScannerTest { + @Test + void testNext() { + final RpmVersionScanner scanner = new RpmVersionScanner("1.0a^pre0"); + assertThat(scanner.hasNextDigit()).isTrue(); + assertThat(scanner.next()).asString().isEqualTo("1"); + assertThat(scanner.hasNextDigit()).isTrue(); + assertThat(scanner.next()).asString().isEqualTo("0"); + assertThat(scanner.hasNextAlpha()).isTrue(); + assertThat(scanner.next()).asString().isEqualTo("a"); + assertThat(scanner.hasNextCarat()).isTrue(); + assertThat(scanner.next()).asString().isEqualTo("^"); + assertThat(scanner.hasNextAlpha()).isTrue(); + assertThat(scanner.hasNextDigit()).isFalse(); + assertThat(scanner.hasNextCarat()).isFalse(); + assertThat(scanner.next()).asString().isEqualTo("pre"); + assertThat(scanner.next()).asString().isEqualTo("0"); + assertThat(scanner.hasNext()).isFalse(); + assertThat(scanner.hasNextAlpha()).isFalse(); + assertThat(scanner.hasNextDigit()).isFalse(); + assertThat(scanner.hasNextCarat()).isFalse(); + assertThat(scanner.hasNextTilde()).isFalse(); + assertThatThrownBy(scanner::next).isExactlyInstanceOf(NoSuchElementException.class).hasMessage(null); + } + + @Test + void testTokenize() { + final RpmVersionScanner scanner = new RpmVersionScanner("2.0.01~Final"); + final Spliterator spliterator = Spliterators.spliteratorUnknownSize(scanner, Spliterator.ORDERED); + final List tokens = StreamSupport.stream(spliterator, false).map(CharSequence::toString).collect(Collectors.toList()); + assertThat(tokens).containsExactly("2" , "0", "1", "~", "Final"); + } +} diff --git a/rpm/src/test/java/org/eclipse/packager/rpm/VersionTest.java b/rpm/src/test/java/org/eclipse/packager/rpm/VersionTest.java index d76af35..468a91e 100644 --- a/rpm/src/test/java/org/eclipse/packager/rpm/VersionTest.java +++ b/rpm/src/test/java/org/eclipse/packager/rpm/VersionTest.java @@ -14,7 +14,9 @@ package org.eclipse.packager.rpm; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -29,4 +31,26 @@ void testVersion(final String version, final Integer expectedEpoch, final String assertThat(v.getVersion()).isEqualTo(expectedVersion); assertThat(v.getRelease()).isEqualTo(Optional.ofNullable(expectedRelease)); } + + @ParameterizedTest + @CsvSource(value = {"1.0,1.0,0", "1.0,2.0,-1", "2.0,1.0,1", "2.0.1,2.0.1,0", "2.0,2.0.1,-1", "2.0.1,2.0,1", "2.0.1a,2.0.1a,0", "2.0.1a,2.0.1,1", "2.0.1,2.0.1a,-1", "5.5p1,5.5p1,0", "5.5p1,5.5p2,-1", "5.5p2,5.5p1,1", "5.5p10,5.5p10,0", "5.5p1,5.5p10,-1", "5.5p10,5.5p1,1", "10xyz,10.1xyz,-1", "10.1xyz,10xyz,1", "xyz10,xyz10,0", "xyz10,xyz10.1,-1", "xyz10.1,xyz10,1", "xyz.4,xyz.4,0", "xyz.4,8,-1", "8,xyz.4,1", "xyz.4,2,-1", "2,xyz.4,1", "5.5p2,5.6p1,-1", "5.6p1,5.5p2,1", "5.6p1,6.5p1,-1", "6.5p1,5.6p1,1", "6.0.rc1,6.0,1", "6.0,6.0.rc1,-1", "10b2,10a1,1", "10a2,10b2,-1", "1.0aa,1.0aa,0", "1.0a,1.0aa,-1", "1.0aa,1.0a,1", "10.0001,10.0001,0", "10.0001,10.1,0", "10.1,10.0001,0", "10.0001,10.0039,-1", "10.0039,10.0001,1", "4.999.9,5.0,-1", "5.0,4.999.9,1", "20101121,20101121,0", "20101121,20101122,-1", "20101122,20101121,1", "2_0,2_0,0", "2.0,2_0,0", "2_0,2.0,0", "a,a,0", "a+,a+,0", "a+,a_,0", "a_,a+,0", "+a,+a,0", "+a,_a,0", "_a,+a,0", "+_,+_,0", "_+,+_,0", "_+,_+,0", "+,_,0", "_,+,0", "1.0~rc1,1.0~rc1,0", "1.0~rc1,1.0,-1", "1.0,1.0~rc1,1", "1.0~rc1,1.0~rc2,-1", "1.0~rc2,1.0~rc1,1", "1.0~rc1~git123,1.0~rc1~git123,0", "1.0~rc1~git123,1.0~rc1,-1", "1.0~rc1,1.0~rc1~git123,1", "1.0^,1.0^,0", "1.0^,1.0,1", "1.0,1.0^,-1", "1.0^git1,1.0^git1,0", "1.0^git1,1.0,1", "1.0,1.0^git1,-1", "1.0^git1,1.0^git2,-1", "1.0^git2,1.0^git1,1", "1.0^git1,1.01,-1", "1.01,1.0^git1,1", "1.0^20160101,1.0^20160101,0", "1.0^20160101,1.0.1,-1", "1.0.1,1.0^20160101,1", "1.0^20160101^git1,1.0^20160101^git1,0", "1.0^20160102,1.0^20160101^git1,1", "1.0^20160101^git1,1.0^20160102,-1", "1.0~rc1^git1,1.0~rc1^git1,0", "1.0~rc1^git1,1.0~rc1,1", "1.0~rc1,1.0~rc1^git1,-1", "1.0^git1~pre,1.0^git1~pre,0", "1.0^git1,1.0^git1~pre,1", "1.0^git1~pre,1.0^git1,-1", "1.900,1.8000,-1", "FC5,fc4,-1", "2a,2.0,-1", "1.0,1.fc4,1", "0:1.0,1.0,0", "1:1.0,2:1.0,-1", "1.0-1,1.0-2,-1", "1.0-1,1.0,1", "1.0,1.0-1,-1"}) + void testCompare(final String version1, final String version2, final int expected) { + final RpmVersion v1 = RpmVersion.valueOf(version1); + assertThat(v1).hasToString(version1); + final RpmVersion v2 = RpmVersion.valueOf(version2); + assertThat(v2).hasToString(version2); + assertThat(v1.compareTo(v2)).isEqualTo(expected); + + if (v1.equals(v2)) { + assertThat(expected).isZero(); + assertThat(v1).hasSameHashCodeAs(v2); + } + } + + @Test + @SuppressWarnings("ConstantConditions") + void testCompareWithNull() { + final RpmVersion v1 = RpmVersion.valueOf("1.0"); + assertThatThrownBy(() -> v1.compareTo(null)).isExactlyInstanceOf(NullPointerException.class); + } }