Merge branch '4.1.x'

Closes gh-51586
This commit is contained in:
Andy Wilkinson
2026-09-04 14:54:05 +01:00
3 changed files with 49 additions and 3 deletions
@@ -56,6 +56,8 @@ class AppendableByteArray implements Appendable {
private ByteBuffer out;
private char highSurrogate;
AppendableByteArray(Charset charset) {
this(charset, DEFAULT_INITIAL_SIZE, DEFAULT_EXPANSION_SIZE);
}
@@ -89,8 +91,19 @@ class AppendableByteArray implements Appendable {
}
private AppendableByteArray append(CharBuffer in) throws IOException {
CoderResult result = this.encoder.encode(in, this.out, false);
if (this.highSurrogate != 0) {
CharBuffer pending = CharBuffer.allocate(in.remaining() + 1);
pending.put(this.highSurrogate).put(in).flip();
this.highSurrogate = 0;
in = pending;
}
return append(in, false);
}
private AppendableByteArray append(CharBuffer in, boolean endOfInput) throws IOException {
CoderResult result = this.encoder.encode(in, this.out, endOfInput);
if (result.isUnderflow()) {
this.highSurrogate = (in.hasRemaining()) ? in.get() : 0;
return this;
}
if (result.isOverflow()) {
@@ -98,13 +111,18 @@ class AppendableByteArray implements Appendable {
this.out = ByteBuffer.allocate(out.capacity() + this.expansionSize);
out.flip();
this.out.put(out);
return append(in);
return append(in, endOfInput);
}
result.throwException();
return this;
}
byte[] toByteArray() {
byte[] toByteArray() throws IOException {
if (this.highSurrogate != 0) {
CharBuffer in = CharBuffer.wrap(new char[] { this.highSurrogate });
this.highSurrogate = 0;
append(in, true);
}
this.out.flip();
int limit = this.out.limit();
int position = this.out.position();
@@ -120,6 +138,7 @@ class AppendableByteArray implements Appendable {
private void reset() {
this.out.clear();
this.encoder.reset();
this.highSurrogate = 0;
}
static byte[] toByteArray(Charset charset, ThrowingConsumer<Appendable> appendable) throws IOException {
@@ -90,6 +90,22 @@ class AppendableByteArrayTests {
assertThat(reused).isEqualTo("clean".getBytes(StandardCharsets.UTF_8));
}
@Test
void writesSurrogatePairAppendedAsIndividualChars() throws Exception {
assertByteArray(StandardCharsets.UTF_8, (appendable) -> appendable.append('\uD83D').append('\uDE00'));
assertByteArray(StandardCharsets.UTF_16, (appendable) -> appendable.append('\uD83D').append('\uDE00'));
}
@Test
void writesSurrogatePairSplitAcrossAppendedStrings() throws Exception {
assertByteArray(StandardCharsets.UTF_8, (appendable) -> appendable.append("a\uD83D").append("\uDE00b"));
}
@Test
void writesUnpairedHighSurrogate() throws Exception {
assertByteArray(StandardCharsets.UTF_8, (appendable) -> appendable.append('\uD83D'));
}
private void assertByteArray(Charset charset, ThrowingConsumer<Appendable> action) throws Exception {
assertByteArray(4, 4, charset, action);
}
@@ -66,6 +66,17 @@ class WritableJsonTests {
assertThat(writable.toByteArray()).isEqualTo("{}".getBytes());
}
@Test
void toByteArrayWhenContentIsAppendedCharByCharWritesSupplementaryCharacters() {
String emoji = new String(Character.toChars(0x1F600));
WritableJson writable = (out) -> {
for (int i = 0; i < emoji.length(); i++) {
out.append(emoji.charAt(i));
}
};
assertThat(writable.toByteArray(StandardCharsets.UTF_8)).isEqualTo(emoji.getBytes(StandardCharsets.UTF_8));
}
@Test
void toResourceWritesJson() throws Exception {
File file = new File(this.temp, "out.json");