From d5de364a851fb8f3a594652e0ea58716b2aac514 Mon Sep 17 00:00:00 2001 From: Shun Sakai Date: Thu, 27 Feb 2025 02:21:19 +0900 Subject: [PATCH] feat: Add support for NTFS extra field (#279) --- Cargo.toml | 2 + README.md | 1 + src/extra_fields/mod.rs | 5 ++ src/extra_fields/ntfs.rs | 97 ++++++++++++++++++++++++++++++++ src/read.rs | 7 ++- tests/data/ntfs.zip | Bin 0 -> 317 bytes tests/zip_extended_timestamp.rs | 10 ++-- tests/zip_ntfs.rs | 29 ++++++++++ 8 files changed, 144 insertions(+), 7 deletions(-) create mode 100644 src/extra_fields/ntfs.rs create mode 100644 tests/data/ntfs.zip create mode 100644 tests/zip_ntfs.rs diff --git a/Cargo.toml b/Cargo.toml index 2a0a79dcf..de1c3fa9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ flate2 = { version = "1.0", default-features = false, optional = true } indexmap = "2" hmac = { version = "0.12", optional = true, features = ["reset"] } memchr = "2.7" +nt-time = { version = "0.10.6", optional = true } pbkdf2 = { version = "0.12", optional = true } rand = { version = "0.8", optional = true } sha1 = { version = "0.10", optional = true } @@ -76,6 +77,7 @@ deflate-miniz = ["deflate", "deflate-flate2"] deflate-zlib = ["flate2/zlib", "deflate-flate2"] deflate-zlib-ng = ["flate2/zlib-ng", "deflate-flate2"] deflate-zopfli = ["zopfli", "_deflate-any"] +nt-time = ["dep:nt-time"] lzma = ["lzma-rs/stream"] unreserved = [] xz = ["lzma-rs/raw_decoder"] diff --git a/README.md b/README.md index d89697173..a29e8415e 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ The features available are: * `bzip2`: Enables the BZip2 compression algorithm. * `time`: Enables features using the [time](https://github.com/rust-lang-deprecated/time) crate. * `chrono`: Enables converting last-modified `zip::DateTime` to and from `chrono::NaiveDateTime`. +* `nt-time`: Enables returning timestamps stored in the NTFS extra field as `nt_time::FileTime`. * `zstd`: Enables the Zstandard compression algorithm. By default `aes-crypto`, `bzip2`, `deflate`, `deflate64`, `lzma`, `time` and `zstd` are enabled. diff --git a/src/extra_fields/mod.rs b/src/extra_fields/mod.rs index ee8defec2..767a56952 100644 --- a/src/extra_fields/mod.rs +++ b/src/extra_fields/mod.rs @@ -17,14 +17,19 @@ impl ExtraFieldVersion for LocalHeaderVersion {} impl ExtraFieldVersion for CentralHeaderVersion {} mod extended_timestamp; +mod ntfs; mod zipinfo_utf8; pub use extended_timestamp::*; +pub use ntfs::Ntfs; pub use zipinfo_utf8::*; /// contains one extra field #[derive(Debug, Clone)] pub enum ExtraField { + /// NTFS extra field + Ntfs(Ntfs), + /// extended timestamp, as described in ExtendedTimestamp(ExtendedTimestamp), } diff --git a/src/extra_fields/ntfs.rs b/src/extra_fields/ntfs.rs new file mode 100644 index 000000000..d303735ba --- /dev/null +++ b/src/extra_fields/ntfs.rs @@ -0,0 +1,97 @@ +use std::io::Read; + +use crate::{ + result::{ZipError, ZipResult}, + unstable::LittleEndianReadExt, +}; + +/// The NTFS extra field as described in [PKWARE's APPNOTE.TXT v6.3.9]. +/// +/// This field stores [Windows file times], which are 64-bit unsigned integer +/// values that represents the number of 100-nanosecond intervals that have +/// elapsed since "1601-01-01 00:00:00 UTC". +/// +/// [PKWARE's APPNOTE.TXT v6.3.9]: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT +/// [Windows file times]: https://docs.microsoft.com/en-us/windows/win32/sysinfo/file-times +#[derive(Clone, Debug)] +pub struct Ntfs { + mtime: u64, + atime: u64, + ctime: u64, +} + +impl Ntfs { + /// Creates a NTFS extra field struct by reading the required bytes from the + /// reader. + /// + /// This method assumes that the length has already been read, therefore it + /// must be passed as an argument. + pub fn try_from_reader(reader: &mut R, len: u16) -> ZipResult + where + R: Read, + { + if len != 32 { + return Err(ZipError::UnsupportedArchive( + "NTFS extra field has an unsupported length", + )); + } + + // Read reserved for future use. + let _ = reader.read_u32_le()?; + + let tag = reader.read_u16_le()?; + if tag != 0x0001 { + return Err(ZipError::UnsupportedArchive( + "NTFS extra field has an unsupported attribute tag", + )); + } + let size = reader.read_u16_le()?; + if size != 24 { + return Err(ZipError::UnsupportedArchive( + "NTFS extra field has an unsupported attribute size", + )); + } + + let mtime = reader.read_u64_le()?; + let atime = reader.read_u64_le()?; + let ctime = reader.read_u64_le()?; + Ok(Self { + mtime, + atime, + ctime, + }) + } + + /// Returns the file last modification time as a file time. + pub fn mtime(&self) -> u64 { + self.mtime + } + + /// Returns the file last modification time as a file time. + #[cfg(feature = "nt-time")] + pub fn modified_file_time(&self) -> nt_time::FileTime { + nt_time::FileTime::new(self.mtime) + } + + /// Returns the file last access time as a file time. + pub fn atime(&self) -> u64 { + self.atime + } + + /// Returns the file last access time as a file time. + #[cfg(feature = "nt-time")] + pub fn accessed_file_time(&self) -> nt_time::FileTime { + nt_time::FileTime::new(self.atime) + } + + /// Returns the file creation time as a file time. + pub fn ctime(&self) -> u64 { + self.ctime + } + + /// Returns the file creation time as a file time. + #[cfg(feature = "nt-time")] + pub fn created_file_time(&self) -> nt_time::FileTime { + nt_time::FileTime::new(self.ctime) + } +} diff --git a/src/read.rs b/src/read.rs index 39686012d..2b40eca3b 100644 --- a/src/read.rs +++ b/src/read.rs @@ -5,7 +5,7 @@ use crate::aes::{AesReader, AesReaderValid}; use crate::compression::{CompressionMethod, Decompressor}; use crate::cp437::FromCp437; use crate::crc32::Crc32Reader; -use crate::extra_fields::{ExtendedTimestamp, ExtraField}; +use crate::extra_fields::{ExtendedTimestamp, ExtraField, Ntfs}; use crate::read::zip_archive::{Shared, SharedBuilder}; use crate::result::{ZipError, ZipResult}; use crate::spec::{self, CentralDirectoryEndInfo, DataAndPosition, FixedSizeBlock, Pod}; @@ -1249,6 +1249,11 @@ pub(crate) fn parse_single_extra_field( reader.read_exact(&mut vec![0u8; leftover_len])?; return Ok(true); } + 0x000a => { + // NTFS extra field + file.extra_fields + .push(ExtraField::Ntfs(Ntfs::try_from_reader(reader, len)?)); + } 0x9901 => { // AES if len != 7 { diff --git a/tests/data/ntfs.zip b/tests/data/ntfs.zip new file mode 100644 index 0000000000000000000000000000000000000000..d826e6b43f1e8461f1f691b596ff7bcfcbd7a934 GIT binary patch literal 317 zcmWIWW@h1HfB;2?xMM~<>Oc+%a{zH}W^QUpWkG6UK|xMta$-qlex80=UW#6RVsU1% zUVcGpUP^v)X>Mv>iC#%+MM(hMusuck literal 0 HcmV?d00001 diff --git a/tests/zip_extended_timestamp.rs b/tests/zip_extended_timestamp.rs index 9657028f9..06c2d24e6 100644 --- a/tests/zip_extended_timestamp.rs +++ b/tests/zip_extended_timestamp.rs @@ -8,12 +8,10 @@ fn test_extended_timestamp() { let mut archive = ZipArchive::new(io::Cursor::new(v)).expect("couldn't open test zip file"); for field in archive.by_name("test.txt").unwrap().extra_data_fields() { - match field { - zip::ExtraField::ExtendedTimestamp(ts) => { - assert!(ts.ac_time().is_none()); - assert!(ts.cr_time().is_none()); - assert_eq!(ts.mod_time().unwrap(), 1714635025); - } + if let zip::ExtraField::ExtendedTimestamp(ts) = field { + assert!(ts.ac_time().is_none()); + assert!(ts.cr_time().is_none()); + assert_eq!(ts.mod_time().unwrap(), 1714635025); } } } diff --git a/tests/zip_ntfs.rs b/tests/zip_ntfs.rs new file mode 100644 index 000000000..9570d974c --- /dev/null +++ b/tests/zip_ntfs.rs @@ -0,0 +1,29 @@ +use std::io; + +use zip::ZipArchive; + +#[test] +fn test_ntfs() { + let mut v = Vec::new(); + v.extend_from_slice(include_bytes!("../tests/data/ntfs.zip")); + let mut archive = ZipArchive::new(io::Cursor::new(v)).expect("couldn't open test zip file"); + + for field in archive.by_name("test.txt").unwrap().extra_data_fields() { + if let zip::ExtraField::Ntfs(ts) = field { + assert_eq!(ts.mtime(), 133_813_273_144_169_390); + #[cfg(feature = "nt-time")] + assert_eq!( + time::OffsetDateTime::try_from(ts.modified_file_time()).unwrap(), + time::macros::datetime!(2025-01-14 11:21:54.416_939_000 UTC) + ); + + assert_eq!(ts.atime(), 0); + #[cfg(feature = "nt-time")] + assert_eq!(ts.accessed_file_time(), nt_time::FileTime::NT_TIME_EPOCH); + + assert_eq!(ts.ctime(), 0); + #[cfg(feature = "nt-time")] + assert_eq!(ts.created_file_time(), nt_time::FileTime::NT_TIME_EPOCH); + } + } +}