1use std::collections::BTreeMap;
11
12pub const C2S_FS_SYNC: u8 = 0x40;
16pub const C2S_FS_STOP: u8 = 0x41;
18pub const C2S_FS_ACK: u8 = 0x42;
20pub const C2S_FS_FETCH: u8 = 0x43;
22pub const C2S_FS_WRITE: u8 = 0x44;
24pub const C2S_FS_OP: u8 = 0x45;
26pub const C2S_FS_SEARCH: u8 = 0x46;
29pub const C2S_FS_INDEX: u8 = 0x47;
33
34pub const C2S_FS_GREP: u8 = 0x48;
40
41pub const S2C_FS_SYNCED: u8 = 0x40;
44pub const S2C_FS_UPDATE: u8 = 0x41;
46pub const S2C_FS_FILE: u8 = 0x42;
48pub const S2C_FS_CLOSED: u8 = 0x43;
50pub const S2C_FS_DONE: u8 = 0x44;
52pub const S2C_FS_SEARCH: u8 = 0x45;
54pub const S2C_FS_INDEX: u8 = 0x46;
58
59pub const S2C_FS_GREP: u8 = 0x47;
64
65pub const FEATURE_FS: u32 = 1 << 6;
70
71pub const FS_SYNC_ID_INVALID: u16 = 0xFFFF;
73
74pub const FS_SYNC_RECURSIVE: u16 = 1 << 0;
78pub const FS_SYNC_CONTENT: u16 = 1 << 1;
79pub const FS_SYNC_CROSS_FILESYSTEM: u16 = 1 << 2;
80pub const FS_SYNC_SINGLE: u16 = 1 << 3;
88pub const FS_SYNC_FROM_PTY: u16 = 1 << 4;
92pub const FS_SYNC_EXCLUDE_GIT: u16 = 1 << 5;
96pub const FS_SYNC_GITIGNORE: u16 = 1 << 6;
101pub const FS_SYNC_EXCLUDE: u16 = 1 << 7;
107pub const FS_SYNC_DOTIGNORE: u16 = 1 << 8;
114
115pub const FS_SYNC_FLAGS_KNOWN: u16 = FS_SYNC_RECURSIVE
118 | FS_SYNC_CONTENT
119 | FS_SYNC_CROSS_FILESYSTEM
120 | FS_SYNC_SINGLE
121 | FS_SYNC_FROM_PTY
122 | FS_SYNC_EXCLUDE_GIT
123 | FS_SYNC_GITIGNORE
124 | FS_SYNC_EXCLUDE
125 | FS_SYNC_DOTIGNORE;
126
127pub const FS_SYNC_EXCLUSION_FLAGS: u16 =
129 FS_SYNC_EXCLUDE_GIT | FS_SYNC_GITIGNORE | FS_SYNC_EXCLUDE | FS_SYNC_DOTIGNORE;
130
131pub fn fs_sync_flags_valid(flags: u16) -> bool {
137 flags & FS_SYNC_SINGLE == 0 || flags & (FS_SYNC_RECURSIVE | FS_SYNC_EXCLUSION_FLAGS) == 0
138}
139
140pub const FS_UPDATE_RESET: u8 = 1 << 0;
144pub const FS_UPDATE_SYNC: u8 = 1 << 1;
146
147pub const FS_STATUS_OK: u8 = 0;
149pub const FS_STATUS_NOT_FOUND: u8 = 1;
150pub const FS_STATUS_PERMISSION_DENIED: u8 = 2;
151pub const FS_STATUS_RESOURCE_LIMIT: u8 = 3;
152pub const FS_STATUS_OTHER: u8 = 4;
153
154pub const FS_INDEX_TRUNCATED: u8 = 1 << 0;
158pub const FS_INDEX_MAX_COUNT: usize = 1_000_000;
163
164pub const FS_FILE_OK: u8 = 0;
166pub const FS_FILE_NOT_FOUND: u8 = 1;
167pub const FS_FILE_UNREADABLE: u8 = 2;
168pub const FS_FILE_OTHER: u8 = 3;
169
170pub const FS_DONE_OK: u8 = 0;
174pub const FS_DONE_NOT_FOUND: u8 = 2;
175pub const FS_DONE_WRONG_TYPE: u8 = 3;
176pub const FS_DONE_PERMISSION: u8 = 4;
177pub const FS_DONE_TOO_LARGE: u8 = 5;
178pub const FS_DONE_BUDGET: u8 = 6;
179pub const FS_DONE_INVALID: u8 = 7;
180pub const FS_DONE_OTHER: u8 = 9;
181
182pub const FS_GREP_CASE_SENSITIVE: u8 = 1 << 0;
186pub const FS_GREP_REGEX: u8 = 1 << 1;
188pub const FS_GREP_NO_IGNORE: u8 = 1 << 2;
193pub const FS_GREP_WORD: u8 = 1 << 3;
197pub const FS_GREP_FLAGS_KNOWN: u8 =
199 FS_GREP_CASE_SENSITIVE | FS_GREP_REGEX | FS_GREP_NO_IGNORE | FS_GREP_WORD;
200
201pub const FS_GREP_TRUNCATED: u8 = 1 << 0;
206
207pub const FS_GREP_RECORD_FILE: u8 = 0x01;
210pub const FS_GREP_RECORD_MATCH: u8 = 0x02;
211
212pub const FS_GREP_FILE_IGNORED: u8 = 1 << 0;
217
218pub const FS_GREP_MAX_LINE: usize = 512;
222pub const FS_DONE_CONFLICT: u8 = 11;
227
228pub fn fs_done_status_text(status: u8) -> &'static str {
230 match status {
231 FS_DONE_OK => "ok",
232 FS_DONE_NOT_FOUND => "not found",
233 FS_DONE_WRONG_TYPE => "wrong type",
234 FS_DONE_PERMISSION => "permission denied",
235 FS_DONE_TOO_LARGE => "too large",
236 FS_DONE_BUDGET => "budget exhausted",
237 FS_DONE_INVALID => "invalid request",
238 FS_DONE_CONFLICT => "conflict",
239 _ => "error",
240 }
241}
242
243pub const FS_WRITE_NO_CAS: u8 = 1 << 0;
246pub const FS_WRITE_MKPARENTS: u8 = 1 << 1;
248pub const FS_WRITE_DURABLE: u8 = 1 << 2;
250pub const FS_WRITE_FOLLOW_SYMLINK: u8 = 1 << 3;
253
254pub const FS_WRITE_CONTENT_FULL: u8 = 1;
261pub const FS_WRITE_CONTENT_DELTA: u8 = 2;
262
263pub const FS_OP_MKDIR: u8 = 1;
265pub const FS_OP_REMOVE: u8 = 2;
266pub const FS_OP_RENAME: u8 = 3;
267pub const FS_OP_SYMLINK: u8 = 4;
272pub const FS_OP_HARDLINK: u8 = 5;
275
276pub const FS_OP_NO_CAS: u8 = 1 << 0;
278pub const FS_OP_MKPARENTS: u8 = 1 << 1;
279
280pub const FS_CLOSED_CLIENT_REQUEST: u8 = 0;
282pub const FS_CLOSED_ROOT_GONE: u8 = 1;
283pub const FS_CLOSED_PERMISSION_LOST: u8 = 2;
284pub const FS_CLOSED_BACKEND_FAILED: u8 = 3;
285pub const FS_CLOSED_RESOURCE_LIMIT: u8 = 4;
286
287pub const FS_RECORD_UPSERT: u8 = 0x01;
289pub const FS_RECORD_DELETE: u8 = 0x02;
290pub const FS_RECORD_MOVE: u8 = 0x03;
291
292pub const FS_ENTRY_TYPE_MASK: u8 = 0b11;
294pub const FS_ENTRY_FILE: u8 = 0;
295pub const FS_ENTRY_DIR: u8 = 1;
296pub const FS_ENTRY_SYMLINK: u8 = 2;
297pub const FS_ENTRY_OTHER: u8 = 3;
298pub const FS_ENTRY_UNREADABLE: u8 = 1 << 2;
300pub const FS_ENTRY_NO_CONTENT: u8 = 1 << 3;
302pub const FS_ENTRY_UNSTABLE: u8 = 1 << 4;
305pub const FS_ENTRY_LINK_DIR: u8 = 1 << 5;
310pub const FS_ENTRY_FILTERED: u8 = 1 << 6;
324
325pub const FS_CONTENT_NONE: u8 = 0;
327pub const FS_CONTENT_FULL: u8 = 1;
328pub const FS_CONTENT_DELTA: u8 = 2;
329
330#[derive(Clone, Debug, PartialEq, Eq)]
332pub enum FsRecord<'a> {
333 Upsert {
334 path: &'a str,
335 entry_flags: u8,
336 size: u64,
337 mtime_ns: u64,
338 mode: u32,
339 hash: u128,
341 content: FsContent<'a>,
342 },
343 Delete { path: &'a str },
345 Move { from: &'a str, to: &'a str },
347}
348
349#[derive(Clone, Debug, PartialEq, Eq)]
350pub enum FsContent<'a> {
351 None,
352 Full(&'a [u8]),
353 Delta(&'a [u8]),
356}
357
358pub fn append_fs_record(buf: &mut Vec<u8>, record: &FsRecord<'_>) {
360 let start = buf.len();
361 buf.extend_from_slice(&0u32.to_le_bytes()); match record {
363 FsRecord::Upsert {
364 path,
365 entry_flags,
366 size,
367 mtime_ns,
368 mode,
369 hash,
370 content,
371 } => {
372 buf.push(FS_RECORD_UPSERT);
373 buf.push(*entry_flags);
374 let pb = path.as_bytes();
375 buf.extend_from_slice(&(pb.len() as u16).to_le_bytes());
376 buf.extend_from_slice(pb);
377 buf.extend_from_slice(&size.to_le_bytes());
378 buf.extend_from_slice(&mtime_ns.to_le_bytes());
379 buf.extend_from_slice(&mode.to_le_bytes());
380 buf.extend_from_slice(&hash.to_le_bytes());
381 match content {
382 FsContent::None => buf.push(FS_CONTENT_NONE),
383 FsContent::Full(data) => {
384 buf.push(FS_CONTENT_FULL);
385 buf.extend_from_slice(&(data.len() as u32).to_le_bytes());
386 buf.extend_from_slice(data);
387 }
388 FsContent::Delta(ops) => {
389 buf.push(FS_CONTENT_DELTA);
390 buf.extend_from_slice(&(ops.len() as u32).to_le_bytes());
391 buf.extend_from_slice(ops);
392 }
393 }
394 }
395 FsRecord::Delete { path } => {
396 buf.push(FS_RECORD_DELETE);
397 let pb = path.as_bytes();
398 buf.extend_from_slice(&(pb.len() as u16).to_le_bytes());
399 buf.extend_from_slice(pb);
400 }
401 FsRecord::Move { from, to } => {
402 buf.push(FS_RECORD_MOVE);
403 let fb = from.as_bytes();
404 buf.extend_from_slice(&(fb.len() as u16).to_le_bytes());
405 buf.extend_from_slice(fb);
406 let tb = to.as_bytes();
407 buf.extend_from_slice(&(tb.len() as u16).to_le_bytes());
408 buf.extend_from_slice(tb);
409 }
410 }
411 let len = (buf.len() - start - 4) as u32;
412 buf[start..start + 4].copy_from_slice(&len.to_le_bytes());
413}
414
415pub struct FsRecordIter<'a> {
420 data: &'a [u8],
421}
422
423pub fn fs_records(data: &[u8]) -> FsRecordIter<'_> {
424 FsRecordIter { data }
425}
426
427fn take_path<'a>(body: &mut &'a [u8]) -> Option<&'a str> {
428 if body.len() < 2 {
429 return None;
430 }
431 let len = u16::from_le_bytes([body[0], body[1]]) as usize;
432 if body.len() < 2 + len {
433 return None;
434 }
435 let s = std::str::from_utf8(&body[2..2 + len]).ok()?;
436 *body = &body[2 + len..];
437 Some(s)
438}
439
440impl<'a> Iterator for FsRecordIter<'a> {
441 type Item = FsRecord<'a>;
442
443 fn next(&mut self) -> Option<FsRecord<'a>> {
444 loop {
445 if self.data.len() < 4 {
446 return None;
447 }
448 let rec_len =
449 u32::from_le_bytes([self.data[0], self.data[1], self.data[2], self.data[3]])
450 as usize;
451 if self.data.len() < 4 + rec_len || rec_len == 0 {
452 return None;
453 }
454 let mut body = &self.data[4..4 + rec_len];
455 self.data = &self.data[4 + rec_len..];
456 let kind = body[0];
457 body = &body[1..];
458 match kind {
459 FS_RECORD_UPSERT => {
460 if body.is_empty() {
461 return None;
462 }
463 let entry_flags = body[0];
464 body = &body[1..];
465 let path = take_path(&mut body)?;
466 if body.len() < 8 + 8 + 4 + 16 + 1 {
467 return None;
468 }
469 let size = u64::from_le_bytes(body[0..8].try_into().unwrap());
470 let mtime_ns = u64::from_le_bytes(body[8..16].try_into().unwrap());
471 let mode = u32::from_le_bytes(body[16..20].try_into().unwrap());
472 let hash = u128::from_le_bytes(body[20..36].try_into().unwrap());
473 let content_kind = body[36];
474 body = &body[37..];
475 let content = match content_kind {
476 FS_CONTENT_NONE => FsContent::None,
477 FS_CONTENT_FULL | FS_CONTENT_DELTA => {
478 if body.len() < 4 {
479 return None;
480 }
481 let len = u32::from_le_bytes(body[0..4].try_into().unwrap()) as usize;
482 if body.len() < 4 + len {
483 return None;
484 }
485 let data = &body[4..4 + len];
486 if content_kind == FS_CONTENT_FULL {
487 FsContent::Full(data)
488 } else {
489 FsContent::Delta(data)
490 }
491 }
492 _ => return None,
493 };
494 return Some(FsRecord::Upsert {
495 path,
496 entry_flags,
497 size,
498 mtime_ns,
499 mode,
500 hash,
501 content,
502 });
503 }
504 FS_RECORD_DELETE => {
505 let path = take_path(&mut body)?;
506 return Some(FsRecord::Delete { path });
507 }
508 FS_RECORD_MOVE => {
509 let from = take_path(&mut body)?;
510 let to = take_path(&mut body)?;
511 return Some(FsRecord::Move { from, to });
512 }
513 _ => continue, }
515 }
516 }
517}
518
519pub fn msg_fs_sync(
524 nonce: u16,
525 flags: u16,
526 latency_ms: u16,
527 inline_max: u32,
528 path: &str,
529) -> Vec<u8> {
530 msg_fs_sync_full(nonce, flags, latency_ms, inline_max, path, "", None)
531}
532
533pub fn msg_fs_sync_excluding(
539 nonce: u16,
540 flags: u16,
541 latency_ms: u16,
542 inline_max: u32,
543 path: &str,
544 exclude: &str,
545) -> Vec<u8> {
546 msg_fs_sync_full(nonce, flags, latency_ms, inline_max, path, exclude, None)
547}
548
549pub fn msg_fs_sync_from_pty(
554 nonce: u16,
555 flags: u16,
556 latency_ms: u16,
557 inline_max: u32,
558 path: &str,
559 src_pty_id: u16,
560) -> Vec<u8> {
561 msg_fs_sync_full(
562 nonce,
563 flags,
564 latency_ms,
565 inline_max,
566 path,
567 "",
568 Some(src_pty_id),
569 )
570}
571
572pub fn msg_fs_sync_full(
576 nonce: u16,
577 flags: u16,
578 latency_ms: u16,
579 inline_max: u32,
580 path: &str,
581 exclude: &str,
582 src_pty_id: Option<u16>,
583) -> Vec<u8> {
584 let pb = path.as_bytes();
585 let eb = exclude.as_bytes();
586 let mut flags = flags;
587 if eb.is_empty() {
588 flags &= !FS_SYNC_EXCLUDE;
589 } else {
590 flags |= FS_SYNC_EXCLUDE;
591 }
592 if src_pty_id.is_some() {
593 flags |= FS_SYNC_FROM_PTY;
594 }
595 let mut msg = Vec::with_capacity(FS_SYNC_HEADER + pb.len() + eb.len() + 4);
596 msg.push(C2S_FS_SYNC);
597 msg.extend_from_slice(&nonce.to_le_bytes());
598 msg.extend_from_slice(&flags.to_le_bytes());
599 msg.extend_from_slice(&latency_ms.to_le_bytes());
600 msg.extend_from_slice(&inline_max.to_le_bytes());
601 msg.extend_from_slice(&(pb.len() as u16).to_le_bytes());
602 msg.extend_from_slice(pb);
603 if !eb.is_empty() {
604 msg.extend_from_slice(&(eb.len() as u16).to_le_bytes());
605 msg.extend_from_slice(eb);
606 }
607 if let Some(src) = src_pty_id {
608 msg.extend_from_slice(&src.to_le_bytes());
609 }
610 msg
611}
612
613pub const FS_SYNC_HEADER: usize = 13;
615
616pub fn fs_sync_flags(msg: &[u8]) -> Option<u16> {
618 if msg.first().copied() != Some(C2S_FS_SYNC) || msg.len() < FS_SYNC_HEADER {
619 return None;
620 }
621 Some(u16::from_le_bytes([msg[3], msg[4]]))
622}
623
624fn fs_sync_trailer_start(msg: &[u8]) -> Option<usize> {
626 if msg.first().copied() != Some(C2S_FS_SYNC) || msg.len() < FS_SYNC_HEADER {
627 return None;
628 }
629 let path_len = u16::from_le_bytes([msg[11], msg[12]]) as usize;
630 let end = FS_SYNC_HEADER.checked_add(path_len)?;
631 (end <= msg.len()).then_some(end)
632}
633
634fn fs_sync_exclude_span(msg: &[u8]) -> Option<(std::ops::Range<usize>, usize)> {
638 let off = fs_sync_trailer_start(msg)?;
639 if fs_sync_flags(msg)? & FS_SYNC_EXCLUDE == 0 {
640 return Some((off..off, off));
641 }
642 let len_bytes = msg.get(off..off + 2)?;
643 let len = u16::from_le_bytes([len_bytes[0], len_bytes[1]]) as usize;
644 let start = off + 2;
645 let end = start.checked_add(len)?;
646 (end <= msg.len()).then_some((start..end, end))
647}
648
649pub fn fs_sync_exclude(msg: &[u8]) -> Option<&str> {
652 let (span, _) = fs_sync_exclude_span(msg)?;
653 std::str::from_utf8(&msg[span]).ok()
654}
655
656pub fn fs_sync_src_pty(msg: &[u8]) -> Option<u16> {
659 if fs_sync_flags(msg)? & FS_SYNC_FROM_PTY == 0 {
660 return None;
661 }
662 let (_, off) = fs_sync_exclude_span(msg)?;
663 let b = msg.get(off..off + 2)?;
664 Some(u16::from_le_bytes([b[0], b[1]]))
665}
666
667pub fn fs_sync_rebase(msg: &[u8], cwd: Option<&str>) -> Option<Vec<u8>> {
673 fs_sync_src_pty(msg)?;
674 let nonce = u16::from_le_bytes([msg[1], msg[2]]);
675 let flags = fs_sync_flags(msg)? & !FS_SYNC_FROM_PTY;
676 let latency_ms = u16::from_le_bytes([msg[5], msg[6]]);
677 let inline_max = u32::from_le_bytes([msg[7], msg[8], msg[9], msg[10]]);
678 let path_len = u16::from_le_bytes([msg[11], msg[12]]) as usize;
679 let path = std::str::from_utf8(msg.get(FS_SYNC_HEADER..FS_SYNC_HEADER + path_len)?).ok()?;
680 let exclude = fs_sync_exclude(msg)?;
681 let joined = cwd.map(|dir| {
682 std::path::Path::new(dir)
683 .join(path)
684 .to_string_lossy()
685 .into_owned()
686 });
687 let eff = joined.as_deref().unwrap_or(path);
688 Some(msg_fs_sync_full(
689 nonce, flags, latency_ms, inline_max, eff, exclude, None,
690 ))
691}
692
693pub fn msg_fs_stop(sync_id: u16) -> Vec<u8> {
694 let mut msg = Vec::with_capacity(3);
695 msg.push(C2S_FS_STOP);
696 msg.extend_from_slice(&sync_id.to_le_bytes());
697 msg
698}
699
700pub fn msg_fs_ack(sync_id: u16, update_id: u32) -> Vec<u8> {
701 let mut msg = Vec::with_capacity(7);
702 msg.push(C2S_FS_ACK);
703 msg.extend_from_slice(&sync_id.to_le_bytes());
704 msg.extend_from_slice(&update_id.to_le_bytes());
705 msg
706}
707
708pub fn msg_fs_fetch(nonce: u16, sync_id: u16, path: &str) -> Vec<u8> {
709 let pb = path.as_bytes();
710 let mut msg = Vec::with_capacity(7 + pb.len());
711 msg.push(C2S_FS_FETCH);
712 msg.extend_from_slice(&nonce.to_le_bytes());
713 msg.extend_from_slice(&sync_id.to_le_bytes());
714 msg.extend_from_slice(&(pb.len() as u16).to_le_bytes());
715 msg.extend_from_slice(pb);
716 msg
717}
718
719pub fn msg_fs_search(nonce: u16, limit: u16, root: &str, query: &str) -> Vec<u8> {
721 let rb = root.as_bytes();
722 let qb = query.as_bytes();
723 let mut m = Vec::with_capacity(9 + rb.len() + qb.len());
724 m.push(C2S_FS_SEARCH);
725 m.extend_from_slice(&nonce.to_le_bytes());
726 m.extend_from_slice(&limit.to_le_bytes());
727 m.extend_from_slice(&(rb.len() as u16).to_le_bytes());
728 m.extend_from_slice(rb);
729 m.extend_from_slice(&(qb.len() as u16).to_le_bytes());
730 m.extend_from_slice(qb);
731 m
732}
733
734pub fn parse_fs_search(data: &[u8]) -> Option<(u16, u16, String, String)> {
736 if data.first().copied() != Some(C2S_FS_SEARCH) || data.len() < 9 {
737 return None;
738 }
739 let nonce = u16::from_le_bytes([data[1], data[2]]);
740 let limit = u16::from_le_bytes([data[3], data[4]]);
741 let rl = u16::from_le_bytes([data[5], data[6]]) as usize;
742 let ro = 7;
743 if data.len() < ro + rl + 2 {
744 return None;
745 }
746 let root = String::from_utf8_lossy(&data[ro..ro + rl]).into_owned();
747 let qo = ro + rl;
748 let ql = u16::from_le_bytes([data[qo], data[qo + 1]]) as usize;
749 let qs = qo + 2;
750 if data.len() < qs + ql {
751 return None;
752 }
753 let query = String::from_utf8_lossy(&data[qs..qs + ql]).into_owned();
754 Some((nonce, limit, root, query))
755}
756
757pub fn msg_fs_search_result(nonce: u16, status: u8, paths: &[String]) -> Vec<u8> {
759 let mut m = Vec::with_capacity(6 + paths.iter().map(|p| 2 + p.len()).sum::<usize>());
760 m.push(S2C_FS_SEARCH);
761 m.extend_from_slice(&nonce.to_le_bytes());
762 m.push(status);
763 m.extend_from_slice(&(paths.len() as u16).to_le_bytes());
764 for p in paths {
765 let pb = p.as_bytes();
766 m.extend_from_slice(&(pb.len() as u16).to_le_bytes());
767 m.extend_from_slice(pb);
768 }
769 m
770}
771
772pub fn parse_fs_search_result(data: &[u8]) -> Option<(u16, u8, Vec<String>)> {
774 if data.first().copied() != Some(S2C_FS_SEARCH) || data.len() < 6 {
775 return None;
776 }
777 let nonce = u16::from_le_bytes([data[1], data[2]]);
778 let status = data[3];
779 let count = u16::from_le_bytes([data[4], data[5]]) as usize;
780 let mut paths = Vec::with_capacity(count);
781 let mut off = 6;
782 for _ in 0..count {
783 if off + 2 > data.len() {
784 return None;
785 }
786 let pl = u16::from_le_bytes([data[off], data[off + 1]]) as usize;
787 off += 2;
788 if off + pl > data.len() {
789 return None;
790 }
791 paths.push(String::from_utf8_lossy(&data[off..off + pl]).into_owned());
792 off += pl;
793 }
794 Some((nonce, status, paths))
795}
796
797pub fn msg_fs_index(nonce: u16, root: &str) -> Vec<u8> {
799 let rb = root.as_bytes();
800 let mut m = Vec::with_capacity(6 + rb.len());
801 m.push(C2S_FS_INDEX);
802 m.extend_from_slice(&nonce.to_le_bytes());
803 m.push(0); m.extend_from_slice(&(rb.len() as u16).to_le_bytes());
805 m.extend_from_slice(rb);
806 m
807}
808
809pub fn parse_fs_index(data: &[u8]) -> Option<(u16, u8, String)> {
811 if data.first().copied() != Some(C2S_FS_INDEX) || data.len() < 6 {
813 return None;
814 }
815 let nonce = u16::from_le_bytes([data[1], data[2]]);
816 let flags = data[3];
817 let rl = u16::from_le_bytes([data[4], data[5]]) as usize;
818 if data.len() < 6 + rl {
819 return None;
820 }
821 let root = String::from_utf8_lossy(&data[6..6 + rl]).into_owned();
822 Some((nonce, flags, root))
823}
824
825pub fn msg_fs_index_result(nonce: u16, status: u8, flags: u8, paths: &[String]) -> Vec<u8> {
829 let mut raw = Vec::with_capacity(paths.iter().map(|p| 2 + p.len()).sum::<usize>());
830 for p in paths {
831 let pb = p.as_bytes();
832 raw.extend_from_slice(&(pb.len() as u16).to_le_bytes());
833 raw.extend_from_slice(pb);
834 }
835 let compressed = lz4_flex::compress_prepend_size(&raw);
836 let mut m = Vec::with_capacity(9 + compressed.len());
837 m.push(S2C_FS_INDEX);
838 m.extend_from_slice(&nonce.to_le_bytes());
839 m.push(status);
840 m.push(flags);
841 m.extend_from_slice(&(paths.len() as u32).to_le_bytes());
842 m.extend_from_slice(&compressed);
843 m
844}
845
846pub fn parse_fs_index_result(data: &[u8]) -> Option<(u16, u8, u8, Vec<String>)> {
850 if data.first().copied() != Some(S2C_FS_INDEX) || data.len() < 9 {
852 return None;
853 }
854 let nonce = u16::from_le_bytes([data[1], data[2]]);
855 let status = data[3];
856 let flags = data[4];
857 let count = u32::from_le_bytes([data[5], data[6], data[7], data[8]]) as usize;
858 if count > FS_INDEX_MAX_COUNT {
859 return None;
860 }
861 let raw = decompress_guarded(&data[9..])?;
862 if count > raw.len() / 2 + 1 {
864 return None;
865 }
866 let mut paths = Vec::with_capacity(count);
867 let mut off = 0;
868 while off < raw.len() {
869 if off + 2 > raw.len() {
870 return None;
871 }
872 let pl = u16::from_le_bytes([raw[off], raw[off + 1]]) as usize;
873 off += 2;
874 if off + pl > raw.len() {
875 return None;
876 }
877 paths.push(String::from_utf8_lossy(&raw[off..off + pl]).into_owned());
878 off += pl;
879 }
880 if paths.len() != count {
881 return None;
882 }
883 Some((nonce, status, flags, paths))
884}
885
886pub fn msg_fs_synced(nonce: u16, sync_id: u16, status: u8, detail: &str) -> Vec<u8> {
887 let db = detail.as_bytes();
888 let mut msg = Vec::with_capacity(8 + db.len());
889 msg.push(S2C_FS_SYNCED);
890 msg.extend_from_slice(&nonce.to_le_bytes());
891 msg.extend_from_slice(&sync_id.to_le_bytes());
892 msg.push(status);
893 msg.extend_from_slice(&(db.len() as u16).to_le_bytes());
894 msg.extend_from_slice(db);
895 msg
896}
897
898pub fn msg_fs_update(sync_id: u16, update_id: u32, flags: u8, records: &[u8]) -> Vec<u8> {
900 let compressed = lz4_flex::compress_prepend_size(records);
901 let mut msg = Vec::with_capacity(8 + compressed.len());
902 msg.push(S2C_FS_UPDATE);
903 msg.extend_from_slice(&sync_id.to_le_bytes());
904 msg.extend_from_slice(&update_id.to_le_bytes());
905 msg.push(flags);
906 msg.extend_from_slice(&compressed);
907 msg
908}
909
910pub fn msg_fs_file(nonce: u16, status: u8, data: &[u8]) -> Vec<u8> {
911 let compressed = lz4_flex::compress_prepend_size(data);
912 let mut msg = Vec::with_capacity(4 + compressed.len());
913 msg.push(S2C_FS_FILE);
914 msg.extend_from_slice(&nonce.to_le_bytes());
915 msg.push(status);
916 msg.extend_from_slice(&compressed);
917 msg
918}
919
920pub fn msg_fs_closed(sync_id: u16, reason: u8) -> Vec<u8> {
921 let mut msg = Vec::with_capacity(4);
922 msg.push(S2C_FS_CLOSED);
923 msg.extend_from_slice(&sync_id.to_le_bytes());
924 msg.push(reason);
925 msg
926}
927
928#[derive(Clone, Debug, PartialEq, Eq)]
934pub struct FsNode {
935 pub entry_flags: u8,
936 pub size: u64,
937 pub mtime_ns: u64,
938 pub mode: u32,
939 pub hash: u128,
940 pub content: Option<Vec<u8>>,
943}
944
945pub const FS_MAX_DECOMPRESSED: usize = crate::MAX_DECOMPRESSED;
952
953fn decompress_guarded(data: &[u8]) -> Option<Vec<u8>> {
956 if data.len() < 4 {
957 return None;
958 }
959 let declared = u32::from_le_bytes(data[0..4].try_into().unwrap()) as usize;
960 if declared > FS_MAX_DECOMPRESSED {
961 return None;
962 }
963 lz4_flex::decompress_size_prepended(data).ok()
964}
965
966pub fn fs_update_records(msg: &[u8]) -> Option<Vec<u8>> {
969 if msg.len() < 8 || msg[0] != S2C_FS_UPDATE {
970 return None;
971 }
972 decompress_guarded(&msg[8..])
973}
974
975pub fn parse_fs_file(msg: &[u8]) -> Option<(u16, u8, Vec<u8>)> {
979 if msg.len() < 4 || msg[0] != S2C_FS_FILE {
980 return None;
981 }
982 let nonce = u16::from_le_bytes([msg[1], msg[2]]);
983 let status = msg[3];
984 let data = decompress_guarded(&msg[4..])?;
985 Some((nonce, status, data))
986}
987
988#[derive(Clone, Debug, PartialEq, Eq)]
999pub struct FsWrite {
1000 pub nonce: u16,
1001 pub sync_id: u16,
1002 pub flags: u8,
1003 pub base: u128,
1004 pub mode: u32,
1005 pub content_kind: u8,
1006 pub path: String,
1007 pub content: Vec<u8>,
1008}
1009
1010pub fn msg_fs_write(w: &FsWrite) -> Vec<u8> {
1011 let pb = w.path.as_bytes();
1012 let compressed = lz4_flex::compress_prepend_size(&w.content);
1013 let mut msg = Vec::with_capacity(29 + pb.len() + compressed.len());
1014 msg.push(C2S_FS_WRITE);
1015 msg.extend_from_slice(&w.nonce.to_le_bytes());
1016 msg.extend_from_slice(&w.sync_id.to_le_bytes());
1017 msg.push(w.flags);
1018 msg.extend_from_slice(&w.base.to_le_bytes());
1019 msg.extend_from_slice(&w.mode.to_le_bytes());
1020 msg.push(w.content_kind);
1021 msg.extend_from_slice(&(pb.len() as u16).to_le_bytes());
1022 msg.extend_from_slice(pb);
1023 msg.extend_from_slice(&compressed);
1024 msg
1025}
1026
1027pub fn parse_fs_write(msg: &[u8]) -> Option<FsWrite> {
1030 if msg.len() < 29 || msg[0] != C2S_FS_WRITE {
1032 return None;
1033 }
1034 let nonce = u16::from_le_bytes([msg[1], msg[2]]);
1035 let sync_id = u16::from_le_bytes([msg[3], msg[4]]);
1036 let flags = msg[5];
1037 let base = u128::from_le_bytes(msg[6..22].try_into().unwrap());
1038 let mode = u32::from_le_bytes(msg[22..26].try_into().unwrap());
1039 let content_kind = msg[26];
1040 let path_len = u16::from_le_bytes([msg[27], msg[28]]) as usize;
1041 let path = std::str::from_utf8(msg.get(29..29 + path_len)?)
1042 .ok()?
1043 .to_string();
1044 let content = decompress_guarded(&msg[29 + path_len..])?;
1045 Some(FsWrite {
1046 nonce,
1047 sync_id,
1048 flags,
1049 base,
1050 mode,
1051 content_kind,
1052 path,
1053 content,
1054 })
1055}
1056
1057#[derive(Clone, Debug, PartialEq, Eq)]
1061pub struct FsOp {
1062 pub nonce: u16,
1063 pub sync_id: u16,
1064 pub op: u8,
1065 pub flags: u8,
1066 pub base: u128,
1067 pub mode: u32,
1068 pub a: String,
1069 pub b: String,
1070}
1071
1072pub fn msg_fs_op(o: &FsOp) -> Vec<u8> {
1073 let ab = o.a.as_bytes();
1074 let bb = o.b.as_bytes();
1075 let mut msg = Vec::with_capacity(29 + ab.len() + bb.len());
1076 msg.push(C2S_FS_OP);
1077 msg.extend_from_slice(&o.nonce.to_le_bytes());
1078 msg.extend_from_slice(&o.sync_id.to_le_bytes());
1079 msg.push(o.op);
1080 msg.push(o.flags);
1081 msg.extend_from_slice(&o.base.to_le_bytes());
1082 msg.extend_from_slice(&o.mode.to_le_bytes());
1083 msg.extend_from_slice(&(ab.len() as u16).to_le_bytes());
1084 msg.extend_from_slice(ab);
1085 msg.extend_from_slice(&(bb.len() as u16).to_le_bytes());
1086 msg.extend_from_slice(bb);
1087 msg
1088}
1089
1090pub fn parse_fs_op(msg: &[u8]) -> Option<FsOp> {
1092 if msg.len() < 29 || msg[0] != C2S_FS_OP {
1094 return None;
1095 }
1096 let nonce = u16::from_le_bytes([msg[1], msg[2]]);
1097 let sync_id = u16::from_le_bytes([msg[3], msg[4]]);
1098 let op = msg[5];
1099 let flags = msg[6];
1100 let base = u128::from_le_bytes(msg[7..23].try_into().unwrap());
1101 let mode = u32::from_le_bytes(msg[23..27].try_into().unwrap());
1102 let a_len = u16::from_le_bytes([msg[27], msg[28]]) as usize;
1103 let a = std::str::from_utf8(msg.get(29..29 + a_len)?)
1104 .ok()?
1105 .to_string();
1106 let b_off = 29 + a_len;
1107 let b_len = u16::from_le_bytes([*msg.get(b_off)?, *msg.get(b_off + 1)?]) as usize;
1108 let b = std::str::from_utf8(msg.get(b_off + 2..b_off + 2 + b_len)?)
1109 .ok()?
1110 .to_string();
1111 Some(FsOp {
1112 nonce,
1113 sync_id,
1114 op,
1115 flags,
1116 base,
1117 mode,
1118 a,
1119 b,
1120 })
1121}
1122
1123pub fn msg_fs_done(nonce: u16, status: u8, hash: u128, mtime_ns: u64) -> Vec<u8> {
1126 let mut msg = Vec::with_capacity(28);
1127 msg.push(S2C_FS_DONE);
1128 msg.extend_from_slice(&nonce.to_le_bytes());
1129 msg.push(status);
1130 msg.extend_from_slice(&hash.to_le_bytes());
1131 msg.extend_from_slice(&mtime_ns.to_le_bytes());
1132 msg
1133}
1134
1135pub fn parse_fs_done(msg: &[u8]) -> Option<(u16, u8, u128, u64)> {
1137 if msg.len() < 28 || msg[0] != S2C_FS_DONE {
1139 return None;
1140 }
1141 let nonce = u16::from_le_bytes([msg[1], msg[2]]);
1142 let status = msg[3];
1143 let hash = u128::from_le_bytes(msg[4..20].try_into().unwrap());
1144 let mtime_ns = u64::from_le_bytes(msg[20..28].try_into().unwrap());
1145 Some((nonce, status, hash, mtime_ns))
1146}
1147
1148#[derive(Debug, Default)]
1152pub struct FsMirror {
1153 pub live: BTreeMap<String, FsNode>,
1154 staging: Option<BTreeMap<String, FsNode>>,
1155}
1156
1157impl FsMirror {
1158 pub fn new() -> Self {
1159 Self::default()
1160 }
1161
1162 pub fn apply_update(&mut self, msg: &[u8]) -> Option<u32> {
1165 if msg.len() < 8 || msg[0] != S2C_FS_UPDATE {
1166 return None;
1167 }
1168 let update_id = u32::from_le_bytes([msg[3], msg[4], msg[5], msg[6]]);
1169 let flags = msg[7];
1170 let records = decompress_guarded(&msg[8..])?;
1171 if flags & FS_UPDATE_RESET != 0 {
1172 self.staging = Some(BTreeMap::new());
1173 }
1174 let map = self.staging.as_mut().unwrap_or(&mut self.live);
1175 for record in fs_records(&records) {
1176 match record {
1177 FsRecord::Upsert {
1178 path,
1179 entry_flags,
1180 size,
1181 mtime_ns,
1182 mode,
1183 hash,
1184 content,
1185 } => {
1186 let content = match content {
1187 FsContent::None => {
1188 let entry_type = entry_flags & FS_ENTRY_TYPE_MASK;
1189 let content_bearing =
1190 entry_type == FS_ENTRY_FILE || entry_type == FS_ENTRY_SYMLINK;
1191 if !content_bearing
1192 || entry_flags
1193 & (FS_ENTRY_NO_CONTENT
1194 | FS_ENTRY_UNREADABLE
1195 | FS_ENTRY_UNSTABLE)
1196 != 0
1197 {
1198 None
1199 } else {
1200 map.remove(path)
1205 .filter(|n| n.entry_flags & FS_ENTRY_TYPE_MASK == entry_type)
1206 .and_then(|n| n.content)
1207 }
1208 }
1209 FsContent::Full(data) => Some(data.to_vec()),
1210 FsContent::Delta(ops) => {
1211 let base = map
1212 .get(path)
1213 .and_then(|n| n.content.as_deref())
1214 .unwrap_or(&[]);
1215 Some(apply_fs_delta(base, ops)?)
1216 }
1217 };
1218 map.insert(
1219 path.to_string(),
1220 FsNode {
1221 entry_flags,
1222 size,
1223 mtime_ns,
1224 mode,
1225 hash,
1226 content,
1227 },
1228 );
1229 }
1230 FsRecord::Delete { path } => {
1231 remove_subtree(map, path);
1232 }
1233 FsRecord::Move { from, to } => {
1234 let moved = take_subtree(map, from);
1235 for (suffix, node) in moved {
1236 let new_path = join_moved(to, &suffix);
1237 map.insert(new_path, node);
1238 }
1239 }
1240 }
1241 }
1242 if flags & FS_UPDATE_SYNC != 0
1243 && let Some(staged) = self.staging.take()
1244 {
1245 self.live = staged;
1246 }
1247 Some(update_id)
1248 }
1249}
1250
1251fn subtree_keys(map: &BTreeMap<String, FsNode>, root: &str) -> Vec<String> {
1255 if root.is_empty() {
1256 return map.keys().cloned().collect();
1257 }
1258 let mut keys: Vec<String> = Vec::new();
1259 if map.contains_key(root) {
1260 keys.push(root.to_string());
1261 }
1262 let prefix = format!("{root}/");
1263 keys.extend(
1264 map.range(prefix.clone()..)
1265 .take_while(|(k, _)| k.starts_with(&prefix))
1266 .map(|(k, _)| k.clone()),
1267 );
1268 keys
1269}
1270
1271fn remove_subtree(map: &mut BTreeMap<String, FsNode>, root: &str) {
1272 for key in subtree_keys(map, root) {
1273 map.remove(&key);
1274 }
1275}
1276
1277fn take_subtree(map: &mut BTreeMap<String, FsNode>, root: &str) -> Vec<(String, FsNode)> {
1280 subtree_keys(map, root)
1281 .into_iter()
1282 .map(|key| {
1283 let node = map.remove(&key).unwrap();
1284 let suffix = if key.len() > root.len() {
1285 key[root.len() + if root.is_empty() { 0 } else { 1 }..].to_string()
1286 } else {
1287 String::new()
1288 };
1289 (suffix, node)
1290 })
1291 .collect()
1292}
1293
1294fn join_moved(to: &str, suffix: &str) -> String {
1295 if suffix.is_empty() {
1296 to.to_string()
1297 } else if to.is_empty() {
1298 suffix.to_string()
1299 } else {
1300 format!("{to}/{suffix}")
1301 }
1302}
1303
1304pub fn apply_fs_delta(base: &[u8], mut ops: &[u8]) -> Option<Vec<u8>> {
1306 fn leb128(data: &mut &[u8]) -> Option<u64> {
1307 let mut value = 0u64;
1308 let mut shift = 0u32;
1309 loop {
1310 let (&byte, rest) = data.split_first()?;
1311 *data = rest;
1312 if shift >= 64 {
1313 return None;
1314 }
1315 value |= u64::from(byte & 0x7F) << shift;
1316 if byte & 0x80 == 0 {
1317 return Some(value);
1318 }
1319 shift += 7;
1320 }
1321 }
1322 let mut out = Vec::new();
1323 while let Some((&op, rest)) = ops.split_first() {
1324 ops = rest;
1325 match op {
1326 0x01 => {
1327 let offset = leb128(&mut ops)? as usize;
1328 let len = leb128(&mut ops)? as usize;
1329 if out.len().checked_add(len)? > FS_MAX_DECOMPRESSED {
1330 return None;
1331 }
1332 out.extend_from_slice(base.get(offset..offset.checked_add(len)?)?);
1333 }
1334 0x02 => {
1335 let len = leb128(&mut ops)? as usize;
1336 if ops.len() < len {
1337 return None;
1338 }
1339 if out.len().checked_add(len)? > FS_MAX_DECOMPRESSED {
1340 return None;
1341 }
1342 out.extend_from_slice(&ops[..len]);
1343 ops = &ops[len..];
1344 }
1345 _ => return None,
1346 }
1347 }
1348 Some(out)
1349}
1350
1351#[derive(Debug, Clone, PartialEq, Eq)]
1355pub enum FsGrepRecord {
1356 File {
1359 flags: u8,
1360 n: u16,
1361 path: String,
1363 },
1364 Match {
1370 line: u32,
1371 col: u32,
1372 end_line: u32,
1373 end_col: u32,
1374 text: String,
1376 },
1377}
1378
1379pub fn msg_fs_grep(
1381 nonce: u16,
1382 flags: u8,
1383 max_matches: u16,
1384 max_per_file: u16,
1385 root: &str,
1386 query: &str,
1387) -> Vec<u8> {
1388 let rb = root.as_bytes();
1389 let qb = query.as_bytes();
1390 let mut m = Vec::with_capacity(12 + rb.len() + qb.len());
1391 m.push(C2S_FS_GREP);
1392 m.extend_from_slice(&nonce.to_le_bytes());
1393 m.push(flags);
1394 m.extend_from_slice(&max_matches.to_le_bytes());
1395 m.extend_from_slice(&max_per_file.to_le_bytes());
1396 m.extend_from_slice(&(rb.len() as u16).to_le_bytes());
1397 m.extend_from_slice(rb);
1398 m.extend_from_slice(&(qb.len() as u16).to_le_bytes());
1399 m.extend_from_slice(qb);
1400 m
1401}
1402
1403pub fn parse_fs_grep(data: &[u8]) -> Option<(u16, u8, u16, u16, String, String)> {
1405 if data.first().copied() != Some(C2S_FS_GREP) || data.len() < 12 {
1406 return None;
1407 }
1408 let nonce = u16::from_le_bytes([data[1], data[2]]);
1409 let flags = data[3];
1410 let max_matches = u16::from_le_bytes([data[4], data[5]]);
1411 let max_per_file = u16::from_le_bytes([data[6], data[7]]);
1412 let rl = u16::from_le_bytes([data[8], data[9]]) as usize;
1413 let ro = 10;
1414 if data.len() < ro + rl + 2 {
1415 return None;
1416 }
1417 let root = String::from_utf8_lossy(&data[ro..ro + rl]).into_owned();
1418 let qo = ro + rl;
1419 let ql = u16::from_le_bytes([data[qo], data[qo + 1]]) as usize;
1420 let qs = qo + 2;
1421 if data.len() < qs + ql {
1422 return None;
1423 }
1424 let query = String::from_utf8_lossy(&data[qs..qs + ql]).into_owned();
1425 Some((nonce, flags, max_matches, max_per_file, root, query))
1426}
1427
1428pub fn append_fs_grep_record(buf: &mut Vec<u8>, record: &FsGrepRecord) {
1430 let start = buf.len();
1431 buf.extend_from_slice(&0u32.to_le_bytes()); match record {
1433 FsGrepRecord::File { flags, n, path } => {
1434 buf.push(FS_GREP_RECORD_FILE);
1435 buf.push(*flags);
1436 buf.extend_from_slice(&n.to_le_bytes());
1437 let pb = path.as_bytes();
1438 buf.extend_from_slice(&(pb.len() as u16).to_le_bytes());
1439 buf.extend_from_slice(pb);
1440 }
1441 FsGrepRecord::Match {
1442 line,
1443 col,
1444 end_line,
1445 end_col,
1446 text,
1447 } => {
1448 buf.push(FS_GREP_RECORD_MATCH);
1449 buf.extend_from_slice(&line.to_le_bytes());
1450 buf.extend_from_slice(&col.to_le_bytes());
1451 buf.extend_from_slice(&end_line.to_le_bytes());
1452 buf.extend_from_slice(&end_col.to_le_bytes());
1453 let tb = text.as_bytes();
1454 buf.extend_from_slice(&(tb.len() as u32).to_le_bytes());
1455 buf.extend_from_slice(tb);
1456 }
1457 }
1458 let len = (buf.len() - start - 4) as u32;
1459 buf[start..start + 4].copy_from_slice(&len.to_le_bytes());
1460}
1461
1462pub fn fs_grep_records(data: &[u8]) -> Vec<FsGrepRecord> {
1466 let mut out = Vec::new();
1467 let mut off = 0usize;
1468 while off + 4 <= data.len() {
1469 let len = u32::from_le_bytes(data[off..off + 4].try_into().unwrap()) as usize;
1470 if len == 0 || off + 4 + len > data.len() {
1471 return out;
1472 }
1473 let body = &data[off + 4..off + 4 + len];
1474 off += 4 + len;
1475 match body[0] {
1476 FS_GREP_RECORD_FILE => {
1477 if body.len() < 6 {
1478 return out;
1479 }
1480 let flags = body[1];
1481 let n = u16::from_le_bytes([body[2], body[3]]);
1482 let pl = u16::from_le_bytes([body[4], body[5]]) as usize;
1483 if body.len() < 6 + pl {
1484 return out;
1485 }
1486 out.push(FsGrepRecord::File {
1487 flags,
1488 n,
1489 path: String::from_utf8_lossy(&body[6..6 + pl]).into_owned(),
1490 });
1491 }
1492 FS_GREP_RECORD_MATCH => {
1493 if body.len() < 21 {
1494 return out;
1495 }
1496 let line = u32::from_le_bytes(body[1..5].try_into().unwrap());
1497 let col = u32::from_le_bytes(body[5..9].try_into().unwrap());
1498 let end_line = u32::from_le_bytes(body[9..13].try_into().unwrap());
1499 let end_col = u32::from_le_bytes(body[13..17].try_into().unwrap());
1500 let tl = u32::from_le_bytes(body[17..21].try_into().unwrap()) as usize;
1501 if body.len() < 21 + tl {
1502 return out;
1503 }
1504 out.push(FsGrepRecord::Match {
1505 line,
1506 col,
1507 end_line,
1508 end_col,
1509 text: String::from_utf8_lossy(&body[21..21 + tl]).into_owned(),
1510 });
1511 }
1512 _ => {}
1514 }
1515 }
1516 out
1517}
1518
1519pub fn msg_fs_grep_result(
1521 nonce: u16,
1522 status: u8,
1523 flags: u8,
1524 detail: &str,
1525 records: &[u8],
1526) -> Vec<u8> {
1527 let db = detail.as_bytes();
1528 let compressed = lz4_flex::compress_prepend_size(records);
1529 let mut m = Vec::with_capacity(7 + db.len() + compressed.len());
1530 m.push(S2C_FS_GREP);
1531 m.extend_from_slice(&nonce.to_le_bytes());
1532 m.push(status);
1533 m.push(flags);
1534 m.extend_from_slice(&(db.len() as u16).to_le_bytes());
1535 m.extend_from_slice(db);
1536 m.extend_from_slice(&compressed);
1537 m
1538}
1539
1540pub fn parse_fs_grep_result(data: &[u8]) -> Option<(u16, u8, u8, String, Vec<u8>)> {
1543 if data.first().copied() != Some(S2C_FS_GREP) || data.len() < 7 {
1544 return None;
1545 }
1546 let nonce = u16::from_le_bytes([data[1], data[2]]);
1547 let status = data[3];
1548 let flags = data[4];
1549 let dl = u16::from_le_bytes([data[5], data[6]]) as usize;
1550 let ds = 7;
1551 if data.len() < ds + dl {
1552 return None;
1553 }
1554 let detail = String::from_utf8_lossy(&data[ds..ds + dl]).into_owned();
1555 let records = decompress_guarded(&data[ds + dl..])?;
1556 Some((nonce, status, flags, detail, records))
1557}
1558
1559#[cfg(test)]
1560mod tests {
1561 #[test]
1562 fn fs_grep_request_roundtrip() {
1563 let m = msg_fs_grep(
1565 0x0102,
1566 FS_GREP_CASE_SENSITIVE | FS_GREP_REGEX,
1567 500,
1568 50,
1569 "/tmp/root",
1570 "fn \\w+",
1571 );
1572 assert_eq!(
1573 m.iter().map(|b| format!("{b:02x}")).collect::<String>(),
1574 "48020103f401320009002f746d702f726f6f740600666e205c772b"
1577 );
1578 assert_eq!(
1579 parse_fs_grep(&m),
1580 Some((
1581 0x0102,
1582 FS_GREP_CASE_SENSITIVE | FS_GREP_REGEX,
1583 500,
1584 50,
1585 "/tmp/root".to_string(),
1586 "fn \\w+".to_string()
1587 ))
1588 );
1589 for cut in 0..m.len() {
1591 assert_eq!(parse_fs_grep(&m[..cut]), None, "cut at {cut}");
1592 }
1593 }
1594
1595 #[test]
1596 fn fs_grep_result_roundtrip() {
1597 let recs = vec![
1598 FsGrepRecord::File {
1599 flags: 0,
1600 n: 2,
1601 path: "src/main.rs".to_string(),
1602 },
1603 FsGrepRecord::Match {
1604 line: 41,
1605 col: 4,
1606 end_line: 41,
1607 end_col: 6,
1608 text: " fn main() {".to_string(),
1609 },
1610 FsGrepRecord::Match {
1611 line: 99,
1612 col: 0,
1613 end_line: 99,
1614 end_col: 2,
1615 text: "fn helper()".to_string(),
1616 },
1617 FsGrepRecord::File {
1619 flags: FS_GREP_FILE_IGNORED,
1620 n: 1,
1621 path: "target/debug/build.rs".to_string(),
1622 },
1623 FsGrepRecord::Match {
1624 line: 0,
1625 col: 0,
1626 end_line: 0,
1627 end_col: 2,
1628 text: String::new(),
1629 },
1630 ];
1631 let mut buf = Vec::new();
1632 for r in &recs {
1633 append_fs_grep_record(&mut buf, r);
1634 }
1635 assert_eq!(fs_grep_records(&buf), recs);
1636
1637 let msg = msg_fs_grep_result(7, FS_DONE_OK, FS_GREP_TRUNCATED, "", &buf);
1638 let (nonce, status, flags, detail, records) = parse_fs_grep_result(&msg).unwrap();
1639 assert_eq!(
1640 (nonce, status, flags, detail.as_str()),
1641 (7, FS_DONE_OK, FS_GREP_TRUNCATED, "")
1642 );
1643 assert_eq!(fs_grep_records(&records), recs);
1644
1645 let bad = msg_fs_grep_result(8, FS_DONE_INVALID, 0, "unclosed character class", &[]);
1647 let (_, st, _, d, r) = parse_fs_grep_result(&bad).unwrap();
1648 assert_eq!(st, FS_DONE_INVALID);
1649 assert_eq!(d, "unclosed character class");
1650 assert!(fs_grep_records(&r).is_empty());
1651 }
1652
1653 #[test]
1654 fn fs_grep_records_skip_unknown_kinds() {
1655 let mut buf = Vec::new();
1656 append_fs_grep_record(
1659 &mut buf,
1660 &FsGrepRecord::File {
1661 flags: 0,
1662 n: 0,
1663 path: "a".to_string(),
1664 },
1665 );
1666 let start = buf.len();
1667 buf.extend_from_slice(&0u32.to_le_bytes());
1668 buf.push(0x7f); buf.extend_from_slice(b"whatever");
1670 let len = (buf.len() - start - 4) as u32;
1671 buf[start..start + 4].copy_from_slice(&len.to_le_bytes());
1672 append_fs_grep_record(
1673 &mut buf,
1674 &FsGrepRecord::File {
1675 flags: 0,
1676 n: 0,
1677 path: "b".to_string(),
1678 },
1679 );
1680 let got = fs_grep_records(&buf);
1681 assert_eq!(got.len(), 2, "unknown kind must not end the stream");
1682
1683 let mut trunc = buf.clone();
1685 trunc.truncate(trunc.len() - 1);
1686 assert!(fs_grep_records(&trunc).len() <= 2);
1687 }
1688
1689 use super::*;
1690
1691 #[test]
1692 fn fs_search_request_roundtrip() {
1693 let m = msg_fs_search(9, 50, "/a/b:c", "eng.rs");
1694 let (nonce, limit, root, query) = parse_fs_search(&m).unwrap();
1695 assert_eq!(nonce, 9);
1696 assert_eq!(limit, 50);
1697 assert_eq!(root, "/a/b:c");
1698 assert_eq!(query, "eng.rs");
1699 }
1700
1701 #[test]
1702 fn fs_search_result_roundtrip() {
1703 let paths = vec!["src/main.rs".to_string(), "a/b:c/engine.rs".to_string()];
1704 let m = msg_fs_search_result(3, FS_STATUS_OK, &paths);
1705 let (nonce, status, out) = parse_fs_search_result(&m).unwrap();
1706 assert_eq!(nonce, 3);
1707 assert_eq!(status, FS_STATUS_OK);
1708 assert_eq!(out, paths);
1709 }
1710
1711 #[test]
1712 fn fs_index_request_roundtrip() {
1713 let m = msg_fs_index(0x0102, "/tmp/watch me");
1714 assert_eq!(
1717 m.iter().map(|x| format!("{x:02x}")).collect::<String>(),
1718 "470201000d002f746d702f7761746368206d65"
1719 );
1720 let (nonce, flags, root) = parse_fs_index(&m).unwrap();
1721 assert_eq!(nonce, 0x0102);
1722 assert_eq!(flags, 0);
1723 assert_eq!(root, "/tmp/watch me");
1724 assert_eq!(parse_fs_index(&m[..5]), None);
1725 assert_eq!(parse_fs_index(&msg_fs_stop(1)), None);
1726 }
1727
1728 #[test]
1729 fn fs_index_result_roundtrip() {
1730 let paths = vec![
1731 "a/b:c/engine.rs".to_string(),
1732 "src/main.rs".to_string(),
1733 String::new(),
1734 ];
1735 let m = msg_fs_index_result(3, FS_DONE_OK, FS_INDEX_TRUNCATED, &paths);
1736 let (nonce, status, flags, out) = parse_fs_index_result(&m).unwrap();
1737 assert_eq!(nonce, 3);
1738 assert_eq!(status, FS_DONE_OK);
1739 assert_eq!(flags, FS_INDEX_TRUNCATED);
1740 assert_eq!(out, paths);
1741
1742 let empty = msg_fs_index_result(4, FS_DONE_NOT_FOUND, 0, &[]);
1744 let (_, status, _, out) = parse_fs_index_result(&empty).unwrap();
1745 assert_eq!(status, FS_DONE_NOT_FOUND);
1746 assert!(out.is_empty());
1747
1748 let mut lying = msg_fs_index_result(5, FS_DONE_OK, 0, &paths);
1750 lying[5..9].copy_from_slice(&2u32.to_le_bytes());
1751 assert_eq!(parse_fs_index_result(&lying), None);
1752
1753 let mut huge = msg_fs_index_result(6, FS_DONE_OK, 0, &[]);
1756 huge[5..9].copy_from_slice(&((FS_INDEX_MAX_COUNT as u32) + 1).to_le_bytes());
1757 assert_eq!(parse_fs_index_result(&huge), None);
1758 }
1759
1760 #[test]
1761 fn single_and_recursive_are_mutually_exclusive() {
1762 assert!(fs_sync_flags_valid(FS_SYNC_SINGLE));
1763 assert!(fs_sync_flags_valid(FS_SYNC_SINGLE | FS_SYNC_CONTENT));
1764 assert!(fs_sync_flags_valid(FS_SYNC_RECURSIVE | FS_SYNC_CONTENT));
1765 assert!(!fs_sync_flags_valid(FS_SYNC_SINGLE | FS_SYNC_RECURSIVE));
1766 assert!(!fs_sync_flags_valid(
1767 FS_SYNC_SINGLE | FS_SYNC_RECURSIVE | FS_SYNC_CONTENT
1768 ));
1769 }
1770
1771 #[test]
1772 fn fs_sync_from_pty_roundtrip_and_rebase() {
1773 let m = msg_fs_sync_from_pty(7, FS_SYNC_RECURSIVE, 0, 0, "sub", 42);
1774 assert_eq!(
1775 m,
1776 vec![
1777 0x40, 0x07, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x73,
1778 0x75, 0x62, 0x2a, 0x00
1779 ]
1780 );
1781 assert_eq!(fs_sync_src_pty(&m), Some(42));
1782 assert_eq!(
1783 fs_sync_src_pty(&msg_fs_sync(7, FS_SYNC_RECURSIVE, 0, 0, "sub")),
1784 None
1785 );
1786 let reb = fs_sync_rebase(&m, Some("/home/u")).unwrap();
1788 assert_eq!(fs_sync_flags(&reb).unwrap() & FS_SYNC_FROM_PTY, 0);
1789 let plen = u16::from_le_bytes([reb[11], reb[12]]) as usize;
1790 assert_eq!(
1791 std::str::from_utf8(&reb[13..13 + plen]).unwrap(),
1792 "/home/u/sub"
1793 );
1794 let reb0 = fs_sync_rebase(&m, None).unwrap();
1796 let plen0 = u16::from_le_bytes([reb0[11], reb0[12]]) as usize;
1797 assert_eq!(std::str::from_utf8(&reb0[13..13 + plen0]).unwrap(), "sub");
1798 }
1799
1800 #[test]
1804 fn fs_sync_exclude_field_roundtrips_alongside_from_pty() {
1805 let plain = msg_fs_sync(1, FS_SYNC_RECURSIVE, 0, 0, "sub");
1806 assert_eq!(fs_sync_flags(&plain).unwrap() & FS_SYNC_EXCLUDE, 0);
1807 assert_eq!(fs_sync_exclude(&plain), Some(""));
1808
1809 assert_eq!(
1811 msg_fs_sync_excluding(1, FS_SYNC_RECURSIVE, 0, 0, "sub", ""),
1812 plain
1813 );
1814
1815 let ex = msg_fs_sync_excluding(1, FS_SYNC_RECURSIVE, 0, 0, "sub", "target\n!keep");
1816 assert_ne!(fs_sync_flags(&ex).unwrap() & FS_SYNC_EXCLUDE, 0);
1817 assert_eq!(fs_sync_exclude(&ex), Some("target\n!keep"));
1818 assert_eq!(fs_sync_src_pty(&ex), None);
1819
1820 let both = msg_fs_sync_full(1, FS_SYNC_RECURSIVE, 0, 0, "sub", "target", Some(42));
1821 assert_eq!(fs_sync_exclude(&both), Some("target"));
1822 assert_eq!(fs_sync_src_pty(&both), Some(42), "reached past the field");
1823 let reb = fs_sync_rebase(&both, Some("/home/u")).unwrap();
1824 assert_eq!(fs_sync_flags(&reb).unwrap() & FS_SYNC_FROM_PTY, 0);
1825 assert_ne!(fs_sync_flags(&reb).unwrap() & FS_SYNC_EXCLUDE, 0);
1826 assert_eq!(fs_sync_exclude(&reb), Some("target"), "filter survives");
1827 let plen = u16::from_le_bytes([reb[11], reb[12]]) as usize;
1828 assert_eq!(
1829 std::str::from_utf8(&reb[13..13 + plen]).unwrap(),
1830 "/home/u/sub"
1831 );
1832
1833 assert_eq!(fs_sync_exclude(&ex[..ex.len() - 1]), None);
1835 let mut headerless = ex.clone();
1836 headerless.truncate(17);
1837 assert_eq!(fs_sync_exclude(&headerless), None);
1838 }
1839
1840 #[test]
1844 fn exclusion_flags_are_rejected_with_single() {
1845 for flag in [FS_SYNC_EXCLUDE_GIT, FS_SYNC_GITIGNORE, FS_SYNC_EXCLUDE] {
1846 assert!(fs_sync_flags_valid(flag));
1847 assert!(fs_sync_flags_valid(flag | FS_SYNC_RECURSIVE));
1848 assert!(!fs_sync_flags_valid(flag | FS_SYNC_SINGLE));
1849 }
1850 }
1851
1852 fn upsert(path: &str, content: &[u8]) -> Vec<u8> {
1853 let mut buf = Vec::new();
1854 append_fs_record(
1855 &mut buf,
1856 &FsRecord::Upsert {
1857 path,
1858 entry_flags: FS_ENTRY_FILE,
1859 size: content.len() as u64,
1860 mtime_ns: 42,
1861 mode: 0o644,
1862 hash: 7,
1863 content: FsContent::Full(content),
1864 },
1865 );
1866 buf
1867 }
1868
1869 #[test]
1870 fn record_roundtrip() {
1871 let mut buf = Vec::new();
1872 append_fs_record(
1873 &mut buf,
1874 &FsRecord::Upsert {
1875 path: "a/b.txt",
1876 entry_flags: FS_ENTRY_FILE | FS_ENTRY_NO_CONTENT,
1877 size: 10,
1878 mtime_ns: 1_700_000_000_000_000_000,
1879 mode: 0o755,
1880 hash: 0xDEAD_BEEF_DEAD_BEEF_DEAD_BEEF,
1881 content: FsContent::None,
1882 },
1883 );
1884 append_fs_record(&mut buf, &FsRecord::Delete { path: "old" });
1885 append_fs_record(
1886 &mut buf,
1887 &FsRecord::Move {
1888 from: "src",
1889 to: "dst",
1890 },
1891 );
1892 let records: Vec<_> = fs_records(&buf).collect();
1893 assert_eq!(records.len(), 3);
1894 match &records[0] {
1895 FsRecord::Upsert {
1896 path,
1897 entry_flags,
1898 size,
1899 mtime_ns,
1900 mode,
1901 hash,
1902 content,
1903 } => {
1904 assert_eq!(*path, "a/b.txt");
1905 assert_eq!(*entry_flags, FS_ENTRY_FILE | FS_ENTRY_NO_CONTENT);
1906 assert_eq!(*size, 10);
1907 assert_eq!(*mtime_ns, 1_700_000_000_000_000_000);
1908 assert_eq!(*mode, 0o755);
1909 assert_eq!(*hash, 0xDEAD_BEEF_DEAD_BEEF_DEAD_BEEF);
1910 assert_eq!(*content, FsContent::None);
1911 }
1912 other => panic!("unexpected {other:?}"),
1913 }
1914 assert_eq!(records[1], FsRecord::Delete { path: "old" });
1915 assert_eq!(
1916 records[2],
1917 FsRecord::Move {
1918 from: "src",
1919 to: "dst"
1920 }
1921 );
1922 }
1923
1924 #[test]
1930 fn wire_fixtures() {
1931 fn hex(b: &[u8]) -> String {
1932 b.iter().map(|x| format!("{x:02x}")).collect()
1933 }
1934
1935 assert_eq!(
1936 hex(&msg_fs_sync(
1937 0x0102,
1938 FS_SYNC_RECURSIVE | FS_SYNC_CONTENT,
1939 25,
1940 65536,
1941 "/tmp/watch me"
1942 )),
1943 "40020103001900000001000d002f746d702f7761746368206d65"
1944 );
1945 assert_eq!(
1947 hex(&msg_fs_sync_full(
1948 7,
1949 FS_SYNC_RECURSIVE,
1950 0,
1951 0,
1952 "sub",
1953 "target",
1954 Some(42)
1955 )),
1956 "4007009100000000000000030073756206007461726765742a00"
1957 );
1958 assert_eq!(hex(&msg_fs_stop(0x0102)), "410201");
1959 assert_eq!(hex(&msg_fs_ack(0x0102, 0x01020304)), "42020104030201");
1960 assert_eq!(
1961 hex(&msg_fs_fetch(3, 0x0102, "sub/%FF.bin")),
1962 "43030002010b007375622f2546462e62696e"
1963 );
1964 assert_eq!(
1965 hex(&msg_fs_synced(0x0102, 3, 0, "/w")),
1966 "40020103000002002f77"
1967 );
1968
1969 let mut records = Vec::new();
1970 append_fs_record(
1971 &mut records,
1972 &FsRecord::Upsert {
1973 path: "a.txt",
1974 entry_flags: FS_ENTRY_FILE,
1975 size: 5,
1976 mtime_ns: 1_700_000_000_123_456_789,
1977 mode: 0o100644,
1978 hash: 0x0123_4567_89ab_cdef_1122_3344_5566_7788,
1979 content: FsContent::Full(b"hello"),
1980 },
1981 );
1982 append_fs_record(
1983 &mut records,
1984 &FsRecord::Upsert {
1985 path: "sub",
1986 entry_flags: FS_ENTRY_DIR,
1987 size: 0,
1988 mtime_ns: 0,
1989 mode: 0o40755,
1990 hash: 0,
1991 content: FsContent::None,
1992 },
1993 );
1994 append_fs_record(
1995 &mut records,
1996 &FsRecord::Upsert {
1997 path: "sub/%FF.bin", entry_flags: FS_ENTRY_FILE | FS_ENTRY_NO_CONTENT,
1999 size: 1 << 20,
2000 mtime_ns: 1,
2001 mode: 0o100600,
2002 hash: 0xff,
2003 content: FsContent::None,
2004 },
2005 );
2006 append_fs_record(&mut records, &FsRecord::Delete { path: "old" });
2007 append_fs_record(
2008 &mut records,
2009 &FsRecord::Move {
2010 from: "src",
2011 to: "dst",
2012 },
2013 );
2014 assert_eq!(
2015 hex(&records),
2016 "3700000001000500612e747874050000000000000015cd853dfe9c9717a48100008877665544332211efcdab8967452301010500000068656c6c6f2c0000000101030073756200000000000000000000000000000000ed41000000000000000000000000000000000000003400000001080b007375622f2546462e62696e0000100000000000010000000000000080810000ff00000000000000000000000000000000060000000203006f6c640b0000000303007372630300647374"
2017 );
2018
2019 let decoded: Vec<_> = fs_records(&records).collect();
2021 assert_eq!(decoded.len(), 5);
2022 assert!(matches!(
2023 &decoded[0],
2024 FsRecord::Upsert {
2025 path: "a.txt",
2026 size: 5,
2027 mtime_ns: 1_700_000_000_123_456_789,
2028 hash: 0x0123_4567_89ab_cdef_1122_3344_5566_7788,
2029 content: FsContent::Full(b"hello"),
2030 ..
2031 }
2032 ));
2033 assert_eq!(decoded[3], FsRecord::Delete { path: "old" });
2034 assert_eq!(
2035 decoded[4],
2036 FsRecord::Move {
2037 from: "src",
2038 to: "dst"
2039 }
2040 );
2041 }
2042
2043 #[test]
2044 fn oversized_declared_length_is_rejected_before_allocation() {
2045 let mut msg = vec![S2C_FS_UPDATE];
2047 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();
2053 assert_eq!(mirror.apply_update(&msg), None);
2054
2055 let mut file = vec![S2C_FS_FILE];
2056 file.extend_from_slice(&7u16.to_le_bytes()); file.push(FS_FILE_OK);
2058 file.extend_from_slice(&(1u32 << 30).to_le_bytes());
2059 file.extend_from_slice(&[0u8; 16]);
2060 assert_eq!(parse_fs_file(&file), None);
2061 }
2062
2063 #[test]
2064 fn fs_file_roundtrip() {
2065 let msg = msg_fs_file(9, FS_FILE_OK, b"contents");
2066 assert_eq!(
2067 parse_fs_file(&msg),
2068 Some((9, FS_FILE_OK, b"contents".to_vec()))
2069 );
2070 }
2071
2072 #[test]
2073 fn fs_write_roundtrip() {
2074 let w = FsWrite {
2075 nonce: 7,
2076 sync_id: 3,
2077 flags: FS_WRITE_MKPARENTS | FS_WRITE_DURABLE,
2078 base: 0x0123_4567_89ab_cdef_0123_4567_89ab_cdef,
2079 mode: 0o644,
2080 content_kind: FS_WRITE_CONTENT_FULL,
2081 path: "dir/50%25.txt".to_string(),
2082 content: b"hello world".to_vec(),
2083 };
2084 assert_eq!(parse_fs_write(&msg_fs_write(&w)), Some(w));
2085 let w0 = FsWrite {
2087 nonce: 1,
2088 sync_id: 1,
2089 flags: 0,
2090 base: 0,
2091 mode: 0,
2092 content_kind: FS_WRITE_CONTENT_FULL,
2093 path: "new.txt".to_string(),
2094 content: Vec::new(),
2095 };
2096 assert_eq!(parse_fs_write(&msg_fs_write(&w0)), Some(w0));
2097 assert_eq!(parse_fs_write(&[C2S_FS_WRITE, 0, 0]), None);
2099 assert_eq!(parse_fs_write(&msg_fs_file(1, 0, b"x")), None);
2100 }
2101
2102 #[test]
2103 fn fs_op_roundtrip() {
2104 let rename = FsOp {
2105 nonce: 42,
2106 sync_id: 9,
2107 op: FS_OP_RENAME,
2108 flags: FS_OP_MKPARENTS,
2109 base: 0,
2110 mode: 0,
2111 a: "old/name".to_string(),
2112 b: "new/name".to_string(),
2113 };
2114 assert_eq!(parse_fs_op(&msg_fs_op(&rename)), Some(rename));
2115 let mkdir = FsOp {
2116 nonce: 2,
2117 sync_id: 1,
2118 op: FS_OP_MKDIR,
2119 flags: 0,
2120 base: 0,
2121 mode: 0o700,
2122 a: "sub".to_string(),
2123 b: String::new(),
2124 };
2125 assert_eq!(parse_fs_op(&msg_fs_op(&mkdir)), Some(mkdir));
2126 assert_eq!(parse_fs_op(&[C2S_FS_OP, 0],), None);
2127 }
2128
2129 #[test]
2130 fn fs_write_family_byte_fixtures() {
2131 let hex = |b: &[u8]| b.iter().map(|x| format!("{x:02x}")).collect::<String>();
2133 let w = FsWrite {
2134 nonce: 0x0102,
2135 sync_id: 0x0304,
2136 flags: FS_WRITE_MKPARENTS,
2137 base: 0x0f0e_0d0c_0b0a_0908_0706_0504_0302_0100,
2138 mode: 0o644,
2139 content_kind: FS_WRITE_CONTENT_FULL,
2140 path: "a/b.txt".into(),
2141 content: b"hi".to_vec(),
2142 };
2143 assert_eq!(
2144 hex(&msg_fs_write(&w)),
2145 "440201040302000102030405060708090a0b0c0d0e0fa4010000010700612f622e74787402000000206869"
2146 );
2147 let o = FsOp {
2148 nonce: 0x0102,
2149 sync_id: 0x0304,
2150 op: FS_OP_RENAME,
2151 flags: FS_OP_MKPARENTS,
2152 base: 0,
2153 mode: 0,
2154 a: "x".into(),
2155 b: "y".into(),
2156 };
2157 assert_eq!(
2158 hex(&msg_fs_op(&o)),
2159 "450201040303020000000000000000000000000000000000000000010078010079"
2160 );
2161 let ln = FsOp {
2164 nonce: 0x0102,
2165 sync_id: 0x0304,
2166 op: FS_OP_SYMLINK,
2167 flags: FS_OP_NO_CAS,
2168 base: 0,
2169 mode: 0,
2170 a: "../t".into(),
2171 b: "l".into(),
2172 };
2173 assert_eq!(
2174 hex(&msg_fs_op(&ln)),
2175 "45020104030401000000000000000000000000000000000000000004002e2e2f7401006c"
2176 );
2177 assert_eq!(
2178 hex(&msg_fs_done(
2179 0x0102,
2180 FS_DONE_CONFLICT,
2181 0x0f0e_0d0c_0b0a_0908_0706_0504_0302_0100,
2182 0x1122_3344_5566_7788
2183 )),
2184 "4402010b000102030405060708090a0b0c0d0e0f8877665544332211"
2185 );
2186 }
2187
2188 #[test]
2189 fn fs_done_roundtrip() {
2190 let hash = 0xdead_beef_dead_beef_dead_beef_dead_beefu128;
2191 let msg = msg_fs_done(5, FS_DONE_OK, hash, 1_700_000_000_000_000_000);
2192 assert_eq!(
2193 parse_fs_done(&msg),
2194 Some((5, FS_DONE_OK, hash, 1_700_000_000_000_000_000))
2195 );
2196 let c = msg_fs_done(6, FS_DONE_CONFLICT, hash, 0);
2198 assert_eq!(parse_fs_done(&c), Some((6, FS_DONE_CONFLICT, hash, 0)));
2199 }
2200
2201 #[test]
2202 fn unknown_record_kind_is_skipped() {
2203 let mut buf = Vec::new();
2204 buf.extend_from_slice(&4u32.to_le_bytes());
2206 buf.push(0x7F);
2207 buf.extend_from_slice(&[1, 2, 3]);
2208 append_fs_record(&mut buf, &FsRecord::Delete { path: "x" });
2209 let records: Vec<_> = fs_records(&buf).collect();
2210 assert_eq!(records, vec![FsRecord::Delete { path: "x" }]);
2211 }
2212
2213 #[test]
2214 fn mirror_staged_snapshot_and_live() {
2215 let mut mirror = FsMirror::new();
2216 let mut records = upsert("a.txt", b"alpha");
2218 records.extend_from_slice(&upsert("d/b.txt", b"beta"));
2219 let msg = msg_fs_update(1, 1, FS_UPDATE_RESET | FS_UPDATE_SYNC, &records);
2220 assert_eq!(mirror.apply_update(&msg), Some(1));
2221 assert_eq!(mirror.live.len(), 2);
2222 assert_eq!(mirror.live["a.txt"].content.as_deref(), Some(&b"alpha"[..]));
2223
2224 let mut records = Vec::new();
2226 append_fs_record(&mut records, &FsRecord::Delete { path: "a.txt" });
2227 append_fs_record(&mut records, &FsRecord::Move { from: "d", to: "e" });
2228 let msg = msg_fs_update(1, 2, 0, &records);
2229 assert_eq!(mirror.apply_update(&msg), Some(2));
2230 assert_eq!(mirror.live.len(), 1);
2231 assert_eq!(
2232 mirror.live["e/b.txt"].content.as_deref(),
2233 Some(&b"beta"[..])
2234 );
2235
2236 let msg = msg_fs_update(1, 3, FS_UPDATE_RESET, &upsert("n.txt", b"new"));
2238 assert_eq!(mirror.apply_update(&msg), Some(3));
2239 assert_eq!(mirror.live.len(), 1);
2240 let msg = msg_fs_update(1, 4, FS_UPDATE_SYNC, &[]);
2242 assert_eq!(mirror.apply_update(&msg), Some(4));
2243 assert_eq!(mirror.live.len(), 1);
2244 assert!(mirror.live.contains_key("n.txt"));
2245 }
2246
2247 #[test]
2248 fn delta_content() {
2249 let mut mirror = FsMirror::new();
2250 let msg = msg_fs_update(
2251 1,
2252 1,
2253 FS_UPDATE_RESET | FS_UPDATE_SYNC,
2254 &upsert("f", b"hello world"),
2255 );
2256 mirror.apply_update(&msg).unwrap();
2257
2258 let ops: Vec<u8> = vec![0x01, 0, 6, 0x02, 4, b'b', b'l', b'i', b't'];
2260 let mut records = Vec::new();
2261 append_fs_record(
2262 &mut records,
2263 &FsRecord::Upsert {
2264 path: "f",
2265 entry_flags: FS_ENTRY_FILE,
2266 size: 10,
2267 mtime_ns: 43,
2268 mode: 0o644,
2269 hash: 8,
2270 content: FsContent::Delta(&ops),
2271 },
2272 );
2273 let msg = msg_fs_update(1, 2, 0, &records);
2274 mirror.apply_update(&msg).unwrap();
2275 assert_eq!(
2276 mirror.live["f"].content.as_deref(),
2277 Some(&b"hello blit"[..])
2278 );
2279 }
2280
2281 #[test]
2282 fn subtree_semantics() {
2283 let mut map = BTreeMap::new();
2284 for p in ["a", "a/b", "a/b/c", "ab", "z"] {
2285 map.insert(
2286 p.to_string(),
2287 FsNode {
2288 entry_flags: FS_ENTRY_FILE,
2289 size: 0,
2290 mtime_ns: 0,
2291 mode: 0,
2292 hash: 0,
2293 content: None,
2294 },
2295 );
2296 }
2297 let taken = take_subtree(&mut map.clone(), "a");
2300 let suffixes: Vec<_> = taken.iter().map(|(s, _)| s.clone()).collect();
2301 assert_eq!(
2302 suffixes,
2303 vec![String::new(), "b".to_string(), "b/c".to_string()]
2304 );
2305 remove_subtree(&mut map, "a");
2306 let left: Vec<_> = map.keys().cloned().collect();
2307 assert_eq!(left, vec!["ab".to_string(), "z".to_string()]);
2308 }
2309}