Skip to main content

azul_css/props/layout/
text.rs

1//! CSS `text-justify` property.
2//!
3//! Defines [`LayoutTextJustify`] and its parser [`parse_layout_text_justify`],
4//! used by the CSS property parsing pipeline.
5
6use crate::corety::AzString;
7use alloc::string::{String, ToString};
8use core::fmt;
9
10use crate::{codegen::format::FormatAsRustCode, props::formatter::PrintAsCssValue};
11
12/// CSS `text-justify` property value.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
14#[repr(C)]
15#[derive(Default)]
16pub enum LayoutTextJustify {
17    #[default]
18    Auto,
19    None,
20    InterWord,
21    InterCharacter,
22    /// Legacy value; the parser maps `"distribute"` to `InterCharacter` per spec.
23    /// Retained for `#[repr(C)]` FFI backward compatibility.
24    Distribute,
25}
26
27impl PrintAsCssValue for LayoutTextJustify {
28    fn print_as_css_value(&self) -> String {
29        match self {
30            Self::Auto => "auto",
31            Self::None => "none",
32            Self::InterWord => "inter-word",
33            Self::InterCharacter => "inter-character",
34            Self::Distribute => "distribute",
35        }
36        .to_string()
37    }
38}
39
40impl FormatAsRustCode for LayoutTextJustify {
41    fn format_as_rust_code(&self, _tabs: usize) -> String {
42        format!("LayoutTextJustify::{self:?}")
43    }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum TextJustifyParseError<'a> {
48    InvalidValue(&'a str),
49}
50
51impl fmt::Display for TextJustifyParseError<'_> {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        match self {
54            TextJustifyParseError::InvalidValue(s) => {
55                write!(f, "Invalid text-justify value: '{s}'.")
56            }
57        }
58    }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
62#[repr(C, u8)]
63pub enum TextJustifyParseErrorOwned {
64    InvalidValue(AzString),
65}
66
67impl TextJustifyParseError<'_> {
68    #[must_use]
69    pub fn to_owned(&self) -> TextJustifyParseErrorOwned {
70        match self {
71            TextJustifyParseError::InvalidValue(s) => {
72                TextJustifyParseErrorOwned::InvalidValue((*s).to_string().into())
73            }
74        }
75    }
76}
77
78impl TextJustifyParseErrorOwned {
79    #[must_use]
80    pub fn to_borrowed(&self) -> TextJustifyParseError<'_> {
81        match self {
82            Self::InvalidValue(s) => TextJustifyParseError::InvalidValue(s.as_str()),
83        }
84    }
85}
86
87/// Parses a `text-justify` CSS value string into a [`LayoutTextJustify`].
88/// # Errors
89///
90/// Returns an error if `input` is not a valid CSS `text-justify` value.
91pub fn parse_layout_text_justify(
92    input: &str,
93) -> Result<LayoutTextJustify, TextJustifyParseError<'_>> {
94    match input.trim() {
95        "auto" => Ok(LayoutTextJustify::Auto),
96        "none" => Ok(LayoutTextJustify::None),
97        "inter-word" => Ok(LayoutTextJustify::InterWord),
98        // "distribute" is a legacy alias that computes to inter-character:
99        // +spec:text-alignment-spacing:4a88c2  +spec:text-alignment-spacing:58c33f
100        "inter-character" | "distribute" => Ok(LayoutTextJustify::InterCharacter),
101        other => Err(TextJustifyParseError::InvalidValue(other)),
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    #[test]
109    fn test_parse_layout_text_justify() {
110        assert_eq!(
111            parse_layout_text_justify("auto"),
112            Ok(LayoutTextJustify::Auto)
113        );
114        assert_eq!(
115            parse_layout_text_justify("none"),
116            Ok(LayoutTextJustify::None)
117        );
118        assert_eq!(
119            parse_layout_text_justify("inter-word"),
120            Ok(LayoutTextJustify::InterWord)
121        );
122        assert_eq!(
123            parse_layout_text_justify("inter-character"),
124            Ok(LayoutTextJustify::InterCharacter)
125        );
126        assert_eq!(
127            parse_layout_text_justify("distribute"),
128            Ok(LayoutTextJustify::InterCharacter)
129        );
130        assert!(parse_layout_text_justify("invalid").is_err());
131    }
132}
133
134#[cfg(test)]
135mod autotest_generated {
136    use super::*;
137
138    // ---------------------------------------------------------------------
139    // Variant table, kept honest by `justify_variant_index`: the match below
140    // is deliberately exhaustive (no `_` arm), so adding a variant to
141    // `LayoutTextJustify` is a compile error here rather than a silently
142    // untested variant in every loop of this module.
143    // ---------------------------------------------------------------------
144
145    const ALL_JUSTIFY: [LayoutTextJustify; 5] = [
146        LayoutTextJustify::Auto,
147        LayoutTextJustify::None,
148        LayoutTextJustify::InterWord,
149        LayoutTextJustify::InterCharacter,
150        LayoutTextJustify::Distribute,
151    ];
152
153    const fn justify_variant_index(j: LayoutTextJustify) -> usize {
154        match j {
155            LayoutTextJustify::Auto => 0,
156            LayoutTextJustify::None => 1,
157            LayoutTextJustify::InterWord => 2,
158            LayoutTextJustify::InterCharacter => 3,
159            LayoutTextJustify::Distribute => 4,
160        }
161    }
162
163    #[test]
164    fn all_justify_lists_every_variant_exactly_once() {
165        for (i, j) in ALL_JUSTIFY.iter().enumerate() {
166            assert_eq!(
167                justify_variant_index(*j),
168                i,
169                "ALL_JUSTIFY is out of sync at index {i} ({j:?})"
170            );
171        }
172    }
173
174    // ---------------------------------------------------------------------
175    // parse_layout_text_justify — positive controls
176    // ---------------------------------------------------------------------
177
178    #[test]
179    fn every_documented_keyword_parses_to_its_variant() {
180        assert_eq!(
181            parse_layout_text_justify("auto"),
182            Ok(LayoutTextJustify::Auto)
183        );
184        assert_eq!(
185            parse_layout_text_justify("none"),
186            Ok(LayoutTextJustify::None)
187        );
188        assert_eq!(
189            parse_layout_text_justify("inter-word"),
190            Ok(LayoutTextJustify::InterWord)
191        );
192        assert_eq!(
193            parse_layout_text_justify("inter-character"),
194            Ok(LayoutTextJustify::InterCharacter)
195        );
196        // Legacy alias: "distribute" computes to inter-character.
197        assert_eq!(
198            parse_layout_text_justify("distribute"),
199            Ok(LayoutTextJustify::InterCharacter)
200        );
201    }
202
203    /// `Distribute` exists only so the `#[repr(C)]` discriminants stay
204    /// backwards compatible; no input string may ever produce it, otherwise
205    /// two distinct values would represent the same computed style.
206    #[test]
207    fn no_input_ever_parses_to_the_legacy_distribute_variant() {
208        let candidates = [
209            "distribute",
210            "  distribute  ",
211            "inter-character",
212            "auto",
213            "none",
214            "inter-word",
215        ];
216        for input in candidates {
217            assert_ne!(
218                parse_layout_text_justify(input),
219                Ok(LayoutTextJustify::Distribute),
220                "{input:?} parsed to the FFI-only Distribute variant"
221            );
222        }
223    }
224
225    // ---------------------------------------------------------------------
226    // parse_layout_text_justify — empty / whitespace / trimming
227    // ---------------------------------------------------------------------
228
229    #[test]
230    fn empty_and_whitespace_only_input_is_rejected_with_an_empty_payload() {
231        // Everything here trims down to "", so the error must carry "" (not
232        // the original padding) and must not panic on the empty slice.
233        for input in ["", " ", "   ", "\t", "\n", "\r\n", " \t\r\n\u{b}\u{c} "] {
234            assert_eq!(
235                parse_layout_text_justify(input),
236                Err(TextJustifyParseError::InvalidValue("")),
237                "whitespace-only input {input:?}"
238            );
239        }
240    }
241
242    #[test]
243    fn surrounding_ascii_whitespace_is_trimmed_before_matching() {
244        for (input, expected) in [
245            ("  auto  ", LayoutTextJustify::Auto),
246            ("\tnone\t", LayoutTextJustify::None),
247            ("\n inter-word \r\n", LayoutTextJustify::InterWord),
248            ("\r\n\tdistribute\r\n\t", LayoutTextJustify::InterCharacter),
249        ] {
250            assert_eq!(parse_layout_text_justify(input), Ok(expected), "{input:?}");
251        }
252    }
253
254    /// `str::trim` is Unicode-aware, so keywords padded with Unicode
255    /// `White_Space` characters (NBSP, ideographic space, NEL) are accepted
256    /// even though CSS whitespace is only space/tab/LF/CR/FF. Pinned as the
257    /// *current* behaviour — it is more lenient than the spec, never stricter.
258    #[test]
259    fn unicode_whitespace_padding_is_also_trimmed() {
260        assert_eq!(
261            parse_layout_text_justify("\u{a0}auto\u{a0}"),
262            Ok(LayoutTextJustify::Auto),
263            "NBSP-padded keyword"
264        );
265        assert_eq!(
266            parse_layout_text_justify("\u{3000}none\u{3000}"),
267            Ok(LayoutTextJustify::None),
268            "ideographic-space-padded keyword"
269        );
270        assert_eq!(
271            parse_layout_text_justify("\u{85}inter-word\u{85}"),
272            Ok(LayoutTextJustify::InterWord),
273            "NEL-padded keyword"
274        );
275        // ...but zero-width characters are NOT `White_Space`, so they survive
276        // the trim and must be rejected rather than silently stripped.
277        assert_eq!(
278            parse_layout_text_justify("\u{200b}auto"),
279            Err(TextJustifyParseError::InvalidValue("\u{200b}auto")),
280            "zero-width space is not whitespace"
281        );
282        assert_eq!(
283            parse_layout_text_justify("\u{feff}auto"),
284            Err(TextJustifyParseError::InvalidValue("\u{feff}auto")),
285            "BOM is not whitespace"
286        );
287    }
288
289    #[test]
290    fn interior_whitespace_is_never_collapsed() {
291        for input in [
292            "inter word",
293            "inter -word",
294            "inter- word",
295            "inter - character",
296            "auto auto",
297            "auto none",
298            "au to",
299        ] {
300            assert_eq!(
301                parse_layout_text_justify(input),
302                Err(TextJustifyParseError::InvalidValue(input.trim())),
303                "interior whitespace in {input:?} must not be collapsed away"
304            );
305        }
306    }
307
308    // ---------------------------------------------------------------------
309    // parse_layout_text_justify — malformed input
310    // ---------------------------------------------------------------------
311
312    /// CSS keywords are ASCII case-insensitive (css-values-4 §3.1), but this
313    /// parser matches case-sensitively and `parse_css_property` only trims
314    /// (it does not lower-case) before dispatching here. Pinned as the current
315    /// behaviour; see the report accompanying these tests.
316    #[test]
317    fn keyword_matching_is_case_sensitive() {
318        for input in [
319            "AUTO",
320            "Auto",
321            "aUtO",
322            "NONE",
323            "Inter-Word",
324            "INTER-CHARACTER",
325            "Distribute",
326        ] {
327            assert!(
328                parse_layout_text_justify(input).is_err(),
329                "{input:?} unexpectedly parsed"
330            );
331        }
332    }
333
334    #[test]
335    fn garbage_and_near_miss_input_is_rejected_without_panicking() {
336        for input in [
337            "invalid",
338            "interword",
339            "inter_word",
340            "inter–word", // en dash, not hyphen-minus
341            "inter-",
342            "-character",
343            "inter-characters",
344            "autos",
345            "aut",
346            "auto;",
347            "auto;garbage",
348            "auto !important",
349            "auto/**/",
350            "/*auto*/",
351            "url(auto)",
352            "attr(auto)",
353            "\"auto\"",
354            "'auto'",
355            ";",
356            "{}",
357            "\\",
358            "-",
359            "--",
360            "\0",
361            "auto\0",
362            "\0auto",
363            "initial",
364            "inherit",
365            "unset",
366            "revert",
367        ] {
368            assert_eq!(
369                parse_layout_text_justify(input),
370                Err(TextJustifyParseError::InvalidValue(input.trim())),
371                "{input:?} must be rejected verbatim"
372            );
373        }
374    }
375
376    #[test]
377    fn boundary_numeric_strings_are_rejected() {
378        let big = i64::MAX.to_string();
379        let small = i64::MIN.to_string();
380        let umax = u64::MAX.to_string();
381        let fmax = f64::MAX.to_string();
382        let ftiny = f64::MIN_POSITIVE.to_string();
383        let inputs = [
384            "0",
385            "-0",
386            "+0",
387            "0.0",
388            "1",
389            "1e400",
390            "-1e-400",
391            "NaN",
392            "nan",
393            "inf",
394            "-inf",
395            "Infinity",
396            big.as_str(),
397            small.as_str(),
398            umax.as_str(),
399            fmax.as_str(),
400            ftiny.as_str(),
401        ];
402        for input in inputs {
403            assert_eq!(
404                parse_layout_text_justify(input),
405                Err(TextJustifyParseError::InvalidValue(input)),
406                "numeric-looking input {input:?} must not parse as a keyword"
407            );
408        }
409    }
410
411    #[test]
412    fn unicode_input_is_rejected_and_preserved_verbatim_in_the_error() {
413        for input in [
414            "\u{1F600}",             // emoji
415            "auto\u{301}",           // combining acute on the last char
416            "\u{301}\u{301}\u{301}", // lone combining marks
417            "auto",              // fullwidth latin
418            "аuto",                  // Cyrillic homoglyph 'а'
419            "\u{202e}auto",          // RTL override
420            "🙂🙃🙂",
421            "日本語",
422            "\u{fffd}", // replacement char
423        ] {
424            assert_eq!(
425                parse_layout_text_justify(input),
426                Err(TextJustifyParseError::InvalidValue(input)),
427                "unicode input {input:?}"
428            );
429            // The error must round-trip through the owned form untouched, i.e.
430            // no char boundary was sliced through.
431            let err = parse_layout_text_justify(input).unwrap_err();
432            assert_eq!(err.to_owned().to_borrowed(), err);
433        }
434    }
435
436    #[test]
437    fn extremely_long_input_neither_panics_nor_hangs() {
438        let million = "a".repeat(1_000_000);
439        assert_eq!(
440            parse_layout_text_justify(&million),
441            Err(TextJustifyParseError::InvalidValue(million.as_str()))
442        );
443
444        // A valid keyword repeated until it is no longer a keyword.
445        let repeated = "auto".repeat(250_000);
446        assert!(parse_layout_text_justify(&repeated).is_err());
447
448        // Huge padding around a valid keyword: trimming must still find it.
449        let padded = format!("{}auto{}", " ".repeat(500_000), "\n".repeat(500_000));
450        assert_eq!(
451            parse_layout_text_justify(&padded),
452            Ok(LayoutTextJustify::Auto)
453        );
454
455        // Huge padding around garbage: the error payload is the *trimmed*
456        // slice, so it must not carry the megabyte of padding along.
457        let padded_garbage = format!("{}garbage{}", " ".repeat(500_000), " ".repeat(500_000));
458        let err = parse_layout_text_justify(&padded_garbage).unwrap_err();
459        assert_eq!(err, TextJustifyParseError::InvalidValue("garbage"));
460    }
461
462    #[test]
463    fn deeply_nested_brackets_do_not_stack_overflow() {
464        // The parser is a flat match, but a future rewrite that recurses on
465        // nested constructs would blow the stack here instead of shipping.
466        let nested = format!("{}auto{}", "(".repeat(10_000), ")".repeat(10_000));
467        assert!(parse_layout_text_justify(&nested).is_err());
468
469        let braces = "{".repeat(50_000);
470        assert_eq!(
471            parse_layout_text_justify(&braces),
472            Err(TextJustifyParseError::InvalidValue(braces.as_str()))
473        );
474    }
475
476    /// The error borrows *into* the caller's string rather than copying it:
477    /// the returned slice must point inside `input` and equal `input.trim()`.
478    #[test]
479    fn error_payload_is_a_borrowed_subslice_of_the_input() {
480        let input = "   bogus-value   ";
481        let Err(TextJustifyParseError::InvalidValue(slice)) = parse_layout_text_justify(input)
482        else {
483            panic!("expected an error for {input:?}");
484        };
485        assert_eq!(slice, "bogus-value");
486        assert_eq!(slice, input.trim());
487
488        let base = input.as_ptr() as usize;
489        let borrowed = slice.as_ptr() as usize;
490        assert!(
491            borrowed >= base && borrowed + slice.len() <= base + input.len(),
492            "error payload does not point into the input buffer"
493        );
494    }
495
496    // ---------------------------------------------------------------------
497    // Round trip: print_as_css_value <-> parse_layout_text_justify
498    // ---------------------------------------------------------------------
499
500    #[test]
501    fn every_printed_value_parses_back_to_the_same_variant() {
502        for j in ALL_JUSTIFY {
503            let printed = j.print_as_css_value();
504            let reparsed = parse_layout_text_justify(&printed);
505            let expected = if j == LayoutTextJustify::Distribute {
506                // Documented, deliberate asymmetry: "distribute" is a legacy
507                // alias that computes to inter-character, so the round trip is
508                // lossy for exactly this one (FFI-only) variant.
509                LayoutTextJustify::InterCharacter
510            } else {
511                j
512            };
513            assert_eq!(
514                reparsed,
515                Ok(expected),
516                "round trip of {j:?} via {printed:?}"
517            );
518        }
519    }
520
521    #[test]
522    fn distribute_round_trip_is_lossy_by_design() {
523        let printed = LayoutTextJustify::Distribute.print_as_css_value();
524        assert_eq!(printed, "distribute");
525        assert_eq!(
526            parse_layout_text_justify(&printed),
527            Ok(LayoutTextJustify::InterCharacter)
528        );
529        assert_ne!(
530            parse_layout_text_justify(&printed),
531            Ok(LayoutTextJustify::Distribute)
532        );
533    }
534
535    #[test]
536    fn printed_values_are_distinct_well_formed_css_idents() {
537        let mut seen: Vec<String> = Vec::new();
538        for j in ALL_JUSTIFY {
539            let printed = j.print_as_css_value();
540            assert!(!printed.is_empty(), "{j:?} printed an empty value");
541            assert_eq!(printed.trim(), printed, "{j:?} printed padded value");
542            assert!(
543                !printed.contains(char::is_whitespace),
544                "{j:?} printed interior whitespace: {printed:?}"
545            );
546            assert!(
547                printed.chars().all(|c| c.is_ascii_lowercase() || c == '-'),
548                "{j:?} printed a non-ident value: {printed:?}"
549            );
550            assert!(
551                !seen.contains(&printed),
552                "{j:?} printed a value already used by another variant: {printed:?}"
553            );
554            seen.push(printed);
555        }
556        assert_eq!(seen.len(), ALL_JUSTIFY.len());
557    }
558
559    // ---------------------------------------------------------------------
560    // FormatAsRustCode
561    // ---------------------------------------------------------------------
562
563    #[test]
564    fn format_as_rust_code_is_a_variant_path_and_ignores_indentation() {
565        for j in ALL_JUSTIFY {
566            let code = j.format_as_rust_code(0);
567            assert_eq!(code, format!("LayoutTextJustify::{j:?}"));
568            assert!(code.starts_with("LayoutTextJustify::"));
569            assert!(!code.contains(char::is_whitespace), "{code:?}");
570            // The `tabs` argument is unused; extreme values must not panic or
571            // change the output.
572            assert_eq!(j.format_as_rust_code(usize::MAX), code);
573            assert_eq!(j.format_as_rust_code(usize::MIN), code);
574        }
575    }
576
577    // ---------------------------------------------------------------------
578    // Enum invariants: Default, FFI discriminants, Ord / Hash
579    // ---------------------------------------------------------------------
580
581    #[test]
582    fn default_is_auto() {
583        assert_eq!(LayoutTextJustify::default(), LayoutTextJustify::Auto);
584        assert_eq!(
585            parse_layout_text_justify(&LayoutTextJustify::default().print_as_css_value()),
586            Ok(LayoutTextJustify::default())
587        );
588    }
589
590    /// The `Distribute` variant is documented as retained for `#[repr(C)]` FFI
591    /// backwards compatibility, which only holds if the discriminants keep
592    /// their values. Reordering the enum breaks every compiled C/Python
593    /// binding — so it must break this test first.
594    #[test]
595    fn repr_c_discriminants_are_ffi_stable() {
596        assert_eq!(LayoutTextJustify::Auto as u8, 0);
597        assert_eq!(LayoutTextJustify::None as u8, 1);
598        assert_eq!(LayoutTextJustify::InterWord as u8, 2);
599        assert_eq!(LayoutTextJustify::InterCharacter as u8, 3);
600        assert_eq!(LayoutTextJustify::Distribute as u8, 4);
601    }
602
603    #[test]
604    fn derived_ord_follows_declaration_order() {
605        for (i, a) in ALL_JUSTIFY.iter().enumerate() {
606            for (k, b) in ALL_JUSTIFY.iter().enumerate() {
607                assert_eq!(
608                    a.cmp(b),
609                    i.cmp(&k),
610                    "Ord disagrees with declaration order for {a:?} vs {b:?}"
611                );
612                assert_eq!(a.partial_cmp(b), Some(a.cmp(b)));
613                assert_eq!(a == b, i == k);
614            }
615        }
616    }
617
618    #[test]
619    fn equal_variants_hash_equally_and_distinct_variants_do_not_collide() {
620        use std::collections::hash_map::DefaultHasher;
621        use std::hash::{Hash, Hasher};
622
623        fn hash_of(j: LayoutTextJustify) -> u64 {
624            let mut hasher = DefaultHasher::new();
625            j.hash(&mut hasher);
626            hasher.finish()
627        }
628
629        for j in ALL_JUSTIFY {
630            assert_eq!(hash_of(j), hash_of(j), "{j:?} hashes unstably");
631        }
632        let mut hashes: Vec<u64> = ALL_JUSTIFY.iter().copied().map(hash_of).collect();
633        hashes.sort_unstable();
634        hashes.dedup();
635        assert_eq!(
636            hashes.len(),
637            ALL_JUSTIFY.len(),
638            "two variants share a hash — HashMap<LayoutTextJustify, _> would be needlessly slow"
639        );
640    }
641
642    // ---------------------------------------------------------------------
643    // TextJustifyParseError: Display + owned/borrowed conversions
644    // ---------------------------------------------------------------------
645
646    /// Nasty payloads reused by the error tests below.
647    const NASTY: [&str; 9] = [
648        "",
649        "auto",
650        "\0",
651        "line\nbreak",
652        "🙂",
653        "a\u{301}",
654        "{}",
655        "{0} {1} {{}}",
656        "%s %d %n",
657    ];
658
659    #[test]
660    fn display_is_non_empty_and_quotes_the_offending_value() {
661        for value in NASTY {
662            let msg = TextJustifyParseError::InvalidValue(value).to_string();
663            assert!(!msg.is_empty(), "empty message for {value:?}");
664            assert!(
665                msg.starts_with("Invalid text-justify value: '"),
666                "unexpected prefix: {msg:?}"
667            );
668            assert!(msg.ends_with("'."), "unexpected suffix: {msg:?}");
669            assert!(
670                msg.contains(value),
671                "message {msg:?} dropped the offending value {value:?}"
672            );
673        }
674    }
675
676    /// The payload is interpolated as a *value*, never re-parsed as a format
677    /// string: braces and printf-style escapes must survive literally.
678    #[test]
679    fn display_does_not_interpret_braces_in_the_payload() {
680        let msg = TextJustifyParseError::InvalidValue("{0} {{}} %s").to_string();
681        assert_eq!(msg, "Invalid text-justify value: '{0} {{}} %s'.");
682    }
683
684    #[test]
685    fn display_survives_empty_and_megabyte_payloads() {
686        assert_eq!(
687            TextJustifyParseError::InvalidValue("").to_string(),
688            "Invalid text-justify value: ''."
689        );
690
691        let huge = "x".repeat(1_000_000);
692        let msg = TextJustifyParseError::InvalidValue(huge.as_str()).to_string();
693        // prefix + payload + suffix, with nothing truncated.
694        assert_eq!(
695            msg.len(),
696            "Invalid text-justify value: ''.".len() + huge.len()
697        );
698        assert!(msg.contains(huge.as_str()));
699    }
700
701    #[test]
702    fn to_owned_then_to_borrowed_is_the_identity() {
703        for value in NASTY {
704            let borrowed = TextJustifyParseError::InvalidValue(value);
705            let owned = borrowed.to_owned();
706            assert_eq!(
707                owned,
708                TextJustifyParseErrorOwned::InvalidValue(String::from(value).into())
709            );
710            assert_eq!(owned.to_borrowed(), borrowed, "round trip of {value:?}");
711            // The message must survive the detour through the owned form.
712            assert_eq!(owned.to_borrowed().to_string(), borrowed.to_string());
713        }
714    }
715
716    #[test]
717    fn to_owned_survives_a_large_multibyte_payload_and_keeps_its_length() {
718        let huge = "\u{1F600}".repeat(100_000); // 400 kB of 4-byte chars
719        let borrowed = TextJustifyParseError::InvalidValue(huge.as_str());
720        let owned = borrowed.to_owned();
721        let TextJustifyParseErrorOwned::InvalidValue(s) = &owned;
722        assert_eq!(s.as_str().len(), huge.len());
723        assert_eq!(owned.to_borrowed(), borrowed);
724    }
725
726    /// A parse failure carries the *trimmed* input, and that payload must
727    /// survive the borrowed -> owned -> borrowed detour byte for byte.
728    #[test]
729    fn parse_error_payload_round_trips_through_the_owned_form() {
730        for input in ["  💥  ", "\tnot-a-keyword\n", "", "   ", "\0\0"] {
731            let err = parse_layout_text_justify(input).unwrap_err();
732            let TextJustifyParseError::InvalidValue(borrowed) = &err;
733            assert_eq!(*borrowed, input.trim());
734
735            let owned = err.to_owned();
736            let TextJustifyParseErrorOwned::InvalidValue(s) = &owned;
737            assert_eq!(s.as_str(), input.trim());
738            assert_eq!(owned.to_borrowed(), err);
739        }
740    }
741
742    #[test]
743    fn owned_error_is_independent_of_the_input_buffer() {
744        // Build the error from a string that is dropped immediately after; the
745        // owned form must not dangle or lose data.
746        let owned = {
747            let scratch = String::from("  transient-garbage  ");
748            parse_layout_text_justify(&scratch).unwrap_err().to_owned()
749        };
750        let TextJustifyParseErrorOwned::InvalidValue(s) = &owned;
751        assert_eq!(s.as_str(), "transient-garbage");
752        assert_eq!(
753            owned.to_borrowed().to_string(),
754            "Invalid text-justify value: 'transient-garbage'."
755        );
756    }
757}