fast_ntfs 1.0.2

Forked a low-level NTFS filesystem library
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
// Copyright 2021-2026 Colin Finck <colin@reactos.org>
// SPDX-License-Identifier: MIT OR Apache-2.0

use core::mem;

#[cfg(not(feature = "std"))]
use core::cell::OnceCell;
#[cfg(feature = "std")]
use std::sync::OnceLock as OnceCell;

use alloc::vec::Vec;
use zerocopy::FromBytes;

use crate::attribute::NtfsAttributeType;
use crate::attribute_value::NtfsAttributeValue;
use crate::boot_sector::BootSector;
use crate::error::{NtfsError, Result};
use crate::file::{KnownNtfsFileRecordNumber, NtfsFile, NtfsFileRef};
use crate::indexes::NtfsFileNameIndex;
use crate::io::{Read, Seek, SeekFrom};
use crate::mapped_data_runs::MappedDataRuns;
use crate::structured_values::{NtfsVolumeInformation, NtfsVolumeName};
use crate::traits::NtfsReadSeek;
use crate::types::NtfsPosition;
use crate::upcase_table::UpcaseTable;

/// Root structure describing an NTFS filesystem.
#[derive(Debug)]
pub struct Ntfs {
    /// The size of a single cluster, in bytes. This is usually 4096.
    cluster_size: u32,
    /// The size of a single sector, in bytes. This is usually 512.
    sector_size: u16,
    /// Size of the filesystem, in bytes.
    size: u64,
    /// Absolute position of the Master File Table (MFT), in bytes.
    mft_position: NtfsPosition,
    /// Size of a single File Record, in bytes.
    file_record_size: u32,
    /// Cached mapping of logical MFT byte ranges to their absolute filesystem positions.
    mft_data_runs: OnceCell<MappedDataRuns>,
    /// Serial number of the NTFS volume.
    serial_number: u64,
    /// Table of Unicode uppercase characters (only required for case-insensitive comparisons).
    upcase_table: OnceCell<UpcaseTable>,
}

/// Reuses the MFT mapping and one File Record buffer across repeated file lookups.
///
/// Create this through [`Ntfs::file_resolver`]. [`with_file`][Self::with_file] lends each loaded
/// file to a callback and automatically recovers its buffer afterwards.
#[derive(Debug)]
pub struct NtfsFileResolver<'n> {
    ntfs: &'n Ntfs,
    buffer: Vec<u8>,
}

impl<'n> NtfsFileResolver<'n> {
    /// Resolves a root-relative path and returns its [`NtfsFile`].
    ///
    /// Both `/` and `\` are accepted as separators. Leading, repeated, trailing, and `.`
    /// components are ignored. The returned file owns a separately allocated File Record buffer;
    /// use [`with_path`][Self::with_path] to reuse this resolver's buffer for the final file too.
    pub fn resolve_path<T>(&mut self, fs: &mut T, path: &str) -> Result<NtfsFile<'n>>
    where
        T: Read + Seek,
    {
        let file_record_number = self.path_file_record_number(fs, path)?;
        self.ntfs.file(fs, file_record_number)
    }

    /// Resolves a root-relative path and lends the resulting file to `f` while reusing one File
    /// Record buffer for every path component and the final file.
    ///
    /// Both `/` and `\` are accepted as separators. Leading, repeated, trailing, and `.`
    /// components are ignored.
    pub fn with_path<T, F, R>(&mut self, fs: &mut T, path: &str, f: F) -> Result<R>
    where
        T: Read + Seek,
        F: FnOnce(&NtfsFile<'n>, &mut T) -> R,
    {
        let file_record_number = self.path_file_record_number(fs, path)?;
        self.with_file(fs, file_record_number, f)
    }

    /// Loads a file, calls `f` with that file and the filesystem reader, and then recycles the
    /// File Record buffer for the next call.
    pub fn with_file<T, F, R>(&mut self, fs: &mut T, file_record_number: u64, f: F) -> Result<R>
    where
        T: Read + Seek,
        F: FnOnce(&NtfsFile<'n>, &mut T) -> R,
    {
        let buffer = mem::take(&mut self.buffer);
        let file = self.ntfs.file_with_buffer(fs, file_record_number, buffer)?;
        let result = f(&file, fs);
        self.buffer = file.into_buffer();
        Ok(result)
    }

    fn path_file_record_number<T>(&mut self, fs: &mut T, path: &str) -> Result<u64>
    where
        T: Read + Seek,
    {
        let ntfs = self.ntfs;
        let mut file_record_number = KnownNtfsFileRecordNumber::RootDirectory as u64;
        let mut upcase_table_loaded = false;

        for component in path
            .split(['/', '\\'])
            .filter(|component| !component.is_empty() && *component != ".")
        {
            if !upcase_table_loaded {
                ntfs.read_upcase_table(fs)?;
                upcase_table_loaded = true;
            }
            let (position, next_file_record_number) =
                self.with_file(fs, file_record_number, |directory, fs| -> Result<_> {
                    let index = directory.directory_index(fs)?;
                    let mut finder = index.finder();
                    let entry =
                        NtfsFileNameIndex::find(&mut finder, ntfs, fs, component).transpose()?;
                    Ok((
                        directory.position(),
                        entry.map(|entry| entry.file_reference().file_record_number()),
                    ))
                })??;
            file_record_number =
                next_file_record_number.ok_or(NtfsError::PathComponentNotFound { position })?;
        }

        Ok(file_record_number)
    }
}

impl Ntfs {
    /// Creates a new [`Ntfs`] object from a reader and validates its boot sector information.
    ///
    /// The reader must cover the entire NTFS partition, not more and not less.
    /// It will be rewinded to the beginning before reading anything.
    pub fn new<T>(fs: &mut T) -> Result<Self>
    where
        T: Read + Seek,
    {
        // Read and validate the boot sector.
        fs.rewind()?;
        let mut boot_sector_bytes = [0u8; mem::size_of::<BootSector>()];
        fs.read_exact(&mut boot_sector_bytes)?;
        let boot_sector = BootSector::ref_from_bytes(&boot_sector_bytes).unwrap();
        boot_sector.validate()?;

        let bpb = boot_sector.bpb();
        let cluster_size = bpb.cluster_size()?;
        let sector_size = bpb.sector_size()?;
        let total_sectors = bpb.total_sectors();
        let size = total_sectors
            .checked_mul(sector_size as u64)
            .ok_or(NtfsError::TotalSectorsTooBig { total_sectors })?;
        let mft_position = NtfsPosition::none();
        let file_record_size = bpb.file_record_size()?;
        let serial_number = bpb.serial_number();
        let upcase_table = OnceCell::new();

        let mut ntfs = Self {
            cluster_size,
            sector_size,
            size,
            mft_position,
            file_record_size,
            mft_data_runs: OnceCell::new(),
            serial_number,
            upcase_table,
        };
        ntfs.mft_position = bpb.mft_lcn()?.position(&ntfs)?;

        Ok(ntfs)
    }

    /// Creates a new [`Ntfs`] object from a reader, using manually-supplied parameters.
    ///
    /// Unlike [`new`], this function does not validate bootsector information.
    /// Use this function if you're trying to read a damaged NTFS filesystem.
    /// The supplied filesystem size must be nonzero. If an MFT position is supplied, its first
    /// File Record must fit within that size. Omitting the MFT position is permitted, but APIs
    /// that need to read files then return [`NtfsError::MissingMftPosition`].
    ///
    /// The reader must cover the entire NTFS partition, not more and not less.
    /// It will be rewinded to the beginning before reading anything.
    #[allow(clippy::seek_to_start_instead_of_rewind)]
    pub fn new_manual<T>(
        fs: &mut T,
        cluster_size: u32,
        sector_size: u16,
        size: u64,
        mft_position: Option<NtfsPosition>,
        file_record_size: u32,
        serial_number: Option<u64>,
    ) -> Result<Self>
    where
        T: Read + Seek,
    {
        if size == 0 {
            return Err(NtfsError::InvalidFilesystemSize { size });
        }

        let mft_position = mft_position.unwrap_or(NtfsPosition::none());
        if let Some(position) = mft_position.value()
            && position
                .get()
                .checked_add(file_record_size as u64)
                .is_none_or(|end| end > size)
        {
            return Err(NtfsError::MftPositionOutOfBounds {
                position: mft_position,
                file_record_size,
                size,
            });
        }

        // Keep the documented behavior of rewinding the reader without parsing its boot sector.
        fs.seek(SeekFrom::Start(0))?;

        let ntfs = Self {
            cluster_size,
            sector_size,
            size,
            mft_position,
            file_record_size,
            mft_data_runs: OnceCell::new(),
            serial_number: serial_number.unwrap_or_default(),
            upcase_table: OnceCell::new(),
        };

        Ok(ntfs)
    }

    /// Returns the size of a single cluster, in bytes.
    pub fn cluster_size(&self) -> u32 {
        self.cluster_size
    }

    /// Returns the [`NtfsFile`] for the given NTFS File Record Number.
    ///
    /// The first few NTFS files have fixed indexes and contain filesystem
    /// management information (see the [`KnownNtfsFileRecordNumber`] enum).
    pub fn file<'n, T>(&'n self, fs: &mut T, file_record_number: u64) -> Result<NtfsFile<'n>>
    where
        T: Read + Seek,
    {
        self.file_with_buffer(fs, file_record_number, Vec::new())
    }

    /// Returns the [`NtfsFile`] for the given NTFS File Record Number using a caller-provided
    /// reusable buffer.
    ///
    /// The buffer is resized to [`file_record_size`][Self::file_record_size] and fully overwritten.
    /// After processing the file, call [`NtfsFile::into_buffer`] to recover it for the next lookup.
    /// This avoids allocation and zero-initialization when repeatedly opening files.
    pub fn file_with_buffer<'n, T>(
        &'n self,
        fs: &mut T,
        file_record_number: u64,
        buffer: Vec<u8>,
    ) -> Result<NtfsFile<'n>>
    where
        T: Read + Seek,
    {
        self.read_file_with_buffer(fs, file_record_number, buffer)
    }

    /// Applies FILE-record fixups in caller-owned memory and lends an allocation-free view to `f`.
    ///
    /// The original update-sequence trailers are restored when the callback ends, including during
    /// unwinding. Use the owning file APIs when the view reports an `$ATTRIBUTE_LIST` fallback.
    pub fn with_file_record_in_place<F, R>(
        &self,
        record_bytes: &mut [u8],
        position: core::num::NonZeroU64,
        file_record_number: u64,
        f: F,
    ) -> Result<R>
    where
        F: FnOnce(&NtfsFileRef<'_, '_>) -> R,
    {
        if record_bytes.len() != self.file_record_size as usize {
            return Err(NtfsError::BufferTooSmall {
                expected: self.file_record_size as usize,
                actual: record_bytes.len(),
            });
        }
        let file = NtfsFileRef::new(self, position, file_record_number, record_bytes)?;
        Ok(f(&file))
    }

    /// Creates a file resolver for repeated lookups without per-file allocation or MFT reparsing.
    ///
    /// This initializes the MFT data-run cache once and reserves a reusable File Record buffer.
    pub fn file_resolver<'n, T>(&'n self, fs: &mut T) -> Result<NtfsFileResolver<'n>>
    where
        T: Read + Seek,
    {
        self.read_mft_runs(fs)?;
        let buffer = Vec::with_capacity(self.file_record_size as usize);
        Ok(NtfsFileResolver { ntfs: self, buffer })
    }

    fn read_file_with_buffer<'n, T>(
        &'n self,
        fs: &mut T,
        file_record_number: u64,
        mut data: Vec<u8>,
    ) -> Result<NtfsFile<'n>>
    where
        T: Read + Seek,
    {
        let offset = file_record_number
            .checked_mul(self.file_record_size as u64)
            .ok_or(NtfsError::InvalidFileRecordNumber { file_record_number })?;

        data.resize(self.file_record_size as usize, 0);
        let position = if let Some(mft_data_runs) = self.mft_data_runs.get() {
            mft_data_runs
                .read_exact_at(fs, offset, &mut data, || {
                    NtfsError::InvalidFileRecordNumber { file_record_number }
                })?
                .value()
                .ok_or(NtfsError::InvalidFileRecordNumber { file_record_number })?
        } else {
            // The MFT may be split into multiple data runs, referenced by its $DATA attribute.
            // We therefore read it just like any other non-resident attribute value.
            // However, this code assumes that the MFT does not have an Attribute List!
            let mft_position = self
                .mft_position
                .value()
                .ok_or(NtfsError::MissingMftPosition)?;
            let mft = NtfsFile::new(self, fs, mft_position, 0)?;
            let mft_data_attribute =
                mft.find_resident_attribute(NtfsAttributeType::Data, None, None)?;
            let mut mft_data_value = mft_data_attribute.value(fs)?;
            let record_end = offset
                .checked_add(self.file_record_size as u64)
                .filter(|end| *end <= mft_data_value.len())
                .ok_or(NtfsError::InvalidFileRecordNumber { file_record_number })?;

            mft_data_value.seek(fs, SeekFrom::Start(offset))?;
            let position = mft_data_value
                .data_position()
                .value()
                .ok_or(NtfsError::InvalidFileRecordNumber { file_record_number })?;
            mft_data_value.read_exact(fs, &mut data)?;
            debug_assert_eq!(mft_data_value.stream_position(), record_end);
            position
        };

        NtfsFile::from_data(self, position, file_record_number, data)
    }

    /// Reads and caches the data runs of the Master File Table (MFT).
    ///
    /// Calling this once avoids rereading and reparsing the MFT File Record whenever [`file`][Self::file]
    /// opens another file. Repeated calls reuse the existing cache.
    pub fn read_mft_runs<T>(&self, fs: &mut T) -> Result<()>
    where
        T: Read + Seek,
    {
        if self.mft_data_runs.get().is_some() {
            return Ok(());
        }

        let runs = self.load_mft_runs(fs)?;
        // Another thread may have initialized the same immutable cache while this thread was
        // loading it with its own reader. In that case, keep the first completed value.
        let _ = self.mft_data_runs.set(runs);
        Ok(())
    }

    fn load_mft_runs<T>(&self, fs: &mut T) -> Result<MappedDataRuns>
    where
        T: Read + Seek,
    {
        // Read the MFT File Record directly because `file` cannot use the cache until it is built.
        let mft_position = self
            .mft_position
            .value()
            .ok_or(NtfsError::MissingMftPosition)?;
        let mft = NtfsFile::new(self, fs, mft_position, 0)?;
        let mft_data_attribute =
            mft.find_resident_attribute(NtfsAttributeType::Data, None, None)?;
        let value = mft_data_attribute.value(fs)?;
        let data_size = value.len();
        let NtfsAttributeValue::NonResident(value) = value else {
            // MFT data is required to be non-resident. Keep the public error surface consistent
            // with an MFT offset that cannot be resolved to physical storage.
            return Err(NtfsError::InvalidFileRecordNumber {
                file_record_number: 0,
            });
        };

        let mut runs = MappedDataRuns::new(data_size);
        for data_run in value.data_runs() {
            let data_run = data_run?;
            if !runs.push(data_run.allocated_size(), data_run.data_position()) {
                return Err(NtfsError::InvalidFileRecordNumber {
                    file_record_number: 0,
                });
            }
        }
        runs.validate_complete(value.data_position())?;

        Ok(runs)
    }

    /// Returns the size of a File Record of this NTFS filesystem, in bytes.
    pub fn file_record_size(&self) -> u32 {
        self.file_record_size
    }

    /// Returns the absolute byte position of the Master File Table (MFT).
    ///
    /// This [`NtfsPosition`] is nonzero unless [`new_manual`][Self::new_manual] was called without
    /// an MFT position.
    pub fn mft_position(&self) -> NtfsPosition {
        self.mft_position
    }

    /// Reads the $UpCase file from the filesystem and stores it in this [`Ntfs`] object.
    ///
    /// This function only needs to be called if case-insensitive comparisons are later performed
    /// (i.e. finding files).
    pub fn read_upcase_table<T>(&self, fs: &mut T) -> Result<()>
    where
        T: Read + Seek,
    {
        if self.upcase_table.get().is_some() {
            return Ok(());
        }

        let upcase_table = UpcaseTable::read(self, fs)?;
        // Another thread may have initialized the same immutable cache while this thread was
        // loading it with its own reader. In that case, keep the first completed value.
        let _ = self.upcase_table.set(upcase_table);
        Ok(())
    }

    /// Returns the root directory of this NTFS volume as an [`NtfsFile`].
    pub fn root_directory<'n, T>(&'n self, fs: &mut T) -> Result<NtfsFile<'n>>
    where
        T: Read + Seek,
    {
        self.file(fs, KnownNtfsFileRecordNumber::RootDirectory as u64)
    }

    /// Returns the size of a single sector in bytes.
    pub fn sector_size(&self) -> u16 {
        self.sector_size
    }

    /// Returns the 64-bit serial number of this NTFS volume.
    pub fn serial_number(&self) -> u64 {
        self.serial_number
    }

    /// Returns the partition size in bytes.
    pub fn size(&self) -> u64 {
        self.size
    }

    /// Returns the stored [`UpcaseTable`].
    ///
    /// # Panics
    ///
    /// Panics if [`read_upcase_table`][Ntfs::read_upcase_table] had not been called.
    pub(crate) fn upcase_table(&self) -> &UpcaseTable {
        self.upcase_table
            .get()
            .expect("You need to call read_upcase_table first")
    }

    /// Returns an [`NtfsVolumeInformation`] containing general information about
    /// the volume, like the NTFS version.
    pub fn volume_info<T>(&self, fs: &mut T) -> Result<NtfsVolumeInformation>
    where
        T: Read + Seek,
    {
        let volume_file = self.file(fs, KnownNtfsFileRecordNumber::Volume as u64)?;
        volume_file.find_resident_attribute_structured_value::<NtfsVolumeInformation>(None)
    }

    /// Returns an [`NtfsVolumeName`] to read the volume name (also called volume label)
    /// of this NTFS volume.
    ///
    /// Note that a volume may also have no label, which is why the return value is further
    /// encapsulated in an `Option`.
    pub fn volume_name<T>(&self, fs: &mut T) -> Option<Result<NtfsVolumeName>>
    where
        T: Read + Seek,
    {
        let volume_file = iter_try!(self.file(fs, KnownNtfsFileRecordNumber::Volume as u64));

        match volume_file.find_resident_attribute_structured_value::<NtfsVolumeName>(None) {
            Ok(volume_name) => Some(Ok(volume_name)),
            Err(NtfsError::AttributeNotFound { .. }) => None,
            Err(e) => Some(Err(e)),
        }
    }
}