mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-09-17 12:09:16 +00:00
Fix corruption of supplementary characters in AppendableByteArray
A high surrogate left unconsumed by the encoder was dropped between append calls, so surrogate pairs written a character at a time by JsonValueWriter were replaced with `?`. See gh-51464 Signed-off-by: JaeHyunAn <98042706+yyuneu@users.noreply.github.com>
This commit is contained in:
committed by
Andy Wilkinson
parent
61a62d56d8
commit
87cadf2c9c
+22
-3
@@ -56,6 +56,8 @@ class AppendableByteArray implements Appendable {
|
|||||||
|
|
||||||
private ByteBuffer out;
|
private ByteBuffer out;
|
||||||
|
|
||||||
|
private char highSurrogate;
|
||||||
|
|
||||||
AppendableByteArray(Charset charset) {
|
AppendableByteArray(Charset charset) {
|
||||||
this(charset, DEFAULT_INITIAL_SIZE, DEFAULT_EXPANSION_SIZE);
|
this(charset, DEFAULT_INITIAL_SIZE, DEFAULT_EXPANSION_SIZE);
|
||||||
}
|
}
|
||||||
@@ -89,8 +91,19 @@ class AppendableByteArray implements Appendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private AppendableByteArray append(CharBuffer in) throws IOException {
|
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()) {
|
if (result.isUnderflow()) {
|
||||||
|
this.highSurrogate = (in.hasRemaining()) ? in.get() : 0;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
if (result.isOverflow()) {
|
if (result.isOverflow()) {
|
||||||
@@ -98,13 +111,18 @@ class AppendableByteArray implements Appendable {
|
|||||||
this.out = ByteBuffer.allocate(out.capacity() + this.expansionSize);
|
this.out = ByteBuffer.allocate(out.capacity() + this.expansionSize);
|
||||||
out.flip();
|
out.flip();
|
||||||
this.out.put(out);
|
this.out.put(out);
|
||||||
return append(in);
|
return append(in, endOfInput);
|
||||||
}
|
}
|
||||||
result.throwException();
|
result.throwException();
|
||||||
return this;
|
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();
|
this.out.flip();
|
||||||
int limit = this.out.limit();
|
int limit = this.out.limit();
|
||||||
int position = this.out.position();
|
int position = this.out.position();
|
||||||
@@ -120,6 +138,7 @@ class AppendableByteArray implements Appendable {
|
|||||||
private void reset() {
|
private void reset() {
|
||||||
this.out.clear();
|
this.out.clear();
|
||||||
this.encoder.reset();
|
this.encoder.reset();
|
||||||
|
this.highSurrogate = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static byte[] toByteArray(Charset charset, ThrowingConsumer<Appendable> appendable) throws IOException {
|
static byte[] toByteArray(Charset charset, ThrowingConsumer<Appendable> appendable) throws IOException {
|
||||||
|
|||||||
+16
@@ -90,6 +90,22 @@ class AppendableByteArrayTests {
|
|||||||
assertThat(reused).isEqualTo("clean".getBytes(StandardCharsets.UTF_8));
|
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 {
|
private void assertByteArray(Charset charset, ThrowingConsumer<Appendable> action) throws Exception {
|
||||||
assertByteArray(4, 4, charset, action);
|
assertByteArray(4, 4, charset, action);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,17 @@ class WritableJsonTests {
|
|||||||
assertThat(writable.toByteArray()).isEqualTo("{}".getBytes());
|
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
|
@Test
|
||||||
void toResourceWritesJson() throws Exception {
|
void toResourceWritesJson() throws Exception {
|
||||||
File file = new File(this.temp, "out.json");
|
File file = new File(this.temp, "out.json");
|
||||||
|
|||||||
Reference in New Issue
Block a user