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