polyjose/error.rs
1//! The polyjose error type and its stable, cross-platform error codes.
2//!
3//! # Stability contract
4//!
5//! - **Stable:** the code strings returned by [`Error::code`] and listed in
6//! [`Error::CODES`]. Every binding (Node, WASM, Kotlin, Swift) surfaces
7//! them unchanged, and the shared conformance vectors name them in
8//! `"expect": { "ok": false, "error": "<code>" }`. A released code is never
9//! renamed or removed; new codes may be added.
10//! - **Not stable:** the `Display` text and the `&'static str` details. They
11//! are for humans and may be reworded in any release. Don't match on them.
12//!
13//! # No secrets, no input (ADR 0007)
14//!
15//! An error never contains key material or bytes of the input being
16//! processed: a detail can only be a string literal from polyjose itself,
17//! and no error wraps another error as its `source()`. polyjose parses JSON
18//! and base64url itself (ADR 0016), so no dependency's error message, which
19//! might quote input, ever reaches an `Error`. JWE decryption has a single,
20//! detail-free failure, [`Error::DecryptionFailed`] (RFC 7516 §11.5).
21//!
22//! # Codes are for logs, not for clients
23//!
24//! A code says exactly which check failed. That is what logs and
25//! diagnostics need, but don't pass it on to whoever presented the token.
26//! A server should answer every rejected token with one opaque failure,
27//! such as the `invalid_token` error of an OAuth bearer-token response
28//! (RFC 6750 §3.1), and keep the code in its own logs.
29//!
30//! Returning codes to clients lets an unauthenticated token probe the
31//! verifier's configuration. Verifying against a JWK Set, for example
32//! (ADR 0029 §4):
33//!
34//! - [`Error::NoMatchingKey`] says that no member serves the token's
35//! `kid` and `alg`, while [`Error::InvalidSignature`] says that one does;
36//! - [`Error::InvalidKey`] says that a matching `oct` member without a
37//! JWK `alg` is shorter than the hash output of the token's `HS*`
38//! (RFC 7518 §3.2).
39//!
40//! Selecting one key from a set (`JwkSet::select`, ADR 0054 §4) is
41//! likewise caller-driven diagnostics: [`Error::MultipleMatchingKeys`]
42//! against [`Error::NoMatchingKey`] says whether the selector matched
43//! several members or none, so it is not for a client either.
44//!
45//! The codes stay deterministic and stable regardless (ADR 0007); only
46//! what reaches the client should be opaque.
47//!
48//! # Adding a variant
49//!
50//! Give it a doc comment citing its RFC section, add its code to
51//! [`Error::code`] and [`Error::CODES`] (same order as the declaration), and
52//! add it to `one_of_each` in this module's tests. The compiler flags the
53//! first and the last; the tests check the list.
54
55/// Errors returned by polyjose.
56///
57/// Each variant has a stable [`code`](Error::code). The code is the contract
58/// shared by every binding and every conformance vector (ADR 0007), so it
59/// never changes once released. New variants may be added (the enum is
60/// `#[non_exhaustive]`).
61///
62/// No variant carries key material or bytes from the input being processed.
63/// Any detail is a `&'static str` written into polyjose itself, so `Debug`
64/// and `Display` never echo untrusted input or secrets (ADR 0007).
65///
66/// ```
67/// use polyjose::Error;
68///
69/// let err = Error::InvalidClaim("aud");
70/// assert_eq!(err.code(), "InvalidClaim");
71/// assert!(Error::CODES.contains(&err.code()));
72///
73/// // Match on the code (or the variant), never on the message text.
74/// assert_eq!(Error::DecryptionFailed.code(), "DecryptionFailed");
75/// ```
76#[derive(Debug, Clone, PartialEq, Eq)]
77#[non_exhaustive]
78pub enum Error {
79 /// Input is not valid unpadded base64url (RFC 7515 §2, RFC 4648 §5).
80 ///
81 /// Raised when decoding any base64url-encoded JOSE component
82 /// (RFC 7515 §5.2 steps 2, 6 and 7; RFC 7516 §5.2) or JWK member.
83 ///
84 /// In JWS verification the payload's base64url (skipped for
85 /// `b64: false`, RFC 7797 §3) is checked after the protected header is
86 /// decoded and before the whole-header checks (ADR 0048, clarifying
87 /// ADR 0026 §2).
88 InvalidBase64,
89
90 /// Input is not a valid JSON object of the expected shape.
91 ///
92 /// A JOSE Header must be a completely valid JSON object (RFC 7515 §5.2
93 /// step 3, RFC 7516 §5.2 step 3), and so must a JWT Claims Set
94 /// (RFC 7519 §7.2 step 10) and a JWK (RFC 7517 §4). That includes a
95 /// member name occurring twice in one object, a JOSE Header included
96 /// (RFC 7515 §5.2 step 4, RFC 7516 §5.2 step 4; ADR 0041 §1). The
97 /// detail names the failure category only, never the offending input.
98 InvalidJson(&'static str),
99
100 /// The JOSE Header is structurally invalid.
101 ///
102 /// For example: a required parameter such as `alg` is missing
103 /// (RFC 7515 §4.1.1, RFC 7516 §4.1.1–§4.1.2), a registered parameter
104 /// has the wrong type, the protected and unprotected headers share a
105 /// name (RFC 7515 §5.2 step 4, §7.2.1; RFC 7516 §5.2 step 4), `crit`
106 /// is malformed or not in the protected header (RFC 7515 §4.1.11,
107 /// RFC 7516 §4.1.13), or `b64` breaks the RFC 7797 §3/§6 rules.
108 ///
109 /// A member name that occurs twice **inside one** header object is not
110 /// this error: the JSON layer rejects it first, as
111 /// [`Error::InvalidJson`] (ADR 0041 §1, amending ADR 0026 §2 step 4).
112 ///
113 /// In JWS verification (ADR 0026 §2 as clarified by ADR 0048), the
114 /// per-parameter type checks and the `crit` syntax checks run while the
115 /// protected header is decoded, so before the payload's base64url is
116 /// checked. The whole-header checks (`crit` naming an absent parameter,
117 /// a missing `alg`, overlapping protected and unprotected headers,
118 /// `b64` placement) run after it.
119 InvalidHeader(&'static str),
120
121 /// The `alg` or `enc` value does not name an algorithm polyjose
122 /// implements.
123 ///
124 /// The JWS is invalid if `alg` is not a supported algorithm
125 /// (RFC 7515 §4.1.1); the same holds for JWE `alg` and `enc`
126 /// (RFC 7516 §4.1.1–§4.1.2). The rejected value is not echoed.
127 ///
128 /// Also a well-formed key or key encoding that polyjose does not
129 /// support, e.g. encrypted PKCS#8 (RFC 5958 §3) or multi-prime RSA keys
130 /// (ADR 0018, ADR 0067 §3).
131 UnsupportedAlgorithm,
132
133 /// The algorithm is supported but not in the caller's allow-list.
134 ///
135 /// Libraries must let the caller specify the accepted algorithms and
136 /// must not use any other (RFC 8725 §3.1, §2.1; ADR 0007). `none` is
137 /// never allowed.
138 AlgorithmNotAllowed,
139
140 /// The key is malformed or too weak for any use.
141 ///
142 /// For example: missing or malformed JWK members (RFC 7517 §4,
143 /// RFC 7518 §6), an RSA modulus under 2048 bits (RFC 7518 §3.3, §3.5,
144 /// §4.2), an HMAC key shorter than the hash output (RFC 7518 §3.2), or
145 /// an EC point that is not on its curve (RFC 8725 §3.4), and malformed
146 /// or non-canonical DER/PEM key encodings (ADR 0018). The detail never
147 /// contains key material.
148 InvalidKey(&'static str),
149
150 /// The key is valid but not for this algorithm or operation.
151 ///
152 /// Each key must be used with exactly one algorithm (RFC 8725 §3.1),
153 /// which blocks the RS256→HS256 confusion attack (RFC 8725 §2.1). Also
154 /// raised when the JWK `alg`, `use` or `key_ops` members forbid the
155 /// operation (RFC 7517 §4.2–§4.4).
156 KeyAlgorithmMismatch,
157
158 /// A JWS signature or MAC did not validate (RFC 7515 §5.2 step 8).
159 InvalidSignature,
160
161 /// JWE decryption failed.
162 ///
163 /// Every decryption failure maps to this one variant, whatever the
164 /// internal cause, and it deliberately carries no detail. The recipient
165 /// must not distinguish format, padding and length errors
166 /// (RFC 7516 §11.5), so no padding or key-unwrap oracle is exposed
167 /// (RFC 7516 §5.2, ADR 0007).
168 DecryptionFailed,
169
170 /// `crit` lists an extension polyjose does not understand.
171 ///
172 /// Recipients must reject the JWS/JWE in that case (RFC 7515 §4.1.11,
173 /// RFC 7516 §4.1.13). The unknown name is not echoed.
174 UnknownCritical,
175
176 /// The JWT `exp` time has passed, allowing for the configured clock
177 /// skew (RFC 7519 §4.1.4).
178 TokenExpired,
179
180 /// The JWT `nbf` time has not been reached yet, allowing for the
181 /// configured clock skew (RFC 7519 §4.1.5).
182 TokenNotYetValid,
183
184 /// A JWT claim is missing, malformed or failed validation.
185 ///
186 /// The detail names the claim (for example `"aud"`), never its value.
187 /// Covers RFC 7519 §4.1 and §7.2 and the RFC 8725 §3.8–§3.9 issuer and
188 /// audience checks.
189 InvalidClaim(&'static str),
190
191 /// The operating-system CSPRNG could not supply random bytes.
192 ///
193 /// Keys, IVs and CEKs require a cryptographically secure source
194 /// (RFC 7515 §10.1, RFC 8725 §3.5); polyjose never falls back to a
195 /// weaker one (ADR 0005).
196 RandomnessUnavailable,
197
198 /// The serialization does not have the shape its RFC requires, or the
199 /// input exceeds a configured limit (ADR 0026 §1, §7).
200 ///
201 /// For JWS: a compact JWS without exactly three dot-separated parts
202 /// (RFC 7515 §7.1, §5.2 step 1); a JSON JWS that is not an object of
203 /// the RFC 7515 §7.2.1/§7.2.2 shape (for example a missing or
204 /// non-string `signature`, an empty `signatures` array, `signatures`
205 /// mixed with top-level signature members, or an empty-string
206 /// `protected`); a payload that is present or absent against the
207 /// detached-content mode (RFC 7515 Appendix F); a `b64: false` payload
208 /// that the serialization cannot carry (RFC 7797 §5.2, §5.3); or an
209 /// input over the size or signature-count limits.
210 ///
211 /// For JWE (RFC 7516 §5.2 step 1, §7.1, §7.2): a compact JWE without
212 /// exactly five dot-separated parts (§7.1, §5.2 step 1); a JSON JWE
213 /// that is not an object of the §7.2.1/§7.2.2 shape (for example a
214 /// missing `ciphertext`, an empty `recipients` array, a flattened JWE
215 /// with `recipients`, or a member of the wrong JSON type); an input over
216 /// the size or recipient-count limits; and, on encrypt, a request the
217 /// chosen serialization cannot carry (compact with more or fewer than
218 /// one recipient, with AAD or with an unprotected header, §7.1).
219 ///
220 /// JSON syntax errors stay [`Error::InvalidJson`] and base64url errors
221 /// stay [`Error::InvalidBase64`]. The detail names the rule, never the
222 /// input.
223 InvalidFormat(&'static str),
224
225 /// No key of the caller's JWK Set may verify this JWS (ADR 0029 §4).
226 ///
227 /// Raised by JWT verification against a JWK Set when no member is a
228 /// candidate for the JWS Protected Header's `alg` and `kid`: none has
229 /// a matching key type and curve, JWK `alg`, `use`, `key_ops` and
230 /// `kid` (RFC 7515 §4.1.4, §6; RFC 7517 §4.2–§4.5, §5; RFC 8725 §3.1),
231 /// or more members match than the policy's candidate bound, which is
232 /// checked before any signature is computed (ADR 0029 §4). Neither the
233 /// `kid` nor any key is echoed.
234 NoMatchingKey,
235
236 /// More than one key of a JWK Set matches the caller's selector
237 /// (ADR 0054 §4, §5).
238 ///
239 /// A JWK Set's keys have no order of preference (RFC 7517 §5.1), and
240 /// distinct `kid` values within a set are only a SHOULD
241 /// (RFC 7517 §4.5), so two members can share a `kid`. polyjose refuses
242 /// to guess between two equally valid members.
243 ///
244 /// Only `JwkSet::select` (and the bindings' `jwks_select`) raises it.
245 /// JWT verification against a set never does: it tries every candidate
246 /// in order, and more candidates than its bound is
247 /// [`Error::NoMatchingKey`] (ADR 0029 §4). Neither the selector nor
248 /// any key is echoed.
249 MultipleMatchingKeys,
250}
251
252impl Error {
253 /// Every stable error code, in declaration order.
254 ///
255 /// Bindings and the conformance harness use this to check that an
256 /// expected `error` value names a real code.
257 pub const CODES: &'static [&'static str] = &[
258 "InvalidBase64",
259 "InvalidJson",
260 "InvalidHeader",
261 "UnsupportedAlgorithm",
262 "AlgorithmNotAllowed",
263 "InvalidKey",
264 "KeyAlgorithmMismatch",
265 "InvalidSignature",
266 "DecryptionFailed",
267 "UnknownCritical",
268 "TokenExpired",
269 "TokenNotYetValid",
270 "InvalidClaim",
271 "RandomnessUnavailable",
272 "InvalidFormat",
273 "NoMatchingKey",
274 "MultipleMatchingKeys",
275 ];
276
277 /// The stable, cross-platform code for this error (ADR 0007).
278 ///
279 /// Match on this, not on the `Display` text: the code never changes,
280 /// while the message may be reworded.
281 pub fn code(&self) -> &'static str {
282 match self {
283 Error::InvalidBase64 => "InvalidBase64",
284 Error::InvalidJson(_) => "InvalidJson",
285 Error::InvalidHeader(_) => "InvalidHeader",
286 Error::UnsupportedAlgorithm => "UnsupportedAlgorithm",
287 Error::AlgorithmNotAllowed => "AlgorithmNotAllowed",
288 Error::InvalidKey(_) => "InvalidKey",
289 Error::KeyAlgorithmMismatch => "KeyAlgorithmMismatch",
290 Error::InvalidSignature => "InvalidSignature",
291 Error::DecryptionFailed => "DecryptionFailed",
292 Error::UnknownCritical => "UnknownCritical",
293 Error::TokenExpired => "TokenExpired",
294 Error::TokenNotYetValid => "TokenNotYetValid",
295 Error::InvalidClaim(_) => "InvalidClaim",
296 Error::RandomnessUnavailable => "RandomnessUnavailable",
297 Error::InvalidFormat(_) => "InvalidFormat",
298 Error::NoMatchingKey => "NoMatchingKey",
299 Error::MultipleMatchingKeys => "MultipleMatchingKeys",
300 }
301 }
302}
303
304/// Human-readable text: a fixed phrase per variant, followed by the
305/// `&'static str` detail for the variants that carry one.
306///
307/// The text holds nothing but string literals from polyjose, so it never
308/// contains input or key material (ADR 0007). It is not stable: match on
309/// [`Error::code`] instead.
310impl core::fmt::Display for Error {
311 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
312 match self {
313 Error::InvalidBase64 => f.write_str("invalid base64url"),
314 Error::InvalidJson(detail) => write!(f, "invalid JSON: {detail}"),
315 Error::InvalidHeader(detail) => write!(f, "invalid header: {detail}"),
316 Error::UnsupportedAlgorithm => f.write_str("unsupported algorithm"),
317 Error::AlgorithmNotAllowed => f.write_str("algorithm not allowed"),
318 Error::InvalidKey(detail) => write!(f, "invalid key: {detail}"),
319 Error::KeyAlgorithmMismatch => f.write_str("key does not match the algorithm"),
320 Error::InvalidSignature => f.write_str("signature verification failed"),
321 Error::DecryptionFailed => f.write_str("decryption failed"),
322 Error::UnknownCritical => f.write_str("unknown critical header parameter"),
323 Error::TokenExpired => f.write_str("token expired"),
324 Error::TokenNotYetValid => f.write_str("token not yet valid"),
325 Error::InvalidClaim(detail) => write!(f, "invalid claim: {detail}"),
326 Error::RandomnessUnavailable => f.write_str("randomness unavailable"),
327 Error::InvalidFormat(detail) => write!(f, "invalid format: {detail}"),
328 Error::NoMatchingKey => f.write_str("no matching key"),
329 Error::MultipleMatchingKeys => f.write_str("several keys match"),
330 }
331 }
332}
333
334/// No variant has a [`source`](std::error::Error::source): an error never
335/// wraps a dependency's error, whose message could quote input (ADR 0007).
336/// In particular [`Error::DecryptionFailed`] exposes no cause, so callers
337/// cannot tell decryption failures apart (RFC 7516 §11.5).
338impl std::error::Error for Error {}
339
340/// Result alias using [`Error`].
341pub type Result<T> = core::result::Result<T, Error>;
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346
347 /// One value of every variant, in declaration order.
348 ///
349 /// Keep in sync with [`variant_index`]: adding a variant breaks that
350 /// exhaustive match at compile time, which is the reminder to add it here.
351 fn one_of_each() -> Vec<Error> {
352 vec![
353 Error::InvalidBase64,
354 Error::InvalidJson("syntax error"),
355 Error::InvalidHeader("missing \"alg\""),
356 Error::UnsupportedAlgorithm,
357 Error::AlgorithmNotAllowed,
358 Error::InvalidKey("RSA modulus is under 2048 bits"),
359 Error::KeyAlgorithmMismatch,
360 Error::InvalidSignature,
361 Error::DecryptionFailed,
362 Error::UnknownCritical,
363 Error::TokenExpired,
364 Error::TokenNotYetValid,
365 Error::InvalidClaim("aud"),
366 Error::RandomnessUnavailable,
367 Error::InvalidFormat("compact JWS must have three parts"),
368 Error::NoMatchingKey,
369 Error::MultipleMatchingKeys,
370 ]
371 }
372
373 /// Compile-time exhaustiveness guard for [`one_of_each`].
374 fn variant_index(e: &Error) -> usize {
375 match e {
376 Error::InvalidBase64 => 0,
377 Error::InvalidJson(_) => 1,
378 Error::InvalidHeader(_) => 2,
379 Error::UnsupportedAlgorithm => 3,
380 Error::AlgorithmNotAllowed => 4,
381 Error::InvalidKey(_) => 5,
382 Error::KeyAlgorithmMismatch => 6,
383 Error::InvalidSignature => 7,
384 Error::DecryptionFailed => 8,
385 Error::UnknownCritical => 9,
386 Error::TokenExpired => 10,
387 Error::TokenNotYetValid => 11,
388 Error::InvalidClaim(_) => 12,
389 Error::RandomnessUnavailable => 13,
390 Error::InvalidFormat(_) => 14,
391 Error::NoMatchingKey => 15,
392 Error::MultipleMatchingKeys => 16,
393 }
394 }
395
396 #[test]
397 fn codes_are_the_stable_names() {
398 // These strings are the cross-binding contract (ADR 0007). Changing one
399 // is a breaking change for every binding and conformance vector.
400 let expected = [
401 "InvalidBase64",
402 "InvalidJson",
403 "InvalidHeader",
404 "UnsupportedAlgorithm",
405 "AlgorithmNotAllowed",
406 "InvalidKey",
407 "KeyAlgorithmMismatch",
408 "InvalidSignature",
409 "DecryptionFailed",
410 "UnknownCritical",
411 "TokenExpired",
412 "TokenNotYetValid",
413 "InvalidClaim",
414 "RandomnessUnavailable",
415 "InvalidFormat",
416 "NoMatchingKey",
417 "MultipleMatchingKeys",
418 ];
419 let actual: Vec<&str> = one_of_each().iter().map(Error::code).collect();
420 assert_eq!(actual, expected);
421 }
422
423 #[test]
424 fn multiple_matching_keys_is_appended_last() {
425 // ADR 0054 §5: a new code is appended, so every released index
426 // keeps its meaning.
427 assert_eq!(Error::CODES.len(), 17);
428 assert_eq!(Error::CODES[15], "NoMatchingKey");
429 assert_eq!(Error::CODES[16], "MultipleMatchingKeys");
430 assert_eq!(Error::MultipleMatchingKeys.code(), "MultipleMatchingKeys");
431 assert_eq!(
432 Error::MultipleMatchingKeys.to_string(),
433 "several keys match"
434 );
435 assert_eq!(
436 format!("{:?}", Error::MultipleMatchingKeys),
437 "MultipleMatchingKeys"
438 );
439 }
440
441 #[test]
442 fn one_of_each_covers_every_variant_once() {
443 let indices: Vec<usize> = one_of_each().iter().map(variant_index).collect();
444 let expected: Vec<usize> = (0..Error::CODES.len()).collect();
445 assert_eq!(indices, expected);
446 }
447
448 #[test]
449 fn codes_list_matches_variants() {
450 let from_variants: Vec<&str> = one_of_each().iter().map(Error::code).collect();
451 assert_eq!(Error::CODES, from_variants.as_slice());
452 }
453
454 #[test]
455 fn codes_are_unique_identifiers() {
456 let mut seen = std::collections::BTreeSet::new();
457 for code in Error::CODES {
458 assert!(seen.insert(*code), "duplicate code {code}");
459 assert!(
460 !code.is_empty() && code.bytes().all(|b| b.is_ascii_alphanumeric()),
461 "code {code} must be a non-empty ASCII identifier"
462 );
463 }
464 }
465
466 #[test]
467 fn display_text_of_every_variant() {
468 // Not part of the stability contract (codes are), but pinned so a
469 // change to the formatting code cannot alter messages by accident.
470 let expected = [
471 "invalid base64url",
472 "invalid JSON: syntax error",
473 "invalid header: missing \"alg\"",
474 "unsupported algorithm",
475 "algorithm not allowed",
476 "invalid key: RSA modulus is under 2048 bits",
477 "key does not match the algorithm",
478 "signature verification failed",
479 "decryption failed",
480 "unknown critical header parameter",
481 "token expired",
482 "token not yet valid",
483 "invalid claim: aud",
484 "randomness unavailable",
485 "invalid format: compact JWS must have three parts",
486 "no matching key",
487 "several keys match",
488 ];
489 let actual: Vec<String> = one_of_each().iter().map(Error::to_string).collect();
490 assert_eq!(actual, expected);
491 }
492
493 /// A distinctive value standing in for untrusted input or key material.
494 const MARKER: &str = "pj-SECRET-marker-7f3a";
495
496 /// Asserts that neither `Debug` nor `Display` of `e` contains `MARKER`.
497 fn assert_no_marker(e: &Error) {
498 let debug = format!("{e:?}");
499 let display = e.to_string();
500 assert!(!debug.contains(MARKER), "Debug leaks input: {debug}");
501 assert!(!display.contains(MARKER), "Display leaks input: {display}");
502 }
503
504 #[test]
505 fn no_variant_renders_input_or_has_a_cause() {
506 // Details are &'static str, so a variant cannot hold runtime input;
507 // this guards the rendering and the (absent) cause chain.
508 for e in one_of_each() {
509 assert_no_marker(&e);
510 assert!(!e.to_string().is_empty(), "{} has no message", e.code());
511 assert!(
512 std::error::Error::source(&e).is_none(),
513 "{} exposes a cause",
514 e.code()
515 );
516 }
517 }
518
519 #[test]
520 fn decryption_failed_has_no_detail() {
521 // RFC 7516 §11.5, ADR 0007: one uniform failure, no oracle.
522 let e = Error::DecryptionFailed;
523 assert_eq!(format!("{e:?}"), "DecryptionFailed");
524 assert_eq!(e.to_string(), "decryption failed");
525 assert!(std::error::Error::source(&e).is_none());
526 }
527
528 /// Malformed JSON texts containing `MARKER`, each with the
529 /// [`Error::InvalidJson`] detail that [`crate::json::parse`] must return.
530 ///
531 /// The marker sits next to the fault, where a parser that quotes the
532 /// offending input (as `serde_json` does) would echo it.
533 fn marked_malformed_json() -> Vec<(Vec<u8>, &'static str)> {
534 let mut invalid_utf8 = format!("\"{MARKER}").into_bytes();
535 invalid_utf8.extend_from_slice(&[0xff, 0xfe, b'"']);
536 vec![
537 (
538 format!("{{\"{MARKER}\" {MARKER}}}").into_bytes(),
539 "unexpected character",
540 ),
541 (
542 format!("{{\"{MARKER}\":").into_bytes(),
543 "unexpected end of input",
544 ),
545 (
546 format!("\"{MARKER}\" {MARKER}").into_bytes(),
547 "trailing content after the value",
548 ),
549 (format!("\"{MARKER}\\q\"").into_bytes(), "invalid escape"),
550 (
551 format!("\"{MARKER}\\ud800\"").into_bytes(),
552 "lone surrogate escape",
553 ),
554 (
555 format!("\"{MARKER}\n\"").into_bytes(),
556 "unescaped control character in string",
557 ),
558 (
559 format!("{{\"{MARKER}\":1,\"{MARKER}\":2}}").into_bytes(),
560 "duplicate member name",
561 ),
562 (
563 format!("{{\"{MARKER}\":1e400}}").into_bytes(),
564 "number out of range",
565 ),
566 (invalid_utf8, "invalid UTF-8"),
567 ]
568 }
569
570 #[test]
571 fn json_errors_are_invalid_json_without_input() {
572 for (input, detail) in marked_malformed_json() {
573 // The probe is meaningful only if the marker is in the input.
574 assert!(
575 input.windows(MARKER.len()).any(|w| w == MARKER.as_bytes()),
576 "probe for {detail:?} lacks the marker"
577 );
578 let e = crate::json::parse(&input).err();
579 assert_eq!(e, Some(Error::InvalidJson(detail)));
580 if let Some(e) = e {
581 assert_eq!(e.code(), "InvalidJson");
582 assert_no_marker(&e);
583 assert!(std::error::Error::source(&e).is_none());
584 }
585 }
586 }
587}