girt 0.18.0

An incremental, idiomatic Rust implementation of Git
Documentation
use std::sync::atomic::AtomicBool;

use super::{Error, check, encoding};

#[derive(Clone, Default)]
pub(super) struct Headers(Vec<(Vec<u8>, Vec<u8>)>);
pub(super) struct HeaderBudget {
    pub(super) bytes: usize,
    pub(super) fields: usize,
}
impl Headers {
    pub(super) fn get(&self, name: &[u8]) -> Option<&[u8]> {
        self.0
            .iter()
            .rev()
            .find(|(key, _)| key.eq_ignore_ascii_case(name))
            .map(|(_, value)| value.as_slice())
    }
    pub(super) fn decode(
        &self,
        name: &[u8],
        budget: &mut usize,
        cancel: &AtomicBool,
    ) -> Result<Option<String>, Error> {
        self.get(name)
            .map(|value| {
                let text = encoding::header(value, cancel)?;
                if text.len() > *budget {
                    return Err(Error::Limit("decoded bytes"));
                }
                *budget -= text.len();
                Ok(text)
            })
            .transpose()
    }
    pub(super) fn overridden(mut self, inner: Self) -> Self {
        for name in [b"from".as_slice(), b"subject", b"date"] {
            if let Some(value) = inner.get(name) {
                self.0.retain(|(key, _)| !key.eq_ignore_ascii_case(name));
                self.0.push((name.to_vec(), value.to_vec()));
            }
        }
        self
    }
}
pub(super) fn read<'a>(
    input: &'a [u8],
    budget: &mut HeaderBudget,
    in_body: bool,
    cancel: &AtomicBool,
) -> Result<(Headers, &'a [u8]), Error> {
    read_lines(
        input,
        input.split_inclusive(|&byte| byte == b'\n'),
        budget,
        in_body,
        cancel,
    )
}

pub(super) fn read_lines<'a>(
    input: &'a [u8],
    lines: impl Iterator<Item = &'a [u8]>,
    budget: &mut HeaderBudget,
    in_body: bool,
    cancel: &AtomicBool,
) -> Result<(Headers, &'a [u8]), Error> {
    let mut result = Headers::default();
    let mut offset = 0;
    for line in lines {
        check(cancel)?;
        let value = line.strip_suffix(b"\n").unwrap_or(line);
        if value.is_empty() {
            offset += line.len();
            break;
        }
        let continuation = value.starts_with(b" ") || value.starts_with(b"\t");
        let field = crate::bytes::split_once(value, b':');
        if !continuation && field.is_none() {
            break;
        }
        if in_body
            && !continuation
            && !field.is_some_and(|(key, _)| {
                [b"from".as_slice(), b"subject", b"date"]
                    .iter()
                    .any(|name| key.eq_ignore_ascii_case(name))
            })
        {
            break;
        }
        if line.len() > budget.bytes {
            return Err(Error::Limit("header bytes"));
        }
        budget.bytes -= line.len();
        if value.contains(&0) {
            return Err(Error::Malformed("header NUL"));
        }
        if continuation {
            let (_, previous) = result
                .0
                .last_mut()
                .ok_or(Error::Malformed("header continuation"))?;
            previous.push(b' ');
            previous.extend_from_slice(value.trim_ascii());
        } else if let Some((name, value)) = field {
            if budget.fields == 0 {
                return Err(Error::Limit("headers"));
            }
            budget.fields -= 1;
            let mut value = value.trim_ascii().to_vec();
            if in_body && !name.eq_ignore_ascii_case(b"subject") {
                value.push(b' ');
            }
            result.0.push((name.to_ascii_lowercase(), value));
        }
        offset += line.len();
    }
    Ok((result, &input[offset..]))
}