Skip to content

refactor(uffs-mft): introduce Usn(i64) newtype for journal USNs#257

Merged
githubrobbi merged 2 commits into
mainfrom
refactor/phase-4-5c-usn-newtype
May 16, 2026
Merged

refactor(uffs-mft): introduce Usn(i64) newtype for journal USNs#257
githubrobbi merged 2 commits into
mainfrom
refactor/phase-4-5c-usn-newtype

Conversation

@githubrobbi
Copy link
Copy Markdown
Collaborator

Summary

Replaces ad-hoc `i64` USN values across the cache-checkpoint, journal-read, and incremental-apply paths with a typed `uffs_mft::usn::Usn` newtype. Cache wrap detection (`cached_usn < first_usn`), checkpoint persistence, and `UsnRecord.usn` now read directly off the type instead of relying on bare integer arithmetic that any caller could mis-interpret as a generic counter.

Why

Phase 4 sub-phase 5c of the platform-typing refactor. After 5b.0/5b.1 (DriveLetter, PipeName) it was the last hot-path identifier still flowing as a primitive. The newtype is `Copy + Eq + Ord + Hash + Display` so:

  • monotonic comparisons stay byte-for-byte equivalent to the old `i64` ordering;
  • `Usn::ZERO` documents the "no prior checkpoint — read from journal head" sentinel that scattered `0` literals used to encode by convention;
  • tracing fields use `%`-Display so logs render `123` (not `Usn(123)` from `Debug`).

Wire format unchanged

Three regression tests pin the contract:

  • `tests_extensions::serialize_roundtrip` and `tests_storage::empty_serialized_index` — `IndexHeader.next_usn` round-trips through 8 LE i64 bytes via `.raw().to_le_bytes()` / `Usn::new(read_i64)`, identical to v13.
  • `usn::tests::raw_roundtrip_preserves_i64_exactly` — every `i64` in `[MIN, -1, 0, 1, 42, MAX]` round-trips byte-identical through the newtype.
  • Daemon-side `JournalSource::poll` keeps its `cursor: u64` persistence contract; the `Usn` wrap/unwrap is a single narrowing in `sources.rs`.

Out of scope on purpose

  • `#[repr(C)]` Win32 ABI mirror structs (`UsnJournalDataV0`, `ReadUsnJournalDataV0`, `UsnRecordV2Header`) keep raw `i64` fields — they mirror `winioctl.h` byte-for-byte.
  • The `uffs-mft usn-read --start-usn ` CLI flag stays `Option` (user-facing surface) and is wrapped at the consumer site only.
  • `StandardInfo.usn: u64` (from the NTFS `$STANDARD_INFORMATION` attribute) is a separate USN flavour with different signedness; not touched.

File split

The migration (newtype + unit tests) pushed `usn.rs` from 767 LOC over the 800-LOC policy limit. Rather than add an exception entry, split into two cohesive units:

  • `crates/uffs-mft/src/usn/mod.rs` (428 LOC) — DTOs, reason flags, `ChangeType`, `FileChange`, `aggregate_changes`, non-Windows stubs, unit tests.
  • `crates/uffs-mft/src/usn/windows.rs` (527 LOC) — Win32 FFI mirror structs + `FSCTL_QUERY_USN_JOURNAL` / `FSCTL_READ_USN_JOURNAL` / targeted FRS reads.

`pub use windows::{query_usn_journal, read_targeted_frs_records, read_usn_journal}` keeps the public API path `uffs_mft::usn::*` unchanged.

Verification

  • `just lint-pre-push` — all gates green locally, including `lint-ci-windows` (cargo xwin clippy).
  • All migrated consumers compile clean: `reader/usn_apply.rs`, `reader/multi_drive/index.rs`, `reader/index_cache.rs`, `cache.rs`, `cache_dataframe.rs`, `commands/windows/{incremental,usn}.rs`, `daemon/cache/journal_loop/sources.rs`, `compact_loader.rs`.
  • Test fixtures (`tests_extensions`, `tests_storage`) strengthened to assert against typed `Usn::new(...)` values.

Replaces ad-hoc `i64` USN values with a typed `uffs_mft::usn::Usn`
newtype so cache wrap detection, checkpoint persistence, and journal
record fields stop relying on raw integers that any caller could
mis-interpret as a generic counter.

The newtype is `Copy + Eq + Ord + Hash + Display` and exposes
`Usn::ZERO`, `Usn::new`, `Usn::raw`, and `Usn::is_zero` so:

* monotonic comparisons (`cached_usn < first_usn`, `start_usn >=
  next_usn`) read directly from the type rather than re-deriving
  meaning from `i64` arithmetic;
* the `0` sentinel meaning "no prior checkpoint — read from journal
  head" gets a self-documenting constant;
* tracing fields use `%`-Display so logs render `123` not
  `Usn(123)`.

## Wire format unchanged

Pinned by `tests_extensions::serialize_roundtrip` /
`tests_storage::empty_serialized_index` /
`usn::tests::raw_roundtrip_preserves_i64_exactly`:

* on-disk `IndexHeader.next_usn` still serializes as 8 LE i64 bytes
  via `.raw().to_le_bytes()` and deserializes via `Usn::new(read_i64)`;
* daemon-side `JournalSource::poll` keeps its `cursor: u64`
  persistence contract — the `Usn` wrap/unwrap is a single
  call-boundary narrowing inside `sources.rs`;
* the `#[repr(C)]` Win32 ABI mirror structs (`UsnJournalDataV0`,
  `ReadUsnJournalDataV0`, `UsnRecordV2Header`) keep their raw `i64`
  fields — they mirror `winioctl.h` byte-for-byte;
* the `uffs-mft usn-read --start-usn <i64>` CLI flag stays
  `Option<i64>` and is wrapped at the consumer site.

## File split

Migrating UI doc + tests pushed `usn.rs` over the 800-LOC policy
limit.  Split into `usn/mod.rs` (DTOs + reason flags + aggregation +
non-Windows stubs + tests, 428 LOC) and `usn/windows.rs` (Win32 FFI
mirror structs + ioctl helpers + targeted FRS reads, 527 LOC).  No
file needs a size-policy exception.

## Untouched on purpose

`StandardInfo.usn: u64` (from the NTFS `$STANDARD_INFORMATION`
attribute) is a separate USN flavour with a different signedness
contract; out of scope for this PR.
@githubrobbi githubrobbi enabled auto-merge (squash) May 16, 2026 00:41
@githubrobbi githubrobbi merged commit d61fa02 into main May 16, 2026
26 checks passed
@githubrobbi githubrobbi deleted the refactor/phase-4-5c-usn-newtype branch May 16, 2026 01:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant