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 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 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 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}