From 94862b7f262fad45dd013c0035940c90a5210056 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Wed, 30 Sep 2026 15:11:10 +0200 Subject: [PATCH] Add 9P client to ork-cli Simple 9P2000 client supporting: - Connect via Unix socket or TCP - read/write/rdwr operations - Directory listing Preparation for reworking CLI to use 9P server. --- crates/ork-cli/src/client.rs | 418 +++++++++++++++++++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 crates/ork-cli/src/client.rs diff --git a/crates/ork-cli/src/client.rs b/crates/ork-cli/src/client.rs new file mode 100644 index 0000000..bf09726 --- /dev/null +++ b/crates/ork-cli/src/client.rs @@ -0,0 +1,418 @@ +//! Simple 9P2000 client for connecting to ork-server. + +use std::io::{self, Read, Write}; +use std::net::TcpStream; +use std::os::unix::net::UnixStream; + +const MSIZE: u32 = 8192; + +/// 9P client connection. +pub struct Client { + stream: Box, + msize: u32, + fid_counter: u32, + root_fid: u32, +} + +trait ReadWrite: Read + Write + Send {} +impl ReadWrite for T {} + +impl Client { + /// Connect to a 9P server. + pub fn connect(addr: &str) -> io::Result { + let stream: Box = if addr.starts_with("unix://") { + Box::new(UnixStream::connect(&addr[7..])?) + } else { + let tcp_addr = if addr.starts_with("tcp://") { + &addr[6..] + } else { + addr + }; + Box::new(TcpStream::connect(tcp_addr)?) + }; + + let mut client = Self { + stream, + msize: MSIZE, + fid_counter: 1, + root_fid: 0, + }; + + // Version handshake + client.version()?; + + // Attach to root + client.root_fid = client.fid_counter; + client.fid_counter += 1; + client.attach(client.root_fid)?; + + Ok(client) + } + + /// Get a new fid. + fn new_fid(&mut self) -> u32 { + let fid = self.fid_counter; + self.fid_counter += 1; + fid + } + + /// Send Tversion and receive Rversion. + fn version(&mut self) -> io::Result<()> { + let version = "9P2000"; + let mut msg = Vec::new(); + msg.extend_from_slice(&[0u8; 4]); // size placeholder + msg.push(100); // Tversion + msg.extend_from_slice(&0u16.to_le_bytes()); // tag + msg.extend_from_slice(&MSIZE.to_le_bytes()); + msg.extend_from_slice(&(version.len() as u16).to_le_bytes()); + msg.extend_from_slice(version.as_bytes()); + + let size = msg.len() as u32; + msg[0..4].copy_from_slice(&size.to_le_bytes()); + + self.stream.write_all(&msg)?; + self.stream.flush()?; + + let resp = self.read_response()?; + if resp[4] == 107 { // Rerror + return Err(self.parse_error(&resp)); + } + if resp[4] != 101 { // Rversion + return Err(io::Error::new(io::ErrorKind::InvalidData, "unexpected response")); + } + + self.msize = u32::from_le_bytes([resp[7], resp[8], resp[9], resp[10]]); + Ok(()) + } + + /// Send Tattach. + fn attach(&mut self, fid: u32) -> io::Result<()> { + let mut msg = Vec::new(); + msg.extend_from_slice(&[0u8; 4]); // size + msg.push(104); // Tattach + msg.extend_from_slice(&0u16.to_le_bytes()); // tag + msg.extend_from_slice(&fid.to_le_bytes()); // fid + msg.extend_from_slice(&u32::MAX.to_le_bytes()); // afid (no auth) + msg.extend_from_slice(&0u16.to_le_bytes()); // uname len + msg.extend_from_slice(&0u16.to_le_bytes()); // aname len + + let size = msg.len() as u32; + msg[0..4].copy_from_slice(&size.to_le_bytes()); + + self.stream.write_all(&msg)?; + self.stream.flush()?; + + let resp = self.read_response()?; + if resp[4] == 107 { + return Err(self.parse_error(&resp)); + } + Ok(()) + } + + /// Walk to a path and return a new fid. + fn walk(&mut self, path: &str) -> io::Result { + let newfid = self.new_fid(); + + let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect(); + + let mut msg = Vec::new(); + msg.extend_from_slice(&[0u8; 4]); // size + msg.push(110); // Twalk + msg.extend_from_slice(&0u16.to_le_bytes()); // tag + msg.extend_from_slice(&self.root_fid.to_le_bytes()); // fid + msg.extend_from_slice(&newfid.to_le_bytes()); // newfid + msg.extend_from_slice(&(parts.len() as u16).to_le_bytes()); // nwname + + for part in &parts { + msg.extend_from_slice(&(part.len() as u16).to_le_bytes()); + msg.extend_from_slice(part.as_bytes()); + } + + let size = msg.len() as u32; + msg[0..4].copy_from_slice(&size.to_le_bytes()); + + self.stream.write_all(&msg)?; + self.stream.flush()?; + + let resp = self.read_response()?; + if resp[4] == 107 { + return Err(self.parse_error(&resp)); + } + + // Check we walked all components + let nwqid = u16::from_le_bytes([resp[7], resp[8]]) as usize; + if nwqid != parts.len() { + return Err(io::Error::new(io::ErrorKind::NotFound, "path not found")); + } + + Ok(newfid) + } + + /// Open a fid with the given mode. + fn open(&mut self, fid: u32, mode: u8) -> io::Result<()> { + let mut msg = Vec::new(); + msg.extend_from_slice(&[0u8; 4]); // size + msg.push(112); // Topen + msg.extend_from_slice(&0u16.to_le_bytes()); // tag + msg.extend_from_slice(&fid.to_le_bytes()); + msg.push(mode); + + let size = msg.len() as u32; + msg[0..4].copy_from_slice(&size.to_le_bytes()); + + self.stream.write_all(&msg)?; + self.stream.flush()?; + + let resp = self.read_response()?; + if resp[4] == 107 { + return Err(self.parse_error(&resp)); + } + Ok(()) + } + + /// Clunk (close) a fid. + fn clunk(&mut self, fid: u32) -> io::Result<()> { + let mut msg = Vec::new(); + msg.extend_from_slice(&[0u8; 4]); // size + msg.push(120); // Tclunk + msg.extend_from_slice(&0u16.to_le_bytes()); // tag + msg.extend_from_slice(&fid.to_le_bytes()); + + let size = msg.len() as u32; + msg[0..4].copy_from_slice(&size.to_le_bytes()); + + self.stream.write_all(&msg)?; + self.stream.flush()?; + + let resp = self.read_response()?; + if resp[4] == 107 { + return Err(self.parse_error(&resp)); + } + Ok(()) + } + + /// Read file content. + pub fn read(&mut self, path: &str) -> io::Result> { + let fid = self.walk(path)?; + self.open(fid, 0)?; // OREAD + + let mut result = Vec::new(); + let mut offset = 0u64; + + loop { + let mut msg = Vec::new(); + msg.extend_from_slice(&[0u8; 4]); // size + msg.push(116); // Tread + msg.extend_from_slice(&0u16.to_le_bytes()); // tag + msg.extend_from_slice(&fid.to_le_bytes()); + msg.extend_from_slice(&offset.to_le_bytes()); + msg.extend_from_slice(&(self.msize - 24).to_le_bytes()); // count + + let size = msg.len() as u32; + msg[0..4].copy_from_slice(&size.to_le_bytes()); + + self.stream.write_all(&msg)?; + self.stream.flush()?; + + let resp = self.read_response()?; + if resp[4] == 107 { + self.clunk(fid)?; + return Err(self.parse_error(&resp)); + } + + let count = u32::from_le_bytes([resp[7], resp[8], resp[9], resp[10]]) as usize; + if count == 0 { + break; + } + + result.extend_from_slice(&resp[11..11 + count]); + offset += count as u64; + } + + self.clunk(fid)?; + Ok(result) + } + + /// Read file as string. + pub fn read_string(&mut self, path: &str) -> io::Result { + let data = self.read(path)?; + Ok(String::from_utf8_lossy(&data).trim().to_string()) + } + + /// Write to a file. + pub fn write(&mut self, path: &str, data: &[u8]) -> io::Result<()> { + let fid = self.walk(path)?; + self.open(fid, 1)?; // OWRITE + + let mut msg = Vec::new(); + msg.extend_from_slice(&[0u8; 4]); // size + msg.push(118); // Twrite + msg.extend_from_slice(&0u16.to_le_bytes()); // tag + msg.extend_from_slice(&fid.to_le_bytes()); + msg.extend_from_slice(&0u64.to_le_bytes()); // offset + msg.extend_from_slice(&(data.len() as u32).to_le_bytes()); + msg.extend_from_slice(data); + + let size = msg.len() as u32; + msg[0..4].copy_from_slice(&size.to_le_bytes()); + + self.stream.write_all(&msg)?; + self.stream.flush()?; + + let resp = self.read_response()?; + self.clunk(fid)?; + + if resp[4] == 107 { + return Err(self.parse_error(&resp)); + } + Ok(()) + } + + /// Write then read (for rdwr files like new, ctl). + pub fn rdwr(&mut self, path: &str, data: &[u8]) -> io::Result> { + let fid = self.walk(path)?; + self.open(fid, 2)?; // ORDWR + + // Write + let mut msg = Vec::new(); + msg.extend_from_slice(&[0u8; 4]); // size + msg.push(118); // Twrite + msg.extend_from_slice(&0u16.to_le_bytes()); // tag + msg.extend_from_slice(&fid.to_le_bytes()); + msg.extend_from_slice(&0u64.to_le_bytes()); // offset + msg.extend_from_slice(&(data.len() as u32).to_le_bytes()); + msg.extend_from_slice(data); + + let size = msg.len() as u32; + msg[0..4].copy_from_slice(&size.to_le_bytes()); + + self.stream.write_all(&msg)?; + self.stream.flush()?; + + let resp = self.read_response()?; + if resp[4] == 107 { + self.clunk(fid)?; + return Err(self.parse_error(&resp)); + } + + // Read response + let mut msg = Vec::new(); + msg.extend_from_slice(&[0u8; 4]); // size + msg.push(116); // Tread + msg.extend_from_slice(&0u16.to_le_bytes()); // tag + msg.extend_from_slice(&fid.to_le_bytes()); + msg.extend_from_slice(&0u64.to_le_bytes()); // offset + msg.extend_from_slice(&(self.msize - 24).to_le_bytes()); // count + + let size = msg.len() as u32; + msg[0..4].copy_from_slice(&size.to_le_bytes()); + + self.stream.write_all(&msg)?; + self.stream.flush()?; + + let resp = self.read_response()?; + self.clunk(fid)?; + + if resp[4] == 107 { + return Err(self.parse_error(&resp)); + } + + let count = u32::from_le_bytes([resp[7], resp[8], resp[9], resp[10]]) as usize; + Ok(resp[11..11 + count].to_vec()) + } + + /// List directory contents. + pub fn list(&mut self, path: &str) -> io::Result> { + let fid = self.walk(path)?; + self.open(fid, 0)?; // OREAD + + let mut data = Vec::new(); + let mut offset = 0u64; + + loop { + let mut msg = Vec::new(); + msg.extend_from_slice(&[0u8; 4]); // size + msg.push(116); // Tread + msg.extend_from_slice(&0u16.to_le_bytes()); // tag + msg.extend_from_slice(&fid.to_le_bytes()); + msg.extend_from_slice(&offset.to_le_bytes()); + msg.extend_from_slice(&(self.msize - 24).to_le_bytes()); // count + + let size = msg.len() as u32; + msg[0..4].copy_from_slice(&size.to_le_bytes()); + + self.stream.write_all(&msg)?; + self.stream.flush()?; + + let resp = self.read_response()?; + if resp[4] == 107 { + self.clunk(fid)?; + return Err(self.parse_error(&resp)); + } + + let count = u32::from_le_bytes([resp[7], resp[8], resp[9], resp[10]]) as usize; + if count == 0 { + break; + } + + data.extend_from_slice(&resp[11..11 + count]); + offset += count as u64; + } + + self.clunk(fid)?; + + // Parse directory entries (stat structures) + let mut names = Vec::new(); + let mut pos = 0; + while pos + 2 < data.len() { + let stat_size = u16::from_le_bytes([data[pos], data[pos + 1]]) as usize; + if pos + 2 + stat_size > data.len() { + break; + } + + // Skip to name field: size(2) + type(2) + dev(4) + qid(13) + mode(4) + atime(4) + mtime(4) + length(8) = 41 + let name_offset = pos + 2 + 41; + if name_offset + 2 > data.len() { + break; + } + + let name_len = u16::from_le_bytes([data[name_offset], data[name_offset + 1]]) as usize; + if name_offset + 2 + name_len > data.len() { + break; + } + + let name = String::from_utf8_lossy(&data[name_offset + 2..name_offset + 2 + name_len]).to_string(); + names.push(name); + + pos += 2 + stat_size; + } + + Ok(names) + } + + /// Read a 9P response message. + fn read_response(&mut self) -> io::Result> { + let mut size_buf = [0u8; 4]; + self.stream.read_exact(&mut size_buf)?; + + let size = u32::from_le_bytes(size_buf) as usize; + let mut buf = vec![0u8; size]; + buf[0..4].copy_from_slice(&size_buf); + self.stream.read_exact(&mut buf[4..])?; + + Ok(buf) + } + + /// Parse an Rerror message. + fn parse_error(&self, resp: &[u8]) -> io::Error { + if resp.len() < 9 { + return io::Error::new(io::ErrorKind::InvalidData, "error response too short"); + } + let len = u16::from_le_bytes([resp[7], resp[8]]) as usize; + if resp.len() < 9 + len { + return io::Error::new(io::ErrorKind::InvalidData, "error response truncated"); + } + let msg = String::from_utf8_lossy(&resp[9..9 + len]).to_string(); + io::Error::new(io::ErrorKind::Other, msg) + } +}