Author: msahyoun
Date: Wed Aug 26 07:38:25 2026
New Revision: 1937470
Log:
PDFBOX-6242: properly escape characters for XML 1.0; parts by Claude Sonnet
Added:
pdfbox/branches/2.0/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/fdf/FDFUtilsTest.java
Modified:
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/fdf/FDFUtils.java
Modified:
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/fdf/FDFUtils.java
==============================================================================
---
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/fdf/FDFUtils.java
Wed Aug 26 07:31:20 2026 (r1937469)
+++
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/fdf/FDFUtils.java
Wed Aug 26 07:38:25 2026 (r1937470)
@@ -16,8 +16,13 @@
*/
package org.apache.pdfbox.pdmodel.fdf;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
public class FDFUtils {
+ private static final Log LOG = LogFactory.getLog(FDFUtils.class);
+
/**
* Escape special characters.
*
@@ -28,10 +33,22 @@ public class FDFUtils {
static String escapeXML10(String input)
{
StringBuilder escapedXML = new StringBuilder();
- for (int i = 0; i < input.length(); i++)
+ int invalidCount = 0;
+ int i = 0;
+ while (i < input.length())
{
- char c = input.charAt(i);
- switch (c)
+ int cp = input.codePointAt(i);
+ int charCount = Character.charCount(cp);
+
+ if (!isValidXML10Char(cp))
+ {
+ invalidCount++;
+ escapedXML.append('\uFFFD');
+ i += charCount;
+ continue;
+ }
+
+ switch (cp)
{
case '<':
escapedXML.append("<");
@@ -49,16 +66,32 @@ public class FDFUtils {
escapedXML.append("'");
break;
default:
- if (c > 0x7e)
+ if (cp > 0x7e)
{
- escapedXML.append("&#").append((int) c).append(';');
+ escapedXML.append("&#").append(cp).append(';');
}
else
{
- escapedXML.append(c);
+ escapedXML.appendCodePoint(cp);
}
}
+ i += charCount;
+ }
+
+ if (invalidCount > 0 && LOG.isInfoEnabled())
+ {
+ LOG.info("Replaced " + invalidCount + " character(s) invalid in
XML 1.0 with U+FFFD");
}
+
+
return escapedXML.toString();
}
+
+ private static boolean isValidXML10Char(int cp)
+ {
+ return cp == 0x9 || cp == 0xA || cp == 0xD
+ || (cp >= 0x20 && cp <= 0xD7FF)
+ || (cp >= 0xE000 && cp <= 0xFFFD)
+ || (cp >= 0x10000 && cp <= 0x10FFFF);
+ }
}
Added:
pdfbox/branches/2.0/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/fdf/FDFUtilsTest.java
==============================================================================
--- /dev/null 00:00:00 1970 (empty, because file is newly added)
+++
pdfbox/branches/2.0/pdfbox/src/test/java/org/apache/pdfbox/pdmodel/fdf/FDFUtilsTest.java
Wed Aug 26 07:38:25 2026 (r1937470)
@@ -0,0 +1,123 @@
+/*
+ * 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.pdfbox.pdmodel.fdf;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import org.junit.Test;
+/*
+ * Test the XML 1.0 escaping performed by FDFUtils.escapeXML10().
+ */
+public class FDFUtilsTest
+{
+ @Test
+ public void testPlainAsciiIsUnchanged()
+ {
+ String input = "Hello World 123";
+ assertEquals(input, FDFUtils.escapeXML10(input));
+ }
+
+ @Test
+ public void testEmptyStringReturnsEmptyString()
+ {
+ assertEquals("", FDFUtils.escapeXML10(""));
+ }
+
+ @Test
+ public void testEscapesLessThanSign()
+ {
+ assertEquals("<", FDFUtils.escapeXML10("<"));
+ }
+
+ @Test
+ public void testEscapesGreaterThanSign()
+ {
+ assertEquals(">", FDFUtils.escapeXML10(">"));
+ }
+
+ @Test
+ public void testEscapesAmpersand()
+ {
+ assertEquals("&", FDFUtils.escapeXML10("&"));
+ }
+
+ @Test
+ public void testEscapesDoubleQuote()
+ {
+ assertEquals(""", FDFUtils.escapeXML10("\""));
+ }
+
+ @Test
+ public void testEscapesSingleQuote()
+ {
+ assertEquals("'", FDFUtils.escapeXML10("'"));
+ }
+
+ @Test
+ public void testEscapesAllSpecialCharactersInOneString()
+ {
+ String input = "<tag attr=\"value\" other='x'>&</tag>";
+ String expected = "<tag attr="value"
other='x'>&</tag>";
+ assertEquals(expected, FDFUtils.escapeXML10(input));
+ }
+
+ @Test
+ public void testLegalWhitespaceControlCharactersPassThroughUnescaped()
+ {
+ // Tab (0x9), line feed (0xA) and carriage return (0xD) are explicitly
+ // legal XML 1.0 characters and are not part of the escaped set.
+ String input = "line1\tline2\nline3\rline4";
+ assertEquals(input, FDFUtils.escapeXML10(input));
+ }
+
+ @Test
+ public void testNonAsciiBmpCharacterIsEscapedAsNumericReference()
+ {
+ // 'é' is U+00E9 (233 decimal)
+ assertEquals("café", FDFUtils.escapeXML10("caf\u00e9"));
+ }
+
+ @Test
+ public void testMultipleNonAsciiCharactersAreEachEscaped()
+ {
+ // '\u00e9' = 233, '\u00e8' = 232
+ assertEquals("éè", FDFUtils.escapeXML10("\u00e9\u00e8"));
+ }
+
+ @Test
+ public void testIllegalControlCharacterIsNotPassedThroughRaw()
+ {
+ // 0x0B (vertical tab) is not a legal XML 1.0 character and must not
+ // appear unescaped in the output.
+ String result = FDFUtils.escapeXML10("a\u000bb");
+ assertFalse("Illegal control character must not appear raw in escaped
output",
+ result.indexOf('\u000b') >= 0);
+ assertEquals("a\ufffdb", result);
+ }
+
+ @Test
+ public void testSupplementaryCharacterProducesSingleValidReference()
+ {
+ // U+1F600 (GRINNING FACE) is represented in Java as a surrogate pair.
+ // It must be escaped as a single reference to its code point (128512),
+ // not as two references to the individual (illegal) surrogate values.
+ String input = new String(Character.toChars(0x1F600));
+ assertEquals("😀", FDFUtils.escapeXML10(input));
+ }
+}
\ No newline at end of file