honeymaker 0.12.2 → 0.12.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/native-gems.yml +100 -0
  3. data/.github/workflows/release.yml +36 -0
  4. data/.github/workflows/test.yml +19 -4
  5. data/Cargo.lock +1078 -0
  6. data/Cargo.toml +14 -0
  7. data/README.md +39 -0
  8. data/Rakefile +28 -16
  9. data/crates/honeymaker/Cargo.toml +18 -0
  10. data/crates/honeymaker/src/encode.rs +106 -0
  11. data/crates/honeymaker/src/kraken/lookup.rs +140 -0
  12. data/crates/honeymaker/src/kraken/mod.rs +5 -0
  13. data/crates/honeymaker/src/kraken/normalize.rs +536 -0
  14. data/crates/honeymaker/src/kraken/requests.rs +286 -0
  15. data/crates/honeymaker/src/kraken/sign.rs +166 -0
  16. data/crates/honeymaker/src/kraken/trades.rs +201 -0
  17. data/crates/honeymaker/src/lib.rs +10 -0
  18. data/crates/honeymaker/src/num.rs +115 -0
  19. data/crates/honeymaker/src/semantics.rs +100 -0
  20. data/crates/honeymaker/src/types.rs +85 -0
  21. data/crates/honeymaker/tests/kraken_stage2.rs +592 -0
  22. data/crates/honeymaker/tests/kraken_trades_pager.rs +307 -0
  23. data/crates/honeymaker/tests/kraken_vectors.rs +78 -0
  24. data/crates/honeymaker/tests/vectors/decimal.json +146 -0
  25. data/crates/honeymaker/tests/vectors/kraken_add_order_wire.json +74 -0
  26. data/crates/honeymaker/tests/vectors/kraken_signing.json +3314 -0
  27. data/crates/honeymaker/tests/vectors/kraken_trades_pager.json +1583 -0
  28. data/crates/honeymaker-client/Cargo.toml +30 -0
  29. data/crates/honeymaker-client/examples/probe.rs +147 -0
  30. data/crates/honeymaker-client/src/kraken.rs +514 -0
  31. data/crates/honeymaker-client/src/lib.rs +4 -0
  32. data/crates/honeymaker-client/src/transport/connect.rs +583 -0
  33. data/crates/honeymaker-client/src/transport/io_timeout.rs +352 -0
  34. data/crates/honeymaker-client/src/transport/response.rs +169 -0
  35. data/crates/honeymaker-client/src/transport.rs +467 -0
  36. data/crates/honeymaker-client/tests/classification.rs +203 -0
  37. data/crates/honeymaker-client/tests/kraken_client.rs +949 -0
  38. data/crates/honeymaker-client/tests/response.rs +522 -0
  39. data/crates/honeymaker-client/tests/support/mod.rs +550 -0
  40. data/crates/honeymaker-client/tests/transport.rs +828 -0
  41. data/ext/honeymaker_native/Cargo.toml +16 -0
  42. data/ext/honeymaker_native/extconf.rb +6 -0
  43. data/ext/honeymaker_native/src/convert.rs +150 -0
  44. data/ext/honeymaker_native/src/kraken.rs +362 -0
  45. data/ext/honeymaker_native/src/lib.rs +90 -0
  46. data/ext/honeymaker_native/src/ruby_decimal.rs +90 -0
  47. data/lib/honeymaker/clients/bybit.rb +19 -2
  48. data/lib/honeymaker/clients/coinbase.rb +3 -2
  49. data/lib/honeymaker/clients/kraken.rb +17 -1
  50. data/lib/honeymaker/exchanges/coinbase.rb +5 -4
  51. data/lib/honeymaker/native/kraken.rb +263 -0
  52. data/lib/honeymaker/native.rb +42 -0
  53. data/lib/honeymaker/version.rb +1 -1
  54. data/lib/honeymaker.rb +12 -2
  55. data/script/parity/kraken_client_live.rb +191 -0
  56. data/script/parity/kraken_live.rb +70 -0
  57. data/script/parity/kraken_transport.rb +604 -0
  58. data/script/parity/resolve.c +49 -0
  59. data/script/release/push_gems.sh +28 -0
  60. data/script/vectors/kraken.rb +50 -0
  61. data/script/vectors/kraken_stage2.rb +146 -0
  62. data/test/fixtures/coinbase_products.json +1 -1
  63. data/test/honeymaker/clients/bybit_client_test.rb +25 -0
  64. data/test/honeymaker/clients/coinbase_client_test.rb +14 -0
  65. data/test/honeymaker/clients/kraken_client_test.rb +27 -0
  66. data/test/honeymaker/exchanges/coinbase_test.rb +22 -2
  67. data/test/native/backend_switch_test.rb +53 -0
  68. data/test/native/convert_test.rb +137 -0
  69. data/test/native/gem_isolation_test.rb +17 -0
  70. data/test/native/loader_test.rb +29 -0
  71. data/test/native/lockstep_test.rb +23 -0
  72. data/test/native/signing_smoke_test.rb +28 -0
  73. data/test/parity/kraken_client_parity_test.rb +398 -0
  74. data/test/parity/kraken_exchange_parity_test.rb +172 -0
  75. data/test/release/branch_hardening_test.rb +34 -0
  76. data/test/release/push_gems_test.rb +79 -0
  77. data/test/smoke.rb +24 -0
  78. data/test/support/kraken_signing_smoke.rb +21 -0
  79. data/test/support/parity_helper.rb +62 -0
  80. data/test/support/venue_double.rb +100 -0
  81. metadata +84 -4
@@ -0,0 +1,522 @@
1
+ //! Faraday 2.14 with `request :json`, `response :json` (content_type /\bjson$/),
2
+ //! `response :raise_error`, rescued by Honeymaker::Client#with_rescue (spec §4 rows).
3
+ use bytes::Bytes;
4
+ use flate2::Compression;
5
+ use flate2::write::{GzEncoder, ZlibEncoder};
6
+ use honeymaker_client::transport::{BodyFraming, Cause, Raw, Reply, decode, shape};
7
+ use serde_json::json;
8
+ use std::io::Write;
9
+
10
+ const URL: &str = "https://api.kraken.com/0/private/AddOrder";
11
+ fn raw(status: u16, ct: Option<&str>, enc: Option<&str>, body: Vec<u8>) -> Raw {
12
+ Raw {
13
+ status,
14
+ content_type: ct.map(str::to_string),
15
+ content_encoding: enc.map(str::to_string),
16
+ content_range: false,
17
+ framing: BodyFraming::ContentLength,
18
+ body: Bytes::from(body),
19
+ }
20
+ }
21
+ fn reply(status: u16, ct: Option<&str>, body: &str) -> Reply {
22
+ shape("POST", URL, raw(status, ct, None, body.as_bytes().to_vec()))
23
+ }
24
+ fn gzip(b: &[u8]) -> Vec<u8> {
25
+ let mut e = GzEncoder::new(Vec::new(), Compression::default());
26
+ e.write_all(b).unwrap();
27
+ e.finish().unwrap()
28
+ }
29
+ fn zlib(b: &[u8]) -> Vec<u8> {
30
+ let mut e = ZlibEncoder::new(Vec::new(), Compression::default());
31
+ e.write_all(b).unwrap();
32
+ e.finish().unwrap()
33
+ }
34
+ fn decoded(r: Raw) -> Reply {
35
+ shape("POST", URL, decode(r).unwrap())
36
+ }
37
+
38
+ #[test]
39
+ fn gzip_and_deflate_bodies_are_inflated_at_any_status_as_net_http_does() {
40
+ let ok = br#"{"error":[],"result":{"txid":["OTX-1"]}}"#;
41
+ for enc in ["gzip", "x-gzip", "GZIP"] {
42
+ assert_eq!(
43
+ decoded(raw(200, Some("application/json"), Some(enc), gzip(ok))),
44
+ Reply::Parsed(json!({"error": [], "result": {"txid": ["OTX-1"]}})),
45
+ "{enc}"
46
+ );
47
+ }
48
+ // "deflate" is zlib-wrapped; Zlib::Inflate.new(32 + MAX_WBITS) also accepts a gzip stream there.
49
+ assert_eq!(
50
+ decoded(raw(
51
+ 200,
52
+ Some("application/json"),
53
+ Some("deflate"),
54
+ zlib(ok)
55
+ )),
56
+ Reply::Parsed(json!({"error": [], "result": {"txid": ["OTX-1"]}}))
57
+ );
58
+ assert_eq!(
59
+ decoded(raw(
60
+ 200,
61
+ Some("application/json"),
62
+ Some("deflate"),
63
+ gzip(ok)
64
+ )),
65
+ Reply::Parsed(json!({"error": [], "result": {"txid": ["OTX-1"]}}))
66
+ );
67
+ let refusal = br#"{"error":["EOrder:Insufficient funds"]}"#;
68
+ assert_eq!(
69
+ decoded(raw(
70
+ 400,
71
+ Some("application/json"),
72
+ Some("gzip"),
73
+ gzip(refusal)
74
+ )),
75
+ st(400, r#"{"error":["EOrder:Insufficient funds"]}"#)
76
+ );
77
+ assert_eq!(
78
+ decoded(raw(200, Some("application/json"), Some("gzip"), vec![])),
79
+ Reply::NotJson,
80
+ "an empty stream inflates to nothing"
81
+ );
82
+ }
83
+
84
+ #[test]
85
+ fn other_encodings_and_ranges_pass_through_and_a_bad_stream_is_a_zlib_failure() {
86
+ assert_eq!(
87
+ decoded(raw(
88
+ 200,
89
+ Some("application/json"),
90
+ Some("identity"),
91
+ br#"{"a":1}"#.to_vec()
92
+ )),
93
+ Reply::Parsed(json!({"a": 1}))
94
+ );
95
+ assert_eq!(
96
+ decoded(raw(
97
+ 200,
98
+ Some("application/json"),
99
+ Some("br"),
100
+ br#"{"a":1}"#.to_vec()
101
+ )),
102
+ Reply::Parsed(json!({"a": 1})),
103
+ "not ours to decode"
104
+ );
105
+ let mut ranged = raw(206, Some("application/json"), Some("gzip"), gzip(b"{}"));
106
+ ranged.content_range = true;
107
+ assert_eq!(
108
+ decode(ranged).unwrap().content_encoding.as_deref(),
109
+ Some("gzip"),
110
+ "Content-Range: left encoded"
111
+ );
112
+ for bad in [b"not gzip at all".to_vec(), vec![0x78, 0x9c, 0xff, 0xff]] {
113
+ let f = decode(raw(200, Some("application/json"), Some("gzip"), bad)).unwrap_err();
114
+ assert_eq!(f.cause, Cause::Other);
115
+ assert!(f.message.starts_with("Zlib::"), "{}", f.message);
116
+ assert!(!f.pre_transmission(), "the gem's client_error: ambiguous");
117
+ }
118
+ }
119
+ fn st(status: u16, message: &str) -> Reply {
120
+ Reply::Status {
121
+ status,
122
+ message: message.into(),
123
+ }
124
+ }
125
+
126
+ #[test]
127
+ fn error_statuses_carry_the_raw_body_or_faradays_message() {
128
+ assert_eq!(
129
+ reply(
130
+ 500,
131
+ Some("application/json"),
132
+ r#"{"error":["EService:Unavailable"]}"#
133
+ ),
134
+ st(500, r#"{"error":["EService:Unavailable"]}"#)
135
+ );
136
+ assert_eq!(
137
+ reply(500, Some("text/plain"), ""),
138
+ st(
139
+ 500,
140
+ &format!("the server responded with status 500 for POST {URL}")
141
+ )
142
+ );
143
+ assert_eq!(
144
+ reply(404, Some("text/html"), "<h1>nf</h1> é"),
145
+ st(404, "<h1>nf</h1> é")
146
+ );
147
+ assert_eq!(reply(429, None, "no"), st(429, "no"));
148
+ assert_eq!(
149
+ reply(400, Some("application/json; charset=utf-8"), r#"{"e":1}"#),
150
+ st(400, r#"{"e":1}"#)
151
+ );
152
+ }
153
+
154
+ #[test]
155
+ fn json_is_parsed_only_under_a_json_media_type() {
156
+ assert_eq!(
157
+ reply(200, Some("application/json"), r#"{"a":1}"#),
158
+ Reply::Parsed(json!({"a": 1}))
159
+ );
160
+ assert_eq!(
161
+ reply(200, Some("application/json; charset=utf-8"), r#"{"a":1}"#),
162
+ Reply::Parsed(json!({"a": 1}))
163
+ );
164
+ assert_eq!(
165
+ reply(200, Some("application/vnd.api+json"), "[1]"),
166
+ Reply::Parsed(json!([1]))
167
+ );
168
+ assert_eq!(
169
+ reply(200, Some("Application/JSON"), r#"{"a":1}"#),
170
+ Reply::NotJson,
171
+ "Faraday's match is case-sensitive"
172
+ );
173
+ assert_eq!(reply(200, Some("text/jsonx"), r#"{"a":1}"#), Reply::NotJson);
174
+ assert_eq!(reply(200, Some("text/html"), "<p>hi</p>"), Reply::NotJson);
175
+ assert_eq!(reply(200, None, r#"{"a":1}"#), Reply::NotJson);
176
+ }
177
+
178
+ #[test]
179
+ fn blank_bodies_are_not_json_and_bad_ones_fail_with_the_body() {
180
+ assert_eq!(reply(200, Some("application/json"), ""), Reply::NotJson);
181
+ assert_eq!(
182
+ reply(200, Some("application/json"), " \n "),
183
+ Reply::NotJson
184
+ );
185
+ assert_eq!(
186
+ reply(200, Some("application/json"), "{not json"),
187
+ st(200, "{not json")
188
+ );
189
+ assert_eq!(
190
+ reply(200, Some("application/json"), r#"{"a""#),
191
+ st(200, r#"{"a""#)
192
+ );
193
+ let dup = r#"{"dup":1,"dup":2}"#;
194
+ assert_eq!(
195
+ reply(200, Some("application/json"), dup),
196
+ st(200, dup),
197
+ "json 3.0.2 rejects duplicate keys"
198
+ );
199
+ let nested_dup = r#"{"result":{"x":{"k":1,"k":1}}}"#;
200
+ assert_eq!(
201
+ reply(200, Some("application/json"), nested_dup),
202
+ st(200, nested_dup)
203
+ );
204
+ }
205
+
206
+ #[test]
207
+ fn numbers_keep_their_text_and_other_statuses_parse_like_success() {
208
+ let Reply::Parsed(v) = reply(
209
+ 200,
210
+ Some("application/json"),
211
+ r#"{"i":12345678901234567890123,"f":1688888888.1234,"z":-0.0}"#,
212
+ ) else {
213
+ panic!()
214
+ };
215
+ assert_eq!(v["i"].to_string(), "12345678901234567890123");
216
+ assert_eq!(v["f"].to_string(), "1688888888.1234");
217
+ assert_eq!(
218
+ reply(302, Some("application/json"), r#"{"a":1}"#),
219
+ Reply::Parsed(json!({"a": 1}))
220
+ );
221
+ assert_eq!(reply(302, Some("application/json"), "{"), st(302, "{"));
222
+ }
223
+
224
+ fn truncated(framing: BodyFraming) -> Raw {
225
+ let mut r = raw(
226
+ 200,
227
+ Some("application/json"),
228
+ Some("gzip"),
229
+ gzip(br#"{"a":1}"#)[..12].to_vec(),
230
+ );
231
+ r.framing = framing;
232
+ r
233
+ }
234
+
235
+ #[test]
236
+ fn truncated_gzip_with_content_length_yields_an_empty_body() {
237
+ let r = decode(truncated(BodyFraming::ContentLength)).unwrap();
238
+ assert!(r.body.is_empty());
239
+ assert_eq!(r.content_encoding, None);
240
+ assert_eq!(shape("POST", URL, r), Reply::NotJson);
241
+ }
242
+
243
+ #[test]
244
+ fn truncated_gzip_with_chunked_framing_yields_an_empty_body() {
245
+ let r = decode(truncated(BodyFraming::Chunked)).unwrap();
246
+ assert!(r.body.is_empty());
247
+ assert_eq!(r.content_encoding, None);
248
+ assert_eq!(shape("POST", URL, r), Reply::NotJson);
249
+ }
250
+
251
+ #[test]
252
+ fn truncated_gzip_with_close_delimited_framing_is_an_ambiguous_failure() {
253
+ use honeymaker_client::kraken::{VenueError, outcome};
254
+ let f = decode(truncated(BodyFraming::CloseDelimited)).unwrap_err();
255
+ assert_eq!(f.cause, Cause::Other);
256
+ assert!(f.message.starts_with("Zlib::BufError:"), "{}", f.message);
257
+ assert!(!f.pre_transmission());
258
+ assert_eq!(
259
+ outcome(Err(f.clone())),
260
+ Err(VenueError::Ambiguous(f.message))
261
+ );
262
+ }
263
+
264
+ #[test]
265
+ fn incomplete_streams_discard_only_the_pending_ruby_zlib_buffer() {
266
+ for body in [b"{}".to_vec(), vec![b'x'; 16384 * 2 + 123]] {
267
+ for mut compressed in [gzip(&body), zlib(&body)] {
268
+ compressed.pop(); // Missing trailer: partial output is buffered until stream end.
269
+ for framing in [
270
+ BodyFraming::ContentLength,
271
+ BodyFraming::Chunked,
272
+ BodyFraming::CloseDelimited,
273
+ ] {
274
+ let mut r = raw(200, Some("text/plain"), Some("gzip"), compressed.clone());
275
+ r.framing = framing;
276
+ match framing {
277
+ BodyFraming::CloseDelimited => {
278
+ assert_eq!(decode(r).unwrap_err().cause, Cause::Other)
279
+ }
280
+ _ => assert_eq!(
281
+ &decode(r).unwrap().body[..],
282
+ &body[..body.len() / 16384 * 16384]
283
+ ),
284
+ }
285
+ }
286
+ }
287
+ }
288
+ }
289
+
290
+ #[test]
291
+ fn corruption_is_not_suppressed_by_body_framing() {
292
+ let mut bad_crc = gzip(b"{}");
293
+ let crc_index = bad_crc.len() - 8;
294
+ bad_crc[crc_index] ^= 1;
295
+ for bad in [
296
+ b"not gzip at all".to_vec(),
297
+ vec![0x78, 0x9c, 0xff, 0xff],
298
+ bad_crc,
299
+ ] {
300
+ for framing in [
301
+ BodyFraming::ContentLength,
302
+ BodyFraming::Chunked,
303
+ BodyFraming::CloseDelimited,
304
+ ] {
305
+ let mut r = raw(200, Some("application/json"), Some("gzip"), bad.clone());
306
+ r.framing = framing;
307
+ let f = decode(r).unwrap_err();
308
+ assert_eq!(f.cause, Cause::Other);
309
+ assert!(f.message.starts_with("Zlib::DataError:"), "{}", f.message);
310
+ assert!(!f.pre_transmission());
311
+ }
312
+ }
313
+ }
314
+
315
+ #[test]
316
+ fn empty_407_uses_faradays_proxy_auth_message() {
317
+ assert_eq!(
318
+ reply(407, None, ""),
319
+ st(407, "407 \"Proxy Authentication Required\"")
320
+ );
321
+ assert_eq!(reply(407, None, "refused"), st(407, "refused"));
322
+ }
323
+
324
+ #[test]
325
+ fn fallback_messages_uppercase_the_request_method() {
326
+ assert_eq!(
327
+ shape("post", URL, raw(500, None, None, vec![])),
328
+ st(
329
+ 500,
330
+ &format!("the server responded with status 500 for POST {URL}")
331
+ )
332
+ );
333
+ }
334
+
335
+ #[test]
336
+ fn recognized_encoding_headers_are_removed_even_on_empty_bodies() {
337
+ for enc in [
338
+ "gzip", "x-gzip", "GZIP", "deflate", "none", "identity", "IDENTITY",
339
+ ] {
340
+ for framing in [
341
+ BodyFraming::ContentLength,
342
+ BodyFraming::Chunked,
343
+ BodyFraming::CloseDelimited,
344
+ ] {
345
+ let mut r = raw(200, None, Some(enc), vec![]);
346
+ r.framing = framing;
347
+ let r = decode(r).unwrap();
348
+ assert_eq!(r.content_encoding, None, "{enc}");
349
+ assert!(r.body.is_empty());
350
+ }
351
+ let r = decode(raw(
352
+ 200,
353
+ None,
354
+ Some(enc),
355
+ if matches!(enc, "none" | "identity" | "IDENTITY") {
356
+ b"{}".to_vec()
357
+ } else {
358
+ gzip(b"{}")
359
+ },
360
+ ))
361
+ .unwrap();
362
+ assert_eq!(r.content_encoding, None, "{enc}");
363
+ assert_eq!(&r.body[..], b"{}");
364
+ let mut ranged = raw(206, None, Some(enc), vec![]);
365
+ ranged.content_range = true;
366
+ assert_eq!(
367
+ decode(ranged).unwrap().content_encoding.as_deref(),
368
+ Some(enc)
369
+ );
370
+ }
371
+ assert_eq!(
372
+ decode(raw(200, None, Some("br"), vec![]))
373
+ .unwrap()
374
+ .content_encoding
375
+ .as_deref(),
376
+ Some("br")
377
+ );
378
+ }
379
+
380
+ #[test]
381
+ fn json_limits_array_object_and_mixed_nesting_to_100_levels() {
382
+ for (open, close) in [("[", "]"), ("{\"x\":", "}"), ("[{\"x\":", "}]")] {
383
+ let levels = open.matches(['[', '{']).count();
384
+ for depth in [100 / levels, 100 / levels + 1] {
385
+ let body = format!("{}0{}", open.repeat(depth), close.repeat(depth));
386
+ if depth * levels <= 100 {
387
+ assert_eq!(
388
+ reply(200, Some("application/json"), &body),
389
+ Reply::Parsed(serde_json::from_str(&body).unwrap())
390
+ );
391
+ } else {
392
+ assert_eq!(reply(200, Some("application/json"), &body), st(200, &body));
393
+ }
394
+ }
395
+ }
396
+ // Text inside strings is not nesting; arbitrary-precision number pseudo-maps aren't either.
397
+ let body = format!(
398
+ "{}{{\"[\\\"{{\":12345678901234567890123}}{}",
399
+ "[".repeat(99),
400
+ "]".repeat(99)
401
+ );
402
+ assert!(matches!(
403
+ reply(200, Some("application/json"), &body),
404
+ Reply::Parsed(_)
405
+ ));
406
+ }
407
+
408
+ #[test]
409
+ fn empty_json_containers_do_not_increment_the_nesting_limit() {
410
+ for leaf in ["[]", "{}"] {
411
+ let body = format!("{}{}{}", "[".repeat(100), leaf, "]".repeat(100));
412
+ assert_eq!(
413
+ reply(200, Some("application/json"), &body),
414
+ Reply::Parsed(serde_json::from_str(&body).unwrap())
415
+ );
416
+ }
417
+ }
418
+
419
+ #[test]
420
+ fn invalid_utf8_in_an_add_order_description_still_parses() {
421
+ let body =
422
+ b"{\"error\":[],\"result\":{\"txid\":[\"OTX-1\"],\"descr\":{\"order\":\"buy \xff\"}}}";
423
+ assert_eq!(
424
+ decoded(raw(200, Some("application/json"), None, body.to_vec())),
425
+ Reply::Parsed(
426
+ json!({"error": [], "result": {"txid": ["OTX-1"], "descr": {"order": "buy �"}}})
427
+ )
428
+ );
429
+ }
430
+
431
+ #[test]
432
+ fn invalid_utf8_does_not_relax_json_strictness() {
433
+ for bad in [
434
+ "NaN",
435
+ "Infinity",
436
+ "-Infinity",
437
+ r#""\q""#,
438
+ r#""\ud800""#,
439
+ r#""\udc00""#,
440
+ r#""\ud800\u0041""#,
441
+ r#""\uZZZZ""#,
442
+ "01",
443
+ "\"raw\nnewline\"",
444
+ r#"{"a":1,"a":2}"#,
445
+ r#"{"a":1,"\u0061":2}"#,
446
+ r#"{"nested":{"a":1,"a":2}}"#,
447
+ ] {
448
+ for prefix in [b"[".as_slice(), b"[\"\xff\","] {
449
+ let body = [prefix, bad.as_bytes(), b"]"].concat();
450
+ assert_eq!(
451
+ decoded(raw(200, Some("application/json"), None, body.clone())),
452
+ st(200, &String::from_utf8_lossy(&body)),
453
+ "{bad}"
454
+ );
455
+ }
456
+ }
457
+ for body in [
458
+ b"\xef\xbb\xbf{}".as_slice(),
459
+ b"\xff{}",
460
+ b"{\"a\":1} trailing",
461
+ ] {
462
+ assert_eq!(
463
+ decoded(raw(200, Some("application/json"), None, body.to_vec())),
464
+ st(200, &String::from_utf8_lossy(body))
465
+ );
466
+ }
467
+ let body = b"[\"\xff\",\"\\ud83d\\ude00\"]";
468
+ assert_eq!(
469
+ decoded(raw(200, Some("application/json"), None, body.to_vec())),
470
+ Reply::Parsed(json!(["�", "😀"]))
471
+ );
472
+ for depth in [100, 101] {
473
+ let body = [
474
+ "[".repeat(depth).as_bytes(),
475
+ b"\"\xff\"",
476
+ "]".repeat(depth).as_bytes(),
477
+ ]
478
+ .concat();
479
+ let result = decoded(raw(200, Some("application/json"), None, body.clone()));
480
+ if depth == 100 {
481
+ assert!(matches!(result, Reply::Parsed(_)));
482
+ } else {
483
+ assert_eq!(result, st(200, &String::from_utf8_lossy(&body)));
484
+ }
485
+ }
486
+ }
487
+
488
+ #[test]
489
+ fn duplicate_keys_are_compared_before_lossy_decoding() {
490
+ for body in [
491
+ b"{\"\xff\":1,\"\xff\":2}".as_slice(),
492
+ b"{\"\xffa\":1,\"\xff\\u0061\":2}",
493
+ b"{\"\xff\":0,\"nested\":{\"a\":1,\"\\u0061\":2}}",
494
+ b"{\"\xff\":0,\"nested\":{\"\xc3\xa9\":1,\"\\u00e9\":2}}",
495
+ ] {
496
+ assert_eq!(
497
+ decoded(raw(200, Some("application/json"), None, body.to_vec())),
498
+ st(200, &String::from_utf8_lossy(body))
499
+ );
500
+ }
501
+ let body = b"{\"\xff\":1,\"\xfe\":2,\"\xef\xbf\xbd\":3}";
502
+ assert!(
503
+ matches!(
504
+ decoded(raw(200, Some("application/json"), None, body.to_vec())),
505
+ Reply::Parsed(_)
506
+ ),
507
+ "distinct byte keys must not become a duplicate-key parse error"
508
+ );
509
+ // Lossy key collisions must not hide an over-deep subtree when Value keeps the last key.
510
+ let body = [
511
+ b"{\"\xff\":".as_slice(),
512
+ "[".repeat(100).as_bytes(),
513
+ b"0",
514
+ "]".repeat(100).as_bytes(),
515
+ b",\"\xfe\":0}",
516
+ ]
517
+ .concat();
518
+ assert_eq!(
519
+ decoded(raw(200, Some("application/json"), None, body.clone())),
520
+ st(200, &String::from_utf8_lossy(&body))
521
+ );
522
+ }