Skip to main content

lux_lib/
hash.rs

1use bytes::Bytes;
2use nix_nar::Encoder;
3use ssri::{Algorithm, Integrity, IntegrityOpts};
4use std::fs::File;
5use std::io::{self, Read};
6use std::path::{Path, PathBuf};
7
8pub trait HasIntegrity {
9    fn hash(&self) -> io::Result<Integrity>;
10}
11
12impl HasIntegrity for PathBuf {
13    #[tracing::instrument(level = "trace")]
14    fn hash(&self) -> io::Result<Integrity> {
15        let mut integrity_opts = IntegrityOpts::new().algorithm(Algorithm::Sha256);
16        if self.is_dir() {
17            // NOTE: To ensure our source hashes are compatible with Nix,
18            // we encode the path to the Nix Archive (NAR) format.
19            let mut enc = Encoder::new(self).map_err(io::Error::other)?;
20            let mut nar_bytes = Vec::new();
21            io::copy(&mut enc, &mut nar_bytes)?;
22            integrity_opts.input(nar_bytes);
23        } else if self.is_file() {
24            hash_file(self, &mut integrity_opts)?;
25        }
26        Ok(integrity_opts.result())
27    }
28}
29
30impl HasIntegrity for Path {
31    fn hash(&self) -> io::Result<Integrity> {
32        let path_buf: PathBuf = self.into();
33        path_buf.hash()
34    }
35}
36
37impl HasIntegrity for Bytes {
38    #[tracing::instrument(level = "trace")]
39    fn hash(&self) -> io::Result<Integrity> {
40        let mut integrity_opts = IntegrityOpts::new().algorithm(Algorithm::Sha256);
41        integrity_opts.input(self);
42        Ok(integrity_opts.result())
43    }
44}
45
46fn hash_file(path: &Path, integrity_opts: &mut IntegrityOpts) -> io::Result<()> {
47    let mut file = File::open(path)?;
48    let mut buffer = Vec::new();
49    file.read_to_end(&mut buffer)?;
50    integrity_opts.input(&buffer);
51    Ok(())
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57    use assert_fs::prelude::*;
58    use std::{fs::write, process::Command};
59
60    #[cfg(unix)]
61    /// Compute nix-hash --sri --type sha256 .
62    fn nix_hash(path: &Path) -> Integrity {
63        let ssri_str = Command::new("nix-hash")
64            .args(vec!["--sri", "--type", "sha256"])
65            .arg(path)
66            .output()
67            .unwrap()
68            .stdout;
69        String::from_utf8_lossy(&ssri_str).parse().unwrap()
70    }
71
72    #[cfg(unix)]
73    /// Compute nix-hash --sri --type sha256 --flat .
74    fn nix_hash_file(path: &Path) -> Integrity {
75        let ssri_str = Command::new("nix-hash")
76            .args(vec!["--sri", "--type", "sha256", "--flat"])
77            .arg(path)
78            .output()
79            .unwrap()
80            .stdout;
81        String::from_utf8_lossy(&ssri_str).parse().unwrap()
82    }
83
84    #[test]
85    fn test_hash_empty_dir() {
86        let temp = assert_fs::TempDir::new().unwrap();
87        let hash1 = temp.path().to_path_buf().hash().unwrap();
88        let hash2 = temp.path().to_path_buf().hash().unwrap();
89        assert_eq!(hash1, hash2);
90        let nix_hash = nix_hash(temp.path());
91        assert_eq!(hash1, nix_hash);
92    }
93
94    #[test]
95    #[cfg(unix)]
96    fn test_hash_file() {
97        let temp = assert_fs::TempDir::new().unwrap();
98        let file = temp.child("test.txt");
99        file.write_str("test content").unwrap();
100
101        let hash = file.path().to_path_buf().hash().unwrap();
102        let nix_hash = nix_hash_file(file.path());
103        assert_eq!(hash, nix_hash);
104    }
105
106    #[test]
107    fn test_hash_dir_with_single_file() {
108        let temp = assert_fs::TempDir::new().unwrap();
109        let file = temp.child("test.txt");
110        file.write_str("test content").unwrap();
111
112        let hash1 = temp.path().to_path_buf().hash().unwrap();
113        let hash2 = temp.path().to_path_buf().hash().unwrap();
114        assert_eq!(hash1, hash2);
115
116        #[cfg(unix)]
117        {
118            let nix_hash = nix_hash(temp.path());
119            assert_eq!(hash1, nix_hash);
120        }
121    }
122
123    #[test]
124    fn test_hash_multiple_files_different_creation_order() {
125        let temp = assert_fs::TempDir::new().unwrap();
126
127        write(temp.child("a.txt").path(), "content a").unwrap();
128        write(temp.child("b.txt").path(), "content b").unwrap();
129        write(temp.child("c.txt").path(), "content c").unwrap();
130        let hash1 = temp.path().to_path_buf().hash().unwrap();
131
132        let temp2 = assert_fs::TempDir::new().unwrap();
133        write(temp2.child("c.txt").path(), "content c").unwrap();
134        write(temp2.child("a.txt").path(), "content a").unwrap();
135        write(temp2.child("b.txt").path(), "content b").unwrap();
136        let hash2 = temp2.path().to_path_buf().hash().unwrap();
137
138        assert_eq!(hash1, hash2);
139
140        #[cfg(unix)]
141        {
142            let nix_hash = nix_hash(temp.path());
143            assert_eq!(hash1, nix_hash);
144        }
145    }
146
147    #[test]
148    fn test_hash_nested_directories_different_creation_order() {
149        let temp = assert_fs::TempDir::new().unwrap();
150
151        temp.child("a/b").create_dir_all().unwrap();
152        temp.child("b").create_dir_all().unwrap();
153        write(temp.child("a/b/file1.txt").path(), "content 1").unwrap();
154        write(temp.child("a/file2.txt").path(), "content 2").unwrap();
155        write(temp.child("b/file3.txt").path(), "content 3").unwrap();
156        let hash1 = temp.path().to_path_buf().hash().unwrap();
157
158        let temp2 = assert_fs::TempDir::new().unwrap();
159        temp2.child("a/b").create_dir_all().unwrap();
160        temp2.child("b").create_dir_all().unwrap();
161        write(temp2.child("b/file3.txt").path(), "content 3").unwrap();
162        write(temp2.child("a/file2.txt").path(), "content 2").unwrap();
163        write(temp2.child("a/b/file1.txt").path(), "content 1").unwrap();
164        let hash2 = temp2.path().to_path_buf().hash().unwrap();
165
166        assert_eq!(hash1, hash2);
167    }
168
169    #[test]
170    fn test_hash_with_different_line_endings() {
171        let temp = assert_fs::TempDir::new().unwrap();
172        write(temp.child("unix.txt").path(), "line1\nline2\n").unwrap();
173        let hash1 = temp.path().to_path_buf().hash().unwrap();
174
175        let temp2 = assert_fs::TempDir::new().unwrap();
176        write(temp2.child("windows.txt").path(), "line1\r\nline2\r\n").unwrap();
177        let hash2 = temp2.path().to_path_buf().hash().unwrap();
178
179        assert_ne!(hash1, hash2);
180    }
181
182    #[test]
183    fn test_hash_with_symlinks() {
184        let temp = assert_fs::TempDir::new().unwrap();
185
186        write(temp.child("target.txt").path(), "content").unwrap();
187
188        #[cfg(target_family = "unix")]
189        std::os::unix::fs::symlink(
190            temp.child("target.txt").path(),
191            temp.child("link.txt").path(),
192        )
193        .unwrap();
194        #[cfg(target_family = "windows")]
195        std::os::windows::fs::symlink_file(
196            temp.child("target.txt").path(),
197            temp.child("link.txt").path(),
198        )
199        .unwrap();
200
201        let hash1 = temp.path().to_path_buf().hash().unwrap();
202
203        let temp2 = assert_fs::TempDir::new().unwrap();
204        write(temp2.child("target.txt").path(), "content").unwrap();
205        let hash2 = temp2.path().to_path_buf().hash().unwrap();
206
207        assert_ne!(hash1, hash2);
208
209        #[cfg(unix)]
210        {
211            let nix_hash = nix_hash(temp.path());
212            assert_eq!(hash1, nix_hash);
213        }
214    }
215}