AkurAI Build
Menu

Public repository / master

AkurAI-Framework / crates/assets/src/fingerprint.rs

Latest change 9df51040bf1687e4dc8a91557f02df52e8d5cf3a - feat(assets): content-hash fingerprinting + conservative minifiers by Ólafur Búi Ólafsson

//! Content-hash fingerprinting for cache-busting static assets.
//!
//! The hash is a 64-bit [FNV-1a](https://en.wikipedia.org/wiki/Fowler–Noll–Vo_hash_function)
//! digest rendered as zero-padded lowercase hex. FNV-1a is not cryptographic —
//! it does not need to be. The only requirement is that the same bytes always
//! map to the same short string and that a single changed byte reliably changes
//! the digest, which is exactly what cache-busting needs.

const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;

/// Content hash of `bytes` as a stable, lowercase, 16-char hex string.
///
/// Deterministic: identical input always yields the same digest, and flipping a
/// single byte changes it. Suitable as the cache-busting token in a filename.
///
/// ```
/// use akurai_assets::fingerprint;
/// assert_eq!(fingerprint(b"hello"), fingerprint(b"hello"));
/// assert_ne!(fingerprint(b"hello"), fingerprint(b"hellp"));
/// assert_eq!(fingerprint(b"hello").len(), 16);
/// ```
pub fn fingerprint(bytes: &[u8]) -> String {
    let mut hash = FNV_OFFSET_BASIS;
    for &b in bytes {
        hash ^= b as u64;
        hash = hash.wrapping_mul(FNV_PRIME);
    }
    format!("{hash:016x}")
}

/// Fold the content hash of `bytes` into `name`, before the file extension.
///
/// `app.css` ⇒ `app.<hash>.css`. The hash is inserted before the **last** `.`
/// so multi-dot names keep their full stem (`vendor.min.js` ⇒
/// `vendor.min.<hash>.js`). A name with no extension just gets the hash
/// appended (`LICENSE` ⇒ `LICENSE.<hash>`). A leading-dot dotfile with no other
/// `.` (`.htaccess`) is treated as extensionless and gets the hash appended.
///
/// ```
/// use akurai_assets::hashed_name;
/// let n = hashed_name("app.css", b"x");
/// assert!(n.starts_with("app.") && n.ends_with(".css"));
/// assert_eq!(hashed_name("LICENSE", b"x").matches('.').count(), 1);
/// ```
pub fn hashed_name(name: &str, bytes: &[u8]) -> String {
    let hash = fingerprint(bytes);
    // Find the extension dot: the last '.' that is not the very first char
    // (so leading-dot dotfiles like ".htaccess" are extensionless).
    match name.rfind('.').filter(|&i| i > 0) {
        Some(dot) => {
            let (stem, ext) = name.split_at(dot); // ext includes the leading '.'
            format!("{stem}.{hash}{ext}")
        }
        None => format!("{name}.{hash}"),
    }
}

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

    #[test]
    fn fingerprint_is_stable() {
        assert_eq!(
            fingerprint(b"the same bytes"),
            fingerprint(b"the same bytes")
        );
    }

    #[test]
    fn fingerprint_is_sensitive() {
        // One byte different → different digest.
        assert_ne!(fingerprint(b"abcdef"), fingerprint(b"abcdeg"));
        // Empty vs non-empty.
        assert_ne!(fingerprint(b""), fingerprint(b"a"));
    }

    #[test]
    fn fingerprint_is_lowercase_hex_16() {
        let h = fingerprint(b"some asset contents");
        assert_eq!(h.len(), 16);
        assert!(h
            .chars()
            .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()));
    }

    #[test]
    fn hashed_name_inserts_before_extension() {
        let h = fingerprint(b"body{}");
        assert_eq!(hashed_name("app.css", b"body{}"), format!("app.{h}.css"));
    }

    #[test]
    fn hashed_name_uses_last_dot() {
        let h = fingerprint(b"x");
        assert_eq!(
            hashed_name("vendor.min.js", b"x"),
            format!("vendor.min.{h}.js")
        );
    }

    #[test]
    fn hashed_name_without_extension_appends() {
        let h = fingerprint(b"x");
        assert_eq!(hashed_name("LICENSE", b"x"), format!("LICENSE.{h}"));
    }

    #[test]
    fn hashed_name_dotfile_is_extensionless() {
        let h = fingerprint(b"x");
        assert_eq!(hashed_name(".htaccess", b"x"), format!(".htaccess.{h}"));
    }

    #[test]
    fn hashed_name_changes_with_content() {
        assert_ne!(hashed_name("app.css", b"v1"), hashed_name("app.css", b"v2"));
    }
}