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