Skip to main content

ic_host_text/normalize/
mod.rs

1//! Bounded line normalization without grammar, terminal or output policy.
2
3use std::fmt;
4
5#[cfg(test)]
6mod tests;
7
8/// UTF-8 or resource failure while normalizing text lines.
9#[derive(Debug)]
10pub enum NormalizationError {
11    /// Input bytes exceed the caller's bound before decoding.
12    InputLimit {
13        /// Observed byte count.
14        actual: usize,
15        /// Maximum permitted bytes.
16        limit: usize,
17    },
18    /// Input is not valid UTF-8; no lossy decoding is performed.
19    Utf8(std::str::Utf8Error),
20    /// Normalized text, including added line endings, would exceed the bound.
21    OutputLimit {
22        /// Maximum permitted normalized bytes.
23        limit: usize,
24    },
25    /// Storage for normalized text could not be allocated.
26    Allocation(std::collections::TryReserveError),
27}
28
29impl fmt::Display for NormalizationError {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        match self {
32            Self::InputLimit { actual, limit } => {
33                write!(
34                    f,
35                    "normalization input has {actual} bytes, exceeding {limit}"
36                )
37            }
38            Self::Utf8(_) => f.write_str("normalization input is not UTF-8"),
39            Self::OutputLimit { limit } => write!(f, "normalized text exceeds {limit} bytes"),
40            Self::Allocation(_) => f.write_str("line normalization allocation failed"),
41        }
42    }
43}
44impl std::error::Error for NormalizationError {
45    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
46        match self {
47            Self::Utf8(source) => Some(source),
48            Self::Allocation(source) => Some(source),
49            _ => None,
50        }
51    }
52}
53
54/// Normalize UTF-8 lines with an explicit input/output byte bound.
55///
56/// Uses `str::lines` / `trim_end`: remove each line's
57/// trailing Unicode whitespace, emit LF endings, and preserve blank lines.
58/// Empty input stays empty; a nonempty unterminated line gains one LF.
59/// This does not parse a grammar, escape terminal controls or canonicalize a format.
60///
61/// # Errors
62/// Returns input overflow, invalid UTF-8, normalized-output overflow or
63/// allocation failure. No partial normalized text is returned.
64pub fn normalize_lines(bytes: &[u8], max_bytes: usize) -> Result<String, NormalizationError> {
65    if bytes.len() > max_bytes {
66        return Err(NormalizationError::InputLimit {
67            actual: bytes.len(),
68            limit: max_bytes,
69        });
70    }
71    let text = std::str::from_utf8(bytes).map_err(NormalizationError::Utf8)?;
72    let mut length = 0_usize;
73    for line in text.lines() {
74        length = length
75            .checked_add(line.trim_end().len())
76            .and_then(|length| length.checked_add(1))
77            .filter(|&length| length <= max_bytes)
78            .ok_or(NormalizationError::OutputLimit { limit: max_bytes })?;
79    }
80    let mut normalized = String::new();
81    normalized
82        .try_reserve_exact(length)
83        .map_err(NormalizationError::Allocation)?;
84    for line in text.lines() {
85        normalized.push_str(line.trim_end());
86        normalized.push('\n');
87    }
88    Ok(normalized)
89}