pray-core 1.9.1

Core library for Prayfile, the package manager for the language placed before inference
Documentation
use sha2::{Digest, Sha256};

pub fn sha256_hex(bytes: &[u8]) -> String {
    let digest = Sha256::digest(bytes);
    let mut output = String::with_capacity(digest.len() * 2);
    for byte in digest {
        use std::fmt::Write as _;
        let _ = write!(output, "{byte:02x}");
    }
    output
}

pub fn sha256_prefixed(bytes: &[u8]) -> String {
    prefixed_hex_digest(Sha256::digest(bytes))
}

pub(crate) fn prefixed_hex_digest(digest: impl AsRef<[u8]>) -> String {
    let digest = digest.as_ref();
    let mut output = String::with_capacity(7 + digest.len() * 2);
    output.push_str("sha256:");
    for byte in digest {
        use std::fmt::Write as _;
        let _ = write!(output, "{byte:02x}");
    }
    output
}

pub fn marker_id(seed: &str) -> String {
    sha256_hex(seed.as_bytes())[0..8].to_string()
}

pub fn normalize_line_endings(text: &str) -> String {
    text.replace("\r\n", "\n").replace('\r', "\n")
}

pub fn checksum_managed_span_content(body: &str) -> String {
    sha256_prefixed(
        normalize_line_endings(body)
            .trim_end_matches('\n')
            .as_bytes(),
    )
}

pub fn checksum_managed_body_line_refs(body_lines: &[&str]) -> String {
    let body_lines = trim_trailing_empty_lines(body_lines);
    let mut hasher = Sha256::new();
    for (index, line) in body_lines.iter().enumerate() {
        if index > 0 {
            hasher.update(b"\n");
        }
        update_line_endings_normalized(&mut hasher, line);
    }
    prefixed_hex_digest(hasher.finalize())
}

fn trim_trailing_empty_lines<'a>(mut lines: &'a [&'a str]) -> &'a [&'a str] {
    while lines.last().is_some_and(|line| line.is_empty()) {
        lines = &lines[..lines.len() - 1];
    }
    lines
}

fn update_line_endings_normalized(hasher: &mut Sha256, line: &str) {
    if !line.contains('\r') {
        hasher.update(line.as_bytes());
        return;
    }
    hasher.update(normalize_line_endings(line).as_bytes());
}

#[cfg(test)]
mod tests {
    use super::{checksum_managed_body_line_refs, checksum_managed_span_content};

    #[test]
    fn checksum_trims_trailing_newlines() {
        let checksum = checksum_managed_span_content("alpha\nbeta\n\n");
        assert_eq!(checksum, checksum_managed_span_content("alpha\nbeta"));
    }

    #[test]
    fn checksum_body_line_refs_matches_joined_content() {
        let joined = checksum_managed_span_content("alpha\nbeta\n\n");
        let refs = checksum_managed_body_line_refs(&["alpha", "beta", "", ""]);
        assert_eq!(joined, refs);

        let blank_line = checksum_managed_span_content("alpha\n\nbeta");
        assert_eq!(
            blank_line,
            checksum_managed_body_line_refs(&["alpha", "", "beta"])
        );
    }
}