OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 library test; |
| 6 |
| 7 import "package:expect/expect.dart"; |
| 8 import "dart:convert"; |
| 9 import "unicode_tests.dart" show UNICODE_TESTS; |
| 10 |
| 11 bool badFormat(e) => e is FormatException; |
| 12 |
| 13 main() { |
| 14 testNumbers(); |
| 15 testStrings(); |
| 16 testKeywords(); |
| 17 testAll(); |
| 18 testMalformed(); |
| 19 testUnicodeTests(); |
| 20 } |
| 21 |
| 22 // Create an UTF-8 sink from a chunked JSON decoder, then let [action] |
| 23 // put data into it, and check that what comes out is equal to [expect]. |
| 24 void jsonTest(testName, expect, action(sink), [bool allowMalformed = false]) { |
| 25 jsonParse(testName, (value) { |
| 26 Expect.equals(expect, value, "$testName:$value"); |
| 27 }, action, allowMalformed); |
| 28 } |
| 29 |
| 30 void jsonParse(testName, check, action, [bool allowMalformed = false]) { |
| 31 var sink = new ChunkedConversionSink.withCallback((values) { |
| 32 var value = values[0]; |
| 33 check(value); |
| 34 }); |
| 35 var decoderSink = JSON.decoder.startChunkedConversion(sink) |
| 36 .asUtf8Sink(allowMalformed); |
| 37 try { |
| 38 action(decoderSink); |
| 39 } on FormatException catch (e, s) { |
| 40 print("Source: ${e.source} @ ${e.offset}"); |
| 41 Expect.fail("Unexpected throw($testName): $e\n$s"); |
| 42 } |
| 43 } |
| 44 |
| 45 void testStrings() { |
| 46 // String literal containing characters, all escape types, |
| 47 // and a number of UTF-8 encoded characters. |
| 48 var s = r'"abc\f\ndef\r\t\b\"\/\\\u0001\u9999\uffff' |
| 49 '\x7f\xc2\x80\xdf\xbf\xe0\xa0\x80\xef\xbf\xbf' |
| 50 '\xf0\x90\x80\x80\xf4\x8f\xbf\xbf"'; // UTF-8. |
| 51 var expected = "abc\f\ndef\r\t\b\"\/\\\u0001\u9999\uffff" |
| 52 "\x7f\x80\u07ff\u0800\uffff" |
| 53 "\u{10000}\u{10ffff}"; |
| 54 for (var i = 1; i < s.length - 1; i++) { |
| 55 var s1 = s.substring(0, i); |
| 56 var s2 = s.substring(i); |
| 57 jsonTest("$s1|$s2-$i", expected, (sink) { |
| 58 sink.add(s1.codeUnits); |
| 59 sink.add(s2.codeUnits); |
| 60 sink.close(); |
| 61 }); |
| 62 jsonTest("$s1|$s2-$i-slice", expected, (sink) { |
| 63 sink.addSlice(s.codeUnits, 0, i, false); |
| 64 sink.addSlice(s.codeUnits, i, s.length, true); |
| 65 }); |
| 66 for (var j = i; j < s.length - 1; j++) { |
| 67 var s2a = s.substring(i, j); |
| 68 var s2b = s.substring(j); |
| 69 jsonTest("$s1|$s2a|$s2b-$i-$j", expected, (sink) { |
| 70 sink.add(s1.codeUnits); |
| 71 sink.add(s2a.codeUnits); |
| 72 sink.add(s2b.codeUnits); |
| 73 sink.close(); |
| 74 }); |
| 75 } |
| 76 } |
| 77 } |
| 78 |
| 79 void testNumbers() { |
| 80 for (var number in ["-0.12e-12", "-34.12E+12", "0.0e0", "9.9E9", "0", "9" |
| 81 "1234.56789123456701418035663664340972900390625", |
| 82 "1.2345678912345671e-14", |
| 83 "99999999999999999999"]) { |
| 84 var expected = num.parse(number); |
| 85 for (int i = 1; i < number.length - 1; i++) { |
| 86 var p1 = number.substring(0, i); |
| 87 var p2 = number.substring(i); |
| 88 jsonTest("$p1|$p2", expected, (sink) { |
| 89 sink.add(p1.codeUnits); |
| 90 sink.add(p2.codeUnits); |
| 91 sink.close(); |
| 92 }); |
| 93 |
| 94 jsonTest("$p1|$p2/slice", expected, (sink) { |
| 95 sink.addSlice(number.codeUnits, 0, i, false); |
| 96 sink.addSlice(number.codeUnits, i, number.length, true); |
| 97 }); |
| 98 |
| 99 for (int j = i; j < number.length - 1; j++) { |
| 100 var p2a = number.substring(i, j); |
| 101 var p2b = number.substring(j); |
| 102 jsonTest("$p1|$p2a|$p2b", expected, (sink) { |
| 103 sink.add(p1.codeUnits); |
| 104 sink.add(p2a.codeUnits); |
| 105 sink.add(p2b.codeUnits); |
| 106 sink.close(); |
| 107 }); |
| 108 } |
| 109 } |
| 110 } |
| 111 } |
| 112 |
| 113 // Test that `null`, `true`, and `false` keywords are decoded correctly. |
| 114 void testKeywords() { |
| 115 for (var expected in [null, true, false]) { |
| 116 var s = "$expected"; |
| 117 for (int i = 1; i < s.length - 1; i++) { |
| 118 var s1 = s.substring(0, i); |
| 119 var s2 = s.substring(i); |
| 120 jsonTest("$s1|$s2", expected, (sink) { |
| 121 sink.add(s1.codeUnits); |
| 122 sink.add(s2.codeUnits); |
| 123 sink.close(); |
| 124 }); |
| 125 jsonTest("$s1|$s2", expected, (sink) { |
| 126 sink.addSlice(s.codeUnits, 0, i, false); |
| 127 sink.addSlice(s.codeUnits, i, s.length, true); |
| 128 }); |
| 129 for (var j = i; j < s.length - 1; j++) { |
| 130 var s2a = s.substring(i, j); |
| 131 var s2b = s.substring(j); |
| 132 jsonTest("$s1|$s2a|$s2b", expected, (sink) { |
| 133 sink.add(s1.codeUnits); |
| 134 sink.add(s2a.codeUnits); |
| 135 sink.add(s2b.codeUnits); |
| 136 sink.close(); |
| 137 }); |
| 138 } |
| 139 } |
| 140 } |
| 141 } |
| 142 |
| 143 // Tests combinations of numbers, strings and keywords. |
| 144 void testAll() { |
| 145 var s = r'{"":[true,false,42, -33e-3,null,"\u0080"], "z": 0}'; |
| 146 bool check(o) { |
| 147 if (o is Map) { |
| 148 Expect.equals(2, o.length); |
| 149 Expect.equals(0, o["z"]); |
| 150 var v = o[""]; |
| 151 if (v is List) { |
| 152 Expect.listEquals([true, false, 42, -33e-3, null, "\u0080"], v); |
| 153 } else { |
| 154 Expect.fail("Expected list, found ${v.runtimeType}"); |
| 155 } |
| 156 } else { |
| 157 Expect.fail("Expected map, found ${o.runtimeType}"); |
| 158 } |
| 159 } |
| 160 for (var i = 1; i < s.length - 1; i++) { |
| 161 var s1 = s.substring(0, i); |
| 162 var s2 = s.substring(i); |
| 163 jsonParse("$s1|$s2-$i", check, (sink) { |
| 164 sink.add(s1.codeUnits); |
| 165 sink.add(s2.codeUnits); |
| 166 sink.close(); |
| 167 }); |
| 168 jsonParse("$s1|$s2-$i-slice", check, (sink) { |
| 169 sink.addSlice(s.codeUnits, 0, i, false); |
| 170 sink.addSlice(s.codeUnits, i, s.length, true); |
| 171 }); |
| 172 for (var j = i; j < s.length - 1; j++) { |
| 173 var s2a = s.substring(i, j); |
| 174 var s2b = s.substring(j); |
| 175 jsonParse("$s1|$s2a|$s2b-$i-$j", check, (sink) { |
| 176 sink.add(s1.codeUnits); |
| 177 sink.add(s2a.codeUnits); |
| 178 sink.add(s2b.codeUnits); |
| 179 sink.close(); |
| 180 }); |
| 181 } |
| 182 } |
| 183 |
| 184 } |
| 185 |
| 186 // Check that [codes] decode to [expect] when allowing malformed UTF-8, |
| 187 // and throws otherwise. |
| 188 void jsonMalformedTest(name, expect, codes) { |
| 189 // Helper method. |
| 190 void test(name, expect, action(sink)) { |
| 191 var tag = "Malform:$name-$expect"; |
| 192 { // Allowing malformed, expect [expect] |
| 193 var sink = new ChunkedConversionSink.withCallback((values) { |
| 194 var value = values[0]; |
| 195 Expect.equals(expect, value, tag); |
| 196 }); |
| 197 var decoderSink = JSON.decoder.startChunkedConversion(sink) |
| 198 .asUtf8Sink(true); |
| 199 try { |
| 200 action(decoderSink); |
| 201 } catch (e, s) { |
| 202 Expect.fail("Unexpected throw ($tag): $e\n$s"); |
| 203 } |
| 204 } |
| 205 { // Not allowing malformed, expect throw. |
| 206 var sink = new ChunkedConversionSink.withCallback((values) { |
| 207 Expect.unreachable(tag); |
| 208 }); |
| 209 var decoderSink = JSON.decoder.startChunkedConversion(sink) |
| 210 .asUtf8Sink(false); |
| 211 Expect.throws(() { action(decoderSink); }, null, tag); |
| 212 } |
| 213 } |
| 214 |
| 215 // Test all two and three part slices. |
| 216 for (int i = 1; i < codes.length - 1; i++) { |
| 217 test("$name:$i", expect, (sink) { |
| 218 sink.add(codes.sublist(0, i)); |
| 219 sink.add(codes.sublist(i)); |
| 220 sink.close(); |
| 221 }); |
| 222 test("$name:$i-slice", expect, (sink) { |
| 223 sink.addSlice(codes, 0, i, false); |
| 224 sink.addSlice(codes, i, codes.length, true); |
| 225 }); |
| 226 for (int j = i; j < codes.length - 1; j++) { |
| 227 test("$name:$i|$j", expect, (sink) { |
| 228 sink.add(codes.sublist(0, i)); |
| 229 sink.add(codes.sublist(i, j)); |
| 230 sink.add(codes.sublist(j)); |
| 231 sink.close(); |
| 232 }); |
| 233 } |
| 234 } |
| 235 } |
| 236 |
| 237 // Test that `codeString.codeUnits` fails to parse as UTF-8 JSON, |
| 238 // even with decoder not throwing on malformed encodings. |
| 239 void jsonThrows(Strig name, String codeString) { |
| 240 testJsonThrows(tag, action) { |
| 241 // Not allowing malformed, expect throw. |
| 242 var sink = new ChunkedConversionSink.withCallback((values) { |
| 243 Expect.unreachable(tag); |
| 244 }); |
| 245 var decoderSink = JSON.decoder.startChunkedConversion(sink) |
| 246 .asUtf8Sink(true); |
| 247 Expect.throws(() { action(decoderSink); }, null, tag); |
| 248 } |
| 249 |
| 250 var codes = codeString.codeUnits; |
| 251 for (int i = 1; i < codes.length - 1; i++) { |
| 252 testJsonThrows("$name:$i", (sink) { |
| 253 sink.add(codes.sublist(0, i)); |
| 254 sink.add(codes.sublist(i)); |
| 255 sink.close(); |
| 256 }); |
| 257 testJsonThrows("$name:$i-slice", (sink) { |
| 258 sink.addSlice(codes, 0, i, false); |
| 259 sink.addSlice(codes, i, codes.length, true); |
| 260 }); |
| 261 for (int j = i; j < codes.length - 1; j++) { |
| 262 testJsonThrows("$name:$i|$j", (sink) { |
| 263 sink.add(codes.sublist(0, i)); |
| 264 sink.add(codes.sublist(i, j)); |
| 265 sink.add(codes.sublist(j)); |
| 266 sink.close(); |
| 267 }); |
| 268 } |
| 269 } |
| 270 } |
| 271 |
| 272 // Malformed UTF-8 encodings. |
| 273 void testMalformed() { |
| 274 // Overlong encodings. |
| 275 jsonMalformedTest("overlong-0-2", "@\uFFFD@", |
| 276 [0x22, 0x40, 0xc0, 0x80, 0x40, 0x22]); |
| 277 jsonMalformedTest("overlong-0-3", "@\uFFFD@", |
| 278 [0x22, 0x40, 0xe0, 0x80, 0x80, 0x40, 0x22]); |
| 279 jsonMalformedTest("overlong-0-4", "@\uFFFD@", |
| 280 [0x22, 0x40, 0xf0, 0x80, 0x80, 0x80, 0x40, 0x22]); |
| 281 |
| 282 jsonMalformedTest("overlong-7f-2", "@\uFFFD@", |
| 283 [0x22, 0x40, 0xc1, 0xbf, 0x40, 0x22]); |
| 284 jsonMalformedTest("overlong-7f-3", "@\uFFFD@", |
| 285 [0x22, 0x40, 0xe0, 0x81, 0xbf, 0x40, 0x22]); |
| 286 jsonMalformedTest("overlong-7f-4", "@\uFFFD@", |
| 287 [0x22, 0x40, 0xf0, 0x80, 0x81, 0xbf, 0x40, 0x22]); |
| 288 |
| 289 jsonMalformedTest("overlong-80-3", "@\uFFFD@", |
| 290 [0x22, 0x40, 0xe0, 0x82, 0x80, 0x40, 0x22]); |
| 291 jsonMalformedTest("overlong-80-4", "@\uFFFD@", |
| 292 [0x22, 0x40, 0xf0, 0x80, 0x82, 0x80, 0x40, 0x22]); |
| 293 |
| 294 jsonMalformedTest("overlong-7ff-3", "@\uFFFD@", |
| 295 [0x22, 0x40, 0xe0, 0x9f, 0xbf, 0x40, 0x22]); |
| 296 jsonMalformedTest("overlong-7ff-4", "@\uFFFD@", |
| 297 [0x22, 0x40, 0xf0, 0x80, 0x9f, 0xbf, 0x40, 0x22]); |
| 298 |
| 299 jsonMalformedTest("overlong-800-4", "@\uFFFD@", |
| 300 [0x22, 0x40, 0xf0, 0x80, 0xa0, 0x80, 0x40, 0x22]); |
| 301 jsonMalformedTest("overlong-ffff-4", "@\uFFFD@", |
| 302 [0x22, 0x40, 0xf0, 0x8f, 0xbf, 0xbf, 0x40, 0x22]); |
| 303 |
| 304 // Unterminated multibyte sequences. |
| 305 jsonMalformedTest("unterminated-2-normal", "@\uFFFD@", |
| 306 [0x22, 0x40, 0xc0, 0x40, 0x22]); |
| 307 |
| 308 jsonMalformedTest("unterminated-3-normal", "@\uFFFD@", |
| 309 [0x22, 0x40, 0xe0, 0x80, 0x40, 0x22]); |
| 310 |
| 311 jsonMalformedTest("unterminated-4-normal", "@\uFFFD@", |
| 312 [0x22, 0x40, 0xf0, 0x80, 0x80, 0x40, 0x22]); |
| 313 |
| 314 jsonMalformedTest("unterminated-2-multi", "@\uFFFD\x80@", |
| 315 [0x22, 0x40, 0xc0, 0xc2, 0x80, 0x40, 0x22]); |
| 316 |
| 317 jsonMalformedTest("unterminated-3-multi", "@\uFFFD\x80@", |
| 318 [0x22, 0x40, 0xe0, 0x80, 0xc2, 0x80, 0x40, 0x22]); |
| 319 |
| 320 jsonMalformedTest("unterminated-4-multi", "@\uFFFD\x80@", |
| 321 [0x22, 0x40, 0xf0, 0x80, 0x80, 0xc2, 0x80, 0x40, 0x22]); |
| 322 |
| 323 jsonMalformedTest("unterminated-2-escape", "@\uFFFD\n@", |
| 324 [0x22, 0x40, 0xc0, 0x5c, 0x6e, 0x40, 0x22]); |
| 325 |
| 326 jsonMalformedTest("unterminated-3-escape", "@\uFFFD\n@", |
| 327 [0x22, 0x40, 0xe0, 0x80, 0x5c, 0x6e, 0x40, 0x22]); |
| 328 |
| 329 jsonMalformedTest("unterminated-4-escape", "@\uFFFD\n@", |
| 330 [0x22, 0x40, 0xf0, 0x80, 0x80, 0x5c, 0x6e, 0x40, 0x22]); |
| 331 |
| 332 jsonMalformedTest("unterminated-2-end", "@\uFFFD", |
| 333 [0x22, 0x40, 0xc0, 0x22]); |
| 334 |
| 335 jsonMalformedTest("unterminated-3-end", "@\uFFFD", |
| 336 [0x22, 0x40, 0xe0, 0x80, 0x22]); |
| 337 |
| 338 jsonMalformedTest("unterminated-4-end", "@\uFFFD", |
| 339 [0x22, 0x40, 0xf0, 0x80, 0x80, 0x22]); |
| 340 |
| 341 // Unexpected continuation byte |
| 342 // - after a normal character. |
| 343 jsonMalformedTest("continuation-normal", "@\uFFFD@", |
| 344 [0x22, 0x40, 0x80, 0x40, 0x22]); |
| 345 |
| 346 // - after a valid continuation byte. |
| 347 jsonMalformedTest("continuation-continuation-2", "@\x80\uFFFD@", |
| 348 [0x22, 0x40, 0xc2, 0x80, 0x80, 0x40, 0x22]); |
| 349 jsonMalformedTest("continuation-continuation-3", "@\u0800\uFFFD@", |
| 350 [0x22, 0x40, 0xe0, 0xa0, 0x80, 0x80, 0x40, 0x22]); |
| 351 jsonMalformedTest("continuation-continuation-4", "@\u{10000}\uFFFD@", |
| 352 [0x22, 0x40, 0xf0, 0x90, 0x80, 0x80, 0x80, 0x40, 0x22]); |
| 353 |
| 354 // - after another invalid continuation byte |
| 355 jsonMalformedTest("continuation-twice", "@\uFFFD\uFFFD\uFFFD@", |
| 356 [0x22, 0x40, 0x80, 0x80, 0x80, 0x40, 0x22]); |
| 357 // - at start. |
| 358 jsonMalformedTest("continuation-start", "\uFFFD@", |
| 359 [0x22, 0x80, 0x40, 0x22]); |
| 360 |
| 361 // Unexpected leading byte where continuation byte expected. |
| 362 jsonMalformedTest("leading-2", "@\uFFFD\x80@", |
| 363 [0x22, 0x40, 0xc0, 0xc2, 0x80, 0x40, 0x22]); |
| 364 jsonMalformedTest("leading-3-1", "@\uFFFD\x80@", |
| 365 [0x22, 0x40, 0xe0, 0xc2, 0x80, 0x40, 0x22]); |
| 366 jsonMalformedTest("leading-3-2", "@\uFFFD\x80@", |
| 367 [0x22, 0x40, 0xe0, 0x80, 0xc2, 0x80, 0x40, 0x22]); |
| 368 jsonMalformedTest("leading-4-1", "@\uFFFD\x80@", |
| 369 [0x22, 0x40, 0xf0, 0xc2, 0x80, 0x40, 0x22]); |
| 370 jsonMalformedTest("leading-4-2", "@\uFFFD\x80@", |
| 371 [0x22, 0x40, 0xf0, 0x80, 0xc2, 0x80, 0x40, 0x22]); |
| 372 jsonMalformedTest("leading-4-3", "@\uFFFD\x80@", |
| 373 [0x22, 0x40, 0xf0, 0x80, 0x80, 0xc2, 0x80, 0x40, 0x22]); |
| 374 |
| 375 // Overlong encodings of ASCII outside of strings always fail. |
| 376 // Use Latin-1 strings as argument since most chars are correct, |
| 377 // pass string.codeUnits to decoder as UTF-8. |
| 378 jsonThrows("number-1", "\xc0\xab0.0e-0"); // '-' is 0x2b => \xc0\xab |
| 379 jsonThrows("number-2", "-\xc0\xb0.0e-0"); // '0' is 0x30 => \xc0\xb0 |
| 380 jsonThrows("number-3", "-0\xc0\xae0e-0"); // '.' is 0x2e => \xc0\xae |
| 381 jsonThrows("number-4", "-0.\xc0\xb0e-0"); |
| 382 jsonThrows("number-5", "-0.0\xc1\xa5-0"); // 'e' is 0x65 => \xc1\xa5 |
| 383 jsonThrows("number-6", "-0.0e\xc0\xab0"); |
| 384 jsonThrows("number-7", "-0.0e-\xc0\xb0"); |
| 385 |
| 386 jsonThrows("true-1", "\xc1\xb4rue"); // 't' is 0x74 |
| 387 jsonThrows("true-2", "t\xc1\xb2ue"); // 'r' is 0x72 |
| 388 jsonThrows("true-3", "tr\xc1\xb5e"); // 'u' is 0x75 |
| 389 jsonThrows("true-4", "tru\xc1\xa5"); // 'e' is 0x65 |
| 390 |
| 391 jsonThrows("false-1", "\xc1\xa6alse"); // 'f' is 0x66 |
| 392 jsonThrows("false-2", "f\xc1\xa1lse"); // 'a' is 0x61 |
| 393 jsonThrows("false-3", "fa\xc1\xacse"); // 'l' is 0x6c |
| 394 jsonThrows("false-4", "fal\xc1\xb3e"); // 's' is 0x73 |
| 395 jsonThrows("false-5", "fals\xc1\xa5"); // 'e' is 0x65 |
| 396 |
| 397 jsonThrows("null-1", "\xc1\xaeull"); // 'n' is 0x6e |
| 398 jsonThrows("null-2", "n\xc1\xb5ll"); // 'u' is 0x75 |
| 399 jsonThrows("null-3", "nu\xc1\xacl"); // 'l' is 0x6c |
| 400 jsonThrows("null-4", "nul\xc1\xac"); // 'l' is 0x6c |
| 401 |
| 402 jsonThrows("array-1", "\xc1\x9b0,0]"); // '[' is 0x5b |
| 403 jsonThrows("array-2", "[0,0\xc1\x9d"); // ']' is 0x5d |
| 404 jsonThrows("array-2", "[0\xc0\xac0]"); // ',' is 0x2c |
| 405 |
| 406 jsonThrows("object-1", '\xc1\xbb"x":0}'); // '{' is 0x7b |
| 407 jsonThrows("object-2", '{"x":0\xc1\xbd'); // '}' is 0x7d |
| 408 jsonThrows("object-2", '{"x\xc0\xba0}'); // ':' is 0x3a |
| 409 |
| 410 jsonThrows("string-1", '\xc0\xa2x"'); // '"' is 0x22 |
| 411 jsonThrows("string-1", '"x\xc0\xa2'); // Unterminated string. |
| 412 |
| 413 jsonThrows("whitespace-1", "\xc0\xa01"); // ' ' is 0x20 |
| 414 } |
| 415 |
| 416 void testUnicodeTests() { |
| 417 for (var pair in UNICODE_TESTS) { |
| 418 var bytes = pair[0]; |
| 419 var string = pair[1]; |
| 420 int step = 1; |
| 421 if (bytes.length > 100) step = bytes.length ~/ 13; |
| 422 for (int i = 1; i < bytes.length - 1; i += step) { |
| 423 jsonTest("$string:$i", string, (sink) { |
| 424 sink.add([0x22]); // Double-quote. |
| 425 sink.add(bytes.sublist(0, i)); |
| 426 sink.add(bytes.sublist(i)); |
| 427 sink.add([0x22]); |
| 428 sink.close(); |
| 429 }); |
| 430 jsonTest("$string:$i-slice", string, (sink) { |
| 431 sink.addSlice([0x22], 0, 1, false); |
| 432 sink.addSlice(bytes, 0, i, false); |
| 433 sink.addSlice(bytes, i, bytes.length, false); |
| 434 sink.addSlice([0x22], 0, 1, true); |
| 435 }); |
| 436 int skip = 1; |
| 437 if (bytes.length > 25) skip = bytes.length ~/ 17; |
| 438 for (int j = i; j < bytes.length - 1; j += skip) { |
| 439 jsonTest("$string:$i|$j", string, (sink) { |
| 440 sink.add([0x22]); |
| 441 sink.add(bytes.sublist(0, i)); |
| 442 sink.add(bytes.sublist(i, j)); |
| 443 sink.add(bytes.sublist(j)); |
| 444 sink.add([0x22]); |
| 445 sink.close(); |
| 446 }); |
| 447 } |
| 448 } |
| 449 } |
| 450 } |
OLD | NEW |