Skip to content

Commit

Permalink
feat: Add support for NTFS extra field
Browse files Browse the repository at this point in the history
  • Loading branch information
sorairolake committed Jan 14, 2025
1 parent 7c20fa3 commit 9674c04
Show file tree
Hide file tree
Showing 9 changed files with 146 additions and 9 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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"]
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions src/extra_fields/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://libzip.org/specifications/extrafld.txt>
ExtendedTimestamp(ExtendedTimestamp),
}
97 changes: 97 additions & 0 deletions src/extra_fields/ntfs.rs
Original file line number Diff line number Diff line change
@@ -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<R>(reader: &mut R, len: u16) -> ZipResult<Self>
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)
}
}
7 changes: 6 additions & 1 deletion src/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -1249,6 +1249,11 @@ pub(crate) fn parse_single_extra_field<R: Read>(
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 {
Expand Down
4 changes: 2 additions & 2 deletions src/write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -536,10 +536,10 @@ impl FileOptions<'_, ExtendedFileOptions> {
/// Removes the extra data fields.
#[must_use]
pub fn clear_extra_data(mut self) -> Self {
if self.extended_options.extra_data.len() > 0 {
if !self.extended_options.extra_data.is_empty() {
self.extended_options.extra_data = Arc::new(vec![]);
}
if self.extended_options.central_extra_data.len() > 0 {
if !self.extended_options.central_extra_data.is_empty() {
self.extended_options.central_extra_data = Arc::new(vec![]);
}
self
Expand Down
Binary file added tests/data/ntfs.zip
Binary file not shown.
10 changes: 4 additions & 6 deletions tests/zip_extended_timestamp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
29 changes: 29 additions & 0 deletions tests/zip_ntfs.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}

0 comments on commit 9674c04

Please sign in to comment.