diff --git a/src/main/java/org/apache/maven/shared/archiver/PomPropertiesUtil.java b/src/main/java/org/apache/maven/shared/archiver/PomPropertiesUtil.java index 3eb523e..131ea14 100644 --- a/src/main/java/org/apache/maven/shared/archiver/PomPropertiesUtil.java +++ b/src/main/java/org/apache/maven/shared/archiver/PomPropertiesUtil.java @@ -18,19 +18,18 @@ */ package org.apache.maven.shared.archiver; -import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; -import java.io.PrintWriter; -import java.io.StringReader; -import java.io.StringWriter; +import java.io.Writer; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Properties; +import java.util.Set; import org.apache.maven.api.Project; import org.apache.maven.api.Session; @@ -61,36 +60,81 @@ private boolean sameContents(Properties props, Path file) throws IOException { return fileProps.equals(props); } - private void createPropertiesFile(Properties properties, Path outputFile, boolean forceCreation) + private void createPropertiesFile(Properties unsortedProperties, Path outputFile, boolean forceCreation) throws IOException { Path outputDir = outputFile.getParent(); if (outputDir != null && !Files.isDirectory(outputDir)) { Files.createDirectories(outputDir); } - if (!forceCreation && sameContents(properties, outputFile)) { + if (!forceCreation && sameContents(unsortedProperties, outputFile)) { return; } - try (PrintWriter pw = new PrintWriter(outputFile.toFile(), StandardCharsets.ISO_8859_1.name()); - StringWriter sw = new StringWriter()) { - - properties.store(sw, null); + // For reproducible builds, sort the properties and drop comments. + // The java.util.Properties class doesn't guarantee order so we have + // to write the file using a Writer. + Set propertyNames = unsortedProperties.stringPropertyNames(); + List sortedPropertyNames = new ArrayList<>(propertyNames); + Collections.sort(sortedPropertyNames); + + try (Writer out = Files.newBufferedWriter(outputFile, StandardCharsets.ISO_8859_1)) { + for (String key : sortedPropertyNames) { + out.write(escapeKey(key)); + out.write("="); + out.write(escapeValue(unsortedProperties.getProperty(key))); + out.write('\n'); + } + } + } - List lines = new ArrayList<>(); - try (BufferedReader r = new BufferedReader(new StringReader(sw.toString()))) { - String line; - while ((line = r.readLine()) != null) { - if (!line.startsWith("#")) { - lines.add(line); - } - } + private static String escapeKey(String s) { + StringBuilder sb = new StringBuilder(s.length()); + for (char c : s.toCharArray()) { + if (Character.isWhitespace(c) + || c == '#' + || c == '!' + || c == '=' + || c == ':' + || c == '\\') { // backslash escape + sb.append('\\'); + sb.append(c); + } else if (c < 128) { // ASCII + sb.append(c); + } else { + sb.append(hexEncode(c)); } + } + return sb.toString(); + } - Collections.sort(lines); - for (String l : lines) { - pw.println(l); + private static String escapeValue(String s) { + StringBuilder sb = new StringBuilder(s.length()); + boolean atBeginning = true; + for (char c : s.toCharArray()) { + if (Character.isWhitespace(c) && atBeginning) { + sb.append('\\'); + sb.append(c); + } else if (c == '#' || c == '!' || c == '=' || c == ':' || c == '\\') { // backslash escape + sb.append('\\'); + sb.append(c); + atBeginning = false; + } else if (c < 128) { // ASCII + sb.append(c); + atBeginning = false; + } else { + sb.append(hexEncode(c)); + atBeginning = false; } } + return sb.toString(); + } + + private static String hexEncode(char c) { + String hexString = Integer.toHexString(c).toUpperCase(Locale.ENGLISH); + while (hexString.length() < 4) { + hexString = '0' + hexString; + } + return "\\u" + hexString; } /** @@ -129,6 +173,7 @@ public void createPomProperties( // CHECKSTYLE_OFF: ParameterNumber public void createPomProperties( + // TODO the session isn't needed here Session session, String groupId, String artifactId, diff --git a/src/test/java/org/apache/maven/shared/archiver/PomPropertiesUtilTest.java b/src/test/java/org/apache/maven/shared/archiver/PomPropertiesUtilTest.java new file mode 100644 index 0000000..c1dc57e --- /dev/null +++ b/src/test/java/org/apache/maven/shared/archiver/PomPropertiesUtilTest.java @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.shared.archiver; + +import java.io.IOException; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Properties; + +import org.apache.maven.api.Session; +import org.codehaus.plexus.archiver.jar.JarArchiver; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class PomPropertiesUtilTest { + + private PomPropertiesUtil util = new PomPropertiesUtil(); + + @TempDir + Path tempDirectory; + + @Test + void testCreatePomProperties() throws IOException { + Path pomPropertiesFile = tempDirectory.resolve("bar.properties"); + util.createPomProperties( + (Session) null, "org.foo", "bar", "2.1.5", new JarArchiver(), null, pomPropertiesFile, true); + + assertThat(pomPropertiesFile).exists(); + Properties actual = new Properties(); + actual.load(Files.newInputStream(pomPropertiesFile)); + assertEquals("org.foo", actual.getProperty("groupId")); + assertEquals("bar", actual.getProperty("artifactId")); + assertEquals("2.1.5", actual.getProperty("version")); + + // Now read the file directly to check for alphabetical order + List contents = Files.readAllLines(pomPropertiesFile, StandardCharsets.ISO_8859_1); + assertEquals("artifactId=bar", contents.get(0)); + assertEquals("groupId=org.foo", contents.get(1)); + assertEquals("version=2.1.5", contents.get(2)); + assertEquals(3, contents.size()); + } + + @Test + void testUnicodeEscape() throws IOException { + Path pomPropertiesFile = tempDirectory.resolve("bar.properties"); + util.createPomProperties( + (Session) null, "org.foo", "こんにちは", "2.1.5", new JarArchiver(), null, pomPropertiesFile, true); + + assertThat(pomPropertiesFile).exists(); + Properties actual = new Properties(); + actual.load(Files.newInputStream(pomPropertiesFile)); + assertEquals("org.foo", actual.getProperty("groupId")); + assertEquals("こんにちは", actual.getProperty("artifactId")); + assertEquals("2.1.5", actual.getProperty("version")); + + // Now read the file directly to check for alphabetical order and encoding + List contents = Files.readAllLines(pomPropertiesFile, StandardCharsets.ISO_8859_1); + assertEquals("artifactId=\\u3053\\u3093\\u306B\\u3061\\u306F", contents.get(0)); + assertEquals("groupId=org.foo", contents.get(1)); + assertEquals("version=2.1.5", contents.get(2)); + assertEquals(3, contents.size()); + } + + @Test + void testWhitespaceEscape() throws IOException { + Path pomPropertiesFile = tempDirectory.resolve("bar.properties"); + Path customPomPropertiesFile = tempDirectory.resolve("custom.properties"); + try (Writer out = Files.newBufferedWriter(customPomPropertiesFile, StandardCharsets.ISO_8859_1)) { + out.write("a\\u0020key\\u0020with\\u0009whitespace=value\\u0020with\\u0009whitespace\n"); + out.write("zkey=value with \\\\ not at end of line\n"); + out.write("ykey=\\tvalue with whitespace at beginning\n"); + out.write("xkey=\\u00E9\\u00FC\\u00E5\n"); + } + + util.createPomProperties( + (Session) null, + "org.foo", + "こんにちは", + "2.1.5", + new JarArchiver(), + customPomPropertiesFile, + pomPropertiesFile, + true); + assertThat(pomPropertiesFile).exists(); + + Properties actual = new Properties(); + actual.load(Files.newInputStream(pomPropertiesFile)); + assertEquals("value with\twhitespace", actual.getProperty("a key with\twhitespace")); + assertEquals("value with \\ not at end of line", actual.getProperty("zkey")); + assertEquals("\tvalue with whitespace at beginning", actual.getProperty("ykey")); + assertEquals("éüå", actual.getProperty("xkey")); + + // Now read the file directly to check for alphabetical order and encoding + List contents = Files.readAllLines(pomPropertiesFile, StandardCharsets.ISO_8859_1); + assertEquals(7, contents.size()); + assertEquals("a\\ key\\ with\\\twhitespace=value with\twhitespace", contents.get(0)); + assertEquals("artifactId=\\u3053\\u3093\\u306B\\u3061\\u306F", contents.get(1)); + assertEquals("groupId=org.foo", contents.get(2)); + assertEquals("version=2.1.5", contents.get(3)); + assertEquals("xkey=\\u00E9\\u00FC\\u00E5", contents.get(4)); + assertEquals("ykey=\\\tvalue with whitespace at beginning", contents.get(5)); + assertEquals("zkey=value with \\\\ not at end of line", contents.get(6)); + } +}