use std::collections::HashMap;
use std::path::PathBuf;
use crate::frn_table::{FrnEntry, FrnTable};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) struct UsnReason {
pub file_create: bool,
pub file_delete: bool,
pub rename_old_name: bool,
pub rename_new_name: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct UsnRecord {
pub usn: i64,
pub frn: u64,
pub parent_frn: u64,
pub name: String,
pub is_dir: bool,
pub reason: UsnReason,
}
const USN_REASON_FILE_CREATE: u32 = 0x100;
const USN_REASON_FILE_DELETE: u32 = 0x200;
const USN_REASON_RENAME_OLD_NAME: u32 = 0x1000;
const USN_REASON_RENAME_NEW_NAME: u32 = 0x2000;
const FILE_ATTRIBUTE_DIRECTORY_BIT: u32 = 0x10;
pub(crate) fn parse_usn_record_v2_bytes(record: &[u8]) -> Option<UsnRecord> {
if record.len() < 60 {
return None;
}
let frn = u64::from_ne_bytes(record[8..16].try_into().ok()?);
let parent_frn = u64::from_ne_bytes(record[16..24].try_into().ok()?);
let usn = i64::from_ne_bytes(record[24..32].try_into().ok()?);
let reason_bits = u32::from_ne_bytes(record[40..44].try_into().ok()?);
let file_attributes = u32::from_ne_bytes(record[52..56].try_into().ok()?);
let name_length = u16::from_ne_bytes(record[56..58].try_into().ok()?) as usize;
let name_offset = u16::from_ne_bytes(record[58..60].try_into().ok()?) as usize;
if name_offset.checked_add(name_length)? > record.len() {
return None;
}
let name_bytes = &record[name_offset..name_offset + name_length];
let name_u16: Vec<u16> = name_bytes
.chunks_exact(2)
.map(|c| u16::from_ne_bytes([c[0], c[1]]))
.collect();
let name = String::from_utf16_lossy(&name_u16);
Some(UsnRecord {
usn,
frn,
parent_frn,
name,
is_dir: file_attributes & FILE_ATTRIBUTE_DIRECTORY_BIT != 0,
reason: UsnReason {
file_create: reason_bits & USN_REASON_FILE_CREATE != 0,
file_delete: reason_bits & USN_REASON_FILE_DELETE != 0,
rename_old_name: reason_bits & USN_REASON_RENAME_OLD_NAME != 0,
rename_new_name: reason_bits & USN_REASON_RENAME_NEW_NAME != 0,
},
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum UsnOutcome {
Upsert {
path: PathBuf,
is_dir: bool,
},
Remove {
path: PathBuf,
is_dir: bool,
},
Rename {
from: PathBuf,
to: PathBuf,
to_is_dir: bool,
},
None,
}
pub(crate) struct UsnTranslator {
table: FrnTable,
pending_renames: HashMap<u64, PathBuf>,
}
impl UsnTranslator {
pub fn new(table: FrnTable) -> Self {
Self {
table,
pending_renames: HashMap::new(),
}
}
#[cfg(test)]
pub fn table(&self) -> &FrnTable {
&self.table
}
pub fn into_table(self) -> FrnTable {
self.table
}
pub fn translate(&mut self, record: UsnRecord) -> UsnOutcome {
if record.reason.rename_old_name {
return self.handle_rename_old_name(record);
}
if record.reason.rename_new_name {
return self.handle_rename_new_name(record);
}
if record.reason.file_delete {
return self.handle_delete(record);
}
self.handle_upsert(record)
}
fn handle_upsert(&mut self, record: UsnRecord) -> UsnOutcome {
self.pending_renames.remove(&record.frn);
self.table.upsert(
record.frn,
FrnEntry {
parent_frn: record.parent_frn,
name: record.name.clone(),
is_dir: record.is_dir,
},
);
match self.table.reconstruct_path(record.frn) {
Some(path) => UsnOutcome::Upsert {
path,
is_dir: record.is_dir,
},
None => UsnOutcome::None,
}
}
fn handle_rename_old_name(&mut self, record: UsnRecord) -> UsnOutcome {
self.table.upsert(
record.frn,
FrnEntry {
parent_frn: record.parent_frn,
name: record.name.clone(),
is_dir: record.is_dir,
},
);
if let Some(from_path) = self.table.reconstruct_path(record.frn) {
self.pending_renames.insert(record.frn, from_path);
}
UsnOutcome::None
}
fn handle_rename_new_name(&mut self, record: UsnRecord) -> UsnOutcome {
self.table.upsert(
record.frn,
FrnEntry {
parent_frn: record.parent_frn,
name: record.name.clone(),
is_dir: record.is_dir,
},
);
let Some(to_path) = self.table.reconstruct_path(record.frn) else {
if let Some(from_path) = self.pending_renames.remove(&record.frn) {
return UsnOutcome::Remove {
path: from_path,
is_dir: record.is_dir,
};
}
return UsnOutcome::None;
};
match self.pending_renames.remove(&record.frn) {
Some(from_path) => {
if from_path == to_path {
return UsnOutcome::None;
}
UsnOutcome::Rename {
from: from_path,
to: to_path,
to_is_dir: record.is_dir,
}
}
None => UsnOutcome::Upsert {
path: to_path,
is_dir: record.is_dir,
},
}
}
fn handle_delete(&mut self, record: UsnRecord) -> UsnOutcome {
if let Some(from_path) = self.pending_renames.remove(&record.frn) {
self.table.remove(record.frn);
return UsnOutcome::Remove {
path: from_path,
is_dir: record.is_dir,
};
}
self.table.upsert(
record.frn,
FrnEntry {
parent_frn: record.parent_frn,
name: record.name.clone(),
is_dir: record.is_dir,
},
);
let path = self.table.reconstruct_path(record.frn);
self.table.remove(record.frn);
match path {
Some(path) => UsnOutcome::Remove {
path,
is_dir: record.is_dir,
},
None => UsnOutcome::None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn build_usn_record_v2_bytes(
frn: u64,
parent_frn: u64,
usn: i64,
reason_bits: u32,
file_attributes: u32,
name: &str,
) -> Vec<u8> {
let name_u16: Vec<u16> = name.encode_utf16().collect();
let name_bytes_len = name_u16.len() * 2;
let name_offset = 60u16;
let record_length = name_offset as usize + name_bytes_len;
let mut buf = Vec::with_capacity(record_length);
buf.extend_from_slice(&(record_length as u32).to_ne_bytes()); buf.extend_from_slice(&2u16.to_ne_bytes()); buf.extend_from_slice(&0u16.to_ne_bytes()); buf.extend_from_slice(&frn.to_ne_bytes());
buf.extend_from_slice(&parent_frn.to_ne_bytes());
buf.extend_from_slice(&usn.to_ne_bytes());
buf.extend_from_slice(&0i64.to_ne_bytes()); buf.extend_from_slice(&reason_bits.to_ne_bytes());
buf.extend_from_slice(&0u32.to_ne_bytes()); buf.extend_from_slice(&0u32.to_ne_bytes()); buf.extend_from_slice(&file_attributes.to_ne_bytes());
buf.extend_from_slice(&(name_bytes_len as u16).to_ne_bytes()); buf.extend_from_slice(&name_offset.to_ne_bytes()); for unit in &name_u16 {
buf.extend_from_slice(&unit.to_ne_bytes());
}
assert_eq!(
buf.len(),
record_length,
"构造的字节数应该跟算出的 RecordLength 一致"
);
buf
}
#[test]
fn parses_plain_file_record() {
let bytes = build_usn_record_v2_bytes(42, 1, 999, USN_REASON_FILE_CREATE, 0, "a.md");
let record = parse_usn_record_v2_bytes(&bytes).expect("应该解析成功");
assert_eq!(record.frn, 42);
assert_eq!(record.parent_frn, 1);
assert_eq!(record.usn, 999);
assert_eq!(record.name, "a.md");
assert!(!record.is_dir);
assert!(record.reason.file_create);
assert!(!record.reason.file_delete);
}
#[test]
fn parses_directory_attribute_flag() {
let bytes = build_usn_record_v2_bytes(
7,
1,
1,
USN_REASON_FILE_CREATE,
FILE_ATTRIBUTE_DIRECTORY_BIT,
"sub",
);
let record = parse_usn_record_v2_bytes(&bytes).expect("应该解析成功");
assert!(record.is_dir);
}
#[test]
fn parses_rename_reason_bits() {
let bytes = build_usn_record_v2_bytes(7, 1, 5, USN_REASON_RENAME_OLD_NAME, 0, "old.md");
let record = parse_usn_record_v2_bytes(&bytes).expect("应该解析成功");
assert!(record.reason.rename_old_name);
assert!(!record.reason.rename_new_name);
let bytes2 = build_usn_record_v2_bytes(7, 1, 6, USN_REASON_RENAME_NEW_NAME, 0, "new.md");
let record2 = parse_usn_record_v2_bytes(&bytes2).expect("应该解析成功");
assert!(record2.reason.rename_new_name);
}
#[test]
fn parses_cjk_filename_correctly() {
let bytes =
build_usn_record_v2_bytes(9, 1, 1, USN_REASON_FILE_CREATE, 0, "分布式限流器.md");
let record = parse_usn_record_v2_bytes(&bytes).expect("应该解析成功");
assert_eq!(record.name, "分布式限流器.md");
}
#[test]
fn truncated_record_shorter_than_fixed_header_fails_gracefully() {
let bytes = vec![0u8; 30]; assert_eq!(parse_usn_record_v2_bytes(&bytes), None);
}
#[test]
fn name_offset_beyond_buffer_fails_gracefully_instead_of_panicking() {
let mut bytes = build_usn_record_v2_bytes(1, 1, 1, USN_REASON_FILE_CREATE, 0, "a.md");
bytes[58..60].copy_from_slice(&5000u16.to_ne_bytes());
assert_eq!(parse_usn_record_v2_bytes(&bytes), None);
}
#[test]
fn odd_name_length_drops_trailing_partial_code_unit_without_panicking() {
let mut bytes = build_usn_record_v2_bytes(5, 1, 1, USN_REASON_FILE_CREATE, 0, "XY");
bytes[56..58].copy_from_slice(&3u16.to_ne_bytes());
let record = parse_usn_record_v2_bytes(&bytes).expect("奇数长度也应解析成功而不 panic");
assert_eq!(record.name, "X", "尾部不满 2 字节的部分应被丢弃");
}
#[test]
fn zero_name_length_yields_empty_name() {
let bytes = build_usn_record_v2_bytes(6, 1, 1, USN_REASON_FILE_CREATE, 0, "");
let record = parse_usn_record_v2_bytes(&bytes).expect("空文件名也应解析成功");
assert_eq!(record.name, "");
assert_eq!(record.frn, 6);
}
fn root_path() -> PathBuf {
if cfg!(windows) {
PathBuf::from(r"C:\watch")
} else {
PathBuf::from("/watch")
}
}
fn translator_with_root() -> (UsnTranslator, u64) {
let root_frn = 1;
let mut table = FrnTable::new();
table.register_root(root_frn, root_path());
(UsnTranslator::new(table), root_frn)
}
fn reason(f: impl FnOnce(&mut UsnReason)) -> UsnReason {
let mut r = UsnReason::default();
f(&mut r);
r
}
#[test]
fn create_record_emits_upsert_with_is_dir_flag() {
let (mut t, root) = translator_with_root();
let outcome = t.translate(UsnRecord {
usn: 10,
frn: 2,
parent_frn: root,
name: "a.md".to_string(),
is_dir: false,
reason: reason(|r| r.file_create = true),
});
assert_eq!(
outcome,
UsnOutcome::Upsert {
path: root_path().join("a.md"),
is_dir: false,
}
);
}
#[test]
fn plain_content_change_without_reason_bits_still_upserts() {
let (mut t, root) = translator_with_root();
let outcome = t.translate(UsnRecord {
usn: 5,
frn: 2,
parent_frn: root,
name: "a.md".to_string(),
is_dir: false,
reason: UsnReason::default(),
});
assert_eq!(
outcome,
UsnOutcome::Upsert {
path: root_path().join("a.md"),
is_dir: false,
}
);
}
#[test]
fn delete_record_emits_remove_and_forgets_frn() {
let (mut t, root) = translator_with_root();
t.translate(UsnRecord {
usn: 1,
frn: 2,
parent_frn: root,
name: "a.md".to_string(),
is_dir: false,
reason: reason(|r| r.file_create = true),
});
let outcome = t.translate(UsnRecord {
usn: 2,
frn: 2,
parent_frn: root,
name: "a.md".to_string(),
is_dir: false,
reason: reason(|r| r.file_delete = true),
});
assert_eq!(
outcome,
UsnOutcome::Remove {
path: root_path().join("a.md"),
is_dir: false,
}
);
assert!(t.table().get(2).is_none());
}
#[test]
fn directory_delete_flags_is_dir_true() {
let (mut t, root) = translator_with_root();
t.translate(UsnRecord {
usn: 1,
frn: 2,
parent_frn: root,
name: "sub".to_string(),
is_dir: true,
reason: reason(|r| r.file_create = true),
});
let outcome = t.translate(UsnRecord {
usn: 2,
frn: 2,
parent_frn: root,
name: "sub".to_string(),
is_dir: true,
reason: reason(|r| r.file_delete = true),
});
assert_eq!(
outcome,
UsnOutcome::Remove {
path: root_path().join("sub"),
is_dir: true,
}
);
}
#[test]
fn rename_old_name_alone_produces_no_event() {
let (mut t, root) = translator_with_root();
t.translate(UsnRecord {
usn: 1,
frn: 2,
parent_frn: root,
name: "old.md".to_string(),
is_dir: false,
reason: reason(|r| r.file_create = true),
});
let outcome = t.translate(UsnRecord {
usn: 2,
frn: 2,
parent_frn: root,
name: "old.md".to_string(),
is_dir: false,
reason: reason(|r| r.rename_old_name = true),
});
assert_eq!(outcome, UsnOutcome::None);
}
#[test]
fn old_name_then_new_name_pairs_into_rename_event() {
let (mut t, root) = translator_with_root();
t.translate(UsnRecord {
usn: 1,
frn: 2,
parent_frn: root,
name: "old.md".to_string(),
is_dir: false,
reason: reason(|r| r.file_create = true),
});
t.translate(UsnRecord {
usn: 2,
frn: 2,
parent_frn: root,
name: "old.md".to_string(),
is_dir: false,
reason: reason(|r| r.rename_old_name = true),
});
let outcome = t.translate(UsnRecord {
usn: 3,
frn: 2,
parent_frn: root,
name: "new.md".to_string(),
is_dir: false,
reason: reason(|r| r.rename_new_name = true),
});
assert_eq!(
outcome,
UsnOutcome::Rename {
from: root_path().join("old.md"),
to: root_path().join("new.md"),
to_is_dir: false,
}
);
assert_eq!(
t.table().reconstruct_path(2),
Some(root_path().join("new.md"))
);
}
#[test]
fn rename_immediately_followed_by_delete_before_pairing_completes_is_not_swallowed() {
let (mut t, root) = translator_with_root();
t.translate(UsnRecord {
usn: 1,
frn: 2,
parent_frn: root,
name: "old.md".to_string(),
is_dir: false,
reason: reason(|r| r.file_create = true),
});
let rename_outcome = t.translate(UsnRecord {
usn: 2,
frn: 2,
parent_frn: root,
name: "old.md".to_string(),
is_dir: false,
reason: reason(|r| r.rename_old_name = true),
});
assert_eq!(rename_outcome, UsnOutcome::None, "OLD_NAME 本身不产出事件");
let delete_outcome = t.translate(UsnRecord {
usn: 3,
frn: 2,
parent_frn: root,
name: "old.md".to_string(), is_dir: false,
reason: reason(|r| r.file_delete = true),
});
assert_eq!(
delete_outcome,
UsnOutcome::Remove {
path: root_path().join("old.md"),
is_dir: false,
},
"配对未完成时被删除,必须退回用旧路径发一次 Remove,不能吞事件"
);
assert!(t.table().get(2).is_none(), "FRN 应该被彻底清出表");
let late_new_name = t.translate(UsnRecord {
usn: 4,
frn: 2,
parent_frn: root,
name: "new.md".to_string(),
is_dir: false,
reason: reason(|r| r.rename_new_name = true),
});
assert_eq!(
late_new_name,
UsnOutcome::Upsert {
path: root_path().join("new.md"),
is_dir: false,
}
);
}
#[test]
fn rename_completes_then_delete_targets_the_new_path_not_the_old_one() {
let (mut t, root) = translator_with_root();
t.translate(UsnRecord {
usn: 1,
frn: 2,
parent_frn: root,
name: "old.md".to_string(),
is_dir: false,
reason: reason(|r| r.file_create = true),
});
t.translate(UsnRecord {
usn: 2,
frn: 2,
parent_frn: root,
name: "old.md".to_string(),
is_dir: false,
reason: reason(|r| r.rename_old_name = true),
});
let rename = t.translate(UsnRecord {
usn: 3,
frn: 2,
parent_frn: root,
name: "new.md".to_string(),
is_dir: false,
reason: reason(|r| r.rename_new_name = true),
});
assert_eq!(
rename,
UsnOutcome::Rename {
from: root_path().join("old.md"),
to: root_path().join("new.md"),
to_is_dir: false,
}
);
let delete = t.translate(UsnRecord {
usn: 4,
frn: 2,
parent_frn: root,
name: "new.md".to_string(),
is_dir: false,
reason: reason(|r| r.file_delete = true),
});
assert_eq!(
delete,
UsnOutcome::Remove {
path: root_path().join("new.md"),
is_dir: false,
},
"配对已完成,删除必须对准新路径,不能残留指向旧路径的孤儿文档"
);
}
#[test]
fn interleaved_renames_of_two_different_frns_pair_independently() {
let (mut t, root) = translator_with_root();
t.translate(UsnRecord {
usn: 1,
frn: 10,
parent_frn: root,
name: "a-old.md".to_string(),
is_dir: false,
reason: reason(|r| r.file_create = true),
});
t.translate(UsnRecord {
usn: 2,
frn: 20,
parent_frn: root,
name: "b-old.md".to_string(),
is_dir: false,
reason: reason(|r| r.file_create = true),
});
t.translate(UsnRecord {
usn: 3,
frn: 10,
parent_frn: root,
name: "a-old.md".to_string(),
is_dir: false,
reason: reason(|r| r.rename_old_name = true),
});
t.translate(UsnRecord {
usn: 4,
frn: 20,
parent_frn: root,
name: "b-old.md".to_string(),
is_dir: false,
reason: reason(|r| r.rename_old_name = true),
});
let a_rename = t.translate(UsnRecord {
usn: 5,
frn: 10,
parent_frn: root,
name: "a-new.md".to_string(),
is_dir: false,
reason: reason(|r| r.rename_new_name = true),
});
let b_rename = t.translate(UsnRecord {
usn: 6,
frn: 20,
parent_frn: root,
name: "b-new.md".to_string(),
is_dir: false,
reason: reason(|r| r.rename_new_name = true),
});
assert_eq!(
a_rename,
UsnOutcome::Rename {
from: root_path().join("a-old.md"),
to: root_path().join("a-new.md"),
to_is_dir: false,
}
);
assert_eq!(
b_rename,
UsnOutcome::Rename {
from: root_path().join("b-old.md"),
to: root_path().join("b-new.md"),
to_is_dir: false,
}
);
}
#[test]
fn new_name_without_prior_old_name_is_best_effort_upsert() {
let (mut t, root) = translator_with_root();
let outcome = t.translate(UsnRecord {
usn: 1,
frn: 2,
parent_frn: root,
name: "new.md".to_string(),
is_dir: false,
reason: reason(|r| r.rename_new_name = true),
});
assert_eq!(
outcome,
UsnOutcome::Upsert {
path: root_path().join("new.md"),
is_dir: false,
}
);
}
#[test]
fn rename_moving_out_of_scope_resolves_to_remove_of_old_path() {
let (mut t, root) = translator_with_root();
t.translate(UsnRecord {
usn: 1,
frn: 2,
parent_frn: root,
name: "old.md".to_string(),
is_dir: false,
reason: reason(|r| r.file_create = true),
});
t.translate(UsnRecord {
usn: 2,
frn: 2,
parent_frn: root,
name: "old.md".to_string(),
is_dir: false,
reason: reason(|r| r.rename_old_name = true),
});
let outcome = t.translate(UsnRecord {
usn: 3,
frn: 2,
parent_frn: 9999, name: "old.md".to_string(),
is_dir: false,
reason: reason(|r| r.rename_new_name = true),
});
assert_eq!(
outcome,
UsnOutcome::Remove {
path: root_path().join("old.md"),
is_dir: false,
}
);
}
#[test]
fn frn_reused_after_delete_does_not_leak_stale_pending_rename() {
let (mut t, root) = translator_with_root();
t.translate(UsnRecord {
usn: 1,
frn: 2,
parent_frn: root,
name: "old.md".to_string(),
is_dir: false,
reason: reason(|r| r.file_create = true),
});
t.translate(UsnRecord {
usn: 2,
frn: 2,
parent_frn: root,
name: "old.md".to_string(),
is_dir: false,
reason: reason(|r| r.rename_old_name = true),
});
let outcome = t.translate(UsnRecord {
usn: 3,
frn: 2,
parent_frn: root,
name: "brand-new.md".to_string(),
is_dir: false,
reason: reason(|r| r.file_create = true),
});
assert_eq!(
outcome,
UsnOutcome::Upsert {
path: root_path().join("brand-new.md"),
is_dir: false,
}
);
let delete = t.translate(UsnRecord {
usn: 4,
frn: 2,
parent_frn: root,
name: "brand-new.md".to_string(),
is_dir: false,
reason: reason(|r| r.file_delete = true),
});
assert_eq!(
delete,
UsnOutcome::Remove {
path: root_path().join("brand-new.md"),
is_dir: false,
}
);
}
#[test]
fn rename_to_identical_path_is_a_no_op() {
let (mut t, root) = translator_with_root();
t.translate(UsnRecord {
usn: 1,
frn: 2,
parent_frn: root,
name: "same.md".to_string(),
is_dir: false,
reason: reason(|r| r.file_create = true),
});
t.translate(UsnRecord {
usn: 2,
frn: 2,
parent_frn: root,
name: "same.md".to_string(),
is_dir: false,
reason: reason(|r| r.rename_old_name = true),
});
let outcome = t.translate(UsnRecord {
usn: 3,
frn: 2,
parent_frn: root,
name: "same.md".to_string(),
is_dir: false,
reason: reason(|r| r.rename_new_name = true),
});
assert_eq!(outcome, UsnOutcome::None);
}
}