//! UTF-8-safe bounded output tail (B8 sub-task 1). //! //! A background command can print megabytes; the completion contract only keeps a //! bounded *tail* (the most recent bytes). [`BoundedTail`] is a byte ring buffer //! capped at construction; [`bounded_tail`] renders the retained bytes as a //! `String`, trimming any partial UTF-8 sequence at the front so a multi-byte //! character never gets sliced by the cap. use std::collections::VecDeque; use domain::background_task::BACKGROUND_TASK_OUTPUT_TAIL_MAX_BYTES; /// Default tail cap when `IDEA_BG_TAIL_BYTES` is unset (8 KiB). const DEFAULT_TAIL_BYTES: usize = 8 * 1024; /// Floor so a hostile/typo'd env value can't disable the tail entirely. const MIN_TAIL_BYTES: usize = 1024; /// Resolves the tail cap in bytes from `IDEA_BG_TAIL_BYTES`. /// /// Defaults to 8 KiB, floored at 1 KiB, and clamped to the domain maximum /// ([`BACKGROUND_TASK_OUTPUT_TAIL_MAX_BYTES`], 16 KiB) so the persisted result /// always validates. #[must_use] pub fn tail_cap_bytes() -> usize { std::env::var("IDEA_BG_TAIL_BYTES") .ok() .and_then(|v| v.trim().parse::().ok()) .unwrap_or(DEFAULT_TAIL_BYTES) .clamp(MIN_TAIL_BYTES, BACKGROUND_TASK_OUTPUT_TAIL_MAX_BYTES) } /// A byte ring buffer that retains only the last `cap` bytes pushed into it. #[derive(Debug)] pub struct BoundedTail { buf: VecDeque, cap: usize, } impl BoundedTail { /// Builds a tail retaining at most `cap` bytes (a `cap` of `0` retains /// nothing but never panics). #[must_use] pub fn with_cap(cap: usize) -> Self { Self { buf: VecDeque::new(), cap, } } /// Appends a chunk, dropping the oldest bytes past the cap. pub fn push(&mut self, chunk: &[u8]) { if self.cap == 0 { return; } // If the incoming chunk alone exceeds the cap, only its tail can survive. let start = chunk.len().saturating_sub(self.cap); self.buf.extend(&chunk[start..]); let overflow = self.buf.len().saturating_sub(self.cap); if overflow > 0 { self.buf.drain(0..overflow); } } /// Returns whether any byte is retained. #[must_use] pub fn is_empty(&self) -> bool { self.buf.is_empty() } /// Renders the retained bytes as a UTF-8 string, dropping a leading partial /// multi-byte sequence left dangling by the cap. Returns `None` when empty. #[must_use] pub fn to_tail_string(&self) -> Option { if self.buf.is_empty() { return None; } let bytes: Vec = self.buf.iter().copied().collect(); Some(bounded_tail(&bytes, self.cap)) } } /// Renders the last `cap` bytes of `bytes` as a lossless-at-the-boundary /// `String`. /// /// The slice is taken from the end, then any partial UTF-8 sequence at the *front* /// (a continuation byte with no lead byte, because the cap cut mid-character) is /// skipped so the result is valid UTF-8 without replacement characters at the /// seam. Invalid bytes in the interior are still replaced lossily. #[must_use] pub fn bounded_tail(bytes: &[u8], cap: usize) -> String { let start = bytes.len().saturating_sub(cap); let mut slice = &bytes[start..]; // Only realign when we actually truncated the front (start > 0): otherwise a // legitimately leading continuation byte is just invalid input, handled below. if start > 0 { let mut skip = 0; while skip < slice.len() && is_utf8_continuation(slice[skip]) { skip += 1; } slice = &slice[skip..]; } String::from_utf8_lossy(slice).into_owned() } /// Whether `b` is a UTF-8 continuation byte (`10xxxxxx`). const fn is_utf8_continuation(b: u8) -> bool { b & 0b1100_0000 == 0b1000_0000 } #[cfg(test)] mod tests { use super::*; #[test] fn keeps_only_the_last_cap_bytes() { let mut tail = BoundedTail::with_cap(4); tail.push(b"abcdef"); assert_eq!(tail.to_tail_string().as_deref(), Some("cdef")); } #[test] fn push_bigger_than_cap_keeps_tail() { let mut tail = BoundedTail::with_cap(3); tail.push(b"0123456789"); assert_eq!(tail.to_tail_string().as_deref(), Some("789")); } #[test] fn empty_tail_is_none() { let tail = BoundedTail::with_cap(8); assert!(tail.is_empty()); assert_eq!(tail.to_tail_string(), None); } #[test] fn cut_multibyte_char_is_trimmed_at_front() { // "é" is 0xC3 0xA9. Cap of 1 would keep only the trailing 0xA9 (a lone // continuation byte); it must be dropped, yielding an empty string. let bytes = "é".as_bytes(); assert_eq!(bounded_tail(bytes, 1), ""); // Cap of 2 keeps the whole character. assert_eq!(bounded_tail(bytes, 2), "é"); } #[test] fn interior_is_preserved() { assert_eq!(bounded_tail("héllo".as_bytes(), 100), "héllo"); } }