use crate::closed_enum::closed_enum;
macro_rules! index_newtype {
($(#[$meta:meta])* $name:ident) => {
$(#[$meta])*
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct $name(usize);
impl $name {
#[must_use]
pub const fn new(index: usize) -> Self {
Self(index)
}
#[must_use]
pub const fn get(self) -> usize {
self.0
}
}
impl ::std::fmt::Display for $name {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
::std::fmt::Display::fmt(&self.0, f)
}
}
};
}
index_newtype! {
ZipEntryIndex
}
index_newtype! {
CentralRecordOrdinal
}
closed_enum! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ZipHeaderField {
FileName => "file name",
UnicodePath => "unicode path",
CompressionMethod => "compression method",
EncryptionFlag => "encryption flag",
DataDescriptorFlag => "data descriptor flag",
ModifiedTime => "modification time",
Crc32 => "crc32",
CompressedSize => "compressed size",
UncompressedSize => "uncompressed size",
}
}
closed_enum! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ZipUncomparable {
Unrecognized => "unrecognized field",
Unparseable => "unparseable local header",
}
}
closed_enum! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum HiddenRegion {
Prefix => "before the archive start",
Gap => "between entries",
DescriptorTail => "inside the range of a data-descriptor entry",
}
}
closed_enum! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum EndRecordKind {
Classic => "end of central directory record",
Zip64 => "ZIP64 end of central directory record",
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ZipInconsistency {
HeaderMismatch {
entry: ZipEntryIndex,
field: ZipHeaderField,
name: String,
},
DuplicateName {
record: CentralRecordOrdinal,
name: String,
},
OverlappingEntries,
HiddenLocalHeader {
offset: u64,
region: HiddenRegion,
},
IgnoredEndRecord {
offset: u64,
},
StrayCentralRecord {
offset: u64,
},
StrayEndRecord {
offset: u64,
kind: EndRecordKind,
},
DescriptorOverrun {
entry: ZipEntryIndex,
offset: u64,
},
XlTypeDivergence {
entry: ZipEntryIndex,
offset: u64,
},
UncountedCentralRecords {
counted: usize,
found: usize,
},
HeaderUncomparable {
entry: ZipEntryIndex,
name: String,
reason: ZipUncomparable,
},
}
impl std::fmt::Display for ZipInconsistency {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::HeaderMismatch { entry, field, name } => write!(
f,
"local header disagrees with central directory on {field} for entry {entry} \
({name:?})"
),
Self::HeaderUncomparable {
entry,
name,
reason,
} => match reason {
ZipUncomparable::Unparseable => write!(
f,
"local header of entry {entry} ({name:?}) cannot be parsed, so it cannot be \
compared with the central directory"
),
ZipUncomparable::Unrecognized => write!(
f,
"local header of entry {entry} ({name:?}) fails the header consistency check \
in a way this version does not recognize"
),
},
Self::DuplicateName { record, name } => {
write!(f, "duplicate entry name {name:?} (record {record})")
}
Self::OverlappingEntries => f.write_str("entry data ranges overlap"),
Self::HiddenLocalHeader { offset, region } => write!(
f,
"local header at offset {offset} ({region}) is not in the central directory"
),
Self::IgnoredEndRecord { offset } => write!(
f,
"end of central directory record at offset {offset} is ignored by the reader"
),
Self::StrayCentralRecord { offset } => write!(
f,
"central directory record outside the central directory at offset {offset}"
),
Self::StrayEndRecord { offset, kind } => {
write!(f, "{kind} before an entry, at offset {offset}")
}
Self::DescriptorOverrun { entry, offset } => write!(
f,
"data descriptor of entry {entry} at offset {offset} makes a streaming reader \
resume past the next entry"
),
Self::XlTypeDivergence { entry, offset } => write!(
f,
"an xl extra field of entry {entry} (local header at offset {offset}) gives it a \
file type that the central directory attributes do not"
),
Self::UncountedCentralRecords { counted, found } => write!(
f,
"central directory holds {found} records but the end record counts {counted}"
),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn mismatch(field: ZipHeaderField) -> String {
ZipInconsistency::HeaderMismatch {
entry: ZipEntryIndex::new(2),
field,
name: "a.txt".into(),
}
.to_string()
}
#[test]
fn mismatch_message_names_the_field() {
assert_eq!(
mismatch(ZipHeaderField::Crc32),
"local header disagrees with central directory on crc32 for entry 2 (\"a.txt\")"
);
}
#[test]
fn unparseable_and_unrecognized_get_their_own_wording() {
let uncomparable = |reason| {
ZipInconsistency::HeaderUncomparable {
entry: ZipEntryIndex::new(2),
name: "a.txt".into(),
reason,
}
.to_string()
};
assert_eq!(
uncomparable(ZipUncomparable::Unparseable),
"local header of entry 2 (\"a.txt\") cannot be parsed, so it cannot be compared \
with the central directory"
);
assert_eq!(
uncomparable(ZipUncomparable::Unrecognized),
"local header of entry 2 (\"a.txt\") fails the header consistency check in a way \
this version does not recognize"
);
}
#[test]
fn findings_render_their_positions() {
assert_eq!(
ZipInconsistency::HiddenLocalHeader {
offset: 45,
region: HiddenRegion::Gap
}
.to_string(),
"local header at offset 45 (between entries) is not in the central directory"
);
assert_eq!(
ZipInconsistency::IgnoredEndRecord { offset: 7 }.to_string(),
"end of central directory record at offset 7 is ignored by the reader"
);
assert_eq!(
ZipInconsistency::StrayCentralRecord { offset: 9 }.to_string(),
"central directory record outside the central directory at offset 9"
);
assert_eq!(
ZipInconsistency::StrayEndRecord {
offset: 11,
kind: EndRecordKind::Zip64
}
.to_string(),
"ZIP64 end of central directory record before an entry, at offset 11"
);
assert_eq!(
ZipInconsistency::DescriptorOverrun {
entry: ZipEntryIndex::new(2),
offset: 70
}
.to_string(),
"data descriptor of entry 2 at offset 70 makes a streaming reader resume past the \
next entry"
);
assert_eq!(
ZipInconsistency::XlTypeDivergence {
entry: ZipEntryIndex::new(2),
offset: 70
}
.to_string(),
"an xl extra field of entry 2 (local header at offset 70) gives it a file type \
that the central directory attributes do not"
);
assert_eq!(
ZipInconsistency::UncountedCentralRecords {
counted: 1,
found: 2
}
.to_string(),
"central directory holds 2 records but the end record counts 1"
);
}
}