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