diff --git a/release-notes/CREDITS b/release-notes/CREDITS index a898cf21..f6481e84 100644 --- a/release-notes/CREDITS +++ b/release-notes/CREDITS @@ -168,3 +168,5 @@ Christian Beikov (@beikov) (3.3.0) * Fixed #883: Translate `XMLStreamReader` creation errors to `StreamReadException` (3.3.0) + * Fixed #886: Escape names already starting with prefix in `Base64NameProcessor` + (3.3.0) diff --git a/release-notes/VERSION b/release-notes/VERSION index 082c59c2..1f878a56 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -18,6 +18,8 @@ Version: 3.x (for earlier see VERSION-2.x) #883: Translate `XMLStreamReader` creation errors to `StreamReadException` (restores guard lost in 3.x port; see 2.x `_createParser(byte[])` for #618) (fix by @Sahana2524) +#886: Escape names already starting with prefix in `Base64NameProcessor` + (fix by @Sahana2524) 3.2.2 (not yet released) diff --git a/src/main/java/tools/jackson/dataformat/xml/XmlNameProcessors.java b/src/main/java/tools/jackson/dataformat/xml/XmlNameProcessors.java index e13a2b41..1abef18e 100644 --- a/src/main/java/tools/jackson/dataformat/xml/XmlNameProcessors.java +++ b/src/main/java/tools/jackson/dataformat/xml/XmlNameProcessors.java @@ -118,8 +118,9 @@ public static XmlNameProcessor newReplacementProcessor() { * * } *

- * NOTE: you must ensure that no incoming element or attribute name starts - * with {@code prefix}, otherwise decoding will not work. + * NOTE: names that already start with {@code prefix} are escaped as well, even + * though they are otherwise valid, so that decoding cannot confuse them with + * names this processor encoded. * * @param prefix The prefix to use for name that are escaped */ @@ -197,7 +198,12 @@ public Base64NameProcessor(String prefix) { @Override public void encodeName(XmlName name) { - if (!VALID_XML_NAME.matcher(name.localPart).matches()) { + // Names already starting with the prefix have to be escaped as well, even + // when otherwise valid: decodeName() base64-decodes anything carrying the + // prefix, so passing such a name through as-is would decode it into a + // different name than was written. + if (!VALID_XML_NAME.matcher(name.localPart).matches() + || name.localPart.startsWith(_prefix)) { name.localPart = _prefix + new String(BASE64_ENCODER.encode(name.localPart.getBytes(UTF_8)), UTF_8); } } diff --git a/src/test/java/tools/jackson/dataformat/xml/misc/XmlNameEscapeTest.java b/src/test/java/tools/jackson/dataformat/xml/misc/XmlNameEscapeTest.java index e47f313d..cd442a44 100644 --- a/src/test/java/tools/jackson/dataformat/xml/misc/XmlNameEscapeTest.java +++ b/src/test/java/tools/jackson/dataformat/xml/misc/XmlNameEscapeTest.java @@ -154,6 +154,42 @@ public void testAlwaysOnBase64UndecodableNameFailsAsReadException() throws Excep assertNotEquals(TokenStreamLocation.NA, e.getLocation()); } + // A name that is a valid XML name but already starts with the magic prefix has + // to be escaped too: decoding keys off that prefix, so writing such a name + // through as-is makes it read back as a different name than was written. + @Test + public void testBase64PrefixedNamesRoundTrip() throws Exception { + DTO dto = new DTO(); + + // would otherwise read back as "admin" + dto.badMap.put("base64_tag_YWRtaW4", "xyz"); + // valid XML name, but not valid base64 past the prefix + dto.badMap.put("base64_tag_hello", "bar"); + dto.badMap.put("plain", "abc"); + + XmlMapper mapper = XmlMapper.builder( + xmlFactory(XmlNameProcessors.newBase64Processor()) + ).build(); + + final String res = mapper.writeValueAsString(dto); + DTO reversed = mapper.readValue(res, DTO.class); + assertEquals(dto, reversed); + } + + @Test + public void testBase64CustomPrefixedNameRoundTrips() throws Exception { + DTO dto = new DTO(); + dto.badMap.put("esc_YWRtaW4", "xyz"); + + XmlMapper mapper = XmlMapper.builder( + xmlFactory(XmlNameProcessors.newBase64Processor("esc_")) + ).build(); + + final String res = mapper.writeValueAsString(dto); + DTO reversed = mapper.readValue(res, DTO.class); + assertEquals(dto, reversed); + } + protected XmlFactory xmlFactory(XmlNameProcessor proc) { return XmlFactory.builder().xmlNameProcessor(proc).build(); }