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.
This commit is contained in:
Levi Neely 2026-09-30 15:11:10 +02:00
parent 9e2fcb9010
commit 94862b7f26
1 changed files with 418 additions and 0 deletions

View File

@ -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<dyn ReadWrite>,
msize: u32,
fid_counter: u32,
root_fid: u32,
}
trait ReadWrite: Read + Write + Send {}
impl<T: Read + Write + Send> ReadWrite for T {}
impl Client {
/// Connect to a 9P server.
pub fn connect(addr: &str) -> io::Result<Self> {
let stream: Box<dyn ReadWrite> = 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<u32> {
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<Vec<u8>> {
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<String> {
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<Vec<u8>> {
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<Vec<String>> {
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<Vec<u8>> {
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)
}
}