This repository has no description
11 kB
349 lines
1//! NAR unpacking and store path listings.
2
3use std::collections::BTreeMap;
4use std::fs;
5use std::io::{self, Read, Write};
6use std::os::unix::ffi::OsStrExt;
7use std::os::unix::fs::{symlink, MetadataExt, PermissionsExt};
8use std::path::Path;
9
10use harmonia_file_core::{FileSystemObject, FileTree};
11use harmonia_utils_base_encoding::base32;
12use serde::Deserialize;
13use sha2::{Digest, Sha256};
14
15/// Does `digest` match a narinfo NarHash (`sha256:<nix32>` or base16)?
16pub fn hash_matches(expected: &str, digest: &[u8]) -> bool {
17 let Some(h) = expected.strip_prefix("sha256:") else {
18 return false;
19 };
20 let hex: String = digest.iter().map(|b| format!("{b:02x}")).collect();
21 h == base32::encode_string(digest) || h == hex
22}
23
24/// The metadata of a store path, as in the caches' `.ls` listings.
25#[derive(Debug, Clone, PartialEq)]
26pub enum Tree {
27 Dir(BTreeMap<String, Tree>),
28 File { size: u64, exec: bool },
29 Symlink(String),
30}
31
32impl Tree {
33 /// Parse a `.ls` listing (`{"version": 1, "root": {...}}`).
34 pub fn from_listing(body: &[u8]) -> io::Result<Tree> {
35 #[derive(Deserialize)]
36 struct Size {
37 size: u64,
38 }
39 #[derive(Deserialize)]
40 struct Listing {
41 root: FileTree<Size>,
42 }
43 fn tree(FileTree(t): FileTree<Size>) -> Tree {
44 match t {
45 FileSystemObject::Directory(d) => {
46 Tree::Dir(d.entries.into_iter().map(|(k, c)| (k, tree(*c))).collect())
47 }
48 FileSystemObject::Regular(r) => Tree::File {
49 size: r.contents.size,
50 exec: r.executable,
51 },
52 FileSystemObject::Symlink(s) => Tree::Symlink(s.target),
53 }
54 }
55 let l: Listing = serde_json::from_slice(body).map_err(io::Error::other)?;
56 Ok(tree(l.root))
57 }
58
59 /// Listing of an unpacked store path (for paths without `.ls`).
60 pub fn from_path(p: &Path) -> io::Result<Tree> {
61 let m = fs::symlink_metadata(p)?;
62 let t = m.file_type();
63 Ok(if t.is_dir() {
64 let mut e = BTreeMap::new();
65 for d in fs::read_dir(p)? {
66 let d = d?;
67 let name = d.file_name().into_string().map_err(|n| {
68 io::Error::other(format!("non-UTF-8 name {n:?} in {}", p.display()))
69 })?;
70 e.insert(name, Tree::from_path(&d.path())?);
71 }
72 Tree::Dir(e)
73 } else if t.is_symlink() {
74 Tree::Symlink(fs::read_link(p)?.to_string_lossy().into_owned())
75 } else {
76 Tree::File {
77 size: m.size(),
78 exec: m.mode() & 0o100 != 0,
79 }
80 })
81 }
82}
83
84/// Reads through to `inner`, hashing and counting what passes.
85pub struct HashingReader<R> {
86 inner: R,
87 pub hasher: Sha256,
88 pub n: u64,
89}
90
91impl<R: Read> Read for HashingReader<R> {
92 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
93 let n = self.inner.read(buf)?;
94 self.hasher.update(&buf[..n]);
95 self.n += n as u64;
96 Ok(n)
97 }
98}
99
100fn bad(msg: impl Into<String>) -> io::Error {
101 io::Error::new(io::ErrorKind::InvalidData, format!("NAR: {}", msg.into()))
102}
103
104struct Nar<R> {
105 r: HashingReader<R>,
106}
107
108impl<R: Read> Nar<R> {
109 fn int(&mut self) -> io::Result<u64> {
110 let mut b = [0u8; 8];
111 self.r.read_exact(&mut b)?;
112 Ok(u64::from_le_bytes(b))
113 }
114
115 fn pad(&mut self, n: u64) -> io::Result<()> {
116 let p = ((8 - n % 8) % 8) as usize;
117 let mut b = [0u8; 8];
118 self.r.read_exact(&mut b[..p])?;
119 if b[..p].iter().any(|&x| x != 0) {
120 return Err(bad("non-zero padding"));
121 }
122 Ok(())
123 }
124
125 fn str(&mut self, limit: u64) -> io::Result<Vec<u8>> {
126 let n = self.int()?;
127 if n > limit {
128 return Err(bad("string too long"));
129 }
130 let mut s = vec![0u8; n as usize];
131 self.r.read_exact(&mut s)?;
132 self.pad(n)?;
133 Ok(s)
134 }
135
136 fn expect(&mut self, s: &[u8]) -> io::Result<()> {
137 let got = self.str(4096)?;
138 if got != s {
139 return Err(bad(format!(
140 "expected {:?}, got {:?}",
141 String::from_utf8_lossy(s),
142 String::from_utf8_lossy(&got)
143 )));
144 }
145 Ok(())
146 }
147
148 fn node(&mut self, dest: &Path) -> io::Result<()> {
149 self.expect(b"(")?;
150 self.expect(b"type")?;
151 match &self.str(4096)?[..] {
152 b"regular" => {
153 let mut tag = self.str(4096)?;
154 let mut mode = 0o444;
155 if tag == b"executable" {
156 self.expect(b"")?;
157 tag = self.str(4096)?;
158 mode = 0o555;
159 }
160 if tag != b"contents" {
161 return Err(bad("expected contents"));
162 }
163 let n = self.int()?;
164 let mut out = io::BufWriter::with_capacity(1 << 20, fs::File::create(dest)?);
165 let copied = io::copy(&mut (&mut self.r).take(n), &mut out)?;
166 if copied != n {
167 return Err(bad("short file contents"));
168 }
169 out.flush()?;
170 self.pad(n)?;
171 fs::set_permissions(dest, fs::Permissions::from_mode(mode))?;
172 self.expect(b")")?;
173 }
174 b"symlink" => {
175 self.expect(b"target")?;
176 let t = self.str(4096)?;
177 symlink(std::ffi::OsStr::from_bytes(&t), dest)?;
178 self.expect(b")")?;
179 }
180 b"directory" => {
181 fs::create_dir(dest)?;
182 loop {
183 let tag = self.str(4096)?;
184 if tag == b")" {
185 break;
186 }
187 if tag != b"entry" {
188 return Err(bad("expected entry"));
189 }
190 self.expect(b"(")?;
191 self.expect(b"name")?;
192 let name = self.str(255)?;
193 if name.is_empty() || name == b"." || name == b".." || name.contains(&b'/') {
194 return Err(bad("bad entry name"));
195 }
196 self.expect(b"node")?;
197 let name = String::from_utf8(name).map_err(|_| bad("non-UTF-8 name"))?;
198 self.node(&dest.join(name))?;
199 self.expect(b")")?;
200 }
201 fs::set_permissions(dest, fs::Permissions::from_mode(0o555))?;
202 }
203 t => {
204 return Err(bad(format!(
205 "unknown type {:?}",
206 String::from_utf8_lossy(t)
207 )))
208 }
209 }
210 Ok(())
211 }
212}
213
214/// Unpack the NAR read from `r` to `dest` (which must not exist). Returns
215/// the NAR's sha256 and size; checking them is up to the caller. Fails if
216/// anything follows the NAR.
217pub fn unpack(r: impl Read, dest: &Path) -> io::Result<(Vec<u8>, u64)> {
218 let mut nar = Nar {
219 r: HashingReader {
220 inner: r,
221 hasher: Sha256::new(),
222 n: 0,
223 },
224 };
225 nar.expect(b"nix-archive-1")?;
226 nar.node(dest)?;
227 let mut rest = [0u8; 1];
228 if nar.r.inner.read(&mut rest)? != 0 {
229 return Err(bad("trailing data"));
230 }
231 let n = nar.r.n;
232 Ok((nar.r.hasher.finalize().to_vec(), n))
233}
234
235/// Remove a (partially) unpacked tree, including read-only directories.
236pub fn remove(p: &Path) -> io::Result<()> {
237 let m = match fs::symlink_metadata(p) {
238 Ok(m) => m,
239 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
240 Err(e) => return Err(e),
241 };
242 if m.is_dir() {
243 fs::set_permissions(p, fs::Permissions::from_mode(0o755))?;
244 for d in fs::read_dir(p)? {
245 remove(&d?.path())?;
246 }
247 fs::remove_dir(p)
248 } else {
249 fs::remove_file(p)
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 fn s(out: &mut Vec<u8>, b: &[u8]) {
258 out.extend_from_slice(&(b.len() as u64).to_le_bytes());
259 out.extend_from_slice(b);
260 out.resize(out.len() + (8 - b.len() % 8) % 8, 0);
261 }
262
263 fn nar() -> Vec<u8> {
264 let mut o = Vec::new();
265 for t in [&b"nix-archive-1"[..], b"(", b"type", b"directory"] {
266 s(&mut o, t);
267 }
268 for t in [&b"entry"[..], b"(", b"name", b"bin", b"node", b"("] {
269 s(&mut o, t);
270 }
271 for t in [
272 &b"type"[..],
273 b"regular",
274 b"executable",
275 b"",
276 b"contents",
277 b"#!/bin/sh\n",
278 ] {
279 s(&mut o, t);
280 }
281 for t in [&b")"[..], b")"] {
282 s(&mut o, t);
283 }
284 for t in [&b"entry"[..], b"(", b"name", b"link", b"node", b"("] {
285 s(&mut o, t);
286 }
287 for t in [
288 &b"type"[..],
289 b"symlink",
290 b"target",
291 b"bin",
292 b")",
293 b")",
294 b")",
295 ] {
296 s(&mut o, t);
297 }
298 o
299 }
300
301 #[test]
302 fn hash_matches_nix() {
303 // nix hash convert --to nix32 sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
304 let d = Sha256::digest(b"");
305 assert!(hash_matches(
306 "sha256:0mdqa9w1p6cmli6976v4wi0sw9r4p5prkj7lzfd1877wk11c9c73",
307 &d
308 ));
309 assert!(hash_matches(
310 "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
311 &d
312 ));
313 assert!(!hash_matches(
314 "0mdqa9w1p6cmli6976v4wi0sw9r4p5prkj7lzfd1877wk11c9c73",
315 &d
316 ));
317 }
318
319 #[test]
320 fn unpack_and_list() {
321 let dir = std::env::temp_dir().join(format!("lazy-store-test-{}", std::process::id()));
322 let _ = remove(&dir);
323 let data = nar();
324 let (h, n) = unpack(&data[..], &dir).unwrap();
325 assert_eq!(n, data.len() as u64);
326 assert_eq!(h, Sha256::digest(&data).to_vec());
327 let t = Tree::from_path(&dir).unwrap();
328 let mut e = BTreeMap::new();
329 e.insert(
330 "bin".into(),
331 Tree::File {
332 size: 10,
333 exec: true,
334 },
335 );
336 e.insert("link".into(), Tree::Symlink("bin".into()));
337 assert_eq!(t, Tree::Dir(e));
338 let ls = br#"{"version":1,"root":{"type":"directory","entries":{
339 "bin":{"type":"regular","size":10,"executable":true,"narOffset":96},
340 "link":{"type":"symlink","target":"bin"}}}}"#;
341 assert_eq!(Tree::from_listing(ls).unwrap(), t);
342 remove(&dir).unwrap();
343
344 let mut trailing = data.clone();
345 trailing.push(0);
346 assert!(unpack(&trailing[..], &dir).is_err());
347 remove(&dir).unwrap();
348 }
349}