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