diff options
author | Colin Finck <colin@reactos.org> | 2021-04-19 18:00:33 +0300 |
---|---|---|
committer | Colin Finck <colin@reactos.org> | 2021-04-20 07:53:54 +0300 |
commit | 20e0600072e8e837d9b478cc77871a43a16a446d (patch) | |
tree | 7a5f32ca17196b7a7682276d8af542ba236fde8b |
Initial implementation of an NTFS filesystem crate, with access to NTFS files, resident attributes, StandardInformation and FileName structures, string and time parsing, with real filesystem tests for all of that.
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Cargo.toml | 19 | ||||
-rw-r--r-- | LICENSE | 339 | ||||
-rw-r--r-- | src/attribute.rs | 410 | ||||
-rw-r--r-- | src/attribute_value.rs | 215 | ||||
-rw-r--r-- | src/boot_sector.rs | 91 | ||||
-rw-r--r-- | src/error.rs | 73 | ||||
-rw-r--r-- | src/helpers.rs | 26 | ||||
-rw-r--r-- | src/lib.rs | 28 | ||||
-rw-r--r-- | src/ntfs.rs | 129 | ||||
-rw-r--r-- | src/ntfs_file.rs | 98 | ||||
-rw-r--r-- | src/string.rs | 163 | ||||
-rw-r--r-- | src/structured_values/attribute_list.rs | 2 | ||||
-rw-r--r-- | src/structured_values/file_name.rs | 142 | ||||
-rw-r--r-- | src/structured_values/mod.rs | 44 | ||||
-rw-r--r-- | src/structured_values/object_id.rs | 2 | ||||
-rw-r--r-- | src/structured_values/security_descriptor.rs | 2 | ||||
-rw-r--r-- | src/structured_values/standard_information.rs | 119 | ||||
-rw-r--r-- | src/structured_values/volume_information.rs | 2 | ||||
-rw-r--r-- | src/structured_values/volume_name.rs | 2 | ||||
-rw-r--r-- | src/time.rs | 162 | ||||
-rw-r--r-- | testdata/create-testfs1.sh | 25 | ||||
-rw-r--r-- | testdata/testfs1 | bin | 0 -> 1049600 bytes |
23 files changed, 2095 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8e4da25 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ntfs" +version = "0.1.0" +authors = ["Colin Finck <colin@reactos.org>"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +binread = { path = "../binread/binread", features = ["const_generics"], default-features = false } +chrono = { version = "0.4.19", optional = true } +bitflags = "1.2.1" +displaydoc = { version = "0.1.7", default-features = false } +enumn = "0.1.3" +memoffset = "0.6.1" + +[features] +default = ["std"] +std = ["binread/std"] @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/src/attribute.rs b/src/attribute.rs new file mode 100644 index 0000000..254fa58 --- /dev/null +++ b/src/attribute.rs @@ -0,0 +1,410 @@ +// Copyright 2021 Colin Finck <colin@reactos.org> +// SPDX-License-Identifier: GPL-2.0-or-later + +use crate::attribute_value::{NtfsAttributeResidentValue, NtfsAttributeValue}; +use crate::error::{NtfsError, Result}; +use crate::ntfs_file::NtfsFile; +use crate::string::NtfsString; +use crate::structured_values::{NtfsFileName, NtfsStandardInformation, NtfsStructuredValue}; +use binread::io::{Read, Seek, SeekFrom}; +use binread::{BinRead, BinReaderExt}; +use bitflags::bitflags; +use core::iter::FusedIterator; +use core::mem; +use core::ops::Range; +use enumn::N; + +/// On-disk structure of the generic header of an NTFS attribute. +#[allow(unused)] +#[derive(BinRead)] +struct NtfsAttributeHeader { + /// Type of the attribute, known types are in [`NtfsAttributeType`]. + ty: u32, + /// Length of the resident part of this attribute, in bytes. + length: u32, + /// 0 if this attribute has a resident value, 1 if this attribute has a non-resident value. + is_non_resident: u8, + /// Length of the name, in UTF-16 code points (every code point is 2 bytes). + name_length: u8, + /// Offset to the beginning of the name, in bytes from the beginning of this header. + name_offset: u16, + /// Flags of the attribute, known flags are in [`NtfsAttributeFlags`]. + flags: u16, + /// Identifier of this attribute that is unique within the [`NtfsFile`]. + instance: u16, +} + +impl NtfsAttributeHeader { + fn is_resident(&self) -> bool { + self.is_non_resident == 0 + } +} + +bitflags! { + pub struct NtfsAttributeFlags: u16 { + /// The attribute value is compressed. + const COMPRESSED = 0x0001; + /// The attribute value is encrypted. + const ENCRYPTED = 0x4000; + /// The attribute value is stored sparsely. + const SPARSE = 0x8000; + } +} + +/// On-disk structure of the extra header of an NTFS attribute that has a resident value. +#[allow(unused)] +#[derive(BinRead)] +struct NtfsAttributeResidentHeader { + /// Length of the value, in bytes. + value_length: u32, + /// Offset to the beginning of the value, in bytes from the beginning of the [`NtfsAttributeHeader`]. + value_offset: u16, + /// 1 if this attribute (with resident value) is referenced in an index. + indexed_flag: u8, +} + +/// On-disk structure of the extra header of an NTFS attribute that has a non-resident value. +#[allow(unused)] +#[derive(BinRead)] +struct NtfsAttributeNonResidentHeader { + /// Lower boundary of Virtual Cluster Numbers (VCNs) referenced by this attribute. + /// This becomes relevant when file data is split over multiple attributes. + /// Otherwise, it's zero. + lowest_vcn: i64, + /// Upper boundary of Virtual Cluster Numbers (VCNs) referenced by this attribute. + /// This becomes relevant when file data is split over multiple attributes. + /// Otherwise, it's zero (or even -1 for zero-length files according to NTFS-3G). + highest_vcn: i64, + /// Offset to the beginning of the value data runs. + data_runs_offset: u16, + /// Binary exponent denoting the number of clusters in a compression unit. + /// A typical value is 4, meaning that 2^4 = 16 clusters are part of a compression unit. + /// A value of zero means no compression (but that should better be determined via + /// [`NtfsAttributeFlags`]). + compression_unit_exponent: u8, + reserved: [u8; 5], + /// Allocated space for the attribute value, in bytes. This is always a multiple of the cluster size. + /// For compressed files, this is always a multiple of the compression unit size. + allocated_size: i64, + /// Size of the attribute value, in bytes. + /// This can be larger than `allocated_size` if the value is compressed or stored sparsely. + data_size: i64, + /// Size of the initialized part of the attribute value, in bytes. + /// This is usually the same as `data_size`. + initialized_size: i64, +} + +#[derive(Clone, Copy, Debug, Eq, N, PartialEq)] +#[repr(u32)] +pub enum NtfsAttributeType { + StandardInformation = 0x10, + AttributeList = 0x20, + FileName = 0x30, + ObjectId = 0x40, + SecurityDescriptor = 0x50, + VolumeName = 0x60, + VolumeInformation = 0x70, + Data = 0x80, + IndexRoot = 0x90, + IndexAllocation = 0xA0, + Bitmap = 0xB0, + ReparsePoint = 0xC0, + EAInformation = 0xD0, + EA = 0xE0, + PropertySet = 0xF0, + LoggedUtilityStream = 0x100, + End = 0xFFFF_FFFF, +} + +enum NtfsAttributeExtraHeader { + Resident(NtfsAttributeResidentHeader), + NonResident(NtfsAttributeNonResidentHeader), +} + +impl NtfsAttributeExtraHeader { + fn new<T>(fs: &mut T, header: &NtfsAttributeHeader) -> Result<Self> + where + T: Read + Seek, + { + if header.is_resident() { + // Read the resident header. + let resident_header = fs.read_le::<NtfsAttributeResidentHeader>()?; + Ok(Self::Resident(resident_header)) + } else { + // Read the non-resident header. + let non_resident_header = fs.read_le::<NtfsAttributeNonResidentHeader>()?; + Ok(Self::NonResident(non_resident_header)) + } + } +} + +pub struct NtfsAttribute { + position: u64, + header: NtfsAttributeHeader, + extra_header: NtfsAttributeExtraHeader, +} + +impl NtfsAttribute { + fn new<T>(fs: &mut T, position: u64) -> Result<Self> + where + T: Read + Seek, + { + // Read the common header for resident and non-resident attributes. + fs.seek(SeekFrom::Start(position))?; + let header = fs.read_le::<NtfsAttributeHeader>()?; + + // This must be a real attribute and not an end marker! + // The caller must have already checked for potential end markers. + debug_assert!(header.ty != NtfsAttributeType::End as u32); + + // Read the extra header specific to the attribute type. + let extra_header = NtfsAttributeExtraHeader::new(fs, &header)?; + + let attribute = Self { + position, + header, + extra_header, + }; + Ok(attribute) + } + + /// Returns the length of this NTFS attribute, in bytes. + /// + /// This denotes the length of the attribute structure on disk. + /// Apart from various headers, this structure also includes the name and, + /// for resident attributes, the actual value. + pub fn attribute_length(&self) -> u32 { + self.header.length + } + + /// Returns flags set for this attribute as specified by [`NtfsAttributeFlags`]. + pub fn flags(&self) -> NtfsAttributeFlags { + NtfsAttributeFlags::from_bits_truncate(self.header.flags) + } + + /// Returns `true` if this is a resident attribute, i.e. one where its value + /// is part of the attribute structure. + pub fn is_resident(&self) -> bool { + self.header.is_resident() + } + + /// Returns the length of the name of this NTFS attribute, in bytes. + /// + /// An attribute name has a maximum length of 255 UTF-16 code points (510 bytes). + /// It is always part of the attribute itself and hence also of the length + /// returned by [`NtfsAttribute::attribute_length`]. + pub fn name_length(&self) -> usize { + self.header.name_length as usize * mem::size_of::<u16>() + } + + /// Returns the absolute position of this NTFS attribute within the filesystem, in bytes. + pub fn position(&self) -> u64 { + self.position + } + + /// Reads the name of this NTFS attribute into the given buffer, and returns an + /// [`NtfsString`] wrapping that buffer. + /// + /// Note that most NTFS attributes have no name and are distinguished by their types. + /// Use [`NtfsAttribute::ty`] to get the attribute type. + pub fn read_name<'a, T>(&self, fs: &mut T, buf: &'a mut [u8]) -> Result<NtfsString<'a>> + where + T: Read + Seek, + { + let name_length = self.name_length(); + if buf.len() < name_length { + return Err(NtfsError::BufferTooSmall { + expected: name_length, + actual: buf.len(), + }); + } + + let name_position = self.position + self.header.name_offset as u64; + fs.seek(SeekFrom::Start(name_position))?; + fs.read_exact(&mut buf[..name_length])?; + + Ok(NtfsString(&buf[..name_length])) + } + + pub fn read_structured_value<T>(&self, fs: &mut T) -> Result<NtfsStructuredValue> + where + T: Read + Seek, + { + let attached_value = self.value().attach(fs); + + match self.ty()? { + NtfsAttributeType::StandardInformation => { + let inner = NtfsStandardInformation::new( + self.position, + attached_value, + self.value_length(), + )?; + Ok(NtfsStructuredValue::StandardInformation(inner)) + } + NtfsAttributeType::AttributeList => panic!("TODO"), + NtfsAttributeType::FileName => { + let inner = NtfsFileName::new(self.position, attached_value, self.value_length())?; + Ok(NtfsStructuredValue::FileName(inner)) + } + NtfsAttributeType::ObjectId => panic!("TODO"), + NtfsAttributeType::SecurityDescriptor => panic!("TODO"), + NtfsAttributeType::VolumeName => panic!("TODO"), + NtfsAttributeType::VolumeInformation => panic!("TODO"), + ty => Err(NtfsError::UnsupportedStructuredValue { + position: self.position, + ty, + }), + } + } + + /// Returns the type of this NTFS attribute, or [`NtfsError::UnsupportedNtfsAttributeType`] + /// if it's an unknown type. + pub fn ty(&self) -> Result<NtfsAttributeType> { + NtfsAttributeType::n(self.header.ty).ok_or(NtfsError::UnsupportedNtfsAttributeType { + position: self.position, + actual: self.header.ty, + }) + } + + /// Returns an [`NtfsAttributeValue`] structure to read the value of this NTFS attribute. + pub fn value(&self) -> NtfsAttributeValue { + match &self.extra_header { + NtfsAttributeExtraHeader::Resident(resident_header) => { + let value_position = self.position + resident_header.value_offset as u64; + let value_length = resident_header.value_length; + let value = NtfsAttributeResidentValue::new(value_position, value_length); + NtfsAttributeValue::Resident(value) + } + NtfsAttributeExtraHeader::NonResident(_non_resident_header) => { + panic!("TODO") + } + } + } + + /// Returns the length of the value of this NTFS attribute, in bytes. + pub fn value_length(&self) -> u64 { + match &self.extra_header { + NtfsAttributeExtraHeader::Resident(resident_header) => { + resident_header.value_length as u64 + } + NtfsAttributeExtraHeader::NonResident(non_resident_header) => { + non_resident_header.data_size as u64 + } + } + } +} + +pub struct NtfsAttributes<'a, T: Read + Seek> { + fs: &'a mut T, + items_range: Range<u64>, +} + +impl<'a, T> NtfsAttributes<'a, T> +where + T: Read + Seek, +{ + pub(crate) fn new(fs: &'a mut T, file: &NtfsFile) -> Self { + let start = file.position + file.header.first_attribute_offset as u64; + let end = file.position + file.header.used_size as u64; + let items_range = start..end; + + Self { fs, items_range } + } +} + +impl<'a, T> Iterator for NtfsAttributes<'a, T> +where + T: Read + Seek, +{ + type Item = Result<NtfsAttribute>; + + fn next(&mut self) -> Option<Self::Item> { + if self.items_range.is_empty() { + return None; + } + + // This may be an entire attribute or just the 4-byte end marker. + // Check if this marks the end of the attribute list. + let offset = self.items_range.start; + iter_try!(self.fs.seek(SeekFrom::Start(offset))); + let ty = iter_try!(self.fs.read_le::<u32>()); + if ty == NtfsAttributeType::End as u32 { + return None; + } + + // It's a real attribute. + let attribute = iter_try!(NtfsAttribute::new(&mut self.fs, offset)); + self.items_range.start += attribute.attribute_length() as u64; + + Some(Ok(attribute)) + } +} + +impl<'a, T> FusedIterator for NtfsAttributes<'a, T> where T: Read + Seek {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ntfs::Ntfs; + use crate::ntfs_file::KnownNtfsFile; + use crate::time::tests::NT_TIMESTAMP_2021_01_01; + + #[test] + fn test_data() { + let mut testfs1 = crate::helpers::tests::testfs1(); + let ntfs = Ntfs::new(&mut testfs1).unwrap(); + let mft = ntfs + .ntfs_file(&mut testfs1, KnownNtfsFile::MFT as u64) + .unwrap(); + let mut mft_attributes = mft.attributes(&mut testfs1); + + // Check the StandardInformation attribute. + let attribute = mft_attributes.next().unwrap().unwrap(); + assert_eq!( + attribute.ty().unwrap(), + NtfsAttributeType::StandardInformation, + ); + assert_eq!(attribute.attribute_length(), 96); + assert!(attribute.is_resident()); + assert_eq!(attribute.name_length(), 0); + assert_eq!(attribute.value_length(), 72); + + // Check the FileName attribute. + let attribute = mft_attributes.next().unwrap().unwrap(); + assert_eq!(attribute.ty().unwrap(), NtfsAttributeType::FileName); + assert_eq!(attribute.attribute_length(), 104); + assert!(attribute.is_resident()); + assert_eq!(attribute.name_length(), 0); + assert_eq!(attribute.value_length(), 74); + + // Check the actual "file name" of the MFT. + let value = attribute.read_structured_value(&mut testfs1).unwrap(); + let file_name = match value { + NtfsStructuredValue::FileName(file_name) => file_name, + v => panic!("Unexpected NtfsStructuredValue: {:?}", v), + }; + + let creation_time = file_name.creation_time(); + assert!(*creation_time > NT_TIMESTAMP_2021_01_01); + assert_eq!(creation_time, file_name.modification_time()); + assert_eq!(creation_time, file_name.mft_record_modification_time()); + assert_eq!(creation_time, file_name.access_time()); + + let allocated_size = file_name.allocated_size(); + assert!(allocated_size > 0); + assert_eq!(allocated_size, file_name.data_size()); + + assert_eq!(file_name.name_length(), 8); + + let mut buf = [0u8; 8]; + let file_name_string = file_name.read_name(&mut testfs1, &mut buf).unwrap(); + + // Test various ways to compare the same string. + assert_eq!(file_name_string, "$MFT"); + assert_eq!(file_name_string.to_string_lossy(), String::from("$MFT")); + assert_eq!( + file_name_string, + NtfsString(&[b'$', 0, b'M', 0, b'F', 0, b'T', 0]) + ); + } +} diff --git a/src/attribute_value.rs b/src/attribute_value.rs new file mode 100644 index 0000000..20593a7 --- /dev/null +++ b/src/attribute_value.rs @@ -0,0 +1,215 @@ +// Copyright 2021 Colin Finck <colin@reactos.org> +// SPDX-License-Identifier: GPL-2.0-or-later + +use binread::io; +use binread::io::{Error, ErrorKind, Read, Seek, SeekFrom}; +use core::cmp; + +pub trait NtfsAttributeRead<T> +where + T: Read + Seek, +{ + fn read(&mut self, fs: &mut T, buf: &mut [u8]) -> io::Result<usize>; + + fn read_exact(&mut self, fs: &mut T, mut buf: &mut [u8]) -> io::Result<()> { + // This implementation is taken from https://github.com/rust-lang/rust/blob/5662d9343f0696efcc38a1264656737c9f22d427/library/std/src/io/mod.rs + // It handles all corner cases properly and outputs the known `io` error messages. + while !buf.is_empty() { + match self.read(fs, buf) { + Ok(0) => break, + Ok(n) => { + buf = &mut buf[n..]; + } + Err(ref e) if e.kind() == ErrorKind::Interrupted => {} + Err(e) => return Err(e), + } + } + + if !buf.is_empty() { + Err(Error::new( + ErrorKind::UnexpectedEof, + "failed to fill whole buffer", + )) + } else { + Ok(()) + } + } +} + +pub enum NtfsAttributeValue { + Resident(NtfsAttributeResidentValue), + NonResident(NtfsAttributeNonResidentValue), +} + +impl NtfsAttributeValue { + pub fn attach<'a, T>(self, fs: &'a mut T) -> NtfsAttributeValueAttached<'a, T> + where + T: Read + Seek, + { + NtfsAttributeValueAttached { fs, value: self } + } + + pub(crate) fn position(&self) -> u64 { + match self { + Self::Resident(inner) => inner.position(), + Self::NonResident(inner) => inner.position(), + } + } +} + +impl<T> NtfsAttributeRead<T> for NtfsAttributeValue +where + T: Read + Seek, +{ + fn read(&mut self, fs: &mut T, buf: &mut [u8]) -> io::Result<usize> { + match self { + Self::Resident(inner) => inner.read(fs, buf), + Self::NonResident(inner) => inner.read(fs, buf), + } + } +} + +impl Seek for NtfsAttributeValue { + fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> { + match self { + Self::Resident(inner) => inner.seek(pos), + Self::NonResident(inner) => inner.seek(pos), + } + } +} + +pub struct NtfsAttributeValueAttached<'a, T: Read + Seek> { + fs: &'a mut T, + value: NtfsAttributeValue, +} + +impl<'a, T> NtfsAttributeValueAttached<'a, T> +where + T: Read + Seek, +{ + pub fn detach(self) -> NtfsAttributeValue { + self.value + } + + pub fn position(&self) -> u64 { + self.value.position() + } +} + +impl<'a, T> Read for NtfsAttributeValueAttached<'a, T> +where + T: Read + Seek, +{ + fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { + self.value.read(self.fs, buf) + } +} + +impl<'a, T> Seek for NtfsAttributeValueAttached<'a, T> +where + T: Read + Seek, +{ + fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> { + self.value.seek(pos) + } +} + +pub struct NtfsAttributeResidentValue { + /// Absolute position of the attribute's value within the filesystem, in bytes. + position: u64, + /// Total length of the attribute's value, in bytes. + length: u32, + /// Current relative seek position within the value, in bytes. + seek_position: u64, +} + +impl NtfsAttributeResidentValue { + pub(crate) fn new(position: u64, length: u32) -> Self { + Self { + position, + length, + seek_position: 0, + } + } + + pub fn position(&self) -> u64 { + self.position + } +} + +impl<T> NtfsAttributeRead<T> for NtfsAttributeResidentValue +where + T: Read + Seek, +{ + fn read(&mut self, fs: &mut T, buf: &mut [u8]) -> io::Result<usize> { + let bytes_left = (self.length as u64).saturating_sub(self.seek_position); + if bytes_left == 0 { + return Ok(0); + } + + let bytes_to_read = cmp::min(buf.len(), bytes_left as usize); + + fs.seek(SeekFrom::Start(self.position + self.seek_position))?; + fs.read(&mut buf[..bytes_to_read])?; + + self.seek_position += bytes_to_read as u64; + Ok(bytes_to_read) + } +} + +impl Seek for NtfsAttributeResidentValue { + fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> { + // This implementation is taken from https://github.com/rust-lang/rust/blob/18c524fbae3ab1bf6ed9196168d8c68fc6aec61a/library/std/src/io/cursor.rs + // It handles all signed/unsigned arithmetics properly and outputs the known `io` error messages. + let (base_pos, offset) = match pos { + SeekFrom::Start(n) => { + self.seek_position = n; + return Ok(n); + } + SeekFrom::End(n) => (self.length as u64, n), + SeekFrom::Current(n) => (self.seek_position, n), + }; + + let new_pos = if offset >= 0 { + base_pos.checked_add(offset as u64) + } else { + base_pos.checked_sub(offset.wrapping_neg() as u64) + }; + + match new_pos { + Some(n) => { + self.seek_position = n; + Ok(self.seek_position) + } + None => Err(Error::new( + ErrorKind::InvalidInput, + "invalid seek to a negative or overflowing position", + )), + } + } +} + +pub struct NtfsAttributeNonResidentValue { + // TODO +} + +impl NtfsAttributeNonResidentValue { + pub fn position(&self) -> u64 { + panic!("TODO") + } +} + +impl<T> NtfsAttributeRead<T> for NtfsAttributeNonResidentValue +where + T: Read + Seek, +{ + fn read(&mut self, _fs: &mut T, _buf: &mut [u8]) -> io::Result<usize> { + panic!("TODO") + } +} + +impl Seek for NtfsAttributeNonResidentValue { + fn seek(&mut self, _pos: SeekFrom) -> io::Result<u64> { + panic!("TODO") + } +} diff --git a/src/boot_sector.rs b/src/boot_sector.rs new file mode 100644 index 0000000..0ab2f64 --- /dev/null +++ b/src/boot_sector.rs @@ -0,0 +1,91 @@ +// Copyright 2021 Colin Finck <colin@reactos.org> +// SPDX-License-Identifier: GPL-2.0-or-later + +use crate::error::{NtfsError, Result}; +use binread::BinRead; +use memoffset::offset_of; + +/// The usual exponent of `BiosParameterBlock::file_record_size_info` is 10 (2^10 = 1024 bytes). +/// Exponents > 10 would come as a surprise, but our code should still be able to handle those. +/// Exponents > 32 (2^32 = 4 GiB) would make no sense, exceed a u32, and must be outright denied. +const MAXIMUM_SIZE_INFO_EXPONENT: u32 = 32; + +// Sources: +// - https://en.wikipedia.org/wiki/NTFS#Partition_Boot_Sector_(VBR) +// - https://en.wikipedia.org/wiki/BIOS_parameter_block#NTFS +// - https://wiki.osdev.org/NTFS +// - The iBored tool from https://apps.tempel.org/iBored/ +#[allow(unused)] +#[derive(BinRead)] +pub(crate) struct BiosParameterBlock { + pub(crate) bytes_per_sector: u16, + pub(crate) sectors_per_cluster: u8, + zeros_1: [u8; 7], + media: u8, + zeros_2: [u8; 2], + dummy_sectors_per_track: u16, + dummy_heads: u16, + hidden_sectors: u32, + zeros_3: u32, + physical_drive_number: u8, + flags: u8, + extended_boot_signature: u8, + reserved: u8, + pub(crate) total_sectors: u64, + /// Logical Cluster Number (LCN) to the beginning of the Master File Table (MFT). + pub(crate) mft_lcn: u64, + mft_mirror_lcn: u64, + pub(crate) file_record_size_info: i8, + zeros_4: [u8; 3], + index_record_size_info: i8, + zeros_5: [u8; 3], + pub(crate) serial_number: u64, + checksum: u32, +} + +impl BiosParameterBlock { + /// Source: https://en.wikipedia.org/wiki/NTFS#Partition_Boot_Sector_(VBR) + pub(crate) fn record_size(size_info_value: i8, bytes_per_cluster: u32) -> Result<u32> { + if size_info_value > 0 { + // The size field denotes a cluster count. + Ok(size_info_value as u32 * bytes_per_cluster as u32) + } else { + // The size field denotes a binary exponent after negation. + let exponent = (-size_info_value) as u32; + if exponent > MAXIMUM_SIZE_INFO_EXPONENT { + return Err(NtfsError::InvalidRecordSizeExponent { + expected: MAXIMUM_SIZE_INFO_EXPONENT, + actual: exponent, + }); + } + + Ok(1 << exponent) + } + } +} + +#[allow(unused)] +#[derive(BinRead)] +pub(crate) struct BootSector { + bootjmp: [u8; 3], + oem_name: [u8; 8], + pub(crate) bpb: BiosParameterBlock, + boot_code: [u8; 426], + signature: [u8; 2], +} + +impl BootSector { + pub(crate) fn validate(&self) -> Result<()> { + // Validate the infamous [0x55, 0xAA] signature at the end of the boot sector. + let expected_signature = &[0x55, 0xAA]; + if &self.signature != expected_signature { + return Err(NtfsError::InvalidTwoByteSignature { + position: offset_of!(BootSector, signature) as u64, + expected: expected_signature, + actual: self.signature, + }); + } + + Ok(()) + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..ef85bb9 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,73 @@ +// Copyright 2021 Colin Finck <colin@reactos.org> +// SPDX-License-Identifier: GPL-2.0-or-later + +use crate::attribute::NtfsAttributeType; +use displaydoc::Display; + +/// Central result type of ntfs. +pub type Result<T, E = NtfsError> = core::result::Result<T, E>; + +/// Central error type of ntfs. +#[derive(Debug, Display)] +pub enum NtfsError { + /// The given buffer should have at least {expected} bytes, but it only has {actual} bytes + BufferTooSmall { expected: usize, actual: usize }, + /// The NTFS attribute at byte position {position:#010x} of type {ty:?} should have at least {expected} bytes, but it only has {actual} bytes + InvalidAttributeSize { + position: u64, + ty: NtfsAttributeType, + expected: u64, + actual: u64, + }, + /// The requested NTFS file {n} is invalid + InvalidNtfsFile { n: u64 }, + /// The NTFS file at byte position {position:#010x} should have signature {expected:?}, but it has signature {actual:?} + InvalidNtfsFileSignature { + position: u64, + expected: &'static [u8], + actual: [u8; 4], + }, + /// The given time can't be represented as an NtfsTime + InvalidNtfsTime, + /// A record size field in the BIOS Parameter Block denotes the exponent {actual}, but the maximum valid one is {expected} + InvalidRecordSizeExponent { expected: u32, actual: u32 }, + /// The 2-byte signature field at byte position {position:#010x} should contain {expected:?}, but it contains {actual:?} + InvalidTwoByteSignature { + position: u64, + expected: &'static [u8], + actual: [u8; 2], + }, + /// I/O error: {0:?} + Io(binread::io::Error), + /// The cluster size is {actual} bytes, but the maximum supported one is {expected} + UnsupportedClusterSize { expected: u32, actual: u32 }, + /// The type of the NTFS attribute at byte position {position:#010x} is {actual:#010x}, which is not supported + UnsupportedNtfsAttributeType { position: u64, actual: u32 }, + /// The namespace of the NTFS file name starting at byte position {position:#010x} is {actual}, which is not supported + UnsupportedNtfsFileNamespace { position: u64, actual: u8 }, + /// The NTFS attribute at byte position {position:#010x} has type {ty:?}, which cannot be read as a structured value + UnsupportedStructuredValue { + position: u64, + ty: NtfsAttributeType, + }, +} + +impl From<binread::error::Error> for NtfsError { + fn from(error: binread::error::Error) -> Self { + if let binread::error::Error::Io(io_error) = error { + Self::Io(io_error) + } else { + // We don't use any binread attributes that result in other errors. + unreachable!("Got a binread error of unexpected type: {:?}", error); + } + } +} + +impl From<binread::io::Error> for NtfsError { + fn from(error: binread::io::Error) -> Self { + Self::Io(error) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for NtfsError {} diff --git a/src/helpers.rs b/src/helpers.rs new file mode 100644 index 0000000..02abdcf --- /dev/null +++ b/src/helpers.rs @@ -0,0 +1,26 @@ +// Copyright 2021 Colin Finck <colin@reactos.org> +// SPDX-License-Identifier: GPL-2.0-or-later + +macro_rules! iter_try { + ($e:expr) => { + match $e { + Ok(x) => x, + Err(e) => return Some(Err(e.into())), + } + }; +} + +#[cfg(test)] +pub mod tests { + use std::fs::File; + use std::io::{Cursor, Read}; + + pub fn testfs1() -> Cursor<Vec<u8>> { + let mut buffer = Vec::new(); + File::open("testdata/testfs1") + .unwrap() + .read_to_end(&mut buffer) + .unwrap(); + Cursor::new(buffer) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..673092b --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,28 @@ +// Copyright 2021 Colin Finck <colin@reactos.org> +// SPDX-License-Identifier: GPL-2.0-or-later + +#![cfg_attr(not(feature = "std"), no_std)] +#![forbid(unsafe_code)] + +extern crate alloc; + +#[macro_use] +mod helpers; + +mod attribute; +mod attribute_value; +mod boot_sector; +mod error; +mod ntfs; +mod ntfs_file; +mod string; +pub mod structured_values; +mod time; + +pub use crate::attribute::*; +pub use crate::attribute_value::*; +pub use crate::error::*; +pub use crate::ntfs::*; +pub use crate::ntfs_file::*; +pub use crate::string::*; +pub use crate::time::*; diff --git a/src/ntfs.rs b/src/ntfs.rs new file mode 100644 index 0000000..b8998f3 --- /dev/null +++ b/src/ntfs.rs @@ -0,0 +1,129 @@ +// Copyright 2021 Colin Finck <colin@reactos.org> +// SPDX-License-Identifier: GPL-2.0-or-later + +use crate::boot_sector::{BiosParameterBlock, BootSector}; +//use crate::dir::Dir; +use crate::error::{NtfsError, Result}; +use crate::ntfs_file::NtfsFile; +use binread::io::{Read, Seek, SeekFrom}; +use binread::BinReaderExt; + +/// The maximum cluster size supported by Windows. +/// Source: https://support.microsoft.com/en-us/topic/default-cluster-size-for-ntfs-fat-and-exfat-9772e6f1-e31a-00d7-e18f-73169155af95 +const MAXIMUM_CLUSTER_SIZE: u32 = 65536; + +pub struct Ntfs { + /// How many bytes a sector occupies. This is usually 512. + bytes_per_sector: u16, + /// How many sectors a cluster occupies. This is usually 8. + sectors_per_cluster: u8, + /// Size of the filesystem, in bytes. + size: u64, + /// Absolute position of the Master File Table (MFT), in bytes. + mft_position: u64, + /// Size of a single file record, in bytes. + pub(crate) file_record_size: u32, + /// Serial number of the NTFS volume. + serial_number: u64, +} + +impl Ntfs { + pub fn new<T>(fs: &mut T) -> Result<Self> + where + T: Read + Seek, + { + // Read and validate the boot sector. + fs.seek(SeekFrom::Start(0))?; + let boot_sector = fs.read_le::<BootSector>()?; + boot_sector.validate()?; + + let bytes_per_sector = boot_sector.bpb.bytes_per_sector; + let sectors_per_cluster = boot_sector.bpb.sectors_per_cluster; + let bytes_per_cluster = sectors_per_cluster as u32 * bytes_per_sector as u32; + if bytes_per_cluster > MAXIMUM_CLUSTER_SIZE { + return Err(NtfsError::UnsupportedClusterSize { + expected: MAXIMUM_CLUSTER_SIZE, + actual: bytes_per_cluster, + }); + } + + let size = boot_sector.bpb.total_sectors * bytes_per_sector as u64; + let mft_position = boot_sector.bpb.mft_lcn * bytes_per_cluster as u64; + let file_record_size = BiosParameterBlock::record_size( + boot_sector.bpb.file_record_size_info, + bytes_per_cluster, + )?; + let serial_number = boot_sector.bpb.serial_number; + + Ok(Self { + bytes_per_sector, + sectors_per_cluster, + size, + mft_position, + file_record_size, + serial_number, + }) + } + + /// Returns the size of a single cluster, in bytes. + pub fn cluster_size(&self) -> u16 { + self.bytes_per_sector * self.sectors_per_cluster as u16 + } + + /// Returns the [`NtfsFile`] for the `n`-th NTFS file record. + /// + /// The first few NTFS files have fixed indexes and contain filesystem + /// management information (see the [`KnownNtfsFile`] enum). + /// + /// TODO: + /// - Check if `n` can be u32 instead of u64. + /// - Check if `n` should be in a newtype, with easier conversion from + /// KnownNtfsFile. + pub fn ntfs_file<T>(&self, fs: &mut T, n: u64) -> Result<NtfsFile> + where + T: Read + Seek, + { + let offset = n + .checked_mul(self.file_record_size as u64) + .ok_or(NtfsError::InvalidNtfsFile { n })?; + let position = self + .mft_position + .checked_add(offset) + .ok_or(NtfsError::InvalidNtfsFile { n })?; + NtfsFile::new(fs, position) + } + + /// Returns the root [`Dir`] of this NTFS volume. + pub fn root_dir(&self) -> ! { + panic!("TODO") + } + + /// Returns the size of a single sector in bytes. + pub fn sector_size(&self) -> u16 { + self.bytes_per_sector + } + + /// Returns the 64-bit serial number of this NTFS volume. + pub fn serial_number(&self) -> u64 { + self.serial_number + } + + /// Returns the partition size in bytes. + pub fn size(&self) -> u64 { + self.size + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ntfs() { + let mut testfs1 = crate::helpers::tests::testfs1(); + let ntfs = Ntfs::new(&mut testfs1).unwrap(); + assert_eq!(ntfs.cluster_size(), 512); + assert_eq!(ntfs.sector_size(), 512); + assert_eq!(ntfs.size(), 1049088); + } +} diff --git a/src/ntfs_file.rs b/src/ntfs_file.rs new file mode 100644 index 0000000..e663848 --- /dev/null +++ b/src/ntfs_file.rs @@ -0,0 +1,98 @@ +// Copyright 2021 Colin Finck <colin@reactos.org> +// SPDX-License-Identifier: GPL-2.0-or-later + +use crate::attribute::NtfsAttributes; +use crate::error::{NtfsError, Result}; +use binread::io::{Read, Seek, SeekFrom}; +use binread::{BinRead, BinReaderExt}; +use bitflags::bitflags; + +#[repr(u64)] +pub enum KnownNtfsFile { + MFT = 0, + MFTMirr = 1, + LogFile = 2, + Volume = 3, + AttrDef = 4, + RootDirectory = 5, + Bitmap = 6, + Boot = 7, + BadClus = 8, + Secure = 9, + UpCase = 10, + Extend = 11, +} + +#[allow(unused)] +#[derive(BinRead)] +struct NtfsRecordHeader { + signature: [u8; 4], + update_sequence_array_offset: u16, + update_sequence_array_size: u16, + logfile_sequence_number: u64, +} + +#[allow(unused)] +#[derive(BinRead)] +pub(crate) struct NtfsFileRecordHeader { + record_header: NtfsRecordHeader, + sequence_number: u16, + hard_link_count: u16, + pub(crate) first_attribute_offset: u16, + flags: u16, + pub(crate) used_size: u32, + allocated_size: u32, + base_file_record: u64, + next_attribute_number: u16, +} + +bitflags! { + struct NtfsFileRecordFlags: u16 { + /// Record is in use. + const IN_USE = 0x0001; + /// Record is a directory. + const IS_DIRECTORY = 0x0002; + } +} + +pub struct NtfsFile { + pub(crate) header: NtfsFileRecordHeader, + pub(crate) position: u64, +} + +impl NtfsFile { + pub(crate) fn new<T>(fs: &mut T, position: u64) -> Result<Self> + where + T: Read + Seek, + { + fs.seek(SeekFrom::Start(position))?; + let header = fs.read_le::<NtfsFileRecordHeader>()?; + + let file = Self { header, position }; + file.validate_signature()?; + + Ok(file) + } + + pub fn attributes<'a, T>(&self, fs: &'a mut T) -> NtfsAttributes<'a, T> + where + T: Read + Seek, + { + NtfsAttributes::new(fs, &self) + } + + fn validate_signature(&self) -> Result<()> { + let signature = &self.header.record_header.signature; + let expected = b"FILE"; + + if signature == expected { + Ok(()) + } else { + Err(NtfsError::InvalidNtfsFileSignature { + position: self.position, + expected, + actual: *signature, + }) + } + } +} diff --git a/src/string.rs b/src/string.rs new file mode 100644 index 0000000..18acc4d --- /dev/null +++ b/src/string.rs @@ -0,0 +1,163 @@ +// Copyright 2021 Colin Finck <colin@reactos.org> +// SPDX-License-Identifier: GPL-2.0-or-later + +use alloc::string::String; +use core::char; +use core::cmp::Ordering; +use core::convert::TryInto; +use core::fmt; + +/// Zero-copy representation of a string stored in an NTFS filesystem structure. +#[derive(Clone, Debug, Eq)] +pub struct NtfsString<'a>(pub &'a [u8]); + +impl<'a> NtfsString<'a> { + fn cmp_iter<TI, OI>(mut this_iter: TI, mut other_iter: OI) -> Ordering + where + TI: Iterator<Item = u16>, + OI: Iterator<Item = u16>, + { + loop { + match (this_iter.next(), other_iter.next()) { + (Some(this_code_unit), Some(other_code_unit)) => { + // We have two UTF-16 code units to compare. + if this_code_unit != other_code_unit { + return this_code_unit.cmp(&other_code_unit); + } + } + (Some(_), None) => { + // `this_iter` is longer than `other_iter` but otherwise equal. + return Ordering::Greater; + } + (None, Some(_)) => { + // `other_iter` is longer than `this_iter` but otherwise equal. + return Ordering::Less; + } + (None, None) => { + // We made it to the end of both strings, so they must be equal. + return Ordering::Equal; + } + } + } + } + + fn cmp_str(&self, other: &str) -> Ordering { + let other_iter = other.encode_utf16(); + Self::cmp_iter(self.utf16le_iter(), other_iter) + } + + fn utf16le_iter(&'a self) -> impl Iterator<Item = u16> + 'a { + self.0 + .chunks_exact(2) + .map(|two_bytes| u16::from_le_bytes(two_bytes.try_into().unwrap())) + } + + /// Returns `true` if `self` has a length of zero bytes. + pub const fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns the length of `self`. + /// + /// This length is in bytes, not characters! In other words, + /// it may not be what a human considers the length of the string. + pub const fn len(&self) -> usize { + self.0.len() + } + + /// Attempts to convert `self` to an owned `String`. + /// Returns `Some(String)` if all characters could be converted successfully or `None` if a decoding error occurred. + pub fn to_string_checked(&self) -> Option<String> { + char::decode_utf16(self.utf16le_iter()) + .map(|x| x.ok()) + .collect::<Option<String>>() + } + + /// Converts `self` to an owned `String`, replacing invalid data with the replacement character (U+FFFD). + pub fn to_string_lossy(&self) -> String { + char::decode_utf16(self.utf16le_iter()) + .map(|x| x.unwrap_or(char::REPLACEMENT_CHARACTER)) + .collect() + } +} + +impl<'a> fmt::Display for NtfsString<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let utf16_iter = char::decode_utf16(self.utf16le_iter()) + .map(|x| x.unwrap_or(char::REPLACEMENT_CHARACTER)); + + for single_char in utf16_iter { + single_char.fmt(f)?; + } + + Ok(()) + } +} + +impl<'a> Ord for NtfsString<'a> { + fn cmp(&self, other: &Self) -> Ordering { + Self::cmp_iter(self.utf16le_iter(), other.utf16le_iter()) + } +} + +impl<'a> PartialEq for NtfsString<'a> { + /// Checks that two strings are a (case-sensitive!) match. + fn eq(&self, other: &Self) -> bool { + let ordering = self.cmp(other); + ordering == Ordering::Equal + } +} + +impl<'a> PartialEq<str> for NtfsString<'a> { + fn eq(&self, other: &str) -> bool { + self.cmp_str(other) == Ordering::Equal + } +} + +impl<'a> PartialEq<NtfsString<'a>> for str { + fn eq(&self, other: &NtfsString<'a>) -> bool { + other.cmp_str(self) == Ordering::Equal + } +} + +impl<'a> PartialEq<&str> for NtfsString<'a> { + fn eq(&self, other: &&str) -> bool { + self.cmp_str(other) == Ordering::Equal + } +} + +impl<'a> PartialEq<NtfsString<'a>> for &str { + fn eq(&self, other: &NtfsString<'a>) -> bool { + other.cmp_str(self) == Ordering::Equal + } +} + +impl<'a> PartialOrd for NtfsString<'a> { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +impl<'a> PartialOrd<str> for NtfsString<'a> { + fn partial_cmp(&self, other: &str) -> Option<Ordering> { + Some(self.cmp_str(other)) + } +} + +impl<'a> PartialOrd<NtfsString<'a>> for str { + fn partial_cmp(&self, other: &NtfsString<'a>) -> Option<Ordering> { + Some(other.cmp_str(self)) + } +} + +impl<'a> PartialOrd<&str> for NtfsString<'a> { + fn partial_cmp(&self, other: &&str) -> Option<Ordering> { + Some(self.cmp_str(other)) + } +} + +impl<'a> PartialOrd<NtfsString<'a>> for &str { + fn partial_cmp(&self, other: &NtfsString<'a>) -> Option<Ordering> { + Some(other.cmp_str(self)) + } +} diff --git a/src/structured_values/attribute_list.rs b/src/structured_values/attribute_list.rs new file mode 100644 index 0000000..53769a7 --- /dev/null +++ b/src/structured_values/attribute_list.rs @@ -0,0 +1,2 @@ +// Copyright 2021 Colin Finck <colin@reactos.org> +// SPDX-License-Identifier: GPL-2.0-or-later diff --git a/src/structured_values/file_name.rs b/src/structured_values/file_name.rs new file mode 100644 index 0000000..19f50cc --- /dev/null +++ b/src/structured_values/file_name.rs @@ -0,0 +1,142 @@ +// Copyright 2021 Colin Finck <colin@reactos.org> +// SPDX-License-Identifier: GPL-2.0-or-later + +use crate::attribute::NtfsAttributeType; +use crate::attribute_value::NtfsAttributeValueAttached; +use crate::error::{NtfsError, Result}; +use crate::string::NtfsString; +use crate::structured_values::NtfsFileAttributeFlags; +use crate::time::NtfsTime; +use binread::io::{Read, Seek, SeekFrom}; +use binread::{BinRead, BinReaderExt}; +use core::mem; +use enumn::N; + +/// Size of all [`FileNameHeader`] fields. +const FILE_NAME_HEADER_SIZE: u64 = 66; + +/// The smallest FileName attribute has a name containing just a single character. +const FILE_NAME_MIN_SIZE: u64 = FILE_NAME_HEADER_SIZE + mem::size_of::<u16>() as u64; + +#[allow(unused)] +#[derive(BinRead, Clone, Debug)] +struct FileNameHeader { + parent_directory_ref: u64, + creation_time: NtfsTime, + modification_time: NtfsTime, + mft_record_modification_time: NtfsTime, + access_time: NtfsTime, + allocated_size: u64, + data_size: u64, + file_attributes: u32, + reparse_point_tag: u32, + name_length: u8, + namespace: u8, +} + +#[derive(Clone, Copy, Debug, Eq, N, PartialEq)] +#[repr(u8)] +pub enum NtfsFileNamespace { + Posix = 0, + Win32 = 1, + Dos = 2, + Win32AndDos = 3, +} + +#[derive(Clone, Debug)] +pub struct NtfsFileName { + header: FileNameHeader, + name_position: u64, +} + +impl NtfsFileName { + pub(crate) fn new<T>( + attribute_position: u64, + mut value_attached: NtfsAttributeValueAttached<'_, T>, + value_length: u64, + ) -> Result<Self> + where + T: Read + Seek, + { + if value_length < FILE_NAME_MIN_SIZE { + return Err(NtfsError::InvalidAttributeSize { + position: attribute_position, + ty: NtfsAttributeType::FileName, + expected: FILE_NAME_MIN_SIZE, + actual: value_length, + }); + } + + let header = value_attached.read_le::<FileNameHeader>()?; + let name_position = value_attached.position() + FILE_NAME_HEADER_SIZE; + + Ok(Self { + header, + name_position, + }) + } + + pub fn access_time(&self) -> NtfsTime { + self.header.access_time + } + + pub fn allocated_size(&self) -> u64 { + self.header.allocated_size + } + + pub fn creation_time(&self) -> NtfsTime { + self.header.creation_time + } + + pub fn data_size(&self) -> u64 { + self.header.data_size + } + + pub fn file_attributes(&self) -> NtfsFileAttributeFlags { + NtfsFileAttributeFlags::from_bits_truncate(self.header.file_attributes) + } + + pub fn mft_record_modification_time(&self) -> NtfsTime { + self.header.mft_record_modification_time + } + + pub fn modification_time(&self) -> NtfsTime { + self.header.modification_time + } + + /// Returns the file name length, in bytes. + /// + /// A file name has a maximum length of 255 UTF-16 code points (510 bytes). + pub fn name_length(&self) -> usize { + self.header.name_length as usize * mem::size_of::<u16>() + } + + /// Returns the namespace this name belongs to, or [`NtfsError::UnsupportedNtfsFileNamespace`] + /// if it's an unknown namespace. + pub fn namespace(&self) -> Result<NtfsFileNamespace> { + NtfsFileNamespace::n(self.header.namespace).ok_or(NtfsError::UnsupportedNtfsFileNamespace { + position: self.name_position, + actual: self.header.namespace, + }) + } + + /// Reads the file name into the given buffer, and returns an + /// [`NtfsString`] wrapping that buffer. + pub fn read_name<'a, T>(&self, fs: &mut T, buf: &'a mut [u8]) -> Result<NtfsString<'a>> + where + T: Read + Seek, + { + let name_length = self.name_length(); + if buf.len() < name_length { + return Err(NtfsError::BufferTooSmall { + expected: name_length, + actual: buf.len(), + }); + } + + fs.seek(SeekFrom::Start(self.name_position))?; + fs.read_exact(&mut buf[..name_length])?; + + Ok(NtfsString(&buf[..name_length])) + } +} diff --git a/src/structured_values/mod.rs b/src/structured_values/mod.rs new file mode 100644 index 0000000..df4aa90 --- /dev/null +++ b/src/structured_values/mod.rs @@ -0,0 +1,44 @@ +// Copyright 2021 Colin Finck <colin@reactos.org> +// SPDX-License-Identifier: GPL-2.0-or-later + +mod attribute_list; +mod file_name; +mod object_id; +mod security_descriptor; +mod standard_information; +mod volume_information; +mod volume_name; + +pub use attribute_list::*; +pub use file_name::*; +pub use object_id::*; +pub use security_descriptor::*; +pub use standard_information::*; +pub use volume_information::*; +pub use volume_name::*; + +use bitflags::bitflags; + +bitflags! { + pub struct NtfsFileAttributeFlags: u32 { + const READ_ONLY = 0x0001; + const HIDDEN = 0x0002; + const SYSTEM = 0x0004; + const ARCHIVE = 0x0020; + const DEVICE = 0x0040; + const NORMAL = 0x0080; + const TEMPORARY = 0x0100; + const SPARSE_FILE = 0x0200; + const REPARSE_POINT = 0x0400; + const COMPRESSED = 0x0800; + const OFFLINE = 0x1000; + const NOT_CONTENT_INDEXED = 0x2000; + const ENCRYPTED = 0x4000; + } +} + +#[derive(Clone, Debug)] +pub enum NtfsStructuredValue { + StandardInformation(NtfsStandardInformation), + FileName(NtfsFileName), +} diff --git a/src/structured_values/object_id.rs b/src/structured_values/object_id.rs new file mode 100644 index 0000000..53769a7 --- /dev/null +++ b/src/structured_values/object_id.rs @@ -0,0 +1,2 @@ +// Copyright 2021 Colin Finck <colin@reactos.org> +// SPDX-License-Identifier: GPL-2.0-or-later diff --git a/src/structured_values/security_descriptor.rs b/src/structured_values/security_descriptor.rs new file mode 100644 index 0000000..53769a7 --- /dev/null +++ b/src/structured_values/security_descriptor.rs @@ -0,0 +1,2 @@ +// Copyright 2021 Colin Finck <colin@reactos.org> +// SPDX-License-Identifier: GPL-2.0-or-later diff --git a/src/structured_values/standard_information.rs b/src/structured_values/standard_information.rs new file mode 100644 index 0000000..f566278 --- /dev/null +++ b/src/structured_values/standard_information.rs @@ -0,0 +1,119 @@ +// Copyright 2021 Colin Finck <colin@reactos.org> +// SPDX-License-Identifier: GPL-2.0-or-later + +use crate::attribute::NtfsAttributeType; +use crate::attribute_value::NtfsAttributeValueAttached; +use crate::error::{NtfsError, Result}; +use crate::structured_values::NtfsFileAttributeFlags; +use crate::time::NtfsTime; +use binread::io::{Read, Seek}; +use binread::{BinRead, BinReaderExt}; + +/// Size of all [`StandardInformationData`] fields plus some reserved bytes. +const STANDARD_INFORMATION_SIZE_NTFS1: u64 = 48; + +/// Size of all [`StandardInformationData`] plus [`StandardInformationDataNtfs3`] fields. +const STANDARD_INFORMATION_SIZE_NTFS3: u64 = 72; + +#[derive(BinRead, Clone, Debug)] +struct StandardInformationData { + creation_time: NtfsTime, + modification_time: NtfsTime, + mft_record_modification_time: NtfsTime, + access_time: NtfsTime, + file_attributes: u32, +} + +#[derive(BinRead, Clone, Debug)] +struct StandardInformationDataNtfs3 { + maximum_versions: u32, + version: u32, + class_id: u32, + owner_id: u32, + security_id: u32, + quota_charged: u64, + usn: u64, +} + +#[derive(Clone, Debug)] +pub struct NtfsStandardInformation { + data: StandardInformationData, + ntfs3_data: Option<StandardInformationDataNtfs3>, +} + +impl NtfsStandardInformation { + pub(crate) fn new<T>( + attribute_position: u64, + mut value_attached: NtfsAttributeValueAttached<'_, T>, + value_length: u64, + ) -> Result<Self> + where + T: Read + Seek, + { + if value_length < STANDARD_INFORMATION_SIZE_NTFS1 { + return Err(NtfsError::InvalidAttributeSize { + position: attribute_position, + ty: NtfsAttributeType::StandardInformation, + expected: STANDARD_INFORMATION_SIZE_NTFS1, + actual: value_length, + }); + } + + let data = value_attached.read_le::<StandardInformationData>()?; + let ntfs3_data = if value_length >= STANDARD_INFORMATION_SIZE_NTFS3 as u64 { + Some(value_attached.read_le::<StandardInformationDataNtfs3>()?) + } else { + None + }; + + Ok(Self { data, ntfs3_data }) + } + + pub fn access_time(&self) -> NtfsTime { + self.data.access_time + } + + pub fn class_id(&self) -> Option<u32> { + self.ntfs3_data.as_ref().map(|x| x.class_id) + } + + pub fn creation_time(&self) -> NtfsTime { + self.data.creation_time + } + + pub fn file_attributes(&self) -> NtfsFileAttributeFlags { + NtfsFileAttributeFlags::from_bits_truncate(self.data.file_attributes) + } + + pub fn maximum_versions(&self) -> Option<u32> { + self.ntfs3_data.as_ref().map(|x| x.maximum_versions) + } + + pub fn mft_record_modification_time(&self) -> NtfsTime { + self.data.mft_record_modification_time + } + + pub fn modification_time(&self) -> NtfsTime { + self.data.modification_time + } + + pub fn owner_id(&self) -> Option<u32> { + self.ntfs3_data.as_ref().map(|x| x.owner_id) + } + + pub fn quota_charged(&self) -> Option<u64> { + self.ntfs3_data.as_ref().map(|x| x.quota_charged) + } + + pub fn security_id(&self) -> Option<u32> { + self.ntfs3_data.as_ref().map(|x| x.security_id) + } + + pub fn usn(&self) -> Option<u64> { + self.ntfs3_data.as_ref().map(|x| x.usn) + } + + pub fn version(&self) -> Option<u32> { + self.ntfs3_data.as_ref().map(|x| x.version) + } +} diff --git a/src/structured_values/volume_information.rs b/src/structured_values/volume_information.rs new file mode 100644 index 0000000..53769a7 --- /dev/null +++ b/src/structured_values/volume_information.rs @@ -0,0 +1,2 @@ +// Copyright 2021 Colin Finck <colin@reactos.org> +// SPDX-License-Identifier: GPL-2.0-or-later diff --git a/src/structured_values/volume_name.rs b/src/structured_values/volume_name.rs new file mode 100644 index 0000000..53769a7 --- /dev/null +++ b/src/structured_values/volume_name.rs @@ -0,0 +1,2 @@ +// Copyright 2021 Colin Finck <colin@reactos.org> +// SPDX-License-Identifier: GPL-2.0-or-later diff --git a/src/time.rs b/src/time.rs new file mode 100644 index 0000000..ea351da --- /dev/null +++ b/src/time.rs @@ -0,0 +1,162 @@ +// Copyright 2021 Colin Finck <colin@reactos.org> +// SPDX-License-Identifier: GPL-2.0-or-later + +use binread::BinRead; +use core::ops::Deref; + +#[cfg(any(feature = "chrono", feature = "std"))] +use core::convert::TryFrom; + +#[cfg(feature = "chrono")] +use { + crate::error::NtfsError, + chrono::{DateTime, Datelike, NaiveDate, Timelike, Utc}, +}; + +#[cfg(feature = "std")] +use std::time::{SystemTime, SystemTimeError}; + +/// How many days we have between 0001-01-01 and 1601-01-01. +#[cfg(feature = "chrono")] +const DAYS_FROM_0001_TO_1601: i32 = 584389; + +/// Difference in 100-nanosecond intervals between the Windows/NTFS epoch (1601-01-01) and the Unix epoch (1970-01-01). +#[cfg(feature = "std")] +const EPOCH_DIFFERENCE_IN_INTERVALS: i64 = 116_444_736_000_000_000; + +/// How many 100-nanosecond intervals we have in a second. +#[cfg(any(feature = "chrono", feature = "std"))] +const INTERVALS_PER_SECOND: u64 = 10_000_000; + +/// How many 100-nanosecond intervals we have in a day. +#[cfg(feature = "chrono")] +const INTERVALS_PER_DAY: u64 = 24 * 60 * 60 * INTERVALS_PER_SECOND; + +#[derive(BinRead, Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct NtfsTime(u64); + +impl Deref for NtfsTime { + type Target = u64; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[cfg(feature = "chrono")] +impl TryFrom<DateTime<Utc>> for NtfsTime { + type Error = NtfsError; + + fn try_from(dt: DateTime<Utc>) -> Result<Self, Self::Error> { + // First do the time calculations, which safely fit into a u64. + let mut intervals = dt.hour() as u64; + + intervals *= 60; + intervals += dt.minute() as u64; + + intervals *= 60; + intervals += dt.second() as u64; + + intervals *= INTERVALS_PER_SECOND; + intervals += dt.nanosecond() as u64 / 100; + + // Now do checked arithmetics for the day calculations, which may + // exceed the lower bounds (years before 1601) or upper bounds + // (dates after approximately 28 May 60056). + let num_days_from_ce = dt.num_days_from_ce(); + let num_days_from_1601 = num_days_from_ce + .checked_sub(DAYS_FROM_0001_TO_1601) + .ok_or(NtfsError::InvalidNtfsTime)?; + let intervals_days = INTERVALS_PER_DAY + .checked_mul(num_days_from_1601 as u64) + .ok_or(NtfsError::InvalidNtfsTime)?; + intervals = intervals + .checked_add(intervals_days) + .ok_or(NtfsError::InvalidNtfsTime)?; + + Ok(Self(intervals)) + } +} + +#[cfg(feature = "chrono")] +impl From<NtfsTime> for DateTime<Utc> { + fn from(nt: NtfsTime) -> DateTime<Utc> { + let mut remainder = *nt; + + let nano = (remainder % INTERVALS_PER_SECOND) as u32 * 100; + remainder /= INTERVALS_PER_SECOND; + + let sec = (remainder % 60) as u32; + remainder /= 60; + + let min = (remainder % 60) as u32; + remainder /= 60; + + let hour = (remainder % 24) as u32; + remainder /= 24; + + let num_days_from_1601 = remainder as i32; + let num_days_from_ce = num_days_from_1601 + DAYS_FROM_0001_TO_1601; + + let ndt = + NaiveDate::from_num_days_from_ce(num_days_from_ce).and_hms_nano(hour, min, sec, nano); + DateTime::<Utc>::from_utc(ndt, Utc) + } +} + +#[cfg(feature = "std")] +impl TryFrom<SystemTime> for NtfsTime { + type Error = SystemTimeError; + + fn try_from(st: SystemTime) -> Result<Self, Self::Error> { + let duration_since_unix_epoch = st.duration_since(SystemTime::UNIX_EPOCH)?; + let intervals_since_unix_epoch = duration_since_unix_epoch.as_secs() as u64 + * INTERVALS_PER_SECOND + + duration_since_unix_epoch.subsec_nanos() as u64 / 100; + let intervals_since_windows_epoch = + intervals_since_unix_epoch + EPOCH_DIFFERENCE_IN_INTERVALS as u64; + Ok(Self(intervals_since_windows_epoch)) + } +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + + #[cfg(feature = "chrono")] + use chrono::TimeZone; + + pub(crate) const NT_TIMESTAMP_2021_01_01: u64 = 132539328000000000u64; + + #[cfg(feature = "chrono")] + #[test] + fn test_chrono() { + let dt = Utc.ymd(2013, 1, 5).and_hms(18, 15, 00); + let nt = NtfsTime::try_from(dt).unwrap(); + assert_eq!(*nt, 130018833000000000u64); + + let dt2 = DateTime::<Utc>::from(nt); + assert_eq!(dt, dt2); + + let dt = Utc.ymd(1601, 1, 1).and_hms(0, 0, 0); + let nt = NtfsTime::try_from(dt).unwrap(); + assert_eq!(*nt, 0u64); + + let dt = Utc.ymd(1600, 12, 31).and_hms(23, 59, 59); + assert!(NtfsTime::try_from(dt).is_err()); + + let dt = Utc.ymd(60056, 5, 28).and_hms(0, 0, 0); + assert!(NtfsTime::try_from(dt).is_ok()); + + let dt = Utc.ymd(60056, 5, 29).and_hms(0, 0, 0); + assert!(NtfsTime::try_from(dt).is_err()); + } + + #[cfg(feature = "std")] + #[test] + fn test_systemtime() { + let st = SystemTime::now(); + let nt = NtfsTime::try_from(st).unwrap(); + assert!(*nt > NT_TIMESTAMP_2021_01_01); + } +} diff --git a/testdata/create-testfs1.sh b/testdata/create-testfs1.sh new file mode 100644 index 0000000..65dac63 --- /dev/null +++ b/testdata/create-testfs1.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -eu + +if [ "`whoami`" != "root" ]; then + echo Needs to be run as root! + exit 1 +fi + +dd if=/dev/zero of=testfs1 bs=1k count=1025 +mkntfs -c 512 -L mylabel -F testfs1 + +mkdir mnt +mount -t ntfs-3g -o loop testfs1 mnt +cd mnt + +touch -m -t 202101011337 empty-file +dd if=/dev/zero of=file-with-5-zeros bs=1 count=5 +dd if=/dev/zero of=big-sparse-file skip=5M bs=1 count=1 + +mkdir -p subdir/subsubdir +echo abcdef > subdir/subsubdir/file-with-6-letters + +cd .. +umount mnt +rmdir mnt diff --git a/testdata/testfs1 b/testdata/testfs1 Binary files differnew file mode 100644 index 0000000..4afb44a --- /dev/null +++ b/testdata/testfs1 |