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