Skip to content

Commit 069f62f

Browse files
authored
Make RPM versions comparable (#73)
1 parent 89813dc commit 069f62f

File tree

4 files changed

+362
-1
lines changed

4 files changed

+362
-1
lines changed

rpm/src/main/java/org/eclipse/packager/rpm/RpmVersion.java

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import java.util.Objects;
1717
import java.util.Optional;
1818

19-
public class RpmVersion {
19+
public class RpmVersion implements Comparable<RpmVersion> {
2020
private final Optional<Integer> epoch;
2121

2222
private final String version;
@@ -93,4 +93,132 @@ public static RpmVersion valueOf(final String version) {
9393

9494
return new RpmVersion(epoch, ver, rel);
9595
}
96+
97+
public static int compare(final String a, final String b) {
98+
if (a.equals(b)) {
99+
return 0;
100+
}
101+
102+
final RpmVersionScanner scanner1 = new RpmVersionScanner(a);
103+
final RpmVersionScanner scanner2 = new RpmVersionScanner(b);
104+
105+
while (scanner1.hasNext() || scanner2.hasNext()) {
106+
if (scanner1.hasNextTilde() || scanner2.hasNextTilde()) {
107+
if (!scanner1.hasNextTilde()) {
108+
return 1;
109+
}
110+
111+
if (!scanner2.hasNextTilde()) {
112+
return -1;
113+
}
114+
115+
scanner1.next();
116+
scanner2.next();
117+
continue;
118+
}
119+
120+
if (scanner1.hasNextCarat() || scanner2.hasNextCarat()) {
121+
if (!scanner1.hasNext()) {
122+
return -1;
123+
}
124+
125+
if (!scanner2.hasNext()) {
126+
return 1;
127+
}
128+
129+
if (!scanner1.hasNextCarat()) {
130+
return 1;
131+
}
132+
133+
if (!scanner2.hasNextCarat()) {
134+
return -1;
135+
}
136+
137+
scanner1.next();
138+
scanner2.next();
139+
continue;
140+
}
141+
142+
if (scanner1.hasNextAlpha() && scanner2.hasNextAlpha()) {
143+
final CharSequence one = scanner1.next();
144+
final CharSequence two = scanner2.next();
145+
final int i = CharSequence.compare(one, two);
146+
147+
if (i != 0) {
148+
return i < 0 ? -1 : 1;
149+
}
150+
} else {
151+
final boolean digit1 = scanner1.hasNextDigit();
152+
final boolean digit2 = scanner2.hasNextDigit();
153+
154+
if (digit1 && digit2) {
155+
final CharSequence one = scanner1.next();
156+
final CharSequence two = scanner2.next();
157+
final int oneLength = one.length();
158+
final int twoLength = two.length();
159+
160+
if (oneLength > twoLength) {
161+
return 1;
162+
}
163+
164+
if (twoLength > oneLength) {
165+
return -1;
166+
}
167+
168+
final int i = CharSequence.compare(one, two);
169+
170+
if (i != 0) {
171+
return i < 0 ? -1 : 1;
172+
}
173+
} else if (digit1) {
174+
return 1;
175+
} else if (digit2) {
176+
return -1;
177+
} else if (scanner1.hasNext()) {
178+
return 1;
179+
} else {
180+
return -1;
181+
}
182+
}
183+
}
184+
185+
return 0;
186+
}
187+
188+
@Override
189+
public boolean equals(final Object o) {
190+
if (o == null || getClass() != o.getClass()) {
191+
return false;
192+
}
193+
194+
final RpmVersion that = (RpmVersion) o;
195+
return Objects.equals(this.epoch, that.epoch) && Objects.equals(this.version, that.version) && Objects.equals(this.release, that.release);
196+
}
197+
198+
@Override
199+
public int hashCode() {
200+
return Objects.hash(this.epoch, this.version, this.release);
201+
}
202+
203+
@Override
204+
public int compareTo(final RpmVersion that) {
205+
// RPM currently treats no epoch as 0
206+
final int i = Integer.compare(epoch.orElse(0), that.epoch.orElse(0));
207+
208+
if (i != 0) {
209+
return i;
210+
}
211+
212+
final int j = compare(this.version, that.version);
213+
214+
if (j != 0) {
215+
return j;
216+
}
217+
218+
if (this.release.isPresent() || that.release.isPresent()) {
219+
return this.release.map(rel -> that.release.map(otherRel -> compare(rel, otherRel)).orElse(1)).orElse(-1);
220+
}
221+
222+
return 0;
223+
}
96224
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
* Copyright (c) 2015, 2019 Contributors to the Eclipse Foundation
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
14+
package org.eclipse.packager.rpm;
15+
16+
import java.nio.CharBuffer;
17+
import java.util.BitSet;
18+
import java.util.Iterator;
19+
import java.util.NoSuchElementException;
20+
import java.util.Objects;
21+
22+
final class RpmVersionScanner implements Iterator<CharSequence> {
23+
private static final char TILDE_CHAR = '~';
24+
25+
private static final String TILDE_STRING = "~";
26+
27+
private static final char CARAT_CHAR = '^';
28+
29+
private static final String CARAT_STRING = "^";
30+
31+
private static final BitSet ALPHA = new BitSet(128);
32+
33+
static {
34+
ALPHA.set('A', 'Z');
35+
ALPHA.set('a', 'z');
36+
}
37+
38+
private static final BitSet DIGIT = new BitSet(128);
39+
40+
static {
41+
DIGIT.set('0', '9');
42+
}
43+
44+
private static final BitSet SIGNIFICANT = new BitSet(128);
45+
46+
static {
47+
SIGNIFICANT.or(ALPHA);
48+
SIGNIFICANT.or(DIGIT);
49+
SIGNIFICANT.set(TILDE_CHAR);
50+
SIGNIFICANT.set(CARAT_CHAR);
51+
}
52+
53+
private final CharBuffer buf;
54+
55+
private int position;
56+
57+
public RpmVersionScanner(final CharSequence input) {
58+
this.buf = CharBuffer.wrap(Objects.requireNonNull(input));
59+
}
60+
61+
@Override
62+
public boolean hasNext() {
63+
skipInsignificantChars();
64+
return (position < buf.length());
65+
}
66+
67+
public boolean hasNextAlpha() {
68+
return hasNext(ALPHA);
69+
}
70+
71+
public boolean hasNextDigit() {
72+
return hasNext(DIGIT);
73+
}
74+
75+
public boolean hasNextTilde() {
76+
return hasNext(TILDE_CHAR);
77+
}
78+
79+
public boolean hasNextCarat() {
80+
return hasNext(CARAT_CHAR);
81+
}
82+
83+
@Override
84+
public CharSequence next() {
85+
if (position >= buf.length()) {
86+
throw new NoSuchElementException();
87+
}
88+
89+
skipInsignificantChars();
90+
91+
final char c = buf.charAt(position);
92+
93+
if (c == TILDE_CHAR) {
94+
position++;
95+
return TILDE_STRING;
96+
}
97+
98+
if (c == CARAT_CHAR) {
99+
position++;
100+
return CARAT_STRING;
101+
}
102+
103+
return DIGIT.get(c) ? nextDigit() : nextAlpha();
104+
}
105+
106+
private CharSequence nextAlpha() {
107+
return next(ALPHA);
108+
}
109+
110+
private CharSequence nextDigit() {
111+
return next(DIGIT);
112+
}
113+
114+
private void skipInsignificantChars() {
115+
while (position < buf.length() && !SIGNIFICANT.get(buf.charAt(position))) {
116+
position++;
117+
}
118+
}
119+
120+
private boolean hasNext(BitSet bitSet) {
121+
return (hasNext() && bitSet.get(buf.charAt(position)));
122+
}
123+
124+
private boolean hasNext(char c) {
125+
return (hasNext() && buf.charAt(position) == c);
126+
}
127+
128+
private int skipLeadingZeros() {
129+
int start = position;
130+
131+
while (start + 1 < buf.length() && buf.charAt(start) == '0' && DIGIT.get(buf.charAt(start + 1))) {
132+
start++;
133+
}
134+
135+
return start;
136+
}
137+
138+
private CharBuffer next(BitSet bitSet) {
139+
skipInsignificantChars();
140+
141+
final int start = skipLeadingZeros();
142+
143+
while (position < buf.length() && bitSet.get(buf.charAt(position))) {
144+
position++;
145+
}
146+
147+
return buf.subSequence(start, position);
148+
}
149+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright (c) 2015, 2019 Contributors to the Eclipse Foundation
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
14+
package org.eclipse.packager.rpm;
15+
16+
import org.junit.jupiter.api.Test;
17+
18+
import java.util.List;
19+
import java.util.NoSuchElementException;
20+
import java.util.Spliterator;
21+
import java.util.Spliterators;
22+
import java.util.stream.Collectors;
23+
import java.util.stream.StreamSupport;
24+
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
27+
28+
class RpmVersionScannerTest {
29+
@Test
30+
void testNext() {
31+
final RpmVersionScanner scanner = new RpmVersionScanner("1.0a^pre0");
32+
assertThat(scanner.hasNextDigit()).isTrue();
33+
assertThat(scanner.next()).asString().isEqualTo("1");
34+
assertThat(scanner.hasNextDigit()).isTrue();
35+
assertThat(scanner.next()).asString().isEqualTo("0");
36+
assertThat(scanner.hasNextAlpha()).isTrue();
37+
assertThat(scanner.next()).asString().isEqualTo("a");
38+
assertThat(scanner.hasNextCarat()).isTrue();
39+
assertThat(scanner.next()).asString().isEqualTo("^");
40+
assertThat(scanner.hasNextAlpha()).isTrue();
41+
assertThat(scanner.hasNextDigit()).isFalse();
42+
assertThat(scanner.hasNextCarat()).isFalse();
43+
assertThat(scanner.next()).asString().isEqualTo("pre");
44+
assertThat(scanner.next()).asString().isEqualTo("0");
45+
assertThat(scanner.hasNext()).isFalse();
46+
assertThat(scanner.hasNextAlpha()).isFalse();
47+
assertThat(scanner.hasNextDigit()).isFalse();
48+
assertThat(scanner.hasNextCarat()).isFalse();
49+
assertThat(scanner.hasNextTilde()).isFalse();
50+
assertThatThrownBy(scanner::next).isExactlyInstanceOf(NoSuchElementException.class).hasMessage(null);
51+
}
52+
53+
@Test
54+
void testTokenize() {
55+
final RpmVersionScanner scanner = new RpmVersionScanner("2.0.01~Final");
56+
final Spliterator<CharSequence> spliterator = Spliterators.spliteratorUnknownSize(scanner, Spliterator.ORDERED);
57+
final List<String> tokens = StreamSupport.stream(spliterator, false).map(CharSequence::toString).collect(Collectors.toList());
58+
assertThat(tokens).containsExactly("2" , "0", "1", "~", "Final");
59+
}
60+
}

rpm/src/test/java/org/eclipse/packager/rpm/VersionTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
package org.eclipse.packager.rpm;
1515

1616
import static org.assertj.core.api.Assertions.assertThat;
17+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
1718

19+
import org.junit.jupiter.api.Test;
1820
import org.junit.jupiter.params.ParameterizedTest;
1921
import org.junit.jupiter.params.provider.CsvSource;
2022

@@ -29,4 +31,26 @@ void testVersion(final String version, final Integer expectedEpoch, final String
2931
assertThat(v.getVersion()).isEqualTo(expectedVersion);
3032
assertThat(v.getRelease()).isEqualTo(Optional.ofNullable(expectedRelease));
3133
}
34+
35+
@ParameterizedTest
36+
@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"})
37+
void testCompare(final String version1, final String version2, final int expected) {
38+
final RpmVersion v1 = RpmVersion.valueOf(version1);
39+
assertThat(v1).hasToString(version1);
40+
final RpmVersion v2 = RpmVersion.valueOf(version2);
41+
assertThat(v2).hasToString(version2);
42+
assertThat(v1.compareTo(v2)).isEqualTo(expected);
43+
44+
if (v1.equals(v2)) {
45+
assertThat(expected).isZero();
46+
assertThat(v1).hasSameHashCodeAs(v2);
47+
}
48+
}
49+
50+
@Test
51+
@SuppressWarnings("ConstantConditions")
52+
void testCompareWithNull() {
53+
final RpmVersion v1 = RpmVersion.valueOf("1.0");
54+
assertThatThrownBy(() -> v1.compareTo(null)).isExactlyInstanceOf(NullPointerException.class);
55+
}
3256
}

0 commit comments

Comments
 (0)