Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions src/main/java/tools/jackson/dataformat/xml/XmlNameProcessors.java
Original file line number Diff line number Diff line change
Expand Up @@ -229,16 +229,38 @@ static class AlwaysOnBase64NameProcessor implements XmlNameProcessor {
private static final Base64.Decoder BASE64_DECODER = Base64.getUrlDecoder();
private static final Base64.Encoder BASE64_ENCODER = Base64.getUrlEncoder().withoutPadding();

// Marker used to restore a valid XML name start character; see encodeName().
private static final char START_MARKER = '_';

public AlwaysOnBase64NameProcessor() { }

@Override
public void encodeName(XmlName name) {
name.localPart = new String(BASE64_ENCODER.encode(name.localPart.getBytes(UTF_8)), UTF_8);
String encoded = new String(BASE64_ENCODER.encode(name.localPart.getBytes(UTF_8)), UTF_8);
// base64url's alphabet contains digits and '-', but neither can begin an
// XML name (only letters, '_' and ':' are NameStartChars). A name whose
// first character is U+0400 or above encodes to a leading digit, which
// produces an invalid element/attribute name and breaks the round trip
// this processor is meant to guarantee. A base64url encoding of UTF-8 bytes
// never itself begins with '_', so prepending one restores a valid start
// character without making decoding ambiguous.
if (!encoded.isEmpty() && !_isNameStartChar(encoded.charAt(0))) {
encoded = START_MARKER + encoded;
}
name.localPart = encoded;
}

@Override
public void decodeName(XmlName name) {
name.localPart = new String(BASE64_DECODER.decode(name.localPart), UTF_8);
String localName = name.localPart;
if (!localName.isEmpty() && localName.charAt(0) == START_MARKER) {
localName = localName.substring(1);
}
name.localPart = new String(BASE64_DECODER.decode(localName), UTF_8);
}

private static boolean _isNameStartChar(char c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_';
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import tools.jackson.dataformat.xml.*;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
Expand Down Expand Up @@ -92,6 +93,31 @@ public void testAlwaysOnBase64() throws Exception {
assertEquals(dto, reversed);
}

// base64url's alphabet includes digits, but a digit can not start an XML name.
// Names whose first character is U+0400 or above encode to a leading digit, so
// the "always on" processor has to keep the encoded name a valid NameStartChar
// and still round-trip.
@Test
public void testAlwaysOnBase64NonAsciiKeysRoundTrip() throws Exception {
DTO dto = new DTO();
// U+4E2D U+6587 (Chinese) encodes to a name starting with a digit
dto.badMap.put(new String(new int[] { 0x4E2D, 0x6587 }, 0, 2), "cjk");
// U+043F U+0440 U+0438 U+0432 (Cyrillic)
dto.badMap.put(new String(new int[] { 0x43F, 0x440, 0x438, 0x432 }, 0, 4), "cyrillic");
dto.badMap.put("abc", "ascii"); // starts with a letter, unchanged

XmlMapper mapper = XmlMapper.builder(
xmlFactory(XmlNameProcessors.newAlwaysOnBase64Processor())
).build();

final String res = mapper.writeValueAsString(dto);
// no encoded element/attribute name may start with a digit
assertFalse(res.matches("(?s).*<[0-9].*"), res);

DTO reversed = mapper.readValue(res, DTO.class);
assertEquals(dto, reversed);
}

@Test
public void testReplace() throws Exception {
DTO dto = new DTO();
Expand Down
Loading