polyc-crypto 2026.9.0

Provenance signatures (commonware-cryptography ed25519) for polychrome tool calls.
//! Lowercase-hex encoding shared by every signed-artifact module.
//!
//! Signatures, public keys, and payload digests travel as lowercase hex in
//! this crate's canonical JSON payloads. One encoder/decoder pair keeps the
//! four signed-artifact modules (`approval`, `mandate`, `handoff`, `grant`)
//! byte-identical instead of each carrying a private copy.

/// Encode `bytes` as lowercase hex.
#[must_use]
pub fn lower(bytes: &[u8]) -> String {
    use std::fmt::Write as _;
    let mut s = String::with_capacity(bytes.len() * 2);
    for b in bytes {
        let _ = write!(&mut s, "{b:02x}");
    }
    s
}

/// Decode a hex string into bytes.
///
/// Fail-closed: `None` on odd length or any non-hex character — callers treat
/// an undecodable field as an unverifiable artifact, never a partial one.
#[must_use]
pub fn decode(s: &str) -> Option<Vec<u8>> {
    if !s.len().is_multiple_of(2) {
        return None;
    }
    (0..s.len())
        .step_by(2)
        .map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok())
        .collect()
}

#[cfg(test)]
mod tests {
    #![allow(clippy::pedantic, clippy::nursery, missing_docs)]

    use super::*;

    #[test]
    fn round_trips_and_fails_closed() {
        assert_eq!(lower(&[0x00, 0xab, 0xff]), "00abff");
        assert_eq!(decode("00abff"), Some(vec![0x00, 0xab, 0xff]));
        assert_eq!(decode(""), Some(Vec::new()));
        assert_eq!(decode("abc"), None, "odd length fails closed");
        assert_eq!(decode("zz"), None, "non-hex fails closed");
    }
}