├── .github └── workflows │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples └── ntfs-shell │ ├── main.rs │ └── sector_reader.rs ├── img ├── ntfs-shell.gif ├── ntfs-shell.yml └── ntfs.svg ├── src ├── attribute.rs ├── attribute_value │ ├── attribute_list_non_resident.rs │ ├── mod.rs │ ├── non_resident.rs │ └── resident.rs ├── boot_sector.rs ├── error.rs ├── file.rs ├── file_reference.rs ├── guid.rs ├── helpers.rs ├── index.rs ├── index_entry.rs ├── index_record.rs ├── indexes │ ├── file_name.rs │ └── mod.rs ├── lib.rs ├── ntfs.rs ├── record.rs ├── structured_values │ ├── attribute_list.rs │ ├── file_name.rs │ ├── index_allocation.rs │ ├── index_root.rs │ ├── mod.rs │ ├── object_id.rs │ ├── standard_information.rs │ ├── volume_information.rs │ └── volume_name.rs ├── time.rs ├── traits.rs ├── types.rs └── upcase_table.rs └── testdata ├── create-testfs1.sh └── testfs1 /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Rust Version 20 | run: rustc --version 21 | - name: Format 22 | run: cargo fmt --all -- --check 23 | - name: Clippy 24 | run: cargo clippy --workspace --all-targets --all-features -- -D warnings 25 | - name: Build no_std 26 | run: cargo build --workspace --no-default-features 27 | - name: Build std 28 | run: cargo build --workspace --all-features 29 | - name: Tests 30 | run: cargo test --workspace --all-features 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .vscode 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | 8 | ## [0.4.0] - 2023-06-13 9 | 10 | ### Added 11 | - Added `Display` implementation for all flags structures 12 | 13 | ### Changed 14 | - Replaced `NtfsString` by `U16StrLe` from my new `nt-string` crate 15 | - Replaced abandoned `binread` dependency by its `binrw` successor crate 16 | - Upgraded `bitflags` dependency to 2.3.1 (#28) 17 | Note that this changes the output of the `Debug` implementation of the flags structures. 18 | If you need something similar to the previous output, use the new `Display` implementation instead. 19 | - Upgraded `memoffset` dependency to 0.9.0 (#28) 20 | 21 | ### Fixed 22 | - Fixed broken intra-doc link to `NtfsIndexRoot` 23 | 24 | 25 | ## [0.3.1] - 2023-02-01 26 | 27 | ### Changed 28 | - Made `NtfsError::InvalidUpdateSequenceNumberRange` message more precise 29 | 30 | ### Fixed 31 | - Fixed `Record::update_sequence_array_count` calculation and perform safe subtraction (#25, #26) 32 | 33 | 34 | ## [0.3.0] - 2023-01-25 35 | 36 | ### Added 37 | - Added validation of the attribute length when creating an `NtfsAttribute` object (#23) 38 | - Added tests for resident and non-resident read/seek semantics (#23) 39 | 40 | ### Changed 41 | - Changed `NtfsFile::data` to look up attribute names case-insensitively (#17) 42 | - Changed `NtfsDataRuns::next` to bail out early if the cluster count of a Data Run is zero (#22, #23) 43 | - Changed `NtfsAttributeListNonResidentAttributeValue::seek` and `NtfsNonResidentAttributeValue::seek` to reset the internal `stream_data_run` (and thereby the external `data_position`) to `None` when seeking beyond the valid total length of an attribute (#22, #23) 44 | - Upgraded `memoffset` dependency to 0.8.0 45 | 46 | ### Fixed 47 | - Fixed no_std build and added that to CI 48 | - Fixed out-of-bounds access in `NtfsAttribute::non_resident_value_data_and_position` (#20, #23) 49 | - Fixed unsafe `i8` to `u32` conversion in `BiosParameterBlock::record_size` (#20, #23) 50 | - Fixed out-of-bounds access in `Record::update_sequence_offset` (#20, #23) 51 | - Fixed out-of-bounds access in `NtfsAttributeListEntries::next_resident` (#20, #23) 52 | - Fixed infinite loop in `StreamState::read_data_run` when reading zero bytes (#20, #23) 53 | - Fixed addition overflow in `NtfsAttribute::validate_resident_sizes` (#20, #23) 54 | - Fixed potential panic when reading an attribute list (#23) 55 | - Fixed sparse file / sparse Data Run handling broken in 0.2.0 56 | - Fixed formatting when printing information about sparse Data Runs 57 | - Fixed trivial issues reported by clippy of Rust 1.66.1 58 | - Fixed out-of-bounds access in `Record::fixup` (#24) 59 | 60 | 61 | ## [0.2.0] - 2022-04-14 62 | 63 | ### Added 64 | - Added support for Index Roots that are part of an Attribute List (#7) 65 | - Added support for native sector sizes up to 4K in the BPB and `ntfs-shell`, and use `NTFS_BLOCK_SIZE` instead of the partition's sector size for Record Fixup (#14) 66 | 67 | ### Changed 68 | - Changed `Ntfs::new` to check BPB-reported sizes more thoroughly and output better error messages (#1, #4) 69 | - Tightened the cluster size and record size limits for safety (#2) 70 | - Introduced `NtfsPosition` for all `position` and `data_position` values 71 | - Replaced `chrono` by the better maintained and no_std compatible `time` 72 | - Updated to Rust 2021, MSRV to 1.60, use new features where appropriate 73 | - Upgraded dependencies 74 | 75 | ### Fixed 76 | - Fixed accessing attributes of zero length (#6) 77 | - Fixed reading empty volume name strings (#9) 78 | - Fixed accessing Subnode VCN 0 in an Index Allocation that is part of an Attribute List (#10, #11) 79 | - Fixed handling `sectors_per_cluster` for cluster sizes > 64K, up to 2M (#12, #13) 80 | 81 | 82 | ## [0.1.0] - 2022-01-14 83 | - Initial release 84 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ntfs" 3 | version = "0.4.0" 4 | authors = ["Colin Finck "] 5 | description = "A low-level NTFS filesystem library" 6 | homepage = "https://github.com/ColinFinck/ntfs" 7 | repository = "https://github.com/ColinFinck/ntfs" 8 | documentation = "https://docs.rs/ntfs" 9 | readme = "README.md" 10 | edition = "2021" 11 | rust-version = "1.60" 12 | license = "MIT OR Apache-2.0" 13 | keywords = ["filesystem", "nt", "ntfs", "windows"] 14 | categories = ["filesystem", "no-std", "os::windows-apis", "parser-implementations"] 15 | 16 | [dependencies] 17 | arrayvec = { version = "0.7.2", default-features = false } 18 | binrw = { version = "0.12.0", default-features = false } 19 | byteorder = { version = "1.4.3", default-features = false } 20 | bitflags = "2.3.1" 21 | derive_more = "0.99.17" 22 | displaydoc = { version = "0.2.3", default-features = false } 23 | enumn = "0.1.3" 24 | memoffset = "0.9.0" 25 | nt-string = { version = "0.1.1", features = ["alloc"], default-features = false } 26 | strum_macros = "0.24.0" 27 | time = { version = "0.3.9", features = ["large-dates", "macros"], default-features = false, optional = true } 28 | 29 | [dev-dependencies] 30 | anyhow = "1.0" 31 | time = { version = "0.3.9", features = ["formatting", "large-dates", "macros"], default-features = false } 32 | 33 | [features] 34 | default = ["std"] 35 | std = ["arrayvec/std", "binrw/std", "byteorder/std", "nt-string/std", "time?/std"] 36 | 37 | [[example]] 38 | name = "ntfs-shell" 39 | required-features = ["time"] 40 | 41 | [package.metadata.docs.rs] 42 | all-features = true 43 | rustdoc-args = ["--cfg", "docsrs"] 44 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright 2021 Colin Finck 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # ntfs Rust crate 4 | 5 | [![crates.io](https://img.shields.io/crates/v/ntfs)](https://crates.io/crates/ntfs) 6 | [![docs.rs](https://img.shields.io/docsrs/ntfs)](https://docs.rs/ntfs) 7 | ![license: MIT OR Apache-2.0](https://img.shields.io/crates/l/ntfs) 8 | 9 | *by Colin Finck <>* 10 | 11 | A low-level NTFS filesystem library implemented in Rust. 12 | 13 | [NTFS](https://en.wikipedia.org/wiki/NTFS) is the primary filesystem in all versions of Windows (since Windows NT 3.1 in 1993). 14 | This crate is geared towards the NTFS 3.x versions used in Windows 2000 up to the current Windows 11. 15 | However, the basics are expected to be compatible to even earlier versions. 16 | 17 | The crate is `no_std`-compatible and therefore usable from firmware-level code up to user-mode applications. 18 | 19 | ## ntfs-shell 20 | ![ntfs-shell demo](img/ntfs-shell.gif) 21 | 22 | The `ntfs-shell` example comes with this crate to demonstrate all library features. 23 | Use it to explore the internal structures of an NTFS filesystem at any detail level, even of your running Windows partition. 24 | No artificial security restrictions will block you from accessing files and folders, extracting their data or Alternate Data Streams. 25 | The filesystem is opened read-only, so you can safely browse even a mounted filesystem without worrying about data corruption. 26 | That is also helpful to get an idea of the Windows NTFS driver, e.g. to find out when its lazy writer actually updates the data on disk. 27 | 28 | I originally wrote `ntfs-shell` for myself to comfortably develop the library in user-mode before running the code in production in kernel-mode. 29 | 30 | To build `ntfs-shell`, just clone this repo and call 31 | 32 | ``` 33 | cargo build --example ntfs-shell --all-features 34 | ``` 35 | 36 | To run it, pass the path to an NTFS image (on all operating systems) or to a partition (like `\\.\C:`, on Windows only with administrative privileges) to the resulting `ntfs-shell` binary. 37 | 38 | Calling `help` gives you a list of all supported commands. 39 | `help COMMAND` details the syntax of that command. 40 | 41 | Most commands that take a filename also take an NTFS File Record Number (if prepended by `/`). 42 | This File Record Number may be decimal or hexadecimal (if prepended by `0x`). 43 | Some examples: 44 | 45 | ``` 46 | fileinfo Windows 47 | fileinfo /146810 48 | fileinfo /0x23d7a 49 | ``` 50 | 51 | ## Library Features 52 | * For the impatient: Convenience functions to treat NTFS like any other filesystem and just read files and directories using `Read`/`Seek` traits. 53 | At your option, you may also explore the filesystem at any detail level. 54 | * Reading arbitrary resident and non-resident attributes, attributes in Attribute Lists, and attributes connected over multiple Attribute List entries, including sparse attribute data. 55 | All of this together enables reading file data and Alternate Data Streams of any size and on-disk structure. 56 | * Iterating over a flattened "data-centric" view of the NTFS Attributes, abstracting away any nested Attribute List. 57 | * Efficiently finding files in a directory, adhering to the filesystem's $Upcase Table for case-insensitive search. 58 | * In-order iteration of directory contents at O(1). 59 | * Leveraging Rust's typesystem to handle the various types of NTFS indexes in a typesafe way. 60 | * Error propagation through a custom `NtfsError` type that implements `Display`. 61 | Where it makes sense, variants have additional fields to pinpoint any error to a specific location. 62 | * Full functionality even in a `no_std` environment with `alloc`. 63 | * No usage of `unsafe` anywhere. Checked arithmetic where needed. 64 | * Platform and endian independence. 65 | 66 | ## Not yet supported 67 | * Any write support 68 | * Caching for better performance 69 | * Compression 70 | * Encryption 71 | * Journaling 72 | * Quotas 73 | * Reparse Points 74 | * Security Descriptors 75 | 76 | ## Examples 77 | The following example dumps the names of all files and folders in the root directory of a given NTFS filesystem. 78 | The list is directly taken from the NTFS index, hence it's sorted in ascending order with respect to NTFS's understanding of case-insensitive string comparison. 79 | 80 | ```rust,no_run 81 | let mut ntfs = Ntfs::new(&mut fs).unwrap(); 82 | let root_dir = ntfs.root_directory(&mut fs).unwrap(); 83 | let index = root_dir.directory_index(&mut fs).unwrap(); 84 | let mut iter = index.entries(); 85 | 86 | while let Some(entry) = iter.next(&mut fs) { 87 | let entry = entry.unwrap(); 88 | let file_name = entry.key().unwrap(); 89 | println!("{}", file_name.name()); 90 | } 91 | ``` 92 | 93 | Check out the [docs](https://docs.rs/ntfs), the tests, and the supplied `ntfs-shell` application for more examples on how to use the `ntfs` library. 94 | 95 | ## License 96 | This crate is licensed under either of 97 | 98 | * [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) 99 | * [MIT license](http://opensource.org/licenses/MIT) 100 | 101 | at your option. 102 | 103 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 104 | 105 | ## Further Resources 106 | * [flatcap.github.io linux-ntfs documentation](https://flatcap.github.io/linux-ntfs/ntfs/) 107 | * [ntfs-3g driver](https://github.com/tuxera/ntfs-3g) 108 | -------------------------------------------------------------------------------- /examples/ntfs-shell/sector_reader.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Colin Finck 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use std::io; 5 | use std::io::{Read, Seek, SeekFrom}; 6 | 7 | /// `SectorReader` encapsulates any reader and only performs read and seek operations on it 8 | /// on boundaries of the given sector size. 9 | /// 10 | /// This can be very useful for readers that only accept sector-sized reads (like reading 11 | /// from a raw partition on Windows). 12 | /// The sector size must be a power of two. 13 | /// 14 | /// This reader does not keep any buffer. 15 | /// You are advised to encapsulate `SectorReader` in a buffered reader, as unbuffered reads of 16 | /// just a few bytes here and there are highly inefficient. 17 | pub struct SectorReader 18 | where 19 | R: Read + Seek, 20 | { 21 | /// The inner reader stream. 22 | inner: R, 23 | /// The sector size set at creation. 24 | sector_size: usize, 25 | /// The current stream position as requested by the caller through `read` or `seek`. 26 | /// The implementation will internally make sure to only read/seek on sector boundaries. 27 | stream_position: u64, 28 | /// This buffer is only part of the struct as a small performance optimization (keeping it allocated between reads). 29 | temp_buf: Vec, 30 | } 31 | 32 | impl SectorReader 33 | where 34 | R: Read + Seek, 35 | { 36 | pub fn new(inner: R, sector_size: usize) -> io::Result { 37 | if !sector_size.is_power_of_two() { 38 | return Err(io::Error::new( 39 | io::ErrorKind::Other, 40 | "sector_size is not a power of two", 41 | )); 42 | } 43 | 44 | Ok(Self { 45 | inner, 46 | sector_size, 47 | stream_position: 0, 48 | temp_buf: Vec::new(), 49 | }) 50 | } 51 | 52 | fn align_down_to_sector_size(&self, n: u64) -> u64 { 53 | n / self.sector_size as u64 * self.sector_size as u64 54 | } 55 | 56 | fn align_up_to_sector_size(&self, n: u64) -> u64 { 57 | self.align_down_to_sector_size(n) + self.sector_size as u64 58 | } 59 | } 60 | 61 | impl Read for SectorReader 62 | where 63 | R: Read + Seek, 64 | { 65 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 66 | // We can only read from a sector boundary, and `self.stream_position` specifies the position where the 67 | // caller thinks we are. 68 | // Align down to a sector boundary to determine the position where we really are (see our `seek` implementation). 69 | let aligned_position = self.align_down_to_sector_size(self.stream_position); 70 | 71 | // We have to read more bytes now to make up for the alignment difference. 72 | // We can also only read in multiples of the sector size, so align up to the next sector boundary. 73 | let start = (self.stream_position - aligned_position) as usize; 74 | let end = start + buf.len(); 75 | let aligned_bytes_to_read = self.align_up_to_sector_size(end as u64) as usize; 76 | 77 | // Perform the sector-sized read and copy the actually requested bytes into the given buffer. 78 | self.temp_buf.resize(aligned_bytes_to_read, 0); 79 | self.inner.read_exact(&mut self.temp_buf)?; 80 | buf.copy_from_slice(&self.temp_buf[start..end]); 81 | 82 | // We are done. 83 | self.stream_position += buf.len() as u64; 84 | Ok(buf.len()) 85 | } 86 | } 87 | 88 | impl Seek for SectorReader 89 | where 90 | R: Read + Seek, 91 | { 92 | fn seek(&mut self, pos: SeekFrom) -> io::Result { 93 | let new_pos = match pos { 94 | SeekFrom::Start(n) => Some(n), 95 | SeekFrom::End(_n) => { 96 | // This is unsupported, because it's not safely possible under Windows. 97 | // We cannot seek to the end to determine the raw partition size. 98 | // Which makes it impossible to set `self.stream_position`. 99 | return Err(io::Error::new( 100 | io::ErrorKind::Other, 101 | "SeekFrom::End is unsupported for SectorReader", 102 | )); 103 | } 104 | SeekFrom::Current(n) => { 105 | if n >= 0 { 106 | self.stream_position.checked_add(n as u64) 107 | } else { 108 | self.stream_position.checked_sub(n.wrapping_neg() as u64) 109 | } 110 | } 111 | }; 112 | 113 | match new_pos { 114 | Some(n) => { 115 | // We can only seek on sector boundaries, so align down the requested seek position and seek to that. 116 | let aligned_n = self.align_down_to_sector_size(n); 117 | self.inner.seek(SeekFrom::Start(aligned_n))?; 118 | 119 | // Make the caller believe that we seeked to the actually requested position. 120 | // Our `read` implementation will cover the difference. 121 | self.stream_position = n; 122 | Ok(self.stream_position) 123 | } 124 | None => Err(io::Error::new( 125 | io::ErrorKind::InvalidInput, 126 | "invalid seek to a negative or overflowing position", 127 | )), 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /img/ntfs-shell.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColinFinck/ntfs/cf4c127268cdceb2a9f17503dc3fb014071a386c/img/ntfs-shell.gif -------------------------------------------------------------------------------- /img/ntfs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/attribute_value/attribute_list_non_resident.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 Colin Finck 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | // 4 | // It is important to note that `NtfsAttributeListNonResidentAttributeValue` can't just encapsulate `NtfsNonResidentAttributeValue` and provide one 5 | // layer on top to connect the attributes! 6 | // Connected attributes are stored in a way that the first attribute reports the entire data size and all further attributes report a zero value length. 7 | // We have to go down to the Data Run level to get trustable lengths again, and this is what `NtfsAttributeListNonResidentAttributeValue` does here. 8 | 9 | use binrw::io::{Read, Seek, SeekFrom}; 10 | 11 | use super::{DataRunsState, NtfsDataRuns, StreamState}; 12 | use crate::attribute::{NtfsAttribute, NtfsAttributeType}; 13 | use crate::error::{NtfsError, Result}; 14 | use crate::file::NtfsFile; 15 | use crate::ntfs::Ntfs; 16 | use crate::structured_values::{NtfsAttributeListEntries, NtfsAttributeListEntry}; 17 | use crate::traits::NtfsReadSeek; 18 | use crate::types::NtfsPosition; 19 | 20 | /// Reader for a non-resident attribute value that is part of an Attribute List. 21 | /// 22 | /// Such values are not only split up into data runs, but may also be continued by connected attributes 23 | /// which are listed in the same Attribute List. 24 | /// This reader considers that by providing one contiguous data stream for all data runs in all connected attributes. 25 | #[derive(Clone, Debug)] 26 | pub struct NtfsAttributeListNonResidentAttributeValue<'n, 'f> { 27 | /// Reference to the base `Ntfs` object of this filesystem. 28 | ntfs: &'n Ntfs, 29 | /// An untouched copy of the `attribute_list_entries` passed in [`Self::new`] to rewind to the beginning when desired. 30 | initial_attribute_list_entries: NtfsAttributeListEntries<'n, 'f>, 31 | /// Iterator through all connected attributes of this attribute in the Attribute List. 32 | connected_entries: AttributeListConnectedEntries<'n, 'f>, 33 | /// Total length of the value data, in bytes. 34 | data_size: u64, 35 | /// File, location, and data runs iteration state of the current attribute. 36 | attribute_state: Option>, 37 | /// Iteration state of the current Data Run. 38 | stream_state: StreamState, 39 | } 40 | 41 | impl<'n, 'f> NtfsAttributeListNonResidentAttributeValue<'n, 'f> { 42 | pub(crate) fn new( 43 | ntfs: &'n Ntfs, 44 | fs: &mut T, 45 | attribute_list_entries: NtfsAttributeListEntries<'n, 'f>, 46 | instance: u16, 47 | ty: NtfsAttributeType, 48 | data_size: u64, 49 | ) -> Result 50 | where 51 | T: Read + Seek, 52 | { 53 | let connected_entries = 54 | AttributeListConnectedEntries::new(attribute_list_entries.clone(), instance, ty); 55 | let stream_state = StreamState::new(data_size); 56 | 57 | let mut value = Self { 58 | ntfs, 59 | initial_attribute_list_entries: attribute_list_entries, 60 | connected_entries, 61 | data_size, 62 | attribute_state: None, 63 | stream_state, 64 | }; 65 | value.next_attribute(fs)?; 66 | 67 | Ok(value) 68 | } 69 | 70 | /// Returns the absolute current data seek position within the filesystem, in bytes. 71 | /// This may be `None` if: 72 | /// * The current seek position is outside the valid range, or 73 | /// * The current Data Run is a "sparse" Data Run. 74 | pub fn data_position(&self) -> NtfsPosition { 75 | self.stream_state.data_position() 76 | } 77 | 78 | /// Returns `true` if the non-resident attribute value contains no data. 79 | pub fn is_empty(&self) -> bool { 80 | self.len() == 0 81 | } 82 | 83 | /// Returns the total length of the non-resident attribute value data, in bytes. 84 | pub fn len(&self) -> u64 { 85 | self.data_size 86 | } 87 | 88 | /// Advances to the next Data Run and returns whether we got another Data Run. 89 | fn next_data_run(&mut self) -> Result { 90 | // Do we have a file and a (non-resident) attribute to iterate through its data runs? 91 | let attribute_state = match &mut self.attribute_state { 92 | Some(attribute_state) => attribute_state, 93 | None => return Ok(false), 94 | }; 95 | 96 | // Get the state of the `NtfsDataRuns` iterator of that attribute. 97 | let data_runs_state = match attribute_state.data_runs_state.take() { 98 | Some(data_runs_state) => data_runs_state, 99 | None => return Ok(false), 100 | }; 101 | 102 | // Deserialize the state into an `NtfsDataRuns` iterator. 103 | let attribute = NtfsAttribute::new( 104 | &attribute_state.file, 105 | attribute_state.attribute_offset, 106 | None, 107 | )?; 108 | let (data, position) = attribute.non_resident_value_data_and_position()?; 109 | let mut stream_data_runs = 110 | NtfsDataRuns::from_state(self.ntfs, data, position, data_runs_state); 111 | 112 | // Do we have a next Data Run? Save that. 113 | let stream_data_run = match stream_data_runs.next() { 114 | Some(stream_data_run) => stream_data_run, 115 | None => return Ok(false), 116 | }; 117 | let stream_data_run = stream_data_run?; 118 | self.stream_state.set_stream_data_run(Some(stream_data_run)); 119 | 120 | // We got another Data Run, so serialize the updated `NtfsDataRuns` state for the next iteration. 121 | // This step is skipped when we got no Data Run, because it means we have fully iterated this iterator (and hence also the attribute and file). 122 | attribute_state.data_runs_state = Some(stream_data_runs.into_state()); 123 | 124 | Ok(true) 125 | } 126 | 127 | /// Advances to the next attribute and returns whether we got another connected attribute. 128 | fn next_attribute(&mut self, fs: &mut T) -> Result 129 | where 130 | T: Read + Seek, 131 | { 132 | // Do we have another connected attribute? 133 | let entry = match self.connected_entries.next(fs) { 134 | Some(entry) => entry, 135 | None => return Ok(false), 136 | }; 137 | 138 | // Read the correspoding File Record into an `NtfsFile` and get the corresponding `NtfsAttribute`. 139 | let entry = entry?; 140 | let file = entry.to_file(self.ntfs, fs)?; 141 | let attribute = entry.to_attribute(&file)?; 142 | let attribute_offset = attribute.offset(); 143 | 144 | // Connected attributes must always be non-resident. Verify that. 145 | if attribute.is_resident() { 146 | return Err(NtfsError::UnexpectedResidentAttribute { 147 | position: attribute.position(), 148 | }); 149 | } 150 | 151 | // Get an `NtfsDataRuns` iterator for iterating through the attribute value's data runs. 152 | let (data, position) = attribute.non_resident_value_data_and_position()?; 153 | let mut stream_data_runs = NtfsDataRuns::new(self.ntfs, data, position); 154 | 155 | // Get the first Data Run already here to save time and let `data_position` return something meaningful. 156 | let stream_data_run = match stream_data_runs.next() { 157 | Some(stream_data_run) => stream_data_run, 158 | None => return Ok(false), 159 | }; 160 | let stream_data_run = stream_data_run?; 161 | self.stream_state.set_stream_data_run(Some(stream_data_run)); 162 | 163 | // Store the `NtfsFile` and serialize the `NtfsDataRuns` state for a later iteration. 164 | let data_runs_state = Some(stream_data_runs.into_state()); 165 | self.attribute_state = Some(AttributeState { 166 | file, 167 | attribute_offset, 168 | data_runs_state, 169 | }); 170 | 171 | Ok(true) 172 | } 173 | 174 | /// Returns the [`Ntfs`] object reference associated to this value. 175 | pub fn ntfs(&self) -> &'n Ntfs { 176 | self.ntfs 177 | } 178 | 179 | /// Rewinds this value reader to the very beginning. 180 | fn rewind(&mut self, fs: &mut T) -> Result<()> 181 | where 182 | T: Read + Seek, 183 | { 184 | self.connected_entries.attribute_list_entries = 185 | Some(self.initial_attribute_list_entries.clone()); 186 | self.stream_state = StreamState::new(self.len()); 187 | self.next_attribute(fs)?; 188 | 189 | Ok(()) 190 | } 191 | } 192 | 193 | impl<'n, 'f> NtfsReadSeek for NtfsAttributeListNonResidentAttributeValue<'n, 'f> { 194 | fn read(&mut self, fs: &mut T, buf: &mut [u8]) -> Result 195 | where 196 | T: Read + Seek, 197 | { 198 | let mut bytes_read = 0usize; 199 | 200 | while bytes_read < buf.len() { 201 | // Read from the current Data Run if there is one. 202 | if self.stream_state.read_data_run(fs, buf, &mut bytes_read)? { 203 | // We read something, so check the loop condition again if we need to read more. 204 | continue; 205 | } 206 | 207 | // Move to the next Data Run of the current attribute. 208 | if self.next_data_run()? { 209 | // We got another Data Run of the current attribute, so read again. 210 | continue; 211 | } 212 | 213 | // Move to the first Data Run of the next connected attribute. 214 | if self.next_attribute(fs)? { 215 | // We got another attribute, so read again. 216 | continue; 217 | } else { 218 | // We read everything we could. 219 | break; 220 | } 221 | } 222 | 223 | Ok(bytes_read) 224 | } 225 | 226 | fn seek(&mut self, fs: &mut T, pos: SeekFrom) -> Result 227 | where 228 | T: Read + Seek, 229 | { 230 | let pos = self.stream_state.optimize_seek(pos, self.len())?; 231 | 232 | let mut bytes_left_to_seek = match pos { 233 | SeekFrom::Start(n) => { 234 | self.rewind(fs)?; 235 | n 236 | } 237 | SeekFrom::Current(n) if n >= 0 => n as u64, 238 | _ => unreachable!(), 239 | }; 240 | 241 | while bytes_left_to_seek > 0 { 242 | // Seek inside the current Data Run if there is one. 243 | if self 244 | .stream_state 245 | .seek_data_run(fs, pos, &mut bytes_left_to_seek)? 246 | { 247 | // We have reached our final seek position. 248 | break; 249 | } 250 | 251 | // Move to the next Data Run of the current attribute. 252 | if self.next_data_run()? { 253 | // We got another Data Run of the current attribute, so seek some more. 254 | continue; 255 | } 256 | 257 | // Move to the first Data Run of the next connected attribute. 258 | if self.next_attribute(fs)? { 259 | // We got another connected attribute, so seek some more. 260 | continue; 261 | } else { 262 | // We seeked as far as we could. 263 | self.stream_state.set_stream_data_run(None); 264 | break; 265 | } 266 | } 267 | 268 | match pos { 269 | SeekFrom::Start(n) => self.stream_state.set_stream_position(n), 270 | SeekFrom::Current(n) => self 271 | .stream_state 272 | .set_stream_position(self.stream_position() + n as u64), 273 | _ => unreachable!(), 274 | } 275 | 276 | Ok(self.stream_position()) 277 | } 278 | 279 | fn stream_position(&self) -> u64 { 280 | self.stream_state.stream_position() 281 | } 282 | } 283 | 284 | #[derive(Clone, Debug)] 285 | struct AttributeListConnectedEntries<'n, 'f> { 286 | attribute_list_entries: Option>, 287 | instance: u16, 288 | ty: NtfsAttributeType, 289 | } 290 | 291 | impl<'n, 'f> AttributeListConnectedEntries<'n, 'f> { 292 | fn new( 293 | attribute_list_entries: NtfsAttributeListEntries<'n, 'f>, 294 | instance: u16, 295 | ty: NtfsAttributeType, 296 | ) -> Self { 297 | Self { 298 | attribute_list_entries: Some(attribute_list_entries), 299 | instance, 300 | ty, 301 | } 302 | } 303 | 304 | fn next(&mut self, fs: &mut T) -> Option> 305 | where 306 | T: Read + Seek, 307 | { 308 | let attribute_list_entries = self.attribute_list_entries.as_mut()?; 309 | 310 | let entry = iter_try!(attribute_list_entries.next(fs)?); 311 | if entry.instance() == self.instance && iter_try!(entry.ty()) == self.ty { 312 | Some(Ok(entry)) 313 | } else { 314 | self.attribute_list_entries = None; 315 | None 316 | } 317 | } 318 | } 319 | 320 | #[derive(Clone, Debug)] 321 | struct AttributeState<'n> { 322 | file: NtfsFile<'n>, 323 | attribute_offset: usize, 324 | /// We cannot store an `NtfsDataRuns` here, because it has a reference to the `NtfsFile` that is also stored here. 325 | /// This is why we have to go via `DataRunsState` in an `Option` to take() it and deserialize it into an `NtfsDataRuns` whenever necessary. 326 | data_runs_state: Option, 327 | } 328 | -------------------------------------------------------------------------------- /src/attribute_value/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 Colin Finck 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | // 4 | //! Readers for attribute value types. 5 | 6 | mod attribute_list_non_resident; 7 | mod non_resident; 8 | mod resident; 9 | 10 | pub use attribute_list_non_resident::*; 11 | pub use non_resident::*; 12 | pub use resident::*; 13 | 14 | use binrw::io; 15 | use binrw::io::{Read, Seek, SeekFrom}; 16 | 17 | use crate::error::{NtfsError, Result}; 18 | use crate::traits::NtfsReadSeek; 19 | use crate::types::NtfsPosition; 20 | 21 | /// Reader that abstracts over all attribute value types, returned by [`NtfsAttribute::value`]. 22 | /// 23 | /// [`NtfsAttribute::value`]: crate::NtfsAttribute::value 24 | #[allow(clippy::large_enum_variant)] 25 | #[derive(Clone, Debug)] 26 | pub enum NtfsAttributeValue<'n, 'f> { 27 | /// A resident attribute value (which is entirely contained in the NTFS File Record). 28 | Resident(NtfsResidentAttributeValue<'f>), 29 | /// A non-resident attribute value (whose data is in a cluster range outside the File Record). 30 | NonResident(NtfsNonResidentAttributeValue<'n, 'f>), 31 | /// A non-resident attribute value that is part of an Attribute List (and may span multiple connected attributes). 32 | AttributeListNonResident(NtfsAttributeListNonResidentAttributeValue<'n, 'f>), 33 | } 34 | 35 | impl<'n, 'f> NtfsAttributeValue<'n, 'f> { 36 | /// Returns a variant of this reader that implements [`Read`] and [`Seek`] 37 | /// by mutably borrowing the filesystem reader. 38 | pub fn attach<'a, T>(self, fs: &'a mut T) -> NtfsAttributeValueAttached<'n, 'f, 'a, T> 39 | where 40 | T: Read + Seek, 41 | { 42 | NtfsAttributeValueAttached::new(fs, self) 43 | } 44 | 45 | /// Returns the absolute current data seek position within the filesystem, in bytes. 46 | /// This may be `None` if: 47 | /// * The current seek position is outside the valid range, or 48 | /// * The attribute does not have a Data Run, or 49 | /// * The current Data Run is a "sparse" Data Run. 50 | pub fn data_position(&self) -> NtfsPosition { 51 | match self { 52 | Self::Resident(inner) => inner.data_position(), 53 | Self::NonResident(inner) => inner.data_position(), 54 | Self::AttributeListNonResident(inner) => inner.data_position(), 55 | } 56 | } 57 | 58 | /// Returns `true` if the attribute value contains no data. 59 | pub fn is_empty(&self) -> bool { 60 | self.len() == 0 61 | } 62 | 63 | /// Returns the total length of the attribute value data, in bytes. 64 | pub fn len(&self) -> u64 { 65 | match self { 66 | Self::Resident(inner) => inner.len(), 67 | Self::NonResident(inner) => inner.len(), 68 | Self::AttributeListNonResident(inner) => inner.len(), 69 | } 70 | } 71 | } 72 | 73 | impl<'n, 'f> NtfsReadSeek for NtfsAttributeValue<'n, 'f> { 74 | fn read(&mut self, fs: &mut T, buf: &mut [u8]) -> Result 75 | where 76 | T: Read + Seek, 77 | { 78 | match self { 79 | Self::Resident(inner) => inner.read(fs, buf), 80 | Self::NonResident(inner) => inner.read(fs, buf), 81 | Self::AttributeListNonResident(inner) => inner.read(fs, buf), 82 | } 83 | } 84 | 85 | fn seek(&mut self, fs: &mut T, pos: SeekFrom) -> Result 86 | where 87 | T: Read + Seek, 88 | { 89 | match self { 90 | Self::Resident(inner) => inner.seek(fs, pos), 91 | Self::NonResident(inner) => inner.seek(fs, pos), 92 | Self::AttributeListNonResident(inner) => inner.seek(fs, pos), 93 | } 94 | } 95 | 96 | fn stream_position(&self) -> u64 { 97 | match self { 98 | Self::Resident(inner) => inner.stream_position(), 99 | Self::NonResident(inner) => inner.stream_position(), 100 | Self::AttributeListNonResident(inner) => inner.stream_position(), 101 | } 102 | } 103 | } 104 | 105 | /// A variant of [`NtfsAttributeValue`] that implements [`Read`] and [`Seek`] 106 | /// by mutably borrowing the filesystem reader. 107 | #[derive(Debug)] 108 | pub struct NtfsAttributeValueAttached<'n, 'f, 'a, T: Read + Seek> { 109 | fs: &'a mut T, 110 | value: NtfsAttributeValue<'n, 'f>, 111 | } 112 | 113 | impl<'n, 'f, 'a, T> NtfsAttributeValueAttached<'n, 'f, 'a, T> 114 | where 115 | T: Read + Seek, 116 | { 117 | fn new(fs: &'a mut T, value: NtfsAttributeValue<'n, 'f>) -> Self { 118 | Self { fs, value } 119 | } 120 | 121 | /// Returns the absolute current data seek position within the filesystem, in bytes. 122 | /// This may be `None` if: 123 | /// * The current seek position is outside the valid range, or 124 | /// * The attribute does not have a Data Run, or 125 | /// * The current Data Run is a "sparse" Data Run. 126 | pub fn data_position(&self) -> NtfsPosition { 127 | self.value.data_position() 128 | } 129 | 130 | /// Consumes this reader and returns the inner [`NtfsAttributeValue`]. 131 | pub fn detach(self) -> NtfsAttributeValue<'n, 'f> { 132 | self.value 133 | } 134 | 135 | /// Returns `true` if the attribute value contains no data. 136 | pub fn is_empty(&self) -> bool { 137 | self.len() == 0 138 | } 139 | 140 | /// Returns the total length of the attribute value, in bytes. 141 | pub fn len(&self) -> u64 { 142 | self.value.len() 143 | } 144 | } 145 | 146 | impl<'n, 'f, 'a, T> Read for NtfsAttributeValueAttached<'n, 'f, 'a, T> 147 | where 148 | T: Read + Seek, 149 | { 150 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 151 | self.value.read(self.fs, buf).map_err(io::Error::from) 152 | } 153 | } 154 | 155 | impl<'n, 'f, 'a, T> Seek for NtfsAttributeValueAttached<'n, 'f, 'a, T> 156 | where 157 | T: Read + Seek, 158 | { 159 | fn seek(&mut self, pos: SeekFrom) -> io::Result { 160 | self.value.seek(self.fs, pos).map_err(io::Error::from) 161 | } 162 | } 163 | 164 | pub(crate) fn seek_contiguous( 165 | stream_position: &mut u64, 166 | length: u64, 167 | pos: SeekFrom, 168 | ) -> Result { 169 | // This implementation is taken from https://github.com/rust-lang/rust/blob/18c524fbae3ab1bf6ed9196168d8c68fc6aec61a/library/std/src/io/cursor.rs 170 | // It handles all signed/unsigned arithmetics properly and outputs the known `io` error message. 171 | let (base_pos, offset) = match pos { 172 | SeekFrom::Start(n) => { 173 | *stream_position = n; 174 | return Ok(n); 175 | } 176 | SeekFrom::End(n) => (length, n), 177 | SeekFrom::Current(n) => (*stream_position, n), 178 | }; 179 | 180 | let new_pos = if offset >= 0 { 181 | base_pos.checked_add(offset as u64) 182 | } else { 183 | base_pos.checked_sub(offset.wrapping_neg() as u64) 184 | }; 185 | 186 | match new_pos { 187 | Some(n) => { 188 | *stream_position = n; 189 | Ok(*stream_position) 190 | } 191 | None => Err(NtfsError::Io(io::Error::new( 192 | io::ErrorKind::InvalidInput, 193 | "invalid seek to a negative or overflowing position", 194 | ))), 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/attribute_value/resident.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 Colin Finck 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | // 4 | //! This module implements a reader for a value that is already in memory and can therefore be accessed via a slice. 5 | //! This is the case for all resident attribute values and Index Record values. 6 | //! Such values are part of NTFS records. NTFS records can't be directly read from the filesystem, which is why they 7 | //! are always read into a buffer first and then fixed up in memory. 8 | //! Further accesses to the record data can then happen via slices. 9 | 10 | use binrw::io::{Read, Seek, SeekFrom}; 11 | 12 | use super::seek_contiguous; 13 | use crate::error::Result; 14 | use crate::traits::NtfsReadSeek; 15 | use crate::types::NtfsPosition; 16 | 17 | /// Reader for a value of a resident NTFS Attribute (which is entirely contained in the NTFS File Record). 18 | #[derive(Clone, Debug)] 19 | pub struct NtfsResidentAttributeValue<'f> { 20 | data: &'f [u8], 21 | position: NtfsPosition, 22 | stream_position: u64, 23 | } 24 | 25 | impl<'f> NtfsResidentAttributeValue<'f> { 26 | pub(crate) fn new(data: &'f [u8], position: NtfsPosition) -> Self { 27 | Self { 28 | data, 29 | position, 30 | stream_position: 0, 31 | } 32 | } 33 | 34 | /// Returns a slice of the entire value data. 35 | /// 36 | /// Remember that a resident attribute fits entirely inside the NTFS File Record 37 | /// of the requested file. 38 | /// Hence, the fixed up File Record is entirely in memory at this stage and a slice 39 | /// to a resident attribute value can be obtained easily. 40 | pub fn data(&self) -> &'f [u8] { 41 | self.data 42 | } 43 | 44 | /// Returns the absolute current data seek position within the filesystem, in bytes. 45 | /// This may be `None` if the current seek position is outside the valid range. 46 | pub fn data_position(&self) -> NtfsPosition { 47 | if self.stream_position <= self.len() { 48 | self.position + self.stream_position 49 | } else { 50 | NtfsPosition::none() 51 | } 52 | } 53 | 54 | /// Returns `true` if the resident attribute value contains no data. 55 | pub fn is_empty(&self) -> bool { 56 | self.len() == 0 57 | } 58 | 59 | /// Returns the total length of the resident attribute value data, in bytes. 60 | pub fn len(&self) -> u64 { 61 | self.data.len() as u64 62 | } 63 | 64 | fn remaining_len(&self) -> u64 { 65 | self.len().saturating_sub(self.stream_position) 66 | } 67 | } 68 | 69 | impl<'f> NtfsReadSeek for NtfsResidentAttributeValue<'f> { 70 | fn read(&mut self, _fs: &mut T, buf: &mut [u8]) -> Result 71 | where 72 | T: Read + Seek, 73 | { 74 | if self.remaining_len() == 0 { 75 | return Ok(0); 76 | } 77 | 78 | let bytes_to_read = usize::min(buf.len(), self.remaining_len() as usize); 79 | let work_slice = &mut buf[..bytes_to_read]; 80 | 81 | let start = self.stream_position as usize; 82 | let end = start + bytes_to_read; 83 | work_slice.copy_from_slice(&self.data[start..end]); 84 | 85 | self.stream_position += bytes_to_read as u64; 86 | Ok(bytes_to_read) 87 | } 88 | 89 | fn seek(&mut self, _fs: &mut T, pos: SeekFrom) -> Result 90 | where 91 | T: Read + Seek, 92 | { 93 | let length = self.len(); 94 | seek_contiguous(&mut self.stream_position, length, pos) 95 | } 96 | 97 | fn stream_position(&self) -> u64 { 98 | self.stream_position 99 | } 100 | } 101 | 102 | #[cfg(test)] 103 | mod tests { 104 | use binrw::io::SeekFrom; 105 | 106 | use crate::indexes::NtfsFileNameIndex; 107 | use crate::ntfs::Ntfs; 108 | use crate::traits::NtfsReadSeek; 109 | 110 | #[test] 111 | fn test_read_and_seek() { 112 | let mut testfs1 = crate::helpers::tests::testfs1(); 113 | let mut ntfs = Ntfs::new(&mut testfs1).unwrap(); 114 | ntfs.read_upcase_table(&mut testfs1).unwrap(); 115 | let root_dir = ntfs.root_directory(&mut testfs1).unwrap(); 116 | 117 | // Find the "file-with-12345". 118 | let root_dir_index = root_dir.directory_index(&mut testfs1).unwrap(); 119 | let mut root_dir_finder = root_dir_index.finder(); 120 | let entry = 121 | NtfsFileNameIndex::find(&mut root_dir_finder, &ntfs, &mut testfs1, "file-with-12345") 122 | .unwrap() 123 | .unwrap(); 124 | let file = entry.to_file(&ntfs, &mut testfs1).unwrap(); 125 | 126 | // Get its data attribute. 127 | let data_attribute_item = file.data(&mut testfs1, "").unwrap().unwrap(); 128 | let data_attribute = data_attribute_item.to_attribute().unwrap(); 129 | assert!(data_attribute.is_resident()); 130 | assert_eq!(data_attribute.value_length(), 5); 131 | 132 | let mut data_attribute_value = data_attribute.value(&mut testfs1).unwrap(); 133 | assert_eq!(data_attribute_value.stream_position(), 0); 134 | assert_eq!(data_attribute_value.len(), 5); 135 | 136 | // TEST READING 137 | let data_position_before = data_attribute_value.data_position().value().unwrap(); 138 | 139 | // We have a 6 bytes buffer, but the file is only 5 bytes long. 140 | // The last byte should be untouched. 141 | let mut buf = [0xCCu8; 6]; 142 | let bytes_read = data_attribute_value.read(&mut testfs1, &mut buf).unwrap(); 143 | assert_eq!(bytes_read, 5); 144 | assert_eq!(buf, [b'1', b'2', b'3', b'4', b'5', 0xCC]); 145 | 146 | // The internal position should have stopped directly after the last byte of the file, 147 | // and must also yield a valid data position. 148 | assert_eq!(data_attribute_value.stream_position(), 5); 149 | 150 | let data_position_after = data_attribute_value.data_position().value().unwrap(); 151 | assert_eq!( 152 | data_position_after, 153 | data_position_before.checked_add(5).unwrap() 154 | ); 155 | 156 | // TEST SEEKING 157 | // A seek to the beginning should yield the data position before the read. 158 | data_attribute_value 159 | .seek(&mut testfs1, SeekFrom::Start(0)) 160 | .unwrap(); 161 | assert_eq!(data_attribute_value.stream_position(), 0); 162 | assert_eq!( 163 | data_attribute_value.data_position().value().unwrap(), 164 | data_position_before 165 | ); 166 | 167 | // A seek to one byte after the last read byte should yield the data position 168 | // after the read. 169 | data_attribute_value 170 | .seek(&mut testfs1, SeekFrom::Start(5)) 171 | .unwrap(); 172 | assert_eq!(data_attribute_value.stream_position(), 5); 173 | assert_eq!( 174 | data_attribute_value.data_position().value().unwrap(), 175 | data_position_after 176 | ); 177 | 178 | // A seek beyond the size of the data must yield no valid data position. 179 | data_attribute_value 180 | .seek(&mut testfs1, SeekFrom::Start(6)) 181 | .unwrap(); 182 | assert_eq!(data_attribute_value.stream_position(), 6); 183 | assert_eq!(data_attribute_value.data_position().value(), None); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/boot_sector.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 Colin Finck 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use core::ops::RangeInclusive; 5 | 6 | use binrw::BinRead; 7 | use memoffset::offset_of; 8 | 9 | use crate::error::{NtfsError, Result}; 10 | use crate::types::{Lcn, NtfsPosition}; 11 | 12 | // Sources: 13 | // - https://en.wikipedia.org/wiki/NTFS#Partition_Boot_Sector_(VBR) 14 | // - https://en.wikipedia.org/wiki/BIOS_parameter_block#NTFS 15 | // - https://wiki.osdev.org/NTFS 16 | // - The iBored tool from https://apps.tempel.org/iBored/ 17 | #[allow(unused)] 18 | #[derive(BinRead)] 19 | pub(crate) struct BiosParameterBlock { 20 | sector_size: u16, 21 | sectors_per_cluster: u8, 22 | zeros_1: [u8; 7], 23 | media: u8, 24 | zeros_2: [u8; 2], 25 | dummy_sectors_per_track: u16, 26 | dummy_heads: u16, 27 | hidden_sectors: u32, 28 | zeros_3: u32, 29 | physical_drive_number: u8, 30 | flags: u8, 31 | extended_boot_signature: u8, 32 | reserved: u8, 33 | total_sectors: u64, 34 | mft_lcn: Lcn, 35 | mft_mirror_lcn: Lcn, 36 | file_record_size_info: i8, 37 | zeros_4: [u8; 3], 38 | index_record_size_info: i8, 39 | zeros_5: [u8; 3], 40 | serial_number: u64, 41 | checksum: u32, 42 | } 43 | 44 | impl BiosParameterBlock { 45 | /// Returns the size of a single cluster, in bytes. 46 | pub(crate) fn cluster_size(&self) -> Result { 47 | /// The cluster size cannot go lower than a single sector. 48 | const MIN_CLUSTER_SIZE: u32 = 512; 49 | 50 | /// The maximum cluster size supported by Windows is 2 MiB. 51 | /// Source: https://en.wikipedia.org/wiki/NTFS 52 | const MAX_CLUSTER_SIZE: u32 = 2097152; 53 | 54 | const CLUSTER_SIZE_RANGE: RangeInclusive = MIN_CLUSTER_SIZE..=MAX_CLUSTER_SIZE; 55 | 56 | // `sectors_per_cluster` and `sector_size` both check for powers of two. 57 | // Don't need to do that a third time here. 58 | let cluster_size = self.sectors_per_cluster()? as u32 * self.sector_size()? as u32; 59 | if !CLUSTER_SIZE_RANGE.contains(&cluster_size) { 60 | return Err(NtfsError::UnsupportedClusterSize { 61 | min: MIN_CLUSTER_SIZE, 62 | max: MAX_CLUSTER_SIZE, 63 | actual: cluster_size, 64 | }); 65 | } 66 | 67 | Ok(cluster_size) 68 | } 69 | 70 | pub(crate) fn file_record_size(&self) -> Result { 71 | self.record_size(self.file_record_size_info) 72 | } 73 | 74 | /// Returns the Logical Cluster Number (LCN) to the beginning of the Master File Table (MFT). 75 | pub(crate) fn mft_lcn(&self) -> Result { 76 | if self.mft_lcn.value() > 0 { 77 | Ok(self.mft_lcn) 78 | } else { 79 | Err(NtfsError::InvalidMftLcn) 80 | } 81 | } 82 | 83 | /// Source: https://en.wikipedia.org/wiki/NTFS#Partition_Boot_Sector_(VBR) 84 | fn record_size(&self, size_info: i8) -> Result { 85 | // The usual exponent of `BiosParameterBlock::file_record_size_info` is 10 (2^10 = 1024 bytes). 86 | // For index records, it's usually 12 (2^12 = 4096 bytes). 87 | 88 | /// Exponents < 10 have never been seen and are denied to guarantee that every record header 89 | /// fits into a record. 90 | const MIN_EXPONENT: u32 = 10; 91 | 92 | /// Exponents > 12 have neither been seen and are denied to prevent allocating too large buffers. 93 | const MAX_EXPONENT: u32 = 12; 94 | 95 | const EXPONENT_RANGE: RangeInclusive = MIN_EXPONENT..=MAX_EXPONENT; 96 | 97 | let cluster_size = self.cluster_size()?; 98 | 99 | if size_info > 0 { 100 | // The size field denotes a cluster count. 101 | cluster_size 102 | .checked_mul(size_info as u32) 103 | .ok_or(NtfsError::InvalidRecordSizeInfo { 104 | size_info, 105 | cluster_size, 106 | }) 107 | } else { 108 | // The size field denotes a binary exponent after negation. 109 | let exponent = u32::from(size_info.unsigned_abs()); 110 | 111 | if !EXPONENT_RANGE.contains(&exponent) { 112 | return Err(NtfsError::InvalidRecordSizeInfo { 113 | size_info, 114 | cluster_size, 115 | }); 116 | } 117 | 118 | Ok(1 << exponent) 119 | } 120 | } 121 | 122 | pub(crate) fn sector_size(&self) -> Result { 123 | /// This is the minimum supported by Windows. 124 | /// NTFS-3G also supports 256-byte sectors, but I haven't seen them anywhere. 125 | const MIN_SECTOR_SIZE: u16 = 512; 126 | 127 | /// This is the maximum currently supported by Windows. 128 | /// Tested with Arsenal Image Mounter (https://github.com/ColinFinck/ntfs/issues/14). 129 | const MAX_SECTOR_SIZE: u16 = 4096; 130 | 131 | const SECTOR_SIZE_RANGE: RangeInclusive = MIN_SECTOR_SIZE..=MAX_SECTOR_SIZE; 132 | 133 | if !SECTOR_SIZE_RANGE.contains(&self.sector_size) || !self.sector_size.is_power_of_two() { 134 | return Err(NtfsError::UnsupportedSectorSize { 135 | min: MIN_SECTOR_SIZE, 136 | max: MAX_SECTOR_SIZE, 137 | actual: self.sector_size, 138 | }); 139 | } 140 | 141 | Ok(self.sector_size) 142 | } 143 | 144 | fn sectors_per_cluster(&self) -> Result { 145 | /// We can't go lower than a single sector per cluster. 146 | const MIN_SECTORS_PER_CLUSTER: u8 = 1; 147 | 148 | /// 2^12 = 4096 bytes. With 512 bytes sector size, this translates to 2 MiB cluster size, 149 | /// which is the maximum currently supported by Windows. 150 | const MAX_EXPONENT: i8 = 12; 151 | 152 | // Cluster sizes from 512 to 64K are represented by taking `self.sectors_per_cluster` 153 | // as-is (with possible values 1, 2, 4, 8, 16, 32, 64, 128). 154 | // For larger cluster sizes, `self.sectors_per_cluster` is treated as a binary exponent 155 | // after negation. 156 | // 157 | // See https://dfir.ru/2019/04/23/ntfs-large-clusters/ 158 | if self.sectors_per_cluster > 128 { 159 | let exponent = -(self.sectors_per_cluster as i8); 160 | 161 | if exponent > MAX_EXPONENT { 162 | return Err(NtfsError::InvalidSectorsPerCluster { 163 | sectors_per_cluster: self.sectors_per_cluster, 164 | }); 165 | } 166 | 167 | Ok(1 << (exponent as u16)) 168 | } else { 169 | if self.sectors_per_cluster < MIN_SECTORS_PER_CLUSTER 170 | || !self.sectors_per_cluster.is_power_of_two() 171 | { 172 | return Err(NtfsError::InvalidSectorsPerCluster { 173 | sectors_per_cluster: self.sectors_per_cluster, 174 | }); 175 | } 176 | 177 | Ok(self.sectors_per_cluster as u16) 178 | } 179 | } 180 | 181 | pub(crate) fn serial_number(&self) -> u64 { 182 | self.serial_number 183 | } 184 | 185 | pub(crate) fn total_sectors(&self) -> u64 { 186 | self.total_sectors 187 | } 188 | } 189 | 190 | #[allow(unused)] 191 | #[derive(BinRead)] 192 | pub(crate) struct BootSector { 193 | bootjmp: [u8; 3], 194 | oem_name: [u8; 8], 195 | bpb: BiosParameterBlock, 196 | boot_code: [u8; 426], 197 | signature: [u8; 2], 198 | } 199 | 200 | impl BootSector { 201 | pub(crate) fn bpb(&self) -> &BiosParameterBlock { 202 | &self.bpb 203 | } 204 | 205 | pub(crate) fn validate(&self) -> Result<()> { 206 | // Validate the infamous [0x55, 0xAA] signature at the end of the boot sector. 207 | let expected_signature = &[0x55, 0xAA]; 208 | if &self.signature != expected_signature { 209 | return Err(NtfsError::InvalidTwoByteSignature { 210 | position: NtfsPosition::new(offset_of!(BootSector, signature) as u64), 211 | expected: expected_signature, 212 | actual: self.signature, 213 | }); 214 | } 215 | 216 | Ok(()) 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 Colin Finck 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use core::ops::Range; 5 | 6 | use displaydoc::Display; 7 | 8 | use crate::attribute::NtfsAttributeType; 9 | use crate::types::NtfsPosition; 10 | use crate::types::{Lcn, Vcn}; 11 | 12 | /// Central result type of ntfs. 13 | pub type Result = core::result::Result; 14 | 15 | /// Central error type of ntfs. 16 | #[derive(Debug, Display)] 17 | #[non_exhaustive] 18 | pub enum NtfsError { 19 | /// The NTFS file at byte position {position:#x} has no attribute of type {ty:?}, but it was expected 20 | AttributeNotFound { 21 | position: NtfsPosition, 22 | ty: NtfsAttributeType, 23 | }, 24 | /// The NTFS Attribute at byte position {position:#x} should have type {expected:?}, but it actually has type {actual:?} 25 | AttributeOfDifferentType { 26 | position: NtfsPosition, 27 | expected: NtfsAttributeType, 28 | actual: NtfsAttributeType, 29 | }, 30 | /// The given buffer should have at least {expected} bytes, but it only has {actual} bytes 31 | BufferTooSmall { expected: usize, actual: usize }, 32 | /// The NTFS Attribute at byte position {position:#x} has a length of {expected} bytes, but only {actual} bytes are left in the record 33 | InvalidAttributeLength { 34 | position: NtfsPosition, 35 | expected: usize, 36 | actual: usize, 37 | }, 38 | /// The NTFS Attribute at byte position {position:#x} indicates a name length up to offset {expected}, but the attribute only has a size of {actual} bytes 39 | InvalidAttributeNameLength { 40 | position: NtfsPosition, 41 | expected: usize, 42 | actual: u32, 43 | }, 44 | /// The NTFS Attribute at byte position {position:#x} indicates that its name starts at offset {expected}, but the attribute only has a size of {actual} bytes 45 | InvalidAttributeNameOffset { 46 | position: NtfsPosition, 47 | expected: u16, 48 | actual: u32, 49 | }, 50 | /// The NTFS Data Run header at byte position {position:#x} indicates a maximum byte count of {expected}, but {actual} is the limit 51 | InvalidByteCountInDataRunHeader { 52 | position: NtfsPosition, 53 | expected: u8, 54 | actual: u8, 55 | }, 56 | /// The cluster count {cluster_count} read from the NTFS Data Run header at byte position {position:#x} is invalid 57 | InvalidClusterCountInDataRunHeader { 58 | position: NtfsPosition, 59 | cluster_count: u64, 60 | }, 61 | /// The NTFS File Record at byte position {position:#x} indicates an allocated size of {expected} bytes, but the record only has a size of {actual} bytes 62 | InvalidFileAllocatedSize { 63 | position: NtfsPosition, 64 | expected: u32, 65 | actual: u32, 66 | }, 67 | /// The requested NTFS File Record Number {file_record_number} is invalid 68 | InvalidFileRecordNumber { file_record_number: u64 }, 69 | /// The NTFS File Record at byte position {position:#x} should have signature {expected:?}, but it has signature {actual:?} 70 | InvalidFileSignature { 71 | position: NtfsPosition, 72 | expected: &'static [u8], 73 | actual: [u8; 4], 74 | }, 75 | /// The NTFS File Record at byte position {position:#x} indicates a used size of {expected} bytes, but only {actual} bytes are allocated 76 | InvalidFileUsedSize { 77 | position: NtfsPosition, 78 | expected: u32, 79 | actual: u32, 80 | }, 81 | /// The NTFS Index Record at byte position {position:#x} indicates an allocated size of {expected} bytes, but the record only has a size of {actual} bytes 82 | InvalidIndexAllocatedSize { 83 | position: NtfsPosition, 84 | expected: u32, 85 | actual: u32, 86 | }, 87 | /// The NTFS Index Entry at byte position {position:#x} references a data field in the range {range:?}, but the entry only has a size of {size} bytes 88 | InvalidIndexEntryDataRange { 89 | position: NtfsPosition, 90 | range: Range, 91 | size: u16, 92 | }, 93 | /// The NTFS Index Entry at byte position {position:#x} reports a size of {expected} bytes, but it only has {actual} bytes 94 | InvalidIndexEntrySize { 95 | position: NtfsPosition, 96 | expected: u16, 97 | actual: u16, 98 | }, 99 | /// The NTFS index root at byte position {position:#x} indicates that its entries start at offset {expected}, but the index root only has a size of {actual} bytes 100 | InvalidIndexRootEntriesOffset { 101 | position: NtfsPosition, 102 | expected: usize, 103 | actual: usize, 104 | }, 105 | /// The NTFS index root at byte position {position:#x} indicates a used size up to offset {expected}, but the index root only has a size of {actual} bytes 106 | InvalidIndexRootUsedSize { 107 | position: NtfsPosition, 108 | expected: usize, 109 | actual: usize, 110 | }, 111 | /// The NTFS Index Record at byte position {position:#x} should have signature {expected:?}, but it has signature {actual:?} 112 | InvalidIndexSignature { 113 | position: NtfsPosition, 114 | expected: &'static [u8], 115 | actual: [u8; 4], 116 | }, 117 | /// The NTFS Index Record at byte position {position:#x} indicates a used size of {expected} bytes, but only {actual} bytes are allocated 118 | InvalidIndexUsedSize { 119 | position: NtfsPosition, 120 | expected: u32, 121 | actual: u32, 122 | }, 123 | /// The MFT LCN in the BIOS Parameter Block of the NTFS filesystem is invalid. 124 | InvalidMftLcn, 125 | /// The NTFS Non Resident Value Data at byte position {position:#x} references a data field in the range {range:?}, but the entry only has a size of {size} bytes 126 | InvalidNonResidentValueDataRange { 127 | position: NtfsPosition, 128 | range: Range, 129 | size: usize, 130 | }, 131 | /// The resident NTFS Attribute at byte position {position:#x} indicates a value length of {length} starting at offset {offset}, but the attribute only has a size of {actual} bytes 132 | InvalidResidentAttributeValueLength { 133 | position: NtfsPosition, 134 | length: u32, 135 | offset: u16, 136 | actual: u32, 137 | }, 138 | /// The resident NTFS Attribute at byte position {position:#x} indicates that its value starts at offset {expected}, but the attribute only has a size of {actual} bytes 139 | InvalidResidentAttributeValueOffset { 140 | position: NtfsPosition, 141 | expected: u16, 142 | actual: u32, 143 | }, 144 | /// A record size field in the BIOS Parameter Block denotes {size_info}, which is invalid considering the cluster size of {cluster_size} bytes 145 | InvalidRecordSizeInfo { size_info: i8, cluster_size: u32 }, 146 | /// The sectors per cluster field in the BIOS Parameter Block denotes {sectors_per_cluster:#04x}, which is invalid 147 | InvalidSectorsPerCluster { sectors_per_cluster: u8 }, 148 | /// The NTFS structured value at byte position {position:#x} of type {ty:?} has {actual} bytes where {expected} bytes were expected 149 | InvalidStructuredValueSize { 150 | position: NtfsPosition, 151 | ty: NtfsAttributeType, 152 | expected: u64, 153 | actual: u64, 154 | }, 155 | /// The given time can't be represented as an NtfsTime 156 | InvalidTime, 157 | /// The 2-byte signature field at byte position {position:#x} should contain {expected:?}, but it contains {actual:?} 158 | InvalidTwoByteSignature { 159 | position: NtfsPosition, 160 | expected: &'static [u8], 161 | actual: [u8; 2], 162 | }, 163 | /// The Upcase Table should have a size of {expected} bytes, but it has {actual} bytes 164 | InvalidUpcaseTableSize { expected: u64, actual: u64 }, 165 | /// The NTFS Update Sequence Count of the record at byte position {position:#x} has the invalid value {update_sequence_count} 166 | InvalidUpdateSequenceCount { 167 | position: NtfsPosition, 168 | update_sequence_count: u16, 169 | }, 170 | /// The NTFS Update Sequence Number of the record at byte position {position:#x} references a data field in the range {range:?}, but the entry only has a size of {size} bytes 171 | InvalidUpdateSequenceNumberRange { 172 | position: NtfsPosition, 173 | range: Range, 174 | size: usize, 175 | }, 176 | /// The VCN {vcn} read from the NTFS Data Run header at byte position {position:#x} cannot be added to the LCN {previous_lcn} calculated from previous data runs 177 | InvalidVcnInDataRunHeader { 178 | position: NtfsPosition, 179 | vcn: Vcn, 180 | previous_lcn: Lcn, 181 | }, 182 | /// I/O error: {0:?} 183 | Io(binrw::io::Error), 184 | /// The Logical Cluster Number (LCN) {lcn} is too big to be multiplied by the cluster size 185 | LcnTooBig { lcn: Lcn }, 186 | /// The index root at byte position {position:#x} is a large index, but no matching index allocation attribute was provided 187 | MissingIndexAllocation { position: NtfsPosition }, 188 | /// The NTFS file at byte position {position:#x} is not a directory 189 | NotADirectory { position: NtfsPosition }, 190 | /// The total sector count is too big to be multiplied by the sector size 191 | TotalSectorsTooBig { total_sectors: u64 }, 192 | /// The NTFS Attribute at byte position {position:#x} should not belong to an Attribute List, but it does 193 | UnexpectedAttributeListAttribute { position: NtfsPosition }, 194 | /// The NTFS Attribute at byte position {position:#x} should be resident, but it is non-resident 195 | UnexpectedNonResidentAttribute { position: NtfsPosition }, 196 | /// The NTFS Attribute at byte position {position:#x} should be non-resident, but it is resident 197 | UnexpectedResidentAttribute { position: NtfsPosition }, 198 | /// The type of the NTFS Attribute at byte position {position:#x} is {actual:#010x}, which is not supported 199 | UnsupportedAttributeType { position: NtfsPosition, actual: u32 }, 200 | /// The cluster size is {actual} bytes, but it needs to be between {min} and {max} 201 | UnsupportedClusterSize { min: u32, max: u32, actual: u32 }, 202 | /// The namespace of the NTFS file name starting at byte position {position:#x} is {actual}, which is not supported 203 | UnsupportedFileNamespace { position: NtfsPosition, actual: u8 }, 204 | /// The sector size is {actual} bytes, but it needs to be between {min} and {max} 205 | UnsupportedSectorSize { min: u16, max: u16, actual: u16 }, 206 | /// The Update Sequence Array (USA) of the record at byte position {position:#x} has entries for {array_count} blocks of 512 bytes, but the record is only {record_size} bytes long 207 | UpdateSequenceArrayExceedsRecordSize { 208 | position: NtfsPosition, 209 | array_count: u16, 210 | record_size: usize, 211 | }, 212 | /// Sector corruption: The 2 bytes at byte position {position:#x} should match the Update Sequence Number (USN) {expected:?}, but they are {actual:?} 213 | UpdateSequenceNumberMismatch { 214 | position: NtfsPosition, 215 | expected: [u8; 2], 216 | actual: [u8; 2], 217 | }, 218 | /// The index allocation at byte position {position:#x} references a Virtual Cluster Number (VCN) {expected}, but a record with VCN {actual} is found at that offset 219 | VcnMismatchInIndexAllocation { 220 | position: NtfsPosition, 221 | expected: Vcn, 222 | actual: Vcn, 223 | }, 224 | /// The index allocation at byte position {position:#x} references a Virtual Cluster Number (VCN) {vcn}, but this VCN exceeds the boundaries of the filesystem 225 | VcnOutOfBoundsInIndexAllocation { position: NtfsPosition, vcn: Vcn }, 226 | /// The Virtual Cluster Number (VCN) {vcn} is too big to be multiplied by the cluster size 227 | VcnTooBig { vcn: Vcn }, 228 | } 229 | 230 | impl From for NtfsError { 231 | fn from(error: binrw::error::Error) -> Self { 232 | if let binrw::error::Error::Io(io_error) = error { 233 | Self::Io(io_error) 234 | } else { 235 | // We don't use any binrw attributes that result in other errors. 236 | unreachable!("Got a binrw error of unexpected type: {:?}", error); 237 | } 238 | } 239 | } 240 | 241 | impl From for NtfsError { 242 | fn from(error: binrw::io::Error) -> Self { 243 | Self::Io(error) 244 | } 245 | } 246 | 247 | // To stay compatible with standardized interfaces (e.g. io::Read, io::Seek), 248 | // we sometimes need to convert from NtfsError to io::Error. 249 | impl From for binrw::io::Error { 250 | fn from(error: NtfsError) -> Self { 251 | if let NtfsError::Io(io_error) = error { 252 | io_error 253 | } else { 254 | binrw::io::Error::new(binrw::io::ErrorKind::Other, error) 255 | } 256 | } 257 | } 258 | 259 | #[cfg(feature = "std")] 260 | #[cfg_attr(docsrs, doc(cfg(feature = "std")))] 261 | impl std::error::Error for NtfsError {} 262 | -------------------------------------------------------------------------------- /src/file_reference.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 Colin Finck 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use binrw::io::{Read, Seek}; 5 | use binrw::BinRead; 6 | 7 | use crate::error::Result; 8 | use crate::file::NtfsFile; 9 | use crate::ntfs::Ntfs; 10 | 11 | /// Absolute reference to a File Record on the filesystem, composed out of a File Record Number and a Sequence Number. 12 | /// 13 | /// Reference: 14 | #[derive(BinRead, Clone, Copy, Debug)] 15 | pub struct NtfsFileReference([u8; 8]); 16 | 17 | impl NtfsFileReference { 18 | pub(crate) const fn new(file_reference_bytes: [u8; 8]) -> Self { 19 | Self(file_reference_bytes) 20 | } 21 | 22 | /// Returns the 48-bit File Record Number. 23 | /// 24 | /// This can be fed into [`Ntfs::file`] to create an [`NtfsFile`] object for the corresponding File Record 25 | /// (if you cannot use [`Self::to_file`] for some reason). 26 | pub fn file_record_number(&self) -> u64 { 27 | u64::from_le_bytes(self.0) & 0xffff_ffff_ffff 28 | } 29 | 30 | /// Returns the 16-bit sequence number of the File Record. 31 | /// 32 | /// In a consistent file system, this number matches what [`NtfsFile::sequence_number`] returns. 33 | pub fn sequence_number(&self) -> u16 { 34 | (u64::from_le_bytes(self.0) >> 48) as u16 35 | } 36 | 37 | /// Returns an [`NtfsFile`] for the file referenced by this object. 38 | pub fn to_file<'n, T>(&self, ntfs: &'n Ntfs, fs: &mut T) -> Result> 39 | where 40 | T: Read + Seek, 41 | { 42 | ntfs.file(fs, self.file_record_number()) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/guid.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 Colin Finck 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use core::fmt; 5 | 6 | use binrw::BinRead; 7 | 8 | /// Size of a single GUID on disk (= size of all GUID fields). 9 | pub(crate) const GUID_SIZE: usize = 16; 10 | 11 | /// A Globally Unique Identifier (GUID), used for Object IDs in NTFS. 12 | #[derive(BinRead, Clone, Debug, Eq, PartialEq)] 13 | pub struct NtfsGuid { 14 | pub data1: u32, 15 | pub data2: u16, 16 | pub data3: u16, 17 | pub data4: [u8; 8], 18 | } 19 | 20 | impl fmt::Display for NtfsGuid { 21 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 22 | write!( 23 | f, 24 | "{:8X}-{:4X}-{:4X}-{:2X}{:2X}-{:2X}{:2X}{:2X}{:2X}{:2X}{:2X}", 25 | self.data1, 26 | self.data2, 27 | self.data3, 28 | self.data4[0], 29 | self.data4[1], 30 | self.data4[2], 31 | self.data4[3], 32 | self.data4[4], 33 | self.data4[5], 34 | self.data4[6], 35 | self.data4[7] 36 | ) 37 | } 38 | } 39 | 40 | #[cfg(test)] 41 | mod tests { 42 | use super::*; 43 | 44 | #[test] 45 | fn test_guid() { 46 | let guid = NtfsGuid { 47 | data1: 0x67c8770b, 48 | data2: 0x44f1, 49 | data3: 0x410a, 50 | data4: [0xab, 0x9a, 0xf9, 0xb5, 0x44, 0x6f, 0x13, 0xee], 51 | }; 52 | let guid_string = guid.to_string(); 53 | assert_eq!(guid_string, "67C8770B-44F1-410A-AB9A-F9B5446F13EE"); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/helpers.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Colin Finck 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | macro_rules! iter_try { 5 | ($e:expr) => { 6 | match $e { 7 | Ok(x) => x, 8 | Err(e) => return Some(Err(e.into())), 9 | } 10 | }; 11 | } 12 | 13 | #[cfg(test)] 14 | pub mod tests { 15 | use std::fs::File; 16 | use std::io::{Cursor, Read}; 17 | 18 | pub fn testfs1() -> Cursor> { 19 | let mut buffer = Vec::new(); 20 | File::open("testdata/testfs1") 21 | .unwrap() 22 | .read_to_end(&mut buffer) 23 | .unwrap(); 24 | Cursor::new(buffer) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/index_entry.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 Colin Finck 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use core::iter::FusedIterator; 5 | use core::marker::PhantomData; 6 | use core::ops::Range; 7 | use core::{fmt, mem}; 8 | 9 | use alloc::vec::Vec; 10 | use binrw::io::{Read, Seek}; 11 | use bitflags::bitflags; 12 | use byteorder::{ByteOrder, LittleEndian}; 13 | use memoffset::offset_of; 14 | 15 | use crate::error::{NtfsError, Result}; 16 | use crate::file::NtfsFile; 17 | use crate::file_reference::NtfsFileReference; 18 | use crate::indexes::{ 19 | NtfsIndexEntryData, NtfsIndexEntryHasData, NtfsIndexEntryHasFileReference, NtfsIndexEntryKey, 20 | NtfsIndexEntryType, 21 | }; 22 | use crate::ntfs::Ntfs; 23 | use crate::types::NtfsPosition; 24 | use crate::types::Vcn; 25 | 26 | /// Size of all [`IndexEntryHeader`] fields plus some reserved bytes. 27 | const INDEX_ENTRY_HEADER_SIZE: usize = 16; 28 | 29 | #[repr(C, packed)] 30 | struct IndexEntryHeader { 31 | // The following three fields are used for the u64 file reference if the entry type 32 | // has no data, but a file reference instead. 33 | // This is indicated by the entry type implementing `NtfsIndexEntryHasFileReference`. 34 | // Currently, only `NtfsFileNameIndex` has such a file reference. 35 | data_offset: u16, 36 | data_length: u16, 37 | padding: u32, 38 | 39 | index_entry_length: u16, 40 | key_length: u16, 41 | flags: u8, 42 | } 43 | 44 | bitflags! { 45 | /// Flags returned by [`NtfsIndexEntry::flags`]. 46 | #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 47 | pub struct NtfsIndexEntryFlags: u8 { 48 | /// This Index Entry points to a sub-node. 49 | const HAS_SUBNODE = 0x01; 50 | /// This is the last Index Entry in the list. 51 | const LAST_ENTRY = 0x02; 52 | } 53 | } 54 | 55 | impl fmt::Display for NtfsIndexEntryFlags { 56 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 57 | fmt::Display::fmt(&self.0, f) 58 | } 59 | } 60 | 61 | #[derive(Clone, Debug)] 62 | pub(crate) struct IndexEntryRange 63 | where 64 | E: NtfsIndexEntryType, 65 | { 66 | range: Range, 67 | position: NtfsPosition, 68 | entry_type: PhantomData, 69 | } 70 | 71 | impl IndexEntryRange 72 | where 73 | E: NtfsIndexEntryType, 74 | { 75 | pub(crate) fn new(range: Range, position: NtfsPosition) -> Self { 76 | let entry_type = PhantomData; 77 | Self { 78 | range, 79 | position, 80 | entry_type, 81 | } 82 | } 83 | 84 | pub(crate) fn to_entry<'s>(&self, slice: &'s [u8]) -> Result> { 85 | NtfsIndexEntry::new(&slice[self.range.clone()], self.position) 86 | } 87 | } 88 | 89 | /// A single entry of an NTFS index. 90 | /// 91 | /// NTFS uses B-tree indexes to quickly look up files, Object IDs, Reparse Points, Security Descriptors, etc. 92 | /// They are described via [`NtfsIndexRoot`] and [`NtfsIndexAllocation`] attributes, which can be comfortably 93 | /// accessed via [`NtfsIndex`]. 94 | /// 95 | /// The `E` type parameter of [`NtfsIndexEntryType`] specifies the type of the Index Entry. 96 | /// The most common one is [`NtfsFileNameIndex`] for file name indexes, commonly known as "directories". 97 | /// Check out [`NtfsFile::directory_index`] to return an [`NtfsIndex`] object for a directory without 98 | /// any hassles. 99 | /// 100 | /// Reference: 101 | /// 102 | /// [`NtfsFileNameIndex`]: crate::indexes::NtfsFileNameIndex 103 | /// [`NtfsIndex`]: crate::NtfsIndex 104 | /// [`NtfsIndexAllocation`]: crate::structured_values::NtfsIndexAllocation 105 | /// [`NtfsIndexRoot`]: crate::structured_values::NtfsIndexRoot 106 | #[derive(Clone, Debug)] 107 | pub struct NtfsIndexEntry<'s, E> 108 | where 109 | E: NtfsIndexEntryType, 110 | { 111 | slice: &'s [u8], 112 | position: NtfsPosition, 113 | entry_type: PhantomData, 114 | } 115 | 116 | impl<'s, E> NtfsIndexEntry<'s, E> 117 | where 118 | E: NtfsIndexEntryType, 119 | { 120 | pub(crate) fn new(slice: &'s [u8], position: NtfsPosition) -> Result { 121 | let entry_type = PhantomData; 122 | 123 | let mut entry = Self { 124 | slice, 125 | position, 126 | entry_type, 127 | }; 128 | entry.validate_size()?; 129 | entry.slice = &entry.slice[..entry.index_entry_length() as usize]; 130 | 131 | Ok(entry) 132 | } 133 | 134 | /// Returns the data of this Index Entry, if any and if supported by this Index Entry type. 135 | /// 136 | /// This function is mutually exclusive with [`NtfsIndexEntry::file_reference`]. 137 | /// An Index Entry can either have data or a file reference. 138 | pub fn data(&self) -> Option> 139 | where 140 | E: NtfsIndexEntryHasData, 141 | { 142 | if self.data_offset() == 0 || self.data_length() == 0 { 143 | return None; 144 | } 145 | 146 | let start = self.data_offset() as usize; 147 | let end = start + self.data_length() as usize; 148 | let position = self.position + start; 149 | 150 | let slice = self.slice.get(start..end); 151 | let slice = iter_try!(slice.ok_or(NtfsError::InvalidIndexEntryDataRange { 152 | position: self.position, 153 | range: start..end, 154 | size: self.slice.len() as u16 155 | })); 156 | 157 | let data = iter_try!(E::DataType::data_from_slice(slice, position)); 158 | Some(Ok(data)) 159 | } 160 | 161 | fn data_offset(&self) -> u16 162 | where 163 | E: NtfsIndexEntryHasData, 164 | { 165 | let start = offset_of!(IndexEntryHeader, data_offset); 166 | LittleEndian::read_u16(&self.slice[start..]) 167 | } 168 | 169 | /// Returns the length of the data of this Index Entry (if supported by this Index Entry type). 170 | pub fn data_length(&self) -> u16 171 | where 172 | E: NtfsIndexEntryHasData, 173 | { 174 | let start = offset_of!(IndexEntryHeader, data_length); 175 | LittleEndian::read_u16(&self.slice[start..]) 176 | } 177 | 178 | /// Returns an [`NtfsFileReference`] for the file referenced by this Index Entry 179 | /// (if supported by this Index Entry type). 180 | /// 181 | /// This function is mutually exclusive with [`NtfsIndexEntry::data`]. 182 | /// An Index Entry can either have data or a file reference. 183 | pub fn file_reference(&self) -> NtfsFileReference 184 | where 185 | E: NtfsIndexEntryHasFileReference, 186 | { 187 | // The "file_reference_data" is at the same position as the `data_offset`, `data_length`, and `padding` fields. 188 | // There can either be extra data or a file reference! 189 | NtfsFileReference::new(self.slice[..mem::size_of::()].try_into().unwrap()) 190 | } 191 | 192 | /// Returns flags set for this attribute as specified by [`NtfsIndexEntryFlags`]. 193 | pub fn flags(&self) -> NtfsIndexEntryFlags { 194 | let flags = self.slice[offset_of!(IndexEntryHeader, flags)]; 195 | NtfsIndexEntryFlags::from_bits_truncate(flags) 196 | } 197 | 198 | /// Returns the total length of this Index Entry, in bytes. 199 | /// 200 | /// The next Index Entry is exactly at [`NtfsIndexEntry::position`] + [`NtfsIndexEntry::index_entry_length`] 201 | /// on the filesystem, unless this is the last entry ([`NtfsIndexEntry::flags`] contains 202 | /// [`NtfsIndexEntryFlags::LAST_ENTRY`]). 203 | pub fn index_entry_length(&self) -> u16 { 204 | let start = offset_of!(IndexEntryHeader, index_entry_length); 205 | LittleEndian::read_u16(&self.slice[start..]) 206 | } 207 | 208 | /// Returns the structured value of the key of this Index Entry, 209 | /// or `None` if this Index Entry has no key. 210 | /// 211 | /// The last Index Entry never has a key. 212 | pub fn key(&self) -> Option> { 213 | // The key/stream is only set when the last entry flag is not set. 214 | // https://flatcap.github.io/linux-ntfs/ntfs/concepts/index_entry.html 215 | if self.key_length() == 0 || self.flags().contains(NtfsIndexEntryFlags::LAST_ENTRY) { 216 | return None; 217 | } 218 | 219 | let start = INDEX_ENTRY_HEADER_SIZE; 220 | let end = start + self.key_length() as usize; 221 | let position = self.position + start; 222 | 223 | let slice = self.slice.get(start..end); 224 | let slice = iter_try!(slice.ok_or(NtfsError::InvalidIndexEntryDataRange { 225 | position: self.position, 226 | range: start..end, 227 | size: self.slice.len() as u16 228 | })); 229 | 230 | let key = iter_try!(E::KeyType::key_from_slice(slice, position)); 231 | Some(Ok(key)) 232 | } 233 | 234 | /// Returns the length of the key of this Index Entry. 235 | pub fn key_length(&self) -> u16 { 236 | let start = offset_of!(IndexEntryHeader, key_length); 237 | LittleEndian::read_u16(&self.slice[start..]) 238 | } 239 | 240 | /// Returns the absolute position of this NTFS Index Entry within the filesystem, in bytes. 241 | pub fn position(&self) -> NtfsPosition { 242 | self.position 243 | } 244 | 245 | /// Returns the Virtual Cluster Number (VCN) of the subnode of this Index Entry, 246 | /// or `None` if this Index Entry has no subnode. 247 | pub fn subnode_vcn(&self) -> Option> { 248 | if !self.flags().contains(NtfsIndexEntryFlags::HAS_SUBNODE) { 249 | return None; 250 | } 251 | 252 | // Get the subnode VCN from the very end of the Index Entry, but at least after the header. 253 | let start = usize::max( 254 | self.index_entry_length() as usize - mem::size_of::(), 255 | INDEX_ENTRY_HEADER_SIZE, 256 | ); 257 | let end = start + mem::size_of::(); 258 | 259 | let slice = self.slice.get(start..end); 260 | let slice = iter_try!(slice.ok_or(NtfsError::InvalidIndexEntryDataRange { 261 | position: self.position, 262 | range: start..end, 263 | size: self.slice.len() as u16 264 | })); 265 | 266 | let vcn = Vcn::from(LittleEndian::read_i64(slice)); 267 | Some(Ok(vcn)) 268 | } 269 | 270 | /// Returns an [`NtfsFile`] for the file referenced by this Index Entry. 271 | pub fn to_file<'n, T>(&self, ntfs: &'n Ntfs, fs: &mut T) -> Result> 272 | where 273 | E: NtfsIndexEntryHasFileReference, 274 | T: Read + Seek, 275 | { 276 | self.file_reference().to_file(ntfs, fs) 277 | } 278 | 279 | fn validate_size(&self) -> Result<()> { 280 | if self.slice.len() < INDEX_ENTRY_HEADER_SIZE { 281 | return Err(NtfsError::InvalidIndexEntrySize { 282 | position: self.position, 283 | expected: INDEX_ENTRY_HEADER_SIZE as u16, 284 | actual: self.slice.len() as u16, 285 | }); 286 | } 287 | 288 | if self.index_entry_length() as usize > self.slice.len() { 289 | return Err(NtfsError::InvalidIndexEntrySize { 290 | position: self.position, 291 | expected: self.index_entry_length(), 292 | actual: self.slice.len() as u16, 293 | }); 294 | } 295 | 296 | Ok(()) 297 | } 298 | } 299 | 300 | #[derive(Clone, Debug)] 301 | pub(crate) struct IndexNodeEntryRanges 302 | where 303 | E: NtfsIndexEntryType, 304 | { 305 | data: Vec, 306 | range: Range, 307 | position: NtfsPosition, 308 | entry_type: PhantomData, 309 | } 310 | 311 | impl IndexNodeEntryRanges 312 | where 313 | E: NtfsIndexEntryType, 314 | { 315 | pub(crate) fn new(data: Vec, range: Range, position: NtfsPosition) -> Self { 316 | debug_assert!(range.end <= data.len()); 317 | let entry_type = PhantomData; 318 | 319 | Self { 320 | data, 321 | range, 322 | position, 323 | entry_type, 324 | } 325 | } 326 | 327 | pub(crate) fn data(&self) -> &[u8] { 328 | &self.data 329 | } 330 | } 331 | 332 | impl Iterator for IndexNodeEntryRanges 333 | where 334 | E: NtfsIndexEntryType, 335 | { 336 | type Item = Result>; 337 | 338 | fn next(&mut self) -> Option { 339 | if self.range.is_empty() { 340 | return None; 341 | } 342 | 343 | // Get the current entry. 344 | let start = self.range.start; 345 | let position = self.position; 346 | let entry = iter_try!(NtfsIndexEntry::::new(&self.data[start..], position)); 347 | let end = start + entry.index_entry_length() as usize; 348 | 349 | if entry.flags().contains(NtfsIndexEntryFlags::LAST_ENTRY) { 350 | // This is the last entry. 351 | // Ensure that we don't read any other entries by advancing `self.range.start` to the end. 352 | self.range.start = self.data.len(); 353 | } else { 354 | // This is not the last entry. 355 | // Advance our iterator to the next entry. 356 | self.range.start = end; 357 | self.position += entry.index_entry_length(); 358 | } 359 | 360 | Some(Ok(IndexEntryRange::new(start..end, position))) 361 | } 362 | } 363 | 364 | impl FusedIterator for IndexNodeEntryRanges where E: NtfsIndexEntryType {} 365 | 366 | /// Iterator over 367 | /// all index entries of a single index node, 368 | /// sorted ascending by the index key, 369 | /// returning an [`NtfsIndexEntry`] for each entry. 370 | /// 371 | /// An index node can be an [`NtfsIndexRoot`] attribute or an [`NtfsIndexRecord`] 372 | /// (which comes from an [`NtfsIndexAllocation`] attribute). 373 | /// 374 | /// As such, this iterator is returned from the [`NtfsIndexRoot::entries`] and 375 | /// [`NtfsIndexRecord::entries`] functions. 376 | /// 377 | /// [`NtfsIndexAllocation`]: crate::structured_values::NtfsIndexAllocation 378 | /// [`NtfsIndexRecord`]: crate::NtfsIndexRecord 379 | /// [`NtfsIndexRecord::entries`]: crate::NtfsIndexRecord::entries 380 | /// [`NtfsIndexRoot`]: crate::structured_values::NtfsIndexRoot 381 | /// [`NtfsIndexRoot::entries`]: crate::structured_values::NtfsIndexRoot::entries 382 | #[derive(Clone, Debug)] 383 | pub struct NtfsIndexNodeEntries<'s, E> 384 | where 385 | E: NtfsIndexEntryType, 386 | { 387 | slice: &'s [u8], 388 | position: NtfsPosition, 389 | entry_type: PhantomData, 390 | } 391 | 392 | impl<'s, E> NtfsIndexNodeEntries<'s, E> 393 | where 394 | E: NtfsIndexEntryType, 395 | { 396 | pub(crate) fn new(slice: &'s [u8], position: NtfsPosition) -> Self { 397 | let entry_type = PhantomData; 398 | Self { 399 | slice, 400 | position, 401 | entry_type, 402 | } 403 | } 404 | } 405 | 406 | impl<'s, E> Iterator for NtfsIndexNodeEntries<'s, E> 407 | where 408 | E: NtfsIndexEntryType, 409 | { 410 | type Item = Result>; 411 | 412 | fn next(&mut self) -> Option { 413 | if self.slice.is_empty() { 414 | return None; 415 | } 416 | 417 | // Get the current entry. 418 | let entry = iter_try!(NtfsIndexEntry::new(self.slice, self.position)); 419 | 420 | if entry.flags().contains(NtfsIndexEntryFlags::LAST_ENTRY) { 421 | // This is the last entry. 422 | // Ensure that we don't read any other entries by emptying the slice. 423 | self.slice = &[]; 424 | } else { 425 | // This is not the last entry. 426 | // Advance our iterator to the next entry. 427 | let bytes_to_advance = entry.index_entry_length() as usize; 428 | self.slice = &self.slice[bytes_to_advance..]; 429 | self.position += bytes_to_advance; 430 | } 431 | 432 | Some(Ok(entry)) 433 | } 434 | } 435 | 436 | impl<'s, E> FusedIterator for NtfsIndexNodeEntries<'s, E> where E: NtfsIndexEntryType {} 437 | -------------------------------------------------------------------------------- /src/index_record.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 Colin Finck 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use core::ops::Range; 5 | 6 | use alloc::vec; 7 | use binrw::io::{Read, Seek}; 8 | use byteorder::{ByteOrder, LittleEndian}; 9 | use memoffset::offset_of; 10 | 11 | use crate::attribute_value::NtfsAttributeValue; 12 | use crate::error::{NtfsError, Result}; 13 | use crate::index_entry::{IndexNodeEntryRanges, NtfsIndexNodeEntries}; 14 | use crate::indexes::NtfsIndexEntryType; 15 | use crate::record::Record; 16 | use crate::record::RecordHeader; 17 | use crate::traits::NtfsReadSeek; 18 | use crate::types::{NtfsPosition, Vcn}; 19 | 20 | /// Size of all [`IndexRecordHeader`] fields. 21 | const INDEX_RECORD_HEADER_SIZE: u32 = 24; 22 | 23 | #[repr(C, packed)] 24 | struct IndexRecordHeader { 25 | record_header: RecordHeader, 26 | vcn: i64, 27 | } 28 | 29 | /// Size of all [`IndexNodeHeader`] fields plus some reserved bytes. 30 | pub(crate) const INDEX_NODE_HEADER_SIZE: usize = 16; 31 | 32 | #[repr(C, packed)] 33 | pub(crate) struct IndexNodeHeader { 34 | pub(crate) entries_offset: u32, 35 | pub(crate) index_size: u32, 36 | pub(crate) allocated_size: u32, 37 | pub(crate) flags: u8, 38 | } 39 | 40 | /// A single NTFS Index Record. 41 | /// 42 | /// These records are denoted via an `INDX` signature on the filesystem. 43 | /// 44 | /// NTFS uses B-tree indexes to quickly look up files, Object IDs, Reparse Points, Security Descriptors, etc. 45 | /// An Index Record is further comprised of Index Entries, which contain the actual key/data (see [`NtfsIndexEntry`], 46 | /// iterated via [`NtfsIndexNodeEntries`]). 47 | /// 48 | /// [`NtfsIndexEntry`]: crate::NtfsIndexEntry 49 | /// 50 | /// Reference: 51 | #[derive(Debug)] 52 | pub struct NtfsIndexRecord { 53 | record: Record, 54 | } 55 | 56 | const HAS_SUBNODES_FLAG: u8 = 0x01; 57 | 58 | impl NtfsIndexRecord { 59 | pub(crate) fn new( 60 | fs: &mut T, 61 | mut value: NtfsAttributeValue, 62 | index_record_size: u32, 63 | ) -> Result 64 | where 65 | T: Read + Seek, 66 | { 67 | let data_position = value.data_position(); 68 | 69 | let mut data = vec![0; index_record_size as usize]; 70 | value.read_exact(fs, &mut data)?; 71 | 72 | let mut record = Record::new(data, data_position); 73 | Self::validate_signature(&record)?; 74 | record.fixup()?; 75 | 76 | let index_record = Self { record }; 77 | index_record.validate_sizes()?; 78 | 79 | Ok(index_record) 80 | } 81 | 82 | /// Returns an iterator over all entries of this Index Record (cf. [`NtfsIndexEntry`]). 83 | /// 84 | /// [`NtfsIndexEntry`]: crate::NtfsIndexEntry 85 | pub fn entries(&self) -> Result> 86 | where 87 | E: NtfsIndexEntryType, 88 | { 89 | let (entries_range, position) = self.entries_range_and_position(); 90 | let data = &self.record.data()[entries_range]; 91 | 92 | Ok(NtfsIndexNodeEntries::new(data, position)) 93 | } 94 | 95 | fn entries_range_and_position(&self) -> (Range, NtfsPosition) { 96 | let start = INDEX_RECORD_HEADER_SIZE as usize + self.index_entries_offset() as usize; 97 | let end = INDEX_RECORD_HEADER_SIZE as usize + self.index_data_size() as usize; 98 | let position = self.record.position() + start; 99 | 100 | (start..end, position) 101 | } 102 | 103 | /// Returns whether this index node has sub-nodes. 104 | /// Otherwise, this index node is a leaf node. 105 | pub fn has_subnodes(&self) -> bool { 106 | let start = INDEX_RECORD_HEADER_SIZE as usize + offset_of!(IndexNodeHeader, flags); 107 | let flags = self.record.data()[start]; 108 | (flags & HAS_SUBNODES_FLAG) != 0 109 | } 110 | 111 | /// Returns the allocated size of this NTFS Index Record, in bytes. 112 | pub fn index_allocated_size(&self) -> u32 { 113 | let start = INDEX_RECORD_HEADER_SIZE as usize + offset_of!(IndexNodeHeader, allocated_size); 114 | LittleEndian::read_u32(&self.record.data()[start..]) 115 | } 116 | 117 | /// Returns the size actually used by index data within this NTFS Index Record, in bytes. 118 | pub fn index_data_size(&self) -> u32 { 119 | let start = INDEX_RECORD_HEADER_SIZE as usize + offset_of!(IndexNodeHeader, index_size); 120 | LittleEndian::read_u32(&self.record.data()[start..]) 121 | } 122 | 123 | pub(crate) fn index_entries_offset(&self) -> u32 { 124 | let start = INDEX_RECORD_HEADER_SIZE as usize + offset_of!(IndexNodeHeader, entries_offset); 125 | LittleEndian::read_u32(&self.record.data()[start..]) 126 | } 127 | 128 | pub(crate) fn into_entry_ranges(self) -> IndexNodeEntryRanges 129 | where 130 | E: NtfsIndexEntryType, 131 | { 132 | let (entries_range, position) = self.entries_range_and_position(); 133 | IndexNodeEntryRanges::new(self.record.into_data(), entries_range, position) 134 | } 135 | 136 | fn validate_signature(record: &Record) -> Result<()> { 137 | let signature = &record.signature(); 138 | let expected = b"INDX"; 139 | 140 | if signature == expected { 141 | Ok(()) 142 | } else { 143 | Err(NtfsError::InvalidIndexSignature { 144 | position: record.position(), 145 | expected, 146 | actual: *signature, 147 | }) 148 | } 149 | } 150 | 151 | fn validate_sizes(&self) -> Result<()> { 152 | let index_record_size = self.record.len(); 153 | 154 | // The total size allocated for this Index Record must not be larger than 155 | // the size defined for all index records of this index. 156 | let total_allocated_size = INDEX_RECORD_HEADER_SIZE + self.index_allocated_size(); 157 | if total_allocated_size > index_record_size { 158 | return Err(NtfsError::InvalidIndexAllocatedSize { 159 | position: self.record.position(), 160 | expected: index_record_size, 161 | actual: total_allocated_size, 162 | }); 163 | } 164 | 165 | // Furthermore, the total used size for this Index Record must not be 166 | // larger than the total allocated size. 167 | let total_data_size = INDEX_RECORD_HEADER_SIZE + self.index_data_size(); 168 | if total_data_size > total_allocated_size { 169 | return Err(NtfsError::InvalidIndexUsedSize { 170 | position: self.record.position(), 171 | expected: total_allocated_size, 172 | actual: total_data_size, 173 | }); 174 | } 175 | 176 | Ok(()) 177 | } 178 | 179 | /// Returns the Virtual Cluster Number (VCN) of this Index Record, as reported by the header of this Index Record. 180 | /// 181 | /// This can be used to double-check that an Index Record is the actually requested one. 182 | /// [`NtfsIndexAllocation::record_from_vcn`] uses it for that purpose. 183 | /// 184 | /// [`NtfsIndexAllocation::record_from_vcn`]: crate::structured_values::NtfsIndexAllocation::record_from_vcn 185 | pub fn vcn(&self) -> Vcn { 186 | let start = offset_of!(IndexRecordHeader, vcn); 187 | Vcn::from(LittleEndian::read_i64(&self.record.data()[start..])) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/indexes/file_name.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 Colin Finck 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use binrw::io::{Read, Seek}; 5 | 6 | use crate::error::Result; 7 | use crate::index::NtfsIndexFinder; 8 | use crate::index_entry::NtfsIndexEntry; 9 | use crate::indexes::{NtfsIndexEntryHasFileReference, NtfsIndexEntryType}; 10 | use crate::ntfs::Ntfs; 11 | use crate::structured_values::NtfsFileName; 12 | use crate::upcase_table::UpcaseOrd; 13 | 14 | /// Defines the [`NtfsIndexEntryType`] for filename indexes (commonly known as "directories"). 15 | #[derive(Clone, Copy, Debug)] 16 | pub struct NtfsFileNameIndex; 17 | 18 | impl NtfsFileNameIndex { 19 | /// Finds a file in a filename index by name and returns the [`NtfsIndexEntry`] (if any). 20 | /// The name is compared case-insensitively based on the filesystem's $UpCase table. 21 | /// 22 | /// # Panics 23 | /// 24 | /// Panics if [`read_upcase_table`][Ntfs::read_upcase_table] had not been called on the passed [`Ntfs`] object. 25 | pub fn find<'a, T>( 26 | index_finder: &'a mut NtfsIndexFinder, 27 | ntfs: &Ntfs, 28 | fs: &mut T, 29 | name: &str, 30 | ) -> Option>> 31 | where 32 | T: Read + Seek, 33 | { 34 | // TODO: This always performs a case-insensitive comparison. 35 | // There are some corner cases where NTFS uses case-sensitive filenames. These need to be considered! 36 | index_finder.find(fs, |file_name| name.upcase_cmp(ntfs, &file_name.name())) 37 | } 38 | } 39 | 40 | impl NtfsIndexEntryType for NtfsFileNameIndex { 41 | type KeyType = NtfsFileName; 42 | } 43 | 44 | impl NtfsIndexEntryHasFileReference for NtfsFileNameIndex {} 45 | -------------------------------------------------------------------------------- /src/indexes/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2022 Colin Finck 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | // 4 | //! Various types of NTFS indexes and traits to work with them. 5 | //! 6 | //! Thanks to Rust's typesystem, the traits make using the various types of NTFS indexes (and their distinct key 7 | //! and data types) possible in a typesafe way. 8 | //! 9 | //! NTFS uses B-tree indexes to quickly look up files, Object IDs, Reparse Points, Security Descriptors, etc. 10 | //! They are described via [`NtfsIndexRoot`] and [`NtfsIndexAllocation`] attributes, which can be comfortably 11 | //! accessed via [`NtfsIndex`]. 12 | //! 13 | //! [`NtfsIndex`]: crate::NtfsIndex 14 | //! [`NtfsIndexAllocation`]: crate::structured_values::NtfsIndexAllocation 15 | //! [`NtfsIndexRoot`]: crate::structured_values::NtfsIndexRoot 16 | 17 | mod file_name; 18 | 19 | pub use file_name::*; 20 | 21 | use core::fmt; 22 | 23 | use crate::error::Result; 24 | use crate::types::NtfsPosition; 25 | 26 | /// Trait implemented by structures that describe Index Entry types. 27 | /// 28 | /// See also [`NtfsIndex`] and [`NtfsIndexEntry`], and [`NtfsFileNameIndex`] for the most popular Index Entry type. 29 | /// 30 | /// [`NtfsFileNameIndex`]: crate::indexes::NtfsFileNameIndex 31 | /// [`NtfsIndex`]: crate::NtfsIndex 32 | /// [`NtfsIndexEntry`]: crate::NtfsIndexEntry 33 | pub trait NtfsIndexEntryType: Clone + fmt::Debug { 34 | type KeyType: NtfsIndexEntryKey; 35 | } 36 | 37 | /// Trait implemented by a structure that describes an Index Entry key. 38 | pub trait NtfsIndexEntryKey: fmt::Debug + Sized { 39 | fn key_from_slice(slice: &[u8], position: NtfsPosition) -> Result; 40 | } 41 | 42 | /// Indicates that the Index Entry type has additional data (of [`NtfsIndexEntryData`] datatype). 43 | /// 44 | /// This trait and [`NtfsIndexEntryHasFileReference`] are mutually exclusive. 45 | // TODO: Use negative trait bounds of future Rust to enforce mutual exclusion. 46 | pub trait NtfsIndexEntryHasData: NtfsIndexEntryType { 47 | type DataType: NtfsIndexEntryData; 48 | } 49 | 50 | /// Trait implemented by a structure that describes Index Entry data. 51 | pub trait NtfsIndexEntryData: fmt::Debug + Sized { 52 | fn data_from_slice(slice: &[u8], position: NtfsPosition) -> Result; 53 | } 54 | 55 | /// Indicates that the Index Entry type has a file reference. 56 | /// 57 | /// This trait and [`NtfsIndexEntryHasData`] are mutually exclusive. 58 | // TODO: Use negative trait bounds of future Rust to enforce mutual exclusion. 59 | pub trait NtfsIndexEntryHasFileReference: NtfsIndexEntryType {} 60 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Colin Finck 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | // 4 | //! A low-level NTFS filesystem library implemented in Rust. 5 | //! 6 | //! [NTFS](https://en.wikipedia.org/wiki/NTFS) is the primary filesystem in all versions of Windows (since Windows NT 3.1 in 1993). 7 | //! This crate is geared towards the NTFS 3.x versions used in Windows 2000 up to the current Windows 11. 8 | //! However, the basics are expected to be compatible to even earlier versions. 9 | //! 10 | //! The crate is `no_std`-compatible and therefore usable from firmware level code up to user-mode applications. 11 | //! 12 | //! # Getting started 13 | //! 1. Create an [`Ntfs`] structure from a reader by calling [`Ntfs::new`]. 14 | //! 2. Retrieve the [`NtfsFile`] of the root directory via [`Ntfs::root_directory`]. 15 | //! 3. Dig into its attributes via [`NtfsFile::attributes`], go even deeper via [`NtfsFile::attributes_raw`] or use one of the convenience functions, like [`NtfsFile::directory_index`], [`NtfsFile::info`] or [`NtfsFile::name`]. 16 | //! 17 | //! # Example 18 | //! The following example dumps the names of all files and folders in the root directory of a given NTFS filesystem. 19 | //! The list is directly taken from the NTFS index, hence it's sorted in ascending order with respect to NTFS's understanding of case-insensitive string comparison. 20 | //! 21 | //! ```ignore 22 | //! let mut ntfs = Ntfs::new(&mut fs).unwrap(); 23 | //! let root_dir = ntfs.root_directory(&mut fs).unwrap(); 24 | //! let index = root_dir.directory_index(&mut fs).unwrap(); 25 | //! let mut iter = index.entries(); 26 | //! 27 | //! while let Some(entry) = iter.next(&mut fs) { 28 | //! let entry = entry.unwrap(); 29 | //! let file_name = entry.key().unwrap(); 30 | //! println!("{}", file_name.name()); 31 | //! } 32 | //! ``` 33 | //! 34 | //! Check out the [docs](https://docs.rs/ntfs), the tests, and the supplied [`ntfs-shell`](https://github.com/ColinFinck/ntfs/tree/master/examples/ntfs-shell) application for more examples on how to use the `ntfs` library. 35 | 36 | #![cfg_attr(not(feature = "std"), no_std)] 37 | #![cfg_attr(docsrs, feature(doc_cfg))] 38 | #![forbid(unsafe_code)] 39 | 40 | extern crate alloc; 41 | 42 | #[macro_use] 43 | mod helpers; 44 | 45 | mod attribute; 46 | pub mod attribute_value; 47 | mod boot_sector; 48 | mod error; 49 | mod file; 50 | mod file_reference; 51 | mod guid; 52 | mod index; 53 | mod index_entry; 54 | mod index_record; 55 | pub mod indexes; 56 | mod ntfs; 57 | mod record; 58 | pub mod structured_values; 59 | mod time; 60 | mod traits; 61 | pub mod types; 62 | mod upcase_table; 63 | 64 | pub use crate::attribute::*; 65 | pub use crate::error::*; 66 | pub use crate::file::*; 67 | pub use crate::file_reference::*; 68 | pub use crate::guid::*; 69 | pub use crate::index::*; 70 | pub use crate::index_entry::*; 71 | pub use crate::index_record::*; 72 | pub use crate::ntfs::*; 73 | pub use crate::time::*; 74 | pub use crate::traits::*; 75 | pub use crate::upcase_table::*; 76 | -------------------------------------------------------------------------------- /src/ntfs.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 Colin Finck 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use binrw::io::{Read, Seek, SeekFrom}; 5 | use binrw::BinReaderExt; 6 | 7 | use crate::attribute::NtfsAttributeType; 8 | use crate::boot_sector::BootSector; 9 | use crate::error::{NtfsError, Result}; 10 | use crate::file::{KnownNtfsFileRecordNumber, NtfsFile}; 11 | use crate::structured_values::{NtfsVolumeInformation, NtfsVolumeName}; 12 | use crate::traits::NtfsReadSeek; 13 | use crate::types::NtfsPosition; 14 | use crate::upcase_table::UpcaseTable; 15 | 16 | /// Root structure describing an NTFS filesystem. 17 | #[derive(Debug)] 18 | pub struct Ntfs { 19 | /// The size of a single cluster, in bytes. This is usually 4096. 20 | cluster_size: u32, 21 | /// The size of a single sector, in bytes. This is usually 512. 22 | sector_size: u16, 23 | /// Size of the filesystem, in bytes. 24 | size: u64, 25 | /// Absolute position of the Master File Table (MFT), in bytes. 26 | mft_position: NtfsPosition, 27 | /// Size of a single File Record, in bytes. 28 | file_record_size: u32, 29 | /// Serial number of the NTFS volume. 30 | serial_number: u64, 31 | /// Table of Unicode uppercase characters (only required for case-insensitive comparisons). 32 | upcase_table: Option, 33 | } 34 | 35 | impl Ntfs { 36 | /// Creates a new [`Ntfs`] object from a reader and validates its boot sector information. 37 | /// 38 | /// The reader must cover the entire NTFS partition, not more and not less. 39 | /// It will be rewinded to the beginning before reading anything. 40 | #[allow(clippy::seek_to_start_instead_of_rewind)] 41 | pub fn new(fs: &mut T) -> Result 42 | where 43 | T: Read + Seek, 44 | { 45 | // Read and validate the boot sector. 46 | fs.seek(SeekFrom::Start(0))?; 47 | let boot_sector = fs.read_le::()?; 48 | boot_sector.validate()?; 49 | 50 | let bpb = boot_sector.bpb(); 51 | let cluster_size = bpb.cluster_size()?; 52 | let sector_size = bpb.sector_size()?; 53 | let total_sectors = bpb.total_sectors(); 54 | let size = total_sectors 55 | .checked_mul(sector_size as u64) 56 | .ok_or(NtfsError::TotalSectorsTooBig { total_sectors })?; 57 | let mft_position = NtfsPosition::none(); 58 | let file_record_size = bpb.file_record_size()?; 59 | let serial_number = bpb.serial_number(); 60 | let upcase_table = None; 61 | 62 | let mut ntfs = Self { 63 | cluster_size, 64 | sector_size, 65 | size, 66 | mft_position, 67 | file_record_size, 68 | serial_number, 69 | upcase_table, 70 | }; 71 | ntfs.mft_position = bpb.mft_lcn()?.position(&ntfs)?; 72 | 73 | Ok(ntfs) 74 | } 75 | 76 | /// Returns the size of a single cluster, in bytes. 77 | pub fn cluster_size(&self) -> u32 { 78 | self.cluster_size 79 | } 80 | 81 | /// Returns the [`NtfsFile`] for the given NTFS File Record Number. 82 | /// 83 | /// The first few NTFS files have fixed indexes and contain filesystem 84 | /// management information (see the [`KnownNtfsFileRecordNumber`] enum). 85 | pub fn file<'n, T>(&'n self, fs: &mut T, file_record_number: u64) -> Result> 86 | where 87 | T: Read + Seek, 88 | { 89 | let offset = file_record_number 90 | .checked_mul(self.file_record_size as u64) 91 | .ok_or(NtfsError::InvalidFileRecordNumber { file_record_number })?; 92 | 93 | // The MFT may be split into multiple data runs, referenced by its $DATA attribute. 94 | // We therefore read it just like any other non-resident attribute value. 95 | // However, this code assumes that the MFT does not have an Attribute List! 96 | // 97 | // This unwrap is safe, because `self.mft_position` has been checked in `Ntfs::new`. 98 | let mft = NtfsFile::new(self, fs, self.mft_position.value().unwrap(), 0)?; 99 | let mft_data_attribute = 100 | mft.find_resident_attribute(NtfsAttributeType::Data, None, None)?; 101 | let mut mft_data_value = mft_data_attribute.value(fs)?; 102 | 103 | mft_data_value.seek(fs, SeekFrom::Start(offset))?; 104 | let position = mft_data_value 105 | .data_position() 106 | .value() 107 | .ok_or(NtfsError::InvalidFileRecordNumber { file_record_number })?; 108 | 109 | NtfsFile::new(self, fs, position, file_record_number) 110 | } 111 | 112 | /// Returns the size of a File Record of this NTFS filesystem, in bytes. 113 | pub fn file_record_size(&self) -> u32 { 114 | self.file_record_size 115 | } 116 | 117 | /// Returns the absolute byte position of the Master File Table (MFT). 118 | /// 119 | /// This [`NtfsPosition`] is guaranteed to be nonzero. 120 | pub fn mft_position(&self) -> NtfsPosition { 121 | self.mft_position 122 | } 123 | 124 | /// Reads the $UpCase file from the filesystem and stores it in this [`Ntfs`] object. 125 | /// 126 | /// This function only needs to be called if case-insensitive comparisons are later performed 127 | /// (i.e. finding files). 128 | pub fn read_upcase_table(&mut self, fs: &mut T) -> Result<()> 129 | where 130 | T: Read + Seek, 131 | { 132 | let upcase_table = UpcaseTable::read(self, fs)?; 133 | self.upcase_table = Some(upcase_table); 134 | Ok(()) 135 | } 136 | 137 | /// Returns the root directory of this NTFS volume as an [`NtfsFile`]. 138 | pub fn root_directory<'n, T>(&'n self, fs: &mut T) -> Result> 139 | where 140 | T: Read + Seek, 141 | { 142 | self.file(fs, KnownNtfsFileRecordNumber::RootDirectory as u64) 143 | } 144 | 145 | /// Returns the size of a single sector in bytes. 146 | pub fn sector_size(&self) -> u16 { 147 | self.sector_size 148 | } 149 | 150 | /// Returns the 64-bit serial number of this NTFS volume. 151 | pub fn serial_number(&self) -> u64 { 152 | self.serial_number 153 | } 154 | 155 | /// Returns the partition size in bytes. 156 | pub fn size(&self) -> u64 { 157 | self.size 158 | } 159 | 160 | /// Returns the stored [`UpcaseTable`]. 161 | /// 162 | /// # Panics 163 | /// 164 | /// Panics if [`read_upcase_table`][Ntfs::read_upcase_table] had not been called. 165 | pub(crate) fn upcase_table(&self) -> &UpcaseTable { 166 | self.upcase_table 167 | .as_ref() 168 | .expect("You need to call read_upcase_table first") 169 | } 170 | 171 | /// Returns an [`NtfsVolumeInformation`] containing general information about 172 | /// the volume, like the NTFS version. 173 | pub fn volume_info(&self, fs: &mut T) -> Result 174 | where 175 | T: Read + Seek, 176 | { 177 | let volume_file = self.file(fs, KnownNtfsFileRecordNumber::Volume as u64)?; 178 | volume_file.find_resident_attribute_structured_value::(None) 179 | } 180 | 181 | /// Returns an [`NtfsVolumeName`] to read the volume name (also called volume label) 182 | /// of this NTFS volume. 183 | /// 184 | /// Note that a volume may also have no label, which is why the return value is further 185 | /// encapsulated in an `Option`. 186 | pub fn volume_name(&self, fs: &mut T) -> Option> 187 | where 188 | T: Read + Seek, 189 | { 190 | let volume_file = iter_try!(self.file(fs, KnownNtfsFileRecordNumber::Volume as u64)); 191 | 192 | match volume_file.find_resident_attribute_structured_value::(None) { 193 | Ok(volume_name) => Some(Ok(volume_name)), 194 | Err(NtfsError::AttributeNotFound { .. }) => None, 195 | Err(e) => Some(Err(e)), 196 | } 197 | } 198 | } 199 | 200 | #[cfg(test)] 201 | mod tests { 202 | use super::*; 203 | 204 | #[test] 205 | fn test_basics() { 206 | let mut testfs1 = crate::helpers::tests::testfs1(); 207 | let ntfs = Ntfs::new(&mut testfs1).unwrap(); 208 | assert_eq!(ntfs.cluster_size(), 512); 209 | assert_eq!(ntfs.sector_size(), 512); 210 | assert_eq!(ntfs.size(), 2096640); 211 | } 212 | 213 | #[test] 214 | fn test_volume_info() { 215 | let mut testfs1 = crate::helpers::tests::testfs1(); 216 | let ntfs = Ntfs::new(&mut testfs1).unwrap(); 217 | let volume_info = ntfs.volume_info(&mut testfs1).unwrap(); 218 | assert_eq!(volume_info.major_version(), 3); 219 | assert_eq!(volume_info.minor_version(), 1); 220 | } 221 | 222 | #[test] 223 | fn test_volume_name() { 224 | let mut testfs1 = crate::helpers::tests::testfs1(); 225 | let ntfs = Ntfs::new(&mut testfs1).unwrap(); 226 | let volume_name = ntfs.volume_name(&mut testfs1).unwrap().unwrap(); 227 | assert_eq!(volume_name.name_length(), 14); 228 | assert_eq!(volume_name.name(), "mylabel"); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/record.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 Colin Finck 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use core::mem; 5 | 6 | use alloc::vec::Vec; 7 | use byteorder::{ByteOrder, LittleEndian}; 8 | use memoffset::{offset_of, span_of}; 9 | 10 | use crate::error::{NtfsError, Result}; 11 | use crate::types::NtfsPosition; 12 | 13 | const NTFS_BLOCK_SIZE: usize = 512; 14 | 15 | #[repr(C, packed)] 16 | pub(crate) struct RecordHeader { 17 | signature: [u8; 4], 18 | update_sequence_offset: u16, 19 | update_sequence_count: u16, 20 | logfile_sequence_number: u64, 21 | } 22 | 23 | #[derive(Clone, Debug)] 24 | pub(crate) struct Record { 25 | data: Vec, 26 | position: NtfsPosition, 27 | } 28 | 29 | impl Record { 30 | pub(crate) fn new(data: Vec, position: NtfsPosition) -> Self { 31 | Self { data, position } 32 | } 33 | 34 | pub(crate) fn data(&self) -> &[u8] { 35 | &self.data 36 | } 37 | 38 | pub(crate) fn fixup(&mut self) -> Result<()> { 39 | let update_sequence_number = self.update_sequence_number()?; 40 | let array_count = self.update_sequence_array_count()?; 41 | 42 | let mut array_position = self.update_sequence_array_start() as usize; 43 | let array_end = 44 | self.update_sequence_offset() as usize + self.update_sequence_size() as usize; 45 | let sectors_end = array_count as usize * NTFS_BLOCK_SIZE; 46 | 47 | if array_end > self.data.len() || sectors_end > self.data.len() { 48 | return Err(NtfsError::UpdateSequenceArrayExceedsRecordSize { 49 | position: self.position, 50 | array_count, 51 | record_size: self.data.len(), 52 | }); 53 | } 54 | 55 | // The Update Sequence Number (USN) is written to the last 2 bytes of each sector. 56 | let mut sector_position = NTFS_BLOCK_SIZE - mem::size_of::(); 57 | 58 | while array_position < array_end { 59 | let array_position_end = array_position + mem::size_of::(); 60 | let sector_position_end = sector_position + mem::size_of::(); 61 | 62 | // The array contains the actual 2 bytes that need to be at `sector_position` after the fixup. 63 | let new_bytes: [u8; 2] = self.data[array_position..array_position_end] 64 | .try_into() 65 | .unwrap(); 66 | 67 | // The current 2 bytes at `sector_position` before the fixup should equal the Update Sequence Number (USN). 68 | // Otherwise, this sector is corrupted. 69 | let bytes_to_update = &mut self.data[sector_position..sector_position_end]; 70 | if bytes_to_update != update_sequence_number { 71 | return Err(NtfsError::UpdateSequenceNumberMismatch { 72 | position: self.position + array_position, 73 | expected: update_sequence_number, 74 | actual: (&*bytes_to_update).try_into().unwrap(), 75 | }); 76 | } 77 | 78 | // Perform the actual fixup. 79 | bytes_to_update.copy_from_slice(&new_bytes); 80 | 81 | // Advance to the next array entry and sector. 82 | array_position += mem::size_of::(); 83 | sector_position += NTFS_BLOCK_SIZE; 84 | } 85 | 86 | Ok(()) 87 | } 88 | 89 | pub(crate) fn into_data(self) -> Vec { 90 | self.data 91 | } 92 | 93 | pub(crate) fn len(&self) -> u32 { 94 | // A record is never larger than a u32. 95 | // Usually, it shouldn't even exceed a u16, but our code could handle that. 96 | self.data.len() as u32 97 | } 98 | 99 | pub(crate) fn position(&self) -> NtfsPosition { 100 | self.position 101 | } 102 | 103 | pub(crate) fn signature(&self) -> [u8; 4] { 104 | self.data[span_of!(RecordHeader, signature)] 105 | .try_into() 106 | .unwrap() 107 | } 108 | 109 | fn update_sequence_array_count(&self) -> Result { 110 | let start = offset_of!(RecordHeader, update_sequence_count); 111 | let update_sequence_count = LittleEndian::read_u16(&self.data[start..]); 112 | 113 | // Subtract the Update Sequence Number (USN) element, so that only the number of array elements remains. 114 | update_sequence_count 115 | .checked_sub(1) 116 | .ok_or(NtfsError::InvalidUpdateSequenceCount { 117 | position: self.position, 118 | update_sequence_count, 119 | }) 120 | } 121 | 122 | fn update_sequence_array_start(&self) -> u16 { 123 | // The Update Sequence Number (USN) comes first and the array begins right after that. 124 | self.update_sequence_offset() + mem::size_of::() as u16 125 | } 126 | 127 | fn update_sequence_number(&self) -> Result<[u8; 2]> { 128 | let start = self.update_sequence_offset() as usize; 129 | let end = start + mem::size_of::(); 130 | self.data 131 | .get(start..end) 132 | .and_then(|bytes| bytes.try_into().ok()) 133 | .ok_or(NtfsError::InvalidUpdateSequenceNumberRange { 134 | position: self.position, 135 | range: start..end, 136 | size: self.data.len(), 137 | }) 138 | } 139 | 140 | fn update_sequence_offset(&self) -> u16 { 141 | let start = offset_of!(RecordHeader, update_sequence_offset); 142 | LittleEndian::read_u16(&self.data[start..]) 143 | } 144 | 145 | pub(crate) fn update_sequence_size(&self) -> u32 { 146 | let start = offset_of!(RecordHeader, update_sequence_count); 147 | let update_sequence_count = LittleEndian::read_u16(&self.data[start..]); 148 | update_sequence_count as u32 * mem::size_of::() as u32 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/structured_values/attribute_list.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 Colin Finck 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use core::mem; 5 | 6 | use arrayvec::ArrayVec; 7 | use binrw::io::{Cursor, Read, Seek, SeekFrom}; 8 | use binrw::{BinRead, BinReaderExt}; 9 | use nt_string::u16strle::U16StrLe; 10 | 11 | use crate::attribute::{NtfsAttribute, NtfsAttributeType}; 12 | use crate::attribute_value::{NtfsAttributeValue, NtfsNonResidentAttributeValue}; 13 | use crate::error::{NtfsError, Result}; 14 | use crate::file::NtfsFile; 15 | use crate::file_reference::NtfsFileReference; 16 | use crate::ntfs::Ntfs; 17 | use crate::structured_values::NtfsStructuredValue; 18 | use crate::traits::NtfsReadSeek; 19 | use crate::types::{NtfsPosition, Vcn}; 20 | 21 | /// Size of all [`AttributeListEntryHeader`] fields. 22 | const ATTRIBUTE_LIST_ENTRY_HEADER_SIZE: usize = 26; 23 | 24 | /// [`AttributeListEntryHeader::name_length`] is an `u8` length field specifying the number of UTF-16 code points. 25 | /// Hence, the name occupies up to 510 bytes. 26 | const NAME_MAX_SIZE: usize = (u8::MAX as usize) * mem::size_of::(); 27 | 28 | #[allow(unused)] 29 | #[derive(BinRead, Clone, Debug)] 30 | struct AttributeListEntryHeader { 31 | /// Type of the attribute, known types are in [`NtfsAttributeType`]. 32 | ty: u32, 33 | /// Length of this attribute list entry, in bytes. 34 | list_entry_length: u16, 35 | /// Length of the name, in UTF-16 code points (every code point is 2 bytes). 36 | name_length: u8, 37 | /// Offset to the beginning of the name, in bytes from the beginning of this header. 38 | name_offset: u8, 39 | /// Lower boundary of Virtual Cluster Numbers (VCNs) referenced by this attribute. 40 | /// This becomes relevant when file data is split over multiple attributes. 41 | /// Otherwise, it's zero. 42 | lowest_vcn: Vcn, 43 | /// Reference to the File Record where this attribute is stored. 44 | base_file_reference: NtfsFileReference, 45 | /// Identifier of this attribute that is unique within the [`NtfsFile`]. 46 | instance: u16, 47 | } 48 | 49 | /// Structure of an $ATTRIBUTE_LIST attribute. 50 | /// 51 | /// When a File Record lacks space to incorporate further attributes, NTFS creates an additional File Record, 52 | /// moves all or some of the existing attributes there, and references them via a resident $ATTRIBUTE_LIST attribute 53 | /// in the original File Record. 54 | /// When you add even more attributes, NTFS may turn the resident $ATTRIBUTE_LIST into a non-resident one to 55 | /// make up the required space. 56 | /// 57 | /// An $ATTRIBUTE_LIST attribute can hence be resident or non-resident. 58 | /// 59 | /// Reference: 60 | #[derive(Clone, Debug)] 61 | pub enum NtfsAttributeList<'n, 'f> { 62 | /// A resident $ATTRIBUTE_LIST attribute. 63 | Resident(&'f [u8], NtfsPosition), 64 | /// A non-resident $ATTRIBUTE_LIST attribute. 65 | NonResident(NtfsNonResidentAttributeValue<'n, 'f>), 66 | } 67 | 68 | impl<'n, 'f> NtfsAttributeList<'n, 'f> { 69 | /// Returns an iterator over all entries of this $ATTRIBUTE_LIST attribute (cf. [`NtfsAttributeListEntry`]). 70 | pub fn entries(&self) -> NtfsAttributeListEntries<'n, 'f> { 71 | NtfsAttributeListEntries::new(self.clone()) 72 | } 73 | 74 | /// Returns the absolute position of this $ATTRIBUTE_LIST attribute value within the filesystem, in bytes. 75 | pub fn position(&self) -> NtfsPosition { 76 | match self { 77 | Self::Resident(_slice, position) => *position, 78 | Self::NonResident(value) => value.data_position(), 79 | } 80 | } 81 | } 82 | 83 | impl<'n, 'f> NtfsStructuredValue<'n, 'f> for NtfsAttributeList<'n, 'f> { 84 | const TY: NtfsAttributeType = NtfsAttributeType::AttributeList; 85 | 86 | fn from_attribute_value(_fs: &mut T, value: NtfsAttributeValue<'n, 'f>) -> Result 87 | where 88 | T: Read + Seek, 89 | { 90 | match value { 91 | NtfsAttributeValue::Resident(value) => { 92 | let slice = value.data(); 93 | let position = value.data_position(); 94 | Ok(Self::Resident(slice, position)) 95 | } 96 | NtfsAttributeValue::NonResident(value) => Ok(Self::NonResident(value)), 97 | NtfsAttributeValue::AttributeListNonResident(value) => { 98 | // Attribute Lists are never nested. 99 | // Hence, we must not create this attribute from an attribute that is already part of Attribute List. 100 | let position = value.data_position(); 101 | Err(NtfsError::UnexpectedAttributeListAttribute { position }) 102 | } 103 | } 104 | } 105 | } 106 | 107 | /// Iterator over 108 | /// all entries of an [`NtfsAttributeList`] attribute, 109 | /// returning an [`NtfsAttributeListEntry`] for each entry. 110 | /// 111 | /// This iterator is returned from the [`NtfsAttributeList::entries`] function. 112 | #[derive(Clone, Debug)] 113 | pub struct NtfsAttributeListEntries<'n, 'f> { 114 | attribute_list: NtfsAttributeList<'n, 'f>, 115 | } 116 | 117 | impl<'n, 'f> NtfsAttributeListEntries<'n, 'f> { 118 | fn new(attribute_list: NtfsAttributeList<'n, 'f>) -> Self { 119 | Self { attribute_list } 120 | } 121 | 122 | /// See [`Iterator::next`]. 123 | pub fn next(&mut self, fs: &mut T) -> Option> 124 | where 125 | T: Read + Seek, 126 | { 127 | match &mut self.attribute_list { 128 | NtfsAttributeList::Resident(slice, position) => Self::next_resident(slice, position), 129 | NtfsAttributeList::NonResident(value) => Self::next_non_resident(fs, value), 130 | } 131 | } 132 | 133 | fn next_non_resident( 134 | fs: &mut T, 135 | value: &mut NtfsNonResidentAttributeValue<'n, 'f>, 136 | ) -> Option> 137 | where 138 | T: Read + Seek, 139 | { 140 | if value.stream_position() >= value.len() { 141 | return None; 142 | } 143 | 144 | // Get the current entry. 145 | let mut value_attached = value.clone().attach(fs); 146 | let position = value.data_position(); 147 | let entry = iter_try!(NtfsAttributeListEntry::new(&mut value_attached, position)); 148 | 149 | // Advance our iterator to the next entry. 150 | iter_try!(value.seek(fs, SeekFrom::Current(entry.list_entry_length() as i64))); 151 | 152 | Some(Ok(entry)) 153 | } 154 | 155 | fn next_resident( 156 | slice: &mut &'f [u8], 157 | position: &mut NtfsPosition, 158 | ) -> Option> { 159 | if slice.is_empty() { 160 | return None; 161 | } 162 | 163 | // Get the current entry. 164 | let mut cursor = Cursor::new(*slice); 165 | let entry = iter_try!(NtfsAttributeListEntry::new(&mut cursor, *position)); 166 | 167 | // Advance our iterator to the next entry. 168 | let bytes_to_advance = entry.list_entry_length() as usize; 169 | *slice = slice.get(bytes_to_advance..)?; 170 | *position += bytes_to_advance; 171 | Some(Ok(entry)) 172 | } 173 | } 174 | 175 | /// A single entry of an [`NtfsAttributeList`] attribute. 176 | #[derive(Clone, Debug)] 177 | pub struct NtfsAttributeListEntry { 178 | header: AttributeListEntryHeader, 179 | name: ArrayVec, 180 | position: NtfsPosition, 181 | } 182 | 183 | impl NtfsAttributeListEntry { 184 | fn new(r: &mut T, position: NtfsPosition) -> Result 185 | where 186 | T: Read + Seek, 187 | { 188 | let header = r.read_le::()?; 189 | 190 | let mut entry = Self { 191 | header, 192 | name: ArrayVec::from([0u8; NAME_MAX_SIZE]), 193 | position, 194 | }; 195 | entry.validate_entry_and_name_length()?; 196 | entry.read_name(r)?; 197 | 198 | Ok(entry) 199 | } 200 | 201 | /// Returns a reference to the File Record where the attribute is stored. 202 | pub fn base_file_reference(&self) -> NtfsFileReference { 203 | self.header.base_file_reference 204 | } 205 | 206 | /// Returns the instance number of this attribute list entry. 207 | /// 208 | /// An instance number is unique within a single NTFS File Record. 209 | /// 210 | /// Multiple entries of the same type and instance number form a connected attribute, 211 | /// meaning an attribute whose value is stretched over multiple attributes. 212 | pub fn instance(&self) -> u16 { 213 | self.header.instance 214 | } 215 | 216 | /// Returns the length of this attribute list entry, in bytes. 217 | pub fn list_entry_length(&self) -> u16 { 218 | self.header.list_entry_length 219 | } 220 | 221 | /// Returns the offset of this attribute's value data as a Virtual Cluster Number (VCN). 222 | /// 223 | /// This is zero for all unconnected attributes and for the first attribute of a connected attribute. 224 | /// For subsequent attributes of a connected attribute, this value is nonzero. 225 | /// 226 | /// The lowest_vcn + data length of one attribute equal the lowest_vcn of its following connected attribute. 227 | pub fn lowest_vcn(&self) -> Vcn { 228 | self.header.lowest_vcn 229 | } 230 | 231 | /// Gets the attribute name and returns it wrapped in a [`U16StrLe`]. 232 | pub fn name(&self) -> U16StrLe { 233 | U16StrLe(&self.name) 234 | } 235 | 236 | /// Returns the file name length, in bytes. 237 | /// 238 | /// A file name has a maximum length of 255 UTF-16 code points (510 bytes). 239 | pub fn name_length(&self) -> usize { 240 | self.header.name_length as usize * mem::size_of::() 241 | } 242 | 243 | /// Returns the absolute position of this attribute list entry within the filesystem, in bytes. 244 | pub fn position(&self) -> NtfsPosition { 245 | self.position 246 | } 247 | 248 | fn read_name(&mut self, r: &mut T) -> Result<()> 249 | where 250 | T: Read + Seek, 251 | { 252 | debug_assert_eq!(self.name.len(), NAME_MAX_SIZE); 253 | 254 | let name_length = self.name_length(); 255 | r.read_exact(&mut self.name[..name_length])?; 256 | self.name.truncate(name_length); 257 | 258 | Ok(()) 259 | } 260 | 261 | /// Returns an [`NtfsAttribute`] for the attribute described by this list entry. 262 | /// 263 | /// Use [`NtfsAttributeListEntry::to_file`] first to get the required File Record. 264 | /// 265 | /// # Panics 266 | /// 267 | /// Panics if a wrong File Record has been passed. 268 | pub fn to_attribute<'n, 'f>(&self, file: &'f NtfsFile<'n>) -> Result> { 269 | let file_record_number = self.base_file_reference().file_record_number(); 270 | assert_eq!( 271 | file.file_record_number(), 272 | file_record_number, 273 | "The given NtfsFile's record number does not match the expected record number. \ 274 | Always use NtfsAttributeListEntry::to_file to retrieve the correct NtfsFile." 275 | ); 276 | 277 | let instance = self.instance(); 278 | let ty = self.ty()?; 279 | 280 | file.find_resident_attribute(ty, None, Some(instance)) 281 | } 282 | 283 | /// Reads the entire File Record referenced by this attribute and returns it. 284 | pub fn to_file<'n, T>(&self, ntfs: &'n Ntfs, fs: &mut T) -> Result> 285 | where 286 | T: Read + Seek, 287 | { 288 | let file_record_number = self.base_file_reference().file_record_number(); 289 | ntfs.file(fs, file_record_number) 290 | } 291 | 292 | /// Returns the type of this NTFS Attribute, or [`NtfsError::UnsupportedAttributeType`] 293 | /// if it's an unknown type. 294 | pub fn ty(&self) -> Result { 295 | NtfsAttributeType::n(self.header.ty).ok_or(NtfsError::UnsupportedAttributeType { 296 | position: self.position(), 297 | actual: self.header.ty, 298 | }) 299 | } 300 | 301 | fn validate_entry_and_name_length(&self) -> Result<()> { 302 | let total_size = ATTRIBUTE_LIST_ENTRY_HEADER_SIZE + self.name_length(); 303 | 304 | if total_size > self.list_entry_length() as usize { 305 | return Err(NtfsError::InvalidStructuredValueSize { 306 | position: self.position(), 307 | ty: NtfsAttributeType::AttributeList, 308 | expected: self.list_entry_length() as u64, 309 | actual: total_size as u64, 310 | }); 311 | } 312 | 313 | Ok(()) 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /src/structured_values/file_name.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 Colin Finck 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use core::mem; 5 | 6 | use arrayvec::ArrayVec; 7 | use binrw::io::{Cursor, Read, Seek}; 8 | use binrw::{BinRead, BinReaderExt}; 9 | use enumn::N; 10 | use nt_string::u16strle::U16StrLe; 11 | 12 | use crate::attribute::NtfsAttributeType; 13 | use crate::attribute_value::NtfsAttributeValue; 14 | use crate::error::{NtfsError, Result}; 15 | use crate::file_reference::NtfsFileReference; 16 | use crate::indexes::NtfsIndexEntryKey; 17 | use crate::structured_values::{NtfsFileAttributeFlags, NtfsStructuredValue}; 18 | use crate::time::NtfsTime; 19 | use crate::types::NtfsPosition; 20 | 21 | /// Size of all [`FileNameHeader`] fields. 22 | const FILE_NAME_HEADER_SIZE: usize = 66; 23 | 24 | /// The smallest FileName attribute has a name containing just a single character. 25 | const FILE_NAME_MIN_SIZE: usize = FILE_NAME_HEADER_SIZE + mem::size_of::(); 26 | 27 | /// The "name" stored in the FileName attribute has an `u8` length field specifying the number of UTF-16 code points. 28 | /// Hence, the name occupies up to 510 bytes. 29 | const NAME_MAX_SIZE: usize = (u8::MAX as usize) * mem::size_of::(); 30 | 31 | #[allow(unused)] 32 | #[derive(BinRead, Clone, Debug)] 33 | struct FileNameHeader { 34 | parent_directory_reference: NtfsFileReference, 35 | creation_time: NtfsTime, 36 | modification_time: NtfsTime, 37 | mft_record_modification_time: NtfsTime, 38 | access_time: NtfsTime, 39 | allocated_size: u64, 40 | data_size: u64, 41 | file_attributes: u32, 42 | reparse_point_tag: u32, 43 | name_length: u8, 44 | namespace: u8, 45 | } 46 | 47 | /// Character set constraint of the filename, returned by [`NtfsFileName::namespace`]. 48 | /// 49 | /// Reference: 50 | #[derive(Clone, Copy, Debug, Eq, N, PartialEq)] 51 | #[repr(u8)] 52 | pub enum NtfsFileNamespace { 53 | /// A POSIX-compatible filename, which is case-sensitive and supports all Unicode 54 | /// characters except for the forward slash (/) and the NUL character. 55 | Posix = 0, 56 | /// A long filename for Windows, which is case-insensitive and supports all Unicode 57 | /// characters except for " * < > ? \ | / : (and doesn't end with a dot or a space). 58 | Win32 = 1, 59 | /// An MS-DOS 8+3 filename (8 uppercase characters with a 3-letter uppercase extension) 60 | /// that consists entirely of printable ASCII characters (except for " * < > ? \ | / : ; . , + = [ ]). 61 | Dos = 2, 62 | /// A Windows filename that also fulfills all requirements of an MS-DOS 8+3 filename (minus the 63 | /// uppercase requirement), and therefore only got a single $FILE_NAME record with this name. 64 | Win32AndDos = 3, 65 | } 66 | 67 | /// Structure of a $FILE_NAME attribute. 68 | /// 69 | /// NTFS creates a $FILE_NAME attribute for every hard link. 70 | /// Its valuable information is the actual file name and whether this file represents a directory. 71 | /// Apart from that, it duplicates several fields of $STANDARD_INFORMATION, but these are only updated when the file name changes. 72 | /// You usually want to use the corresponding fields from [`NtfsStandardInformation`] instead. 73 | /// 74 | /// A $FILE_NAME attribute can be resident or non-resident. 75 | /// 76 | /// Reference: 77 | /// 78 | /// [`NtfsStandardInformation`]: crate::structured_values::NtfsStandardInformation 79 | #[derive(Clone, Debug)] 80 | pub struct NtfsFileName { 81 | header: FileNameHeader, 82 | name: ArrayVec, 83 | } 84 | 85 | impl NtfsFileName { 86 | fn new(r: &mut T, position: NtfsPosition, value_length: u64) -> Result 87 | where 88 | T: Read + Seek, 89 | { 90 | if value_length < FILE_NAME_MIN_SIZE as u64 { 91 | return Err(NtfsError::InvalidStructuredValueSize { 92 | position, 93 | ty: NtfsAttributeType::FileName, 94 | expected: FILE_NAME_MIN_SIZE as u64, 95 | actual: value_length, 96 | }); 97 | } 98 | 99 | let header = r.read_le::()?; 100 | 101 | let mut file_name = Self { 102 | header, 103 | name: ArrayVec::from([0u8; NAME_MAX_SIZE]), 104 | }; 105 | file_name.validate_name_length(value_length, position)?; 106 | file_name.validate_namespace(position)?; 107 | file_name.read_name(r)?; 108 | 109 | Ok(file_name) 110 | } 111 | 112 | /// Returns the last access time stored in this $FILE_NAME record. 113 | /// 114 | /// **Note that NTFS only updates it when the file name is changed!** 115 | /// Check [`NtfsStandardInformation::access_time`] for a last access time that is always up to date. 116 | /// 117 | /// [`NtfsStandardInformation::access_time`]: crate::structured_values::NtfsStandardInformation::access_time 118 | pub fn access_time(&self) -> NtfsTime { 119 | self.header.access_time 120 | } 121 | 122 | /// Returns the allocated size of the file data, in bytes. 123 | /// "Data" refers to the unnamed $DATA attribute only. 124 | /// Other $DATA attributes are not considered. 125 | /// 126 | /// **Note that NTFS only updates it when the file name is changed!** 127 | /// If you need an always up-to-date allocated size, use [`NtfsFile::data`] to get the unnamed $DATA attribute, 128 | /// fetch the corresponding [`NtfsAttribute`], and use [`NtfsAttribute::value`] to fetch the corresponding 129 | /// [`NtfsAttributeValue`]. 130 | /// For non-resident attribute values, you now need to walk through each Data Run and sum up the return values of 131 | /// [`NtfsDataRun::allocated_size`]. 132 | /// For resident attribute values, the length equals the allocated size. 133 | /// 134 | /// [`NtfsAttribute`]: crate::NtfsAttribute 135 | /// [`NtfsAttribute::value`]: crate::NtfsAttribute::value 136 | /// [`NtfsDataRun::allocated_size`]: crate::attribute_value::NtfsDataRun::allocated_size 137 | /// [`NtfsFile::data`]: crate::NtfsFile::data 138 | pub fn allocated_size(&self) -> u64 { 139 | self.header.allocated_size 140 | } 141 | 142 | /// Returns the creation time stored in this $FILE_NAME record. 143 | /// 144 | /// **Note that NTFS only updates it when the file name is changed!** 145 | /// Check [`NtfsStandardInformation::creation_time`] for a creation time that is always up to date. 146 | /// 147 | /// [`NtfsStandardInformation::creation_time`]: crate::structured_values::NtfsStandardInformation::creation_time 148 | pub fn creation_time(&self) -> NtfsTime { 149 | self.header.creation_time 150 | } 151 | 152 | /// Returns the size actually used by the file data, in bytes. 153 | /// 154 | /// "Data" refers to the unnamed $DATA attribute only. 155 | /// Other $DATA attributes are not considered. 156 | /// 157 | /// This is less or equal than [`NtfsFileName::allocated_size`]. 158 | /// 159 | /// **Note that NTFS only updates it when the file name is changed!** 160 | /// If you need an always up-to-date size, use [`NtfsFile::data`] to get the unnamed $DATA attribute, 161 | /// fetch the corresponding [`NtfsAttribute`], and use [`NtfsAttribute::value`] to fetch the corresponding 162 | /// [`NtfsAttributeValue`]. 163 | /// Then query [`NtfsAttributeValue::len`]. 164 | /// 165 | /// [`NtfsAttribute`]: crate::attribute::NtfsAttribute 166 | /// [`NtfsAttribute::value`]: crate::attribute::NtfsAttribute::value 167 | /// [`NtfsFile::data`]: crate::file::NtfsFile::data 168 | pub fn data_size(&self) -> u64 { 169 | self.header.data_size 170 | } 171 | 172 | /// Returns flags that a user can set for a file (Read-Only, Hidden, System, Archive, etc.). 173 | /// Commonly called "File Attributes" in Windows Explorer. 174 | /// 175 | /// **Note that NTFS only updates it when the file name is changed!** 176 | /// Check [`NtfsStandardInformation::file_attributes`] for file attributes that are always up to date. 177 | /// 178 | /// [`NtfsStandardInformation::file_attributes`]: crate::structured_values::NtfsStandardInformation::file_attributes 179 | pub fn file_attributes(&self) -> NtfsFileAttributeFlags { 180 | NtfsFileAttributeFlags::from_bits_truncate(self.header.file_attributes) 181 | } 182 | 183 | /// Returns whether this file is a directory. 184 | pub fn is_directory(&self) -> bool { 185 | self.file_attributes() 186 | .contains(NtfsFileAttributeFlags::IS_DIRECTORY) 187 | } 188 | 189 | /// Returns the MFT record modification time stored in this $FILE_NAME record. 190 | /// 191 | /// **Note that NTFS only updates it when the file name is changed!** 192 | /// Check [`NtfsStandardInformation::mft_record_modification_time`] for an MFT record modification time that is always up to date. 193 | /// 194 | /// [`NtfsStandardInformation::mft_record_modification_time`]: crate::structured_values::NtfsStandardInformation::mft_record_modification_time 195 | pub fn mft_record_modification_time(&self) -> NtfsTime { 196 | self.header.mft_record_modification_time 197 | } 198 | 199 | /// Returns the modification time stored in this $FILE_NAME record. 200 | /// 201 | /// **Note that NTFS only updates it when the file name is changed!** 202 | /// Check [`NtfsStandardInformation::modification_time`] for a modification time that is always up to date. 203 | /// 204 | /// [`NtfsStandardInformation::modification_time`]: crate::structured_values::NtfsStandardInformation::modification_time 205 | pub fn modification_time(&self) -> NtfsTime { 206 | self.header.modification_time 207 | } 208 | 209 | /// Gets the file name and returns it wrapped in a [`U16StrLe`]. 210 | pub fn name(&self) -> U16StrLe { 211 | U16StrLe(&self.name) 212 | } 213 | 214 | /// Returns the file name length, in bytes. 215 | /// 216 | /// A file name has a maximum length of 255 UTF-16 code points (510 bytes). 217 | pub fn name_length(&self) -> usize { 218 | self.header.name_length as usize * mem::size_of::() 219 | } 220 | 221 | /// Returns the [`NtfsFileNamespace`] of this file name. 222 | pub fn namespace(&self) -> NtfsFileNamespace { 223 | NtfsFileNamespace::n(self.header.namespace).unwrap() 224 | } 225 | 226 | /// Returns an [`NtfsFileReference`] for the directory where this file is located. 227 | pub fn parent_directory_reference(&self) -> NtfsFileReference { 228 | self.header.parent_directory_reference 229 | } 230 | 231 | fn read_name(&mut self, r: &mut T) -> Result<()> 232 | where 233 | T: Read + Seek, 234 | { 235 | debug_assert_eq!(self.name.len(), NAME_MAX_SIZE); 236 | 237 | let name_length = self.name_length(); 238 | r.read_exact(&mut self.name[..name_length])?; 239 | self.name.truncate(name_length); 240 | 241 | Ok(()) 242 | } 243 | 244 | fn validate_name_length(&self, data_size: u64, position: NtfsPosition) -> Result<()> { 245 | let total_size = (FILE_NAME_HEADER_SIZE + self.name_length()) as u64; 246 | 247 | if total_size > data_size { 248 | return Err(NtfsError::InvalidStructuredValueSize { 249 | position, 250 | ty: NtfsAttributeType::FileName, 251 | expected: data_size, 252 | actual: total_size, 253 | }); 254 | } 255 | 256 | Ok(()) 257 | } 258 | 259 | fn validate_namespace(&self, position: NtfsPosition) -> Result<()> { 260 | if NtfsFileNamespace::n(self.header.namespace).is_none() { 261 | return Err(NtfsError::UnsupportedFileNamespace { 262 | position, 263 | actual: self.header.namespace, 264 | }); 265 | } 266 | 267 | Ok(()) 268 | } 269 | } 270 | 271 | impl<'n, 'f> NtfsStructuredValue<'n, 'f> for NtfsFileName { 272 | const TY: NtfsAttributeType = NtfsAttributeType::FileName; 273 | 274 | fn from_attribute_value(fs: &mut T, value: NtfsAttributeValue<'n, 'f>) -> Result 275 | where 276 | T: Read + Seek, 277 | { 278 | let position = value.data_position(); 279 | let value_length = value.len(); 280 | 281 | let mut value_attached = value.attach(fs); 282 | Self::new(&mut value_attached, position, value_length) 283 | } 284 | } 285 | 286 | // `NtfsFileName` is special in the regard that the Index Entry key has the same structure as the structured value. 287 | impl NtfsIndexEntryKey for NtfsFileName { 288 | fn key_from_slice(slice: &[u8], position: NtfsPosition) -> Result { 289 | let value_length = slice.len() as u64; 290 | 291 | let mut cursor = Cursor::new(slice); 292 | Self::new(&mut cursor, position, value_length) 293 | } 294 | } 295 | 296 | #[cfg(test)] 297 | mod tests { 298 | use super::*; 299 | use crate::file::KnownNtfsFileRecordNumber; 300 | use crate::ntfs::Ntfs; 301 | use crate::time::tests::NT_TIMESTAMP_2021_01_01; 302 | 303 | #[test] 304 | fn test_file_name() { 305 | let mut testfs1 = crate::helpers::tests::testfs1(); 306 | let ntfs = Ntfs::new(&mut testfs1).unwrap(); 307 | let mft = ntfs 308 | .file(&mut testfs1, KnownNtfsFileRecordNumber::MFT as u64) 309 | .unwrap(); 310 | let mut mft_attributes = mft.attributes_raw(); 311 | 312 | // Check the FileName attribute of the MFT. 313 | let attribute = mft_attributes.nth(1).unwrap().unwrap(); 314 | assert_eq!(attribute.ty().unwrap(), NtfsAttributeType::FileName); 315 | assert_eq!(attribute.attribute_length(), 104); 316 | assert!(attribute.is_resident()); 317 | assert_eq!(attribute.name_length(), 0); 318 | assert_eq!(attribute.value_length(), 74); 319 | 320 | // Check the actual "file name" of the MFT. 321 | let file_name = attribute 322 | .structured_value::<_, NtfsFileName>(&mut testfs1) 323 | .unwrap(); 324 | 325 | let creation_time = file_name.creation_time(); 326 | assert!(creation_time.nt_timestamp() > NT_TIMESTAMP_2021_01_01); 327 | assert_eq!(creation_time, file_name.modification_time()); 328 | assert_eq!(creation_time, file_name.mft_record_modification_time()); 329 | assert_eq!(creation_time, file_name.access_time()); 330 | 331 | let allocated_size = file_name.allocated_size(); 332 | assert!(allocated_size > 0); 333 | assert_eq!(allocated_size, file_name.data_size()); 334 | 335 | assert_eq!(file_name.name_length(), 8); 336 | 337 | // Test various ways to compare the same string. 338 | assert_eq!(file_name.name(), "$MFT"); 339 | assert_eq!(file_name.name().to_string_lossy(), String::from("$MFT")); 340 | assert_eq!( 341 | file_name.name(), 342 | U16StrLe(&[b'$', 0, b'M', 0, b'F', 0, b'T', 0]) 343 | ); 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /src/structured_values/index_allocation.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 Colin Finck 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use core::iter::FusedIterator; 5 | 6 | use binrw::io::{Read, Seek, SeekFrom}; 7 | 8 | use crate::attribute::NtfsAttributeType; 9 | use crate::attribute_value::NtfsAttributeValue; 10 | use crate::error::{NtfsError, Result}; 11 | use crate::index_record::NtfsIndexRecord; 12 | use crate::ntfs::Ntfs; 13 | use crate::structured_values::NtfsStructuredValue; 14 | use crate::traits::NtfsReadSeek; 15 | use crate::types::Vcn; 16 | 17 | /// Structure of an $INDEX_ALLOCATION attribute. 18 | /// 19 | /// This attribute describes the sub-nodes of a B-tree. 20 | /// The top-level nodes are managed via [`NtfsIndexRoot`]. 21 | /// 22 | /// NTFS uses B-trees for describing directories (as indexes of [`NtfsFileName`]s), looking up Object IDs, 23 | /// Reparse Points, and Security Descriptors, to just name a few. 24 | /// 25 | /// An $INDEX_ALLOCATION attribute can be resident or non-resident. 26 | /// 27 | /// Reference: 28 | /// 29 | /// [`NtfsFileName`]: crate::structured_values::NtfsFileName 30 | /// [`NtfsIndexRoot`]: crate::structured_values::NtfsIndexRoot 31 | #[derive(Clone, Debug)] 32 | pub struct NtfsIndexAllocation<'n, 'f> { 33 | ntfs: &'n Ntfs, 34 | value: NtfsAttributeValue<'n, 'f>, 35 | } 36 | 37 | impl<'n, 'f> NtfsIndexAllocation<'n, 'f> { 38 | /// Returns the [`NtfsIndexRecord`] located at the given Virtual Cluster Number (VCN). 39 | /// 40 | /// The record is fully read, fixed up, and validated. 41 | /// 42 | /// This function is usually called on the return value of [`NtfsIndexEntry::subnode_vcn`] to move further 43 | /// down in the B-tree. 44 | /// 45 | /// [`NtfsIndexEntry::subnode_vcn`]: crate::NtfsIndexEntry::subnode_vcn 46 | pub fn record_from_vcn( 47 | &self, 48 | fs: &mut T, 49 | index_record_size: u32, 50 | vcn: Vcn, 51 | ) -> Result 52 | where 53 | T: Read + Seek, 54 | { 55 | // Seek to the byte offset of the given VCN. 56 | let mut value = self.value.clone(); 57 | let offset = vcn.offset(self.ntfs)?; 58 | value.seek(fs, SeekFrom::Current(offset))?; 59 | 60 | if value.stream_position() >= value.len() { 61 | return Err(NtfsError::VcnOutOfBoundsInIndexAllocation { 62 | position: self.value.data_position(), 63 | vcn, 64 | }); 65 | } 66 | 67 | // Get the record. 68 | let record = NtfsIndexRecord::new(fs, value, index_record_size)?; 69 | 70 | // Validate that the VCN in the record is the requested one. 71 | if record.vcn() != vcn { 72 | return Err(NtfsError::VcnMismatchInIndexAllocation { 73 | position: self.value.data_position(), 74 | expected: vcn, 75 | actual: record.vcn(), 76 | }); 77 | } 78 | 79 | Ok(record) 80 | } 81 | 82 | /// Returns an iterator over all Index Records of this $INDEX_ALLOCATION attribute (cf. [`NtfsIndexRecord`]). 83 | /// 84 | /// Each Index Record is fully read, fixed up, and validated. 85 | pub fn records(&self, index_record_size: u32) -> NtfsIndexRecords<'n, 'f> { 86 | NtfsIndexRecords::new(self.clone(), index_record_size) 87 | } 88 | } 89 | 90 | impl<'n, 'f> NtfsStructuredValue<'n, 'f> for NtfsIndexAllocation<'n, 'f> { 91 | const TY: NtfsAttributeType = NtfsAttributeType::IndexAllocation; 92 | 93 | fn from_attribute_value(_fs: &mut T, value: NtfsAttributeValue<'n, 'f>) -> Result 94 | where 95 | T: Read + Seek, 96 | { 97 | let ntfs = match &value { 98 | NtfsAttributeValue::AttributeListNonResident(value) => value.ntfs(), 99 | NtfsAttributeValue::NonResident(value) => value.ntfs(), 100 | NtfsAttributeValue::Resident(_) => { 101 | let position = value.data_position(); 102 | return Err(NtfsError::UnexpectedResidentAttribute { position }); 103 | } 104 | }; 105 | 106 | Ok(Self { ntfs, value }) 107 | } 108 | } 109 | 110 | /// Iterator over 111 | /// all index records of an [`NtfsIndexAllocation`], 112 | /// returning an [`NtfsIndexRecord`] for each record. 113 | /// 114 | /// This iterator is returned from the [`NtfsIndexAllocation::records`] function. 115 | /// 116 | /// See [`NtfsIndexRecordsAttached`] for an iterator that implements [`Iterator`] and [`FusedIterator`]. 117 | #[derive(Clone, Debug)] 118 | pub struct NtfsIndexRecords<'n, 'f> { 119 | index_allocation: NtfsIndexAllocation<'n, 'f>, 120 | index_record_size: u32, 121 | } 122 | 123 | impl<'n, 'f> NtfsIndexRecords<'n, 'f> { 124 | fn new(index_allocation: NtfsIndexAllocation<'n, 'f>, index_record_size: u32) -> Self { 125 | Self { 126 | index_allocation, 127 | index_record_size, 128 | } 129 | } 130 | 131 | /// Returns a variant of this iterator that implements [`Iterator`] and [`FusedIterator`] 132 | /// by mutably borrowing the filesystem reader. 133 | pub fn attach<'a, T>(self, fs: &'a mut T) -> NtfsIndexRecordsAttached<'n, 'f, 'a, T> 134 | where 135 | T: Read + Seek, 136 | { 137 | NtfsIndexRecordsAttached::new(fs, self) 138 | } 139 | 140 | /// See [`Iterator::next`]. 141 | pub fn next(&mut self, fs: &mut T) -> Option> 142 | where 143 | T: Read + Seek, 144 | { 145 | if self.index_allocation.value.stream_position() >= self.index_allocation.value.len() { 146 | return None; 147 | } 148 | 149 | // Get the current record. 150 | let record = iter_try!(NtfsIndexRecord::new( 151 | fs, 152 | self.index_allocation.value.clone(), 153 | self.index_record_size 154 | )); 155 | 156 | // Advance our iterator to the next record. 157 | iter_try!(self 158 | .index_allocation 159 | .value 160 | .seek(fs, SeekFrom::Current(self.index_record_size as i64))); 161 | 162 | Some(Ok(record)) 163 | } 164 | } 165 | 166 | /// Iterator over 167 | /// all index records of an [`NtfsIndexAllocation`], 168 | /// returning an [`NtfsIndexRecord`] for each record, 169 | /// implementing [`Iterator`] and [`FusedIterator`]. 170 | /// 171 | /// This iterator is returned from the [`NtfsIndexRecords::attach`] function. 172 | /// Conceptually the same as [`NtfsIndexRecords`], but mutably borrows the filesystem 173 | /// to implement aforementioned traits. 174 | #[derive(Debug)] 175 | pub struct NtfsIndexRecordsAttached<'n, 'f, 'a, T> 176 | where 177 | T: Read + Seek, 178 | { 179 | fs: &'a mut T, 180 | index_records: NtfsIndexRecords<'n, 'f>, 181 | } 182 | 183 | impl<'n, 'f, 'a, T> NtfsIndexRecordsAttached<'n, 'f, 'a, T> 184 | where 185 | T: Read + Seek, 186 | { 187 | fn new(fs: &'a mut T, index_records: NtfsIndexRecords<'n, 'f>) -> Self { 188 | Self { fs, index_records } 189 | } 190 | /// Consumes this iterator and returns the inner [`NtfsIndexRecords`]. 191 | pub fn detach(self) -> NtfsIndexRecords<'n, 'f> { 192 | self.index_records 193 | } 194 | } 195 | 196 | impl<'n, 'f, 'a, T> Iterator for NtfsIndexRecordsAttached<'n, 'f, 'a, T> 197 | where 198 | T: Read + Seek, 199 | { 200 | type Item = Result; 201 | 202 | fn next(&mut self) -> Option { 203 | self.index_records.next(self.fs) 204 | } 205 | } 206 | 207 | impl<'n, 'f, 'a, T> FusedIterator for NtfsIndexRecordsAttached<'n, 'f, 'a, T> where T: Read + Seek {} 208 | -------------------------------------------------------------------------------- /src/structured_values/index_root.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 Colin Finck 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use core::ops::Range; 5 | 6 | use binrw::io::{Read, Seek}; 7 | use byteorder::{ByteOrder, LittleEndian}; 8 | use memoffset::offset_of; 9 | 10 | use crate::attribute::NtfsAttributeType; 11 | use crate::attribute_value::{NtfsAttributeValue, NtfsResidentAttributeValue}; 12 | use crate::error::{NtfsError, Result}; 13 | use crate::index_entry::{IndexNodeEntryRanges, NtfsIndexNodeEntries}; 14 | use crate::index_record::{IndexNodeHeader, INDEX_NODE_HEADER_SIZE}; 15 | use crate::indexes::NtfsIndexEntryType; 16 | use crate::structured_values::{ 17 | NtfsStructuredValue, NtfsStructuredValueFromResidentAttributeValue, 18 | }; 19 | use crate::types::NtfsPosition; 20 | 21 | /// Size of all [`IndexRootHeader`] fields plus some reserved bytes. 22 | const INDEX_ROOT_HEADER_SIZE: usize = 16; 23 | 24 | #[repr(C, packed)] 25 | struct IndexRootHeader { 26 | ty: u32, 27 | collation_rule: u32, 28 | index_record_size: u32, 29 | clusters_per_index_record: i8, 30 | } 31 | 32 | /// Structure of an $INDEX_ROOT attribute. 33 | /// 34 | /// This attribute describes the top-level nodes of a B-tree. 35 | /// The sub-nodes are managed via [`NtfsIndexAllocation`]. 36 | /// 37 | /// NTFS uses B-trees for describing directories (as indexes of [`NtfsFileName`]s), looking up Object IDs, 38 | /// Reparse Points, and Security Descriptors, to just name a few. 39 | /// 40 | /// An $INDEX_ROOT attribute is always resident. 41 | /// 42 | /// Reference: 43 | /// 44 | /// [`NtfsFileName`]: crate::structured_values::NtfsFileName 45 | /// [`NtfsIndexAllocation`]: crate::structured_values::NtfsIndexAllocation 46 | #[derive(Clone, Debug)] 47 | pub struct NtfsIndexRoot<'f> { 48 | slice: &'f [u8], 49 | position: NtfsPosition, 50 | } 51 | 52 | const LARGE_INDEX_FLAG: u8 = 0x01; 53 | 54 | impl<'f> NtfsIndexRoot<'f> { 55 | fn new(slice: &'f [u8], position: NtfsPosition) -> Result { 56 | if slice.len() < INDEX_ROOT_HEADER_SIZE + INDEX_NODE_HEADER_SIZE { 57 | return Err(NtfsError::InvalidStructuredValueSize { 58 | position, 59 | ty: NtfsAttributeType::IndexRoot, 60 | expected: INDEX_ROOT_HEADER_SIZE as u64, 61 | actual: slice.len() as u64, 62 | }); 63 | } 64 | 65 | let index_root = Self { slice, position }; 66 | index_root.validate_sizes()?; 67 | 68 | Ok(index_root) 69 | } 70 | 71 | /// Returns an iterator over all top-level nodes of the B-tree. 72 | pub fn entries(&self) -> Result> 73 | where 74 | E: NtfsIndexEntryType, 75 | { 76 | let (entries_range, position) = self.entries_range_and_position(); 77 | let slice = &self.slice[entries_range]; 78 | 79 | Ok(NtfsIndexNodeEntries::new(slice, position)) 80 | } 81 | 82 | fn entries_range_and_position(&self) -> (Range, NtfsPosition) { 83 | let start = INDEX_ROOT_HEADER_SIZE + self.index_entries_offset() as usize; 84 | let end = INDEX_ROOT_HEADER_SIZE + self.index_data_size() as usize; 85 | let position = self.position + start; 86 | 87 | (start..end, position) 88 | } 89 | 90 | pub(crate) fn entry_ranges(&self) -> IndexNodeEntryRanges 91 | where 92 | E: NtfsIndexEntryType, 93 | { 94 | let (entries_range, position) = self.entries_range_and_position(); 95 | let entries_data = self.slice[entries_range].to_vec(); 96 | let range = 0..entries_data.len(); 97 | 98 | IndexNodeEntryRanges::new(entries_data, range, position) 99 | } 100 | 101 | /// Returns the allocated size of this NTFS Index Root, in bytes. 102 | pub fn index_allocated_size(&self) -> u32 { 103 | let start = INDEX_ROOT_HEADER_SIZE + offset_of!(IndexNodeHeader, allocated_size); 104 | LittleEndian::read_u32(&self.slice[start..]) 105 | } 106 | 107 | /// Returns the size actually used by index data within this NTFS Index Root, in bytes. 108 | pub fn index_data_size(&self) -> u32 { 109 | let start = INDEX_ROOT_HEADER_SIZE + offset_of!(IndexNodeHeader, index_size); 110 | LittleEndian::read_u32(&self.slice[start..]) 111 | } 112 | 113 | fn index_entries_offset(&self) -> u32 { 114 | let start = INDEX_ROOT_HEADER_SIZE + offset_of!(IndexNodeHeader, entries_offset); 115 | LittleEndian::read_u32(&self.slice[start..]) 116 | } 117 | 118 | /// Returns the size of a single Index Record, in bytes. 119 | pub fn index_record_size(&self) -> u32 { 120 | let start = offset_of!(IndexRootHeader, index_record_size); 121 | LittleEndian::read_u32(&self.slice[start..]) 122 | } 123 | 124 | /// Returns whether the index belonging to this Index Root is large enough 125 | /// to need an extra Index Allocation attribute. 126 | /// Otherwise, the entire index information is stored in this Index Root. 127 | pub fn is_large_index(&self) -> bool { 128 | let start = INDEX_ROOT_HEADER_SIZE + offset_of!(IndexNodeHeader, flags); 129 | (self.slice[start] & LARGE_INDEX_FLAG) != 0 130 | } 131 | 132 | /// Returns the absolute position of this Index Root within the filesystem, in bytes. 133 | pub fn position(&self) -> NtfsPosition { 134 | self.position 135 | } 136 | 137 | fn validate_sizes(&self) -> Result<()> { 138 | let (entries_range, _position) = self.entries_range_and_position(); 139 | 140 | if entries_range.start >= self.slice.len() { 141 | return Err(NtfsError::InvalidIndexRootEntriesOffset { 142 | position: self.position, 143 | expected: entries_range.start, 144 | actual: self.slice.len(), 145 | }); 146 | } 147 | 148 | if entries_range.end > self.slice.len() { 149 | return Err(NtfsError::InvalidIndexRootUsedSize { 150 | position: self.position, 151 | expected: entries_range.end, 152 | actual: self.slice.len(), 153 | }); 154 | } 155 | 156 | Ok(()) 157 | } 158 | } 159 | 160 | impl<'n, 'f> NtfsStructuredValue<'n, 'f> for NtfsIndexRoot<'f> { 161 | const TY: NtfsAttributeType = NtfsAttributeType::IndexRoot; 162 | 163 | fn from_attribute_value(_fs: &mut T, value: NtfsAttributeValue<'n, 'f>) -> Result 164 | where 165 | T: Read + Seek, 166 | { 167 | let position = value.data_position(); 168 | 169 | let resident_value = match value { 170 | NtfsAttributeValue::Resident(resident_value) => resident_value, 171 | _ => return Err(NtfsError::UnexpectedNonResidentAttribute { position }), 172 | }; 173 | 174 | Self::new(resident_value.data(), position) 175 | } 176 | } 177 | 178 | impl<'n, 'f> NtfsStructuredValueFromResidentAttributeValue<'n, 'f> for NtfsIndexRoot<'f> { 179 | fn from_resident_attribute_value(value: NtfsResidentAttributeValue<'f>) -> Result { 180 | Self::new(value.data(), value.data_position()) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/structured_values/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 Colin Finck 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | // 4 | //! Various types of NTFS Attribute structured values. 5 | 6 | mod attribute_list; 7 | mod file_name; 8 | mod index_allocation; 9 | mod index_root; 10 | mod object_id; 11 | mod standard_information; 12 | mod volume_information; 13 | mod volume_name; 14 | 15 | use core::fmt; 16 | 17 | pub use attribute_list::*; 18 | pub use file_name::*; 19 | pub use index_allocation::*; 20 | pub use index_root::*; 21 | pub use object_id::*; 22 | pub use standard_information::*; 23 | pub use volume_information::*; 24 | pub use volume_name::*; 25 | 26 | use binrw::io::{Read, Seek}; 27 | use bitflags::bitflags; 28 | 29 | use crate::attribute::NtfsAttributeType; 30 | use crate::attribute_value::{NtfsAttributeValue, NtfsResidentAttributeValue}; 31 | use crate::error::Result; 32 | 33 | bitflags! { 34 | /// Flags that a user can set for a file (Read-Only, Hidden, System, Archive, etc.). 35 | /// Commonly called "File Attributes" in Windows Explorer. 36 | /// 37 | /// Not to be confused with [`NtfsAttribute`]. 38 | /// 39 | /// Returned by [`NtfsStandardInformation::file_attributes`] and [`NtfsFileName::file_attributes`]. 40 | /// 41 | /// [`NtfsAttribute`]: crate::attribute::NtfsAttribute 42 | #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 43 | pub struct NtfsFileAttributeFlags: u32 { 44 | /// File is marked read-only. 45 | const READ_ONLY = 0x0001; 46 | /// File is hidden (in file browsers that care). 47 | const HIDDEN = 0x0002; 48 | /// File is marked as a system file. 49 | const SYSTEM = 0x0004; 50 | /// File is marked for archival (cf. ). 51 | const ARCHIVE = 0x0020; 52 | /// File denotes a device. 53 | const DEVICE = 0x0040; 54 | /// Set when no other attributes are set. 55 | const NORMAL = 0x0080; 56 | /// File is a temporary file that is likely to be deleted. 57 | const TEMPORARY = 0x0100; 58 | /// File is stored sparsely. 59 | const SPARSE_FILE = 0x0200; 60 | /// File is a reparse point. 61 | const REPARSE_POINT = 0x0400; 62 | /// File is transparently compressed by the filesystem (using LZNT1 algorithm). 63 | /// For directories, this attribute denotes that compression is enabled by default for new files inside that directory. 64 | const COMPRESSED = 0x0800; 65 | const OFFLINE = 0x1000; 66 | /// File has not (yet) been indexed by the Windows Indexing Service. 67 | const NOT_CONTENT_INDEXED = 0x2000; 68 | /// File is encrypted via EFS. 69 | /// For directories, this attribute denotes that encryption is enabled by default for new files inside that directory. 70 | const ENCRYPTED = 0x4000; 71 | /// File is a directory. 72 | /// 73 | /// This attribute is only returned from [`NtfsFileName::file_attributes`]. 74 | const IS_DIRECTORY = 0x1000_0000; 75 | } 76 | } 77 | 78 | impl fmt::Display for NtfsFileAttributeFlags { 79 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 80 | fmt::Display::fmt(&self.0, f) 81 | } 82 | } 83 | 84 | /// Trait implemented by every NTFS attribute structured value. 85 | pub trait NtfsStructuredValue<'n, 'f>: Sized { 86 | const TY: NtfsAttributeType; 87 | 88 | /// Create a structured value from an arbitrary `NtfsAttributeValue`. 89 | fn from_attribute_value(fs: &mut T, value: NtfsAttributeValue<'n, 'f>) -> Result 90 | where 91 | T: Read + Seek; 92 | } 93 | 94 | /// Trait implemented by NTFS Attribute structured values that are always in resident attributes. 95 | pub trait NtfsStructuredValueFromResidentAttributeValue<'n, 'f>: 96 | NtfsStructuredValue<'n, 'f> 97 | { 98 | /// Create a structured value from a resident attribute value. 99 | /// 100 | /// This is a fast path for the few structured values that are always in resident attributes. 101 | fn from_resident_attribute_value(value: NtfsResidentAttributeValue<'f>) -> Result; 102 | } 103 | -------------------------------------------------------------------------------- /src/structured_values/object_id.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 Colin Finck 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use binrw::io::{Cursor, Read, Seek}; 5 | use binrw::BinReaderExt; 6 | 7 | use crate::attribute::NtfsAttributeType; 8 | use crate::attribute_value::{NtfsAttributeValue, NtfsResidentAttributeValue}; 9 | use crate::error::{NtfsError, Result}; 10 | use crate::guid::{NtfsGuid, GUID_SIZE}; 11 | use crate::structured_values::{ 12 | NtfsStructuredValue, NtfsStructuredValueFromResidentAttributeValue, 13 | }; 14 | use crate::types::NtfsPosition; 15 | 16 | /// Structure of an $OBJECT_ID attribute. 17 | /// 18 | /// This optional attribute contains a globally unique identifier of the file. 19 | /// 20 | /// An $OBJECT_ID attribute is always resident. 21 | /// 22 | /// Reference: 23 | #[derive(Clone, Debug)] 24 | pub struct NtfsObjectId { 25 | object_id: NtfsGuid, 26 | birth_volume_id: Option, 27 | birth_object_id: Option, 28 | domain_id: Option, 29 | } 30 | 31 | impl NtfsObjectId { 32 | fn new(r: &mut T, position: NtfsPosition, value_length: u64) -> Result 33 | where 34 | T: Read + Seek, 35 | { 36 | if value_length < GUID_SIZE as u64 { 37 | return Err(NtfsError::InvalidStructuredValueSize { 38 | position, 39 | ty: NtfsAttributeType::ObjectId, 40 | expected: GUID_SIZE as u64, 41 | actual: value_length, 42 | }); 43 | } 44 | 45 | let object_id = r.read_le::()?; 46 | 47 | let mut birth_volume_id = None; 48 | if value_length >= 2 * GUID_SIZE as u64 { 49 | birth_volume_id = Some(r.read_le::()?); 50 | } 51 | 52 | let mut birth_object_id = None; 53 | if value_length >= 3 * GUID_SIZE as u64 { 54 | birth_object_id = Some(r.read_le::()?); 55 | } 56 | 57 | let mut domain_id = None; 58 | if value_length >= 4 * GUID_SIZE as u64 { 59 | domain_id = Some(r.read_le::()?); 60 | } 61 | 62 | Ok(Self { 63 | object_id, 64 | birth_volume_id, 65 | birth_object_id, 66 | domain_id, 67 | }) 68 | } 69 | 70 | /// Returns the (optional) first Object ID that has ever been assigned to this file. 71 | pub fn birth_object_id(&self) -> Option<&NtfsGuid> { 72 | self.birth_object_id.as_ref() 73 | } 74 | 75 | /// Returns the (optional) Object ID of the $Volume file of the partition where this file was created. 76 | pub fn birth_volume_id(&self) -> Option<&NtfsGuid> { 77 | self.birth_volume_id.as_ref() 78 | } 79 | 80 | /// Returns the (optional) Domain ID of this file. 81 | pub fn domain_id(&self) -> Option<&NtfsGuid> { 82 | self.domain_id.as_ref() 83 | } 84 | 85 | /// Returns the Object ID, a globally unique identifier of the file. 86 | pub fn object_id(&self) -> &NtfsGuid { 87 | &self.object_id 88 | } 89 | } 90 | 91 | impl<'n, 'f> NtfsStructuredValue<'n, 'f> for NtfsObjectId { 92 | const TY: NtfsAttributeType = NtfsAttributeType::ObjectId; 93 | 94 | fn from_attribute_value(fs: &mut T, value: NtfsAttributeValue<'n, 'f>) -> Result 95 | where 96 | T: Read + Seek, 97 | { 98 | let position = value.data_position(); 99 | let value_length = value.len(); 100 | 101 | let mut value_attached = value.attach(fs); 102 | Self::new(&mut value_attached, position, value_length) 103 | } 104 | } 105 | 106 | impl<'n, 'f> NtfsStructuredValueFromResidentAttributeValue<'n, 'f> for NtfsObjectId { 107 | fn from_resident_attribute_value(value: NtfsResidentAttributeValue<'f>) -> Result { 108 | let position = value.data_position(); 109 | let value_length = value.len(); 110 | 111 | let mut cursor = Cursor::new(value.data()); 112 | Self::new(&mut cursor, position, value_length) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/structured_values/standard_information.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 Colin Finck 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use binrw::io::{Cursor, Read, Seek}; 5 | use binrw::{BinRead, BinReaderExt}; 6 | 7 | use crate::attribute::NtfsAttributeType; 8 | use crate::attribute_value::{NtfsAttributeValue, NtfsResidentAttributeValue}; 9 | use crate::error::{NtfsError, Result}; 10 | use crate::structured_values::{ 11 | NtfsFileAttributeFlags, NtfsStructuredValue, NtfsStructuredValueFromResidentAttributeValue, 12 | }; 13 | use crate::time::NtfsTime; 14 | use crate::types::NtfsPosition; 15 | 16 | /// Size of all [`StandardInformationData`] fields plus some reserved bytes. 17 | const STANDARD_INFORMATION_SIZE_NTFS1: usize = 48; 18 | 19 | /// Size of all [`StandardInformationData`] plus [`StandardInformationDataNtfs3`] fields. 20 | const STANDARD_INFORMATION_SIZE_NTFS3: usize = 72; 21 | 22 | #[derive(BinRead, Clone, Debug)] 23 | struct StandardInformationDataNtfs1 { 24 | creation_time: NtfsTime, 25 | modification_time: NtfsTime, 26 | mft_record_modification_time: NtfsTime, 27 | access_time: NtfsTime, 28 | file_attributes: u32, 29 | } 30 | 31 | #[derive(BinRead, Clone, Debug)] 32 | struct StandardInformationDataNtfs3 { 33 | maximum_versions: u32, 34 | version: u32, 35 | class_id: u32, 36 | owner_id: u32, 37 | security_id: u32, 38 | quota_charged: u64, 39 | usn: u64, 40 | } 41 | 42 | /// Structure of a $STANDARD_INFORMATION attribute. 43 | /// 44 | /// Among other things, this is the place where the file times and "File Attributes" 45 | /// (Read-Only, Hidden, System, Archive, etc.) are stored. 46 | /// 47 | /// A $STANDARD_INFORMATION attribute is always resident. 48 | /// 49 | /// Reference: 50 | #[derive(Clone, Debug)] 51 | pub struct NtfsStandardInformation { 52 | ntfs1_data: StandardInformationDataNtfs1, 53 | ntfs3_data: Option, 54 | } 55 | 56 | impl NtfsStandardInformation { 57 | fn new(r: &mut T, position: NtfsPosition, value_length: u64) -> Result 58 | where 59 | T: Read + Seek, 60 | { 61 | if value_length < STANDARD_INFORMATION_SIZE_NTFS1 as u64 { 62 | return Err(NtfsError::InvalidStructuredValueSize { 63 | position, 64 | ty: NtfsAttributeType::StandardInformation, 65 | expected: STANDARD_INFORMATION_SIZE_NTFS1 as u64, 66 | actual: value_length, 67 | }); 68 | } 69 | 70 | let ntfs1_data = r.read_le::()?; 71 | 72 | let mut ntfs3_data = None; 73 | if value_length >= STANDARD_INFORMATION_SIZE_NTFS3 as u64 { 74 | ntfs3_data = Some(r.read_le::()?); 75 | } 76 | 77 | Ok(Self { 78 | ntfs1_data, 79 | ntfs3_data, 80 | }) 81 | } 82 | 83 | /// Returns the time this file was last accessed. 84 | pub fn access_time(&self) -> NtfsTime { 85 | self.ntfs1_data.access_time 86 | } 87 | 88 | /// Returns the Class ID of the file, if stored via NTFS 3.x file information. 89 | pub fn class_id(&self) -> Option { 90 | self.ntfs3_data.as_ref().map(|x| x.class_id) 91 | } 92 | 93 | /// Returns the time this file was created. 94 | pub fn creation_time(&self) -> NtfsTime { 95 | self.ntfs1_data.creation_time 96 | } 97 | 98 | /// Returns flags that a user can set for a file (Read-Only, Hidden, System, Archive, etc.). 99 | /// Commonly called "File Attributes" in Windows Explorer. 100 | pub fn file_attributes(&self) -> NtfsFileAttributeFlags { 101 | NtfsFileAttributeFlags::from_bits_truncate(self.ntfs1_data.file_attributes) 102 | } 103 | 104 | /// Returns the maximum allowed versions for this file, if stored via NTFS 3.x file information. 105 | /// 106 | /// A value of zero means that versioning is disabled for this file. 107 | pub fn maximum_versions(&self) -> Option { 108 | self.ntfs3_data.as_ref().map(|x| x.maximum_versions) 109 | } 110 | 111 | /// Returns the time the MFT record of this file was last modified. 112 | pub fn mft_record_modification_time(&self) -> NtfsTime { 113 | self.ntfs1_data.mft_record_modification_time 114 | } 115 | 116 | /// Returns the time this file was last modified. 117 | pub fn modification_time(&self) -> NtfsTime { 118 | self.ntfs1_data.modification_time 119 | } 120 | 121 | /// Returns the Owner ID of the file, if stored via NTFS 3.x file information. 122 | pub fn owner_id(&self) -> Option { 123 | self.ntfs3_data.as_ref().map(|x| x.owner_id) 124 | } 125 | 126 | /// Returns the quota charged by this file, if stored via NTFS 3.x file information. 127 | pub fn quota_charged(&self) -> Option { 128 | self.ntfs3_data.as_ref().map(|x| x.quota_charged) 129 | } 130 | 131 | /// Returns the Security ID of the file, if stored via NTFS 3.x file information. 132 | pub fn security_id(&self) -> Option { 133 | self.ntfs3_data.as_ref().map(|x| x.security_id) 134 | } 135 | 136 | /// Returns the Update Sequence Number (USN) of the file, if stored via NTFS 3.x file information. 137 | pub fn usn(&self) -> Option { 138 | self.ntfs3_data.as_ref().map(|x| x.usn) 139 | } 140 | 141 | /// Returns the version of the file, if stored via NTFS 3.x file information. 142 | /// 143 | /// This will be zero if versioning is disabled for this file. 144 | pub fn version(&self) -> Option { 145 | self.ntfs3_data.as_ref().map(|x| x.version) 146 | } 147 | } 148 | 149 | impl<'n, 'f> NtfsStructuredValue<'n, 'f> for NtfsStandardInformation { 150 | const TY: NtfsAttributeType = NtfsAttributeType::StandardInformation; 151 | 152 | fn from_attribute_value(fs: &mut T, value: NtfsAttributeValue<'n, 'f>) -> Result 153 | where 154 | T: Read + Seek, 155 | { 156 | let position = value.data_position(); 157 | let value_length = value.len(); 158 | 159 | let mut value_attached = value.attach(fs); 160 | Self::new(&mut value_attached, position, value_length) 161 | } 162 | } 163 | 164 | impl<'n, 'f> NtfsStructuredValueFromResidentAttributeValue<'n, 'f> for NtfsStandardInformation { 165 | fn from_resident_attribute_value(value: NtfsResidentAttributeValue<'f>) -> Result { 166 | let position = value.data_position(); 167 | let value_length = value.len(); 168 | 169 | let mut cursor = Cursor::new(value.data()); 170 | Self::new(&mut cursor, position, value_length) 171 | } 172 | } 173 | 174 | #[cfg(test)] 175 | mod tests { 176 | use super::*; 177 | use crate::file::KnownNtfsFileRecordNumber; 178 | use crate::ntfs::Ntfs; 179 | 180 | #[test] 181 | fn test_standard_information() { 182 | let mut testfs1 = crate::helpers::tests::testfs1(); 183 | let ntfs = Ntfs::new(&mut testfs1).unwrap(); 184 | let mft = ntfs 185 | .file(&mut testfs1, KnownNtfsFileRecordNumber::MFT as u64) 186 | .unwrap(); 187 | let mut mft_attributes = mft.attributes_raw(); 188 | 189 | // Check the StandardInformation attribute of the MFT. 190 | let attribute = mft_attributes.next().unwrap().unwrap(); 191 | assert_eq!( 192 | attribute.ty().unwrap(), 193 | NtfsAttributeType::StandardInformation, 194 | ); 195 | assert_eq!(attribute.attribute_length(), 96); 196 | assert!(attribute.is_resident()); 197 | assert_eq!(attribute.name_length(), 0); 198 | assert_eq!(attribute.value_length(), 72); 199 | 200 | // Try to read the actual information. 201 | let _standard_info = attribute 202 | .resident_structured_value::() 203 | .unwrap(); 204 | 205 | // There are no reliable values to check here, so that's it. 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/structured_values/volume_information.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 Colin Finck 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use core::fmt; 5 | 6 | use binrw::io::{Cursor, Read, Seek}; 7 | use binrw::{BinRead, BinReaderExt}; 8 | use bitflags::bitflags; 9 | 10 | use crate::attribute::NtfsAttributeType; 11 | use crate::attribute_value::{NtfsAttributeValue, NtfsResidentAttributeValue}; 12 | use crate::error::{NtfsError, Result}; 13 | use crate::structured_values::{ 14 | NtfsStructuredValue, NtfsStructuredValueFromResidentAttributeValue, 15 | }; 16 | use crate::types::NtfsPosition; 17 | 18 | /// Size of all [`VolumeInformationData`] fields. 19 | const VOLUME_INFORMATION_SIZE: usize = 12; 20 | 21 | #[derive(BinRead, Clone, Debug)] 22 | struct VolumeInformationData { 23 | _reserved: u64, 24 | major_version: u8, 25 | minor_version: u8, 26 | flags: u16, 27 | } 28 | 29 | bitflags! { 30 | /// Flags returned by [`NtfsVolumeInformation::flags`]. 31 | #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 32 | pub struct NtfsVolumeFlags: u16 { 33 | /// The volume needs to be checked by `chkdsk`. 34 | const IS_DIRTY = 0x0001; 35 | const RESIZE_LOG_FILE = 0x0002; 36 | const UPGRADE_ON_MOUNT = 0x0004; 37 | const MOUNTED_ON_NT4 = 0x0008; 38 | const DELETE_USN_UNDERWAY = 0x0010; 39 | const REPAIR_OBJECT_ID = 0x0020; 40 | const CHKDSK_UNDERWAY = 0x4000; 41 | const MODIFIED_BY_CHKDSK = 0x8000; 42 | } 43 | } 44 | 45 | impl fmt::Display for NtfsVolumeFlags { 46 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 47 | fmt::Display::fmt(&self.0, f) 48 | } 49 | } 50 | 51 | /// Structure of a $VOLUME_INFORMATION attribute. 52 | /// 53 | /// This attribute is only used by the top-level $Volume file and contains general information about the filesystem. 54 | /// You can easily access it via [`Ntfs::volume_info`]. 55 | /// 56 | /// A $VOLUME_INFORMATION attribute is always resident. 57 | /// 58 | /// Reference: 59 | /// 60 | /// [`Ntfs::volume_info`]: crate::Ntfs::volume_info 61 | #[derive(Clone, Debug)] 62 | pub struct NtfsVolumeInformation { 63 | info: VolumeInformationData, 64 | } 65 | 66 | impl NtfsVolumeInformation { 67 | fn new(r: &mut T, position: NtfsPosition, value_length: u64) -> Result 68 | where 69 | T: Read + Seek, 70 | { 71 | if value_length < VOLUME_INFORMATION_SIZE as u64 { 72 | return Err(NtfsError::InvalidStructuredValueSize { 73 | position, 74 | ty: NtfsAttributeType::StandardInformation, 75 | expected: VOLUME_INFORMATION_SIZE as u64, 76 | actual: value_length, 77 | }); 78 | } 79 | 80 | let info = r.read_le::()?; 81 | 82 | Ok(Self { info }) 83 | } 84 | 85 | /// Returns flags set for this NTFS filesystem/volume as specified by [`NtfsVolumeFlags`]. 86 | pub fn flags(&self) -> NtfsVolumeFlags { 87 | NtfsVolumeFlags::from_bits_truncate(self.info.flags) 88 | } 89 | 90 | /// Returns the major NTFS version of this filesystem (e.g. `3` for NTFS 3.1). 91 | pub fn major_version(&self) -> u8 { 92 | self.info.major_version 93 | } 94 | 95 | /// Returns the minor NTFS version of this filesystem (e.g. `1` for NTFS 3.1). 96 | pub fn minor_version(&self) -> u8 { 97 | self.info.minor_version 98 | } 99 | } 100 | 101 | impl<'n, 'f> NtfsStructuredValue<'n, 'f> for NtfsVolumeInformation { 102 | const TY: NtfsAttributeType = NtfsAttributeType::VolumeInformation; 103 | 104 | fn from_attribute_value(fs: &mut T, value: NtfsAttributeValue<'n, 'f>) -> Result 105 | where 106 | T: Read + Seek, 107 | { 108 | let position = value.data_position(); 109 | let value_length = value.len(); 110 | 111 | let mut value_attached = value.attach(fs); 112 | Self::new(&mut value_attached, position, value_length) 113 | } 114 | } 115 | 116 | impl<'n, 'f> NtfsStructuredValueFromResidentAttributeValue<'n, 'f> for NtfsVolumeInformation { 117 | fn from_resident_attribute_value(value: NtfsResidentAttributeValue<'f>) -> Result { 118 | let position = value.data_position(); 119 | let value_length = value.len(); 120 | 121 | let mut cursor = Cursor::new(value.data()); 122 | Self::new(&mut cursor, position, value_length) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/structured_values/volume_name.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 Colin Finck 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use core::mem; 5 | 6 | use arrayvec::ArrayVec; 7 | use binrw::io::{Cursor, Read, Seek}; 8 | use nt_string::u16strle::U16StrLe; 9 | 10 | use crate::attribute::NtfsAttributeType; 11 | use crate::attribute_value::{NtfsAttributeValue, NtfsResidentAttributeValue}; 12 | use crate::error::{NtfsError, Result}; 13 | use crate::structured_values::{ 14 | NtfsStructuredValue, NtfsStructuredValueFromResidentAttributeValue, 15 | }; 16 | use crate::types::NtfsPosition; 17 | 18 | /// The largest VolumeName attribute has a name containing 128 UTF-16 code points (256 bytes). 19 | const VOLUME_NAME_MAX_SIZE: usize = 128 * mem::size_of::(); 20 | 21 | /// Structure of a $VOLUME_NAME attribute. 22 | /// 23 | /// This attribute is only used by the top-level $Volume file and contains the user-defined name of this filesystem. 24 | /// You can easily access it via [`Ntfs::volume_name`]. 25 | /// 26 | /// A $VOLUME_NAME attribute is always resident. 27 | /// 28 | /// Reference: 29 | /// 30 | /// [`Ntfs::volume_name`]: crate::Ntfs::volume_name 31 | #[derive(Clone, Debug)] 32 | pub struct NtfsVolumeName { 33 | name: ArrayVec, 34 | } 35 | 36 | impl NtfsVolumeName { 37 | fn new(r: &mut T, position: NtfsPosition, value_length: u64) -> Result 38 | where 39 | T: Read + Seek, 40 | { 41 | if value_length > VOLUME_NAME_MAX_SIZE as u64 { 42 | return Err(NtfsError::InvalidStructuredValueSize { 43 | position, 44 | ty: NtfsAttributeType::VolumeName, 45 | expected: VOLUME_NAME_MAX_SIZE as u64, 46 | actual: value_length, 47 | }); 48 | } 49 | 50 | let value_length = value_length as usize; 51 | 52 | let mut name = ArrayVec::from([0u8; VOLUME_NAME_MAX_SIZE]); 53 | r.read_exact(&mut name[..value_length])?; 54 | name.truncate(value_length); 55 | 56 | Ok(Self { name }) 57 | } 58 | 59 | /// Gets the volume name and returns it wrapped in a [`U16StrLe`]. 60 | pub fn name(&self) -> U16StrLe { 61 | U16StrLe(&self.name) 62 | } 63 | 64 | /// Returns the volume name length, in bytes. 65 | /// 66 | /// A volume name has a maximum length of 128 UTF-16 code points (256 bytes). 67 | pub fn name_length(&self) -> usize { 68 | self.name.len() 69 | } 70 | } 71 | 72 | impl<'n, 'f> NtfsStructuredValue<'n, 'f> for NtfsVolumeName { 73 | const TY: NtfsAttributeType = NtfsAttributeType::VolumeName; 74 | 75 | fn from_attribute_value(fs: &mut T, value: NtfsAttributeValue<'n, 'f>) -> Result 76 | where 77 | T: Read + Seek, 78 | { 79 | let position = value.data_position(); 80 | let value_length = value.len(); 81 | 82 | let mut value_attached = value.attach(fs); 83 | Self::new(&mut value_attached, position, value_length) 84 | } 85 | } 86 | 87 | impl<'n, 'f> NtfsStructuredValueFromResidentAttributeValue<'n, 'f> for NtfsVolumeName { 88 | fn from_resident_attribute_value(value: NtfsResidentAttributeValue<'f>) -> Result { 89 | let position = value.data_position(); 90 | let value_length = value.len(); 91 | 92 | let mut cursor = Cursor::new(value.data()); 93 | Self::new(&mut cursor, position, value_length) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/time.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 Colin Finck 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use binrw::BinRead; 5 | use derive_more::From; 6 | 7 | #[cfg(feature = "time")] 8 | use {crate::error::NtfsError, time::OffsetDateTime}; 9 | 10 | #[cfg(feature = "std")] 11 | use std::time::{SystemTime, SystemTimeError}; 12 | 13 | /// Difference in 100-nanosecond intervals between the Windows/NTFS epoch (1601-01-01) and the Unix epoch (1970-01-01). 14 | #[cfg(any(feature = "time", feature = "std"))] 15 | const EPOCH_DIFFERENCE_IN_INTERVALS: u64 = 116_444_736_000_000_000; 16 | 17 | /// Number of 100-nanosecond intervals in a second. 18 | #[cfg(any(feature = "time", feature = "std"))] 19 | const INTERVALS_PER_SECOND: u64 = 10_000_000; 20 | 21 | /// An NTFS timestamp, used for expressing file times. 22 | /// 23 | /// NTFS (and the Windows NT line of operating systems) represent time as an unsigned 64-bit integer 24 | /// counting the number of 100-nanosecond intervals since January 1, 1601. 25 | #[derive(BinRead, Clone, Copy, Debug, Eq, From, Ord, PartialEq, PartialOrd)] 26 | pub struct NtfsTime(u64); 27 | 28 | impl NtfsTime { 29 | /// Returns the stored NT timestamp (number of 100-nanosecond intervals since January 1, 1601). 30 | pub fn nt_timestamp(&self) -> u64 { 31 | self.0 32 | } 33 | } 34 | 35 | #[cfg(feature = "time")] 36 | #[cfg_attr(docsrs, doc(cfg(feature = "time")))] 37 | impl TryFrom for NtfsTime { 38 | type Error = NtfsError; 39 | 40 | fn try_from(dt: OffsetDateTime) -> Result { 41 | let nanos_since_unix_epoch = dt.unix_timestamp_nanos(); 42 | let intervals_since_unix_epoch = nanos_since_unix_epoch / 100; 43 | let intervals_since_windows_epoch = 44 | intervals_since_unix_epoch + EPOCH_DIFFERENCE_IN_INTERVALS as i128; 45 | let nt_timestamp = 46 | u64::try_from(intervals_since_windows_epoch).map_err(|_| NtfsError::InvalidTime)?; 47 | 48 | Ok(Self(nt_timestamp)) 49 | } 50 | } 51 | 52 | #[cfg(feature = "time")] 53 | #[cfg_attr(docsrs, doc(cfg(feature = "time")))] 54 | impl From for OffsetDateTime { 55 | fn from(nt: NtfsTime) -> OffsetDateTime { 56 | let intervals_since_windows_epoch = nt.nt_timestamp() as i128; 57 | let intervals_since_unix_epoch = 58 | intervals_since_windows_epoch - EPOCH_DIFFERENCE_IN_INTERVALS as i128; 59 | let nanos_since_unix_epoch = intervals_since_unix_epoch * 100; 60 | 61 | OffsetDateTime::from_unix_timestamp_nanos(nanos_since_unix_epoch).unwrap() 62 | } 63 | } 64 | 65 | #[cfg(feature = "std")] 66 | #[cfg_attr(docsrs, doc(cfg(feature = "std")))] 67 | impl TryFrom for NtfsTime { 68 | type Error = SystemTimeError; 69 | 70 | fn try_from(st: SystemTime) -> Result { 71 | let duration_since_unix_epoch = st.duration_since(SystemTime::UNIX_EPOCH)?; 72 | let intervals_since_unix_epoch = duration_since_unix_epoch.as_secs() * INTERVALS_PER_SECOND 73 | + duration_since_unix_epoch.subsec_nanos() as u64 / 100; 74 | let intervals_since_windows_epoch = 75 | intervals_since_unix_epoch + EPOCH_DIFFERENCE_IN_INTERVALS; 76 | 77 | Ok(Self(intervals_since_windows_epoch)) 78 | } 79 | } 80 | 81 | #[cfg(test)] 82 | pub(crate) mod tests { 83 | use super::*; 84 | 85 | #[cfg(feature = "time")] 86 | use time::macros::datetime; 87 | 88 | pub(crate) const NT_TIMESTAMP_2021_01_01: u64 = 132539328000000000u64; 89 | 90 | #[cfg(feature = "time")] 91 | #[test] 92 | fn test_offsetdatetime() { 93 | let dt = datetime!(2013-01-05 18:15 UTC); 94 | let nt = NtfsTime::try_from(dt).unwrap(); 95 | assert_eq!(nt.nt_timestamp(), 130018833000000000u64); 96 | 97 | let dt2 = OffsetDateTime::from(nt); 98 | assert_eq!(dt, dt2); 99 | 100 | let dt = datetime!(1601-01-01 0:00 UTC); 101 | let nt = NtfsTime::try_from(dt).unwrap(); 102 | assert_eq!(nt.nt_timestamp(), 0u64); 103 | 104 | let dt = datetime!(1600-12-31 23:59:59 UTC); 105 | assert!(NtfsTime::try_from(dt).is_err()); 106 | 107 | let dt = datetime!(+60056-05-28 0:00 UTC); 108 | assert!(NtfsTime::try_from(dt).is_ok()); 109 | 110 | let dt = datetime!(+60056-05-29 0:00 UTC); 111 | assert!(NtfsTime::try_from(dt).is_err()); 112 | } 113 | 114 | #[cfg(feature = "std")] 115 | #[test] 116 | fn test_systemtime() { 117 | let st = SystemTime::now(); 118 | let nt = NtfsTime::try_from(st).unwrap(); 119 | assert!(nt.nt_timestamp() > NT_TIMESTAMP_2021_01_01); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/traits.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 Colin Finck 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use binrw::io; 5 | use binrw::io::{Read, Seek, SeekFrom}; 6 | 7 | use crate::error::{NtfsError, Result}; 8 | 9 | /// Trait to read/seek in a source by the help of a temporarily passed mutable reference to the filesystem reader. 10 | /// 11 | /// By requiring the user to pass the filesystem reader on every read, we circumvent the problems associated with permanently 12 | /// holding a mutable reference. 13 | /// If we held one, we could not read from two objects in alternation. 14 | pub trait NtfsReadSeek { 15 | /// See [`std::io::Read::read`]. 16 | fn read(&mut self, fs: &mut T, buf: &mut [u8]) -> Result 17 | where 18 | T: Read + Seek; 19 | 20 | /// See [`std::io::Read::read_exact`]. 21 | fn read_exact(&mut self, fs: &mut T, mut buf: &mut [u8]) -> Result<()> 22 | where 23 | T: Read + Seek, 24 | { 25 | // This implementation is taken from https://github.com/rust-lang/rust/blob/5662d9343f0696efcc38a1264656737c9f22d427/library/std/src/io/mod.rs 26 | // It handles all corner cases properly and outputs the known `io` error messages. 27 | while !buf.is_empty() { 28 | match self.read(fs, buf) { 29 | Ok(0) => break, 30 | Ok(n) => { 31 | buf = &mut buf[n..]; 32 | } 33 | Err(NtfsError::Io(e)) if e.kind() == io::ErrorKind::Interrupted => {} 34 | Err(e) => return Err(e), 35 | } 36 | } 37 | 38 | if !buf.is_empty() { 39 | Err(NtfsError::Io(io::Error::new( 40 | io::ErrorKind::UnexpectedEof, 41 | "failed to fill whole buffer", 42 | ))) 43 | } else { 44 | Ok(()) 45 | } 46 | } 47 | 48 | /// See [`std::io::Seek::seek`]. 49 | fn seek(&mut self, fs: &mut T, pos: SeekFrom) -> Result 50 | where 51 | T: Read + Seek; 52 | 53 | /// See [`std::io::Seek::stream_position`]. 54 | fn stream_position(&self) -> u64; 55 | } 56 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 Colin Finck 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | // 4 | //! Supplementary helper types. 5 | 6 | use core::fmt; 7 | use core::num::NonZeroU64; 8 | use core::ops::{Add, AddAssign}; 9 | 10 | use binrw::BinRead; 11 | use derive_more::{Binary, Display, From, LowerHex, Octal, UpperHex}; 12 | 13 | use crate::error::{NtfsError, Result}; 14 | use crate::ntfs::Ntfs; 15 | 16 | /// An absolute nonzero byte position on the NTFS filesystem. 17 | /// Can be used to seek, but even more often in [`NtfsError`] variants to assist with debugging. 18 | /// 19 | /// Note that there may be cases when no valid position can be given for the current situation. 20 | /// For example, this may happen when a reader is on a sparse Data Run or it has been seeked to a 21 | /// position outside the valid range. 22 | /// Therefore, this structure internally uses an [`Option`] of a [`NonZeroU64`] to alternatively 23 | /// store a `None` value if no valid position can be given. 24 | #[derive(Clone, Copy, Debug, Eq, From, Ord, PartialEq, PartialOrd)] 25 | pub struct NtfsPosition(Option); 26 | 27 | impl NtfsPosition { 28 | const NONE_STR: &'static str = ""; 29 | 30 | pub(crate) const fn new(position: u64) -> Self { 31 | Self(NonZeroU64::new(position)) 32 | } 33 | 34 | pub(crate) const fn none() -> Self { 35 | Self(None) 36 | } 37 | 38 | /// Returns the stored position, or `None` if there is no valid position. 39 | pub const fn value(&self) -> Option { 40 | self.0 41 | } 42 | } 43 | 44 | impl Add for NtfsPosition { 45 | type Output = Self; 46 | 47 | fn add(self, other: u16) -> Self { 48 | self + other as u64 49 | } 50 | } 51 | 52 | impl Add for NtfsPosition { 53 | type Output = Self; 54 | 55 | fn add(self, other: u64) -> Self { 56 | let new_value = self 57 | .0 58 | .and_then(|position| NonZeroU64::new(position.get().wrapping_add(other))); 59 | Self(new_value) 60 | } 61 | } 62 | 63 | impl Add for NtfsPosition { 64 | type Output = Self; 65 | 66 | fn add(self, other: usize) -> Self { 67 | self + other as u64 68 | } 69 | } 70 | 71 | impl AddAssign for NtfsPosition { 72 | fn add_assign(&mut self, other: u16) { 73 | *self = *self + other; 74 | } 75 | } 76 | 77 | impl AddAssign for NtfsPosition { 78 | fn add_assign(&mut self, other: u64) { 79 | *self = *self + other; 80 | } 81 | } 82 | 83 | impl AddAssign for NtfsPosition { 84 | fn add_assign(&mut self, other: usize) { 85 | *self = *self + other; 86 | } 87 | } 88 | 89 | impl fmt::Binary for NtfsPosition { 90 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 91 | match self.0 { 92 | Some(position) => fmt::Binary::fmt(&position, f), 93 | None => fmt::Display::fmt(Self::NONE_STR, f), 94 | } 95 | } 96 | } 97 | 98 | impl fmt::Display for NtfsPosition { 99 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 100 | match self.0 { 101 | Some(position) => fmt::Display::fmt(&position, f), 102 | None => fmt::Display::fmt(Self::NONE_STR, f), 103 | } 104 | } 105 | } 106 | 107 | impl fmt::LowerHex for NtfsPosition { 108 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 109 | match self.0 { 110 | Some(position) => fmt::LowerHex::fmt(&position, f), 111 | None => fmt::Display::fmt(Self::NONE_STR, f), 112 | } 113 | } 114 | } 115 | 116 | impl fmt::Octal for NtfsPosition { 117 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 118 | match self.0 { 119 | Some(position) => fmt::Octal::fmt(&position, f), 120 | None => fmt::Display::fmt(Self::NONE_STR, f), 121 | } 122 | } 123 | } 124 | 125 | impl fmt::UpperHex for NtfsPosition { 126 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 127 | match self.0 { 128 | Some(position) => fmt::UpperHex::fmt(&position, f), 129 | None => fmt::Display::fmt(Self::NONE_STR, f), 130 | } 131 | } 132 | } 133 | 134 | impl From for NtfsPosition { 135 | fn from(value: NonZeroU64) -> Self { 136 | Self(Some(value)) 137 | } 138 | } 139 | 140 | /// A Logical Cluster Number (LCN). 141 | /// 142 | /// NTFS divides a filesystem into clusters of a given size (power of two), see [`Ntfs::cluster_size`]. 143 | /// The LCN is an absolute cluster index into the filesystem. 144 | #[derive( 145 | Binary, 146 | BinRead, 147 | Clone, 148 | Copy, 149 | Debug, 150 | Display, 151 | Eq, 152 | From, 153 | LowerHex, 154 | Octal, 155 | Ord, 156 | PartialEq, 157 | PartialOrd, 158 | UpperHex, 159 | )] 160 | pub struct Lcn(u64); 161 | 162 | impl Lcn { 163 | /// Performs a checked addition of the given Virtual Cluster Number (VCN), returning a new LCN. 164 | pub fn checked_add(&self, vcn: Vcn) -> Option { 165 | if vcn.0 >= 0 { 166 | self.0.checked_add(vcn.0 as u64).map(Into::into) 167 | } else { 168 | self.0 169 | .checked_sub(vcn.0.wrapping_neg() as u64) 170 | .map(Into::into) 171 | } 172 | } 173 | 174 | /// Returns the absolute byte position of this LCN within the filesystem. 175 | pub fn position(&self, ntfs: &Ntfs) -> Result { 176 | let value = self 177 | .0 178 | .checked_mul(ntfs.cluster_size() as u64) 179 | .ok_or(NtfsError::LcnTooBig { lcn: *self })?; 180 | Ok(NtfsPosition::new(value)) 181 | } 182 | 183 | /// Returns the stored Logical Cluster Number. 184 | pub fn value(&self) -> u64 { 185 | self.0 186 | } 187 | } 188 | 189 | /// A Virtual Cluster Number (VCN). 190 | /// 191 | /// NTFS divides a filesystem into clusters of a given size (power of two), see [`Ntfs::cluster_size`]. 192 | /// The VCN is a cluster index into the filesystem that is relative to a Logical Cluster Number (LCN) 193 | /// or relative to the start of an attribute value. 194 | #[derive( 195 | Binary, 196 | BinRead, 197 | Clone, 198 | Copy, 199 | Debug, 200 | Display, 201 | Eq, 202 | From, 203 | LowerHex, 204 | Octal, 205 | Ord, 206 | PartialEq, 207 | PartialOrd, 208 | UpperHex, 209 | )] 210 | pub struct Vcn(i64); 211 | 212 | impl Vcn { 213 | /// Converts this VCN into a byte offset (with respect to the cluster size of the provided [`Ntfs`] filesystem). 214 | pub fn offset(&self, ntfs: &Ntfs) -> Result { 215 | self.0 216 | .checked_mul(ntfs.cluster_size() as i64) 217 | .ok_or(NtfsError::VcnTooBig { vcn: *self }) 218 | } 219 | 220 | /// Returns the stored Virtual Cluster Number. 221 | pub fn value(&self) -> i64 { 222 | self.0 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/upcase_table.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 Colin Finck 2 | // SPDX-License-Identifier: MIT OR Apache-2.0 3 | 4 | use core::cmp::Ordering; 5 | use core::mem; 6 | 7 | use alloc::vec; 8 | use alloc::vec::Vec; 9 | use binrw::io::{Read, Seek}; 10 | use nt_string::u16strle::U16StrLe; 11 | 12 | use crate::attribute::NtfsAttributeType; 13 | use crate::error::{NtfsError, Result}; 14 | use crate::file::KnownNtfsFileRecordNumber; 15 | use crate::ntfs::Ntfs; 16 | use crate::traits::NtfsReadSeek; 17 | 18 | /// The Upcase Table contains an uppercase character for each Unicode character of the Basic Multilingual Plane. 19 | const UPCASE_CHARACTER_COUNT: usize = 65536; 20 | 21 | /// Hence, the table has a size of 128 KiB. 22 | const UPCASE_TABLE_SIZE: u64 = (UPCASE_CHARACTER_COUNT * mem::size_of::()) as u64; 23 | 24 | /// Manages a table for converting characters to uppercase. 25 | /// This table is used for case-insensitive file name comparisons. 26 | /// 27 | /// NTFS stores such a table in the special $UpCase file on every filesystem. 28 | /// As this table is slightly different depending on the Windows version used for creating the filesystem, 29 | /// it is very important to always read the table from the filesystem itself. 30 | /// Hence, this table is not hardcoded into the crate. 31 | #[derive(Clone, Debug)] 32 | pub(crate) struct UpcaseTable { 33 | uppercase_characters: Vec, 34 | } 35 | 36 | impl UpcaseTable { 37 | /// Reads the $UpCase file from the given filesystem into a new [`UpcaseTable`] object. 38 | pub(crate) fn read(ntfs: &Ntfs, fs: &mut T) -> Result 39 | where 40 | T: Read + Seek, 41 | { 42 | // Lookup the $UpCase file and its $DATA attribute. 43 | let upcase_file = ntfs.file(fs, KnownNtfsFileRecordNumber::UpCase as u64)?; 44 | let data_item = upcase_file 45 | .data(fs, "") 46 | .ok_or(NtfsError::AttributeNotFound { 47 | position: upcase_file.position(), 48 | ty: NtfsAttributeType::Data, 49 | })??; 50 | 51 | let data_attribute = data_item.to_attribute()?; 52 | if data_attribute.value_length() != UPCASE_TABLE_SIZE { 53 | return Err(NtfsError::InvalidUpcaseTableSize { 54 | expected: UPCASE_TABLE_SIZE, 55 | actual: data_attribute.value_length(), 56 | }); 57 | } 58 | 59 | // Read the entire raw data from the $DATA attribute. 60 | let mut data_value = data_attribute.value(fs)?; 61 | let mut data = vec![0u8; UPCASE_TABLE_SIZE as usize]; 62 | data_value.read_exact(fs, &mut data)?; 63 | 64 | // Store it in an array of `u16` uppercase characters. 65 | // Any endianness conversion is done here once, which makes `u16_to_uppercase` fast. 66 | let uppercase_characters = data 67 | .chunks_exact(2) 68 | .map(|two_bytes| u16::from_le_bytes(two_bytes.try_into().unwrap())) 69 | .collect(); 70 | 71 | Ok(Self { 72 | uppercase_characters, 73 | }) 74 | } 75 | 76 | /// Returns the uppercase variant of the given UCS-2 character (i.e. a Unicode character 77 | /// from the Basic Multilingual Plane) based on the stored conversion table. 78 | /// A character without an uppercase equivalent is returned as-is. 79 | pub(crate) fn u16_to_uppercase(&self, character: u16) -> u16 { 80 | self.uppercase_characters[character as usize] 81 | } 82 | } 83 | 84 | /// Trait for a case-insensitive ordering with respect to the $UpCase table read from the filesystem. 85 | pub trait UpcaseOrd { 86 | /// Performs a case-insensitive ordering based on the $UpCase table read from the filesystem. 87 | /// 88 | /// # Panics 89 | /// 90 | /// Panics if [`read_upcase_table`][Ntfs::read_upcase_table] had not been called on the passed [`Ntfs`] object. 91 | fn upcase_cmp(&self, ntfs: &Ntfs, other: &Rhs) -> Ordering; 92 | } 93 | 94 | impl<'a, 'b> UpcaseOrd> for U16StrLe<'b> { 95 | fn upcase_cmp(&self, ntfs: &Ntfs, other: &U16StrLe<'a>) -> Ordering { 96 | upcase_cmp_iter(self.u16_iter(), other.u16_iter(), ntfs) 97 | } 98 | } 99 | 100 | impl<'a> UpcaseOrd<&str> for U16StrLe<'a> { 101 | fn upcase_cmp(&self, ntfs: &Ntfs, other: &&str) -> Ordering { 102 | upcase_cmp_iter(self.u16_iter(), other.encode_utf16(), ntfs) 103 | } 104 | } 105 | 106 | impl<'a> UpcaseOrd> for &str { 107 | fn upcase_cmp(&self, ntfs: &Ntfs, other: &U16StrLe<'a>) -> Ordering { 108 | upcase_cmp_iter(self.encode_utf16(), other.u16_iter(), ntfs) 109 | } 110 | } 111 | 112 | fn upcase_cmp_iter(mut this_iter: TI, mut other_iter: OI, ntfs: &Ntfs) -> Ordering 113 | where 114 | TI: Iterator, 115 | OI: Iterator, 116 | { 117 | let upcase_table = ntfs.upcase_table(); 118 | 119 | loop { 120 | match (this_iter.next(), other_iter.next()) { 121 | (Some(this_code_unit), Some(other_code_unit)) => { 122 | // We have two UTF-16 code units to compare. 123 | let this_upper = upcase_table.u16_to_uppercase(this_code_unit); 124 | let other_upper = upcase_table.u16_to_uppercase(other_code_unit); 125 | 126 | if this_upper != other_upper { 127 | return this_upper.cmp(&other_upper); 128 | } 129 | } 130 | (Some(_), None) => { 131 | // `this_iter` is longer than `other_iter` but otherwise equal. 132 | return Ordering::Greater; 133 | } 134 | (None, Some(_)) => { 135 | // `other_iter` is longer than `this_iter` but otherwise equal. 136 | return Ordering::Less; 137 | } 138 | (None, None) => { 139 | // We made it to the end of both strings, so they must be equal. 140 | return Ordering::Equal; 141 | } 142 | } 143 | } 144 | } 145 | 146 | #[cfg(test)] 147 | mod tests { 148 | use super::*; 149 | 150 | #[test] 151 | fn test_upcase_table() { 152 | let mut testfs1 = crate::helpers::tests::testfs1(); 153 | let ntfs = Ntfs::new(&mut testfs1).unwrap(); 154 | let upcase_table = UpcaseTable::read(&ntfs, &mut testfs1).unwrap(); 155 | 156 | // Prove that at least the lowercase English characters are mapped to their uppercase equivalents. 157 | // It makes no sense to check everything here. 158 | for (lowercase, uppercase) in (b'a'..=b'z').zip(b'A'..=b'Z') { 159 | assert_eq!( 160 | upcase_table.u16_to_uppercase(lowercase as u16), 161 | uppercase as u16 162 | ); 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /testdata/create-testfs1.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | if [ "`whoami`" != "root" ]; then 5 | echo Needs to be run as root! 6 | exit 1 7 | fi 8 | 9 | dd if=/dev/zero of=testfs1 bs=1k count=2048 10 | mkntfs -c 512 -L mylabel -F testfs1 11 | 12 | mkdir mnt 13 | mount -t ntfs-3g -o loop testfs1 mnt 14 | cd mnt 15 | 16 | # Create a file with a specific modification time that we can check. 17 | touch -m -t 202101011337 empty-file 18 | 19 | # Create a 5-bytes file with resident data. 20 | echo -n 12345 > file-with-12345 21 | 22 | # Create a 1000-bytes file with non-resident data. 23 | for i in {1..200}; do 24 | echo -n 12345 >> 1000-bytes-file 25 | done 26 | 27 | # Create a sparse file with data at the beginning and at the end. 28 | echo -n 12345 > sparse-file 29 | tr '\0' '1' < /dev/zero | dd of=sparse-file seek=500000 bs=1 count=5 30 | 31 | # Create so many directories that the filesystem needs an INDEX_ROOT and INDEX_ALLOCATION. 32 | mkdir many_subdirs 33 | cd many_subdirs 34 | for i in {1..512}; do 35 | mkdir $i 36 | done 37 | cd .. 38 | 39 | cd .. 40 | umount mnt 41 | rmdir mnt 42 | -------------------------------------------------------------------------------- /testdata/testfs1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColinFinck/ntfs/cf4c127268cdceb2a9f17503dc3fb014071a386c/testdata/testfs1 --------------------------------------------------------------------------------