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