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