manganis_core/
hash.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
//! Utilities for creating hashed paths to assets in Manganis. This module defines [`AssetHash`] which is used to create a hashed path to an asset in both the CLI and the macro.

use std::{
    error::Error,
    hash::{Hash, Hasher},
    io::Read,
    path::{Path, PathBuf},
};

/// An error that can occur when hashing an asset
#[derive(Debug)]
#[non_exhaustive]
pub enum AssetHashError {
    /// An io error occurred
    IoError { err: std::io::Error, path: PathBuf },
}

impl std::fmt::Display for AssetHashError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            AssetHashError::IoError { path, err } => {
                write!(f, "Failed to read file: {}; {}", path.display(), err)
            }
        }
    }
}

impl Error for AssetHashError {}

/// The opaque hash type manganis uses to identify assets. Each time an asset or asset options change, this hash will
/// change. This hash is included in the URL of the bundled asset for cache busting.
pub struct AssetHash {
    /// We use a wrapper type here to hide the exact size of the hash so we can switch to a sha hash in a minor version bump
    hash: [u8; 8],
}

impl AssetHash {
    /// Create a new asset hash
    const fn new(hash: u64) -> Self {
        Self {
            hash: hash.to_le_bytes(),
        }
    }

    /// Get the hash bytes
    pub const fn bytes(&self) -> &[u8] {
        &self.hash
    }

    /// Create a new asset hash for a file. The input file to this function should be fully resolved
    pub fn hash_file_contents(file_path: &Path) -> Result<AssetHash, AssetHashError> {
        // Create a hasher
        let mut hash = std::collections::hash_map::DefaultHasher::new();

        // If this is a folder, hash the folder contents
        if file_path.is_dir() {
            let files = std::fs::read_dir(file_path).map_err(|err| AssetHashError::IoError {
                err,
                path: file_path.to_path_buf(),
            })?;
            for file in files.flatten() {
                let path = file.path();
                Self::hash_file_contents(&path)?.bytes().hash(&mut hash);
            }
            let hash = hash.finish();
            return Ok(AssetHash::new(hash));
        }

        // Otherwise, open the file to get its contents
        let mut file = std::fs::File::open(file_path).map_err(|err| AssetHashError::IoError {
            err,
            path: file_path.to_path_buf(),
        })?;

        // We add a hash to the end of the file so it is invalidated when the bundled version of the file changes
        // The hash includes the file contents, the options, and the version of manganis. From the macro, we just
        // know the file contents, so we only include that hash
        let mut buffer = [0; 8192];
        loop {
            let read = file
                .read(&mut buffer)
                .map_err(|err| AssetHashError::IoError {
                    err,
                    path: file_path.to_path_buf(),
                })?;
            if read == 0 {
                break;
            }
            hash.write(&buffer[..read]);
        }

        Ok(AssetHash::new(hash.finish()))
    }
}