Skip to content

Commit 57b5ecc

Browse files
fix: Document feature unreserved and make the mapping of extra fields public (#616)
* fix #612 * fix: document more extra field * better fix * feat: add a usage test * aslways use defined constants * Update src/write.rs Co-authored-by: Chris Hennick <4961925+Pr0methean@users.noreply.github.com> Signed-off-by: n4n5 <git@n4n5.dev> * Update Cargo.toml Co-authored-by: Chris Hennick <4961925+Pr0methean@users.noreply.github.com> Signed-off-by: n4n5 <git@n4n5.dev> * fix * Fix `test_extra_field_access`: asserting both decimal and hex values --------- Signed-off-by: n4n5 <git@n4n5.dev> Co-authored-by: Chris Hennick <4961925+Pr0methean@users.noreply.github.com>
1 parent 579b67c commit 57b5ecc

7 files changed

Lines changed: 161 additions & 33 deletions

File tree

.github/workflows/cargo_hack.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,4 @@ jobs:
3333
with:
3434
path: target
3535
key: ${{ runner.os }}-rust-cargo-hack-${{ hashFiles('Cargo.toml') }}
36-
- run: cargo hack test --feature-powerset --all-targets --exclude-features _bzip2_any,_arbitrary,_deflate-any,_all-features
36+
- run: cargo hack test --feature-powerset --all-targets --exclude-features _bzip2_any,_arbitrary,_deflate-any

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ jiff-02 = ["dep:jiff"]
9898
nt-time = ["dep:nt-time"]
9999
lzma = ["dep:lzma-rust2"]
100100
ppmd = ["dep:ppmd-rust"]
101+
# This feature allows writing custom extra-data field IDs in file headers.
101102
unreserved = []
102103
xz = ["dep:lzma-rust2"]
103104
_bzip2_any = []

src/extra_fields/mod.rs

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//! types for extra fields
1+
//! Types for extra fields
22
33
/// marker trait to denote the place where this extra field has been stored
44
pub trait ExtraFieldVersion {}
@@ -20,6 +20,7 @@ mod extended_timestamp;
2020
mod ntfs;
2121
mod zipinfo_utf8;
2222

23+
// re-export
2324
pub use extended_timestamp::*;
2425
pub use ntfs::Ntfs;
2526
pub use zipinfo_utf8::UnicodeExtraField;
@@ -33,3 +34,116 @@ pub enum ExtraField {
3334
/// extended timestamp, as described in <https://libzip.org/specifications/extrafld.txt>
3435
ExtendedTimestamp(ExtendedTimestamp),
3536
}
37+
38+
/// Extra field used in this crate
39+
#[repr(u16)]
40+
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
41+
pub(crate) enum UsedExtraField {
42+
/// ZIP64 extended information extra field
43+
Zip64ExtendedInfo = 0x0001,
44+
/// NTFS
45+
Ntfs = 0x000a,
46+
/// extended timestamp
47+
/// from https://libzip.org/specifications/extrafld.txt
48+
ExtendedTimestamp = 0x5455,
49+
/// Info-ZIP Unicode Comment Extra Field
50+
UnicodeComment = 0x6375,
51+
/// Info-ZIP Unicode Path Extra Field
52+
UnicodePath = 0x7075,
53+
/// AE-x encryption structure
54+
AeXEncryption = 0x9901,
55+
/// Data Stream Alignment (Apache Commons-Compress)
56+
DataStreamAlignement = 0xa11e,
57+
}
58+
59+
macro_rules! extra_field_match {
60+
($x:expr, $( $variant:path ),+ $(,)?) => {
61+
match $x {
62+
$(
63+
v if v == $variant as u16 => Ok($variant),
64+
)+
65+
_ => Err(()),
66+
}
67+
};
68+
}
69+
70+
impl TryFrom<u16> for UsedExtraField {
71+
type Error = ();
72+
73+
fn try_from(value: u16) -> Result<Self, Self::Error> {
74+
extra_field_match!(
75+
value,
76+
UsedExtraField::Zip64ExtendedInfo,
77+
UsedExtraField::Ntfs,
78+
UsedExtraField::ExtendedTimestamp,
79+
UsedExtraField::UnicodeComment,
80+
UsedExtraField::UnicodePath,
81+
UsedExtraField::DataStreamAlignement,
82+
UsedExtraField::AeXEncryption,
83+
)
84+
}
85+
}
86+
// AE-x encryption structure
87+
88+
/// Known Extra fields (PKWARE and Third party) mappings
89+
pub const EXTRA_FIELD_MAPPING: [u16; 58] = [
90+
UsedExtraField::Zip64ExtendedInfo as u16,
91+
0x0007, // AV Info
92+
0x0008, // Reserved for extended language encoding data (PFS)
93+
0x0009, // OS/2
94+
UsedExtraField::Ntfs as u16,
95+
0x000c, // OpenVMS
96+
0x000d, // UNIX
97+
0x000e, // Reserved for file stream and fork descriptors
98+
0x000f, // Patch Descriptor
99+
0x0014, // PKCS#7 Store for X.509 Certificates
100+
0x0015, // X.509 Certificate ID and Signature for individual file
101+
0x0016, // X.509 Certificate ID for Central Directory
102+
0x0017, // Strong Encryption Header
103+
0x0018, // Record Management Controls
104+
0x0019, // PKCS#7 Encryption Recipient Certificate List
105+
0x0020, // Reserved for Timestamp record
106+
0x0021, // Policy Decryption Key Record
107+
0x0022, // Smartcrypt Key Provider Record
108+
0x0023, // Smartcrypt Policy Key Data Record
109+
0x0065, // IBM S/390 (Z390), AS/400 (I400) attributes - uncompressed
110+
0x0066, // Reserved for IBM S/390 (Z390), AS/400 (I400) attributes - compressed
111+
0x4690, // POSZIP 4690 (reserved)
112+
// Third party mappings commonly used
113+
0x07c8, // Macintosh
114+
0x1986, // Pixar USD header ID
115+
0x2605, // ZipIt Macintosh
116+
0x2705, // ZipIt Macintosh 1.3.5+
117+
0x2805, // ZipIt Macintosh 1.3.5+
118+
0x334d, // Info-ZIP Macintosh
119+
0x4154, // Tandem
120+
0x4341, // Acorn/SparkFS
121+
0x4453, // Windows NT security descriptor (binary ACL)
122+
0x4704, // VM/CMS
123+
0x470f, // MVS
124+
0x4854, // THEOS (old?)
125+
0x4b46, // FWKCS MD5
126+
0x4c41, // OS/2 access control list (text ACL)
127+
0x4d49, // Info-ZIP OpenVMS
128+
0x4d63, // Macintosh Smartzip (??)
129+
0x4f4c, // Xceed original location extra field
130+
0x5356, // AOS/VS (ACL)
131+
0x554e, // Xceed unicode extra field
132+
0x5855, // Info-ZIP UNIX (original, also OS/2, NT, etc)
133+
UsedExtraField::UnicodeComment as u16,
134+
0x6542, // BeOS/BeBox
135+
0x6854, // THEOS
136+
UsedExtraField::UnicodePath as u16,
137+
0x7441, // AtheOS/Syllable
138+
0x756e, // ASi UNIX
139+
0x7855, // Info-ZIP UNIX (new)
140+
0x7875, // Info-ZIP UNIX (newer UID/GID)
141+
UsedExtraField::DataStreamAlignement as u16,
142+
0xa220, // Microsoft Open Packaging Growth Hint
143+
0xcafe, // Java JAR file Extra Field Header ID
144+
0xd935, // Android ZIP Alignment Extra Field
145+
0xe57a, // Korean ZIP code page info
146+
0xfd4a, // SMS/QDOS
147+
UsedExtraField::AeXEncryption as u16,
148+
0x9902, // unknown
149+
];

src/read.rs

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::cfg_if;
44
use crate::compression::{CompressionMethod, Decompressor};
55
use crate::cp437::FromCp437;
66
use crate::crc32::Crc32Reader;
7-
use crate::extra_fields::{ExtendedTimestamp, ExtraField, Ntfs};
7+
use crate::extra_fields::{ExtendedTimestamp, ExtraField, Ntfs, UsedExtraField};
88
use crate::result::{invalid, ZipError, ZipResult};
99
use crate::spec::{
1010
self, CentralDirectoryEndInfo, DataAndPosition, FixedSizeBlock, Pod, ZIP64_BYTES_THR,
@@ -1573,9 +1573,9 @@ pub(crate) fn parse_single_extra_field<R: Read>(
15731573
}
15741574
Err(e) => return Err(e.into()),
15751575
};
1576-
match kind {
1576+
match UsedExtraField::try_from(kind) {
15771577
// Zip64 extended information extra field
1578-
0x0001 => {
1578+
Ok(UsedExtraField::Zip64ExtendedInfo) => {
15791579
if disallow_zip64 {
15801580
return Err(invalid!("Can't write a custom field using the ZIP64 ID"));
15811581
}
@@ -1622,12 +1622,12 @@ pub(crate) fn parse_single_extra_field<R: Read>(
16221622
}
16231623
return Ok(true);
16241624
}
1625-
0x000a => {
1625+
Ok(UsedExtraField::Ntfs) => {
16261626
// NTFS extra field
16271627
file.extra_fields
16281628
.push(ExtraField::Ntfs(Ntfs::try_from_reader(reader, len)?));
16291629
}
1630-
0x9901 => {
1630+
Ok(UsedExtraField::AeXEncryption) => {
16311631
// AES
16321632
if len != 7 {
16331633
return Err(ZipError::UnsupportedArchive(
@@ -1663,15 +1663,12 @@ pub(crate) fn parse_single_extra_field<R: Read>(
16631663
file.compression_method = compression_method;
16641664
file.aes_extra_data_start = bytes_already_read;
16651665
}
1666-
0x5455 => {
1667-
// extended timestamp
1668-
// https://libzip.org/specifications/extrafld.txt
1669-
1666+
Ok(UsedExtraField::ExtendedTimestamp) => {
16701667
file.extra_fields.push(ExtraField::ExtendedTimestamp(
16711668
ExtendedTimestamp::try_from_reader(reader, len)?,
16721669
));
16731670
}
1674-
0x6375 => {
1671+
Ok(UsedExtraField::UnicodeComment) => {
16751672
// Info-ZIP Unicode Comment Extra Field
16761673
// APPNOTE 4.6.8 and https://libzip.org/specifications/extrafld.txt
16771674
file.file_comment = String::from_utf8(
@@ -1681,7 +1678,7 @@ pub(crate) fn parse_single_extra_field<R: Read>(
16811678
)?
16821679
.into();
16831680
}
1684-
0x7075 => {
1681+
Ok(UsedExtraField::UnicodePath) => {
16851682
// Info-ZIP Unicode Path Extra Field
16861683
// APPNOTE 4.6.9 and https://libzip.org/specifications/extrafld.txt
16871684
file.file_name_raw = UnicodeExtraField::try_from_reader(reader, len)?

src/spec.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#![macro_use]
22

3+
use crate::extra_fields::UsedExtraField;
34
use crate::read::magic_finder::{Backwards, Forward, MagicFinder, OptimisticMagicFinder};
45
use crate::read::ArchiveOffset;
56
use crate::result::{invalid, ZipError, ZipResult};
@@ -86,7 +87,7 @@ impl ExtraFieldMagic {
8687
Self(u16::to_le(self.0))
8788
}
8889

89-
pub const ZIP64_EXTRA_FIELD_TAG: Self = Self::literal(0x0001);
90+
pub const ZIP64_EXTRA_FIELD_TAG: Self = Self::literal(UsedExtraField::Zip64ExtendedInfo as u16);
9091
}
9192

9293
/// The file size at which a ZIP64 record becomes necessary.

src/write.rs

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Writing a ZIP archive
22
33
use crate::compression::CompressionMethod;
4+
use crate::extra_fields::UsedExtraField;
45
use crate::read::{parse_single_extra_field, Config, ZipArchive, ZipFile};
56
use crate::result::{invalid, ZipError, ZipResult};
67
use crate::spec::{self, FixedSizeBlock, Zip32CDEBlock};
@@ -307,14 +308,20 @@ impl ExtendedFileOptions {
307308
}
308309
#[cfg(not(feature = "unreserved"))]
309310
{
310-
use crate::unstable::LittleEndianReadExt;
311+
use crate::{
312+
extra_fields::{UsedExtraField, EXTRA_FIELD_MAPPING},
313+
unstable::LittleEndianReadExt,
314+
};
311315
let header_id = data.read_u16_le()?;
312-
if EXTRA_FIELD_MAPPING.contains(&header_id) {
313-
return Err(ZipError::Io(io::Error::other(
314-
format!(
315-
"Extra data header ID {header_id:#06} requires crate feature \"unreserved\"",
316-
),
317-
)));
316+
// Some extra fields are authorized
317+
if let Err(()) = UsedExtraField::try_from(header_id) {
318+
if EXTRA_FIELD_MAPPING.contains(&header_id) {
319+
return Err(ZipError::Io(io::Error::other(format!(
320+
"Extra data header ID {:#06} (0x{:x}) \
321+
requires crate feature \"unreserved\"",
322+
header_id, header_id,
323+
))));
324+
}
318325
}
319326
data.seek(SeekFrom::Current(-2))?;
320327
}
@@ -1013,7 +1020,11 @@ impl<W: Write + Seek> ZipWriter<W> {
10131020
body[4] = mode as u8; // strength
10141021
[body[5], body[6]] = underlying.serialize_to_u16().to_le_bytes(); // real compression method
10151022
aes_extra_data_start = extra_data.len() as u64;
1016-
ExtendedFileOptions::add_extra_data_unchecked(&mut extra_data, 0x9901, &body)?;
1023+
ExtendedFileOptions::add_extra_data_unchecked(
1024+
&mut extra_data,
1025+
UsedExtraField::AeXEncryption as u16,
1026+
&body,
1027+
)?;
10171028
}
10181029
let header_end =
10191030
header_start + size_of::<ZipLocalEntryBlock>() as u64 + name.to_string().len() as u64;
@@ -1038,7 +1049,7 @@ impl<W: Write + Seek> ZipWriter<W> {
10381049
[pad_body[0], pad_body[1]] = options.alignment.to_le_bytes();
10391050
ExtendedFileOptions::add_extra_data_unchecked(
10401051
&mut extra_data,
1041-
0xa11e,
1052+
UsedExtraField::DataStreamAlignement as u16,
10421053
&pad_body,
10431054
)?;
10441055
debug_assert_eq!((extra_data.len() as u64 + header_end) % align, 0);
@@ -2157,7 +2168,7 @@ fn update_aes_extra_data<W: Write + Seek>(
21572168

21582169
/* TODO: implement this using the Block trait! */
21592170
// Extra field header ID.
2160-
buf.write_u16_le(0x9901)?;
2171+
buf.write_u16_le(UsedExtraField::AeXEncryption as u16)?;
21612172
// Data size.
21622173
buf.write_u16_le(7)?;
21632174
// Integer version number.
@@ -2260,7 +2271,7 @@ fn strip_alignment_extra_field(extra_field: &[u8]) -> Vec<u8> {
22602271
break;
22612272
}
22622273

2263-
if tag != 0xa11e {
2274+
if tag != UsedExtraField::DataStreamAlignement as u16 {
22642275
new_extra.extend_from_slice(&extra_field[cursor..cursor + 4 + len]);
22652276
}
22662277
cursor += 4 + len;
@@ -2350,14 +2361,6 @@ impl<W: Write> Seek for StreamWriter<W> {
23502361
}
23512362
}
23522363

2353-
#[cfg(not(feature = "unreserved"))]
2354-
const EXTRA_FIELD_MAPPING: [u16; 43] = [
2355-
0x0007, 0x0008, 0x0009, 0x000a, 0x000c, 0x000d, 0x000e, 0x000f, 0x0014, 0x0015, 0x0016, 0x0017,
2356-
0x0018, 0x0019, 0x0020, 0x0021, 0x0022, 0x0023, 0x0065, 0x0066, 0x4690, 0x07c8, 0x2605, 0x2705,
2357-
0x2805, 0x334d, 0x4341, 0x4453, 0x4704, 0x470f, 0x4b46, 0x4c41, 0x4d49, 0x4f4c, 0x5356, 0x554e,
2358-
0x5855, 0x6542, 0x756e, 0x7855, 0xa220, 0xfd4a, 0x9902,
2359-
];
2360-
23612364
#[cfg(test)]
23622365
#[allow(unknown_lints)] // needless_update is new in clippy pre 1.29.0
23632366
#[allow(clippy::needless_update)] // So we can use the same FileOptions decls with and without zopfli_buffer_size

tests/end_to_end.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,3 +268,15 @@ const ENTRY_NAME: &str = "test/lorem_ipsum.txt";
268268
const COPY_ENTRY_NAME: &str = "test/lorem_ipsum_renamed.txt";
269269

270270
const INTERNAL_COPY_ENTRY_NAME: &str = "test/lorem_ipsum_copied.txt";
271+
272+
#[test]
273+
fn test_extra_field_access() {
274+
// just a test to access the variable in the crate
275+
use zip::extra_fields::EXTRA_FIELD_MAPPING;
276+
277+
assert_eq!(EXTRA_FIELD_MAPPING[0], 1);
278+
assert_eq!(EXTRA_FIELD_MAPPING[0], 0x0001); // ZIP64 extended information extra field
279+
280+
assert_eq!(EXTRA_FIELD_MAPPING[12], 23);
281+
assert_eq!(EXTRA_FIELD_MAPPING[12], 0x0017); // Strong Encryption Header
282+
}

0 commit comments

Comments
 (0)