This repository has no description
25 kB
742 lines
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}