This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

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}