diff --git a/subprojects/groovy-xml/src/main/java/groovy/xml/streamingmarkupsupport/StreamingMarkupWriter.java b/subprojects/groovy-xml/src/main/java/groovy/xml/streamingmarkupsupport/StreamingMarkupWriter.java index a2944f08186..4b0ed4e40d2 100644 --- a/subprojects/groovy-xml/src/main/java/groovy/xml/streamingmarkupsupport/StreamingMarkupWriter.java +++ b/subprojects/groovy-xml/src/main/java/groovy/xml/streamingmarkupsupport/StreamingMarkupWriter.java @@ -188,6 +188,10 @@ public void flush() throws IOException { public void write(final int c) throws IOException { if (c >= 0XDC00 && c <= 0XDFFF) { // Low surrogate + if (!this.haveHighSurrogate) { + this.surrogatePair.setLength(0); + throw new IOException("Low Surrogate not preceded by High Surrogate"); + } this.surrogatePair.append((char) c); if (this.encoder.canEncode(this.surrogatePair)) { diff --git a/subprojects/groovy-xml/src/test/groovy/groovy/xml/StreamingMarkupWriterTest.groovy b/subprojects/groovy-xml/src/test/groovy/groovy/xml/StreamingMarkupWriterTest.groovy new file mode 100644 index 00000000000..f03fa36eebc --- /dev/null +++ b/subprojects/groovy-xml/src/test/groovy/groovy/xml/StreamingMarkupWriterTest.groovy @@ -0,0 +1,53 @@ +/* + * 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 groovy.xml + +import groovy.xml.streamingmarkupsupport.StreamingMarkupWriter +import org.junit.jupiter.api.Test + +import static org.junit.jupiter.api.Assertions.assertEquals +import static org.junit.jupiter.api.Assertions.assertThrows + +class StreamingMarkupWriterTest { + + // A well-formed surrogate pair (U+1D400) is encoded as a single numeric reference. + @Test + void validSurrogatePairIsEncoded() { + def sw = new StringWriter() + def w = new StreamingMarkupWriter(sw) + w.write('𝐀') + w.flush() + assertEquals('𝐀', sw.toString()) + } + + // A low surrogate with no preceding high surrogate is malformed UTF-16 and must be + // rejected, mirroring the existing rejection of a high surrogate not followed by a low one. + @Test + void loneLowSurrogateIsRejected() { + def w = new StreamingMarkupWriter(new StringWriter()) + assertThrows(IOException) { w.write('\uDC00') } + } + + // Existing behavior: a high surrogate not followed by a low surrogate is rejected. + @Test + void loneHighSurrogateIsRejected() { + def w = new StreamingMarkupWriter(new StringWriter()) + assertThrows(IOException) { w.write('\uD835' + 'A') } + } +}