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 bb8539d..723c4b2 100644 --- a/rpm/src/main/java/org/eclipse/packager/rpm/RpmVersion.java +++ b/rpm/src/main/java/org/eclipse/packager/rpm/RpmVersion.java @@ -16,6 +16,10 @@ import java.util.Objects; import java.util.Optional; +import static org.eclipse.packager.rpm.RpmVersionValidator.validateEVR; +import static org.eclipse.packager.rpm.RpmVersionValidator.validateEpoch; +import static org.eclipse.packager.rpm.RpmVersionValidator.validateVersion; + public class RpmVersion implements Comparable { private final Optional epoch; @@ -32,15 +36,15 @@ public RpmVersion(final String version, final String release) { } public RpmVersion(final Integer epoch, final String version, final String release) { - this.epoch = Optional.ofNullable(epoch); - this.version = Objects.requireNonNull(version); - this.release = Optional.ofNullable(release); + this(Optional.ofNullable(epoch), version, Optional.ofNullable(release)); } public RpmVersion(final Optional epoch, final String version, final Optional release) { this.epoch = Objects.requireNonNull(epoch); this.version = Objects.requireNonNull(version); + validateVersion(this.version); this.release = Objects.requireNonNull(release); + this.release.ifPresent(RpmVersionValidator::validateRelease); } public Optional getEpoch() { @@ -75,12 +79,16 @@ public static RpmVersion valueOf(final String version) { return null; } + validateEVR(version); + final String[] toks1 = version.split(":", 2); final String n; Integer epoch = null; if (toks1.length > 1) { - epoch = Integer.parseInt(toks1[0]); + final String epochStr = toks1[0]; + validateEpoch(epochStr); + epoch = Integer.parseInt(epochStr); n = toks1[1]; } else { n = toks1[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 index 09686bd..b6d128a 100644 --- a/rpm/src/main/java/org/eclipse/packager/rpm/RpmVersionScanner.java +++ b/rpm/src/main/java/org/eclipse/packager/rpm/RpmVersionScanner.java @@ -19,37 +19,17 @@ import java.util.NoSuchElementException; import java.util.Objects; -final class RpmVersionScanner implements Iterator { - private static final char TILDE_CHAR = '~'; +import static org.eclipse.packager.rpm.RpmVersionValidator.ALPHA; +import static org.eclipse.packager.rpm.RpmVersionValidator.CARAT_CHAR; +import static org.eclipse.packager.rpm.RpmVersionValidator.DIGIT; +import static org.eclipse.packager.rpm.RpmVersionValidator.SIGNIFICANT; +import static org.eclipse.packager.rpm.RpmVersionValidator.TILDE_CHAR; +final class RpmVersionScanner implements Iterator { 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; @@ -117,11 +97,11 @@ private void skipInsignificantChars() { } } - private boolean hasNext(BitSet bitSet) { + private boolean hasNext(final BitSet bitSet) { return (hasNext() && bitSet.get(buf.charAt(position))); } - private boolean hasNext(char c) { + private boolean hasNext(final char c) { return (hasNext() && buf.charAt(position) == c); } @@ -135,7 +115,7 @@ private int skipLeadingZeros() { return start; } - private CharBuffer next(BitSet bitSet) { + private CharBuffer next(final BitSet bitSet) { skipInsignificantChars(); final int start = skipLeadingZeros(); diff --git a/rpm/src/main/java/org/eclipse/packager/rpm/RpmVersionValidator.java b/rpm/src/main/java/org/eclipse/packager/rpm/RpmVersionValidator.java new file mode 100644 index 0000000..c643a1e --- /dev/null +++ b/rpm/src/main/java/org/eclipse/packager/rpm/RpmVersionValidator.java @@ -0,0 +1,151 @@ +/* + * 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.util.BitSet; + +public final class RpmVersionValidator { + private static final String DOT_DOT = ".."; + + private static final int NBITS = 128; + + static final char TILDE_CHAR = '~'; + + static final char CARAT_CHAR = '^'; + + static final BitSet ALPHA = new BitSet(NBITS); + + static { + ALPHA.set('A', 'Z' + 1); + ALPHA.set('a', 'z' + 1); + } + + static final BitSet DIGIT = new BitSet(NBITS); + + static { + DIGIT.set('0', '9' + 1); + } + + private static final BitSet ALPHANUM = new BitSet(NBITS); + + static { + ALPHANUM.or(ALPHA); + ALPHANUM.or(DIGIT); + } + + static final BitSet SIGNIFICANT = new BitSet(NBITS); + + static { + SIGNIFICANT.or(ALPHANUM); + SIGNIFICANT.set(TILDE_CHAR); + SIGNIFICANT.set(CARAT_CHAR); + } + + private static final BitSet NAME = new BitSet(NBITS); + + static { + NAME.or(ALPHANUM); + NAME.set('.'); + NAME.set('-'); + NAME.set('_'); + NAME.set('+'); + NAME.set('%'); + NAME.set('{'); + NAME.set('}'); + } + + private static final BitSet FIRST_CHARS_NAME = new BitSet(NBITS); + + static { + FIRST_CHARS_NAME.or(ALPHANUM); + FIRST_CHARS_NAME.set('_'); + FIRST_CHARS_NAME.set('%'); + } + + private static final BitSet VERREL = new BitSet(NBITS); + + static { + VERREL.or(SIGNIFICANT); + VERREL.set('.'); + VERREL.set('_'); + VERREL.set('+'); + } + + private static final BitSet EVR = new BitSet(NBITS); + + static { + EVR.or(VERREL); + EVR.set('-'); + EVR.set(':'); + } + + private RpmVersionValidator() { + + } + + public static void validateName(final String name) { + validateChars(name, NAME, FIRST_CHARS_NAME); + } + + public static void validateEpoch(final String epoch) { + validateChars(epoch, DIGIT); + } + + public static void validateVersion(final String version) { + validateChars(version, VERREL); + } + + public static void validateRelease(final String release) { + validateVersion(release); + } + + public static void validateEVR(final String evr) { + validateChars(evr, EVR); + } + + private static void validateChars(final String field, final BitSet allowedChars) { + validateChars(field, allowedChars, null); + } + + private static void validateChars(final String field, final BitSet allowedChars, final BitSet allowedFirstChars) { + final int start; + + if (allowedFirstChars == null) { + start = 0; + } else { + final char c = field.charAt(0); + + if (!allowedFirstChars.get(c)) { + throw new IllegalArgumentException("Illegal char '" + c + "' (0x" + Integer.toHexString(c) + ") in '" + field + "'"); + } + + start = 1; + } + + final int length = field.length(); + + for (int i = start; i < length; i++) { + final char c = field.charAt(i); + final boolean allowed = allowedChars.get(c); + + if (!allowed) { + throw new IllegalArgumentException("Illegal char '" + c + "' (0x" + Integer.toHexString(c) + ") in '" + field + "'"); + } + } + + if (field.contains(DOT_DOT)) { + throw new IllegalArgumentException("Illegal sequence '..' in '" + field + "'"); + } + } +} diff --git a/rpm/src/main/java/org/eclipse/packager/rpm/build/RpmBuilder.java b/rpm/src/main/java/org/eclipse/packager/rpm/build/RpmBuilder.java index bb7fea4..10219b9 100644 --- a/rpm/src/main/java/org/eclipse/packager/rpm/build/RpmBuilder.java +++ b/rpm/src/main/java/org/eclipse/packager/rpm/build/RpmBuilder.java @@ -15,6 +15,7 @@ import static java.util.Comparator.comparing; import static java.util.Optional.of; +import static org.eclipse.packager.rpm.RpmVersionValidator.validateName; import java.io.IOException; import java.io.InputStream; @@ -653,6 +654,7 @@ private static BuilderOptions makeBuilderOptions(final OpenOption[] openOptions) public RpmBuilder(final String name, final RpmVersion version, final String architecture, final Path targetFile, final BuilderOptions options) throws IOException { this.name = name; + validateName(name); this.version = version; this.architecture = architecture; 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 468a91e..3a6ed98 100644 --- a/rpm/src/test/java/org/eclipse/packager/rpm/VersionTest.java +++ b/rpm/src/test/java/org/eclipse/packager/rpm/VersionTest.java @@ -13,23 +13,112 @@ 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; +import org.junit.jupiter.params.provider.ValueSource; +import java.util.List; +import java.util.NoSuchElementException; import java.util.Optional; +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.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.eclipse.packager.rpm.RpmVersionValidator.validateName; class VersionTest { + @Test + void testName() { + assertThatCode(() -> validateName("foo")).doesNotThrowAnyException(); + assertThatThrownBy(() -> validateName("~foo")).isExactlyInstanceOf(IllegalArgumentException.class).hasMessage("Illegal char '~' (0x7e) in '~foo'"); + assertThatThrownBy(() -> validateName("foo\0")).isExactlyInstanceOf(IllegalArgumentException.class).hasMessage("Illegal char '\0' (0x0) in 'foo\0'"); + assertThatThrownBy(() -> validateName("€foo")).isExactlyInstanceOf(IllegalArgumentException.class).hasMessage("Illegal char '€' (0x20ac) in '€foo'"); + } + + @Test + void testRpmVersion() { + final RpmVersion v = new RpmVersion("1.0"); + assertThat(v.getEpoch()).isEmpty(); + assertThat(v.getVersion()).isEqualTo("1.0"); + assertThat(v.getRelease()).isEmpty(); + assertThat(v).hasToString("1.0"); + } + + @Test + void testRpmVersionWithRelease() { + final RpmVersion v = new RpmVersion("1.0", "1"); + assertThat(v.getEpoch()).isEmpty(); + assertThat(v.getVersion()).isEqualTo("1.0"); + assertThat(v.getRelease()).hasValue("1"); + assertThat(v).hasToString("1.0-1"); + } + + @Test + void testRpmVersionWithEmptyRelease() { + final RpmVersion v = new RpmVersion("1.0", ""); + assertThat(v.getEpoch()).isEmpty(); + assertThat(v.getVersion()).isEqualTo("1.0"); + assertThat(v.getRelease()).hasValue(""); + assertThat(v).hasToString("1.0"); + } + + @Test + void testEquals() { + final RpmVersion v1 = new RpmVersion("1.0"); + final RpmVersion v2 = new RpmVersion(0, "1.0", null); + final RpmVersion v3 = new RpmVersion("2.0"); + final RpmVersion v4 = new RpmVersion("1.0", "1"); + final RpmVersion v5 = new RpmVersion("1.0", "2"); + final RpmVersion v6 = new RpmVersion(1, "1.0", "2"); + assertThat(v1).isNotEqualTo(null).isNotEqualTo("").isNotEqualTo(v2).isNotEqualTo(v3).isNotEqualTo(v4); + assertThat(v4).isNotEqualTo(v5); + assertThat(v5).isNotEqualTo(v6); + } + + @Test + void testRpmVersionNull() { + assertThat(RpmVersion.valueOf(null)).isNull(); + assertThat(RpmVersion.valueOf("")).isNull(); + } + @ParameterizedTest - @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"}) + @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"}) void testVersion(final String version, final Integer expectedEpoch, final String expectedVersion, final String expectedRelease) { final RpmVersion v = RpmVersion.valueOf(version); assertThat(v.getEpoch()).isEqualTo(Optional.ofNullable(expectedEpoch)); assertThat(v.getVersion()).isEqualTo(expectedVersion); assertThat(v.getRelease()).isEqualTo(Optional.ofNullable(expectedRelease)); + assertThat(v).hasToString(version); + } + + @ParameterizedTest + @ValueSource(strings = {"1-2-3\n", "A:1.2.3", "1.2.3-123-456", "1..2"}) + void testInvalidVersion(final String version) { + assertThatThrownBy(() -> RpmVersion.valueOf(version)).isExactlyInstanceOf(IllegalArgumentException.class).hasMessageStartingWith("Illegal "); + } + + @Test + void testRpmScanner() { + final RpmVersionScanner scanner = new RpmVersionScanner("1.0"); + assertThat(scanner.hasNext()).isTrue(); + assertThat(scanner.next()).asString().isEqualTo("1"); + assertThat(scanner.hasNext()).isTrue(); + assertThat(scanner.next()).asString().isEqualTo("0"); + assertThat(scanner.hasNext()).isFalse(); + assertThatThrownBy(scanner::next).isExactlyInstanceOf(NoSuchElementException.class); + } + + @Test + void testRpmScannerTokens() { + final RpmVersionScanner scanner = new RpmVersionScanner("2.0.1"); + 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"); } @ParameterizedTest