cargo / ansi-to-tui / audit
cargo : ansi-to-tui @ 8.0.1
PE Patrick Elsen signed 2026-05-27 published 2026-05-27

Claims

has-binarieshas-build-exechas-fuzz-testshas-install-exechas-integration-testshas-property-testshas-unit-testsimpl-algorithmimpl-concurrencyimpl-cryptoimpl-datastructureimpl-interpreterimpl-jitimpl-parserimpl-protocolis-benignparser-impl-safeparser-impl-testeduses-concurrencyuses-cryptouses-environmentuses-execuses-filesystemuses-interpreteruses-jituses-networkuses-unsafe

Summary

ansi-to-tui 8.0.1 parses ANSI SGR escape sequences from byte buffers into Ratatui Text structures using nom combinators. No unsafe code, no I/O, no build-time execution; zero findings.

Report

Subject

ansi-to-tui 8.0.1 converts byte buffers containing ANSI SGR escape sequences into Ratatui Text structures. The public API is a single trait, IntoText, implemented blanket-style for all T: AsRef<[u8]>. It exposes two methods: into_text (returns Text<'static>) and to_text (returns a borrowed Text<'_>, gated on the zero-copy feature). Two default features are enabled: zero-copy and simd (SIMD-accelerated UTF-8 validation via simdutf8). The crate targets Ratatui-based TUI applications that need to render coloured terminal output.

Methodology

The audit used openvet 0.6.0, diff, and grep. All five source files were read in full: src/lib.rs (136 lines), src/error.rs (26 lines), src/code.rs (140 lines), src/parser.rs (401 lines), and src/tests.rs (548 lines), totalling approximately 1,251 lines. Survey greps confirmed no unsafe blocks, no FFI, no network, filesystem, environment, process-execution, crypto, or concurrency calls anywhere in the source tree. The diff -rq contents vcs comparison found only the expected Cargo.toml normalisation difference; source files are byte-identical with the VCS checkout at the tagged commit.

Results

The published crate is byte-identical with the VCS source at the matching tag (is-benign=true). No binary assets are present (has-binaries=false). There is no build.rs and no proc-macro (has-build-exec=false, has-install-exec=false).

The crate contains no unsafe code (uses-unsafe=false), no FFI, no network I/O (uses-network=false), no filesystem access (uses-filesystem=false), no environment variable reads (uses-environment=false), no process execution (uses-exec=false), no cryptographic operations (uses-crypto=false), no JIT (uses-jit=false), no interpreter (uses-interpreter=false), and no concurrency (uses-concurrency=false). None of impl-crypto, impl-interpreter, impl-jit, impl-protocol, impl-datastructure, impl-algorithm, or impl-concurrency apply.

The crate implements an ANSI SGR escape-sequence parser (impl-parser=true) using nom 8 combinators. The parser is structured as follows: text/text_fast iterate over lines; line/line_fast extract a line up to the next newline; span/span_fast extract a styled span up to the next ESC byte or newline; style attempts to parse an SGR sequence and falls back to any_escape_sequence on failure; ansi_sgr_code parses \x1b[...m sequences; ansi_sgr_item parses individual SGR parameters; color and color_type handle 8-bit and 24-bit colour arguments.

Malformed input handling is a documented design goal: the README states "Unknown or malformed escape sequences are ignored". The any_escape_sequence function always consumes at least the ESC byte before returning, preventing any infinite loop on non-SGR escapes. UTF-8 validation is non-panicking: std::str::from_utf8 or simdutf8::basic::from_utf8 both return Result; failures propagate through map_res as nom::Err::Error, which the From impl in error.rs converts to Error::Utf8Error. No unwrap, expect, or indexing by numeric offset appears anywhere in the parser. parser-impl-safe=true.

The test suite (has-unit-tests=true, has-integration-tests=false, has-fuzz-tests=false, has-property-tests=false) contains 39 #[test] functions: 37 in src/tests.rs and 2 inline in src/parser.rs. Coverage includes plain text, multi-byte Unicode, newline normalization (LF, CR, CRLF, and mixed sequences), style carry-across newlines, all 16 named 4-bit colours and backgrounds, bright-colour codes 90-97 and 100-107, indexed 256-colour (exercised for all 256 indices in a loop), truecolor RGB, all SGR modifier codes and their off-codes, empty SGR sequences (\x1b[m treated as reset), truncated sequences, non-SGR escape sequences, and OSC codes. The test_both helper asserts that the zero-copy and owned parse paths produce identical output on every test case. parser-impl-tested=true. The parser-impl-correct claim was not asserted because no published ANSI SGR conformance suite was evaluated; the documented behaviour (ignore unknown codes) is intentional and tested.

Conclusion

ansi-to-tui 8.0.1 is a pure-Rust library with no unsafe code, no I/O, and no build-time execution. The parser delegates combinator machinery to nom 8 and produces no panics on malformed input: unknown escape sequences are consumed and ignored, invalid UTF-8 propagates as a typed error. The test suite covers the full SGR parameter space systematically, including edge cases such as truncated sequences and mixed newline styles. No findings were identified.

Findings

No findings.

Annotations(2)

src/parser.rs

src/parser.rs, line 89-116

pub(crate) fn text(mut s: &[u8]) -> IResult<&[u8], Text<'static>> {
    let mut lines = Vec::new();
    let mut last = Style::new();
    while let Ok((_s, (line, style))) = line(last)(s) {
        lines.push(line);
        last = style;
        s = _s;
        if s.is_empty() {
            break;
        }
    }
    Ok((s, Text::from(lines)))
}

#[cfg(feature = "zero-copy")]
pub(crate) fn text_fast(mut s: &[u8]) -> IResult<&[u8], Text<'_>> {
    let mut lines = Vec::new();
    let mut last = Style::new();
    while let Ok((_s, (line, style))) = line_fast(last)(s) {
        lines.push(line);
        last = style;
        s = _s;
        if s.is_empty() {
            break;
        }
    }
    Ok((s, Text::from(lines)))
}

The public parsing entry points are text (owned) and text_fast (zero-copy, feature-gated). Both loop over lines using nom combinators, consuming input until empty. The inner loop in each line/line_fast function breaks on an empty remainder and terminates on each newline, with no unbounded recursion or infinite-loop risk on well-formed or malformed input. Unknown and malformed escape sequences are consumed via any_escape_sequence, which always advances past at least the ESC byte, preventing infinite loops. UTF-8 validation occurs in span/span_fast via either std::str::from_utf8 or simdutf8::basic::from_utf8 (feature simd), both return Err on invalid bytes, which propagates as a recoverable Error::Utf8Error through map_res. Justifies impl-parser=true, parser-impl-safe=true.

src/tests.rs

src/tests.rs, line 1-548

use crate::IntoText as _;
use pretty_assertions::assert_eq;
use ratatui_core::{
    style::{Color, Style, Stylize},
    text::{Line, Span, Text},
};

#[test]
fn parses_plain_text_without_styles() {
    let string: Vec<u8> = "FOO".to_string().bytes().collect();
    test_both(string, Text::raw("FOO"));
}

#[test]
fn parses_unicode_text() {
    // These are 8 byte unicode characters.
    // First 4 bytes are for the unicode and the last 4 bytes are for the color / variant.
    let bytes = "AAA🅱️🅱️🅱️".as_bytes().to_vec();
    let output = Text::raw("AAA🅱️🅱️🅱️");
    test_both(bytes, output);
}

#[test]
fn preserves_empty_lines_when_splitting_on_newlines() {
    let bytes = "LINE_1\n\n\n\n\n\n\nLINE_8".as_bytes().to_vec();
    let output = Text::from(vec![
        Line::from("LINE_1"),
        Line::from(""),
        Line::from(""),
        Line::from(""),
        Line::from(""),
        Line::from(""),
        Line::from(""),
        Line::from("LINE_8"),
    ]);

    test_both(bytes, output);
}

#[test]
fn mixed_cr_and_lf_sequences_are_all_newlines() {
    let bytes = "A\r\n\rB\n\nC\r\r\nD".as_bytes().to_vec();
    let output = Text::from(vec![
        Line::from("A"),
        Line::from(""),
        Line::from("B"),
        Line::from(""),
        Line::from("C"),
        Line::from(""),
        Line::from("D"),
    ]);
    test_both(bytes, output);
}

#[test]
/// Treat `\r\n` as a single newline (CRLF).
///
/// This normalizes Windows line endings so the resulting `Text` is stable across platforms.
fn treats_crlf_as_single_newline() {
    let bytes = "LINE_1\r\nLINE_2\r\nLINE_3".as_bytes().to_vec();
    let output = Text::from(vec![
        Line::from("LINE_1"),
        Line::from("LINE_2"),
        Line::from("LINE_3"),
    ]);
    test_both(bytes, output);
}

#[test]
/// Treat bare `\r` as a newline.
///
/// This avoids embedding carriage returns into spans, and makes the output consistent with LF and
/// CRLF inputs.
fn treats_bare_cr_as_newline() {
    let bytes = "ABC\rDEF".as_bytes().to_vec();
    let output = Text::from(vec![Line::from("ABC"), Line::from("DEF")]);
    test_both(bytes, output);
}

#[test]
/// Normalize mixed LF and CRLF into consistent line boundaries.
fn mixed_lf_and_crlf_line_endings_are_normalized() {
    let bytes = "A\nB\r\nC\nD\r\nE".as_bytes().to_vec();
    let output = Text::from(vec![
        Line::from("A"),
        Line::from("B"),
        Line::from("C"),
        Line::from("D"),
        Line::from("E"),
    ]);
    test_both(bytes, output);
}

#[test]
/// A CRLF-only input is a single empty line.
fn crlf_only_input_is_empty_line() {
    let bytes = "\r\n".as_bytes().to_vec();
    let output = Text::raw("");
    test_both(bytes, output);
}

#[test]
/// `\r` acts as a newline, even before non-SGR escape sequences.
///
/// This crate intentionally does not implement cursor movement/erase semantics; it only produces
/// styled text lines.
fn cr_before_non_sgr_escape_sequence_starts_new_line() {
    let bytes: Vec<u8> = b"\r\x1b[KOVERWRITE".to_vec();
    let output = Text::from(vec![Line::from(""), Line::from("OVERWRITE")]);
    test_both(bytes, output);
}

#[test]
/// CRLF ends the line and style continues on the next line.
fn crlf_splits_lines_and_carries_style_across_lines() {
    let bytes: Vec<u8> = b"A\x1b[31mB\r\nC".to_vec();
    let output = Text::from(vec![
        Line::from(vec![Span::raw("A"), "B".red()]),
        Line::from("C".red()),
    ]);
    test_both(bytes, output);
}

#[test]
fn ignores_truncated_escape_sequence() {
    let bytes = b"\x1b[";
    let output = Text::raw("");
    test_both(bytes, output);
}

#[test]
fn ignores_garbage_escape_sequences() {
    let bytes: Vec<u8> = b"\x1b\x1b[0\x1b[m\x1b".to_vec();
    let output = Text::raw("");
    test_both(bytes, output);
}

#[test]
fn ignores_non_sgr_escape_sequences() {
    let bytes: Vec<u8> = b"\x1b[?25hAAABBB".to_vec();
    let output = Text::raw("AAABBB");
    test_both(bytes, output);
}

#[test]
fn ignores_osc_and_other_non_sgr_sequences() {
    // Malformed -> malformed -> empty
    let bytes = b"\x1b[4 q\x1b]12;#fab1ed\x07";
    let output = Text::raw("");
    test_both(bytes, output);
}

#[test]
fn unknown_sgr_codes_are_ignored_and_chained_items_still_apply() {
    let bytes: Vec<u8> = b"\x1b[200;31mred".to_vec();
    let output = Text::from("red".red());
    test_both(bytes, output);
}

#[test]
fn empty_sgr_sequence_is_treated_as_reset() {
    let string = b"\x1b[32mGREEN\x1b[mFOO\nFOO";
    let output = Text::from(vec![
        Line::from(vec!["GREEN".green(), Span::styled("FOO", Style::reset())]),
        Line::from(Span::styled("FOO", Style::reset())),
    ]);
    test_both(string, output);
}

#[test]
fn chained_sgr_items_in_single_escape_sequence_are_applied_in_order() {
    let bytes: Vec<u8> = b"\x1b[31;44;1mX".to_vec();
    let output = Text::from("X".red().on_blue().bold());
    test_both(bytes, output);
}

#[test]
fn does_not_emit_empty_spans_for_style_only_changes() {
    // Yellow -> Red -> Green -> "Hello" -> Reset -> "World"
    let bytes: Vec<u8> = b"\x1b[33m\x1b[31m\x1b[32mHello\x1b[0mWorld".to_vec();
    let output = Text::from(Line::from(vec![
        "Hello".green(),
        Span::styled("World", Style::reset()),
    ]));
    test_both(bytes, output);
}

#[test]
fn sgr_0_resets_style() {
    let string = "\x1b[33mA\x1b[0mB";
    let output = Text::from(Line::from(vec![
        "A".yellow(),
        Span::styled("B", Style::reset()),
    ]));
    test_both(string, output);
}

#[test]
fn sgr_1_and_22_toggle_bold() {
    let bytes = "not, \x1b[1mbold\x1b[22m, not anymore".as_bytes().to_vec();
    let output = Text::from(Line::from(vec![
        Span::raw("not, "),
        "bold".bold(),
        ", not anymore".not_bold().not_dim(),
    ]));
    test_both(bytes, output);
}

#[test]
fn sgr_2_and_22_toggle_faint() {
    let bytes = "not, \x1b[2mfaint\x1b[22m, not anymore".as_bytes().to_vec();
    let output = Text::from(Line::from(vec![
        Span::raw("not, "),
        "faint".dim(),
        ", not anymore".not_bold().not_dim(),
    ]));
    test_both(bytes, output);
}

#[test]
fn sgr_3_and_23_toggle_italic() {
    let bytes = "not, \x1b[3mitalic\x1b[23m, not anymore"
        .as_bytes()
        .to_vec();
    let output = Text::from(Line::from(vec![
        Span::raw("not, "),
        "italic".italic(),
        ", not anymore".not_italic(),
    ]));
    test_both(bytes, output);
}

#[test]
fn sgr_4_and_24_toggle_underline() {
    let bytes = "not, \x1b[4munderlined\x1b[24m, not anymore"
        .as_bytes()
        .to_vec();
    let output = Text::from(Line::from(vec![
        Span::raw("not, "),
        "underlined".underlined(),
        ", not anymore".not_underlined(),
    ]));
    test_both(bytes, output);
}

#[test]
fn sgr_5_and_25_toggle_slow_blink() {
    let bytes = "not, \x1b[5mblinking\x1b[25m, not anymore"
        .as_bytes()
        .to_vec();
    let output = Text::from(Line::from(vec![
        Span::raw("not, "),
        "blinking".slow_blink(),
        ", not anymore".not_slow_blink().not_rapid_blink(),
    ]));
    test_both(bytes, output);
}

#[test]
fn sgr_6_and_25_toggle_rapid_blink() {
    let bytes = "not, \x1b[6mrapid\x1b[25m, not anymore".as_bytes().to_vec();
    let output = Text::from(Line::from(vec![
        Span::raw("not, "),
        "rapid".rapid_blink(),
        ", not anymore".not_slow_blink().not_rapid_blink(),
    ]));
    test_both(bytes, output);
}

#[test]
fn sgr_7_and_27_toggle_reverse_video() {
    let bytes = "not, \x1b[7mreversed\x1b[27m, not anymore"
        .as_bytes()
        .to_vec();
    let output = Text::from(Line::from(vec![
        Span::raw("not, "),
        "reversed".reversed(),
        ", not anymore".not_reversed(),
    ]));
    test_both(bytes, output);
}

#[test]
fn sgr_8_and_28_toggle_conceal() {
    let bytes = "not, \x1b[8mconcealed\x1b[28m, not anymore"
        .as_bytes()
        .to_vec();
    let output = Text::from(Line::from(vec![
        Span::raw("not, "),
        "concealed".hidden(),
        ", not anymore".not_hidden(),
    ]));
    test_both(bytes, output);
}

#[test]
fn sgr_9_and_29_toggle_crossed_out() {
    let bytes = "not, \x1b[9mcrossed\x1b[29m, not anymore"
        .as_bytes()
        .to_vec();
    let output = Text::from(Line::from(vec![
        Span::raw("not, "),
        "crossed".crossed_out(),
        ", not anymore".not_crossed_out(),
    ]));
    test_both(bytes, output);
}

#[test]
fn parses_4bit_named_colors_and_backgrounds() {
    const BLACK: &str = "\x1b[30m";
    const RED: &str = "\x1b[31m";
    const GREEN: &str = "\x1b[32m";
    const YELLOW: &str = "\x1b[33m";
    const BLUE: &str = "\x1b[34m";
    const MAGENTA: &str = "\x1b[35m";
    const CYAN: &str = "\x1b[36m";
    const GRAY: &str = "\x1b[37m";

    const BLACK_BG: &str = "\x1b[40m";
    const RED_BG: &str = "\x1b[41m";
    const GREEN_BG: &str = "\x1b[42m";
    const YELLOW_BG: &str = "\x1b[43m";
    const BLUE_BG: &str = "\x1b[44m";
    const MAGENTA_BG: &str = "\x1b[45m";
    const CYAN_BG: &str = "\x1b[46m";
    const GRAY_BG: &str = "\x1b[47m";

    let bytes = format!(
        "{BLACK}black\n\
            {RED}red\n\
            {GREEN}green\n\
            {YELLOW}yellow\n\
            {BLUE}blue\n\
            {MAGENTA}magenta\n\
            {CYAN}cyan\n\
            {GRAY}gray\n\
            {BLACK}{BLACK_BG}black-bg\n\
            {RED_BG}red-bg\n\
            {GREEN_BG}green-bg\n\
            {YELLOW_BG}yellow-bg\n\
            {BLUE_BG}blue-bg\n\
            {MAGENTA_BG}magenta-bg\n\
            {CYAN_BG}cyan-bg\n\
            {GRAY_BG}gray-bg"
    )
    .into_bytes();

    let output = Text::from(vec![
        Line::from("black".black()),
        Line::from("red".red()),
        Line::from("green".green()),
        Line::from("yellow".yellow()),
        Line::from("blue".blue()),
        Line::from("magenta".magenta()),
        Line::from("cyan".cyan()),
        Line::from("gray".gray()),
        Line::from("black-bg".black().on_black()),
        Line::from("red-bg".black().on_red()),
        Line::from("green-bg".black().on_green()),
        Line::from("yellow-bg".black().on_yellow()),
        Line::from("blue-bg".black().on_blue()),
        Line::from("magenta-bg".black().on_magenta()),
        Line::from("cyan-bg".black().on_cyan()),
        Line::from("gray-bg".black().on_gray()),
    ]);

    test_both(bytes, output);
}

#[test]
fn parses_4bit_bright_colors_and_backgrounds() {
    const DARK_GRAY: &str = "\x1b[90m";
    const LIGHT_RED: &str = "\x1b[91m";
    const LIGHT_GREEN: &str = "\x1b[92m";
    const LIGHT_YELLOW: &str = "\x1b[93m";
    const LIGHT_BLUE: &str = "\x1b[94m";
    const LIGHT_MAGENTA: &str = "\x1b[95m";
    const LIGHT_CYAN: &str = "\x1b[96m";
    const WHITE: &str = "\x1b[97m";

    const DARK_GRAY_BG: &str = "\x1b[100m";
    const LIGHT_RED_BG: &str = "\x1b[101m";
    const LIGHT_GREEN_BG: &str = "\x1b[102m";
    const LIGHT_YELLOW_BG: &str = "\x1b[103m";
    const LIGHT_BLUE_BG: &str = "\x1b[104m";
    const LIGHT_MAGENTA_BG: &str = "\x1b[105m";
    const LIGHT_CYAN_BG: &str = "\x1b[106m";
    const WHITE_BG: &str = "\x1b[107m";

    let bytes = format!(
        "{DARK_GRAY}dark-gray\n\
            {LIGHT_RED}light-red\n\
            {LIGHT_GREEN}light-green\n\
            {LIGHT_YELLOW}light-yellow\n\
            {LIGHT_BLUE}light-blue\n\
            {LIGHT_MAGENTA}light-magenta\n\
            {LIGHT_CYAN}light-cyan\n\
            {WHITE}white\n\
            \x1b[30m{DARK_GRAY_BG}dark-gray-bg\n\
            {LIGHT_RED_BG}light-red-bg\n\
            {LIGHT_GREEN_BG}light-green-bg\n\
            {LIGHT_YELLOW_BG}light-yellow-bg\n\
            {LIGHT_BLUE_BG}light-blue-bg\n\
            {LIGHT_MAGENTA_BG}light-magenta-bg\n\
            {LIGHT_CYAN_BG}light-cyan-bg\n\
            {WHITE_BG}white-bg"
    )
    .into_bytes();

    let output = Text::from(vec![
        Line::from("dark-gray".dark_gray()),
        Line::from("light-red".light_red()),
        Line::from("light-green".light_green()),
        Line::from("light-yellow".light_yellow()),
        Line::from("light-blue".light_blue()),
        Line::from("light-magenta".light_magenta()),
        Line::from("light-cyan".light_cyan()),
        Line::from("white".white()),
        Line::from("dark-gray-bg".black().on_dark_gray()),
        Line::from("light-red-bg".black().on_light_red()),
        Line::from("light-green-bg".black().on_light_green()),
        Line::from("light-yellow-bg".black().on_light_yellow()),
        Line::from("light-blue-bg".black().on_light_blue()),
        Line::from("light-magenta-bg".black().on_light_magenta()),
        Line::from("light-cyan-bg".black().on_light_cyan()),
        Line::from("white-bg".black().on_white()),
    ]);

    test_both(bytes, output);
}

#[test]
fn sgr_31_and_39_toggle_foreground_color() {
    let bytes: Vec<u8> = b"\x1b[31;1mred\x1b[39mdefault".to_vec();
    let output = Text::from(Line::from(vec![
        "red".red().bold(),
        "default".bold().fg(Color::Reset),
    ]));
    test_both(bytes, output);
}

#[test]
fn sgr_44_and_49_toggle_background_color() {
    let bytes: Vec<u8> = b"\x1b[44;1mblue-bg\x1b[49mdefault".to_vec();
    let output = Text::from(Line::from(vec![
        "blue-bg".on_blue().bold(),
        "default".bold().bg(Color::Reset),
    ]));
    test_both(bytes, output);
}

#[test]
fn parses_256color_foreground_palette() {
    for i in 0..256 {
        let bytes = format!("\x1b[38;5;{}mHELLO", i).as_bytes().to_vec();
        let output = Text::from("HELLO".fg(Color::Indexed(i as u8)));
        test_both(bytes, output);
    }
}

#[test]
fn parses_256color_background_palette() {
    for i in 0..256 {
        let bytes = format!("\x1b[48;5;{}mHELLO", i).as_bytes().to_vec();
        let output = Text::from("HELLO".bg(Color::Indexed(i as u8)));
        test_both(bytes, output);
    }
}

#[test]
fn parses_truecolor_foreground() {
    let bytes: Vec<u8> = b"\x1b[38;2;100;100;100mAAABBB".to_vec();
    let output = Text::from("AAABBB".fg(Color::Rgb(100, 100, 100)));
    test_both(bytes, output);
}

#[test]
fn parses_truecolor_foreground_and_background() {
    let test_cases = [
        ((1, 2, 3), (4, 5, 6)),
        ((255, 0, 128), (0, 64, 255)),
        ((17, 34, 51), (68, 85, 102)),
    ];

    for ((fr, fg, fb), (br, bg, bb)) in test_cases {
        let bytes = format!("\x1b[38;2;{fr};{fg};{fb};48;2;{br};{bg};{bb}mHELLO")
            .as_bytes()
            .to_vec();
        let output = Text::from(
            "HELLO"
                .fg(Color::Rgb(fr, fg, fb))
                .bg(Color::Rgb(br, bg, bb)),
        );
        test_both(bytes, output);
    }
}

#[test]
fn carries_style_across_lines_and_handles_resets() {
    let bytes: Vec<u8> = String::from(
        "\u{1b}[32m* \u{1b}[0mRunning before-startup command \u{1b}[1mcommand\u{1b}[0m=make my-simple-package.cabal\n\
            \u{1b}[32m* \u{1b}[0m$ make my-simple-package.cabal\n\
            Build profile: -w ghc-9.0.2 -O1\n",
    )
    .into_bytes();
    let output = Text::from(vec![
        Line::from(vec![
            "* ".green(),
            Span::styled("Running before-startup command ", Style::reset()),
            Span::styled("command", Style::reset()).bold(),
            Span::styled("=make my-simple-package.cabal", Style::reset()),
        ]),
        Line::from(vec![
            Span::styled("* ", Style::reset()).green(),
            Span::styled("$ make my-simple-package.cabal", Style::reset()),
        ]),
        Line::from(vec![Span::styled(
            "Build profile: -w ghc-9.0.2 -O1",
            Style::reset(),
        )]),
    ]);
    test_both(bytes, output);
}

#[track_caller]
fn test_both(bytes: impl AsRef<[u8]>, other: Text) {
    let bytes = bytes.as_ref();

    #[cfg(feature = "zero-copy")]
    let zero_copy = bytes.to_text().unwrap();

    let owned = bytes.into_text().unwrap();

    #[cfg(feature = "zero-copy")]
    assert_eq!(
        zero_copy, owned,
        "zero-copy and owned version of the methods have diverged; this is a bug in the library"
    );

    assert_eq!(
        owned, other,
        "owned and other have diverged; this might be a bug in the library or a ratatui update"
    );

    #[cfg(feature = "zero-copy")]
    assert_eq!(zero_copy, other);
}

The tests module contains 37 #[test] functions covering plain text, unicode, newline normalization (LF, CR, CRLF, mixed), style carry-across lines, all named 4-bit and bright colors, indexed 256-color (a loop over 0..256), truecolor RGB, all SGR modifier codes and their off-codes (bold/22, faint/22, italic/23, underline/24, blink/25, reverse/27, conceal/28, crossed-out/29), empty and truncated escape sequences, non-SGR sequences, and OSC codes. The test_both helper asserts that the zero-copy and owned variants produce identical output. Two additional #[test] functions appear directly in parser.rs. Justifies parser-impl-tested=true, has-unit-tests=true.