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