1pub const INIT: u8 = 1;
4pub const VERSION: u8 = 2;
5pub const OPEN: u8 = 3;
6pub const CLOSE: u8 = 4;
7pub const READ: u8 = 5;
8pub const WRITE: u8 = 6;
9pub const LSTAT: u8 = 7;
10pub const OPENDIR: u8 = 11;
11pub const READDIR: u8 = 12;
12pub const MKDIR: u8 = 14;
13pub const REALPATH: u8 = 16;
14pub const STATUS: u8 = 101;
15pub const HANDLE: u8 = 102;
16pub const DATA: u8 = 103;
17pub const NAME: u8 = 104;
18pub const ATTRS: u8 = 105;
19
20pub const FXF_READ: u32 = 0x0000_0001;
21pub const FXF_WRITE: u32 = 0x0000_0002;
22pub const FXF_APPEND: u32 = 0x0000_0004;
25pub const FXF_CREAT: u32 = 0x0000_0008;
26
27pub const STATUS_EOF: u32 = 1;
30
31pub const STATUS_OK: u32 = 0;
33
34pub const STATUS_NO_SUCH_FILE: u32 = 2;
39
40const A_SIZE: u32 = 0x0000_0001;
41const A_UIDGID: u32 = 0x0000_0002;
42const A_PERM: u32 = 0x0000_0004;
43const A_TIME: u32 = 0x0000_0008;
44const A_EXT: u32 = 0x8000_0000;
45
46const S_IFMT: u32 = 0o170000;
47const S_IFDIR: u32 = 0o040000;
48const S_IFLNK: u32 = 0o120000;
49
50#[derive(Default)]
52pub struct Enc(Vec<u8>);
53
54impl Enc {
55 pub fn new() -> Self {
56 Self(Vec::new())
57 }
58
59 pub fn u32(mut self, v: u32) -> Self {
60 self.0.extend_from_slice(&v.to_be_bytes());
61 self
62 }
63
64 pub fn u64(mut self, v: u64) -> Self {
65 self.0.extend_from_slice(&v.to_be_bytes());
66 self
67 }
68
69 pub fn str(mut self, v: &[u8]) -> Self {
70 self.0.extend_from_slice(&(v.len() as u32).to_be_bytes());
71 self.0.extend_from_slice(v);
72 self
73 }
74
75 pub fn done(self) -> Vec<u8> {
76 self.0
77 }
78}
79
80pub struct Dec<'a> {
83 b: &'a [u8],
84 i: usize,
85}
86
87impl<'a> Dec<'a> {
88 pub fn new(b: &'a [u8]) -> Self {
89 Self { b, i: 0 }
90 }
91
92 pub fn u32(&mut self) -> Option<u32> {
93 let end = self.i.checked_add(4)?;
94 let v = u32::from_be_bytes(self.b.get(self.i..end)?.try_into().ok()?);
95 self.i = end;
96 Some(v)
97 }
98
99 pub fn u64(&mut self) -> Option<u64> {
100 let end = self.i.checked_add(8)?;
101 let v = u64::from_be_bytes(self.b.get(self.i..end)?.try_into().ok()?);
102 self.i = end;
103 Some(v)
104 }
105
106 pub fn str(&mut self) -> Option<&'a [u8]> {
107 let n = self.u32()? as usize;
108 let end = self.i.checked_add(n)?;
109 let v = self.b.get(self.i..end)?;
110 self.i = end;
111 Some(v)
112 }
113}
114
115#[derive(Debug, Clone, Copy, Default)]
125pub struct Attrs {
126 pub size: Option<u64>,
127 pub uid: Option<u32>,
128 pub gid: Option<u32>,
129 pub permissions: Option<u32>,
130 pub atime: Option<u32>,
131 pub mtime: Option<u32>,
132}
133
134impl Attrs {
135 pub fn decode(d: &mut Dec<'_>) -> Option<Self> {
136 let flags = d.u32()?;
137 let mut a = Self::default();
138 if flags & A_SIZE != 0 {
139 a.size = Some(d.u64()?);
140 }
141 if flags & A_UIDGID != 0 {
142 a.uid = Some(d.u32()?);
143 a.gid = Some(d.u32()?);
144 }
145 if flags & A_PERM != 0 {
146 a.permissions = Some(d.u32()?);
147 }
148 if flags & A_TIME != 0 {
149 a.atime = Some(d.u32()?);
150 a.mtime = Some(d.u32()?);
151 }
152 if flags & A_EXT != 0 {
153 for _ in 0..d.u32()? {
154 d.str()?;
155 d.str()?;
156 }
157 }
158 Some(a)
159 }
160
161 pub fn is_dir(&self) -> bool {
162 self.permissions.is_some_and(|p| p & S_IFMT == S_IFDIR)
163 }
164
165 pub fn is_symlink(&self) -> bool {
166 self.permissions.is_some_and(|p| p & S_IFMT == S_IFLNK)
167 }
168}
169
170pub fn owner_of_longname(longname: &str) -> Option<&str> {
186 let mut fields = longname.split_ascii_whitespace();
187 if !is_mode_string(fields.next()?) {
188 return None;
189 }
190 fields.next()?.parse::<u64>().ok()?;
193 let owner = fields.next()?;
194 let _group = fields.next()?;
195 fields.next()?.parse::<u64>().ok()?;
196 Some(owner)
197}
198
199fn is_mode_string(s: &str) -> bool {
201 let b = s
204 .strip_suffix('+')
205 .or_else(|| s.strip_suffix('.'))
206 .unwrap_or(s)
207 .as_bytes();
208 b.len() == 10
209 && matches!(b[0], b'-' | b'd' | b'l' | b'b' | b'c' | b'p' | b's')
210 && b[1..]
211 .iter()
212 .all(|c| matches!(c, b'r' | b'w' | b'x' | b's' | b't' | b'S' | b'T' | b'-'))
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn roundtrips_primitives() {
221 let bytes = Enc::new().u32(7).u64(1 << 40).str(b"hi").done();
222 let mut d = Dec::new(&bytes);
223 assert_eq!(d.u32(), Some(7));
224 assert_eq!(d.u64(), Some(1 << 40));
225 assert_eq!(d.str(), Some(&b"hi"[..]));
226 assert_eq!(d.u32(), None);
227 }
228
229 #[test]
230 fn truncated_input_returns_none_instead_of_panicking() {
231 let mut d = Dec::new(&[0, 0, 0, 9, 1, 2]);
232 assert_eq!(d.str(), None);
233 }
234
235 #[test]
236 fn decodes_attrs_and_classifies_a_directory() {
237 let bytes = Enc::new()
238 .u32(A_SIZE | A_UIDGID | A_PERM | A_TIME)
239 .u64(4096)
240 .u32(1000)
241 .u32(1000)
242 .u32(0o040755)
243 .u32(111)
244 .u32(222)
245 .done();
246 let a = Attrs::decode(&mut Dec::new(&bytes)).expect("attrs");
247 assert_eq!(a.size, Some(4096));
248 assert_eq!(a.uid, Some(1000));
249 assert_eq!(a.mtime, Some(222));
250 assert!(a.is_dir());
251 assert!(!a.is_symlink());
252 }
253
254 #[test]
255 fn skips_extended_attr_pairs() {
256 let bytes = Enc::new()
257 .u32(A_SIZE | A_EXT)
258 .u64(1)
259 .u32(1)
260 .str(b"k")
261 .str(b"v")
262 .done();
263 let a = Attrs::decode(&mut Dec::new(&bytes)).expect("attrs");
264 assert_eq!(a.size, Some(1));
265 }
266
267 #[test]
270 fn reads_the_owner_out_of_a_listing_line() {
271 for line in [
272 "-rw-r--r-- 1 souta devs 1234 Sep 12 01:00 souta.jsonl",
273 "-rw-r--r-- 1 souta devs 1234 Sep 12 2024 souta.jsonl",
275 "drwxrwsr-x 2 souta devs 4096 Sep 12 01:00 ann",
276 "-rwsr-xr-x+ 1 souta devs 1234 Sep 12 01:00 x",
278 "-rw-r--r-- 1 souta devs 1234 Sep 12 01:00 two words.jsonl",
280 ] {
281 assert_eq!(owner_of_longname(line), Some("souta"), "parsing {line:?}");
282 }
283 }
284
285 #[test]
288 fn anything_unfamiliar_yields_no_owner() {
289 for line in [
290 "",
291 "souta.jsonl",
293 "-rw-r--r- 1 souta devs 1234 Sep 12 01:00 x",
295 "-rw-r--r--x 1 souta devs 1234 Sep 12 01:00 x",
296 "?rw-r--r-- 1 souta devs 1234 Sep 12 01:00 x",
297 "-rw-r--r-- one souta devs 1234 Sep 12 01:00 x",
300 "-rw-r--r-- 1 souta devs size Sep 12 01:00 x",
301 "-rw-r--r-- 1 souta",
303 ] {
304 assert_eq!(owner_of_longname(line), None, "parsing {line:?}");
305 }
306 }
307
308 #[test]
312 fn a_ten_byte_mode_column_that_is_not_ten_characters_declines() {
313 let mode = "-\u{e9}-r--r--";
314 assert_eq!(mode.len(), 10, "ten bytes");
315 assert_eq!(mode.chars().count(), 9, "but nine characters");
316 assert_eq!(
317 owner_of_longname(&format!("{mode} 1 souta devs 1 Sep 12 01:00 x")),
318 None
319 );
320 }
321}