Skip to content

Commit 26ca653

Browse files
authored
Validate RpmVersion components (#76)
1 parent 9163cdf commit 26ca653

File tree

5 files changed

+267
-37
lines changed

5 files changed

+267
-37
lines changed

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
import java.util.Objects;
1717
import java.util.Optional;
1818

19+
import static org.eclipse.packager.rpm.RpmVersionValidator.validateEVR;
20+
import static org.eclipse.packager.rpm.RpmVersionValidator.validateEpoch;
21+
import static org.eclipse.packager.rpm.RpmVersionValidator.validateVersion;
22+
1923
public class RpmVersion implements Comparable<RpmVersion> {
2024
private final Optional<Integer> epoch;
2125

@@ -32,15 +36,15 @@ public RpmVersion(final String version, final String release) {
3236
}
3337

3438
public RpmVersion(final Integer epoch, final String version, final String release) {
35-
this.epoch = Optional.ofNullable(epoch);
36-
this.version = Objects.requireNonNull(version);
37-
this.release = Optional.ofNullable(release);
39+
this(Optional.ofNullable(epoch), version, Optional.ofNullable(release));
3840
}
3941

4042
public RpmVersion(final Optional<Integer> epoch, final String version, final Optional<String> release) {
4143
this.epoch = Objects.requireNonNull(epoch);
4244
this.version = Objects.requireNonNull(version);
45+
validateVersion(this.version);
4346
this.release = Objects.requireNonNull(release);
47+
this.release.ifPresent(RpmVersionValidator::validateRelease);
4448
}
4549

4650
public Optional<Integer> getEpoch() {
@@ -75,12 +79,16 @@ public static RpmVersion valueOf(final String version) {
7579
return null;
7680
}
7781

82+
validateEVR(version);
83+
7884
final String[] toks1 = version.split(":", 2);
7985

8086
final String n;
8187
Integer epoch = null;
8288
if (toks1.length > 1) {
83-
epoch = Integer.parseInt(toks1[0]);
89+
final String epochStr = toks1[0];
90+
validateEpoch(epochStr);
91+
epoch = Integer.parseInt(epochStr);
8492
n = toks1[1];
8593
} else {
8694
n = toks1[0];

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

Lines changed: 9 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -19,37 +19,17 @@
1919
import java.util.NoSuchElementException;
2020
import java.util.Objects;
2121

22-
final class RpmVersionScanner implements Iterator<CharSequence> {
23-
private static final char TILDE_CHAR = '~';
22+
import static org.eclipse.packager.rpm.RpmVersionValidator.ALPHA;
23+
import static org.eclipse.packager.rpm.RpmVersionValidator.CARAT_CHAR;
24+
import static org.eclipse.packager.rpm.RpmVersionValidator.DIGIT;
25+
import static org.eclipse.packager.rpm.RpmVersionValidator.SIGNIFICANT;
26+
import static org.eclipse.packager.rpm.RpmVersionValidator.TILDE_CHAR;
2427

28+
final class RpmVersionScanner implements Iterator<CharSequence> {
2529
private static final String TILDE_STRING = "~";
2630

27-
private static final char CARAT_CHAR = '^';
28-
2931
private static final String CARAT_STRING = "^";
3032

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-
5333
private final CharBuffer buf;
5434

5535
private int position;
@@ -117,11 +97,11 @@ private void skipInsignificantChars() {
11797
}
11898
}
11999

120-
private boolean hasNext(BitSet bitSet) {
100+
private boolean hasNext(final BitSet bitSet) {
121101
return (hasNext() && bitSet.get(buf.charAt(position)));
122102
}
123103

124-
private boolean hasNext(char c) {
104+
private boolean hasNext(final char c) {
125105
return (hasNext() && buf.charAt(position) == c);
126106
}
127107

@@ -135,7 +115,7 @@ private int skipLeadingZeros() {
135115
return start;
136116
}
137117

138-
private CharBuffer next(BitSet bitSet) {
118+
private CharBuffer next(final BitSet bitSet) {
139119
skipInsignificantChars();
140120

141121
final int start = skipLeadingZeros();
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
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.util.BitSet;
17+
18+
public final class RpmVersionValidator {
19+
private static final String DOT_DOT = "..";
20+
21+
private static final int NBITS = 128;
22+
23+
static final char TILDE_CHAR = '~';
24+
25+
static final char CARAT_CHAR = '^';
26+
27+
static final BitSet ALPHA = new BitSet(NBITS);
28+
29+
static {
30+
ALPHA.set('A', 'Z' + 1);
31+
ALPHA.set('a', 'z' + 1);
32+
}
33+
34+
static final BitSet DIGIT = new BitSet(NBITS);
35+
36+
static {
37+
DIGIT.set('0', '9' + 1);
38+
}
39+
40+
private static final BitSet ALPHANUM = new BitSet(NBITS);
41+
42+
static {
43+
ALPHANUM.or(ALPHA);
44+
ALPHANUM.or(DIGIT);
45+
}
46+
47+
static final BitSet SIGNIFICANT = new BitSet(NBITS);
48+
49+
static {
50+
SIGNIFICANT.or(ALPHANUM);
51+
SIGNIFICANT.set(TILDE_CHAR);
52+
SIGNIFICANT.set(CARAT_CHAR);
53+
}
54+
55+
private static final BitSet NAME = new BitSet(NBITS);
56+
57+
static {
58+
NAME.or(ALPHANUM);
59+
NAME.set('.');
60+
NAME.set('-');
61+
NAME.set('_');
62+
NAME.set('+');
63+
NAME.set('%');
64+
NAME.set('{');
65+
NAME.set('}');
66+
}
67+
68+
private static final BitSet FIRST_CHARS_NAME = new BitSet(NBITS);
69+
70+
static {
71+
FIRST_CHARS_NAME.or(ALPHANUM);
72+
FIRST_CHARS_NAME.set('_');
73+
FIRST_CHARS_NAME.set('%');
74+
}
75+
76+
private static final BitSet VERREL = new BitSet(NBITS);
77+
78+
static {
79+
VERREL.or(SIGNIFICANT);
80+
VERREL.set('.');
81+
VERREL.set('_');
82+
VERREL.set('+');
83+
}
84+
85+
private static final BitSet EVR = new BitSet(NBITS);
86+
87+
static {
88+
EVR.or(VERREL);
89+
EVR.set('-');
90+
EVR.set(':');
91+
}
92+
93+
private RpmVersionValidator() {
94+
95+
}
96+
97+
public static void validateName(final String name) {
98+
validateChars(name, NAME, FIRST_CHARS_NAME);
99+
}
100+
101+
public static void validateEpoch(final String epoch) {
102+
validateChars(epoch, DIGIT);
103+
}
104+
105+
public static void validateVersion(final String version) {
106+
validateChars(version, VERREL);
107+
}
108+
109+
public static void validateRelease(final String release) {
110+
validateVersion(release);
111+
}
112+
113+
public static void validateEVR(final String evr) {
114+
validateChars(evr, EVR);
115+
}
116+
117+
private static void validateChars(final String field, final BitSet allowedChars) {
118+
validateChars(field, allowedChars, null);
119+
}
120+
121+
private static void validateChars(final String field, final BitSet allowedChars, final BitSet allowedFirstChars) {
122+
final int start;
123+
124+
if (allowedFirstChars == null) {
125+
start = 0;
126+
} else {
127+
final char c = field.charAt(0);
128+
129+
if (!allowedFirstChars.get(c)) {
130+
throw new IllegalArgumentException("Illegal char '" + c + "' (0x" + Integer.toHexString(c) + ") in '" + field + "'");
131+
}
132+
133+
start = 1;
134+
}
135+
136+
final int length = field.length();
137+
138+
for (int i = start; i < length; i++) {
139+
final char c = field.charAt(i);
140+
final boolean allowed = allowedChars.get(c);
141+
142+
if (!allowed) {
143+
throw new IllegalArgumentException("Illegal char '" + c + "' (0x" + Integer.toHexString(c) + ") in '" + field + "'");
144+
}
145+
}
146+
147+
if (field.contains(DOT_DOT)) {
148+
throw new IllegalArgumentException("Illegal sequence '..' in '" + field + "'");
149+
}
150+
}
151+
}

rpm/src/main/java/org/eclipse/packager/rpm/build/RpmBuilder.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import static java.util.Comparator.comparing;
1717
import static java.util.Optional.of;
18+
import static org.eclipse.packager.rpm.RpmVersionValidator.validateName;
1819

1920
import java.io.IOException;
2021
import java.io.InputStream;
@@ -653,6 +654,7 @@ private static BuilderOptions makeBuilderOptions(final OpenOption[] openOptions)
653654

654655
public RpmBuilder(final String name, final RpmVersion version, final String architecture, final Path targetFile, final BuilderOptions options) throws IOException {
655656
this.name = name;
657+
validateName(name);
656658
this.version = version;
657659
this.architecture = architecture;
658660

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

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,112 @@
1313

1414
package org.eclipse.packager.rpm;
1515

16-
import static org.assertj.core.api.Assertions.assertThat;
17-
import static org.assertj.core.api.Assertions.assertThatThrownBy;
18-
1916
import org.junit.jupiter.api.Test;
2017
import org.junit.jupiter.params.ParameterizedTest;
2118
import org.junit.jupiter.params.provider.CsvSource;
19+
import org.junit.jupiter.params.provider.ValueSource;
2220

21+
import java.util.List;
22+
import java.util.NoSuchElementException;
2323
import java.util.Optional;
24+
import java.util.Spliterator;
25+
import java.util.Spliterators;
26+
import java.util.stream.Collectors;
27+
import java.util.stream.StreamSupport;
28+
29+
import static org.assertj.core.api.Assertions.assertThat;
30+
import static org.assertj.core.api.Assertions.assertThatCode;
31+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
32+
import static org.eclipse.packager.rpm.RpmVersionValidator.validateName;
2433

2534
class VersionTest {
35+
@Test
36+
void testName() {
37+
assertThatCode(() -> validateName("foo")).doesNotThrowAnyException();
38+
assertThatThrownBy(() -> validateName("~foo")).isExactlyInstanceOf(IllegalArgumentException.class).hasMessage("Illegal char '~' (0x7e) in '~foo'");
39+
assertThatThrownBy(() -> validateName("foo\0")).isExactlyInstanceOf(IllegalArgumentException.class).hasMessage("Illegal char '\0' (0x0) in 'foo\0'");
40+
assertThatThrownBy(() -> validateName("€foo")).isExactlyInstanceOf(IllegalArgumentException.class).hasMessage("Illegal char '€' (0x20ac) in '€foo'");
41+
}
42+
43+
@Test
44+
void testRpmVersion() {
45+
final RpmVersion v = new RpmVersion("1.0");
46+
assertThat(v.getEpoch()).isEmpty();
47+
assertThat(v.getVersion()).isEqualTo("1.0");
48+
assertThat(v.getRelease()).isEmpty();
49+
assertThat(v).hasToString("1.0");
50+
}
51+
52+
@Test
53+
void testRpmVersionWithRelease() {
54+
final RpmVersion v = new RpmVersion("1.0", "1");
55+
assertThat(v.getEpoch()).isEmpty();
56+
assertThat(v.getVersion()).isEqualTo("1.0");
57+
assertThat(v.getRelease()).hasValue("1");
58+
assertThat(v).hasToString("1.0-1");
59+
}
60+
61+
@Test
62+
void testRpmVersionWithEmptyRelease() {
63+
final RpmVersion v = new RpmVersion("1.0", "");
64+
assertThat(v.getEpoch()).isEmpty();
65+
assertThat(v.getVersion()).isEqualTo("1.0");
66+
assertThat(v.getRelease()).hasValue("");
67+
assertThat(v).hasToString("1.0");
68+
}
69+
70+
@Test
71+
void testEquals() {
72+
final RpmVersion v1 = new RpmVersion("1.0");
73+
final RpmVersion v2 = new RpmVersion(0, "1.0", null);
74+
final RpmVersion v3 = new RpmVersion("2.0");
75+
final RpmVersion v4 = new RpmVersion("1.0", "1");
76+
final RpmVersion v5 = new RpmVersion("1.0", "2");
77+
final RpmVersion v6 = new RpmVersion(1, "1.0", "2");
78+
assertThat(v1).isNotEqualTo(null).isNotEqualTo("").isNotEqualTo(v2).isNotEqualTo(v3).isNotEqualTo(v4);
79+
assertThat(v4).isNotEqualTo(v5);
80+
assertThat(v5).isNotEqualTo(v6);
81+
}
82+
83+
@Test
84+
void testRpmVersionNull() {
85+
assertThat(RpmVersion.valueOf(null)).isNull();
86+
assertThat(RpmVersion.valueOf("")).isNull();
87+
}
88+
2689
@ParameterizedTest
27-
@CsvSource(value = {"1.2.3,,1.2.3,", "0:1.2.3,0,1.2.3,", "0:1.2.3-1,0,1.2.3,1", "1.2.3-1,,1.2.3,1", "1.2.3-123-456,,1.2.3,123-456"})
90+
@CsvSource(value = {"1.2.3,,1.2.3,", "0:1.2.3,0,1.2.3,", "0:1.2.3-1,0,1.2.3,1", "1.2.3-1,,1.2.3,1"})
2891
void testVersion(final String version, final Integer expectedEpoch, final String expectedVersion, final String expectedRelease) {
2992
final RpmVersion v = RpmVersion.valueOf(version);
3093
assertThat(v.getEpoch()).isEqualTo(Optional.ofNullable(expectedEpoch));
3194
assertThat(v.getVersion()).isEqualTo(expectedVersion);
3295
assertThat(v.getRelease()).isEqualTo(Optional.ofNullable(expectedRelease));
96+
assertThat(v).hasToString(version);
97+
}
98+
99+
@ParameterizedTest
100+
@ValueSource(strings = {"1-2-3\n", "A:1.2.3", "1.2.3-123-456", "1..2"})
101+
void testInvalidVersion(final String version) {
102+
assertThatThrownBy(() -> RpmVersion.valueOf(version)).isExactlyInstanceOf(IllegalArgumentException.class).hasMessageStartingWith("Illegal ");
103+
}
104+
105+
@Test
106+
void testRpmScanner() {
107+
final RpmVersionScanner scanner = new RpmVersionScanner("1.0");
108+
assertThat(scanner.hasNext()).isTrue();
109+
assertThat(scanner.next()).asString().isEqualTo("1");
110+
assertThat(scanner.hasNext()).isTrue();
111+
assertThat(scanner.next()).asString().isEqualTo("0");
112+
assertThat(scanner.hasNext()).isFalse();
113+
assertThatThrownBy(scanner::next).isExactlyInstanceOf(NoSuchElementException.class);
114+
}
115+
116+
@Test
117+
void testRpmScannerTokens() {
118+
final RpmVersionScanner scanner = new RpmVersionScanner("2.0.1");
119+
final Spliterator<CharSequence> spliterator = Spliterators.spliteratorUnknownSize(scanner, Spliterator.ORDERED);
120+
final List<String> tokens = StreamSupport.stream(spliterator, false).map(CharSequence::toString).collect(Collectors.toList());
121+
assertThat(tokens).containsExactly("2" , "0", "1");
33122
}
34123

35124
@ParameterizedTest

0 commit comments

Comments
 (0)