fast_ntfs 1.0.2

Forked a low-level NTFS filesystem library
Documentation
# 개선·최적화 백로그 (2026-07-13)


`docs/optimization-review.md`의 최적화 패스 이후 남아 있는 개선 여지를 정리한 목록이다.
1차 검토(핫패스 중심)와 2차 검토(나머지 전 파일)를 합쳐 `src/` 아래 모든 소스 파일을
읽었다. 검토 시점 기준 `cargo test --all-features`와
`cargo clippy --all-targets --all-features`는 모두 통과한다.

이 목록은 정적 코드 리뷰로 찾을 수 있는 범위다. fuzzing 장기 실행, 실제 볼륨 대상 실측,
비정상 볼륨 호환성 테스트로만 드러나는 문제는 여기 포함되지 않는다.

우선순위: **H**(정확성·견고성에 영향) > **M**(측정 가능한 성능·API 개선) > **L**(사소한 정리).

## 1. 정확성 / 견고성


### 1-1. [H] `NtfsAttributesRaw::next``data_size` 경계를 검사하지 않음

`src/attribute.rs:832` — 이터레이터가 `items_range.start`만 사용하고 `items_range.end`
(= File Record의 `data_size`, 즉 사용 중인 크기)를 전혀 확인하지 않는다. 속성 길이 검증도
`NtfsAttribute::new`에서 레코드 버퍼 전체 길이(`data.len()`) 기준으로만 수행된다
(`src/attribute.rs:250`). 따라서 End 마커(0xFFFFFFFF)가 `data_size` 밖에 있는 손상된
레코드에서는 사용 영역 밖(할당 영역 안)의 잔여 바이트를 유효한 속성처럼 계속 파싱한다.
메모리 안전성 문제는 아니지만, 초기 구현(20e0600)에 있던 `items_range.is_empty()` 검사가
리팩터링 과정에서 사라진 견고성 퇴행이다.

**개선안**: `next()` 진입 시 `self.items_range`가 비었거나 4바이트 미만이면 `None`을
반환하고, 속성 길이 검증을 `items_range.end` 기준으로 조이기. fuzz 타깃이 이미 있으므로
(`fuzz/`) 해당 케이스를 fuzz corpus/회귀 테스트에 추가.

### 1-2. [H] 데이터 런 경계에 걸친 File Record를 읽지 못함

`src/ntfs.rs:209-256`(`file_position`) — File Record 조회가 "단일 절대 위치"를 반환하고
`NtfsFile::new_with_buffer`(`src/file.rs:160-162`)가 그 위치에서 연속으로 `read_exact`
한다. 클러스터 크기가 `file_record_size`보다 작은 볼륨(예: 512B 클러스터 + 1KiB 레코드)
에서는 레코드 하나가 서로 떨어진 두 MFT 데이터 런에 걸칠 수 있는데,

- 캐시 경로는 `record_end <= run.end` 필터(`src/ntfs.rs:226`)에 걸려
  `InvalidFileRecordNumber` 에러를 반환하고,
- 비캐시 경로는 잘못된(다음 런과 무관한) 연속 바이트를 읽을 수 있다.

인덱스 레코드 쪽은 이미 런을 넘나들며 스티칭한다
(`src/structured_values/index_allocation.rs:110-132`). 같은 방식을 File Record 읽기에도
적용해야 한다.

**개선안**: `file_position`(위치 반환) 대신 "레코드를 버퍼로 읽는" 내부 함수로 바꾸고,
`MftDataRuns`에 `record_from_offset_with_buffer` 형태의 스티칭 읽기를 구현. 아래 3-1의
데이터 런 타입 통합과 함께 처리하면 한 곳만 고치면 된다.

### 1-3. [M] `NtfsVolumeInformation`이 잘못된 속성 타입으로 에러를 보고

`src/structured_values/volume_information.rs:76` — 크기 검증 실패 시
`InvalidStructuredValueSize`의 `ty` 필드에 `NtfsAttributeType::StandardInformation`을
넣는다. `VolumeInformation`이어야 한다. 복사-붙여넣기 실수로, 에러 메시지가 엉뚱한 속성을
가리켜 디버깅을 오도한다. 한 줄 수정.

### 1-4. [M] `NtfsFileReference::to_file`이 시퀀스 번호를 검증하지 않음

`src/file_reference.rs:35-40` — File Reference는 48비트 레코드 번호 + 16비트 시퀀스
번호로 구성되고, 시퀀스 번호는 레코드 재사용(파일 삭제 후 새 파일 생성)을 탐지하기 위해
존재한다. 현재 `to_file`은 레코드 번호만 사용하고 로드된 레코드의
`NtfsFile::sequence_number()`와 비교하지 않는다. 오래된 인덱스 엔트리나 손상된 참조가
"엉뚱한(재사용된) 파일"을 조용히 반환할 수 있다. 업스트림도 동일하지만, 견고성을 내세우는
포크라면 검증(또는 opt-in 검증 API)을 추가할 가치가 있다.

### 1-5. [L] `NtfsFileNameIndex::find`가 비정상 이름을 조용히 `None` 처리

`src/indexes/file_name.rs:33-41` — 이름이 255 UTF-16 code unit을 넘으면 `try_push` 실패로
`None`(= not found)을 반환한다. NTFS 규격상 존재할 수 없는 이름이므로 동작 자체는 맞지만,
"찾지 못함"과 "질의가 유효하지 않음"이 구분되지 않는다. 최소한 문서화하거나
`NtfsError` 반환을 고려.

## 2. 성능


### 2-1. [M] finder 서브노드 캐시 축출이 LRU가 아님

`src/index.rs:396-403` — 캐시가 256개를 넘으면 VCN 정렬 벡터의 맨 앞 또는 맨 뒤 원소를
제거한다. 축출 기준이 "VCN이 가장 작거나 큼"이어서, 매 조회마다 지나는 상위(루트 인접)
노드가 축출되고 차가운 리프 노드가 남을 수 있다. 또한 `Vec::insert`/`remove(0)`는 O(n)
이동이다. 256개 이하의 디렉터리에서는 영향이 없고, 초대형 디렉터리 반복 조회에서만
나타난다.

**개선안**: 각 캐시 항목에 세대 카운터(마지막 사용 시각)를 두고 초과 시 최소 세대 항목을
제거하는 근사 LRU. 정렬 키(`Vec<Vcn>`)와 본체(slab)를 분리하면 이동 비용도 준다.

### 2-2. [M] 축출된 서브노드의 버퍼를 재사용하지 않음

`src/index.rs:391`, `src/index.rs:126-131` — finder가 새 서브노드를 로드할 때 항상
`Vec::new()`로 시작한다(`subnode_entry_ranges`). 캐시가 포화 상태이면 어차피 한 항목을
축출하므로, 축출 항목의 `IndexNodeEntryRanges::into_data()` 버퍼를 회수해
`subnode_entry_ranges_with_buffer`로 넘기면 포화 상태의 조회당 힙 할당을 제거할 수 있다.
축출을 삽입 전에 수행하도록 순서만 바꾸면 된다.

### 2-3. [L] `IndexNodeEntryRanges::entry_index`의 2-pass 파싱

`src/index_entry.rs:431-463` — 정확한 용량 확보를 위해 엔트리를 한 번 세고 다시 파싱해
넣는다. 각 엔트리 헤더를 두 번 읽는 비용이 든다. 첫 pass에서 `(offset, header, position)`
을 바로 push(amortized 재할당)하면 파싱이 절반이 된다. 현재도 노드당 1회 할당이므로
할당 횟수는 동일; 순수 CPU 절감이라 벤치로 확인 후 반영할 것.

### 2-4. [L] `IndexEntryRange`가 엔트리당 약 48바이트를 캐시에 유지

`src/index_entry.rs:66-74` — `Range<usize>`(16B) + 헤더 사본(16B) + `NtfsPosition`(16B).
수천 엔트리 디렉터리를 finder로 캐시하면 노드당 색인 메모리가 눈에 띈다. `u32` 오프셋
+ 길이로 압축하면 절반 이하가 된다. 메모리 민감(no_std) 환경에서만 의미 있음.

### 2-5. [L] 반복 이름 비교 시 질의 문자열 UTF-16 재인코딩

`src/file.rs:246-248`(`NtfsFile::data`) — 속성을 순회하며 매 속성마다
`data_stream_name.encode_utf16()`을 다시 수행한다. `NtfsFileNameIndex::find`
(`src/indexes/file_name.rs:33-47`)처럼 질의를 한 번 대문자화한 버퍼로 만들어
`uppercase_cmp_u16str`로 비교하면 스트림이 많은 파일에서 이득. 파일당 $DATA 속성 수가
보통 적어 우선순위 낮음.

### 2-6. [L] UpcaseTable 128KiB 이중 초기화

`src/upcase_table.rs:63-69` — `vec![U16::new(0); 65536]`로 제로 초기화한 직후 전량을
`read_exact`로 덮어쓴다. zerocopy의 `FromZeros::new_box_zeroed()`를 쓰면 의도가 명확해
지고(calloc 경로), uninit 읽기까지 가면 초기화 자체를 없앨 수 있으나 1회성 128KiB라
실익은 작다.

### 2-7. [L] 비상주 Attribute List 엔트리 순회 시 값 리더 복제

`src/structured_values/attribute_list.rs:146-151`(`next_non_resident`) — 엔트리마다
`value.clone().attach(fs)`로 읽은 뒤 원본을 별도로 `seek`한다. 커밋 11c7650이
`NtfsIndexRecords`에서 제거한 것과 같은 패턴이다. 클론에 힙 할당은 없고 seek도
`SeekFrom::Current` 최적화를 타므로 영향은 작지만, 같은 방식(원본 스트림에서 직접 읽으며
전진)으로 통일할 수 있다.

## 3. 구조 / API


### 3-1. [M] `MftDataRuns``NtfsIndexAllocationDataRuns`의 중복

`src/ntfs.rs:42-53` vs `src/structured_values/index_allocation.rs:36-49` — 두 타입 모두
"논리 오프셋 → (start, end, position) 런 목록 + `partition_point` 조회" 구조로 거의
동일하고, 후자만 런 경계 스티칭 읽기를 구현했다. 공용 `MappedDataRuns` 타입으로 통합하면
1-2 수정을 한 곳에서 해결하고 코드가 준다.

### 3-2. [M] `file_resolver(&'n mut self)`가 리졸버 수명 동안 `Ntfs`를 독점

`src/ntfs.rs:200-207` — 리졸버가 `&'n Ntfs`를 들고 있는 동안 원래의 가변 차용이 살아
있어, 호출자는 리졸버를 버리기 전까지 `ntfs.file()`, `ntfs.volume_name()` 등 어떤 메서드도
직접 호출할 수 없다. `read_mft_runs(&mut self)`를 먼저 요구하고 `file_resolver(&self)`가
캐시 부재 시 에러를 반환하게 하거나, `mft_data_runs`를 `OnceCell`로 바꾸면 `&self`로
만들 수 있다.

### 3-3. [L] `NtfsFileResolver::with_file`이 에러 경로에서 버퍼를 유실

`src/ntfs.rs:73-77` — `file_with_buffer`가 실패하면 넘긴 버퍼가 내부에서 drop되어 다음
호출이 재할당한다. 손상 레코드가 섞인 볼륨을 대량 스캔할 때 조용한 할당 누수가 된다.
내부 API가 에러 시에도 `(NtfsError, Vec<u8>)` 형태로 버퍼를 돌려주게 개선 가능.

### 3-4. [M] 경로 조회 편의 API 부재

`"\dir\sub\file.txt"` 해석을 하려면 사용자가 디렉터리마다 `directory_index` →
`finder` → `find`를 직접 체이닝해야 한다(ntfs-shell 예제가 그렇게 한다). MFT 런 캐시와
버퍼 재사용을 이미 갖춘 `NtfsFileResolver`에 `resolve_path(fs, "a/b/c")` 류의 헬퍼를
얹으면 "빠른 조회"라는 크레이트 목적에 맞는 대표 API가 된다.

### 3-5. [L] 병렬 스캔 지원 검토

`NtfsIndex`가 `RefCell`(`src/index.rs:42`)을 쓰므로 `!Sync`다. 볼륨 전체 스캔을
멀티스레드로 돌리려면 스레드별 `Ntfs`/리더 복제가 필요하다. 장기적으로 읽기 전용 캐시를
`OnceCell`/불변 구조로 바꿔 `Send + Sync` 경계를 명시하는 것을 검토.

## 4. 정리 / 문서 / 테스트


- [M] `fuzz/fuzz_targets/ntfs.rs` — fuzz 타깃이 루트 디렉터리의 raw 속성 64개 순회까지만
  커버한다. 1-1 같은 버그가 있는 경로(디렉터리 인덱스 순회, finder 조회, Attribute List
  평탄화, 구조화 값 파싱)가 fuzz 범위 밖이다. `directory_index``entries`/`find` 경로를
  타깃에 추가할 것.
- [L] `src/structured_values/attribute_list.rs:170`(`next_resident`) — 엔트리 길이가 남은
  슬라이스보다 크면 `?`로 조용히 순회를 끝낸다. 잘린(손상된) 엔트리를 에러 없이 무시하므로
  에러 반환이 더 적절하다.
- [L] `src/indexes/file_name.rs:57``use arrayvec::ArrayVec;`가 파일 맨 끝에 있다.
  상단 import 블록으로 이동.
- [L] `src/attribute.rs:823` — 1-1을 고치지 않더라도, 현재 `items_range.end`는 사용되지
  않는 죽은 값이므로 의도를 명확히 해야 한다.
- [L] `Cargo.toml``homepage`/`repository``github.com/gembleman/ntfs`를 가리킨다.
  크레이트명이 `fast_ntfs`로 바뀌었으므로 실제 저장소 URL과 일치하는지 확인.
- [L] `README.md` 상단 저자 표기가 원저자(Colin Finck)만 있다. 포크 유지자 병기 여부 결정.

## 권장 착수 순서


1. **1-1** (경계 검사 복원) + **1-3** (에러 타입 오기) — 수정이 작고 fuzz/테스트로 바로
   검증 가능. fuzz 타깃 확장(4장)도 이때 같이.
2. **3-1 + 1-2** (데이터 런 타입 통합 + File Record 스티칭 읽기) — 정확성 이슈를 구조
   개선과 함께 해결.
3. **2-2** (축출 버퍼 재사용) → **2-1** (LRU 축출) — 대형 디렉터리 반복 조회 워크로드
   벤치를 추가한 뒤 측정하며 진행.
4. **3-2, 3-4** — API 개선. semver 영향(시그니처 변경)이 있으므로 다음 마이너/메이저
   버전에 묶어서.

## 검증 명령


```sh
cargo test --all-features
cargo clippy --all-targets --all-features -- -D warnings
cargo check --no-default-features
cargo bench --bench allocations
cargo bench --bench performance
```