Copilot commented on code in PR #208:
URL: 
https://github.com/apache/maven-script-interpreter/pull/208#discussion_r4056807813


##########
src/test/java/org/apache/maven/shared/scriptinterpreter/FileLoggerTest.java:
##########
@@ -81,7 +85,8 @@ void outputFileNoMirror(@TempDir File tempDir) throws 
Exception {
 
         try (FileLogger fileLogger = new FileLogger(outputFile)) {
             fileLogger.consumeLine("Test1");
-            fileLogger.getPrintStream().println("Test2");
+            fileLogger.getPrintStream().print("Test2");
+            fileLogger.getPrintStream().print('\n');
             fileLogger.getPrintStream().flush();

Review Comment:
   This test reads the log file using the platform default charset. Since 
FileLogger now writes UTF-8 unconditionally, decode as UTF-8 here too to keep 
the test platform-independent.
   
   This issue also appears in the following locations of the same file:
   - line 116
   - line 167



##########
src/main/java/org/apache/maven/shared/scriptinterpreter/FileLogger.java:
##########
@@ -133,60 +139,140 @@ public void close() {
         }
     }
 
+    /**
+     * 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 the trailing line end, so each flushed chunk is logged 
as a single line

Review Comment:
   This comment is inaccurate: the code only trims a single trailing LF, so a 
flushed chunk may still contain embedded (or even trailing) newlines if 
multiple are written before flush. Consider rewording to describe the actual 
behavior (trim one trailing LF before mirroring).



##########
src/test/java/org/apache/maven/shared/scriptinterpreter/FileLoggerTest.java:
##########
@@ -81,7 +85,8 @@ void outputFileNoMirror(@TempDir File tempDir) throws 
Exception {
 

Review Comment:
   `outputFile` is created with a child path that starts with '/', so `new 
File(tempDir, "/target/test.log")` ignores `tempDir` and writes to an absolute 
`/target/test.log` path instead. This makes the test non-portable and may fail 
due to permissions or pollute the filesystem.



##########
src/main/java/org/apache/maven/shared/scriptinterpreter/FileLogger.java:
##########
@@ -133,60 +139,140 @@ public void close() {
         }
     }
 
+    /**
+     * 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 the trailing line end, so each flushed chunk is logged 
as a single line
+            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();
         }

Review Comment:
   LineFeedNormalizer wraps the underlying OutputStream but does not override 
close(), so closing the PrintStream only closes the wrapper and leaves the real 
file stream open. This can leak file descriptors and can break cleanup on 
Windows (e.g., delete/rename of the log file after closing).



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to