Skip to main content

ic_host_tools/candid/text/
mod.rs

1//! Bounded Candid text normalization without parsing or process execution.
2
3use std::fmt;
4
5#[cfg(test)]
6mod tests;
7
8/// UTF-8 or resource failure while normalizing captured extractor output.
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    /// Captured output 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!(f, "Candid output has {actual} bytes, exceeding {limit}")
34            }
35            Self::Utf8(_) => f.write_str("Candid extractor output is not UTF-8"),
36            Self::OutputLimit { limit } => write!(f, "normalized Candid exceeds {limit} bytes"),
37            Self::Allocation(_) => f.write_str("Candid normalization allocation failed"),
38        }
39    }
40}
41impl std::error::Error for NormalizationError {
42    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
43        match self {
44            Self::Utf8(source) => Some(source),
45            Self::Allocation(source) => Some(source),
46            _ => None,
47        }
48    }
49}
50
51/// Normalize UTF-8 extractor bytes with an explicit input/output byte bound.
52///
53/// Matches Canic's `str::lines` / `trim_end` contract: remove each line's
54/// trailing Unicode whitespace, emit LF endings, and preserve blank lines.
55/// Empty input stays empty; a nonempty unterminated line gains one LF.
56/// This does not parse Candid or select an expected service contract.
57///
58/// # Errors
59/// Returns input overflow, invalid UTF-8, normalized-output overflow or
60/// allocation failure. No partial normalized text is returned.
61pub fn normalize(bytes: &[u8], max_bytes: usize) -> Result<String, NormalizationError> {
62    if bytes.len() > max_bytes {
63        return Err(NormalizationError::InputLimit {
64            actual: bytes.len(),
65            limit: max_bytes,
66        });
67    }
68    let text = std::str::from_utf8(bytes).map_err(NormalizationError::Utf8)?;
69    let mut length = 0_usize;
70    for line in text.lines() {
71        length = length
72            .checked_add(line.trim_end().len())
73            .and_then(|length| length.checked_add(1))
74            .filter(|&length| length <= max_bytes)
75            .ok_or(NormalizationError::OutputLimit { limit: max_bytes })?;
76    }
77    let mut normalized = String::new();
78    normalized
79        .try_reserve_exact(length)
80        .map_err(NormalizationError::Allocation)?;
81    for line in text.lines() {
82        normalized.push_str(line.trim_end());
83        normalized.push('\n');
84    }
85    Ok(normalized)
86}