This repository has no description
0

Configure Feed

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

1//! The lower layer: all store paths of OUT/paths.json, read-only. Metadata 2//! comes from the caches' `.ls` listings, all fetched up front and kept in 3//! memory; the first open() of a file in a store path fetches, verifies 4//! (NarHash/NarSize) and unpacks that path's NAR. Opens of a path that is 5//! being fetched wait for that fetch. 6 7use std::collections::{BTreeMap, BTreeSet, HashMap}; 8use std::ffi::{CStr, CString}; 9use std::fs::File; 10use std::io; 11use std::mem::ManuallyDrop; 12use std::os::fd::{AsRawFd, FromRawFd}; 13use std::path::{Path, PathBuf}; 14use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; 15use std::sync::{Arc, Mutex, RwLock}; 16use std::time::{Duration, Instant}; 17 18use fuse_backend_rs::abi::fuse_abi::stat64; 19use fuse_backend_rs::api::filesystem::{ 20 Context, DirEntry, Entry, FileSystem, GetxattrReply, ListxattrReply, OpenOptions, 21 ZeroCopyWriter, 22}; 23use futures_util::{stream, StreamExt}; 24use harmonia_file_core::{Directory, FileSystemObject, FileTree, Regular, Symlink}; 25use harmonia_utils_hash::HashFormat; 26use tokio_util::io::InspectReader; 27 28use crate::closure::PathInfo; 29use crate::http::{self, Http}; 30use crate::nar::{self, Size, Tree}; 31use crate::stats::Stats; 32use crate::{Backing, STORE}; 33 34/// The store paths never change: let the kernel cache everything. 35pub const TTL: Duration = Duration::from_secs(86400); 36 37/// Set on the inode numbers (st_ino) of this layer, so they don't collide 38/// with the upper layer's. 39pub const INO_TAG: u64 = 1 << 62; 40 41enum Kind { 42 /// children sorted by name 43 Dir(Vec<(Box<str>, u32)>), 44 File { 45 size: u64, 46 exec: bool, 47 }, 48 Symlink(Box<[u8]>), 49} 50 51struct Node { 52 kind: Kind, 53 /// index into `LazyFs::names` 54 path: u32, 55 /// path below the store path (empty for its root) 56 rel: Box<str>, 57} 58 59struct Handle { 60 file: File, 61 ino: u64, 62} 63 64pub struct LazyFs { 65 names: Vec<String>, 66 infos: Vec<PathInfo>, 67 /// inode `i` is `nodes[i - 1]`; inode 1 is the store dir 68 nodes: Vec<Node>, 69 cache: PathBuf, 70 http: Http, 71 pub stats: Arc<Stats>, 72 /// per store path: where it is unpacked, once fetched 73 fetched: Vec<tokio::sync::Mutex<Option<PathBuf>>>, 74 /// for the FUSE worker threads, to fetch on the runtime 75 rt: tokio::runtime::Handle, 76 handles: RwLock<HashMap<u64, Arc<Handle>>>, 77 next_handle: AtomicU64, 78 backing: Option<Arc<Backing>>, 79 passthrough: AtomicBool, 80 /// backing id and open count per inode: the kernel wants all 81 /// passthrough opens of an inode to use the same backing file 82 backing_ids: Mutex<HashMap<u64, (u32, usize)>>, 83 backing_failed: AtomicBool, 84} 85 86fn err(e: i32) -> io::Error { 87 io::Error::from_raw_os_error(e) 88} 89 90/// `dump` (a `nix-store --load-db` input) without the store paths `failed` 91/// and everything referring to them, like `closure` leaves out what refers 92/// to paths not in the caches; and the names of the paths left out. 93fn leave_out<'a>( 94 dump: &'a str, 95 failed: impl IntoIterator<Item = &'a str>, 96) -> io::Result<(String, BTreeSet<String>)> { 97 let bad = || io::Error::other("malformed db.dump"); 98 let name = |l: &'a str| l.strip_prefix(STORE).ok_or_else(bad); 99 // per path: name, references and its lines 100 let mut records = Vec::new(); 101 let mut lines = dump.split_inclusive('\n'); 102 while let Some(p) = lines.next() { 103 let mut text = p.to_string(); 104 for _ in 0..3 { 105 text += lines.next().ok_or_else(bad)?; 106 } 107 let count = lines.next().ok_or_else(bad)?; 108 text += count; 109 let count: usize = count.trim_end().parse().map_err(|_| bad())?; 110 let mut refs = Vec::with_capacity(count); 111 for _ in 0..count { 112 let r = lines.next().ok_or_else(bad)?; 113 text += r; 114 refs.push(name(r.trim_end())?); 115 } 116 records.push((name(p.trim_end())?, refs, text)); 117 } 118 let mut referrers: HashMap<&str, Vec<&str>> = HashMap::new(); 119 for (n, refs, _) in &records { 120 for r in refs.iter().filter(|r| *r != n) { 121 referrers.entry(r).or_default().push(n); 122 } 123 } 124 let mut out: BTreeSet<&str> = failed.into_iter().collect(); 125 let mut todo: Vec<&str> = out.iter().copied().collect(); 126 while let Some(m) = todo.pop() { 127 for r in referrers.get(m).into_iter().flatten() { 128 if out.insert(r) { 129 todo.push(r); 130 } 131 } 132 } 133 let kept = records 134 .iter() 135 .filter(|(n, _, _)| !out.contains(n)) 136 .map(|(_, _, t)| t.as_str()) 137 .collect(); 138 Ok((kept, out.into_iter().map(str::to_string).collect())) 139} 140 141impl LazyFs { 142 pub async fn new(outdir: &Path, backing: Option<Arc<Backing>>) -> io::Result<LazyFs> { 143 let paths: BTreeMap<String, PathInfo> = 144 serde_json::from_slice(&std::fs::read(outdir.join("paths.json"))?)?; 145 let (names, infos): (Vec<String>, Vec<PathInfo>) = paths.into_iter().unzip(); 146 let cache = outdir.join("cache"); 147 std::fs::create_dir_all(&cache)?; 148 let http = Http::new(infos.iter().map(|i| i.cache.as_str())).await; 149 let mut fs = LazyFs { 150 fetched: names.iter().map(|_| Default::default()).collect(), 151 rt: tokio::runtime::Handle::current(), 152 names, 153 infos, 154 nodes: Vec::new(), 155 cache, 156 http, 157 stats: Arc::new(Stats::new(Some(outdir.join("stats.json")))), 158 handles: RwLock::new(HashMap::new()), 159 next_handle: AtomicU64::new(1), 160 backing, 161 passthrough: AtomicBool::new(false), 162 backing_ids: Mutex::new(HashMap::new()), 163 backing_failed: AtomicBool::new(false), 164 }; 165 fs.load_listings(outdir).await?; 166 fs.stats.save(); 167 Ok(fs) 168 } 169 170 /// Fetch the `.ls` listing of every store path (falling back to the NAR 171 /// for paths without one) and build the inode table from them. Paths 172 /// left without a listing, and everything referring to them, are left 173 /// out of OUT/paths.json and OUT/db.dump (see `leave_out`), so they are 174 /// neither served nor registered; OUT/no-listing.json counts them. 175 async fn load_listings(&mut self, outdir: &Path) -> io::Result<()> { 176 let t = Instant::now(); 177 let total = self.names.len(); 178 let mut trees: Vec<Option<io::Result<Tree>>> = (0..total).map(|_| None).collect(); 179 let this = &*self; 180 let mut done = stream::iter(0..total) 181 .map(|i| async move { (i, this.listing(i).await) }) 182 .buffer_unordered(64); 183 let mut n = 0; 184 while let Some((i, r)) = done.next().await { 185 trees[i] = Some(r); 186 n += 1; 187 if n % 500 == 0 { 188 eprintln!("listings: {n}/{total}"); 189 } 190 } 191 drop(done); 192 let trees = trees.into_iter().map(Option::unwrap); 193 let mut root = Vec::with_capacity(total); 194 self.nodes.push(Node { 195 kind: Kind::Dir(Vec::new()), 196 path: u32::MAX, 197 rel: "".into(), 198 }); 199 let mut ok = Vec::with_capacity(total); 200 let mut failed = Vec::new(); 201 for (i, t) in trees.enumerate() { 202 match t { 203 Ok(t) => ok.push((i, t)), 204 Err(e) => { 205 eprintln!("no listing for {}: {e}", self.names[i]); 206 failed.push(self.names[i].clone()); 207 } 208 } 209 } 210 let mut left_out = BTreeSet::new(); 211 if !failed.is_empty() { 212 let dump = std::fs::read_to_string(outdir.join("db.dump"))?; 213 let (kept, out) = leave_out(&dump, failed.iter().map(String::as_str))?; 214 std::fs::write(outdir.join("db.dump"), kept)?; 215 let paths: BTreeMap<&String, &PathInfo> = self 216 .names 217 .iter() 218 .zip(&self.infos) 219 .filter(|(n, _)| !out.contains(n.as_str())) 220 .collect(); 221 std::fs::write(outdir.join("paths.json"), serde_json::to_vec(&paths)?)?; 222 left_out = out; 223 } 224 std::fs::write( 225 outdir.join("no-listing.json"), 226 serde_json::to_vec(&serde_json::json!({ 227 "failed": failed, 228 "incomplete": left_out.len() - failed.len(), 229 }))?, 230 )?; 231 for (i, t) in ok { 232 if left_out.contains(&self.names[i]) { 233 continue; 234 } 235 let ino = self.add(i as u32, String::new(), t); 236 root.push((self.names[i].clone().into_boxed_str(), ino)); 237 } 238 root.sort(); 239 self.nodes[0].kind = Kind::Dir(root); 240 eprintln!( 241 "listings: {} store paths, {} inodes in {:.1} s; {} without a listing, {} referring to those left out", 242 self.names.len(), 243 self.nodes.len(), 244 t.elapsed().as_secs_f64(), 245 failed.len(), 246 left_out.len() - failed.len(), 247 ); 248 Ok(()) 249 } 250 251 async fn listing(&self, i: usize) -> io::Result<Tree> { 252 let info = &self.infos[i]; 253 let h = self.names[i].split('-').next().unwrap_or(""); 254 match self.http.get(&format!("{}/{h}.ls", info.cache)).await { 255 Ok((body, len)) => { 256 self.stats.add("ls", len as u64); 257 nar::from_listing(&body) 258 } 259 // no listing: fall back to the NAR 260 Err(http::Error::NotFound) => nar::from_path(&self.fetch(i).await?), 261 Err(e) => Err(e.into()), 262 } 263 } 264 265 /// Add the nodes of `t` (at `rel` in store path `path`), return its inode. 266 fn add(&mut self, path: u32, rel: String, FileTree(t): Tree) -> u32 { 267 let ino = self.nodes.len() as u32 + 1; 268 let (kind, entries) = match t { 269 FileSystemObject::Regular(r) => { 270 let (size, exec) = (r.contents.size, r.executable); 271 (Kind::File { size, exec }, None) 272 } 273 FileSystemObject::Symlink(s) => (Kind::Symlink(s.target.into_bytes().into()), None), 274 FileSystemObject::Directory(d) => (Kind::Dir(Vec::new()), Some(d.entries)), 275 }; 276 self.nodes.push(Node { 277 kind, 278 path, 279 rel: rel.clone().into(), 280 }); 281 if let Some(entries) = entries { 282 let mut children = Vec::with_capacity(entries.len()); 283 for (name, c) in entries { 284 let r = if rel.is_empty() { 285 name.clone() 286 } else { 287 format!("{rel}/{name}") 288 }; 289 children.push((name.into_boxed_str(), self.add(path, r, *c))); 290 } 291 // BTreeMap order is sorted already 292 self.nodes[ino as usize - 1].kind = Kind::Dir(children); 293 } 294 ino 295 } 296 297 /// The listing of `ino` as built from the `.ls` (for `fetch`). 298 fn tree_of(&self, ino: u64) -> Tree { 299 FileTree(match &self.nodes[ino as usize - 1].kind { 300 Kind::Dir(c) => FileSystemObject::Directory(Directory { 301 entries: c 302 .iter() 303 .map(|(n, i)| (n.to_string(), Box::new(self.tree_of(*i as u64)))) 304 .collect(), 305 }), 306 Kind::File { size, exec } => FileSystemObject::Regular(Regular { 307 executable: *exec, 308 contents: Size { size: *size }, 309 }), 310 Kind::Symlink(t) => FileSystemObject::Symlink(Symlink { 311 target: String::from_utf8_lossy(t).into_owned(), 312 }), 313 }) 314 } 315 316 /// Fetch store path `name` as an open() would and check that what got 317 /// unpacked matches its listing (`lazy-store fetch`, no FUSE needed). 318 pub async fn fetch_and_check(&self, name: &str) -> io::Result<PathBuf> { 319 let c = CString::new(name).map_err(io::Error::other)?; 320 let ino = self.lookup_child(1, &c).ok_or_else(|| err(libc::ENOENT))?; 321 let p = self 322 .fetch(self.nodes[ino as usize - 1].path as usize) 323 .await?; 324 if nar::from_path(&p)? != self.tree_of(ino) { 325 return Err(io::Error::other(format!( 326 "{name}: unpacked tree differs from its listing" 327 ))); 328 } 329 Ok(p) 330 } 331 332 pub fn set_passthrough(&self, on: bool) { 333 self.passthrough 334 .store(on && self.backing.is_some(), Ordering::Relaxed); 335 } 336 337 fn node(&self, ino: u64) -> io::Result<&Node> { 338 (ino as usize) 339 .checked_sub(1) 340 .and_then(|i| self.nodes.get(i)) 341 .ok_or_else(|| err(libc::ENOENT)) 342 } 343 344 pub fn has_root_entry(&self, name: &CStr) -> bool { 345 self.lookup_child(1, name).is_some() 346 } 347 348 pub fn root_entries(&self) -> impl Iterator<Item = (&str, u64, u32)> { 349 let Kind::Dir(c) = &self.nodes[0].kind else { 350 unreachable!() 351 }; 352 c.iter() 353 .map(|(n, i)| (&**n, *i as u64, self.dtype(*i as u64))) 354 } 355 356 fn lookup_child(&self, parent: u64, name: &CStr) -> Option<u64> { 357 let Kind::Dir(c) = &self.node(parent).ok()?.kind else { 358 return None; 359 }; 360 let name = name.to_str().ok()?; 361 c.binary_search_by(|(n, _)| (**n).cmp(name)) 362 .ok() 363 .map(|i| c[i].1 as u64) 364 } 365 366 fn dtype(&self, ino: u64) -> u32 { 367 match self.nodes[ino as usize - 1].kind { 368 Kind::Dir(_) => libc::DT_DIR as u32, 369 Kind::File { .. } => libc::DT_REG as u32, 370 Kind::Symlink(_) => libc::DT_LNK as u32, 371 } 372 } 373 374 fn stat(&self, ino: u64) -> io::Result<stat64> { 375 let n = self.node(ino)?; 376 // Safe: stat64 is plain old data. 377 let mut st: stat64 = unsafe { std::mem::zeroed() }; 378 st.st_ino = ino | INO_TAG; 379 st.st_blksize = 4096; 380 st.st_atime = 1; 381 st.st_mtime = 1; 382 st.st_ctime = 1; 383 match &n.kind { 384 Kind::Dir(_) => { 385 st.st_mode = libc::S_IFDIR | 0o555; 386 // 1: "subdirectory count unknown", which keeps find(1) 387 // from skipping entries based on it 388 st.st_nlink = 1; 389 } 390 Kind::File { size, exec } => { 391 st.st_mode = libc::S_IFREG | if *exec { 0o555 } else { 0o444 }; 392 st.st_nlink = 1; 393 st.st_size = *size as i64; 394 st.st_blocks = size.div_ceil(512) as i64; 395 } 396 Kind::Symlink(t) => { 397 st.st_mode = libc::S_IFLNK | 0o777; 398 st.st_nlink = 1; 399 st.st_size = t.len() as i64; 400 } 401 } 402 Ok(st) 403 } 404 405 /// The unpacked store path `i`, fetching it first if needed. Concurrent 406 /// callers for the same path wait for one fetch. 407 async fn fetch(&self, i: usize) -> io::Result<PathBuf> { 408 let mut slot = self.fetched[i].lock().await; 409 if let Some(p) = &*slot { 410 return Ok(p.clone()); 411 } 412 let name = &self.names[i]; 413 let dest = self.cache.join(name); 414 let mut last = None; 415 for attempt in 1..=3 { 416 match self.fetch_once(i, &dest).await { 417 Ok(()) => { 418 *slot = Some(dest.clone()); 419 return Ok(dest); 420 } 421 Err(e) => { 422 eprintln!("fetching {name} (attempt {attempt}/3): {e}"); 423 last = Some(e); 424 } 425 } 426 } 427 Err(last.unwrap()) 428 } 429 430 async fn fetch_once(&self, i: usize, dest: &Path) -> io::Result<()> { 431 let (name, info) = (&self.names[i], &self.infos[i]); 432 let t = Instant::now(); 433 let tmp = dest.with_extension("tmp"); 434 nar::remove(&tmp)?; 435 nar::remove(dest)?; 436 let (body, _) = self 437 .http 438 .open(&format!("{}/{}", info.cache, info.url)) 439 .await?; 440 // the compressed download size 441 let mut file_size = 0; 442 let mut body = InspectReader::new(body, |b: &[u8]| file_size += b.len() as u64); 443 let nar = http::decode_body(&info.compression, &mut body)?; 444 let (hash, size) = nar::unpack(nar, &tmp).await?; 445 drop(body); 446 if !nar::hash_matches(&info.nar_hash, &hash) || size != info.nar_size { 447 nar::remove(&tmp)?; 448 return Err(io::Error::other(format!( 449 "{name}: NAR hash mismatch, {} ({size} bytes) != {} ({} bytes)", 450 hash.as_base32(), 451 info.nar_hash, 452 info.nar_size 453 ))); 454 } 455 std::fs::rename(&tmp, dest)?; 456 self.stats.nar(name, file_size, size); 457 eprintln!( 458 "fetched {name}: {file_size} bytes in {:.1} s", 459 t.elapsed().as_secs_f64() 460 ); 461 Ok(()) 462 } 463 464 fn entry(&self, ino: u64) -> io::Result<Entry> { 465 Ok(Entry { 466 inode: ino, 467 generation: 0, 468 attr: self.stat(ino)?, 469 attr_flags: 0, 470 attr_timeout: TTL, 471 entry_timeout: TTL, 472 }) 473 } 474 475 fn backing_id(&self, ino: u64, file: &File) -> Option<u32> { 476 if !self.passthrough.load(Ordering::Relaxed) { 477 return None; 478 } 479 let backing = self.backing.as_ref()?; 480 let mut ids = self.backing_ids.lock().unwrap(); 481 if let Some((id, n)) = ids.get_mut(&ino) { 482 *n += 1; 483 return Some(*id); 484 } 485 match backing.open_fd(file.as_raw_fd()) { 486 Ok(id) => { 487 ids.insert(ino, (id, 1)); 488 Some(id) 489 } 490 Err(e) => { 491 if !self.backing_failed.swap(true, Ordering::Relaxed) { 492 eprintln!( 493 "lower: FUSE passthrough backing open failed, reading via the daemon: {e}" 494 ); 495 } 496 None 497 } 498 } 499 } 500 501 fn release_backing(&self, ino: u64) { 502 let mut ids = self.backing_ids.lock().unwrap(); 503 if let Some((id, n)) = ids.get_mut(&ino) { 504 *n -= 1; 505 if *n == 0 { 506 let id = *id; 507 ids.remove(&ino); 508 drop(ids); 509 if let Some(b) = &self.backing { 510 b.close_id(id); 511 } 512 } 513 } 514 } 515} 516 517impl FileSystem for LazyFs { 518 type Inode = u64; 519 type Handle = u64; 520 521 fn lookup(&self, _ctx: &Context, parent: u64, name: &CStr) -> io::Result<Entry> { 522 match self.lookup_child(parent, name) { 523 Some(ino) => self.entry(ino), 524 None => Err(err(libc::ENOENT)), 525 } 526 } 527 528 fn getattr(&self, _ctx: &Context, ino: u64, _h: Option<u64>) -> io::Result<(stat64, Duration)> { 529 Ok((self.stat(ino)?, TTL)) 530 } 531 532 fn readlink(&self, _ctx: &Context, ino: u64) -> io::Result<Vec<u8>> { 533 match &self.node(ino)?.kind { 534 Kind::Symlink(t) => Ok(t.to_vec()), 535 _ => Err(err(libc::EINVAL)), 536 } 537 } 538 539 fn open( 540 &self, 541 _ctx: &Context, 542 ino: u64, 543 flags: u32, 544 _fuse_flags: u32, 545 ) -> io::Result<(Option<u64>, OpenOptions, Option<u32>)> { 546 let n = self.node(ino)?; 547 if !matches!(n.kind, Kind::File { .. }) { 548 return Err(err(libc::EISDIR)); 549 } 550 let acc = flags as i32 & libc::O_ACCMODE; 551 if acc != libc::O_RDONLY || flags as i32 & libc::O_TRUNC != 0 { 552 return Err(err(libc::EROFS)); 553 } 554 let root = self.rt.block_on(self.fetch(n.path as usize)).map_err(|e| { 555 eprintln!("open {}/{}: {e}", self.names[n.path as usize], n.rel); 556 err(libc::EIO) 557 })?; 558 let file = File::open(if n.rel.is_empty() { 559 root 560 } else { 561 root.join(&*n.rel) 562 })?; 563 let backing = self.backing_id(ino, &file); 564 let h = self.next_handle.fetch_add(1, Ordering::Relaxed); 565 self.handles 566 .write() 567 .unwrap() 568 .insert(h, Arc::new(Handle { file, ino })); 569 Ok(match backing { 570 Some(id) => (Some(h), OpenOptions::PASSTHROUGH, Some(id)), 571 None => (Some(h), OpenOptions::KEEP_CACHE, None), 572 }) 573 } 574 575 fn read( 576 &self, 577 _ctx: &Context, 578 _ino: u64, 579 handle: u64, 580 w: &mut dyn ZeroCopyWriter, 581 size: u32, 582 offset: u64, 583 _lock_owner: Option<u64>, 584 _flags: u32, 585 ) -> io::Result<usize> { 586 let h = self 587 .handles 588 .read() 589 .unwrap() 590 .get(&handle) 591 .cloned() 592 .ok_or_else(|| err(libc::EBADF))?; 593 // Safe: borrows the fd of `h.file`, which outlives `f`, without closing it. 594 let mut f = ManuallyDrop::new(unsafe { File::from_raw_fd(h.file.as_raw_fd()) }); 595 w.write_from(&mut *f, size as usize, offset) 596 } 597 598 fn lseek( 599 &self, 600 _ctx: &Context, 601 _ino: u64, 602 handle: u64, 603 offset: u64, 604 whence: u32, 605 ) -> io::Result<u64> { 606 let h = self 607 .handles 608 .read() 609 .unwrap() 610 .get(&handle) 611 .cloned() 612 .ok_or_else(|| err(libc::EBADF))?; 613 // Safe: plain syscall on an fd we own. 614 let r = unsafe { libc::lseek(h.file.as_raw_fd(), offset as i64, whence as i32) }; 615 if r < 0 { 616 return Err(io::Error::last_os_error()); 617 } 618 Ok(r as u64) 619 } 620 621 fn flush(&self, _ctx: &Context, _ino: u64, _handle: u64, _lock_owner: u64) -> io::Result<()> { 622 Ok(()) 623 } 624 625 fn fsync(&self, _ctx: &Context, _ino: u64, _datasync: bool, _handle: u64) -> io::Result<()> { 626 Ok(()) 627 } 628 629 fn release( 630 &self, 631 _ctx: &Context, 632 _ino: u64, 633 _flags: u32, 634 handle: u64, 635 _flush: bool, 636 _flock_release: bool, 637 _lock_owner: Option<u64>, 638 ) -> io::Result<()> { 639 if let Some(h) = self.handles.write().unwrap().remove(&handle) { 640 self.release_backing(h.ino); 641 } 642 Ok(()) 643 } 644 645 fn opendir( 646 &self, 647 _ctx: &Context, 648 ino: u64, 649 _flags: u32, 650 ) -> io::Result<(Option<u64>, OpenOptions)> { 651 match self.node(ino)?.kind { 652 Kind::Dir(_) => Ok((None, OpenOptions::KEEP_CACHE | OpenOptions::CACHE_DIR)), 653 _ => Err(err(libc::ENOTDIR)), 654 } 655 } 656 657 fn readdir( 658 &self, 659 _ctx: &Context, 660 ino: u64, 661 _handle: u64, 662 _size: u32, 663 offset: u64, 664 add_entry: &mut dyn FnMut(DirEntry) -> io::Result<usize>, 665 ) -> io::Result<()> { 666 let Kind::Dir(c) = &self.node(ino)?.kind else { 667 return Err(err(libc::ENOTDIR)); 668 }; 669 for (i, (name, child)) in c.iter().enumerate().skip(offset as usize) { 670 let child = *child as u64; 671 let n = add_entry(DirEntry { 672 ino: child | INO_TAG, 673 offset: i as u64 + 1, 674 type_: self.dtype(child), 675 name: name.as_bytes(), 676 })?; 677 if n == 0 { 678 break; 679 } 680 } 681 Ok(()) 682 } 683 684 fn releasedir(&self, _ctx: &Context, _ino: u64, _flags: u32, _handle: u64) -> io::Result<()> { 685 Ok(()) 686 } 687 688 fn getxattr( 689 &self, 690 _ctx: &Context, 691 _ino: u64, 692 _name: &CStr, 693 _size: u32, 694 ) -> io::Result<GetxattrReply> { 695 Err(err(libc::ENODATA)) 696 } 697 698 fn listxattr(&self, _ctx: &Context, _ino: u64, size: u32) -> io::Result<ListxattrReply> { 699 Ok(if size == 0 { 700 ListxattrReply::Count(0) 701 } else { 702 ListxattrReply::Names(Vec::new()) 703 }) 704 } 705 706 fn access(&self, _ctx: &Context, _ino: u64, mask: u32) -> io::Result<()> { 707 if mask as i32 & libc::W_OK != 0 { 708 return Err(err(libc::EROFS)); 709 } 710 Ok(()) 711 } 712} 713 714#[cfg(test)] 715mod tests { 716 use super::*; 717 718 #[test] 719 fn leave_out_referrers() { 720 let rec = |n: &str, refs: &[&str]| { 721 let mut s = format!("{STORE}{n}\nsha256:x\n8\n\n{}\n", refs.len()); 722 for r in refs { 723 s += &format!("{STORE}{r}\n"); 724 } 725 s 726 }; 727 // w -> x -> y (failed), y -> y, z 728 let dump = [ 729 rec("w", &["x"]), 730 rec("x", &["x", "y"]), 731 rec("y", &["y"]), 732 rec("z", &[]), 733 ] 734 .concat(); 735 let (kept, out) = leave_out(&dump, ["y"]).unwrap(); 736 assert_eq!(kept, rec("z", &[])); 737 assert_eq!(out, ["w", "x", "y"].map(String::from).into()); 738 let (kept, out) = leave_out(&dump, []).unwrap(); 739 assert_eq!(kept, dump); 740 assert!(out.is_empty()); 741 } 742}