Skip to content

Commit dddb3ee

Browse files
committed
chore: development v0.2.144 - comprehensive testing complete [auto-commit]
1 parent 5cb23b7 commit dddb3ee

File tree

13 files changed

+301
-47
lines changed

13 files changed

+301
-47
lines changed

CHANGELOG.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2323
- Cleaned up all TTAPI references from justfile and build scripts
2424
- Updated justfile header and recipes for UFFS
2525

26-
## [0.2.143] - 2026-01-27
26+
## [0.2.144] - 2026-01-27
2727

2828
### Added
2929
- Baseline CI validation for modernization effort
@@ -46,7 +46,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4646
### Fixed
4747
- Various MFT parsing edge cases
4848

49-
[Unreleased]: https://github.com/githubrobbi/UltraFastFileSearch/compare/v0.2.143...HEAD
50-
[0.2.143]: https://github.com/githubrobbi/UltraFastFileSearch/compare/v0.2.114...v0.2.143
49+
[Unreleased]: https://github.com/githubrobbi/UltraFastFileSearch/compare/v0.2.144...HEAD
50+
[0.2.144]: https://github.com/githubrobbi/UltraFastFileSearch/compare/v0.2.114...v0.2.144
5151
[0.2.114]: https://github.com/githubrobbi/UltraFastFileSearch/releases/tag/v0.2.114
5252

Cargo.lock

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ exclude = [
3939
# Workspace Package Metadata (inherited by all crates)
4040
# ─────────────────────────────────────────────────────────────────────────────
4141
[workspace.package]
42-
version = "0.2.143"
42+
version = "0.2.144"
4343
edition = "2024"
4444
rust-version = "1.85"
4545
license = "MPL-2.0 OR LicenseRef-UFFS-Commercial"
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# CHANGELOG_HEALING - 2026-01-29 22:00
2+
3+
## Summary
4+
5+
Fix for extension record `$FILE_NAME` merging bug that caused 73,721 missing paths on D-Drive.
6+
7+
## What Failed
8+
9+
**Root Cause**: When a directory or file has so many attributes that the `$FILE_NAME` attribute is pushed to an extension record (not in the base record), the Rust MFT parser was not properly merging the name into the base record.
10+
11+
The bug was in two functions in `crates/uffs-mft/src/io.rs`:
12+
1. `parse_extension_to_index`
13+
2. `parse_extension_to_fragment`
14+
15+
Both functions were:
16+
- Adding extension names to the `links` buffer
17+
- Chaining them to `record.first_name.next_entry`
18+
19+
But they were **NOT** checking if `record.first_name` itself was empty! When the base record had no `$FILE_NAME` attribute, `first_name` remained empty, and the chained names were never used for path resolution.
20+
21+
## The Fix
22+
23+
When processing extension records with `$FILE_NAME` attributes:
24+
25+
1. **Check if base record has no name**: `!record.first_name.name.is_valid()`
26+
2. **If empty**: Copy the first extension name **directly into `first_name`** (not just chain it)
27+
3. **If not empty**: Chain extension names as additional hard links (original behavior)
28+
29+
This matches the C++ behavior in `ntfs_index.hpp` lines 559-567.
30+
31+
## Files Changed
32+
33+
- `crates/uffs-mft/src/io.rs`: Fixed `parse_extension_to_index` and `parse_extension_to_fragment`
34+
35+
## Impact
36+
37+
- Fixes 73,721 missing paths on D-Drive (1.0% of total)
38+
- Caused by ~60 directories and ~341 files having `$FILE_NAME` only in extension records
39+
- All 73 uffs-mft tests pass
40+
- Build succeeds for uffs-mft and uffs-cli
41+
42+
## CI Pipeline Status
43+
44+
### Run 1 - Failed (Clippy errors in test code)
45+
- `$FILE_NAME` should be `` `$FILE_NAME` `` in doc comments
46+
- `"test_directory".to_string()` should be `"test_directory".to_owned()`
47+
- `merged` is too similar to `merger` - renamed to `result` and `record_merger`
48+
49+
### Run 2 - Failed (Borrow checker error in Windows cross-compile)
50+
```
51+
error[E0502]: cannot borrow `*fragment` as mutable because it is also borrowed as immutable
52+
--> crates/uffs-mft/src/io.rs:1719:26
53+
```
54+
**Fix**: Copy values from `fragment.links[...]` to local variables before calling `fragment.get_or_create()`.
55+
56+
### Run 3 - Pending
57+

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Traditional file search tools (including `os.walk`, `FindFirstFile`, etc.) work
2121

2222
**UFFS reads the MFT directly** - once - and queries it in memory using Polars DataFrames. This is like reading the entire phonebook once instead of looking up each name individually.
2323

24-
### Benchmark Results (v0.2.143)
24+
### Benchmark Results (v0.2.144)
2525

2626
| Drive Type | Records | Time | Throughput |
2727
|------------|---------|------|------------|
@@ -33,7 +33,7 @@ Traditional file search tools (including `os.walk`, `FindFirstFile`, etc.) work
3333

3434
| Comparison | Records | Time | Notes |
3535
|------------|---------|------|-------|
36-
| **UFFS v0.2.143** | **18.7 Million** | **~142 seconds** | All disks, fast mode |
36+
| **UFFS v0.2.144** | **18.7 Million** | **~142 seconds** | All disks, fast mode |
3737
| UFFS v0.1.30 | 18.7 Million | ~315 seconds | Baseline |
3838
| Everything | 19 Million | 178 seconds | All disks |
3939
| WizFile | 6.5 Million | 299 seconds | Single HDD |

crates/uffs-mft/src/io.rs

Lines changed: 84 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,38 +1061,65 @@ fn parse_extension_to_index(
10611061
if record_idx != NO_ENTRY {
10621062
let record = &mut index.records[record_idx as usize];
10631063

1064-
// Chain new links to the end of the existing link chain
1064+
// Add new links to the record
10651065
if !link_indices.is_empty() {
1066-
// Find the end of the current link chain
1067-
let last_link_idx = if record.first_name.next_entry != NO_ENTRY {
1068-
let mut idx = record.first_name.next_entry;
1069-
while index.links[idx as usize].next_entry != NO_ENTRY {
1070-
idx = index.links[idx as usize].next_entry;
1066+
// Check if base record has no name (first_name is empty)
1067+
// This happens when the $FILE_NAME attribute is ONLY in extension records
1068+
if !record.first_name.name.is_valid() {
1069+
// Copy the first extension name directly into first_name
1070+
// This matches C++ behavior (ntfs_index.hpp lines 559-567)
1071+
let first_link = &index.links[link_indices[0] as usize];
1072+
record.first_name.name = first_link.name;
1073+
record.first_name.parent_frs = first_link.parent_frs;
1074+
// Don't increment name_count for the first name (it's already counted as 1)
1075+
1076+
// Chain remaining links (if any) to first_name.next_entry
1077+
if link_indices.len() > 1 {
1078+
// Chain the remaining links together
1079+
for i in 1..link_indices.len().saturating_sub(1) {
1080+
let current_idx = link_indices[i] as usize;
1081+
let next_idx = link_indices[i + 1];
1082+
index.links[current_idx].next_entry = next_idx;
1083+
}
1084+
// Attach remaining links to first_name
1085+
let record = &mut index.records[record_idx as usize];
1086+
record.first_name.next_entry = link_indices[1];
1087+
// Update name count for additional links only
1088+
record.name_count += (link_indices.len() - 1) as u16;
10711089
}
1072-
Some(idx)
10731090
} else {
1074-
None
1075-
};
1091+
// Base record already has a name - chain extension names as additional hard
1092+
// links Find the end of the current link chain
1093+
let last_link_idx = if record.first_name.next_entry != NO_ENTRY {
1094+
let mut idx = record.first_name.next_entry;
1095+
while index.links[idx as usize].next_entry != NO_ENTRY {
1096+
idx = index.links[idx as usize].next_entry;
1097+
}
1098+
Some(idx)
1099+
} else {
1100+
None
1101+
};
10761102

1077-
// Chain the new links together
1078-
for i in 0..link_indices.len().saturating_sub(1) {
1079-
let current_idx = link_indices[i] as usize;
1080-
let next_idx = link_indices[i + 1];
1081-
index.links[current_idx].next_entry = next_idx;
1082-
}
1103+
// Chain the new links together
1104+
for i in 0..link_indices.len().saturating_sub(1) {
1105+
let current_idx = link_indices[i] as usize;
1106+
let next_idx = link_indices[i + 1];
1107+
index.links[current_idx].next_entry = next_idx;
1108+
}
10831109

1084-
// Attach to the chain
1085-
if let Some(last_idx) = last_link_idx {
1086-
index.links[last_idx as usize].next_entry = link_indices[0];
1087-
} else {
1088-
// first_name has no next_entry, attach directly
1110+
// Attach to the chain
1111+
if let Some(last_idx) = last_link_idx {
1112+
index.links[last_idx as usize].next_entry = link_indices[0];
1113+
} else {
1114+
// first_name has no next_entry, attach directly
1115+
let record = &mut index.records[record_idx as usize];
1116+
record.first_name.next_entry = link_indices[0];
1117+
}
1118+
1119+
// Update name count
10891120
let record = &mut index.records[record_idx as usize];
1090-
record.first_name.next_entry = link_indices[0];
1121+
record.name_count += link_indices.len() as u16;
10911122
}
1092-
1093-
// Update name count
1094-
let record = &mut index.records[record_idx as usize];
1095-
record.name_count += link_indices.len() as u16;
10961123
}
10971124

10981125
// Chain new streams to the end of the existing stream chain
@@ -1652,9 +1679,10 @@ fn parse_extension_to_fragment(
16521679
}
16531680
}
16541681

1655-
// Get the first_name.next_entry and first_stream.next_entry values
1656-
// before we start modifying things
1682+
// Get the first_name.next_entry, first_stream.next_entry, and first_name
1683+
// validity before we start modifying things
16571684
let record = fragment.get_or_create(base_frs);
1685+
let first_name_valid = record.first_name.name.is_valid();
16581686
let first_name_next = record.first_name.next_entry;
16591687
let first_stream_next = record.first_stream.next_entry;
16601688

@@ -1682,14 +1710,38 @@ fn parse_extension_to_fragment(
16821710

16831711
// Now attach the new links
16841712
if !link_indices.is_empty() {
1685-
if let Some(end_idx) = link_chain_end {
1686-
fragment.links[end_idx as usize].next_entry = link_indices[0];
1713+
// Check if base record has no name (first_name is empty)
1714+
// This happens when the $FILE_NAME attribute is ONLY in extension records
1715+
if !first_name_valid {
1716+
// Copy the first extension name directly into first_name
1717+
// This matches C++ behavior (ntfs_index.hpp lines 559-567)
1718+
// Copy values first to avoid borrow conflict
1719+
let first_link_name = fragment.links[link_indices[0] as usize].name;
1720+
let first_link_parent = fragment.links[link_indices[0] as usize].parent_frs;
1721+
let record = fragment.get_or_create(base_frs);
1722+
record.first_name.name = first_link_name;
1723+
record.first_name.parent_frs = first_link_parent;
1724+
// Don't increment name_count for the first name (it's already counted as 1)
1725+
1726+
// Chain remaining links (if any) to first_name.next_entry
1727+
if link_indices.len() > 1 {
1728+
let record = fragment.get_or_create(base_frs);
1729+
record.first_name.next_entry = link_indices[1];
1730+
// Update name count for additional links only
1731+
record.name_count += (link_indices.len() - 1) as u16;
1732+
}
16871733
} else {
1734+
// Base record already has a name - chain extension names as additional hard
1735+
// links
1736+
if let Some(end_idx) = link_chain_end {
1737+
fragment.links[end_idx as usize].next_entry = link_indices[0];
1738+
} else {
1739+
let record = fragment.get_or_create(base_frs);
1740+
record.first_name.next_entry = link_indices[0];
1741+
}
16881742
let record = fragment.get_or_create(base_frs);
1689-
record.first_name.next_entry = link_indices[0];
1743+
record.name_count += link_indices.len() as u16;
16901744
}
1691-
let record = fragment.get_or_create(base_frs);
1692-
record.name_count += link_indices.len() as u16;
16931745
}
16941746

16951747
// Now attach the new streams

0 commit comments

Comments
 (0)