|
| 1 | +package com.fasterxml.jackson.dataformat.cbor; |
| 2 | + |
| 3 | +import java.io.ByteArrayInputStream; |
| 4 | +import java.io.ByteArrayOutputStream; |
| 5 | + |
| 6 | +import org.junit.jupiter.api.Test; |
| 7 | + |
| 8 | +import com.fasterxml.jackson.core.JsonParser; |
| 9 | +import com.fasterxml.jackson.core.JsonToken; |
| 10 | +import com.fasterxml.jackson.core.io.SerializedString; |
| 11 | + |
| 12 | +import static org.junit.jupiter.api.Assertions.assertEquals; |
| 13 | +import static org.junit.jupiter.api.Assertions.assertNull; |
| 14 | + |
| 15 | +/** |
| 16 | + * Tests for [dataformats-binary#733]: Strings decoded by |
| 17 | + * {@code CBORParser._finishLongText()} -- that is, Strings that do not fit |
| 18 | + * in the input buffer, only possible when reading from an {@link java.io.InputStream} |
| 19 | + * -- were not added to the String reference table. Since reference indexes are |
| 20 | + * positional, that made the parser's table disagree with the encoder's, so that |
| 21 | + * following references either failed with "String reference out of range" or, |
| 22 | + * worse, silently resolved to the wrong String. |
| 23 | + */ |
| 24 | +public class StringRef733Test extends CBORTestBase |
| 25 | +{ |
| 26 | + // Longer than the default 8000 byte input buffer, to force use of |
| 27 | + // `_finishLongText()` when reading from an `InputStream` |
| 28 | + private final static int LONG_LENGTH = 9000; |
| 29 | + |
| 30 | + /* |
| 31 | + /********************************************************** |
| 32 | + /* Test methods, hand-crafted documents |
| 33 | + /********************************************************** |
| 34 | + */ |
| 35 | + |
| 36 | + // Long String is entry #0, so reference #1 must resolve to "AAA" |
| 37 | + @Test |
| 38 | + public void testLongStringDoesNotShiftFollowingRefs() throws Exception |
| 39 | + { |
| 40 | + final String longString = _generateAscii(LONG_LENGTH); |
| 41 | + final byte[] doc = _stringRefDoc(longString, 1); |
| 42 | + |
| 43 | + _verifyRefResolvesTo(_parser(doc, false), longString, "AAA"); |
| 44 | + _verifyRefResolvesTo(_parser(doc, true), longString, "AAA"); |
| 45 | + } |
| 46 | + |
| 47 | + // Reference #0 is the long String itself: before fix, failed with |
| 48 | + // "String reference (0) out of range" since table was left empty |
| 49 | + @Test |
| 50 | + public void testReferenceToLongStringItself() throws Exception |
| 51 | + { |
| 52 | + final String longString = _generateAscii(LONG_LENGTH); |
| 53 | + final byte[] doc = _stringRefDoc(longString, 0); |
| 54 | + |
| 55 | + _verifyRefResolvesTo(_parser(doc, false), longString, longString); |
| 56 | + _verifyRefResolvesTo(_parser(doc, true), longString, longString); |
| 57 | + } |
| 58 | + |
| 59 | + // Same, but with a String that needs actual UTF-8 decoding (not just the |
| 60 | + // ASCII path), to verify multi-byte characters do not throw the count off |
| 61 | + @Test |
| 62 | + public void testReferenceToLongUnicodeString() throws Exception |
| 63 | + { |
| 64 | + StringBuilder sb = new StringBuilder(); |
| 65 | + while (sb.length() < LONG_LENGTH) { |
| 66 | + sb.append("Beyoncé über 中文 "); |
| 67 | + } |
| 68 | + final String longString = sb.toString(); |
| 69 | + final byte[] doc = _stringRefDoc(longString, 0); |
| 70 | + |
| 71 | + _verifyRefResolvesTo(_parser(doc, false), longString, longString); |
| 72 | + _verifyRefResolvesTo(_parser(doc, true), longString, longString); |
| 73 | + } |
| 74 | + |
| 75 | + /* |
| 76 | + /********************************************************** |
| 77 | + /* Test methods, generator round-trip |
| 78 | + /********************************************************** |
| 79 | + */ |
| 80 | + |
| 81 | + // Round-trip through `CBORGenerator` with STRINGREF enabled. Needs to use |
| 82 | + // `SerializableString` (or `writeRawUTF8String()`): plain `writeString()` |
| 83 | + // chunks Strings this long, see `testLongChunkedStringNotReferenced()` |
| 84 | + @Test |
| 85 | + public void testLongStringRoundTrip() throws Exception |
| 86 | + { |
| 87 | + final String longString = _generateAscii(LONG_LENGTH); |
| 88 | + final SerializedString longSerialized = new SerializedString(longString); |
| 89 | + |
| 90 | + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); |
| 91 | + try (CBORGenerator gen = stringrefCborGenerator(bytes)) { |
| 92 | + gen.writeStartArray(); |
| 93 | + gen.writeString(longSerialized); |
| 94 | + gen.writeString("AAA"); |
| 95 | + // Written as reference to entry #0: |
| 96 | + gen.writeString(longSerialized); |
| 97 | + // ... and this one as reference to entry #1: |
| 98 | + gen.writeString("AAA"); |
| 99 | + gen.writeEndArray(); |
| 100 | + } |
| 101 | + final byte[] doc = bytes.toByteArray(); |
| 102 | + |
| 103 | + for (boolean stream : new boolean[] { false, true }) { |
| 104 | + try (JsonParser p = _parser(doc, stream)) { |
| 105 | + assertToken(JsonToken.START_ARRAY, p.nextToken()); |
| 106 | + assertToken(JsonToken.VALUE_STRING, p.nextToken()); |
| 107 | + assertEquals(longString, p.getText()); |
| 108 | + assertToken(JsonToken.VALUE_STRING, p.nextToken()); |
| 109 | + assertEquals("AAA", p.getText()); |
| 110 | + assertToken(JsonToken.VALUE_STRING, p.nextToken()); |
| 111 | + assertEquals(longString, p.getText(), |
| 112 | + "Reference to long String, stream = "+stream); |
| 113 | + assertToken(JsonToken.VALUE_STRING, p.nextToken()); |
| 114 | + assertEquals("AAA", p.getText(), |
| 115 | + "Reference to short String, stream = "+stream); |
| 116 | + assertToken(JsonToken.END_ARRAY, p.nextToken()); |
| 117 | + assertNull(p.nextToken()); |
| 118 | + } |
| 119 | + } |
| 120 | + } |
| 121 | + |
| 122 | + // Conversely: chunked (indefinite length) Strings must NOT be added to the |
| 123 | + // reference table, by either side. Generator chunks Strings longer than |
| 124 | + // ~4000 characters when written via `writeString(String)`, so the long |
| 125 | + // String below does not take up an index -- and "AAA" is entry #0 |
| 126 | + @Test |
| 127 | + public void testLongChunkedStringNotReferenced() throws Exception |
| 128 | + { |
| 129 | + final String longString = _generateAscii(LONG_LENGTH); |
| 130 | + |
| 131 | + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); |
| 132 | + try (CBORGenerator gen = stringrefCborGenerator(bytes)) { |
| 133 | + gen.writeStartArray(); |
| 134 | + gen.writeString(longString); |
| 135 | + gen.writeString("AAA"); |
| 136 | + // Written as reference to entry #0, that is, "AAA": |
| 137 | + gen.writeString("AAA"); |
| 138 | + gen.writeEndArray(); |
| 139 | + } |
| 140 | + final byte[] doc = bytes.toByteArray(); |
| 141 | + |
| 142 | + for (boolean stream : new boolean[] { false, true }) { |
| 143 | + try (JsonParser p = _parser(doc, stream)) { |
| 144 | + assertToken(JsonToken.START_ARRAY, p.nextToken()); |
| 145 | + assertToken(JsonToken.VALUE_STRING, p.nextToken()); |
| 146 | + assertEquals(longString, p.getText()); |
| 147 | + assertToken(JsonToken.VALUE_STRING, p.nextToken()); |
| 148 | + assertEquals("AAA", p.getText()); |
| 149 | + assertToken(JsonToken.VALUE_STRING, p.nextToken()); |
| 150 | + assertEquals("AAA", p.getText(), |
| 151 | + "Reference to short String, stream = "+stream); |
| 152 | + assertToken(JsonToken.END_ARRAY, p.nextToken()); |
| 153 | + assertNull(p.nextToken()); |
| 154 | + } |
| 155 | + } |
| 156 | + } |
| 157 | + |
| 158 | + /* |
| 159 | + /********************************************************** |
| 160 | + /* Helper methods |
| 161 | + /********************************************************** |
| 162 | + */ |
| 163 | + |
| 164 | + /** |
| 165 | + * Constructs document |
| 166 | + *<pre> |
| 167 | + * tag(256) [ <longString>, "AAA", "BBB", tag(25) refIndex ] |
| 168 | + *</pre> |
| 169 | + * where encoder-side reference table is {@code longString} = #0, |
| 170 | + * {@code "AAA"} = #1, {@code "BBB"} = #2. |
| 171 | + */ |
| 172 | + private byte[] _stringRefDoc(String longString, int refIndex) throws Exception |
| 173 | + { |
| 174 | + final byte[] rawString = utf8Bytes(longString); |
| 175 | + ByteArrayOutputStream b = new ByteArrayOutputStream(); |
| 176 | + b.write(0xD9); b.write(0x01); b.write(0x00); // tag 256, "stringref-namespace" |
| 177 | + b.write(0x84); // Array, 4 elements |
| 178 | + // text, 4-byte length (works for any length we use here) |
| 179 | + b.write(0x7A); |
| 180 | + b.write(rawString.length >> 24); |
| 181 | + b.write((rawString.length >> 16) & 0xFF); |
| 182 | + b.write((rawString.length >> 8) & 0xFF); |
| 183 | + b.write(rawString.length & 0xFF); |
| 184 | + b.write(rawString, 0, rawString.length); |
| 185 | + b.write(0x63); b.write('A'); b.write('A'); b.write('A'); |
| 186 | + b.write(0x63); b.write('B'); b.write('B'); b.write('B'); |
| 187 | + b.write(0xD8); b.write(0x19); b.write(refIndex); // tag 25, "stringref" |
| 188 | + return b.toByteArray(); |
| 189 | + } |
| 190 | + |
| 191 | + // Reads through document produced by `_stringRefDoc()`, verifying that |
| 192 | + // the trailing reference resolves to expected String |
| 193 | + private void _verifyRefResolvesTo(JsonParser p, String longString, String exp) |
| 194 | + throws Exception |
| 195 | + { |
| 196 | + try { |
| 197 | + assertToken(JsonToken.START_ARRAY, p.nextToken()); |
| 198 | + assertToken(JsonToken.VALUE_STRING, p.nextToken()); |
| 199 | + assertEquals(longString, p.getText()); |
| 200 | + assertToken(JsonToken.VALUE_STRING, p.nextToken()); |
| 201 | + assertEquals("AAA", p.getText()); |
| 202 | + assertToken(JsonToken.VALUE_STRING, p.nextToken()); |
| 203 | + assertEquals("BBB", p.getText()); |
| 204 | + assertToken(JsonToken.VALUE_STRING, p.nextToken()); |
| 205 | + assertEquals(exp, p.getText()); |
| 206 | + assertToken(JsonToken.END_ARRAY, p.nextToken()); |
| 207 | + assertNull(p.nextToken()); |
| 208 | + } finally { |
| 209 | + p.close(); |
| 210 | + } |
| 211 | + } |
| 212 | + |
| 213 | + // `byte[]`-backed parser decodes long Strings via `_finishShortText()` and |
| 214 | + // was never affected; `InputStream`-backed one uses `_finishLongText()` |
| 215 | + private JsonParser _parser(byte[] doc, boolean stream) throws Exception { |
| 216 | + return stream ? cborParser(new ByteArrayInputStream(doc)) : cborParser(doc); |
| 217 | + } |
| 218 | + |
| 219 | + private String _generateAscii(int len) { |
| 220 | + StringBuilder sb = new StringBuilder(len); |
| 221 | + while (sb.length() < len) { |
| 222 | + sb.append((char) ('a' + (sb.length() % 26))); |
| 223 | + } |
| 224 | + return sb.toString(); |
| 225 | + } |
| 226 | +} |
0 commit comments