eazip 0.2.4

An simple yet flexible zip library
Documentation
//! Extra fields parsing
//!
//! Reference: <https://libzip.org/specifications/extrafld.txt>

#![allow(unused)]

use std::fmt;

use crate::utils::Timestamp;

#[derive(Clone)]
struct DataParser<'a>(&'a [u8]);

impl<'a> DataParser<'a> {
    fn read_u8(&mut self) -> Option<u8> {
        let (x, data) = self.0.split_first()?;
        self.0 = data;
        Some(*x)
    }

    fn read_u16(&mut self) -> Option<u16> {
        let (bytes, data) = self.0.split_first_chunk()?;
        self.0 = data;
        Some(u16::from_le_bytes(*bytes))
    }

    fn read_u32(&mut self) -> Option<u32> {
        let (bytes, data) = self.0.split_first_chunk()?;
        self.0 = data;
        Some(u32::from_le_bytes(*bytes))
    }

    fn read_u64(&mut self) -> Option<u64> {
        let (bytes, data) = self.0.split_first_chunk()?;
        self.0 = data;
        Some(u64::from_le_bytes(*bytes))
    }

    fn read_variable(&mut self, len: usize) -> Option<&'a [u8]> {
        let (bytes, data) = self.0.split_at_checked(len)?;
        self.0 = data;
        Some(bytes)
    }

    fn end(self) -> Option<()> {
        if self.0.is_empty() { Some(()) } else { None }
    }
}

#[derive(Clone)]
pub(crate) struct ExtraFields<'a>(pub &'a [u8]);

impl ExtraFields<'_> {
    pub fn iter(&self) -> ExtraFieldIterator<'_> {
        ExtraFieldIterator(DataParser(self.0))
    }
}

impl fmt::Debug for ExtraFields<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        struct FromFn<F: Fn(&mut fmt::Formatter<'_>) -> fmt::Result>(F);

        impl<F: Fn(&mut fmt::Formatter<'_>) -> fmt::Result> fmt::Debug for FromFn<F> {
            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                (self.0)(f)
            }
        }

        f.debug_list()
            .entries(self.iter().map(|e| FromFn(move |f| write!(f, "{e:?}"))))
            .finish()
    }
}

pub struct ExtraFieldIterator<'a>(DataParser<'a>);

impl<'a> Iterator for ExtraFieldIterator<'a> {
    type Item = ExtraField<'a>;

    #[inline]
    fn next(&mut self) -> Option<Self::Item> {
        ExtraField::parse(&mut self.0)
    }
}

#[derive(Debug, Clone)]
pub enum ExtraField<'a> {
    Zip64ExtendedInformation(Zip64ExtendedInformation<'a>),
    Ntfs(Ntfs),
    ExtendedTimestamp(ExtendedTimestamp),
    UnicodeComment(UnicodeComment<'a>),
    UnicodeName(UnicodeName<'a>),
    UnixNew(UnixNew<'a>),
    Aes(Aes),

    Unknown,
    Invalid,
}

impl<'a> ExtraField<'a> {
    fn parse(extra_data: &mut DataParser<'a>) -> Option<Self> {
        // Accept padding null bytes although this is not spec compliant
        if matches!(extra_data.0, [] | [0] | [0, 0] | [0, 0, 0]) {
            return None;
        }

        let field = (|| {
            let id = extra_data.read_u16()?;
            let len = extra_data.read_u16()?;

            let data = DataParser(extra_data.read_variable(len as usize)?);

            Some(match id {
                0x0001 => {
                    ExtraField::Zip64ExtendedInformation(Zip64ExtendedInformation::parse(data)?)
                }
                0x000a => ExtraField::Ntfs(Ntfs::parse(data)?),
                0x5455 => ExtraField::ExtendedTimestamp(ExtendedTimestamp::parse(data)?),
                0x6375 => ExtraField::UnicodeComment(UnicodeComment::parse(data)?),
                0x7075 => ExtraField::UnicodeName(UnicodeName::parse(data)?),
                0x7875 => ExtraField::UnixNew(UnixNew::parse(data)?),
                0x9901 => ExtraField::Aes(Aes::parse(data)?),
                _ => ExtraField::Unknown,
            })
        })();

        Some(field.unwrap_or(ExtraField::Invalid))
    }
}

#[derive(Clone)]
pub struct Zip64ExtendedInformation<'a> {
    info: DataParser<'a>,
}

impl<'a> Zip64ExtendedInformation<'a> {
    fn parse(data: DataParser<'a>) -> Option<Self> {
        Some(Self { info: data })
    }

    pub(crate) fn next(&mut self) -> Option<u64> {
        self.info.read_u64()
    }

    pub(crate) fn end(self) -> Option<()> {
        self.info.end()
    }
}

impl<'a> fmt::Debug for Zip64ExtendedInformation<'a> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Zip64ExtendedInformation")
            .field("info", &self.info.0)
            .finish()
    }
}

#[derive(Default, Debug, Clone)]
pub struct NtfsTimes {
    pub mtime: Option<Timestamp>,
    pub atime: Option<Timestamp>,
    pub ctime: Option<Timestamp>,
}

#[derive(Default, Debug, Clone)]
pub struct Ntfs {
    pub times: NtfsTimes,
}

impl Ntfs {
    fn parse(mut data: DataParser<'_>) -> Option<Self> {
        let _reserved = data.read_u32()?;

        let mut ntfs = Ntfs::default();

        while !data.0.is_empty() {
            let typ = data.read_u16()?;
            let len = data.read_u16()?;
            let mut data = DataParser(data.read_variable(len as _)?);

            match typ {
                0x0001 => {
                    let mut read_time = || {
                        let ts = data.read_u64()?;
                        Some((ts != 0).then(|| Timestamp::from_ntfs(ts)))
                    };

                    ntfs.times = NtfsTimes {
                        mtime: read_time()?,
                        atime: read_time()?,
                        ctime: read_time()?,
                    };
                }
                _ => continue, // unsupported
            }
        }

        Some(ntfs)
    }
}

#[derive(Debug, Clone)]
pub struct ExtendedTimestamp {
    pub modification_time: Option<Timestamp>,
    pub access_time: Option<Timestamp>,
    pub creation_time: Option<Timestamp>,
}

impl ExtendedTimestamp {
    fn parse(mut data: DataParser<'_>) -> Option<Self> {
        let flags = data.read_u8()?;

        // There are ZIP out there that don't respect the spec.
        // If there is only one time, and this doesn't match `flags`,
        // assume that this is modification time
        if data.0.len() == 4 && flags.count_ones() != 1 {
            return Some(ExtendedTimestamp {
                modification_time: Some(Timestamp::from_unix(data.read_u32()? as _)),
                access_time: None,
                creation_time: None,
            });
        }

        let modification_time = if flags & 1 != 0 {
            Some(Timestamp::from_unix(data.read_u32()? as _))
        } else {
            None
        };

        let access_time = if flags & 2 != 0 {
            Some(Timestamp::from_unix(data.read_u32()? as _))
        } else {
            None
        };

        let creation_time = if flags & 4 != 0 {
            Some(Timestamp::from_unix(data.read_u32()? as _))
        } else {
            None
        };

        data.end()?;

        Some(ExtendedTimestamp {
            modification_time,
            access_time,
            creation_time,
        })
    }
}

#[derive(Debug, Clone)]
pub struct UnicodeComment<'a> {
    pub version: u8,
    pub header_comment_crc32: u32,
    pub comment: &'a str,
}

impl<'a> UnicodeComment<'a> {
    fn parse(mut data: DataParser<'a>) -> Option<Self> {
        let version = data.read_u8()?;
        if version != 1 {
            return None;
        }

        let header_comment_crc32 = data.read_u32()?;

        let comment = std::str::from_utf8(data.0).ok()?;

        Some(UnicodeComment {
            version,
            header_comment_crc32,
            comment,
        })
    }
}

#[derive(Debug, Clone)]
pub struct UnicodeName<'a> {
    pub version: u8,
    pub header_name_crc32: u32,
    pub name: &'a str,
}

impl<'a> UnicodeName<'a> {
    fn parse(mut data: DataParser<'a>) -> Option<Self> {
        let version = data.read_u8()?;
        if version != 1 {
            return None;
        }

        let header_name_crc32 = data.read_u32()?;

        let name = std::str::from_utf8(data.0).ok()?;

        Some(UnicodeName {
            version,
            header_name_crc32,
            name,
        })
    }
}

#[derive(Debug, Clone)]
pub struct UnixNew<'a> {
    pub version: u8,
    pub uid: &'a [u8],
    pub gid: &'a [u8],
}

impl<'a> UnixNew<'a> {
    fn parse(mut data: DataParser<'a>) -> Option<Self> {
        let version = data.read_u8()?;

        let uid_size = data.read_u8()?;
        let uid = data.read_variable(uid_size as _)?;

        let gid_size = data.read_u8()?;
        let gid = data.read_variable(gid_size as _)?;

        data.end()?;

        Some(UnixNew { version, uid, gid })
    }
}

#[derive(Debug, Clone)]
pub struct Aes {
    pub check_crc32: bool,
    pub key_size: u16,
    pub compression: crate::CompressionMethod,
}

impl Aes {
    /// Reference: https://www.winzip.com/en/support/aes-encryption/
    fn parse(mut data: DataParser<'_>) -> Option<Self> {
        let check_crc32 = match data.read_u16()? {
            1 => true,
            2 => false,
            _ => return None,
        };

        let vendor_id = data.read_u16()?;
        if vendor_id != u16::from_ne_bytes(*b"AE") {
            return None;
        }

        let mode = match data.read_u8()? {
            1 => 128,
            2 => 192,
            3 => 256,
            _ => return None,
        };

        let compression = crate::CompressionMethod(data.read_u16()?);

        data.end()?;

        Some(Aes {
            check_crc32,
            key_size: mode,
            compression,
        })
    }
}