Fix Cow::Owned panic and transmute safety in SpanIter/parse_ansi_text

- span.rs: Replace unreachable!() for Cow::Owned with continue (skip the
  span). Owned content's String lives only for the local span variable's
  scope, so producing Graphemes<'text> from it would create dangling
  references. Skipping is safe and this branch is never reached in practice.

- builder.rs: Strengthen the unsafe transmute with a debug_assert that
  verifies all Span contents are Cow::Borrowed at runtime in debug builds,
  validating the safety invariant that ansi_to_tui's zero-copy parser always
  borrows from the input bytes. Expanded SAFETY comment explains the lifetime
  mismatch that makes the transmute necessary.

Co-authored-by: sxyazi <17523360+sxyazi@users.noreply.github.com>
Agent-Logs-Url: https://github.com/sxyazi/yazi/sessions/9bca4d59-d1bb-47dc-9799-13960d106841
This commit is contained in:
copilot-swe-agent[bot] 2026-03-23 08:41:46 +00:00
parent 71bacce721
commit b5314d4026
2 changed files with 21 additions and 7 deletions

View file

@ -50,9 +50,17 @@ impl LineIterBuilder {
}
fn parse_ansi_text<'text>(s: &'text str) -> Result<Text<'text>, 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<'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>>(text)) }
}

View file

@ -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),