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}