parlov-core 0.7.0

Shared types, error types, and oracle class definitions for parlov.
Documentation
//! Deterministic finding ID generation from probe identity components.

use sha2::{Digest, Sha256};

/// `SHA-256(technique_id|target_url|oracle_class|method|strategy_id)` truncated to 12 hex chars.
///
/// Collision probability is negligible for practical finding counts.
#[must_use]
pub fn finding_id(
    technique_id: &str,
    target_url: &str,
    oracle_class: &str,
    method: &str,
    strategy_id: &str,
) -> String {
    let mut hasher = Sha256::new();
    hasher.update(technique_id.as_bytes());
    hasher.update(b"|");
    hasher.update(target_url.as_bytes());
    hasher.update(b"|");
    hasher.update(oracle_class.as_bytes());
    hasher.update(b"|");
    hasher.update(method.as_bytes());
    hasher.update(b"|");
    hasher.update(strategy_id.as_bytes());
    let hash = hasher.finalize();
    hex::encode(&hash[..6])
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn finding_id_is_12_hex_chars() {
        let id = finding_id(
            "cp-if-none-match",
            "https://example.com/api",
            "Existence",
            "GET",
            "s1",
        );
        assert_eq!(id.len(), 12);
        assert!(id.chars().all(|c| c.is_ascii_hexdigit()));
    }

    #[test]
    fn finding_id_is_deterministic() {
        let a = finding_id("t1", "https://x.com", "Existence", "GET", "s1");
        let b = finding_id("t1", "https://x.com", "Existence", "GET", "s1");
        assert_eq!(a, b);
    }

    #[test]
    fn finding_id_differs_on_input_change() {
        let a = finding_id("t1", "https://x.com", "Existence", "GET", "s1");
        let b = finding_id("t1", "https://x.com", "Existence", "POST", "s1");
        assert_ne!(a, b);
    }
}