Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 129 additions & 1 deletion rpm/src/main/java/org/eclipse/packager/rpm/RpmVersion.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import java.util.Objects;
import java.util.Optional;

public class RpmVersion {
public class RpmVersion implements Comparable<RpmVersion> {
private final Optional<Integer> epoch;

private final String version;
Expand Down Expand Up @@ -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;
}
}
149 changes: 149 additions & 0 deletions rpm/src/main/java/org/eclipse/packager/rpm/RpmVersionScanner.java
Original file line number Diff line number Diff line change
@@ -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<CharSequence> {
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);
}
}
Original file line number Diff line number Diff line change
@@ -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<CharSequence> spliterator = Spliterators.spliteratorUnknownSize(scanner, Spliterator.ORDERED);
final List<String> tokens = StreamSupport.stream(spliterator, false).map(CharSequence::toString).collect(Collectors.toList());
assertThat(tokens).containsExactly("2" , "0", "1", "~", "Final");
}
}
24 changes: 24 additions & 0 deletions rpm/src/test/java/org/eclipse/packager/rpm/VersionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
}
}