diff --git a/yazi-shim/src/ratatui/builder.rs b/yazi-shim/src/ratatui/builder.rs index 413217df..93681b81 100644 --- a/yazi-shim/src/ratatui/builder.rs +++ b/yazi-shim/src/ratatui/builder.rs @@ -50,9 +50,17 @@ impl LineIterBuilder { } fn parse_ansi_text<'text>(s: &'text str) -> Result, ansi_to_tui::Error> { - // SAFETY: ansi_to_tui::to_text() returns slices into the input text data. - // The public API ties that borrow to the temporary method receiver instead of - // the original `&str`, so we widen it back to `'text`, which is the lifetime of - // the source string stored by `LineIter`. - unsafe { Ok(std::mem::transmute::, Text<'text>>(s.to_text()?)) } + let text = s.to_text()?; + debug_assert!( + text.lines.iter().flat_map(|l| l.spans.iter()).all(|span| { + matches!(span.content, std::borrow::Cow::Borrowed(_)) + }), + "ansi_to_tui produced Cow::Owned content; the transmute below is unsound" + ); + // SAFETY: The zero-copy parser creates Spans whose content borrows from the + // input bytes. The trait method's `'_` lifetime is tied to the method receiver + // (`&&'text str`) rather than to the underlying string data (`&'text str`), so + // we widen it back to `'text` here. The debug_assert above verifies in debug + // builds that all Spans indeed contain Cow::Borrowed content. + unsafe { Ok(std::mem::transmute::, Text<'text>>(text)) } } diff --git a/yazi-shim/src/ratatui/span.rs b/yazi-shim/src/ratatui/span.rs index 7465b388..451c231d 100644 --- a/yazi-shim/src/ratatui/span.rs +++ b/yazi-shim/src/ratatui/span.rs @@ -67,8 +67,14 @@ impl<'lend, 'text> Iterator for SpanIter<'lend, 'text> { } let span = spans.next()?; - let Cow::Borrowed(content) = span.content else { - unreachable!("SpanIter only stores borrowed text") + let content = match &span.content { + Cow::Borrowed(s) => *s, + // Owned content cannot be safely projected to 'text: the String lives + // only as long as `span` (a local variable), so grapheme references + // would dangle after this loop iteration. Skip the span rather than + // panic. In normal usage every Span contains Cow::Borrowed text, so + // this branch is unreachable when the code is used correctly. + Cow::Owned(_) => continue, }; *current = Some(CurrentSpan { style: line_style.patch(span.style),