Skip to main content

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}