use std::collections::BTreeMap;
pub const C2S_FS_SYNC: u8 = 0x40;
pub const C2S_FS_STOP: u8 = 0x41;
pub const C2S_FS_ACK: u8 = 0x42;
pub const C2S_FS_FETCH: u8 = 0x43;
pub const C2S_FS_WRITE: u8 = 0x44;
pub const C2S_FS_OP: u8 = 0x45;
pub const C2S_FS_SEARCH: u8 = 0x46;
pub const C2S_FS_INDEX: u8 = 0x47;
pub const C2S_FS_GREP: u8 = 0x48;
pub const S2C_FS_SYNCED: u8 = 0x40;
pub const S2C_FS_UPDATE: u8 = 0x41;
pub const S2C_FS_FILE: u8 = 0x42;
pub const S2C_FS_CLOSED: u8 = 0x43;
pub const S2C_FS_DONE: u8 = 0x44;
pub const S2C_FS_SEARCH: u8 = 0x45;
pub const S2C_FS_INDEX: u8 = 0x46;
pub const S2C_FS_GREP: u8 = 0x47;
pub const FEATURE_FS: u32 = 1 << 6;
pub const FS_SYNC_ID_INVALID: u16 = 0xFFFF;
pub const FS_SYNC_RECURSIVE: u16 = 1 << 0;
pub const FS_SYNC_CONTENT: u16 = 1 << 1;
pub const FS_SYNC_CROSS_FILESYSTEM: u16 = 1 << 2;
pub const FS_SYNC_SINGLE: u16 = 1 << 3;
pub const FS_SYNC_FROM_PTY: u16 = 1 << 4;
pub const FS_SYNC_EXCLUDE_GIT: u16 = 1 << 5;
pub const FS_SYNC_GITIGNORE: u16 = 1 << 6;
pub const FS_SYNC_EXCLUDE: u16 = 1 << 7;
pub const FS_SYNC_DOTIGNORE: u16 = 1 << 8;
pub const FS_SYNC_FLAGS_KNOWN: u16 = FS_SYNC_RECURSIVE
| FS_SYNC_CONTENT
| FS_SYNC_CROSS_FILESYSTEM
| FS_SYNC_SINGLE
| FS_SYNC_FROM_PTY
| FS_SYNC_EXCLUDE_GIT
| FS_SYNC_GITIGNORE
| FS_SYNC_EXCLUDE
| FS_SYNC_DOTIGNORE;
pub const FS_SYNC_EXCLUSION_FLAGS: u16 =
FS_SYNC_EXCLUDE_GIT | FS_SYNC_GITIGNORE | FS_SYNC_EXCLUDE | FS_SYNC_DOTIGNORE;
pub fn fs_sync_flags_valid(flags: u16) -> bool {
flags & FS_SYNC_SINGLE == 0 || flags & (FS_SYNC_RECURSIVE | FS_SYNC_EXCLUSION_FLAGS) == 0
}
pub const FS_UPDATE_RESET: u8 = 1 << 0;
pub const FS_UPDATE_SYNC: u8 = 1 << 1;
pub const FS_STATUS_OK: u8 = 0;
pub const FS_STATUS_NOT_FOUND: u8 = 1;
pub const FS_STATUS_PERMISSION_DENIED: u8 = 2;
pub const FS_STATUS_RESOURCE_LIMIT: u8 = 3;
pub const FS_STATUS_OTHER: u8 = 4;
pub const FS_INDEX_TRUNCATED: u8 = 1 << 0;
pub const FS_INDEX_MAX_COUNT: usize = 1_000_000;
pub const FS_FILE_OK: u8 = 0;
pub const FS_FILE_NOT_FOUND: u8 = 1;
pub const FS_FILE_UNREADABLE: u8 = 2;
pub const FS_FILE_OTHER: u8 = 3;
pub const FS_DONE_OK: u8 = 0;
pub const FS_DONE_NOT_FOUND: u8 = 2;
pub const FS_DONE_WRONG_TYPE: u8 = 3;
pub const FS_DONE_PERMISSION: u8 = 4;
pub const FS_DONE_TOO_LARGE: u8 = 5;
pub const FS_DONE_BUDGET: u8 = 6;
pub const FS_DONE_INVALID: u8 = 7;
pub const FS_DONE_OTHER: u8 = 9;
pub const FS_GREP_CASE_SENSITIVE: u8 = 1 << 0;
pub const FS_GREP_REGEX: u8 = 1 << 1;
pub const FS_GREP_NO_IGNORE: u8 = 1 << 2;
pub const FS_GREP_WORD: u8 = 1 << 3;
pub const FS_GREP_FLAGS_KNOWN: u8 =
FS_GREP_CASE_SENSITIVE | FS_GREP_REGEX | FS_GREP_NO_IGNORE | FS_GREP_WORD;
pub const FS_GREP_TRUNCATED: u8 = 1 << 0;
pub const FS_GREP_RECORD_FILE: u8 = 0x01;
pub const FS_GREP_RECORD_MATCH: u8 = 0x02;
pub const FS_GREP_FILE_IGNORED: u8 = 1 << 0;
pub const FS_GREP_MAX_LINE: usize = 512;
pub const FS_DONE_CONFLICT: u8 = 11;
pub fn fs_done_status_text(status: u8) -> &'static str {
match status {
FS_DONE_OK => "ok",
FS_DONE_NOT_FOUND => "not found",
FS_DONE_WRONG_TYPE => "wrong type",
FS_DONE_PERMISSION => "permission denied",
FS_DONE_TOO_LARGE => "too large",
FS_DONE_BUDGET => "budget exhausted",
FS_DONE_INVALID => "invalid request",
FS_DONE_CONFLICT => "conflict",
_ => "error",
}
}
pub const FS_WRITE_NO_CAS: u8 = 1 << 0;
pub const FS_WRITE_MKPARENTS: u8 = 1 << 1;
pub const FS_WRITE_DURABLE: u8 = 1 << 2;
pub const FS_WRITE_FOLLOW_SYMLINK: u8 = 1 << 3;
pub const FS_WRITE_CONTENT_FULL: u8 = 1;
pub const FS_WRITE_CONTENT_DELTA: u8 = 2;
pub const FS_OP_MKDIR: u8 = 1;
pub const FS_OP_REMOVE: u8 = 2;
pub const FS_OP_RENAME: u8 = 3;
pub const FS_OP_SYMLINK: u8 = 4;
pub const FS_OP_HARDLINK: u8 = 5;
pub const FS_OP_NO_CAS: u8 = 1 << 0;
pub const FS_OP_MKPARENTS: u8 = 1 << 1;
pub const FS_CLOSED_CLIENT_REQUEST: u8 = 0;
pub const FS_CLOSED_ROOT_GONE: u8 = 1;
pub const FS_CLOSED_PERMISSION_LOST: u8 = 2;
pub const FS_CLOSED_BACKEND_FAILED: u8 = 3;
pub const FS_CLOSED_RESOURCE_LIMIT: u8 = 4;
pub const FS_RECORD_UPSERT: u8 = 0x01;
pub const FS_RECORD_DELETE: u8 = 0x02;
pub const FS_RECORD_MOVE: u8 = 0x03;
pub const FS_ENTRY_TYPE_MASK: u8 = 0b11;
pub const FS_ENTRY_FILE: u8 = 0;
pub const FS_ENTRY_DIR: u8 = 1;
pub const FS_ENTRY_SYMLINK: u8 = 2;
pub const FS_ENTRY_OTHER: u8 = 3;
pub const FS_ENTRY_UNREADABLE: u8 = 1 << 2;
pub const FS_ENTRY_NO_CONTENT: u8 = 1 << 3;
pub const FS_ENTRY_UNSTABLE: u8 = 1 << 4;
pub const FS_ENTRY_LINK_DIR: u8 = 1 << 5;
pub const FS_ENTRY_FILTERED: u8 = 1 << 6;
pub const FS_CONTENT_NONE: u8 = 0;
pub const FS_CONTENT_FULL: u8 = 1;
pub const FS_CONTENT_DELTA: u8 = 2;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum FsRecord<'a> {
Upsert {
path: &'a str,
entry_flags: u8,
size: u64,
mtime_ns: u64,
mode: u32,
hash: u128,
content: FsContent<'a>,
},
Delete { path: &'a str },
Move { from: &'a str, to: &'a str },
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum FsContent<'a> {
None,
Full(&'a [u8]),
Delta(&'a [u8]),
}
pub fn append_fs_record(buf: &mut Vec<u8>, record: &FsRecord<'_>) {
let start = buf.len();
buf.extend_from_slice(&0u32.to_le_bytes()); match record {
FsRecord::Upsert {
path,
entry_flags,
size,
mtime_ns,
mode,
hash,
content,
} => {
buf.push(FS_RECORD_UPSERT);
buf.push(*entry_flags);
let pb = path.as_bytes();
buf.extend_from_slice(&(pb.len() as u16).to_le_bytes());
buf.extend_from_slice(pb);
buf.extend_from_slice(&size.to_le_bytes());
buf.extend_from_slice(&mtime_ns.to_le_bytes());
buf.extend_from_slice(&mode.to_le_bytes());
buf.extend_from_slice(&hash.to_le_bytes());
match content {
FsContent::None => buf.push(FS_CONTENT_NONE),
FsContent::Full(data) => {
buf.push(FS_CONTENT_FULL);
buf.extend_from_slice(&(data.len() as u32).to_le_bytes());
buf.extend_from_slice(data);
}
FsContent::Delta(ops) => {
buf.push(FS_CONTENT_DELTA);
buf.extend_from_slice(&(ops.len() as u32).to_le_bytes());
buf.extend_from_slice(ops);
}
}
}
FsRecord::Delete { path } => {
buf.push(FS_RECORD_DELETE);
let pb = path.as_bytes();
buf.extend_from_slice(&(pb.len() as u16).to_le_bytes());
buf.extend_from_slice(pb);
}
FsRecord::Move { from, to } => {
buf.push(FS_RECORD_MOVE);
let fb = from.as_bytes();
buf.extend_from_slice(&(fb.len() as u16).to_le_bytes());
buf.extend_from_slice(fb);
let tb = to.as_bytes();
buf.extend_from_slice(&(tb.len() as u16).to_le_bytes());
buf.extend_from_slice(tb);
}
}
let len = (buf.len() - start - 4) as u32;
buf[start..start + 4].copy_from_slice(&len.to_le_bytes());
}
pub struct FsRecordIter<'a> {
data: &'a [u8],
}
pub fn fs_records(data: &[u8]) -> FsRecordIter<'_> {
FsRecordIter { data }
}
fn take_path<'a>(body: &mut &'a [u8]) -> Option<&'a str> {
if body.len() < 2 {
return None;
}
let len = u16::from_le_bytes([body[0], body[1]]) as usize;
if body.len() < 2 + len {
return None;
}
let s = std::str::from_utf8(&body[2..2 + len]).ok()?;
*body = &body[2 + len..];
Some(s)
}
impl<'a> Iterator for FsRecordIter<'a> {
type Item = FsRecord<'a>;
fn next(&mut self) -> Option<FsRecord<'a>> {
loop {
if self.data.len() < 4 {
return None;
}
let rec_len =
u32::from_le_bytes([self.data[0], self.data[1], self.data[2], self.data[3]])
as usize;
if self.data.len() < 4 + rec_len || rec_len == 0 {
return None;
}
let mut body = &self.data[4..4 + rec_len];
self.data = &self.data[4 + rec_len..];
let kind = body[0];
body = &body[1..];
match kind {
FS_RECORD_UPSERT => {
if body.is_empty() {
return None;
}
let entry_flags = body[0];
body = &body[1..];
let path = take_path(&mut body)?;
if body.len() < 8 + 8 + 4 + 16 + 1 {
return None;
}
let size = u64::from_le_bytes(body[0..8].try_into().unwrap());
let mtime_ns = u64::from_le_bytes(body[8..16].try_into().unwrap());
let mode = u32::from_le_bytes(body[16..20].try_into().unwrap());
let hash = u128::from_le_bytes(body[20..36].try_into().unwrap());
let content_kind = body[36];
body = &body[37..];
let content = match content_kind {
FS_CONTENT_NONE => FsContent::None,
FS_CONTENT_FULL | FS_CONTENT_DELTA => {
if body.len() < 4 {
return None;
}
let len = u32::from_le_bytes(body[0..4].try_into().unwrap()) as usize;
if body.len() < 4 + len {
return None;
}
let data = &body[4..4 + len];
if content_kind == FS_CONTENT_FULL {
FsContent::Full(data)
} else {
FsContent::Delta(data)
}
}
_ => return None,
};
return Some(FsRecord::Upsert {
path,
entry_flags,
size,
mtime_ns,
mode,
hash,
content,
});
}
FS_RECORD_DELETE => {
let path = take_path(&mut body)?;
return Some(FsRecord::Delete { path });
}
FS_RECORD_MOVE => {
let from = take_path(&mut body)?;
let to = take_path(&mut body)?;
return Some(FsRecord::Move { from, to });
}
_ => continue, }
}
}
}
pub fn msg_fs_sync(
nonce: u16,
flags: u16,
latency_ms: u16,
inline_max: u32,
path: &str,
) -> Vec<u8> {
msg_fs_sync_full(nonce, flags, latency_ms, inline_max, path, "", None)
}
pub fn msg_fs_sync_excluding(
nonce: u16,
flags: u16,
latency_ms: u16,
inline_max: u32,
path: &str,
exclude: &str,
) -> Vec<u8> {
msg_fs_sync_full(nonce, flags, latency_ms, inline_max, path, exclude, None)
}
pub fn msg_fs_sync_from_pty(
nonce: u16,
flags: u16,
latency_ms: u16,
inline_max: u32,
path: &str,
src_pty_id: u16,
) -> Vec<u8> {
msg_fs_sync_full(
nonce,
flags,
latency_ms,
inline_max,
path,
"",
Some(src_pty_id),
)
}
pub fn msg_fs_sync_full(
nonce: u16,
flags: u16,
latency_ms: u16,
inline_max: u32,
path: &str,
exclude: &str,
src_pty_id: Option<u16>,
) -> Vec<u8> {
let pb = path.as_bytes();
let eb = exclude.as_bytes();
let mut flags = flags;
if eb.is_empty() {
flags &= !FS_SYNC_EXCLUDE;
} else {
flags |= FS_SYNC_EXCLUDE;
}
if src_pty_id.is_some() {
flags |= FS_SYNC_FROM_PTY;
}
let mut msg = Vec::with_capacity(FS_SYNC_HEADER + pb.len() + eb.len() + 4);
msg.push(C2S_FS_SYNC);
msg.extend_from_slice(&nonce.to_le_bytes());
msg.extend_from_slice(&flags.to_le_bytes());
msg.extend_from_slice(&latency_ms.to_le_bytes());
msg.extend_from_slice(&inline_max.to_le_bytes());
msg.extend_from_slice(&(pb.len() as u16).to_le_bytes());
msg.extend_from_slice(pb);
if !eb.is_empty() {
msg.extend_from_slice(&(eb.len() as u16).to_le_bytes());
msg.extend_from_slice(eb);
}
if let Some(src) = src_pty_id {
msg.extend_from_slice(&src.to_le_bytes());
}
msg
}
pub const FS_SYNC_HEADER: usize = 13;
pub fn fs_sync_flags(msg: &[u8]) -> Option<u16> {
if msg.first().copied() != Some(C2S_FS_SYNC) || msg.len() < FS_SYNC_HEADER {
return None;
}
Some(u16::from_le_bytes([msg[3], msg[4]]))
}
fn fs_sync_trailer_start(msg: &[u8]) -> Option<usize> {
if msg.first().copied() != Some(C2S_FS_SYNC) || msg.len() < FS_SYNC_HEADER {
return None;
}
let path_len = u16::from_le_bytes([msg[11], msg[12]]) as usize;
let end = FS_SYNC_HEADER.checked_add(path_len)?;
(end <= msg.len()).then_some(end)
}
fn fs_sync_exclude_span(msg: &[u8]) -> Option<(std::ops::Range<usize>, usize)> {
let off = fs_sync_trailer_start(msg)?;
if fs_sync_flags(msg)? & FS_SYNC_EXCLUDE == 0 {
return Some((off..off, off));
}
let len_bytes = msg.get(off..off + 2)?;
let len = u16::from_le_bytes([len_bytes[0], len_bytes[1]]) as usize;
let start = off + 2;
let end = start.checked_add(len)?;
(end <= msg.len()).then_some((start..end, end))
}
pub fn fs_sync_exclude(msg: &[u8]) -> Option<&str> {
let (span, _) = fs_sync_exclude_span(msg)?;
std::str::from_utf8(&msg[span]).ok()
}
pub fn fs_sync_src_pty(msg: &[u8]) -> Option<u16> {
if fs_sync_flags(msg)? & FS_SYNC_FROM_PTY == 0 {
return None;
}
let (_, off) = fs_sync_exclude_span(msg)?;
let b = msg.get(off..off + 2)?;
Some(u16::from_le_bytes([b[0], b[1]]))
}
pub fn fs_sync_rebase(msg: &[u8], cwd: Option<&str>) -> Option<Vec<u8>> {
fs_sync_src_pty(msg)?;
let nonce = u16::from_le_bytes([msg[1], msg[2]]);
let flags = fs_sync_flags(msg)? & !FS_SYNC_FROM_PTY;
let latency_ms = u16::from_le_bytes([msg[5], msg[6]]);
let inline_max = u32::from_le_bytes([msg[7], msg[8], msg[9], msg[10]]);
let path_len = u16::from_le_bytes([msg[11], msg[12]]) as usize;
let path = std::str::from_utf8(msg.get(FS_SYNC_HEADER..FS_SYNC_HEADER + path_len)?).ok()?;
let exclude = fs_sync_exclude(msg)?;
let joined = cwd.map(|dir| {
std::path::Path::new(dir)
.join(path)
.to_string_lossy()
.into_owned()
});
let eff = joined.as_deref().unwrap_or(path);
Some(msg_fs_sync_full(
nonce, flags, latency_ms, inline_max, eff, exclude, None,
))
}
pub fn msg_fs_stop(sync_id: u16) -> Vec<u8> {
let mut msg = Vec::with_capacity(3);
msg.push(C2S_FS_STOP);
msg.extend_from_slice(&sync_id.to_le_bytes());
msg
}
pub fn msg_fs_ack(sync_id: u16, update_id: u32) -> Vec<u8> {
let mut msg = Vec::with_capacity(7);
msg.push(C2S_FS_ACK);
msg.extend_from_slice(&sync_id.to_le_bytes());
msg.extend_from_slice(&update_id.to_le_bytes());
msg
}
pub fn msg_fs_fetch(nonce: u16, sync_id: u16, path: &str) -> Vec<u8> {
let pb = path.as_bytes();
let mut msg = Vec::with_capacity(7 + pb.len());
msg.push(C2S_FS_FETCH);
msg.extend_from_slice(&nonce.to_le_bytes());
msg.extend_from_slice(&sync_id.to_le_bytes());
msg.extend_from_slice(&(pb.len() as u16).to_le_bytes());
msg.extend_from_slice(pb);
msg
}
pub fn msg_fs_search(nonce: u16, limit: u16, root: &str, query: &str) -> Vec<u8> {
let rb = root.as_bytes();
let qb = query.as_bytes();
let mut m = Vec::with_capacity(9 + rb.len() + qb.len());
m.push(C2S_FS_SEARCH);
m.extend_from_slice(&nonce.to_le_bytes());
m.extend_from_slice(&limit.to_le_bytes());
m.extend_from_slice(&(rb.len() as u16).to_le_bytes());
m.extend_from_slice(rb);
m.extend_from_slice(&(qb.len() as u16).to_le_bytes());
m.extend_from_slice(qb);
m
}
pub fn parse_fs_search(data: &[u8]) -> Option<(u16, u16, String, String)> {
if data.first().copied() != Some(C2S_FS_SEARCH) || data.len() < 9 {
return None;
}
let nonce = u16::from_le_bytes([data[1], data[2]]);
let limit = u16::from_le_bytes([data[3], data[4]]);
let rl = u16::from_le_bytes([data[5], data[6]]) as usize;
let ro = 7;
if data.len() < ro + rl + 2 {
return None;
}
let root = String::from_utf8_lossy(&data[ro..ro + rl]).into_owned();
let qo = ro + rl;
let ql = u16::from_le_bytes([data[qo], data[qo + 1]]) as usize;
let qs = qo + 2;
if data.len() < qs + ql {
return None;
}
let query = String::from_utf8_lossy(&data[qs..qs + ql]).into_owned();
Some((nonce, limit, root, query))
}
pub fn msg_fs_search_result(nonce: u16, status: u8, paths: &[String]) -> Vec<u8> {
let mut m = Vec::with_capacity(6 + paths.iter().map(|p| 2 + p.len()).sum::<usize>());
m.push(S2C_FS_SEARCH);
m.extend_from_slice(&nonce.to_le_bytes());
m.push(status);
m.extend_from_slice(&(paths.len() as u16).to_le_bytes());
for p in paths {
let pb = p.as_bytes();
m.extend_from_slice(&(pb.len() as u16).to_le_bytes());
m.extend_from_slice(pb);
}
m
}
pub fn parse_fs_search_result(data: &[u8]) -> Option<(u16, u8, Vec<String>)> {
if data.first().copied() != Some(S2C_FS_SEARCH) || data.len() < 6 {
return None;
}
let nonce = u16::from_le_bytes([data[1], data[2]]);
let status = data[3];
let count = u16::from_le_bytes([data[4], data[5]]) as usize;
let mut paths = Vec::with_capacity(count);
let mut off = 6;
for _ in 0..count {
if off + 2 > data.len() {
return None;
}
let pl = u16::from_le_bytes([data[off], data[off + 1]]) as usize;
off += 2;
if off + pl > data.len() {
return None;
}
paths.push(String::from_utf8_lossy(&data[off..off + pl]).into_owned());
off += pl;
}
Some((nonce, status, paths))
}
pub fn msg_fs_index(nonce: u16, root: &str) -> Vec<u8> {
let rb = root.as_bytes();
let mut m = Vec::with_capacity(6 + rb.len());
m.push(C2S_FS_INDEX);
m.extend_from_slice(&nonce.to_le_bytes());
m.push(0); m.extend_from_slice(&(rb.len() as u16).to_le_bytes());
m.extend_from_slice(rb);
m
}
pub fn parse_fs_index(data: &[u8]) -> Option<(u16, u8, String)> {
if data.first().copied() != Some(C2S_FS_INDEX) || data.len() < 6 {
return None;
}
let nonce = u16::from_le_bytes([data[1], data[2]]);
let flags = data[3];
let rl = u16::from_le_bytes([data[4], data[5]]) as usize;
if data.len() < 6 + rl {
return None;
}
let root = String::from_utf8_lossy(&data[6..6 + rl]).into_owned();
Some((nonce, flags, root))
}
pub fn msg_fs_index_result(nonce: u16, status: u8, flags: u8, paths: &[String]) -> Vec<u8> {
let mut raw = Vec::with_capacity(paths.iter().map(|p| 2 + p.len()).sum::<usize>());
for p in paths {
let pb = p.as_bytes();
raw.extend_from_slice(&(pb.len() as u16).to_le_bytes());
raw.extend_from_slice(pb);
}
let compressed = lz4_flex::compress_prepend_size(&raw);
let mut m = Vec::with_capacity(9 + compressed.len());
m.push(S2C_FS_INDEX);
m.extend_from_slice(&nonce.to_le_bytes());
m.push(status);
m.push(flags);
m.extend_from_slice(&(paths.len() as u32).to_le_bytes());
m.extend_from_slice(&compressed);
m
}
pub fn parse_fs_index_result(data: &[u8]) -> Option<(u16, u8, u8, Vec<String>)> {
if data.first().copied() != Some(S2C_FS_INDEX) || data.len() < 9 {
return None;
}
let nonce = u16::from_le_bytes([data[1], data[2]]);
let status = data[3];
let flags = data[4];
let count = u32::from_le_bytes([data[5], data[6], data[7], data[8]]) as usize;
if count > FS_INDEX_MAX_COUNT {
return None;
}
let raw = decompress_guarded(&data[9..])?;
if count > raw.len() / 2 + 1 {
return None;
}
let mut paths = Vec::with_capacity(count);
let mut off = 0;
while off < raw.len() {
if off + 2 > raw.len() {
return None;
}
let pl = u16::from_le_bytes([raw[off], raw[off + 1]]) as usize;
off += 2;
if off + pl > raw.len() {
return None;
}
paths.push(String::from_utf8_lossy(&raw[off..off + pl]).into_owned());
off += pl;
}
if paths.len() != count {
return None;
}
Some((nonce, status, flags, paths))
}
pub fn msg_fs_synced(nonce: u16, sync_id: u16, status: u8, detail: &str) -> Vec<u8> {
let db = detail.as_bytes();
let mut msg = Vec::with_capacity(8 + db.len());
msg.push(S2C_FS_SYNCED);
msg.extend_from_slice(&nonce.to_le_bytes());
msg.extend_from_slice(&sync_id.to_le_bytes());
msg.push(status);
msg.extend_from_slice(&(db.len() as u16).to_le_bytes());
msg.extend_from_slice(db);
msg
}
pub fn msg_fs_update(sync_id: u16, update_id: u32, flags: u8, records: &[u8]) -> Vec<u8> {
let compressed = lz4_flex::compress_prepend_size(records);
let mut msg = Vec::with_capacity(8 + compressed.len());
msg.push(S2C_FS_UPDATE);
msg.extend_from_slice(&sync_id.to_le_bytes());
msg.extend_from_slice(&update_id.to_le_bytes());
msg.push(flags);
msg.extend_from_slice(&compressed);
msg
}
pub fn msg_fs_file(nonce: u16, status: u8, data: &[u8]) -> Vec<u8> {
let compressed = lz4_flex::compress_prepend_size(data);
let mut msg = Vec::with_capacity(4 + compressed.len());
msg.push(S2C_FS_FILE);
msg.extend_from_slice(&nonce.to_le_bytes());
msg.push(status);
msg.extend_from_slice(&compressed);
msg
}
pub fn msg_fs_closed(sync_id: u16, reason: u8) -> Vec<u8> {
let mut msg = Vec::with_capacity(4);
msg.push(S2C_FS_CLOSED);
msg.extend_from_slice(&sync_id.to_le_bytes());
msg.push(reason);
msg
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FsNode {
pub entry_flags: u8,
pub size: u64,
pub mtime_ns: u64,
pub mode: u32,
pub hash: u128,
pub content: Option<Vec<u8>>,
}
pub const FS_MAX_DECOMPRESSED: usize = crate::MAX_DECOMPRESSED;
fn decompress_guarded(data: &[u8]) -> Option<Vec<u8>> {
if data.len() < 4 {
return None;
}
let declared = u32::from_le_bytes(data[0..4].try_into().unwrap()) as usize;
if declared > FS_MAX_DECOMPRESSED {
return None;
}
lz4_flex::decompress_size_prepended(data).ok()
}
pub fn fs_update_records(msg: &[u8]) -> Option<Vec<u8>> {
if msg.len() < 8 || msg[0] != S2C_FS_UPDATE {
return None;
}
decompress_guarded(&msg[8..])
}
pub fn parse_fs_file(msg: &[u8]) -> Option<(u16, u8, Vec<u8>)> {
if msg.len() < 4 || msg[0] != S2C_FS_FILE {
return None;
}
let nonce = u16::from_le_bytes([msg[1], msg[2]]);
let status = msg[3];
let data = decompress_guarded(&msg[4..])?;
Some((nonce, status, data))
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FsWrite {
pub nonce: u16,
pub sync_id: u16,
pub flags: u8,
pub base: u128,
pub mode: u32,
pub content_kind: u8,
pub path: String,
pub content: Vec<u8>,
}
pub fn msg_fs_write(w: &FsWrite) -> Vec<u8> {
let pb = w.path.as_bytes();
let compressed = lz4_flex::compress_prepend_size(&w.content);
let mut msg = Vec::with_capacity(29 + pb.len() + compressed.len());
msg.push(C2S_FS_WRITE);
msg.extend_from_slice(&w.nonce.to_le_bytes());
msg.extend_from_slice(&w.sync_id.to_le_bytes());
msg.push(w.flags);
msg.extend_from_slice(&w.base.to_le_bytes());
msg.extend_from_slice(&w.mode.to_le_bytes());
msg.push(w.content_kind);
msg.extend_from_slice(&(pb.len() as u16).to_le_bytes());
msg.extend_from_slice(pb);
msg.extend_from_slice(&compressed);
msg
}
pub fn parse_fs_write(msg: &[u8]) -> Option<FsWrite> {
if msg.len() < 29 || msg[0] != C2S_FS_WRITE {
return None;
}
let nonce = u16::from_le_bytes([msg[1], msg[2]]);
let sync_id = u16::from_le_bytes([msg[3], msg[4]]);
let flags = msg[5];
let base = u128::from_le_bytes(msg[6..22].try_into().unwrap());
let mode = u32::from_le_bytes(msg[22..26].try_into().unwrap());
let content_kind = msg[26];
let path_len = u16::from_le_bytes([msg[27], msg[28]]) as usize;
let path = std::str::from_utf8(msg.get(29..29 + path_len)?)
.ok()?
.to_string();
let content = decompress_guarded(&msg[29 + path_len..])?;
Some(FsWrite {
nonce,
sync_id,
flags,
base,
mode,
content_kind,
path,
content,
})
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FsOp {
pub nonce: u16,
pub sync_id: u16,
pub op: u8,
pub flags: u8,
pub base: u128,
pub mode: u32,
pub a: String,
pub b: String,
}
pub fn msg_fs_op(o: &FsOp) -> Vec<u8> {
let ab = o.a.as_bytes();
let bb = o.b.as_bytes();
let mut msg = Vec::with_capacity(29 + ab.len() + bb.len());
msg.push(C2S_FS_OP);
msg.extend_from_slice(&o.nonce.to_le_bytes());
msg.extend_from_slice(&o.sync_id.to_le_bytes());
msg.push(o.op);
msg.push(o.flags);
msg.extend_from_slice(&o.base.to_le_bytes());
msg.extend_from_slice(&o.mode.to_le_bytes());
msg.extend_from_slice(&(ab.len() as u16).to_le_bytes());
msg.extend_from_slice(ab);
msg.extend_from_slice(&(bb.len() as u16).to_le_bytes());
msg.extend_from_slice(bb);
msg
}
pub fn parse_fs_op(msg: &[u8]) -> Option<FsOp> {
if msg.len() < 29 || msg[0] != C2S_FS_OP {
return None;
}
let nonce = u16::from_le_bytes([msg[1], msg[2]]);
let sync_id = u16::from_le_bytes([msg[3], msg[4]]);
let op = msg[5];
let flags = msg[6];
let base = u128::from_le_bytes(msg[7..23].try_into().unwrap());
let mode = u32::from_le_bytes(msg[23..27].try_into().unwrap());
let a_len = u16::from_le_bytes([msg[27], msg[28]]) as usize;
let a = std::str::from_utf8(msg.get(29..29 + a_len)?)
.ok()?
.to_string();
let b_off = 29 + a_len;
let b_len = u16::from_le_bytes([*msg.get(b_off)?, *msg.get(b_off + 1)?]) as usize;
let b = std::str::from_utf8(msg.get(b_off + 2..b_off + 2 + b_len)?)
.ok()?
.to_string();
Some(FsOp {
nonce,
sync_id,
op,
flags,
base,
mode,
a,
b,
})
}
pub fn msg_fs_done(nonce: u16, status: u8, hash: u128, mtime_ns: u64) -> Vec<u8> {
let mut msg = Vec::with_capacity(28);
msg.push(S2C_FS_DONE);
msg.extend_from_slice(&nonce.to_le_bytes());
msg.push(status);
msg.extend_from_slice(&hash.to_le_bytes());
msg.extend_from_slice(&mtime_ns.to_le_bytes());
msg
}
pub fn parse_fs_done(msg: &[u8]) -> Option<(u16, u8, u128, u64)> {
if msg.len() < 28 || msg[0] != S2C_FS_DONE {
return None;
}
let nonce = u16::from_le_bytes([msg[1], msg[2]]);
let status = msg[3];
let hash = u128::from_le_bytes(msg[4..20].try_into().unwrap());
let mtime_ns = u64::from_le_bytes(msg[20..28].try_into().unwrap());
Some((nonce, status, hash, mtime_ns))
}
#[derive(Debug, Default)]
pub struct FsMirror {
pub live: BTreeMap<String, FsNode>,
staging: Option<BTreeMap<String, FsNode>>,
}
impl FsMirror {
pub fn new() -> Self {
Self::default()
}
pub fn apply_update(&mut self, msg: &[u8]) -> Option<u32> {
if msg.len() < 8 || msg[0] != S2C_FS_UPDATE {
return None;
}
let update_id = u32::from_le_bytes([msg[3], msg[4], msg[5], msg[6]]);
let flags = msg[7];
let records = decompress_guarded(&msg[8..])?;
if flags & FS_UPDATE_RESET != 0 {
self.staging = Some(BTreeMap::new());
}
let map = self.staging.as_mut().unwrap_or(&mut self.live);
for record in fs_records(&records) {
match record {
FsRecord::Upsert {
path,
entry_flags,
size,
mtime_ns,
mode,
hash,
content,
} => {
let content = match content {
FsContent::None => {
let entry_type = entry_flags & FS_ENTRY_TYPE_MASK;
let content_bearing =
entry_type == FS_ENTRY_FILE || entry_type == FS_ENTRY_SYMLINK;
if !content_bearing
|| entry_flags
& (FS_ENTRY_NO_CONTENT
| FS_ENTRY_UNREADABLE
| FS_ENTRY_UNSTABLE)
!= 0
{
None
} else {
map.remove(path)
.filter(|n| n.entry_flags & FS_ENTRY_TYPE_MASK == entry_type)
.and_then(|n| n.content)
}
}
FsContent::Full(data) => Some(data.to_vec()),
FsContent::Delta(ops) => {
let base = map
.get(path)
.and_then(|n| n.content.as_deref())
.unwrap_or(&[]);
Some(apply_fs_delta(base, ops)?)
}
};
map.insert(
path.to_string(),
FsNode {
entry_flags,
size,
mtime_ns,
mode,
hash,
content,
},
);
}
FsRecord::Delete { path } => {
remove_subtree(map, path);
}
FsRecord::Move { from, to } => {
let moved = take_subtree(map, from);
for (suffix, node) in moved {
let new_path = join_moved(to, &suffix);
map.insert(new_path, node);
}
}
}
}
if flags & FS_UPDATE_SYNC != 0
&& let Some(staged) = self.staging.take()
{
self.live = staged;
}
Some(update_id)
}
}
fn subtree_keys(map: &BTreeMap<String, FsNode>, root: &str) -> Vec<String> {
if root.is_empty() {
return map.keys().cloned().collect();
}
let mut keys: Vec<String> = Vec::new();
if map.contains_key(root) {
keys.push(root.to_string());
}
let prefix = format!("{root}/");
keys.extend(
map.range(prefix.clone()..)
.take_while(|(k, _)| k.starts_with(&prefix))
.map(|(k, _)| k.clone()),
);
keys
}
fn remove_subtree(map: &mut BTreeMap<String, FsNode>, root: &str) {
for key in subtree_keys(map, root) {
map.remove(&key);
}
}
fn take_subtree(map: &mut BTreeMap<String, FsNode>, root: &str) -> Vec<(String, FsNode)> {
subtree_keys(map, root)
.into_iter()
.map(|key| {
let node = map.remove(&key).unwrap();
let suffix = if key.len() > root.len() {
key[root.len() + if root.is_empty() { 0 } else { 1 }..].to_string()
} else {
String::new()
};
(suffix, node)
})
.collect()
}
fn join_moved(to: &str, suffix: &str) -> String {
if suffix.is_empty() {
to.to_string()
} else if to.is_empty() {
suffix.to_string()
} else {
format!("{to}/{suffix}")
}
}
pub fn apply_fs_delta(base: &[u8], mut ops: &[u8]) -> Option<Vec<u8>> {
fn leb128(data: &mut &[u8]) -> Option<u64> {
let mut value = 0u64;
let mut shift = 0u32;
loop {
let (&byte, rest) = data.split_first()?;
*data = rest;
if shift >= 64 {
return None;
}
value |= u64::from(byte & 0x7F) << shift;
if byte & 0x80 == 0 {
return Some(value);
}
shift += 7;
}
}
let mut out = Vec::new();
while let Some((&op, rest)) = ops.split_first() {
ops = rest;
match op {
0x01 => {
let offset = leb128(&mut ops)? as usize;
let len = leb128(&mut ops)? as usize;
if out.len().checked_add(len)? > FS_MAX_DECOMPRESSED {
return None;
}
out.extend_from_slice(base.get(offset..offset.checked_add(len)?)?);
}
0x02 => {
let len = leb128(&mut ops)? as usize;
if ops.len() < len {
return None;
}
if out.len().checked_add(len)? > FS_MAX_DECOMPRESSED {
return None;
}
out.extend_from_slice(&ops[..len]);
ops = &ops[len..];
}
_ => return None,
}
}
Some(out)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FsGrepRecord {
File {
flags: u8,
n: u16,
path: String,
},
Match {
line: u32,
col: u32,
end_line: u32,
end_col: u32,
text: String,
},
}
pub fn msg_fs_grep(
nonce: u16,
flags: u8,
max_matches: u16,
max_per_file: u16,
root: &str,
query: &str,
) -> Vec<u8> {
let rb = root.as_bytes();
let qb = query.as_bytes();
let mut m = Vec::with_capacity(12 + rb.len() + qb.len());
m.push(C2S_FS_GREP);
m.extend_from_slice(&nonce.to_le_bytes());
m.push(flags);
m.extend_from_slice(&max_matches.to_le_bytes());
m.extend_from_slice(&max_per_file.to_le_bytes());
m.extend_from_slice(&(rb.len() as u16).to_le_bytes());
m.extend_from_slice(rb);
m.extend_from_slice(&(qb.len() as u16).to_le_bytes());
m.extend_from_slice(qb);
m
}
pub fn parse_fs_grep(data: &[u8]) -> Option<(u16, u8, u16, u16, String, String)> {
if data.first().copied() != Some(C2S_FS_GREP) || data.len() < 12 {
return None;
}
let nonce = u16::from_le_bytes([data[1], data[2]]);
let flags = data[3];
let max_matches = u16::from_le_bytes([data[4], data[5]]);
let max_per_file = u16::from_le_bytes([data[6], data[7]]);
let rl = u16::from_le_bytes([data[8], data[9]]) as usize;
let ro = 10;
if data.len() < ro + rl + 2 {
return None;
}
let root = String::from_utf8_lossy(&data[ro..ro + rl]).into_owned();
let qo = ro + rl;
let ql = u16::from_le_bytes([data[qo], data[qo + 1]]) as usize;
let qs = qo + 2;
if data.len() < qs + ql {
return None;
}
let query = String::from_utf8_lossy(&data[qs..qs + ql]).into_owned();
Some((nonce, flags, max_matches, max_per_file, root, query))
}
pub fn append_fs_grep_record(buf: &mut Vec<u8>, record: &FsGrepRecord) {
let start = buf.len();
buf.extend_from_slice(&0u32.to_le_bytes()); match record {
FsGrepRecord::File { flags, n, path } => {
buf.push(FS_GREP_RECORD_FILE);
buf.push(*flags);
buf.extend_from_slice(&n.to_le_bytes());
let pb = path.as_bytes();
buf.extend_from_slice(&(pb.len() as u16).to_le_bytes());
buf.extend_from_slice(pb);
}
FsGrepRecord::Match {
line,
col,
end_line,
end_col,
text,
} => {
buf.push(FS_GREP_RECORD_MATCH);
buf.extend_from_slice(&line.to_le_bytes());
buf.extend_from_slice(&col.to_le_bytes());
buf.extend_from_slice(&end_line.to_le_bytes());
buf.extend_from_slice(&end_col.to_le_bytes());
let tb = text.as_bytes();
buf.extend_from_slice(&(tb.len() as u32).to_le_bytes());
buf.extend_from_slice(tb);
}
}
let len = (buf.len() - start - 4) as u32;
buf[start..start + 4].copy_from_slice(&len.to_le_bytes());
}
pub fn fs_grep_records(data: &[u8]) -> Vec<FsGrepRecord> {
let mut out = Vec::new();
let mut off = 0usize;
while off + 4 <= data.len() {
let len = u32::from_le_bytes(data[off..off + 4].try_into().unwrap()) as usize;
if len == 0 || off + 4 + len > data.len() {
return out;
}
let body = &data[off + 4..off + 4 + len];
off += 4 + len;
match body[0] {
FS_GREP_RECORD_FILE => {
if body.len() < 6 {
return out;
}
let flags = body[1];
let n = u16::from_le_bytes([body[2], body[3]]);
let pl = u16::from_le_bytes([body[4], body[5]]) as usize;
if body.len() < 6 + pl {
return out;
}
out.push(FsGrepRecord::File {
flags,
n,
path: String::from_utf8_lossy(&body[6..6 + pl]).into_owned(),
});
}
FS_GREP_RECORD_MATCH => {
if body.len() < 21 {
return out;
}
let line = u32::from_le_bytes(body[1..5].try_into().unwrap());
let col = u32::from_le_bytes(body[5..9].try_into().unwrap());
let end_line = u32::from_le_bytes(body[9..13].try_into().unwrap());
let end_col = u32::from_le_bytes(body[13..17].try_into().unwrap());
let tl = u32::from_le_bytes(body[17..21].try_into().unwrap()) as usize;
if body.len() < 21 + tl {
return out;
}
out.push(FsGrepRecord::Match {
line,
col,
end_line,
end_col,
text: String::from_utf8_lossy(&body[21..21 + tl]).into_owned(),
});
}
_ => {}
}
}
out
}
pub fn msg_fs_grep_result(
nonce: u16,
status: u8,
flags: u8,
detail: &str,
records: &[u8],
) -> Vec<u8> {
let db = detail.as_bytes();
let compressed = lz4_flex::compress_prepend_size(records);
let mut m = Vec::with_capacity(7 + db.len() + compressed.len());
m.push(S2C_FS_GREP);
m.extend_from_slice(&nonce.to_le_bytes());
m.push(status);
m.push(flags);
m.extend_from_slice(&(db.len() as u16).to_le_bytes());
m.extend_from_slice(db);
m.extend_from_slice(&compressed);
m
}
pub fn parse_fs_grep_result(data: &[u8]) -> Option<(u16, u8, u8, String, Vec<u8>)> {
if data.first().copied() != Some(S2C_FS_GREP) || data.len() < 7 {
return None;
}
let nonce = u16::from_le_bytes([data[1], data[2]]);
let status = data[3];
let flags = data[4];
let dl = u16::from_le_bytes([data[5], data[6]]) as usize;
let ds = 7;
if data.len() < ds + dl {
return None;
}
let detail = String::from_utf8_lossy(&data[ds..ds + dl]).into_owned();
let records = decompress_guarded(&data[ds + dl..])?;
Some((nonce, status, flags, detail, records))
}
#[cfg(test)]
mod tests {
#[test]
fn fs_grep_request_roundtrip() {
let m = msg_fs_grep(
0x0102,
FS_GREP_CASE_SENSITIVE | FS_GREP_REGEX,
500,
50,
"/tmp/root",
"fn \\w+",
);
assert_eq!(
m.iter().map(|b| format!("{b:02x}")).collect::<String>(),
"48020103f401320009002f746d702f726f6f740600666e205c772b"
);
assert_eq!(
parse_fs_grep(&m),
Some((
0x0102,
FS_GREP_CASE_SENSITIVE | FS_GREP_REGEX,
500,
50,
"/tmp/root".to_string(),
"fn \\w+".to_string()
))
);
for cut in 0..m.len() {
assert_eq!(parse_fs_grep(&m[..cut]), None, "cut at {cut}");
}
}
#[test]
fn fs_grep_result_roundtrip() {
let recs = vec![
FsGrepRecord::File {
flags: 0,
n: 2,
path: "src/main.rs".to_string(),
},
FsGrepRecord::Match {
line: 41,
col: 4,
end_line: 41,
end_col: 6,
text: " fn main() {".to_string(),
},
FsGrepRecord::Match {
line: 99,
col: 0,
end_line: 99,
end_col: 2,
text: "fn helper()".to_string(),
},
FsGrepRecord::File {
flags: FS_GREP_FILE_IGNORED,
n: 1,
path: "target/debug/build.rs".to_string(),
},
FsGrepRecord::Match {
line: 0,
col: 0,
end_line: 0,
end_col: 2,
text: String::new(),
},
];
let mut buf = Vec::new();
for r in &recs {
append_fs_grep_record(&mut buf, r);
}
assert_eq!(fs_grep_records(&buf), recs);
let msg = msg_fs_grep_result(7, FS_DONE_OK, FS_GREP_TRUNCATED, "", &buf);
let (nonce, status, flags, detail, records) = parse_fs_grep_result(&msg).unwrap();
assert_eq!(
(nonce, status, flags, detail.as_str()),
(7, FS_DONE_OK, FS_GREP_TRUNCATED, "")
);
assert_eq!(fs_grep_records(&records), recs);
let bad = msg_fs_grep_result(8, FS_DONE_INVALID, 0, "unclosed character class", &[]);
let (_, st, _, d, r) = parse_fs_grep_result(&bad).unwrap();
assert_eq!(st, FS_DONE_INVALID);
assert_eq!(d, "unclosed character class");
assert!(fs_grep_records(&r).is_empty());
}
#[test]
fn fs_grep_records_skip_unknown_kinds() {
let mut buf = Vec::new();
append_fs_grep_record(
&mut buf,
&FsGrepRecord::File {
flags: 0,
n: 0,
path: "a".to_string(),
},
);
let start = buf.len();
buf.extend_from_slice(&0u32.to_le_bytes());
buf.push(0x7f); buf.extend_from_slice(b"whatever");
let len = (buf.len() - start - 4) as u32;
buf[start..start + 4].copy_from_slice(&len.to_le_bytes());
append_fs_grep_record(
&mut buf,
&FsGrepRecord::File {
flags: 0,
n: 0,
path: "b".to_string(),
},
);
let got = fs_grep_records(&buf);
assert_eq!(got.len(), 2, "unknown kind must not end the stream");
let mut trunc = buf.clone();
trunc.truncate(trunc.len() - 1);
assert!(fs_grep_records(&trunc).len() <= 2);
}
use super::*;
#[test]
fn fs_search_request_roundtrip() {
let m = msg_fs_search(9, 50, "/a/b:c", "eng.rs");
let (nonce, limit, root, query) = parse_fs_search(&m).unwrap();
assert_eq!(nonce, 9);
assert_eq!(limit, 50);
assert_eq!(root, "/a/b:c");
assert_eq!(query, "eng.rs");
}
#[test]
fn fs_search_result_roundtrip() {
let paths = vec!["src/main.rs".to_string(), "a/b:c/engine.rs".to_string()];
let m = msg_fs_search_result(3, FS_STATUS_OK, &paths);
let (nonce, status, out) = parse_fs_search_result(&m).unwrap();
assert_eq!(nonce, 3);
assert_eq!(status, FS_STATUS_OK);
assert_eq!(out, paths);
}
#[test]
fn fs_index_request_roundtrip() {
let m = msg_fs_index(0x0102, "/tmp/watch me");
assert_eq!(
m.iter().map(|x| format!("{x:02x}")).collect::<String>(),
"470201000d002f746d702f7761746368206d65"
);
let (nonce, flags, root) = parse_fs_index(&m).unwrap();
assert_eq!(nonce, 0x0102);
assert_eq!(flags, 0);
assert_eq!(root, "/tmp/watch me");
assert_eq!(parse_fs_index(&m[..5]), None);
assert_eq!(parse_fs_index(&msg_fs_stop(1)), None);
}
#[test]
fn fs_index_result_roundtrip() {
let paths = vec![
"a/b:c/engine.rs".to_string(),
"src/main.rs".to_string(),
String::new(),
];
let m = msg_fs_index_result(3, FS_DONE_OK, FS_INDEX_TRUNCATED, &paths);
let (nonce, status, flags, out) = parse_fs_index_result(&m).unwrap();
assert_eq!(nonce, 3);
assert_eq!(status, FS_DONE_OK);
assert_eq!(flags, FS_INDEX_TRUNCATED);
assert_eq!(out, paths);
let empty = msg_fs_index_result(4, FS_DONE_NOT_FOUND, 0, &[]);
let (_, status, _, out) = parse_fs_index_result(&empty).unwrap();
assert_eq!(status, FS_DONE_NOT_FOUND);
assert!(out.is_empty());
let mut lying = msg_fs_index_result(5, FS_DONE_OK, 0, &paths);
lying[5..9].copy_from_slice(&2u32.to_le_bytes());
assert_eq!(parse_fs_index_result(&lying), None);
let mut huge = msg_fs_index_result(6, FS_DONE_OK, 0, &[]);
huge[5..9].copy_from_slice(&((FS_INDEX_MAX_COUNT as u32) + 1).to_le_bytes());
assert_eq!(parse_fs_index_result(&huge), None);
}
#[test]
fn single_and_recursive_are_mutually_exclusive() {
assert!(fs_sync_flags_valid(FS_SYNC_SINGLE));
assert!(fs_sync_flags_valid(FS_SYNC_SINGLE | FS_SYNC_CONTENT));
assert!(fs_sync_flags_valid(FS_SYNC_RECURSIVE | FS_SYNC_CONTENT));
assert!(!fs_sync_flags_valid(FS_SYNC_SINGLE | FS_SYNC_RECURSIVE));
assert!(!fs_sync_flags_valid(
FS_SYNC_SINGLE | FS_SYNC_RECURSIVE | FS_SYNC_CONTENT
));
}
#[test]
fn fs_sync_from_pty_roundtrip_and_rebase() {
let m = msg_fs_sync_from_pty(7, FS_SYNC_RECURSIVE, 0, 0, "sub", 42);
assert_eq!(
m,
vec![
0x40, 0x07, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x73,
0x75, 0x62, 0x2a, 0x00
]
);
assert_eq!(fs_sync_src_pty(&m), Some(42));
assert_eq!(
fs_sync_src_pty(&msg_fs_sync(7, FS_SYNC_RECURSIVE, 0, 0, "sub")),
None
);
let reb = fs_sync_rebase(&m, Some("/home/u")).unwrap();
assert_eq!(fs_sync_flags(&reb).unwrap() & FS_SYNC_FROM_PTY, 0);
let plen = u16::from_le_bytes([reb[11], reb[12]]) as usize;
assert_eq!(
std::str::from_utf8(&reb[13..13 + plen]).unwrap(),
"/home/u/sub"
);
let reb0 = fs_sync_rebase(&m, None).unwrap();
let plen0 = u16::from_le_bytes([reb0[11], reb0[12]]) as usize;
assert_eq!(std::str::from_utf8(&reb0[13..13 + plen0]).unwrap(), "sub");
}
#[test]
fn fs_sync_exclude_field_roundtrips_alongside_from_pty() {
let plain = msg_fs_sync(1, FS_SYNC_RECURSIVE, 0, 0, "sub");
assert_eq!(fs_sync_flags(&plain).unwrap() & FS_SYNC_EXCLUDE, 0);
assert_eq!(fs_sync_exclude(&plain), Some(""));
assert_eq!(
msg_fs_sync_excluding(1, FS_SYNC_RECURSIVE, 0, 0, "sub", ""),
plain
);
let ex = msg_fs_sync_excluding(1, FS_SYNC_RECURSIVE, 0, 0, "sub", "target\n!keep");
assert_ne!(fs_sync_flags(&ex).unwrap() & FS_SYNC_EXCLUDE, 0);
assert_eq!(fs_sync_exclude(&ex), Some("target\n!keep"));
assert_eq!(fs_sync_src_pty(&ex), None);
let both = msg_fs_sync_full(1, FS_SYNC_RECURSIVE, 0, 0, "sub", "target", Some(42));
assert_eq!(fs_sync_exclude(&both), Some("target"));
assert_eq!(fs_sync_src_pty(&both), Some(42), "reached past the field");
let reb = fs_sync_rebase(&both, Some("/home/u")).unwrap();
assert_eq!(fs_sync_flags(&reb).unwrap() & FS_SYNC_FROM_PTY, 0);
assert_ne!(fs_sync_flags(&reb).unwrap() & FS_SYNC_EXCLUDE, 0);
assert_eq!(fs_sync_exclude(&reb), Some("target"), "filter survives");
let plen = u16::from_le_bytes([reb[11], reb[12]]) as usize;
assert_eq!(
std::str::from_utf8(&reb[13..13 + plen]).unwrap(),
"/home/u/sub"
);
assert_eq!(fs_sync_exclude(&ex[..ex.len() - 1]), None);
let mut headerless = ex.clone();
headerless.truncate(17);
assert_eq!(fs_sync_exclude(&headerless), None);
}
#[test]
fn exclusion_flags_are_rejected_with_single() {
for flag in [FS_SYNC_EXCLUDE_GIT, FS_SYNC_GITIGNORE, FS_SYNC_EXCLUDE] {
assert!(fs_sync_flags_valid(flag));
assert!(fs_sync_flags_valid(flag | FS_SYNC_RECURSIVE));
assert!(!fs_sync_flags_valid(flag | FS_SYNC_SINGLE));
}
}
fn upsert(path: &str, content: &[u8]) -> Vec<u8> {
let mut buf = Vec::new();
append_fs_record(
&mut buf,
&FsRecord::Upsert {
path,
entry_flags: FS_ENTRY_FILE,
size: content.len() as u64,
mtime_ns: 42,
mode: 0o644,
hash: 7,
content: FsContent::Full(content),
},
);
buf
}
#[test]
fn record_roundtrip() {
let mut buf = Vec::new();
append_fs_record(
&mut buf,
&FsRecord::Upsert {
path: "a/b.txt",
entry_flags: FS_ENTRY_FILE | FS_ENTRY_NO_CONTENT,
size: 10,
mtime_ns: 1_700_000_000_000_000_000,
mode: 0o755,
hash: 0xDEAD_BEEF_DEAD_BEEF_DEAD_BEEF,
content: FsContent::None,
},
);
append_fs_record(&mut buf, &FsRecord::Delete { path: "old" });
append_fs_record(
&mut buf,
&FsRecord::Move {
from: "src",
to: "dst",
},
);
let records: Vec<_> = fs_records(&buf).collect();
assert_eq!(records.len(), 3);
match &records[0] {
FsRecord::Upsert {
path,
entry_flags,
size,
mtime_ns,
mode,
hash,
content,
} => {
assert_eq!(*path, "a/b.txt");
assert_eq!(*entry_flags, FS_ENTRY_FILE | FS_ENTRY_NO_CONTENT);
assert_eq!(*size, 10);
assert_eq!(*mtime_ns, 1_700_000_000_000_000_000);
assert_eq!(*mode, 0o755);
assert_eq!(*hash, 0xDEAD_BEEF_DEAD_BEEF_DEAD_BEEF);
assert_eq!(*content, FsContent::None);
}
other => panic!("unexpected {other:?}"),
}
assert_eq!(records[1], FsRecord::Delete { path: "old" });
assert_eq!(
records[2],
FsRecord::Move {
from: "src",
to: "dst"
}
);
}
#[test]
fn wire_fixtures() {
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
assert_eq!(
hex(&msg_fs_sync(
0x0102,
FS_SYNC_RECURSIVE | FS_SYNC_CONTENT,
25,
65536,
"/tmp/watch me"
)),
"40020103001900000001000d002f746d702f7761746368206d65"
);
assert_eq!(
hex(&msg_fs_sync_full(
7,
FS_SYNC_RECURSIVE,
0,
0,
"sub",
"target",
Some(42)
)),
"4007009100000000000000030073756206007461726765742a00"
);
assert_eq!(hex(&msg_fs_stop(0x0102)), "410201");
assert_eq!(hex(&msg_fs_ack(0x0102, 0x01020304)), "42020104030201");
assert_eq!(
hex(&msg_fs_fetch(3, 0x0102, "sub/%FF.bin")),
"43030002010b007375622f2546462e62696e"
);
assert_eq!(
hex(&msg_fs_synced(0x0102, 3, 0, "/w")),
"40020103000002002f77"
);
let mut records = Vec::new();
append_fs_record(
&mut records,
&FsRecord::Upsert {
path: "a.txt",
entry_flags: FS_ENTRY_FILE,
size: 5,
mtime_ns: 1_700_000_000_123_456_789,
mode: 0o100644,
hash: 0x0123_4567_89ab_cdef_1122_3344_5566_7788,
content: FsContent::Full(b"hello"),
},
);
append_fs_record(
&mut records,
&FsRecord::Upsert {
path: "sub",
entry_flags: FS_ENTRY_DIR,
size: 0,
mtime_ns: 0,
mode: 0o40755,
hash: 0,
content: FsContent::None,
},
);
append_fs_record(
&mut records,
&FsRecord::Upsert {
path: "sub/%FF.bin", entry_flags: FS_ENTRY_FILE | FS_ENTRY_NO_CONTENT,
size: 1 << 20,
mtime_ns: 1,
mode: 0o100600,
hash: 0xff,
content: FsContent::None,
},
);
append_fs_record(&mut records, &FsRecord::Delete { path: "old" });
append_fs_record(
&mut records,
&FsRecord::Move {
from: "src",
to: "dst",
},
);
assert_eq!(
hex(&records),
"3700000001000500612e747874050000000000000015cd853dfe9c9717a48100008877665544332211efcdab8967452301010500000068656c6c6f2c0000000101030073756200000000000000000000000000000000ed41000000000000000000000000000000000000003400000001080b007375622f2546462e62696e0000100000000000010000000000000080810000ff00000000000000000000000000000000060000000203006f6c640b0000000303007372630300647374"
);
let decoded: Vec<_> = fs_records(&records).collect();
assert_eq!(decoded.len(), 5);
assert!(matches!(
&decoded[0],
FsRecord::Upsert {
path: "a.txt",
size: 5,
mtime_ns: 1_700_000_000_123_456_789,
hash: 0x0123_4567_89ab_cdef_1122_3344_5566_7788,
content: FsContent::Full(b"hello"),
..
}
));
assert_eq!(decoded[3], FsRecord::Delete { path: "old" });
assert_eq!(
decoded[4],
FsRecord::Move {
from: "src",
to: "dst"
}
);
}
#[test]
fn oversized_declared_length_is_rejected_before_allocation() {
let mut msg = vec![S2C_FS_UPDATE];
msg.extend_from_slice(&1u16.to_le_bytes()); msg.extend_from_slice(&1u32.to_le_bytes()); msg.push(0); msg.extend_from_slice(&(1u32 << 30).to_le_bytes()); msg.extend_from_slice(&[0u8; 16]); let mut mirror = FsMirror::new();
assert_eq!(mirror.apply_update(&msg), None);
let mut file = vec![S2C_FS_FILE];
file.extend_from_slice(&7u16.to_le_bytes()); file.push(FS_FILE_OK);
file.extend_from_slice(&(1u32 << 30).to_le_bytes());
file.extend_from_slice(&[0u8; 16]);
assert_eq!(parse_fs_file(&file), None);
}
#[test]
fn fs_file_roundtrip() {
let msg = msg_fs_file(9, FS_FILE_OK, b"contents");
assert_eq!(
parse_fs_file(&msg),
Some((9, FS_FILE_OK, b"contents".to_vec()))
);
}
#[test]
fn fs_write_roundtrip() {
let w = FsWrite {
nonce: 7,
sync_id: 3,
flags: FS_WRITE_MKPARENTS | FS_WRITE_DURABLE,
base: 0x0123_4567_89ab_cdef_0123_4567_89ab_cdef,
mode: 0o644,
content_kind: FS_WRITE_CONTENT_FULL,
path: "dir/50%25.txt".to_string(),
content: b"hello world".to_vec(),
};
assert_eq!(parse_fs_write(&msg_fs_write(&w)), Some(w));
let w0 = FsWrite {
nonce: 1,
sync_id: 1,
flags: 0,
base: 0,
mode: 0,
content_kind: FS_WRITE_CONTENT_FULL,
path: "new.txt".to_string(),
content: Vec::new(),
};
assert_eq!(parse_fs_write(&msg_fs_write(&w0)), Some(w0));
assert_eq!(parse_fs_write(&[C2S_FS_WRITE, 0, 0]), None);
assert_eq!(parse_fs_write(&msg_fs_file(1, 0, b"x")), None);
}
#[test]
fn fs_op_roundtrip() {
let rename = FsOp {
nonce: 42,
sync_id: 9,
op: FS_OP_RENAME,
flags: FS_OP_MKPARENTS,
base: 0,
mode: 0,
a: "old/name".to_string(),
b: "new/name".to_string(),
};
assert_eq!(parse_fs_op(&msg_fs_op(&rename)), Some(rename));
let mkdir = FsOp {
nonce: 2,
sync_id: 1,
op: FS_OP_MKDIR,
flags: 0,
base: 0,
mode: 0o700,
a: "sub".to_string(),
b: String::new(),
};
assert_eq!(parse_fs_op(&msg_fs_op(&mkdir)), Some(mkdir));
assert_eq!(parse_fs_op(&[C2S_FS_OP, 0],), None);
}
#[test]
fn fs_write_family_byte_fixtures() {
let hex = |b: &[u8]| b.iter().map(|x| format!("{x:02x}")).collect::<String>();
let w = FsWrite {
nonce: 0x0102,
sync_id: 0x0304,
flags: FS_WRITE_MKPARENTS,
base: 0x0f0e_0d0c_0b0a_0908_0706_0504_0302_0100,
mode: 0o644,
content_kind: FS_WRITE_CONTENT_FULL,
path: "a/b.txt".into(),
content: b"hi".to_vec(),
};
assert_eq!(
hex(&msg_fs_write(&w)),
"440201040302000102030405060708090a0b0c0d0e0fa4010000010700612f622e74787402000000206869"
);
let o = FsOp {
nonce: 0x0102,
sync_id: 0x0304,
op: FS_OP_RENAME,
flags: FS_OP_MKPARENTS,
base: 0,
mode: 0,
a: "x".into(),
b: "y".into(),
};
assert_eq!(
hex(&msg_fs_op(&o)),
"450201040303020000000000000000000000000000000000000000010078010079"
);
let ln = FsOp {
nonce: 0x0102,
sync_id: 0x0304,
op: FS_OP_SYMLINK,
flags: FS_OP_NO_CAS,
base: 0,
mode: 0,
a: "../t".into(),
b: "l".into(),
};
assert_eq!(
hex(&msg_fs_op(&ln)),
"45020104030401000000000000000000000000000000000000000004002e2e2f7401006c"
);
assert_eq!(
hex(&msg_fs_done(
0x0102,
FS_DONE_CONFLICT,
0x0f0e_0d0c_0b0a_0908_0706_0504_0302_0100,
0x1122_3344_5566_7788
)),
"4402010b000102030405060708090a0b0c0d0e0f8877665544332211"
);
}
#[test]
fn fs_done_roundtrip() {
let hash = 0xdead_beef_dead_beef_dead_beef_dead_beefu128;
let msg = msg_fs_done(5, FS_DONE_OK, hash, 1_700_000_000_000_000_000);
assert_eq!(
parse_fs_done(&msg),
Some((5, FS_DONE_OK, hash, 1_700_000_000_000_000_000))
);
let c = msg_fs_done(6, FS_DONE_CONFLICT, hash, 0);
assert_eq!(parse_fs_done(&c), Some((6, FS_DONE_CONFLICT, hash, 0)));
}
#[test]
fn unknown_record_kind_is_skipped() {
let mut buf = Vec::new();
buf.extend_from_slice(&4u32.to_le_bytes());
buf.push(0x7F);
buf.extend_from_slice(&[1, 2, 3]);
append_fs_record(&mut buf, &FsRecord::Delete { path: "x" });
let records: Vec<_> = fs_records(&buf).collect();
assert_eq!(records, vec![FsRecord::Delete { path: "x" }]);
}
#[test]
fn mirror_staged_snapshot_and_live() {
let mut mirror = FsMirror::new();
let mut records = upsert("a.txt", b"alpha");
records.extend_from_slice(&upsert("d/b.txt", b"beta"));
let msg = msg_fs_update(1, 1, FS_UPDATE_RESET | FS_UPDATE_SYNC, &records);
assert_eq!(mirror.apply_update(&msg), Some(1));
assert_eq!(mirror.live.len(), 2);
assert_eq!(mirror.live["a.txt"].content.as_deref(), Some(&b"alpha"[..]));
let mut records = Vec::new();
append_fs_record(&mut records, &FsRecord::Delete { path: "a.txt" });
append_fs_record(&mut records, &FsRecord::Move { from: "d", to: "e" });
let msg = msg_fs_update(1, 2, 0, &records);
assert_eq!(mirror.apply_update(&msg), Some(2));
assert_eq!(mirror.live.len(), 1);
assert_eq!(
mirror.live["e/b.txt"].content.as_deref(),
Some(&b"beta"[..])
);
let msg = msg_fs_update(1, 3, FS_UPDATE_RESET, &upsert("n.txt", b"new"));
assert_eq!(mirror.apply_update(&msg), Some(3));
assert_eq!(mirror.live.len(), 1);
let msg = msg_fs_update(1, 4, FS_UPDATE_SYNC, &[]);
assert_eq!(mirror.apply_update(&msg), Some(4));
assert_eq!(mirror.live.len(), 1);
assert!(mirror.live.contains_key("n.txt"));
}
#[test]
fn delta_content() {
let mut mirror = FsMirror::new();
let msg = msg_fs_update(
1,
1,
FS_UPDATE_RESET | FS_UPDATE_SYNC,
&upsert("f", b"hello world"),
);
mirror.apply_update(&msg).unwrap();
let ops: Vec<u8> = vec![0x01, 0, 6, 0x02, 4, b'b', b'l', b'i', b't'];
let mut records = Vec::new();
append_fs_record(
&mut records,
&FsRecord::Upsert {
path: "f",
entry_flags: FS_ENTRY_FILE,
size: 10,
mtime_ns: 43,
mode: 0o644,
hash: 8,
content: FsContent::Delta(&ops),
},
);
let msg = msg_fs_update(1, 2, 0, &records);
mirror.apply_update(&msg).unwrap();
assert_eq!(
mirror.live["f"].content.as_deref(),
Some(&b"hello blit"[..])
);
}
#[test]
fn subtree_semantics() {
let mut map = BTreeMap::new();
for p in ["a", "a/b", "a/b/c", "ab", "z"] {
map.insert(
p.to_string(),
FsNode {
entry_flags: FS_ENTRY_FILE,
size: 0,
mtime_ns: 0,
mode: 0,
hash: 0,
content: None,
},
);
}
let taken = take_subtree(&mut map.clone(), "a");
let suffixes: Vec<_> = taken.iter().map(|(s, _)| s.clone()).collect();
assert_eq!(
suffixes,
vec![String::new(), "b".to_string(), "b/c".to_string()]
);
remove_subtree(&mut map, "a");
let left: Vec<_> = map.keys().cloned().collect();
assert_eq!(left, vec!["ab".to_string(), "z".to_string()]);
}
}