Skip to main content

zip/extra_fields/
extra_field.rs

1//! Code related to the `ExtraField` enum
2
3#[cfg(feature = "aes-crypto")]
4use crate::extra_fields::AexEncryption;
5use crate::extra_fields::CustomExtraField;
6use crate::extra_fields::DataStreamAlignment;
7use crate::extra_fields::ExtendedTimestamp;
8use crate::extra_fields::Ntfs;
9use crate::extra_fields::UnicodeExtraField;
10use crate::extra_fields::UsedExtraField;
11use crate::extra_fields::Zip64ExtendedInformation;
12use crate::extra_fields::zip64_extended_information::Zip64Sizes;
13use crate::format::blocks::ZipEntryBlock;
14use crate::format::flags::ZipFlags;
15use crate::result::ZipResult;
16use crate::result::invalid;
17use crate::types::ZipFileData;
18use crate::unstable::LittleEndianReadExt;
19use std::io::ErrorKind;
20use std::io::{Cursor, Read, Write};
21
22/// contains one extra field
23#[derive(Debug, Clone, PartialEq)]
24#[non_exhaustive]
25pub enum ExtraField {
26    /// NTFS extra field
27    Ntfs(Ntfs),
28    /// extended timestamp, as described in <https://libzip.org/specifications/extrafld.txt>
29    ExtendedTimestamp(ExtendedTimestamp),
30    /// AeX Encryption
31    #[cfg(feature = "aes-crypto")]
32    AeXEncryption(AexEncryption),
33    /// Zip64 Information
34    Zip64ExtendedInformation(Zip64ExtendedInformation),
35    /// Unicode Comment
36    UnicodeComment(UnicodeExtraField),
37    /// UnicodePath
38    UnicodePath(UnicodeExtraField),
39    /// Data Stream Alignment
40    DataStreamAlignment(DataStreamAlignment),
41    /// Custom extra field
42    Custom(CustomExtraField),
43}
44
45/// Extra fields list
46#[derive(Debug, Clone, Default)]
47pub struct ExtraFields {
48    pub(crate) inner: Vec<ExtraField>,
49}
50
51impl ExtraFields {
52    pub(crate) fn parse<B: ZipEntryBlock>(
53        buff: &[u8],
54        block: &B,
55        is_local_header: bool,
56    ) -> ZipResult<Self> {
57        let mut reader = Cursor::new(buff);
58        let mut extra_fields = Vec::new();
59        while (reader.position() as usize) < buff.len() {
60            let parsed_extra_field = ExtraField::parse(&mut reader, block, is_local_header)?;
61            let Some(parsed_extra_field) = parsed_extra_field else {
62                break;
63            };
64            extra_fields.push(parsed_extra_field);
65        }
66        Ok(Self {
67            inner: extra_fields,
68        })
69    }
70
71    pub(crate) fn local_extra_fields_mut(&mut self) -> impl Iterator<Item = &mut ExtraField> {
72        self.inner.iter_mut()
73    }
74
75    pub(crate) fn local_extra_fields(&self) -> impl Iterator<Item = &ExtraField> {
76        self.inner.iter().filter(|ef| match ef {
77            ExtraField::Custom(cef) => !cef.central_only,
78            _ => true,
79        })
80    }
81
82    pub(crate) fn central_extra_fields(&self) -> impl Iterator<Item = &ExtraField> {
83        // data alignment is local only
84        self.inner
85            .iter()
86            .filter(|ef| !matches!(ef, ExtraField::DataStreamAlignment(_)))
87    }
88}
89
90impl ExtraField {
91    pub(crate) fn parse<R: Read, B: ZipEntryBlock>(
92        reader: &mut R,
93        file: &B,
94        is_local_header: bool,
95    ) -> ZipResult<Option<Self>> {
96        let extra_field_header_id = match reader.read_u16_le() {
97            Ok(value) => value,
98            Err(e) if e.kind() == ErrorKind::UnexpectedEof => return Ok(None),
99            Err(e) => return Err(e.into()),
100        };
101        let decoded_extra_field = UsedExtraField::try_from(extra_field_header_id);
102        let len = match decoded_extra_field {
103            Ok(known_field) => match reader.read_u16_le() {
104                Ok(len) => len,
105                Err(e) if e.kind() == ErrorKind::UnexpectedEof => {
106                    return Err(invalid!("Extra field {} header truncated", known_field));
107                }
108                Err(e) => return Err(e.into()),
109            },
110            Err(()) => {
111                match reader.read_u16_le() {
112                    Ok(len) => len,
113                    Err(e) if e.kind() == ErrorKind::UnexpectedEof => return Ok(None), // early return, most likely a padding
114                    Err(_e) => {
115                        // Consume remaining bytes to avoid infinite loop in caller
116                        let _ = std::io::copy(reader, &mut std::io::sink());
117                        return Ok(None);
118                    }
119                }
120            }
121        };
122        let parsed_extra_field = match decoded_extra_field {
123            // Zip64 extended information extra field
124            Ok(UsedExtraField::Zip64ExtendedInfo) => {
125                ExtraField::Zip64ExtendedInformation(Zip64ExtendedInformation::parse(
126                    reader,
127                    len,
128                    file.get_uncompressed_size(),
129                    file.get_compressed_size(),
130                    file.get_header_start(),
131                )?)
132            }
133            Ok(UsedExtraField::Ntfs) => {
134                // NTFS extra field
135                ExtraField::Ntfs(Ntfs::try_from_reader(reader, len)?)
136            }
137            #[cfg(feature = "aes-crypto")]
138            Ok(UsedExtraField::AeXEncryption) => {
139                // AES
140                let (new_aes_enc, inner_compression) = AexEncryption::parse(reader, len)?;
141                ExtraField::AeXEncryption(AexEncryption::new(
142                    new_aes_enc.1,
143                    new_aes_enc.0,
144                    inner_compression,
145                ))
146            }
147            Ok(UsedExtraField::ExtendedTimestamp) => ExtraField::ExtendedTimestamp(
148                ExtendedTimestamp::try_from_reader(reader, len, is_local_header)?,
149            ),
150            Ok(UsedExtraField::UnicodeComment) => {
151                // Info-ZIP Unicode Comment Extra Field
152                // APPNOTE 4.6.8 and https://libzip.org/specifications/extrafld.txt
153                let unicode = UnicodeExtraField::try_from_reader(reader, len)?;
154                ExtraField::UnicodeComment(unicode)
155            }
156            Ok(UsedExtraField::UnicodePath) => {
157                // Info-ZIP Unicode Path Extra Field
158                // APPNOTE 4.6.9 and https://libzip.org/specifications/extrafld.txt
159                let unicode = UnicodeExtraField::try_from_reader(reader, len)?;
160                ExtraField::UnicodePath(unicode)
161            }
162            _ => {
163                let mut buf = vec![0u8; len as usize];
164                if let Err(e) = reader.read_exact(&mut buf) {
165                    if e.kind() == ErrorKind::UnexpectedEof {
166                        return Err(invalid!("Extra field content truncated"));
167                    }
168                    return Err(e.into());
169                }
170                ExtraField::Custom(CustomExtraField::new(
171                    false,
172                    extra_field_header_id,
173                    buf.into_boxed_slice(),
174                ))
175                // Other fields are ignored
176            }
177        };
178        Ok(Some(parsed_extra_field))
179    }
180
181    pub(crate) fn size(&self, is_local_header: bool) -> usize {
182        match self {
183            // Zip64 extended information extra field
184            ExtraField::Zip64ExtendedInformation(zip64_extra) => {
185                zip64_extra.full_size(is_local_header)
186            }
187            ExtraField::Ntfs(_ntfs) => {
188                // NTFS extra field
189                0
190            }
191            #[cfg(feature = "aes-crypto")]
192            ExtraField::AeXEncryption(aes) => aes.full_size(),
193            ExtraField::ExtendedTimestamp(_extended_timestamp) => {
194                // nothing to do
195                0
196            }
197            ExtraField::UnicodeComment(unicode_comment) => unicode_comment.full_size(),
198            ExtraField::UnicodePath(unicode_path) => unicode_path.full_size(),
199            ExtraField::Custom(custom) => custom.full_size(is_local_header),
200            ExtraField::DataStreamAlignment(data_stream_alignment) => {
201                data_stream_alignment.full_size(is_local_header)
202            }
203        }
204    }
205
206    pub(crate) fn write<W: Write>(&self, writer: &mut W, is_local_header: bool) -> ZipResult<()> {
207        match self {
208            // Zip64 extended information extra field
209            ExtraField::Zip64ExtendedInformation(zip64_extra) => {
210                zip64_extra.write(writer, is_local_header)?;
211            }
212            #[cfg(feature = "aes-crypto")]
213            ExtraField::AeXEncryption(aex) => {
214                aex.write(writer)?;
215            }
216            ExtraField::Custom(custom) => {
217                custom.write(writer, is_local_header)?;
218            }
219            ExtraField::UnicodeComment(unicode_comment) => {
220                let magic = UsedExtraField::UnicodeComment.as_u16();
221                writer.write_all(&magic.to_le_bytes())?;
222                unicode_comment.write(writer)?;
223            }
224            ExtraField::UnicodePath(unicode_path) => {
225                let magic = UsedExtraField::UnicodePath.as_u16();
226                writer.write_all(&magic.to_le_bytes())?;
227                unicode_path.write(writer)?;
228            }
229            ExtraField::DataStreamAlignment(data_stream_alignment) => {
230                data_stream_alignment.write(writer, is_local_header)?
231            }
232            _ => {
233                // nothing to do
234            }
235        }
236        Ok(())
237    }
238}
239
240impl ZipFileData {
241    pub(crate) fn apply_extra_fields(&mut self, file_name_raw: &mut Vec<u8>) -> ZipResult<()> {
242        for one_extra_field in &self.extra_fields.inner {
243            match one_extra_field {
244                // Zip64 extended information extra field
245                ExtraField::Zip64ExtendedInformation(zip64_block) => {
246                    self.large_file = true;
247                    if let Some(Zip64Sizes {
248                        uncompressed_size,
249                        compressed_size,
250                    }) = zip64_block.sizes
251                    {
252                        self.uncompressed_size = uncompressed_size;
253                        self.compressed_size = compressed_size;
254                    }
255                    if let Some(head_start) = zip64_block.header_start {
256                        self.header_start = head_start;
257                    }
258                }
259                #[cfg(feature = "aes-crypto")]
260                ExtraField::AeXEncryption(AexEncryption {
261                    compression_method, ..
262                }) => {
263                    self.compression_method = *compression_method;
264                }
265                ExtraField::UnicodeComment(unicode) => {
266                    // Info-ZIP Unicode Comment Extra Field
267                    // APPNOTE 4.6.8 and https://libzip.org/specifications/extrafld.txt
268                    // If the CRC check fails, this Unicode Comment extra field SHOULD be ignored and
269                    // the File Comment field in the header SHOULD be used instead.
270                    // Check if the comment is UTF-8
271                    if unicode.is_crc32_valid(self.file_comment.as_bytes())
272                        && let Ok(comment) = String::from_utf8(unicode.content.to_vec())
273                    {
274                        self.file_comment = comment.into_boxed_str();
275                    }
276                }
277                #[allow(clippy::collapsible_match)]
278                ExtraField::UnicodePath(unicode) => {
279                    // Info-ZIP Unicode Path Extra Field
280                    // APPNOTE 4.6.9 and https://libzip.org/specifications/extrafld.txt
281                    // If the CRC check fails, this UTF-8 Path Extra Field SHOULD be ignored and
282                    // the File Name field in the header SHOULD be used instead.
283                    if unicode.is_crc32_valid(file_name_raw)
284                        && std::str::from_utf8(&unicode.content).is_ok()
285                    {
286                        *file_name_raw = unicode.content.to_vec();
287                        self.flags |= ZipFlags::LanguageEncoding.as_u16();
288                    }
289                }
290                _ => {
291                    // nothing to do
292                }
293            }
294        }
295        Ok(())
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use crate::extra_fields::ExtraField;
302    use crate::extra_fields::ExtraFields;
303    use crate::format::blocks::ZipEntryBlock;
304
305    struct PlaceHolderBlock;
306
307    impl ZipEntryBlock for PlaceHolderBlock {
308        fn get_uncompressed_size(&self) -> u32 {
309            0
310        }
311        fn get_compressed_size(&self) -> u32 {
312            0
313        }
314        fn get_header_start(&self) -> Option<u32> {
315            None
316        }
317    }
318
319    #[test]
320    #[cfg(feature = "aes-crypto")]
321    fn aex_extra_field_with_feature() {
322        let buff = [1, 0x99, 7, 0, 1, 0, b'A', b'E', 3, 0, 0];
323
324        let extra_fields = ExtraFields::parse(&buff[..], &PlaceHolderBlock, true).unwrap();
325        assert!(matches!(
326            extra_fields.inner[0],
327            ExtraField::AeXEncryption(..)
328        ));
329    }
330
331    #[test]
332    #[cfg(not(feature = "aes-crypto"))]
333    fn aex_extra_field_without_feature() {
334        use crate::extra_fields::CustomExtraField;
335        use crate::extra_fields::UsedExtraField;
336
337        let buff = [1, 0x99, 7, 0, 1, 0, b'A', b'E', 3, 0, 0];
338
339        let extra_fields = ExtraFields::parse(&buff[..], &PlaceHolderBlock, true).unwrap();
340        let extra = CustomExtraField::new(
341            false,
342            UsedExtraField::AeXEncryption.as_u16(),
343            [1, 0, b'A', b'E', 3, 0, 0].to_vec().into_boxed_slice(),
344        );
345        assert_eq!(extra_fields.inner[0], ExtraField::Custom(extra));
346    }
347}