From f11dc98fcfc08868e041f60bc24b82b652118199 Mon Sep 17 00:00:00 2001 From: thomasjm Date: Sat, 14 Nov 2026 05:41:20 -0800 Subject: [PATCH] Split walks longer than 16 names into several Twalks --- src/client.rs | 32 ++++++++++++++++++++++++++------ tests/diod_fuse.rs | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/src/client.rs b/src/client.rs index 29afcad..74f230c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -18,6 +18,8 @@ use std::time::{Duration, Instant}; use tokio::sync::oneshot; const NOTAG: u16 = 0xffff; +/// The most names one Twalk may carry (MAXWELEM in the 9p spec). +const MAX_WALK_NAMES: usize = 16; const ROOT_FID: u32 = 1; /// A decoded response frame: message type plus the body after the 7-byte header. @@ -319,16 +321,34 @@ impl NineClient { .ok_or_else(|| to_io("short Rattach").into()) } - /// Walk `names` from `fid` into the fresh `newfid`. `names` empty clones the fid (newfid points - /// at the same file). Returns the walked qids (one per name). - /// Walk `names` from `fid` to `newfid`. + /// Walk `names` from `fid` to `newfid`; no names makes `newfid` a copy of `fid`. Returns one + /// qid per name. /// /// On `Ok`, `newfid` is established on the server and belongs to the caller, who must clunk it /// when done. On `Err` it is not, and clunking it is a protocol error the server answers with - /// EIO: 9P2000 leaves `newfid` untouched unless the walk completes in full. A server reports a - /// name that did not resolve by returning fewer qids than asked for rather than an Rlerror, so - /// that short reply is mapped to ENOENT here to keep the rule "Err means no fid" exact. + /// EIO. pub async fn walk(&self, fid: u32, newfid: u32, names: &[&str]) -> Result, i32> { + // A Twalk carries at most MAX_WALK_NAMES names, so walk the rest from newfid in place. + let mut chunks = names.chunks(MAX_WALK_NAMES); + let mut qids = self + .walk_once(fid, newfid, chunks.next().unwrap_or(&[])) + .await?; + for chunk in chunks { + match self.walk_once(newfid, newfid, chunk).await { + Ok(more) => qids.extend(more), + Err(e) => { + let _ = self.clunk(newfid).await; + return Err(e); + } + } + } + Ok(qids) + } + + /// One Twalk. 9P2000 leaves `newfid` untouched unless the walk completes in full. A server + /// reports a name that did not resolve by returning fewer qids than asked for rather than an + /// Rlerror, so that short reply is mapped to ENOENT to keep the rule "Err means no fid" exact. + async fn walk_once(&self, fid: u32, newfid: u32, names: &[&str]) -> Result, i32> { let mut w = W::new(); w.u32(fid).u32(newfid).u16(names.len() as u16); for n in names { diff --git a/tests/diod_fuse.rs b/tests/diod_fuse.rs index fdcdc03..288cf85 100644 --- a/tests/diod_fuse.rs +++ b/tests/diod_fuse.rs @@ -713,6 +713,42 @@ async fn external_growth_with_pinned_inode() { .await; } +/// Invalidating paths deeper than one `Twalk` can carry (16 names), existing or not, works and +/// doesn't break the connection. +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn invalidate_deep_path() { + if let Some(reason) = skip_reason() { + eprintln!("SKIP invalidate_deep_path: {reason}"); + return; + } + with_mount_full(Via::Tcp, Tuning::default(), |mut m| { + let dir = "d/".repeat(20); + std::fs::create_dir_all(m.export.join(&dir)).unwrap(); + std::fs::write(m.export.join(format!("{dir}f")), b"old").unwrap(); + assert_eq!(std::fs::read(m.mp.join(format!("{dir}f"))).unwrap(), b"old"); + + std::fs::write(m.export.join(format!("{dir}f")), b"new").unwrap(); + writeln!(m.inval, "inval 1 {dir}f").unwrap(); + let mut reply = String::new(); + std::io::BufReader::new(&m.inval) + .read_line(&mut reply) + .unwrap(); + assert_eq!(reply, "ok 1\n"); + + assert_eq!(std::fs::read(m.mp.join(format!("{dir}f"))).unwrap(), b"new"); + + // Fails in the second Twalk, after the first has established the fid. + writeln!(m.inval, "inval 2 {dir}missing/{dir}f").unwrap(); + let mut reply = String::new(); + std::io::BufReader::new(&m.inval) + .read_line(&mut reply) + .unwrap(); + assert_eq!(reply, "ok 2\n"); + assert_eq!(std::fs::read(m.mp.join(format!("{dir}f"))).unwrap(), b"new"); + }) + .await; +} + /// `stat` and a read through a second fd see every write made so far on a still-open writer, even /// though the write pipeline has acknowledged writes the server may not have yet. Without the /// writeback cache the kernel takes size and data from us, so we have to wait for those writes.