Skip to main content

netget/server/ninep/
wire.rs

1//! 9P2000 message codec (Plan 9's file protocol, as plan9port and the Go 9P libraries speak
2//! it): little-endian `size[4] type[1] tag[2]` framing, strings with a u16 length, 13-byte
3//! qids and the variable-length stat. Shared by the server and the client; every length the
4//! peer announces is checked before anything is allocated for it.
5use anyhow::{bail, ensure, Context, Result};
6use std::time::Duration;
7use tokio::io::{AsyncRead, AsyncReadExt};
8
9pub const VERSION: &str = "9P2000";
10/// Largest message either side accepts, and the msize NetGet offers and accepts at most.
11pub const MAX_MSIZE: u32 = 65536;
12/// Smallest msize worth negotiating: room for a header and a little data.
13pub const MIN_MSIZE: u32 = 256;
14/// Header bytes in an Rread/Twrite before the data: size, type, tag, count (and fid+offset).
15pub const IOHDRSZ: u32 = 24;
16/// Path elements in one Twalk, as Plan 9's MAXWELEM.
17pub const MAX_WELEM: usize = 16;
18/// Bytes in one path element.
19pub const MAX_NAME: usize = 255;
20/// Fids one connection may hold.
21pub const MAX_FIDS: usize = 256;
22/// Largest file content a handler may supply, and the client accepts.
23pub const MAX_CONTENT: usize = 1024 * 1024;
24/// Entries in one directory listing.
25pub const MAX_ENTRIES: usize = 1024;
26/// Per-message read deadline once a message has started, and per-request write deadline.
27pub const IO_TIMEOUT: Duration = Duration::from_secs(30);
28pub const NOFID: u32 = u32::MAX;
29pub const NOTAG: u16 = u16::MAX;
30
31pub const DMDIR: u32 = 0x8000_0000;
32pub const QTDIR: u8 = 0x80;
33pub const QTFILE: u8 = 0x00;
34pub const OREAD: u8 = 0;
35pub const OWRITE: u8 = 1;
36pub const ORDWR: u8 = 2;
37pub const OEXEC: u8 = 3;
38pub const OTRUNC: u8 = 0x10;
39pub const ORCLOSE: u8 = 0x40;
40
41pub const TVERSION: u8 = 100;
42pub const RVERSION: u8 = 101;
43pub const TAUTH: u8 = 102;
44pub const RAUTH: u8 = 103;
45pub const TATTACH: u8 = 104;
46pub const RATTACH: u8 = 105;
47pub const RERROR: u8 = 107;
48pub const TFLUSH: u8 = 108;
49pub const RFLUSH: u8 = 109;
50pub const TWALK: u8 = 110;
51pub const RWALK: u8 = 111;
52pub const TOPEN: u8 = 112;
53pub const ROPEN: u8 = 113;
54pub const TCREATE: u8 = 114;
55pub const RCREATE: u8 = 115;
56pub const TREAD: u8 = 116;
57pub const RREAD: u8 = 117;
58pub const TWRITE: u8 = 118;
59pub const RWRITE: u8 = 119;
60pub const TCLUNK: u8 = 120;
61pub const RCLUNK: u8 = 121;
62pub const TREMOVE: u8 = 122;
63pub const RREMOVE: u8 = 123;
64pub const TSTAT: u8 = 124;
65pub const RSTAT: u8 = 125;
66pub const TWSTAT: u8 = 126;
67pub const RWSTAT: u8 = 127;
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub struct Qid {
71    pub kind: u8,
72    pub version: u32,
73    pub path: u64,
74}
75
76impl Qid {
77    pub fn is_dir(&self) -> bool {
78        self.kind & QTDIR != 0
79    }
80}
81
82/// One directory entry, as Tstat/Rstat and directory reads carry it.
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct Stat {
85    pub kind: u16,
86    pub dev: u32,
87    pub qid: Qid,
88    pub mode: u32,
89    pub atime: u32,
90    pub mtime: u32,
91    pub length: u64,
92    pub name: String,
93    pub uid: String,
94    pub gid: String,
95    pub muid: String,
96}
97
98impl Stat {
99    /// A wstat entry that changes nothing: every field set to its "don't touch" value.
100    pub fn dont_touch() -> Self {
101        Self {
102            kind: u16::MAX,
103            dev: u32::MAX,
104            qid: Qid {
105                kind: u8::MAX,
106                version: u32::MAX,
107                path: u64::MAX,
108            },
109            mode: u32::MAX,
110            atime: u32::MAX,
111            mtime: u32::MAX,
112            length: u64::MAX,
113            name: String::new(),
114            uid: String::new(),
115            gid: String::new(),
116            muid: String::new(),
117        }
118    }
119}
120
121/// Builds one message body; `finish` prepends size, type and tag.
122#[derive(Default)]
123pub struct Writer {
124    buf: Vec<u8>,
125}
126
127impl Writer {
128    pub fn new() -> Self {
129        Self::default()
130    }
131    pub fn u8(mut self, v: u8) -> Self {
132        self.buf.push(v);
133        self
134    }
135    pub fn u16(mut self, v: u16) -> Self {
136        self.buf.extend_from_slice(&v.to_le_bytes());
137        self
138    }
139    pub fn u32(mut self, v: u32) -> Self {
140        self.buf.extend_from_slice(&v.to_le_bytes());
141        self
142    }
143    pub fn u64(mut self, v: u64) -> Self {
144        self.buf.extend_from_slice(&v.to_le_bytes());
145        self
146    }
147    pub fn string(self, s: &str) -> Self {
148        let len = s.len().min(u16::MAX as usize);
149        let mut w = self.u16(len as u16);
150        w.buf.extend_from_slice(&s.as_bytes()[..len]);
151        w
152    }
153    pub fn bytes(mut self, b: &[u8]) -> Self {
154        self.buf.extend_from_slice(b);
155        self
156    }
157    pub fn qid(self, q: &Qid) -> Self {
158        self.u8(q.kind).u32(q.version).u64(q.path)
159    }
160    pub fn stat(self, s: &Stat) -> Self {
161        self.bytes(&encode_stat(s))
162    }
163    pub fn finish(self, kind: u8, tag: u16) -> Vec<u8> {
164        let size = (7 + self.buf.len()) as u32;
165        let mut out = Vec::with_capacity(size as usize);
166        out.extend_from_slice(&size.to_le_bytes());
167        out.push(kind);
168        out.extend_from_slice(&tag.to_le_bytes());
169        out.extend_from_slice(&self.buf);
170        out
171    }
172}
173
174/// A stat entry with its own leading size field.
175pub fn encode_stat(s: &Stat) -> Vec<u8> {
176    let body = Writer::new()
177        .u16(s.kind)
178        .u32(s.dev)
179        .qid(&s.qid)
180        .u32(s.mode)
181        .u32(s.atime)
182        .u32(s.mtime)
183        .u64(s.length)
184        .string(&s.name)
185        .string(&s.uid)
186        .string(&s.gid)
187        .string(&s.muid)
188        .buf;
189    let mut out = Vec::with_capacity(body.len() + 2);
190    out.extend_from_slice(&(body.len() as u16).to_le_bytes());
191    out.extend_from_slice(&body);
192    out
193}
194
195/// A cursor over one message body.
196pub struct Reader<'a> {
197    buf: &'a [u8],
198}
199
200impl<'a> Reader<'a> {
201    pub fn new(buf: &'a [u8]) -> Self {
202        Self { buf }
203    }
204    pub fn is_empty(&self) -> bool {
205        self.buf.is_empty()
206    }
207    pub fn take(&mut self, n: usize) -> Result<&'a [u8]> {
208        ensure!(self.buf.len() >= n, "9P message ends inside a field");
209        let (head, tail) = self.buf.split_at(n);
210        self.buf = tail;
211        Ok(head)
212    }
213    pub fn u8(&mut self) -> Result<u8> {
214        Ok(self.take(1)?[0])
215    }
216    pub fn u16(&mut self) -> Result<u16> {
217        Ok(u16::from_le_bytes(self.take(2)?.try_into()?))
218    }
219    pub fn u32(&mut self) -> Result<u32> {
220        Ok(u32::from_le_bytes(self.take(4)?.try_into()?))
221    }
222    pub fn u64(&mut self) -> Result<u64> {
223        Ok(u64::from_le_bytes(self.take(8)?.try_into()?))
224    }
225    pub fn string(&mut self) -> Result<String> {
226        let len = self.u16()? as usize;
227        Ok(std::str::from_utf8(self.take(len)?)
228            .context("9P string is not UTF-8")?
229            .to_string())
230    }
231    pub fn qid(&mut self) -> Result<Qid> {
232        Ok(Qid {
233            kind: self.u8()?,
234            version: self.u32()?,
235            path: self.u64()?,
236        })
237    }
238    pub fn stat(&mut self) -> Result<Stat> {
239        let size = self.u16()? as usize;
240        let mut r = Reader::new(self.take(size)?);
241        Ok(Stat {
242            kind: r.u16()?,
243            dev: r.u32()?,
244            qid: r.qid()?,
245            mode: r.u32()?,
246            atime: r.u32()?,
247            mtime: r.u32()?,
248            length: r.u64()?,
249            name: r.string()?,
250            uid: r.string()?,
251            gid: r.string()?,
252            muid: r.string()?,
253        })
254    }
255}
256
257/// One message: (type, tag, body). `None` on a clean EOF between messages. The size field is
258/// checked against `max` before the body is read; the first byte may wait `idle`, the rest
259/// must follow within `IO_TIMEOUT`.
260pub async fn read_message<R: AsyncRead + Unpin>(
261    reader: &mut R,
262    max: u32,
263    idle: Duration,
264) -> Result<Option<(u8, u16, Vec<u8>)>> {
265    let mut size = [0u8; 4];
266    match tokio::time::timeout(idle, reader.read(&mut size[..1])).await {
267        Ok(Ok(0)) => return Ok(None),
268        Ok(Ok(_)) => {}
269        Ok(Err(e)) => return Err(e.into()),
270        Err(_) => bail!("9P peer idle for {}s", idle.as_secs()),
271    }
272    tokio::time::timeout(IO_TIMEOUT, async {
273        reader.read_exact(&mut size[1..]).await?;
274        let size = u32::from_le_bytes(size);
275        ensure!(size >= 7, "9P message size {size} is below the header");
276        ensure!(
277            size <= max,
278            "9P message of {size} bytes exceeds msize {max}"
279        );
280        let mut rest = vec![0u8; size as usize - 4];
281        reader.read_exact(&mut rest).await?;
282        let kind = rest[0];
283        let tag = u16::from_le_bytes([rest[1], rest[2]]);
284        Ok(Some((kind, tag, rest[3..].to_vec())))
285    })
286    .await
287    .context("9P read deadline")?
288}
289
290/// A path element a server may create or walk to: not empty, no '/', not "." and at most
291/// `MAX_NAME` bytes. ".." is handled by the walk itself.
292pub fn check_name(name: &str) -> Result<()> {
293    ensure!(
294        !name.is_empty()
295            && name.len() <= MAX_NAME
296            && !name.contains('/')
297            && name != "."
298            && !name.contains('\0'),
299        "invalid 9P name {name:?}"
300    );
301    Ok(())
302}
303
304/// Stable qid path for a file path: FNV-1a 64.
305pub fn qid_path(path: &str) -> u64 {
306    path.bytes().fold(0xcbf2_9ce4_8422_2325u64, |h, b| {
307        (h ^ u64::from(b)).wrapping_mul(0x0000_0100_0000_01b3)
308    })
309}
310
311/// Join a parent path and a child name ("/" + "a" = "/a").
312pub fn join(parent: &str, name: &str) -> String {
313    if parent == "/" {
314        format!("/{name}")
315    } else {
316        format!("{parent}/{name}")
317    }
318}
319
320/// The parent of a path ("/a/b" -> "/a", "/a" -> "/", "/" -> "/").
321pub fn parent(path: &str) -> String {
322    match path.rsplit_once('/') {
323        Some(("", _)) | None => "/".into(),
324        Some((p, _)) => p.into(),
325    }
326}
327
328/// The last element of a path ("/" for the root).
329pub fn base(path: &str) -> String {
330    match path.rsplit_once('/') {
331        Some((_, "")) | None => "/".into(),
332        Some((_, name)) => name.into(),
333    }
334}
335
336/// Split "/a/b" into elements, refusing anything `check_name` would.
337pub fn elements(path: &str) -> Result<Vec<String>> {
338    ensure!(path.starts_with('/'), "9P paths are absolute: {path:?}");
339    let parts: Vec<String> = path
340        .split('/')
341        .filter(|p| !p.is_empty())
342        .map(str::to_string)
343        .collect();
344    for p in &parts {
345        if p != ".." {
346            check_name(p)?;
347        }
348    }
349    Ok(parts)
350}
351
352/// Bytes as event text: UTF-8 when it is, hex otherwise.
353pub fn to_text(bytes: &[u8]) -> (String, &'static str) {
354    match std::str::from_utf8(bytes) {
355        Ok(s) => (s.to_string(), "utf8"),
356        Err(_) => (bytes.iter().map(|b| format!("{b:02x}")).collect(), "hex"),
357    }
358}
359
360/// Text from an action back to bytes, by its declared encoding.
361pub fn from_text(text: &str, encoding: Option<&str>) -> Result<Vec<u8>> {
362    match encoding.unwrap_or("utf8") {
363        "utf8" => Ok(text.as_bytes().to_vec()),
364        "hex" => {
365            ensure!(
366                text.is_ascii() && text.len().is_multiple_of(2),
367                "hex data must be an even number of hex digits"
368            );
369            (0..text.len())
370                .step_by(2)
371                .map(|i| u8::from_str_radix(&text[i..i + 2], 16).context("invalid hex data"))
372                .collect()
373        }
374        other => bail!("encoding must be utf8 or hex, not {other}"),
375    }
376}