manganis_core/
hash.rs

1//! 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.
2
3use std::{
4    error::Error,
5    hash::{Hash, Hasher},
6    io::Read,
7    path::{Path, PathBuf},
8};
9
10/// An error that can occur when hashing an asset
11#[derive(Debug)]
12#[non_exhaustive]
13pub enum AssetHashError {
14    /// An io error occurred
15    IoError { err: std::io::Error, path: PathBuf },
16}
17
18impl std::fmt::Display for AssetHashError {
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        match self {
21            AssetHashError::IoError { path, err } => {
22                write!(f, "Failed to read file: {}; {}", path.display(), err)
23            }
24        }
25    }
26}
27
28impl Error for AssetHashError {}
29
30/// The opaque hash type manganis uses to identify assets. Each time an asset or asset options change, this hash will
31/// change. This hash is included in the URL of the bundled asset for cache busting.
32pub struct AssetHash {
33    /// 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
34    hash: [u8; 8],
35}
36
37impl AssetHash {
38    /// Create a new asset hash
39    const fn new(hash: u64) -> Self {
40        Self {
41            hash: hash.to_le_bytes(),
42        }
43    }
44
45    /// Get the hash bytes
46    pub const fn bytes(&self) -> &[u8] {
47        &self.hash
48    }
49
50    /// Create a new asset hash for a file. The input file to this function should be fully resolved
51    pub fn hash_file_contents(file_path: &Path) -> Result<AssetHash, AssetHashError> {
52        // Create a hasher
53        let mut hash = std::collections::hash_map::DefaultHasher::new();
54
55        // If this is a folder, hash the folder contents
56        if file_path.is_dir() {
57            let files = std::fs::read_dir(file_path).map_err(|err| AssetHashError::IoError {
58                err,
59                path: file_path.to_path_buf(),
60            })?;
61            for file in files.flatten() {
62                let path = file.path();
63                Self::hash_file_contents(&path)?.bytes().hash(&mut hash);
64            }
65            let hash = hash.finish();
66            return Ok(AssetHash::new(hash));
67        }
68
69        // Otherwise, open the file to get its contents
70        let mut file = std::fs::File::open(file_path).map_err(|err| AssetHashError::IoError {
71            err,
72            path: file_path.to_path_buf(),
73        })?;
74
75        // We add a hash to the end of the file so it is invalidated when the bundled version of the file changes
76        // The hash includes the file contents, the options, and the version of manganis. From the macro, we just
77        // know the file contents, so we only include that hash
78        let mut buffer = [0; 8192];
79        loop {
80            let read = file
81                .read(&mut buffer)
82                .map_err(|err| AssetHashError::IoError {
83                    err,
84                    path: file_path.to_path_buf(),
85                })?;
86            if read == 0 {
87                break;
88            }
89            hash.write(&buffer[..read]);
90        }
91
92        Ok(AssetHash::new(hash.finish()))
93    }
94}