Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for NTFS extra field #279

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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::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 @@
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 Expand Up @@ -1380,7 +1385,7 @@
/// `foo/../bar` as `foo/bar` (instead of `bar`). Because of this,
/// [`ZipFile::enclosed_name`] is the better option in most scenarios.
///
/// [`ParentDir`]: `PathBuf::Component::ParentDir`

Check warning on line 1388 in src/read.rs

View workflow job for this annotation

GitHub Actions / style_and_docs (--no-default-features)

unresolved link to `PathBuf::Component::ParentDir`

Check warning on line 1388 in src/read.rs

View workflow job for this annotation

GitHub Actions / style_and_docs

unresolved link to `PathBuf::Component::ParentDir`

Check warning on line 1388 in src/read.rs

View workflow job for this annotation

GitHub Actions / style_and_docs (--all-features)

unresolved link to `PathBuf::Component::ParentDir`
pub fn mangled_name(&self) -> PathBuf {
self.get_metadata().file_name_sanitized()
}
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 @@
/// 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 Expand Up @@ -652,8 +652,8 @@
/// read previously-written files and not overwrite them.
///
/// Note: when using an `inner` that cannot overwrite flushed bytes, do not wrap it in a
/// [BufWriter], because that has a [Seek::seek] method that implicitly calls

Check warning on line 655 in src/write.rs

View workflow job for this annotation

GitHub Actions / style_and_docs (--no-default-features)

unresolved link to `BufWriter`
/// [BufWriter::flush], and ZipWriter needs to seek backward to update each file's header with

Check warning on line 656 in src/write.rs

View workflow job for this annotation

GitHub Actions / style_and_docs (--no-default-features)

unresolved link to `BufWriter::flush`
/// the size and checksum after writing the body.
///
/// This setting is false by default.
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);
}
}
}
Loading