parlov-core 0.5.0

Shared types, error types, and oracle class definitions for parlov.
Documentation
//! Deterministic finding ID generation from probe identity components.
//!
//! Produces a stable, short hex fingerprint by SHA-256 hashing the canonical
//! identity fields of a finding, then truncating to 12 hex characters (6 bytes).

use sha2::{Digest, Sha256};

/// Computes a deterministic 12-character hex finding ID.
///
/// The ID is derived from `SHA-256(technique_id|target_url|oracle_class|method|strategy_id)`,
/// truncated to the first 6 bytes (12 hex characters). 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(format!(
        "{technique_id}|{target_url}|{oracle_class}|{method}|{strategy_id}"
    ));
    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);
    }
}