You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by el...@apache.org on 2026/09/29 10:40:35 UTC
(maven-script-interpreter) branch master updated: Make FileLogger platform-independent: UTF-8 encoding and LF line separators (#208)
This is an automated email from the ASF dual-hosted git repository.
elharo pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-script-interpreter.git
The following commit(s) were added to refs/heads/master by this push:
new 07c5a13 Make FileLogger platform-independent: UTF-8 encoding and LF line separators (#208)
07c5a13 is described below
commit 07c5a13e0a6dfe7b6d027bc7683682f628acbfa1
Author: Elliotte Rusty Harold <el...@users.noreply.github.com>
AuthorDate: Tue Sep 29 10:40:29 2026 +0000
Make FileLogger platform-independent: UTF-8 encoding and LF line separators (#208)
* Use UTF-8 for mirror stream byte decoding instead of platform default charset
* Encode and mirror as UTF-8 with LF line separators for platform independence
* Simplify javadoc and make FileLoggerTest line separators platform-independent
* Normalize line separators to LF for platform-independent log output
* make test match
* Update comment for clarity on trailing line end handling
Co-authored-by: Copilot Autofix powered by AI <17...@users.noreply.github.com>
* Implement close method in FileLogger
Co-authored-by: Copilot Autofix powered by AI <17...@users.noreply.github.com>
* Fix FileLoggerTest portability: use temp-relative path and read as UTF-8
---------
Co-authored-by: Copilot Autofix powered by AI <17...@users.noreply.github.com>
---
.../maven/shared/scriptinterpreter/FileLogger.java | 138 +++++++++++++++++----
.../shared/scriptinterpreter/FileLoggerTest.java | 72 +++++++++--
.../shared/scriptinterpreter/ScriptRunnerTest.java | 18 +--
.../scriptinterpreter/TestMirrorHandler.java | 2 +-
4 files changed, 188 insertions(+), 42 deletions(-)
diff --git a/src/main/java/org/apache/maven/shared/scriptinterpreter/FileLogger.java b/src/main/java/org/apache/maven/shared/scriptinterpreter/FileLogger.java
index e41b4c9..29237d2 100644
--- a/src/main/java/org/apache/maven/shared/scriptinterpreter/FileLogger.java
+++ b/src/main/java/org/apache/maven/shared/scriptinterpreter/FileLogger.java
@@ -18,10 +18,12 @@
*/
package org.apache.maven.shared.scriptinterpreter;
+import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.Path;
@@ -75,9 +77,10 @@ public class FileLogger implements ExecutionLogger, AutoCloseable {
}
if (mirrorHandler != null) {
- stream = new PrintStream(new MirrorStreamWrapper(outputStream, mirrorHandler));
+ this.stream = new PrintStream(
+ new MirrorStreamWrapper(outputStream, mirrorHandler), false, StandardCharsets.UTF_8.name());
} else {
- stream = new PrintStream(outputStream);
+ this.stream = new PrintStream(new LineFeedNormalizer(outputStream), false, StandardCharsets.UTF_8.name());
}
}
@@ -111,14 +114,17 @@ public class FileLogger implements ExecutionLogger, AutoCloseable {
}
/**
- * Writes the specified line to the log file
- * and invoke {@link FileLoggerMirrorHandler#consumeOutput(String)} if is given.
+ * Writes the specified line to the log file and invokes
+ * {@link FileLoggerMirrorHandler#consumeOutput(String)} if a mirror handler is configured.
+ * A Unix line terminator (LF) is always used, regardless of the platform, so log files are
+ * identical across operating systems.
*
* @param line The message to log.
*/
@Override
public void consumeLine(String line) {
- stream.println(line);
+ stream.print(line);
+ stream.print('\n');
stream.flush();
}
@@ -133,61 +139,147 @@ public class FileLogger implements ExecutionLogger, AutoCloseable {
}
}
+ /**
+ * A byte stream that normalizes line terminators to {@code \n}, forwards every normalized byte
+ * to the underlying output stream, and mirrors each flushed chunk to the
+ * {@link FileLoggerMirrorHandler} as a UTF-8 decoded string. Because this wrapper normalizes
+ * line terminators before forwarding, both the underlying file and the mirror always use the
+ * same {@code \n} separators, independent of the platform.
+ */
private static class MirrorStreamWrapper extends OutputStream {
- private OutputStream out;
+ private final OutputStream out;
private final FileLoggerMirrorHandler mirrorHandler;
- private StringBuilder lineBuffer;
+ private final ByteArrayOutputStream lineBuffer = new ByteArrayOutputStream();
+
+ private boolean lastWasCR;
- MirrorStreamWrapper(OutputStream outputStream, FileLoggerMirrorHandler mirrorHandler) {
- this.out = outputStream;
+ MirrorStreamWrapper(OutputStream out, FileLoggerMirrorHandler mirrorHandler) {
+ this.out = out;
this.mirrorHandler = mirrorHandler;
- this.lineBuffer = new StringBuilder();
}
@Override
public void write(int b) throws IOException {
- out.write(b);
- lineBuffer.append((char) (b));
+ writeByte(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
- out.write(b, off, len);
- lineBuffer.append(new String(b, off, len));
+ for (int i = off; i < off + len; i++) {
+ writeByte(b[i] & 0xFF);
+ }
+ }
+
+ private void writeByte(int b) throws IOException {
+ if (b == '\r') {
+ lastWasCR = true;
+ return;
+ }
+ if (lastWasCR) {
+ lastWasCR = false;
+ emit('\n');
+ if (b == '\n') {
+ return;
+ }
+ }
+ emit(b);
+ }
+
+ private void emit(int b) throws IOException {
+ out.write(b);
+ lineBuffer.write(b);
}
@Override
public void flush() throws IOException {
+ if (lastWasCR) {
+ lastWasCR = false;
+ emit('\n');
+ }
out.flush();
- int len = lineBuffer.length();
+ byte[] bytes = lineBuffer.toByteArray();
+ int len = bytes.length;
if (len == 0) {
// nothing to log
return;
}
- // remove line end for log
- while (len > 0 && (lineBuffer.charAt(len - 1) == '\n' || lineBuffer.charAt(len - 1) == '\r')) {
+ // remove one trailing LF before mirroring (a flushed chunk may still contain embedded newlines)
+ if (bytes[len - 1] == '\n') {
len--;
}
- lineBuffer.setLength(len);
- mirrorHandler.consumeOutput(lineBuffer.toString());
+ mirrorHandler.consumeOutput(new String(bytes, 0, len, StandardCharsets.UTF_8));
// clear buffer
- lineBuffer = new StringBuilder();
+ lineBuffer.reset();
}
@Override
public void close() throws IOException {
flush();
- if (out != null) {
- out.close();
- out = null;
+ out.close();
+ }
+ }
+
+ /**
+ * An output stream that normalizes line terminators to the Unix LF ({@code \n}) character,
+ * so the written content is independent of the platform on which it runs. A CRLF sequence
+ * ({@code \r\n}) or a lone carriage return ({@code \r}) is converted to a single LF.
+ */
+ private static class LineFeedNormalizer extends OutputStream {
+ private final OutputStream out;
+
+ private boolean lastWasCR;
+
+ LineFeedNormalizer(OutputStream out) {
+ this.out = out;
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ writeByte(b);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ for (int i = off; i < off + len; i++) {
+ writeByte(b[i] & 0xFF);
}
}
+
+ private void writeByte(int b) throws IOException {
+ if (b == '\r') {
+ lastWasCR = true;
+ return;
+ }
+ if (lastWasCR) {
+ lastWasCR = false;
+ out.write('\n');
+ if (b == '\n') {
+ return;
+ }
+ }
+ out.write(b);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ if (lastWasCR) {
+ lastWasCR = false;
+ out.write('\n');
+ }
+ out.flush();
+ }
+
+ @Override
+ public void close() throws IOException {
+ flush();
+ out.close();
+ }
}
private static class NullOutputStream extends OutputStream {
diff --git a/src/test/java/org/apache/maven/shared/scriptinterpreter/FileLoggerTest.java b/src/test/java/org/apache/maven/shared/scriptinterpreter/FileLoggerTest.java
index 6dd1387..588759f 100644
--- a/src/test/java/org/apache/maven/shared/scriptinterpreter/FileLoggerTest.java
+++ b/src/test/java/org/apache/maven/shared/scriptinterpreter/FileLoggerTest.java
@@ -19,11 +19,13 @@
package org.apache.maven.shared.scriptinterpreter;
import java.io.File;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -33,13 +35,14 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
*/
public class FileLoggerTest {
- public static final String EXPECTED_LOG = "Test1" + System.lineSeparator() + "Test2" + System.lineSeparator();
+ public static final String EXPECTED_LOG = "Test1\nTest2\n";
@Test
void nullOutputFileNoMirror() throws Exception {
try (FileLogger fileLogger = new FileLogger(null)) {
fileLogger.consumeLine("Test1");
- fileLogger.getPrintStream().println("Test2");
+ fileLogger.getPrintStream().print("Test2");
+ fileLogger.getPrintStream().print('\n');
fileLogger.getPrintStream().flush();
assertNull(fileLogger.getOutputFile());
@@ -52,7 +55,8 @@ public class FileLoggerTest {
try (FileLogger fileLogger = new FileLogger(null, mirrorHandler)) {
fileLogger.consumeLine("Test1");
- fileLogger.getPrintStream().println("Test2");
+ fileLogger.getPrintStream().print("Test2");
+ fileLogger.getPrintStream().print('\n');
fileLogger.getPrintStream().flush();
assertNull(fileLogger.getOutputFile());
@@ -72,23 +76,24 @@ public class FileLoggerTest {
assertNull(fileLogger.getOutputFile());
}
- assertEquals("A" + System.lineSeparator(), mirrorHandler.getLoggedMessage());
+ assertEquals("A\n", mirrorHandler.getLoggedMessage());
}
@Test
void outputFileNoMirror(@TempDir File tempDir) throws Exception {
- File outputFile = new File(tempDir, "/target/test.log");
+ File outputFile = new File(tempDir, "target/test.log");
try (FileLogger fileLogger = new FileLogger(outputFile)) {
fileLogger.consumeLine("Test1");
- fileLogger.getPrintStream().println("Test2");
+ fileLogger.getPrintStream().print("Test2");
+ fileLogger.getPrintStream().print('\n');
fileLogger.getPrintStream().flush();
assertEquals(outputFile, fileLogger.getOutputFile());
}
assertTrue(outputFile.exists());
- assertEquals(EXPECTED_LOG, new String(Files.readAllBytes(outputFile.toPath())));
+ assertEquals(EXPECTED_LOG, new String(Files.readAllBytes(outputFile.toPath()), StandardCharsets.UTF_8));
}
@Test
@@ -98,7 +103,8 @@ public class FileLoggerTest {
try (FileLogger fileLogger = new FileLogger(outputFile, mirrorHandler)) {
fileLogger.consumeLine("Test1");
- fileLogger.getPrintStream().println("Test2");
+ fileLogger.getPrintStream().print("Test2");
+ fileLogger.getPrintStream().print('\n');
fileLogger.getPrintStream().flush();
assertEquals(outputFile, fileLogger.getOutputFile());
@@ -107,7 +113,53 @@ public class FileLoggerTest {
assertEquals(EXPECTED_LOG, mirrorHandler.getLoggedMessage());
assertTrue(outputFile.exists());
- assertEquals(EXPECTED_LOG, new String(Files.readAllBytes(outputFile.toPath())));
+ assertEquals(EXPECTED_LOG, new String(Files.readAllBytes(outputFile.toPath()), StandardCharsets.UTF_8));
+ }
+
+ /**
+ * Verifies that non-ASCII content written through the logger is encoded and decoded as UTF-8
+ * on both the mirror handler and the underlying file, independent of the platform default
+ * charset and line separator.
+ * "café" in UTF-8: {0x63, 0x61, 0x66, 0xC3, 0xA9, 0x0A}.
+ * A platform that decodes these bytes as ISO-8859-1 would produce "café" instead of "café".
+ */
+ @Test
+ void mirrorAndFileShouldUseUtf8(@TempDir File tempDir) throws Exception {
+ File outputFile = new File(tempDir, "utf8.log");
+ TestMirrorHandler mirrorHandler = new TestMirrorHandler();
+
+ try (FileLogger fileLogger = new FileLogger(outputFile, mirrorHandler)) {
+ fileLogger.getPrintStream().write("café".getBytes(StandardCharsets.UTF_8));
+ fileLogger.getPrintStream().write('\n');
+ fileLogger.getPrintStream().flush();
+ }
+
+ assertEquals("café\n", mirrorHandler.getLoggedMessage());
+ assertTrue(outputFile.exists());
+ assertArrayEquals("café\n".getBytes(StandardCharsets.UTF_8), Files.readAllBytes(outputFile.toPath()));
+ }
+
+ /**
+ * Scripts running through the logger write with the platform's native line separator, which is
+ * {@code \r\n} on Windows. This verifies that both CRLF and a lone CR are normalized to a
+ * Unix LF ({@code \n}) in the file and the mirror, so the log content is identical on every
+ * platform and the mirror always matches the file.
+ */
+ @Test
+ void crLfAndCarriageReturnAreNormalizedToLf(@TempDir File tempDir) throws Exception {
+ File outputFile = new File(tempDir, "crlf.log");
+ TestMirrorHandler mirrorHandler = new TestMirrorHandler();
+
+ try (FileLogger fileLogger = new FileLogger(outputFile, mirrorHandler)) {
+ fileLogger.getPrintStream().print("line1\r\n");
+ fileLogger.getPrintStream().print("line2\r");
+ fileLogger.getPrintStream().print("\n");
+ fileLogger.consumeLine("line3");
+ }
+
+ assertEquals("line1\nline2\nline3\n", mirrorHandler.getLoggedMessage());
+ assertArrayEquals(
+ "line1\nline2\nline3\n".getBytes(StandardCharsets.UTF_8), Files.readAllBytes(outputFile.toPath()));
}
@Test
@@ -124,7 +176,7 @@ public class FileLoggerTest {
}
assertTrue(outputFile.exists());
- assertEquals(EXPECTED_LOG, new String(Files.readAllBytes(outputFile.toPath())));
+ assertEquals(EXPECTED_LOG, new String(Files.readAllBytes(outputFile.toPath()), StandardCharsets.UTF_8));
} finally {
Files.deleteIfExists(outputFile.toPath());
}
diff --git a/src/test/java/org/apache/maven/shared/scriptinterpreter/ScriptRunnerTest.java b/src/test/java/org/apache/maven/shared/scriptinterpreter/ScriptRunnerTest.java
index 25f23d3..decc21c 100644
--- a/src/test/java/org/apache/maven/shared/scriptinterpreter/ScriptRunnerTest.java
+++ b/src/test/java/org/apache/maven/shared/scriptinterpreter/ScriptRunnerTest.java
@@ -19,6 +19,7 @@
package org.apache.maven.shared.scriptinterpreter;
import java.io.File;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Collections;
import java.util.HashMap;
@@ -54,7 +55,7 @@ class ScriptRunnerTest {
scriptRunner.run("test", new File("src/test/resources/bsh-test"), "verify", buildContext(), fileLogger);
}
- String logContent = new String(Files.readAllBytes(logFile.toPath()));
+ String logContent = new String(Files.readAllBytes(logFile.toPath()), StandardCharsets.UTF_8);
assertTrue(logContent.contains(new File("src/test/resources/bsh-test/verify.bsh").getPath()));
assertTrue(logContent.contains("foo=bar"));
assertTrue(logContent.contains("globalVar=Yeah baby it's rocks"));
@@ -73,7 +74,7 @@ class ScriptRunnerTest {
scriptRunner.run("test", new File("src/test/resources/bsh-test"), "return-null", null, fileLogger);
}
- String logContent = new String(Files.readAllBytes(logFile.toPath()));
+ String logContent = new String(Files.readAllBytes(logFile.toPath()), StandardCharsets.UTF_8);
assertTrue(logContent.contains(new File("src/test/resources/bsh-test/return-null.bsh").getPath()));
assertTrue(logContent.contains("ok with null result"));
assertEquals(logContent, mirrorHandler.getLoggedMessage());
@@ -125,7 +126,7 @@ class ScriptRunnerTest {
scriptRunner.run("test", new File("src/test/resources/bsh-test/verify.bsh"), buildContext(), fileLogger);
}
- String logContent = new String(Files.readAllBytes(logFile.toPath()));
+ String logContent = new String(Files.readAllBytes(logFile.toPath()), StandardCharsets.UTF_8);
assertTrue(logContent.contains(new File("src/test/resources/bsh-test/verify.bsh").getPath()));
assertTrue(logContent.contains("foo=bar"));
@@ -144,7 +145,7 @@ class ScriptRunnerTest {
scriptRunner.run("test", new File("src/test/resources/groovy-test"), "verify", buildContext(), fileLogger);
}
- String logContent = new String(Files.readAllBytes(logFile.toPath()));
+ String logContent = new String(Files.readAllBytes(logFile.toPath()), StandardCharsets.UTF_8);
assertTrue(logContent.contains(new File("src/test/resources/groovy-test/verify.groovy").getPath()));
assertTrue(logContent.contains("foo=bar"));
assertTrue(logContent.contains("globalVar=Yeah baby it's rocks"));
@@ -164,7 +165,7 @@ class ScriptRunnerTest {
scriptRunner.run("test", new File("src/test/resources/groovy-test"), "return-null", null, fileLogger);
}
- String logContent = new String(Files.readAllBytes(logFile.toPath()));
+ String logContent = new String(Files.readAllBytes(logFile.toPath()), StandardCharsets.UTF_8);
assertTrue(logContent.contains(new File("src/test/resources/groovy-test/return-null.groovy").getPath()));
assertTrue(logContent.contains("ok with null result"));
assertEquals(logContent, mirrorHandler.getLoggedMessage());
@@ -194,6 +195,7 @@ class ScriptRunnerTest {
ScriptRunner scriptRunner = new ScriptRunner()) {
scriptRunner.run(
"test", new File("src/test/resources/groovy-test"), "return-false", buildContext(), fileLogger);
+ fail();
} catch (ScriptReturnException expected) {
assertEquals(false, expected.getResult());
assertEquals("The test returned false.", expected.getMessage());
@@ -215,7 +217,7 @@ class ScriptRunnerTest {
"test", new File("src/test/resources/groovy-test/verify.groovy"), buildContext(), fileLogger);
}
- String logContent = new String(Files.readAllBytes(logFile.toPath()));
+ String logContent = new String(Files.readAllBytes(logFile.toPath()), StandardCharsets.UTF_8);
assertTrue(logContent.contains(new File("src/test/resources/groovy-test/verify.groovy").getPath()));
assertTrue(logContent.contains("foo=bar"));
@@ -240,7 +242,7 @@ class ScriptRunnerTest {
scriptRunner.run("test classpath 2", basedir, "class-path2", context, logger);
}
- String logContent = new String(Files.readAllBytes(logFile.toPath()));
+ String logContent = new String(Files.readAllBytes(logFile.toPath()), StandardCharsets.UTF_8);
assertTrue(logContent.contains("wireMockServer started with port="));
assertTrue(logContent.contains("wireMockServer stopped"));
}
@@ -309,7 +311,7 @@ class ScriptRunnerTest {
scriptRunner.run("utf8-test", new File("src/test/resources/groovy-test/utf8-test.groovy"), context, logger);
}
- String logContent = new String(Files.readAllBytes(logFile.toPath()));
+ String logContent = new String(Files.readAllBytes(logFile.toPath()), StandardCharsets.UTF_8);
assertTrue(logContent.contains("expected=café"));
assertTrue(logContent.contains("actual=café"));
}
diff --git a/src/test/java/org/apache/maven/shared/scriptinterpreter/TestMirrorHandler.java b/src/test/java/org/apache/maven/shared/scriptinterpreter/TestMirrorHandler.java
index 9e45584..a89d922 100644
--- a/src/test/java/org/apache/maven/shared/scriptinterpreter/TestMirrorHandler.java
+++ b/src/test/java/org/apache/maven/shared/scriptinterpreter/TestMirrorHandler.java
@@ -36,6 +36,6 @@ class TestMirrorHandler implements FileLoggerMirrorHandler {
@Override
public void consumeOutput(String message) {
loggedMessage.append(message);
- loggedMessage.append(System.lineSeparator());
+ loggedMessage.append('\n');
}
}