This is an automated email from the ASF dual-hosted git repository.

coheigea pushed a commit to branch coheigea/fork
in repository https://gitbox.apache.org/repos/asf/cxf-xjc-utils.git

commit 77f38ea53db60fb19bcf54bbaf08a953f0e35695
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Mon Oct 19 15:27:11 2026 +0100

    Escape messages passed back from the forked XJC process
---
 .../cxf/maven_plugin/AbstractXSDToJavaMojo.java    |  40 +----
 .../cxf/maven_plugin/ForkedOutputConsumer.java     |  51 +++++++
 .../apache/cxf/maven_plugin/XJCBuildContext.java   | 149 ++++++++++++++----
 .../maven_plugin/ForkedOutputRoundTripTest.java    | 170 +++++++++++++++++++++
 .../cxf/maven_plugin/XJCBuildContextTest.java      |  76 +++++++++
 5 files changed, 421 insertions(+), 65 deletions(-)

diff --git 
a/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/AbstractXSDToJavaMojo.java
 
b/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/AbstractXSDToJavaMojo.java
index da6bb0f..9727d98 100644
--- 
a/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/AbstractXSDToJavaMojo.java
+++ 
b/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/AbstractXSDToJavaMojo.java
@@ -560,45 +560,7 @@ public abstract class AbstractXSDToJavaMojo extends 
AbstractMojo {
         }
         cmd.addArguments(args);
 
-        StreamConsumer out = new StreamConsumer() {
-            File file;
-            int severity;
-            int linenum;
-            int column;
-            StringBuilder message = new StringBuilder();
-
-            public synchronized void consumeLine(String line) {
-                if (getLog().isDebugEnabled()) {
-                    getLog().debug(line);
-                }
-                if (line.startsWith("DONE")) {
-                    buildContext.addMessage(file, linenum, column, 
message.toString(), severity, null);
-                } else if (line.startsWith("MSG: ")
-                    || line.startsWith("ERROR: ")
-                    || line.startsWith("WARNING: ")) {
-                    file = new File(line.substring(line.indexOf(' ')).trim());
-                    String type = line.substring(0, line.indexOf(':'));
-                    if (type.contains("ERROR")) {
-                        severity = BuildContext.SEVERITY_ERROR;
-                    } else if (type.contains("WARNING")) {
-                        severity = BuildContext.SEVERITY_WARNING;
-                    } else {
-                        severity = 0;
-                    }
-                    linenum = 0;
-                    column = 0;
-                    message.setLength(0);
-                } else if (line.startsWith("Col: ")) {
-                    column = Integer.parseInt(line.substring(line.indexOf(' 
')).trim());
-                } else if (line.startsWith("Line: ")) {
-                    linenum = Integer.parseInt(line.substring(line.indexOf(' 
')).trim());
-                } else if (line.startsWith("Severity: ")) {
-                    severity = Integer.parseInt(line.substring(line.indexOf(' 
')).trim());
-                } else {
-                    message.append(line).append('\n');
-                }
-            }
-        };
+        StreamConsumer out = new ForkedOutputConsumer(buildContext, getLog());
         int exitCode;
         try {
             exitCode = CommandLineUtils.executeCommandLine(cmd, out, out);
diff --git 
a/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/ForkedOutputConsumer.java
 
b/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/ForkedOutputConsumer.java
new file mode 100644
index 0000000..d54f45a
--- /dev/null
+++ 
b/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/ForkedOutputConsumer.java
@@ -0,0 +1,51 @@
+/**
+ * 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.cxf.maven_plugin;
+
+import org.apache.maven.plugin.logging.Log;
+import org.codehaus.plexus.util.cli.StreamConsumer;
+import org.sonatype.plexus.build.incremental.BuildContext;
+
+/**
+ * Reads the stdout/stderr of the forked XJC process and passes the messages 
written by
+ * {@link XJCBuildContext} on to the build context.
+ */
+class ForkedOutputConsumer implements StreamConsumer {
+    private final BuildContext buildContext;
+    private final Log log;
+
+    ForkedOutputConsumer(BuildContext buildContext, Log log) {
+        this.buildContext = buildContext;
+        this.log = log;
+    }
+
+    @Override
+    public void consumeLine(String line) {
+        if (log.isDebugEnabled()) {
+            log.debug(line);
+        }
+        XJCBuildContext.Message msg = XJCBuildContext.parseMessage(line);
+        if (msg != null) {
+            int severity = msg.severity == BuildContext.SEVERITY_ERROR
+                ? BuildContext.SEVERITY_ERROR : BuildContext.SEVERITY_WARNING;
+            buildContext.addMessage(msg.file, msg.line, msg.column, msg.text, 
severity, null);
+        }
+    }
+}
diff --git 
a/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/XJCBuildContext.java 
b/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/XJCBuildContext.java
index 5786a82..fc5f725 100644
--- 
a/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/XJCBuildContext.java
+++ 
b/cxf-xjc-plugin/src/main/java/org/apache/cxf/maven_plugin/XJCBuildContext.java
@@ -21,12 +21,42 @@ package org.apache.cxf.maven_plugin;
 import java.io.File;
 import java.io.IOException;
 import java.io.OutputStream;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
 import java.util.List;
 
 import org.codehaus.plexus.util.Scanner;
 import org.sonatype.plexus.build.incremental.BuildContext;
 
+/**
+ * The BuildContext used when XJC runs in a forked process: messages are 
written to stdout/stderr
+ * as single lines, which AbstractXSDToJavaMojo parses with {@link 
#parseMessage(String)}.
+ * The file and message are escaped, so text from a schema (which can contain 
line breaks) can
+ * neither spread over several lines nor be mistaken for a message of its own.
+ */
 public class XJCBuildContext implements BuildContext {
+    static final String MESSAGE_PREFIX = "CXF-XJC-MESSAGE\t";
+
+    /**
+     * A message parsed from the output of the forked process.
+     */
+    static final class Message {
+        final int severity;
+        final int line;
+        final int column;
+        final File file;
+        final String text;
+
+        Message(int severity, int line, int column, File file, String text) {
+            this.severity = severity;
+            this.line = line;
+            this.column = column;
+            this.file = file;
+            this.text = text;
+        }
+    }
+
     public boolean hasDelta(String relpath) {
         return false;
     }
@@ -60,41 +90,108 @@ public class XJCBuildContext implements BuildContext {
         return null;
     }
     public void addWarning(File file, int line, int column, String message, 
Throwable cause) {
-        System.out.println("WARNING: " + file.getAbsolutePath());
-        System.out.println("Line: " + line);
-        System.out.println("Col: " + column);
-        System.out.println(message);
-        if (cause != null) {
-            cause.printStackTrace(System.out);
-        }
-        System.out.println();
-        System.out.println("DONE");
+        System.out.println(formatMessage(SEVERITY_WARNING, file, line, column, 
message, cause));
     }
 
     @Override
     public void addError(File file, int line, int column, String message, 
Throwable cause) {
-        System.err.println("ERROR: " + file.getAbsolutePath());
-        System.err.println("Line: " + line);
-        System.err.println("Col: " + column);
-        System.err.println(message);
-        if (cause != null) {
-            cause.printStackTrace(System.err);
-        }
-        System.err.println();
-        System.err.println("DONE");
+        System.err.println(formatMessage(SEVERITY_ERROR, file, line, column, 
message, cause));
     }
+
     public void addMessage(File file, int line, int column, String message, 
int severity, Throwable cause) {
-        System.out.println("MSG: " + file.getAbsolutePath());
-        System.out.println("Severity: " + severity);
-        System.out.println("Line: " + line);
-        System.out.println("Col: " + column);
-        System.out.println(message);
+        PrintStream out = severity == SEVERITY_ERROR ? System.err : System.out;
+        out.println(formatMessage(severity, file, line, column, message, 
cause));
+    }
+
+    static String formatMessage(int severity, File file, int line, int column, 
String message, Throwable cause) {
+        StringBuilder text = new StringBuilder();
+        if (message != null) {
+            text.append(message);
+        }
         if (cause != null) {
-            cause.printStackTrace(System.out);
+            StringWriter trace = new StringWriter();
+            cause.printStackTrace(new PrintWriter(trace));
+            text.append('\n').append(trace);
         }
-        System.out.println();
-        System.out.println("DONE");
+        return MESSAGE_PREFIX + severity + '\t' + line + '\t' + column + '\t'
+            + escape(file == null ? "" : file.getAbsolutePath()) + '\t' + 
escape(text.toString());
     }
+
+    /**
+     * Parses a line written by {@link #formatMessage}, returning null for any 
other line.
+     */
+    static Message parseMessage(String line) {
+        if (line == null || !line.startsWith(MESSAGE_PREFIX)) {
+            return null;
+        }
+        String[] fields = line.substring(MESSAGE_PREFIX.length()).split("\t", 
-1);
+        if (fields.length != 5) {
+            return null;
+        }
+        String path = unescape(fields[3]);
+        return new Message(parseInt(fields[0]), parseInt(fields[1]), 
parseInt(fields[2]),
+                           path.isEmpty() ? null : new File(path), 
unescape(fields[4]));
+    }
+
+    private static int parseInt(String s) {
+        try {
+            return Integer.parseInt(s);
+        } catch (NumberFormatException e) {
+            return 0;
+        }
+    }
+
+    // escape the characters that separate lines and fields
+    private static String escape(String s) {
+        StringBuilder b = new StringBuilder(s.length());
+        for (int i = 0; i < s.length(); i++) {
+            char c = s.charAt(i);
+            switch (c) {
+            case '\\':
+                b.append("\\\\");
+                break;
+            case '\n':
+                b.append("\\n");
+                break;
+            case '\r':
+                b.append("\\r");
+                break;
+            case '\t':
+                b.append("\\t");
+                break;
+            default:
+                b.append(c);
+            }
+        }
+        return b.toString();
+    }
+
+    private static String unescape(String s) {
+        StringBuilder b = new StringBuilder(s.length());
+        for (int i = 0; i < s.length(); i++) {
+            char c = s.charAt(i);
+            if (c == '\\' && i + 1 < s.length()) {
+                char n = s.charAt(++i);
+                switch (n) {
+                case 'n':
+                    b.append('\n');
+                    break;
+                case 'r':
+                    b.append('\r');
+                    break;
+                case 't':
+                    b.append('\t');
+                    break;
+                default:
+                    b.append(n);
+                }
+            } else {
+                b.append(c);
+            }
+        }
+        return b.toString();
+    }
+
     public void removeMessages(File file) {
     }
     public boolean isUptodate(File target, File source) {
diff --git 
a/cxf-xjc-plugin/src/test/java/org/apache/cxf/maven_plugin/ForkedOutputRoundTripTest.java
 
b/cxf-xjc-plugin/src/test/java/org/apache/cxf/maven_plugin/ForkedOutputRoundTripTest.java
new file mode 100644
index 0000000..777b455
--- /dev/null
+++ 
b/cxf-xjc-plugin/src/test/java/org/apache/cxf/maven_plugin/ForkedOutputRoundTripTest.java
@@ -0,0 +1,170 @@
+/**
+ * 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.cxf.maven_plugin;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.maven.plugin.logging.SystemStreamLog;
+import org.sonatype.plexus.build.incremental.BuildContext;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+/**
+ * Runs XJC as the forked process does and passes its output through the 
parsing used by
+ * AbstractXSDToJavaMojo, checking the messages that reach the build context.
+ */
+public class ForkedOutputRoundTripTest {
+
+    @Rule
+    public TemporaryFolder tmp = new TemporaryFolder();
+
+    static class Recorded {
+        final File file;
+        final int line;
+        final int column;
+        final String message;
+        final int severity;
+
+        Recorded(File file, int line, int column, String message, int 
severity) {
+            this.file = file;
+            this.line = line;
+            this.column = column;
+            this.message = message;
+            this.severity = severity;
+        }
+    }
+
+    static class RecordingBuildContext extends XJCBuildContext {
+        final List<Recorded> messages = new ArrayList<>();
+
+        @Override
+        public void addMessage(File file, int line, int column, String 
message, int severity, Throwable cause) {
+            messages.add(new Recorded(file, line, column, message, severity));
+        }
+    }
+
+    @Test
+    public void testErrors() throws Exception {
+        File xsd = schema("errors.xsd",
+            "  <xs:complexType name='a'><xs:sequence><xs:element name='v' 
type='t:missing1'/>"
+            + "</xs:sequence></xs:complexType>\n"
+            + "  <xs:complexType name='b'><xs:sequence><xs:element name='w' 
type='t:missing2'/>"
+            + "</xs:sequence></xs:complexType>\n");
+        List<Recorded> messages = run(xsd);
+
+        List<Recorded> resolveErrors = new ArrayList<>();
+        for (Recorded m : messages) {
+            Assert.assertEquals(xsd.getAbsolutePath(), 
m.file.getAbsolutePath());
+            Assert.assertEquals(BuildContext.SEVERITY_ERROR, m.severity);
+            if (m.message.contains("src-resolve")) {
+                resolveErrors.add(m);
+            }
+        }
+        Assert.assertEquals(2, resolveErrors.size());
+        Assert.assertTrue(resolveErrors.get(0).message, 
resolveErrors.get(0).message.contains("t:missing1"));
+        Assert.assertEquals(2, resolveErrors.get(0).line);
+        Assert.assertTrue(resolveErrors.get(1).message, 
resolveErrors.get(1).message.contains("t:missing2"));
+        Assert.assertEquals(3, resolveErrors.get(1).line);
+        Assert.assertTrue(resolveErrors.get(0).column > 0);
+    }
+
+    @Test
+    public void testWarning() throws Exception {
+        // more values than XJC maps to an enum by default
+        StringBuilder values = new StringBuilder();
+        for (int i = 0; i < 300; i++) {
+            values.append("<xs:enumeration value='v").append(i).append("'/>");
+        }
+        File xsd = schema("warning.xsd",
+            "  <xs:simpleType name='big'><xs:restriction base='xs:string'>" + 
values
+            + "</xs:restriction></xs:simpleType>\n");
+        List<Recorded> messages = run(xsd);
+
+        Assert.assertEquals(1, messages.size());
+        Assert.assertEquals(xsd.getAbsolutePath(), 
messages.get(0).file.getAbsolutePath());
+        Assert.assertEquals(BuildContext.SEVERITY_WARNING, 
messages.get(0).severity);
+        Assert.assertTrue(messages.get(0).message, 
messages.get(0).message.contains("big"));
+    }
+
+    @Test
+    public void testSchemaTextCannotForgeMessages() throws Exception {
+        // a default value is quoted, line breaks included, in the error for 
its invalid length
+        String forged = "&#10;" + XJCBuildContext.MESSAGE_PREFIX.replace("\t", 
"&#9;")
+            + "2&#9;7&#9;7&#9;/etc/forged.xsd&#9;forged&#10;MSG: 
/etc/forged.xsd&#10;Col: junk&#10;DONE";
+        File xsd = schema("forged.xsd",
+            "  <xs:element name='e' default='ab" + forged + "'><xs:simpleType>"
+            + "<xs:restriction base='xs:string'><xs:maxLength 
value='1'/></xs:restriction>"
+            + "</xs:simpleType></xs:element>\n");
+        List<Recorded> messages = run(xsd);
+
+        Assert.assertFalse(messages.isEmpty());
+        boolean quoted = false;
+        for (Recorded m : messages) {
+            Assert.assertEquals(xsd.getAbsolutePath(), 
m.file.getAbsolutePath());
+            quoted |= m.message.contains("MSG: /etc/forged.xsd\nCol: junk");
+        }
+        Assert.assertTrue("schema text should only appear inside the genuine 
message", quoted);
+    }
+
+    private File schema(String name, String content) throws Exception {
+        File xsd = new File(tmp.getRoot(), name);
+        String schema = "<xs:schema 
xmlns:xs='http://www.w3.org/2001/XMLSchema' xmlns:t='urn:t'"
+            + " targetNamespace='urn:t'>\n" + content + "</xs:schema>\n";
+        Files.write(xsd.toPath(), schema.getBytes(StandardCharsets.UTF_8));
+        return xsd;
+    }
+
+    // run XJC with the forked process's build context, then parse its output 
as the mojo does
+    private List<Recorded> run(File xsd) throws Exception {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        ByteArrayOutputStream err = new ByteArrayOutputStream();
+        PrintStream origOut = System.out;
+        PrintStream origErr = System.err;
+        try {
+            System.setOut(new PrintStream(out, true, "UTF-8"));
+            System.setErr(new PrintStream(err, true, "UTF-8"));
+            XJCErrorListener listener = new XJCErrorListener(new 
XJCBuildContext());
+            new XSDToJavaRunner(new String[] {"-d", 
tmp.newFolder().getAbsolutePath(), xsd.toURI().toString()},
+                                listener, xsd, 
Collections.<String>emptyList()).run();
+        } finally {
+            System.setOut(origOut);
+            System.setErr(origErr);
+        }
+
+        RecordingBuildContext context = new RecordingBuildContext();
+        ForkedOutputConsumer consumer = new ForkedOutputConsumer(context, new 
SystemStreamLog());
+        for (ByteArrayOutputStream stream : new ByteArrayOutputStream[] {out, 
err}) {
+            for (String line : new String(stream.toByteArray(), 
StandardCharsets.UTF_8).split("\r?\n")) {
+                consumer.consumeLine(line);
+            }
+        }
+        return context.messages;
+    }
+}
diff --git 
a/cxf-xjc-plugin/src/test/java/org/apache/cxf/maven_plugin/XJCBuildContextTest.java
 
b/cxf-xjc-plugin/src/test/java/org/apache/cxf/maven_plugin/XJCBuildContextTest.java
new file mode 100644
index 0000000..d6b6512
--- /dev/null
+++ 
b/cxf-xjc-plugin/src/test/java/org/apache/cxf/maven_plugin/XJCBuildContextTest.java
@@ -0,0 +1,76 @@
+/**
+ * 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.cxf.maven_plugin;
+
+import java.io.File;
+
+import org.sonatype.plexus.build.incremental.BuildContext;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class XJCBuildContextTest {
+
+    @Test
+    public void testRoundTrip() {
+        File file = new File("/tmp/a\tb\nc.xsd").getAbsoluteFile();
+        String line = 
XJCBuildContext.formatMessage(BuildContext.SEVERITY_WARNING, file, 3, 7,
+                                                    "first\r\nsecond\\third", 
null);
+        XJCBuildContext.Message msg = XJCBuildContext.parseMessage(line);
+        Assert.assertEquals(BuildContext.SEVERITY_WARNING, msg.severity);
+        Assert.assertEquals(3, msg.line);
+        Assert.assertEquals(7, msg.column);
+        Assert.assertEquals(file.getAbsolutePath(), msg.file.getPath());
+        Assert.assertEquals("first\r\nsecond\\third", msg.text);
+    }
+
+    @Test
+    public void testMessageCannotForgeOtherMessages() {
+        // e.g. text from a schema quoted in an error message
+        String forged = "value 'ab\n" + XJCBuildContext.MESSAGE_PREFIX + 
"2\t1\t1\t/etc/passwd\tforged\n"
+            + "MSG: /etc/passwd\nCol: junk\nDONE'";
+        String line = 
XJCBuildContext.formatMessage(BuildContext.SEVERITY_ERROR, new File("a.xsd"), 
1, 2,
+                                                    forged, new 
IllegalStateException("cause"));
+        Assert.assertFalse(line, line.contains("\n") || line.contains("\r"));
+        XJCBuildContext.Message msg = XJCBuildContext.parseMessage(line);
+        Assert.assertEquals(new File("a.xsd").getAbsolutePath(), 
msg.file.getPath());
+        Assert.assertTrue(msg.text, msg.text.startsWith(forged + "\n"));
+        Assert.assertTrue(msg.text, msg.text.contains("IllegalStateException: 
cause"));
+    }
+
+    @Test
+    public void testOtherLinesAreIgnored() {
+        Assert.assertNull(XJCBuildContext.parseMessage("parsing a schema..."));
+        Assert.assertNull(XJCBuildContext.parseMessage("MSG: /etc/passwd"));
+        
Assert.assertNull(XJCBuildContext.parseMessage(XJCBuildContext.MESSAGE_PREFIX + 
"too\tfew"));
+        Assert.assertNull(XJCBuildContext.parseMessage(null));
+    }
+
+    @Test
+    public void testInvalidNumbers() {
+        XJCBuildContext.Message msg =
+            XJCBuildContext.parseMessage(XJCBuildContext.MESSAGE_PREFIX + 
"x\tjunk\t\t\ttext");
+        Assert.assertEquals(0, msg.severity);
+        Assert.assertEquals(0, msg.line);
+        Assert.assertEquals(0, msg.column);
+        Assert.assertNull(msg.file);
+        Assert.assertEquals("text", msg.text);
+    }
+}

Reply via email to