Skip to main content

typst_pack/
pack_archive.rs

1//! Versioned Pack Archive encoding, decoding, read, and write.
2
3mod read;
4mod write;
5
6pub use crate::CommitCertainty;
7pub use read::*;
8pub use write::*;
9
10use std::collections::{BTreeMap, BTreeSet, btree_map::Entry};
11use std::io::{Cursor, Read, Seek, SeekFrom, Write};
12use std::str::FromStr;
13
14use typst::syntax::package::PackageSpec;
15use zip::ZipWriter;
16use zip::write::SimpleFileOptions;
17
18use crate::limits::{LimitError, Limits, ResourceKind};
19use crate::manifest::PackManifest;
20pub use crate::manifest::{FORMAT_VERSION, MANIFEST_PATH, PackManifestError as ManifestError};
21use crate::pack::{
22    DeclaredFontContainerIdentity, InvalidPackageSpecInput, PackConstructionInput, PackFontInput,
23    PackFontSourceInput, PackInvariantError, PackageFileInput, PackageRequirementInput,
24    PackageRequirementsInput, ProjectFileInput, font_container_path,
25};
26use crate::paths::{canonical_relative_path, has_windows_drive_prefix};
27use crate::payload::SharedBytes;
28use crate::{CanonicalIdentity, CanonicalIdentityRole, Pack, PackArchiveBytes};
29
30/// A resource bounded during Pack Archive Encoding.
31pub type EncodeResource = ResourceKind<4>;
32
33#[allow(non_upper_case_globals)]
34impl ResourceKind<4> {
35    pub const ArchiveBytes: Self = Self::new(0);
36    pub const Members: Self = Self::new(1);
37    pub const GeneratedMemberNameBytes: Self = Self::new(2);
38    pub const ManifestBytes: Self = Self::new(3);
39    pub const MemberBytes: Self = Self::new(4);
40    pub const TotalContentBytes: Self = Self::new(5);
41}
42
43/// A Pack Archive exceeded a mandatory encode ceiling.
44pub type EncodeLimitError = LimitError<EncodeResource>;
45
46/// A valid Pack value that version 1 cannot represent.
47#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
48#[non_exhaustive]
49pub enum RepresentationError {
50    #[error(
51        "version 1 member name {member_name:?} is {observed} bytes, exceeding the {maximum}-byte ZIP limit"
52    )]
53    MemberNameTooLong {
54        member_name: String,
55        maximum: u64,
56        observed: u64,
57    },
58}
59
60/// Mandatory finite resource ceilings for Pack Archive Encoding.
61pub type EncodeLimits = Limits<EncodeResource>;
62
63impl Limits<EncodeResource> {
64    /// Constructs a validated set of mandatory finite encode ceilings.
65    #[track_caller]
66    pub fn new(
67        archive_bytes: u64,
68        members: u64,
69        generated_member_name_bytes: u64,
70        manifest_bytes: u64,
71        member_bytes: u64,
72        total_content_bytes: u64,
73    ) -> Self {
74        Self::from_ceilings([
75            archive_bytes,
76            members,
77            generated_member_name_bytes,
78            manifest_bytes,
79            member_bytes,
80            total_content_bytes,
81            0,
82        ])
83        .assert_probe_resources([
84            EncodeResource::ArchiveBytes,
85            EncodeResource::Members,
86            EncodeResource::GeneratedMemberNameBytes,
87            EncodeResource::ManifestBytes,
88            EncodeResource::MemberBytes,
89            EncodeResource::TotalContentBytes,
90        ])
91    }
92
93    /// The first-party limits for version-1 Pack Archives.
94    pub const fn reference_v1() -> Self {
95        Self::from_ceilings([
96            512 * 1024 * 1024,
97            100_000,
98            16 * 1024 * 1024,
99            4 * 1024 * 1024,
100            256 * 1024 * 1024,
101            2 * 1024 * 1024 * 1024,
102            0,
103        ])
104    }
105
106    pub const fn archive_bytes(&self) -> u64 {
107        self.ceilings[0]
108    }
109
110    pub const fn members(&self) -> u64 {
111        self.ceilings[1]
112    }
113
114    pub const fn generated_member_name_bytes(&self) -> u64 {
115        self.ceilings[2]
116    }
117
118    pub const fn manifest_bytes(&self) -> u64 {
119        self.ceilings[3]
120    }
121
122    pub const fn member_bytes(&self) -> u64 {
123        self.ceilings[4]
124    }
125
126    pub const fn total_content_bytes(&self) -> u64 {
127        self.ceilings[5]
128    }
129}
130
131/// A failure in one phase of Pack Archive Encoding.
132#[derive(Debug, thiserror::Error)]
133#[non_exhaustive]
134pub enum EncodeError {
135    #[error(transparent)]
136    Limit(#[from] EncodeLimitError),
137    #[error(transparent)]
138    Representation(#[from] RepresentationError),
139    #[error("failed to encode ZIP structure: {0}")]
140    Codec(#[source] zip::result::ZipError),
141}
142
143impl From<zip::result::ZipError> for EncodeError {
144    fn from(error: zip::result::ZipError) -> Self {
145        Self::Codec(error)
146    }
147}
148
149impl From<std::io::Error> for EncodeError {
150    fn from(error: std::io::Error) -> Self {
151        Self::Codec(zip::result::ZipError::Io(error))
152    }
153}
154
155/// Encodes one borrowed validated [`Pack`] into uniquely owned exact archive bytes.
156pub fn encode(pack: &Pack) -> Result<PackArchiveBytes, EncodeError> {
157    encode_with_limits(pack, EncodeLimits::reference_v1())
158}
159
160/// Encodes one borrowed validated [`Pack`] under explicit resource ceilings.
161pub fn encode_with_limits(
162    pack: &Pack,
163    limits: EncodeLimits,
164) -> Result<PackArchiveBytes, EncodeError> {
165    let mut members = 1;
166    check_encode_exceeded(EncodeResource::Members, limits.members(), members)?;
167    for _ in pack.files() {
168        account_member(&mut members, limits.members())?;
169    }
170    for (_, files) in pack.packages() {
171        for _ in files {
172            account_member(&mut members, limits.members())?;
173        }
174    }
175    let mut font_members = BTreeMap::new();
176    for font in pack.fonts() {
177        if let Entry::Vacant(entry) = font_members.entry(font.identity().container()) {
178            account_member(&mut members, limits.members())?;
179            entry.insert(font.data());
180        }
181    }
182
183    let mut generated_name_bytes =
184        u64::try_from(MANIFEST_PATH.len()).map_err(|_| EncodeLimitError::AccountingOverflow {
185            resource: EncodeResource::GeneratedMemberNameBytes,
186        })?;
187    check_encode_exceeded(
188        EncodeResource::GeneratedMemberNameBytes,
189        limits.generated_member_name_bytes(),
190        generated_name_bytes,
191    )?;
192    for (path, _) in pack.files() {
193        let observed = generated_name_length([PROJECT_PREFIX.len(), path.len()])?;
194        check_v1_member_name(observed, || format!("{PROJECT_PREFIX}{path}"))?;
195        add_generated_name_bytes(
196            &mut generated_name_bytes,
197            [PROJECT_PREFIX.len(), path.len()],
198        )?;
199        check_encode_exceeded(
200            EncodeResource::GeneratedMemberNameBytes,
201            limits.generated_member_name_bytes(),
202            generated_name_bytes,
203        )?;
204    }
205    for (spec, files) in pack.packages() {
206        let version = spec.version.to_string();
207        for (path, _) in files {
208            let parts = [
209                PACKAGES_PREFIX.len(),
210                spec.namespace.len(),
211                1,
212                spec.name.len(),
213                1,
214                version.len(),
215                1,
216                path.len(),
217            ];
218            let observed = generated_name_length(parts)?;
219            check_v1_member_name(observed, || {
220                format!(
221                    "{PACKAGES_PREFIX}{}/{}/{}/{path}",
222                    spec.namespace, spec.name, spec.version
223                )
224            })?;
225            add_generated_name_bytes(&mut generated_name_bytes, parts)?;
226            check_encode_exceeded(
227                EncodeResource::GeneratedMemberNameBytes,
228                limits.generated_member_name_bytes(),
229                generated_name_bytes,
230            )?;
231        }
232    }
233    for (identity, data) in &font_members {
234        let path = font_container_path(*identity, Some(data));
235        add_generated_name_bytes(&mut generated_name_bytes, [path.len()])?;
236        check_encode_exceeded(
237            EncodeResource::GeneratedMemberNameBytes,
238            limits.generated_member_name_bytes(),
239            generated_name_bytes,
240        )?;
241    }
242
243    let mut total_content_bytes = 0;
244    for (_, data) in pack.files() {
245        account_content(data, limits, &mut total_content_bytes)?;
246    }
247    for (_, files) in pack.packages() {
248        for (_, data) in files {
249            account_content(data, limits, &mut total_content_bytes)?;
250        }
251    }
252    for data in font_members.values() {
253        account_content(data, limits, &mut total_content_bytes)?;
254    }
255
256    let manifest = encode_manifest(pack, limits.manifest_bytes())?;
257
258    let mut output = BoundedArchiveWriter::new(limits.archive_bytes());
259    let result = (|| -> Result<(), EncodeError> {
260        let mut zip = ZipWriter::new(&mut output);
261        zip.start_file(MANIFEST_PATH, zip_file_options(manifest.len()))?;
262        zip.write_all(manifest.as_bytes())?;
263
264        for (path, data) in pack.files() {
265            zip.start_file(
266                format!("{PROJECT_PREFIX}{path}"),
267                zip_file_options(data.len()),
268            )?;
269            zip.write_all(data)?;
270        }
271
272        for (spec, files) in pack.packages() {
273            for (path, data) in files {
274                zip.start_file(
275                    format!(
276                        "{PACKAGES_PREFIX}{}/{}/{}/{path}",
277                        spec.namespace, spec.name, spec.version
278                    ),
279                    zip_file_options(data.len()),
280                )?;
281                zip.write_all(data)?;
282            }
283        }
284
285        for (identity, data) in &font_members {
286            let path = font_container_path(*identity, Some(data));
287            zip.start_file(path, zip_file_options(data.len()))?;
288            zip.write_all(data)?;
289        }
290
291        zip.finish()?;
292        Ok(())
293    })();
294    if let Some(error) = output.limit_error {
295        return Err(error.into());
296    }
297    result?;
298    Ok(PackArchiveBytes::from_vec(output.bytes))
299}
300
301fn account_member(total: &mut u64, ceiling: u64) -> Result<(), EncodeLimitError> {
302    *total = total
303        .checked_add(1)
304        .ok_or(EncodeLimitError::AccountingOverflow {
305            resource: EncodeResource::Members,
306        })?;
307    check_encode_exceeded(EncodeResource::Members, ceiling, *total)
308}
309
310fn encode_manifest(pack: &Pack, ceiling: u64) -> Result<String, EncodeLimitError> {
311    let mut manifest = BoundedManifest::new(ceiling);
312    manifest.push("format-version = 1\n\n[project]\nentrypoint = ")?;
313    manifest.push_quoted(pack.entrypoint())?;
314    manifest.push("\n")?;
315
316    for embedded in [true, false] {
317        for requirement in pack
318            .package_requirements()
319            .iter()
320            .filter(|requirement| requirement.is_embedded() == embedded)
321        {
322            manifest.push(if embedded {
323                "\n[[packages.vendored]]\n"
324            } else {
325                "\n[[packages.unvendored]]\n"
326            })?;
327            manifest.push("spec = ")?;
328            let spec = requirement.spec();
329            manifest.push("\"@")?;
330            manifest.push_escaped(spec.namespace.as_str())?;
331            manifest.push("/")?;
332            manifest.push_escaped(spec.name.as_str())?;
333            manifest.push(":")?;
334            manifest.push_escaped(&spec.version.to_string())?;
335            manifest.push("\"")?;
336            manifest.push("\ntree-digest = ")?;
337            manifest.push_quoted(&requirement.tree_identity().encode())?;
338            manifest.push("\ntree-identity-kind = ")?;
339            manifest.push_quoted(requirement.tree_identity().role().as_str())?;
340            manifest.push("\ntree-identity-schema = ")?;
341            manifest.push_quoted(requirement.tree_identity().schema())?;
342            manifest.push("\ntree-identity-algorithm = ")?;
343            manifest.push_quoted(requirement.tree_identity().algorithm())?;
344            manifest.push("\nfile-count = ")?;
345            manifest.push_u64(requirement.file_count())?;
346            manifest.push("\nbyte-length = ")?;
347            manifest.push_u64(requirement.byte_length())?;
348            manifest.push("\n")?;
349        }
350    }
351
352    for face in pack.font_catalog() {
353        let embedded = pack
354            .fonts()
355            .iter()
356            .find(|font| font.identity() == face.identity());
357        let requirement = pack
358            .font_requirements()
359            .iter()
360            .find(|requirement| requirement.container_identity() == face.identity().container())
361            .expect("Pack Font Catalog requirement invariant violated");
362        manifest.push("\n[[fonts]]\npath = ")?;
363        manifest.push_quoted(&font_container_path(
364            face.identity().container(),
365            embedded.map(|font| font.data()),
366        ))?;
367        if face.identity().index() != 0 {
368            manifest.push("\nindex = ")?;
369            manifest.push_u64(u64::from(face.identity().index()))?;
370        }
371        if let Some(font) = embedded {
372            manifest.push("\nfamilies = [")?;
373            manifest.push_quoted(font.info().family.as_str())?;
374            manifest.push("]")?;
375        }
376        if !face.is_embedded() {
377            manifest.push("\nexternal = true")?;
378        }
379        let container = face.identity().container();
380        manifest.push("\ncontainer-digest = ")?;
381        manifest.push_quoted(&container.encode())?;
382        manifest.push("\ncontainer-identity-kind = ")?;
383        manifest.push_quoted(container.role().as_str())?;
384        manifest.push("\ncontainer-identity-schema = ")?;
385        manifest.push_quoted(container.schema())?;
386        manifest.push("\ncontainer-identity-algorithm = ")?;
387        manifest.push_quoted(container.algorithm())?;
388        manifest.push("\ncontainer-length = ")?;
389        manifest.push_u64(requirement.container_length())?;
390        manifest.push("\n")?;
391    }
392
393    if let Some(metadata) = pack.metadata() {
394        manifest.push("\n[metadata]\n")?;
395        if let Some(name) = metadata.name() {
396            manifest.push("name = ")?;
397            manifest.push_quoted(name)?;
398            manifest.push("\n")?;
399        }
400        if let Some(description) = metadata.description() {
401            manifest.push("description = ")?;
402            manifest.push_quoted(description)?;
403            manifest.push("\n")?;
404        }
405        if !metadata.authors().is_empty() {
406            manifest.push("authors = [")?;
407            for (index, author) in metadata.authors().iter().enumerate() {
408                if index != 0 {
409                    manifest.push(", ")?;
410                }
411                manifest.push_quoted(author)?;
412            }
413            manifest.push("]\n")?;
414        }
415    }
416
417    Ok(manifest.output)
418}
419
420struct BoundedManifest {
421    output: String,
422    ceiling: u64,
423}
424
425impl BoundedManifest {
426    fn new(ceiling: u64) -> Self {
427        Self {
428            output: String::new(),
429            ceiling,
430        }
431    }
432
433    fn push(&mut self, value: &str) -> Result<(), EncodeLimitError> {
434        let bytes =
435            u64::try_from(value.len()).map_err(|_| EncodeLimitError::AccountingOverflow {
436                resource: EncodeResource::ManifestBytes,
437            })?;
438        let observed = u64::try_from(self.output.len())
439            .ok()
440            .and_then(|length| length.checked_add(bytes))
441            .ok_or(EncodeLimitError::AccountingOverflow {
442                resource: EncodeResource::ManifestBytes,
443            })?;
444        check_encode_exceeded(EncodeResource::ManifestBytes, self.ceiling, observed)?;
445        self.output.push_str(value);
446        Ok(())
447    }
448
449    fn push_u64(&mut self, value: u64) -> Result<(), EncodeLimitError> {
450        self.push(&value.to_string())
451    }
452
453    fn push_quoted(&mut self, value: &str) -> Result<(), EncodeLimitError> {
454        self.push("\"")?;
455        self.push_escaped(value)?;
456        self.push("\"")
457    }
458
459    fn push_escaped(&mut self, value: &str) -> Result<(), EncodeLimitError> {
460        let minimum_bytes =
461            u64::try_from(value.len()).map_err(|_| EncodeLimitError::AccountingOverflow {
462                resource: EncodeResource::ManifestBytes,
463            })?;
464        let observed_at_least = u64::try_from(self.output.len())
465            .ok()
466            .and_then(|length| length.checked_add(minimum_bytes))
467            .ok_or(EncodeLimitError::AccountingOverflow {
468                resource: EncodeResource::ManifestBytes,
469            })?;
470        check_encode_exceeded(
471            EncodeResource::ManifestBytes,
472            self.ceiling,
473            observed_at_least,
474        )?;
475        let mut unescaped_start = 0;
476        for (index, character) in value.char_indices() {
477            let escaped = match character {
478                '\u{08}' => Some("\\b"),
479                '\t' => Some("\\t"),
480                '\n' => Some("\\n"),
481                '\u{0c}' => Some("\\f"),
482                '\r' => Some("\\r"),
483                '"' => Some("\\\""),
484                '\\' => Some("\\\\"),
485                character if character.is_control() => {
486                    self.push(&value[unescaped_start..index])?;
487                    let escaped = format!("\\u{:04X}", u32::from(character));
488                    self.push(&escaped)?;
489                    unescaped_start = index + character.len_utf8();
490                    None
491                }
492                _ => None,
493            };
494            if let Some(escaped) = escaped {
495                self.push(&value[unescaped_start..index])?;
496                self.push(escaped)?;
497                unescaped_start = index + character.len_utf8();
498            }
499        }
500        self.push(&value[unescaped_start..])
501    }
502}
503
504fn zip_file_options(size: usize) -> SimpleFileOptions {
505    // Deflate may expand incompressible input. Nine bits per input byte plus
506    // framing is a conservative bound for the configured encoder.
507    let compressed_bound = size.saturating_add(size.div_ceil(8)).saturating_add(16);
508    let compressed_bound = u64::try_from(compressed_bound).unwrap_or(u64::MAX);
509    SimpleFileOptions::default()
510        .compression_method(zip::CompressionMethod::Deflated)
511        .large_file(compressed_bound > zip::ZIP64_BYTES_THR)
512}
513
514struct BoundedArchiveWriter {
515    bytes: Vec<u8>,
516    position: u64,
517    logical_len: u64,
518    ceiling: u64,
519    limit_error: Option<EncodeLimitError>,
520}
521
522impl BoundedArchiveWriter {
523    fn new(ceiling: u64) -> Self {
524        Self {
525            bytes: Vec::new(),
526            position: 0,
527            logical_len: 0,
528            ceiling,
529            limit_error: None,
530        }
531    }
532}
533
534impl Write for BoundedArchiveWriter {
535    fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
536        let bytes = u64::try_from(data.len()).map_err(|_| {
537            self.limit_error = Some(EncodeLimitError::AccountingOverflow {
538                resource: EncodeResource::ArchiveBytes,
539            });
540            std::io::Error::other("Pack Archive encode accounting overflowed")
541        })?;
542        let end = self.position.checked_add(bytes).ok_or_else(|| {
543            self.limit_error = Some(EncodeLimitError::AccountingOverflow {
544                resource: EncodeResource::ArchiveBytes,
545            });
546            std::io::Error::other("Pack Archive encode accounting overflowed")
547        })?;
548        self.logical_len = self.logical_len.max(end);
549        if self.limit_error.is_none() && self.logical_len > self.ceiling {
550            self.limit_error = Some(EncodeLimitError::exceeded(
551                EncodeResource::ArchiveBytes,
552                self.ceiling,
553            ));
554            self.position = end;
555            return Err(std::io::Error::other("Pack Archive encode limit exceeded"));
556        }
557        if self.limit_error.is_none() {
558            let start = usize::try_from(self.position).map_err(|_| {
559                std::io::Error::other("Pack Archive encode position is not addressable")
560            })?;
561            let end = usize::try_from(end).map_err(|_| {
562                std::io::Error::other("Pack Archive encode position is not addressable")
563            })?;
564            if self.bytes.len() < end {
565                self.bytes.resize(end, 0);
566            }
567            self.bytes[start..end].copy_from_slice(data);
568        }
569        self.position = end;
570        Ok(data.len())
571    }
572
573    fn flush(&mut self) -> std::io::Result<()> {
574        Ok(())
575    }
576}
577
578impl Seek for BoundedArchiveWriter {
579    fn seek(&mut self, position: SeekFrom) -> std::io::Result<u64> {
580        let position = match position {
581            SeekFrom::Start(position) => i128::from(position),
582            SeekFrom::End(offset) => i128::from(self.logical_len) + i128::from(offset),
583            SeekFrom::Current(offset) => i128::from(self.position) + i128::from(offset),
584        };
585        self.position = u64::try_from(position)
586            .map_err(|_| std::io::Error::other("invalid Pack Archive encode seek"))?;
587        Ok(self.position)
588    }
589}
590
591fn check_v1_member_name(
592    observed: u64,
593    member_name: impl FnOnce() -> String,
594) -> Result<(), RepresentationError> {
595    const MAXIMUM: u64 = u16::MAX as u64;
596    if observed > MAXIMUM {
597        return Err(RepresentationError::MemberNameTooLong {
598            member_name: member_name(),
599            maximum: MAXIMUM,
600            observed,
601        });
602    }
603    Ok(())
604}
605
606fn generated_name_length<const N: usize>(parts: [usize; N]) -> Result<u64, EncodeLimitError> {
607    parts.into_iter().try_fold(0u64, |length, part| {
608        let part = u64::try_from(part).map_err(|_| EncodeLimitError::AccountingOverflow {
609            resource: EncodeResource::GeneratedMemberNameBytes,
610        })?;
611        length
612            .checked_add(part)
613            .ok_or(EncodeLimitError::AccountingOverflow {
614                resource: EncodeResource::GeneratedMemberNameBytes,
615            })
616    })
617}
618
619fn account_content(
620    data: &[u8],
621    limits: EncodeLimits,
622    total: &mut u64,
623) -> Result<(), EncodeLimitError> {
624    let bytes = u64::try_from(data.len()).map_err(|_| EncodeLimitError::AccountingOverflow {
625        resource: EncodeResource::MemberBytes,
626    })?;
627    check_encode_exceeded(EncodeResource::MemberBytes, limits.member_bytes(), bytes)?;
628    *total = total
629        .checked_add(bytes)
630        .ok_or(EncodeLimitError::AccountingOverflow {
631            resource: EncodeResource::TotalContentBytes,
632        })?;
633    check_encode_exceeded(
634        EncodeResource::TotalContentBytes,
635        limits.total_content_bytes(),
636        *total,
637    )
638}
639
640fn add_generated_name_bytes<const N: usize>(
641    total: &mut u64,
642    parts: [usize; N],
643) -> Result<(), EncodeLimitError> {
644    *total = total.checked_add(generated_name_length(parts)?).ok_or(
645        EncodeLimitError::AccountingOverflow {
646            resource: EncodeResource::GeneratedMemberNameBytes,
647        },
648    )?;
649    Ok(())
650}
651
652fn check_encode_exceeded(
653    resource: EncodeResource,
654    ceiling: u64,
655    observed: u64,
656) -> Result<(), EncodeLimitError> {
657    if observed > ceiling {
658        return Err(EncodeLimitError::exceeded(resource, ceiling));
659    }
660    Ok(())
661}
662
663/// A resource bounded during Pack Archive Decoding.
664pub type DecodeResource = ResourceKind<5>;
665
666#[allow(non_upper_case_globals)]
667impl ResourceKind<5> {
668    pub const ArchiveBytes: Self = Self::new(0);
669    pub const Members: Self = Self::new(1);
670    pub const RawMemberNameBytes: Self = Self::new(2);
671    pub const ManifestBytes: Self = Self::new(3);
672    pub const MemberBytes: Self = Self::new(4);
673    pub const TotalContentBytes: Self = Self::new(5);
674}
675
676/// A Pack Archive exceeded a mandatory decode ceiling.
677pub type DecodeLimitError = LimitError<DecodeResource>;
678
679/// A failure in one phase of Pack Archive Decoding.
680#[derive(Debug, thiserror::Error)]
681#[non_exhaustive]
682pub enum DecodeError {
683    #[error(transparent)]
684    Limit(#[from] DecodeLimitError),
685    #[error(transparent)]
686    Archive(#[from] ArchiveError),
687    #[error(transparent)]
688    Manifest(#[from] ManifestError),
689    #[error("decoded declarations and content do not form a valid Pack: {0}")]
690    InvalidPack(#[from] PackInvariantError),
691}
692
693/// A malformed, unsafe, ambiguous, or unsupported raw Pack Archive.
694#[derive(Debug, thiserror::Error)]
695#[non_exhaustive]
696pub enum ArchiveError {
697    #[error("failed to read ZIP structure: {0}")]
698    Zip(#[from] zip::result::ZipError),
699    #[error("failed to read ZIP structure: {0}")]
700    Io(#[from] std::io::Error),
701    #[error("the archive contains no {MANIFEST_PATH} manifest (is this a Typst Pack?)")]
702    MissingManifest,
703    #[error("the archive contains more than one {MANIFEST_PATH} manifest")]
704    DuplicateManifest,
705    #[error("the archive contains a duplicate raw member named {0:?}")]
706    DuplicateMember(Vec<u8>),
707    #[error("the archive contains members with ambiguous effective names")]
708    AmbiguousMemberNames,
709    #[error("the archive contains a malformed UTF-8 member name {0:?}")]
710    InvalidUtf8MemberName(Vec<u8>),
711    #[error("the {MANIFEST_PATH} manifest is not a regular file")]
712    ManifestNotFile,
713    #[error("archive member {member:?} could not be read: {source}")]
714    MemberUnreadable {
715        member: String,
716        #[source]
717        source: std::io::Error,
718    },
719    #[error("archive member {0:?} has an unsafe path")]
720    UnsafeMemberName(String),
721    #[error("package archive member {0:?} does not name a package file")]
722    MalformedPackageMember(String),
723    #[error(
724        "package archive member {member:?} contains invalid specification {spec:?}: {message:?}"
725    )]
726    InvalidPackageSpec {
727        member: String,
728        spec: String,
729        message: String,
730    },
731    #[error("font declaration path {0:?} is not a safe archive member name")]
732    InvalidFontPath(String),
733    #[error("font declaration path {path:?} conflicts with the {role:?} archive role")]
734    FontPathRoleConflict {
735        path: String,
736        role: ReservedMemberRole,
737    },
738    #[error("font declaration path {descendant:?} has file ancestor {ancestor:?}")]
739    FontPathTreeConflict {
740        ancestor: String,
741        descendant: String,
742    },
743    #[error("archive member {0:?} is not a regular file or directory")]
744    UnsupportedMemberKind(String),
745}
746
747/// A reserved version-1 archive role that font data cannot occupy.
748#[derive(Debug, Clone, Copy, Eq, PartialEq)]
749#[non_exhaustive]
750pub enum ReservedMemberRole {
751    Manifest,
752    Project,
753    Package,
754}
755
756impl From<zip::result::ZipError> for DecodeError {
757    fn from(error: zip::result::ZipError) -> Self {
758        Self::Archive(ArchiveError::Zip(error))
759    }
760}
761
762impl From<std::io::Error> for DecodeError {
763    fn from(error: std::io::Error) -> Self {
764        Self::Archive(ArchiveError::Io(error))
765    }
766}
767
768/// Mandatory finite resource ceilings for Pack Archive Decoding.
769pub type DecodeLimits = Limits<DecodeResource>;
770
771impl Limits<DecodeResource> {
772    /// Constructs a validated set of mandatory finite decode ceilings.
773    #[track_caller]
774    pub fn new(
775        archive_bytes: u64,
776        members: u64,
777        raw_member_name_bytes: u64,
778        manifest_bytes: u64,
779        member_bytes: u64,
780        total_content_bytes: u64,
781    ) -> Self {
782        Self::from_ceilings([
783            archive_bytes,
784            members,
785            raw_member_name_bytes,
786            manifest_bytes,
787            member_bytes,
788            total_content_bytes,
789            0,
790        ])
791        .assert_probe_resources([
792            DecodeResource::ArchiveBytes,
793            DecodeResource::Members,
794            DecodeResource::RawMemberNameBytes,
795            DecodeResource::ManifestBytes,
796            DecodeResource::MemberBytes,
797            DecodeResource::TotalContentBytes,
798        ])
799    }
800
801    /// The first-party limits for version-1 Pack Archives.
802    pub const fn reference_v1() -> Self {
803        Self::from_ceilings([
804            512 * 1024 * 1024,
805            100_000,
806            16 * 1024 * 1024,
807            4 * 1024 * 1024,
808            256 * 1024 * 1024,
809            2 * 1024 * 1024 * 1024,
810            0,
811        ])
812    }
813
814    pub const fn archive_bytes(&self) -> u64 {
815        self.ceilings[0]
816    }
817
818    pub const fn members(&self) -> u64 {
819        self.ceilings[1]
820    }
821
822    pub const fn raw_member_name_bytes(&self) -> u64 {
823        self.ceilings[2]
824    }
825
826    pub const fn manifest_bytes(&self) -> u64 {
827        self.ceilings[3]
828    }
829
830    pub const fn member_bytes(&self) -> u64 {
831        self.ceilings[4]
832    }
833
834    pub const fn total_content_bytes(&self) -> u64 {
835        self.ceilings[5]
836    }
837}
838
839/// Decodes one borrowed exact Pack Archive into an authoritative [`Pack`].
840pub fn decode(archive: &PackArchiveBytes, limits: DecodeLimits) -> Result<Pack, DecodeError> {
841    if archive.len() > limits.archive_bytes() {
842        return Err(DecodeLimitError::exceeded(
843            DecodeResource::ArchiveBytes,
844            limits.archive_bytes(),
845        )
846        .into());
847    }
848    let central_directory = locate_central_directory(archive.as_slice())?;
849    let mut reader = Cursor::new(archive.as_slice());
850    let raw_entries =
851        raw_central_entries(&mut reader, archive.as_slice(), central_directory, limits)?;
852    for entry in &raw_entries {
853        if entry.utf8 && std::str::from_utf8(&entry.name).is_err() {
854            return Err(ArchiveError::InvalidUtf8MemberName(entry.name.clone()).into());
855        }
856    }
857    let mut raw_names = BTreeSet::new();
858    for entry in &raw_entries {
859        if !raw_names.insert(entry.name.clone()) {
860            return Err(if entry.name == MANIFEST_PATH.as_bytes() {
861                ArchiveError::DuplicateManifest
862            } else {
863                ArchiveError::DuplicateMember(entry.name.clone())
864            }
865            .into());
866        }
867    }
868    let mut archive = zip::ZipArchive::new(Cursor::new(archive.as_slice()))?;
869    if raw_entries.len() != archive.len() {
870        return Err(ArchiveError::AmbiguousMemberNames.into());
871    }
872    let mut manifest_index = None;
873    let mut project_entries = Vec::new();
874    let mut package_entries = Vec::new();
875    let mut unknown_entries = Vec::new();
876    let mut effective_names = BTreeMap::new();
877    let mut accepted_members = Vec::new();
878
879    for (index, raw_entry) in raw_entries.iter().enumerate() {
880        let entry = archive.by_index_raw(index)?;
881        let archive_name = entry.name().to_owned();
882        let prefix_normalized_name = strip_current_directory_prefix(&archive_name);
883        let canonical_name = canonical_archive_name(&archive_name)?;
884        register_archive_identity(
885            &mut effective_names,
886            canonical_name.clone(),
887            &raw_entry.name,
888        )?;
889
890        let regular_file = is_regular_file(&entry);
891        let directory = is_directory(&entry);
892        let role_name = if prefix_normalized_name == MANIFEST_PATH
893            || prefix_normalized_name.starts_with(PROJECT_PREFIX)
894            || prefix_normalized_name.starts_with(PACKAGES_PREFIX)
895        {
896            prefix_normalized_name
897        } else {
898            canonical_name.as_str()
899        }
900        .to_owned();
901
902        if role_name == MANIFEST_PATH && !regular_file {
903            return Err(ArchiveError::ManifestNotFile.into());
904        }
905        if !regular_file && !directory {
906            return Err(ArchiveError::UnsupportedMemberKind(archive_name).into());
907        }
908        accepted_members.push(AcceptedMember {
909            index,
910            archive_name,
911            role_name,
912            canonical_name,
913            directory,
914        });
915    }
916
917    for member in accepted_members {
918        if member.role_name == MANIFEST_PATH {
919            manifest_index = Some(member.index);
920        } else if member.directory {
921            continue;
922        } else if let Some(path) = member.role_name.strip_prefix(PROJECT_PREFIX) {
923            project_entries.push(ProjectEntry {
924                index: member.index,
925                path: path.trim_start_matches('/').to_owned(),
926            });
927        } else if let Some(rest) = member.role_name.strip_prefix(PACKAGES_PREFIX) {
928            let (spec, path) = split_package_entry(rest, &member.archive_name)?;
929            package_entries.push(PackageEntry {
930                index: member.index,
931                spec,
932                path,
933            });
934        } else {
935            unknown_entries.push(UnknownEntry {
936                index: member.index,
937                canonical_name: member.canonical_name,
938            });
939        }
940    }
941
942    let manifest_index = manifest_index.ok_or(ArchiveError::MissingManifest)?;
943    let manifest_bytes = read_manifest(&mut archive, manifest_index, limits)?;
944    let manifest_text = std::str::from_utf8(&manifest_bytes).map_err(ManifestError::NotUtf8)?;
945    let manifest = PackManifest::from_toml(manifest_text)?;
946
947    let mut font_paths = BTreeSet::new();
948    let mut canonical_font_paths = BTreeMap::new();
949    for font in manifest.fonts() {
950        let path = canonical_archive_name(font.path())
951            .map_err(|_| ArchiveError::InvalidFontPath(font.path().to_owned()))?;
952        if let Some(role) = reserved_font_archive_role(&path) {
953            return Err(ArchiveError::FontPathRoleConflict {
954                path: font.path().to_owned(),
955                role,
956            }
957            .into());
958        }
959        font_paths.insert(path.clone());
960        canonical_font_paths.insert(font.path().to_owned(), path);
961    }
962
963    let font_entries = unknown_entries
964        .into_iter()
965        .filter(|entry| font_paths.contains(&entry.canonical_name))
966        .map(|entry| (entry.index, entry.canonical_name))
967        .collect::<Vec<_>>();
968    let content_indices = project_entries
969        .iter()
970        .map(|entry| entry.index)
971        .chain(package_entries.iter().map(|entry| entry.index))
972        .chain(font_entries.iter().map(|(index, _)| *index))
973        .collect::<Vec<_>>();
974    preflight_content(&mut archive, &content_indices, limits)?;
975
976    let mut total_content_bytes = 0;
977    let mut files = Vec::new();
978    for project in project_entries {
979        files.push(ProjectFileInput {
980            path: project.path,
981            data: SharedBytes::new(read_content(
982                &mut archive,
983                project.index,
984                limits,
985                &mut total_content_bytes,
986            )?),
987        });
988    }
989    let mut package_files = Vec::new();
990    for package in package_entries {
991        package_files.push(PackageFileInput {
992            spec: package.spec,
993            path: package.path,
994            data: SharedBytes::new(read_content(
995                &mut archive,
996                package.index,
997                limits,
998                &mut total_content_bytes,
999            )?),
1000            embedded: true,
1001        });
1002    }
1003    let mut fonts_by_path = BTreeMap::new();
1004    for (index, path) in font_entries {
1005        fonts_by_path.insert(
1006            path,
1007            SharedBytes::new(read_content(
1008                &mut archive,
1009                index,
1010                limits,
1011                &mut total_content_bytes,
1012            )?),
1013        );
1014    }
1015
1016    let package_requirements = manifest
1017        .packages()
1018        .vendored()
1019        .iter()
1020        .cloned()
1021        .map(|entry| package_requirement_input(entry, true))
1022        .chain(
1023            manifest
1024                .packages()
1025                .unvendored()
1026                .iter()
1027                .cloned()
1028                .map(|entry| package_requirement_input(entry, false)),
1029        )
1030        .collect();
1031    let fonts = manifest
1032        .fonts()
1033        .iter()
1034        .map(|entry| {
1035            let canonical = canonical_font_paths.get(entry.path());
1036            PackFontInput {
1037                source: PackFontSourceInput::Declared {
1038                    label: entry.path().to_owned(),
1039                    identity: declared_font_container_identity(entry),
1040                    length: entry.container_length(),
1041                    data: canonical.and_then(|path| fonts_by_path.get(path).cloned()),
1042                },
1043                index: entry.index(),
1044                embedded: !entry.is_external(),
1045            }
1046        })
1047        .collect();
1048
1049    Pack::construct(PackConstructionInput {
1050        entrypoint: manifest.project().entrypoint().to_owned(),
1051        metadata: manifest.metadata().cloned(),
1052        files,
1053        package_files,
1054        package_requirements: PackageRequirementsInput::Declared(package_requirements),
1055        fonts,
1056    })
1057    .map_err(DecodeError::InvalidPack)
1058}
1059
1060fn package_requirement_input(
1061    entry: crate::manifest::PackageManifest,
1062    embedded: bool,
1063) -> PackageRequirementInput {
1064    let spec = entry.spec().map_err(|error| InvalidPackageSpecInput {
1065        spec: error.spec,
1066        message: error.message,
1067    });
1068    let role = CanonicalIdentityRole::PackageTree;
1069    let tree = (entry.tree_identity_kind() == role.as_str()
1070        && entry.tree_identity_schema() == role.schema()
1071        && entry.tree_identity_algorithm() == "typst-hash128-0.15")
1072        .then(|| CanonicalIdentity::decode(role, entry.tree_digest()))
1073        .flatten();
1074    PackageRequirementInput {
1075        spec,
1076        tree,
1077        file_count: entry.file_count(),
1078        byte_length: entry.byte_length(),
1079        embedded,
1080    }
1081}
1082
1083fn declared_font_container_identity(
1084    entry: &crate::manifest::FontManifest,
1085) -> DeclaredFontContainerIdentity {
1086    let components = (
1087        entry.container_digest(),
1088        entry.container_identity_kind(),
1089        entry.container_identity_schema(),
1090        entry.container_identity_algorithm(),
1091    );
1092    if matches!(components, (None, None, None, None)) {
1093        return DeclaredFontContainerIdentity::Absent;
1094    }
1095    let digest = match components
1096        .0
1097        .map(|value| CanonicalIdentity::decode(CanonicalIdentityRole::FontContainer, value))
1098    {
1099        Some(Some(identity)) => Some(identity),
1100        Some(None) => return DeclaredFontContainerIdentity::Invalid,
1101        None => None,
1102    };
1103    let role = CanonicalIdentityRole::FontContainer;
1104    if components.1.is_some_and(|kind| kind != role.as_str())
1105        || components.2.is_some_and(|schema| schema != role.schema())
1106        || components
1107            .3
1108            .is_some_and(|algorithm| algorithm != "typst-hash128-0.15")
1109    {
1110        return DeclaredFontContainerIdentity::Invalid;
1111    }
1112    if components.0.is_some()
1113        && components.1.is_some()
1114        && components.2.is_some()
1115        && components.3.is_some()
1116    {
1117        DeclaredFontContainerIdentity::Valid(
1118            digest.expect("a complete valid declaration has a parsed digest"),
1119        )
1120    } else {
1121        DeclaredFontContainerIdentity::Partial(digest)
1122    }
1123}
1124
1125#[cfg(test)]
1126mod semantic_input_tests {
1127    use super::*;
1128
1129    #[test]
1130    fn partial_embedded_font_identity_fields_remain_independently_validated() {
1131        let identity = crate::pack::font_container_identity(b"font bytes");
1132        let digest = identity
1133            .digest()
1134            .iter()
1135            .map(|byte| format!("{byte:02x}"))
1136            .collect();
1137
1138        let digest_only =
1139            crate::manifest::FontManifest::with_identity_fields(Some(digest), None, None, None);
1140        assert!(matches!(
1141            declared_font_container_identity(&digest_only),
1142            DeclaredFontContainerIdentity::Partial(Some(actual)) if actual == identity
1143        ));
1144
1145        let kind_only = crate::manifest::FontManifest::with_identity_fields(
1146            None,
1147            Some("font-container".to_owned()),
1148            None,
1149            None,
1150        );
1151        assert!(matches!(
1152            declared_font_container_identity(&kind_only),
1153            DeclaredFontContainerIdentity::Partial(None)
1154        ));
1155    }
1156}
1157
1158#[derive(Clone, Copy)]
1159struct CentralDirectory {
1160    start: u64,
1161    archive_offset: u64,
1162}
1163
1164fn locate_central_directory(bytes: &[u8]) -> Result<CentralDirectory, ArchiveError> {
1165    const EOCD_LEN: usize = 22;
1166    const MAX_COMMENT_LEN: usize = u16::MAX as usize;
1167    let search_start = bytes.len().saturating_sub(EOCD_LEN + MAX_COMMENT_LEN);
1168    let eocd = bytes[search_start..]
1169        .windows(4)
1170        .enumerate()
1171        .rev()
1172        .find_map(|(relative, signature)| {
1173            if signature != b"PK\x05\x06" {
1174                return None;
1175            }
1176            let position = search_start + relative;
1177            let comment_length = read_u16(bytes, position + 20)? as usize;
1178            (position.checked_add(EOCD_LEN + comment_length) == Some(bytes.len()))
1179                .then_some(position)
1180        })
1181        .ok_or_else(|| invalid_zip("could not find end of central directory"))?;
1182
1183    let entries =
1184        read_u16(bytes, eocd + 10).ok_or_else(|| invalid_zip("truncated ZIP end record"))?;
1185    let central_size =
1186        read_u32(bytes, eocd + 12).ok_or_else(|| invalid_zip("truncated ZIP end record"))?;
1187    let central_offset =
1188        read_u32(bytes, eocd + 16).ok_or_else(|| invalid_zip("truncated ZIP end record"))?;
1189    if entries != u16::MAX && central_size != u32::MAX && central_offset != u32::MAX {
1190        let start = eocd
1191            .checked_sub(central_size as usize)
1192            .and_then(|start| u64::try_from(start).ok())
1193            .ok_or_else(|| invalid_zip("invalid central directory size"))?;
1194        let archive_offset = start
1195            .checked_sub(u64::from(central_offset))
1196            .ok_or_else(|| invalid_zip("invalid central directory offset"))?;
1197        return Ok(CentralDirectory {
1198            start,
1199            archive_offset,
1200        });
1201    }
1202
1203    let locator = eocd
1204        .checked_sub(20)
1205        .filter(|position| bytes.get(*position..*position + 4) == Some(b"PK\x06\x07"))
1206        .ok_or_else(|| invalid_zip("missing ZIP64 end locator"))?;
1207    let zip64_eocd = bytes[..locator]
1208        .windows(4)
1209        .enumerate()
1210        .rev()
1211        .find_map(|(position, signature)| {
1212            if signature != b"PK\x06\x06" {
1213                return None;
1214            }
1215            let record_size = read_u64(bytes, position + 4)?;
1216            let record_end = u64::try_from(position)
1217                .ok()?
1218                .checked_add(12)?
1219                .checked_add(record_size)?;
1220            (record_end == locator as u64).then_some(position)
1221        })
1222        .ok_or_else(|| invalid_zip("missing ZIP64 end record"))?;
1223    let central_size = read_u64(bytes, zip64_eocd + 40)
1224        .ok_or_else(|| invalid_zip("truncated ZIP64 end record"))?;
1225    let central_offset = read_u64(bytes, zip64_eocd + 48)
1226        .ok_or_else(|| invalid_zip("truncated ZIP64 end record"))?;
1227    let start = u64::try_from(zip64_eocd)
1228        .ok()
1229        .and_then(|end| end.checked_sub(central_size))
1230        .ok_or_else(|| invalid_zip("invalid ZIP64 central directory size"))?;
1231    let archive_offset = start
1232        .checked_sub(central_offset)
1233        .ok_or_else(|| invalid_zip("invalid ZIP64 central directory offset"))?;
1234    Ok(CentralDirectory {
1235        start,
1236        archive_offset,
1237    })
1238}
1239
1240fn invalid_zip(message: &'static str) -> ArchiveError {
1241    ArchiveError::Zip(zip::result::ZipError::InvalidArchive(message.into()))
1242}
1243
1244fn read_u16(bytes: &[u8], offset: usize) -> Option<u16> {
1245    Some(u16::from_le_bytes(
1246        bytes.get(offset..offset + 2)?.try_into().ok()?,
1247    ))
1248}
1249
1250fn read_u32(bytes: &[u8], offset: usize) -> Option<u32> {
1251    Some(u32::from_le_bytes(
1252        bytes.get(offset..offset + 4)?.try_into().ok()?,
1253    ))
1254}
1255
1256fn read_u64(bytes: &[u8], offset: usize) -> Option<u64> {
1257    Some(u64::from_le_bytes(
1258        bytes.get(offset..offset + 8)?.try_into().ok()?,
1259    ))
1260}
1261
1262struct RawCentralEntry {
1263    name: Vec<u8>,
1264    utf8: bool,
1265}
1266
1267fn raw_central_entries<R: Read + Seek>(
1268    reader: &mut R,
1269    archive: &[u8],
1270    central_directory: CentralDirectory,
1271    limits: DecodeLimits,
1272) -> Result<Vec<RawCentralEntry>, DecodeError> {
1273    reader.seek(SeekFrom::Start(central_directory.start))?;
1274    let mut entries = Vec::new();
1275    let mut total_name_bytes = 0u64;
1276    loop {
1277        let header_start = reader.stream_position()?;
1278        let mut signature = [0; 4];
1279        reader.read_exact(&mut signature)?;
1280        if signature != *b"PK\x01\x02" {
1281            reader.seek(SeekFrom::Start(header_start))?;
1282            break;
1283        }
1284
1285        let mut fixed = [0; 42];
1286        reader.read_exact(&mut fixed)?;
1287        let flags = u16::from_le_bytes([fixed[4], fixed[5]]);
1288        let name_len = u16::from_le_bytes([fixed[24], fixed[25]]) as usize;
1289        let extra_len = u16::from_le_bytes([fixed[26], fixed[27]]) as i64;
1290        let comment_len = u16::from_le_bytes([fixed[28], fixed[29]]) as i64;
1291        let observed_members = u64::try_from(entries.len())
1292            .ok()
1293            .and_then(|count| count.checked_add(1))
1294            .ok_or(DecodeLimitError::AccountingOverflow {
1295                resource: DecodeResource::Members,
1296            })?;
1297        if observed_members > limits.members() {
1298            return Err(
1299                DecodeLimitError::exceeded(DecodeResource::Members, limits.members()).into(),
1300            );
1301        }
1302        total_name_bytes = total_name_bytes
1303            .checked_add(u64::try_from(name_len).map_err(|_| {
1304                DecodeLimitError::AccountingOverflow {
1305                    resource: DecodeResource::RawMemberNameBytes,
1306                }
1307            })?)
1308            .ok_or(DecodeLimitError::AccountingOverflow {
1309                resource: DecodeResource::RawMemberNameBytes,
1310            })?;
1311        if total_name_bytes > limits.raw_member_name_bytes() {
1312            return Err(DecodeLimitError::exceeded(
1313                DecodeResource::RawMemberNameBytes,
1314                limits.raw_member_name_bytes(),
1315            )
1316            .into());
1317        }
1318        let mut name = vec![0; name_len];
1319        reader.read_exact(&mut name)?;
1320        let mut extra = vec![0; extra_len as usize];
1321        reader.read_exact(&mut extra)?;
1322        reader.seek(SeekFrom::Current(comment_len))?;
1323        let local_offset = zip64_local_offset(&fixed, &extra)?
1324            .checked_add(central_directory.archive_offset)
1325            .ok_or_else(|| invalid_zip("local header offset overflow"))?;
1326        let central_unicode_name = unicode_path(&extra, &name)?;
1327        validate_local_name(archive, local_offset, &name, flags, central_unicode_name)?;
1328        entries.push(RawCentralEntry {
1329            name,
1330            utf8: flags & (1 << 11) != 0,
1331        });
1332    }
1333    Ok(entries)
1334}
1335
1336fn zip64_local_offset(fixed: &[u8; 42], extra: &[u8]) -> Result<u64, ArchiveError> {
1337    let offset = u32::from_le_bytes([fixed[38], fixed[39], fixed[40], fixed[41]]);
1338    if offset != u32::MAX {
1339        return Ok(u64::from(offset));
1340    }
1341
1342    for field in ExtraFields::new(extra) {
1343        let (id, data) = field?;
1344        if id != 0x0001 {
1345            continue;
1346        }
1347
1348        let mut offset_cursor = 0usize;
1349        if u32::from_le_bytes([fixed[20], fixed[21], fixed[22], fixed[23]]) == u32::MAX {
1350            offset_cursor += 8;
1351        }
1352        if u32::from_le_bytes([fixed[16], fixed[17], fixed[18], fixed[19]]) == u32::MAX {
1353            offset_cursor += 8;
1354        }
1355        return read_u64(data, offset_cursor)
1356            .ok_or_else(|| invalid_zip("ZIP64 local header offset is missing"));
1357    }
1358    Err(invalid_zip("ZIP64 local header offset is missing"))
1359}
1360
1361fn validate_local_name(
1362    archive: &[u8],
1363    local_offset: u64,
1364    central_name: &[u8],
1365    central_flags: u16,
1366    central_unicode_name: Option<&[u8]>,
1367) -> Result<(), ArchiveError> {
1368    let start =
1369        usize::try_from(local_offset).map_err(|_| invalid_zip("invalid local header offset"))?;
1370    let fixed_end = start
1371        .checked_add(30)
1372        .ok_or_else(|| invalid_zip("local header offset overflow"))?;
1373    let fixed = archive
1374        .get(start..fixed_end)
1375        .ok_or_else(|| invalid_zip("truncated local header"))?;
1376    if &fixed[..4] != b"PK\x03\x04" {
1377        return Err(invalid_zip("invalid local header signature"));
1378    }
1379    let local_flags = u16::from_le_bytes([fixed[6], fixed[7]]);
1380    let name_len = usize::from(u16::from_le_bytes([fixed[26], fixed[27]]));
1381    let extra_len = usize::from(u16::from_le_bytes([fixed[28], fixed[29]]));
1382    let name_start = fixed_end;
1383    let name_end = name_start
1384        .checked_add(name_len)
1385        .ok_or_else(|| invalid_zip("local member name length overflow"))?;
1386    let extra_end = name_end
1387        .checked_add(extra_len)
1388        .ok_or_else(|| invalid_zip("local extra field length overflow"))?;
1389    let local_name = archive
1390        .get(name_start..name_end)
1391        .ok_or_else(|| invalid_zip("truncated local member name"))?;
1392    let local_extra = archive
1393        .get(name_end..extra_end)
1394        .ok_or_else(|| invalid_zip("truncated local extra fields"))?;
1395    if local_name != central_name || (local_flags ^ central_flags) & (1 << 11) != 0 {
1396        return Err(ArchiveError::AmbiguousMemberNames);
1397    }
1398    if local_flags & (1 << 11) != 0 && std::str::from_utf8(local_name).is_err() {
1399        return Err(ArchiveError::InvalidUtf8MemberName(local_name.to_vec()));
1400    }
1401    if let Some(local_unicode_name) = unicode_path(local_extra, local_name)?
1402        && local_unicode_name != central_unicode_name.unwrap_or(central_name)
1403    {
1404        return Err(ArchiveError::AmbiguousMemberNames);
1405    }
1406    Ok(())
1407}
1408
1409fn unicode_path<'a>(extra: &'a [u8], raw_name: &[u8]) -> Result<Option<&'a [u8]>, ArchiveError> {
1410    let mut unicode_name = None;
1411    for field in ExtraFields::new(extra) {
1412        let (id, data) = field?;
1413        if id != 0x7075 {
1414            continue;
1415        }
1416        if unicode_name.is_some() {
1417            return Err(ArchiveError::AmbiguousMemberNames);
1418        }
1419        let crc = data
1420            .get(1..5)
1421            .and_then(|bytes| bytes.try_into().ok())
1422            .map(u32::from_le_bytes)
1423            .ok_or_else(|| invalid_zip("Unicode path extra field is too small"))?;
1424        if crc != crc32(raw_name) {
1425            return Err(invalid_zip(
1426                "Unicode path extra field has an invalid checksum",
1427            ));
1428        }
1429        let name = &data[5..];
1430        std::str::from_utf8(name)
1431            .map_err(|_| invalid_zip("Unicode path extra field is not valid UTF-8"))?;
1432        unicode_name = Some(name);
1433    }
1434    Ok(unicode_name)
1435}
1436
1437struct ExtraFields<'a> {
1438    bytes: &'a [u8],
1439    cursor: usize,
1440}
1441
1442impl<'a> ExtraFields<'a> {
1443    fn new(bytes: &'a [u8]) -> Self {
1444        Self { bytes, cursor: 0 }
1445    }
1446}
1447
1448impl<'a> Iterator for ExtraFields<'a> {
1449    type Item = Result<(u16, &'a [u8]), ArchiveError>;
1450
1451    fn next(&mut self) -> Option<Self::Item> {
1452        if self.cursor == self.bytes.len() {
1453            return None;
1454        }
1455        let result = (|| {
1456            let header_end = self
1457                .cursor
1458                .checked_add(4)
1459                .ok_or_else(|| invalid_zip("extra field header overflow"))?;
1460            let header = self
1461                .bytes
1462                .get(self.cursor..header_end)
1463                .ok_or_else(|| invalid_zip("truncated extra field header"))?;
1464            let id = u16::from_le_bytes([header[0], header[1]]);
1465            let length = usize::from(u16::from_le_bytes([header[2], header[3]]));
1466            let field_end = header_end
1467                .checked_add(length)
1468                .ok_or_else(|| invalid_zip("extra field length overflow"))?;
1469            let data = self
1470                .bytes
1471                .get(header_end..field_end)
1472                .ok_or_else(|| invalid_zip("truncated extra field"))?;
1473            self.cursor = field_end;
1474            Ok((id, data))
1475        })();
1476        if result.is_err() {
1477            self.cursor = self.bytes.len();
1478        }
1479        Some(result)
1480    }
1481}
1482
1483fn crc32(data: &[u8]) -> u32 {
1484    let mut crc = !0u32;
1485    for byte in data {
1486        crc ^= u32::from(*byte);
1487        for _ in 0..8 {
1488            crc = (crc >> 1) ^ (0xedb8_8320 & 0u32.wrapping_sub(crc & 1));
1489        }
1490    }
1491    !crc
1492}
1493
1494const PROJECT_PREFIX: &str = "project/";
1495const PACKAGES_PREFIX: &str = "packages/";
1496const FILE_TYPE_MASK: u32 = 0o170000;
1497const REGULAR_FILE: u32 = 0o100000;
1498const DIRECTORY: u32 = 0o040000;
1499
1500struct ProjectEntry {
1501    index: usize,
1502    path: String,
1503}
1504
1505struct PackageEntry {
1506    index: usize,
1507    spec: PackageSpec,
1508    path: String,
1509}
1510
1511struct UnknownEntry {
1512    index: usize,
1513    canonical_name: String,
1514}
1515
1516struct AcceptedMember {
1517    index: usize,
1518    archive_name: String,
1519    role_name: String,
1520    canonical_name: String,
1521    directory: bool,
1522}
1523
1524fn is_regular_file<R: Read>(entry: &zip::read::ZipFile<'_, R>) -> bool {
1525    entry.is_file()
1526        && entry
1527            .unix_mode()
1528            .is_none_or(|mode| matches!(mode & FILE_TYPE_MASK, 0 | REGULAR_FILE))
1529}
1530
1531fn is_directory<R: Read>(entry: &zip::read::ZipFile<'_, R>) -> bool {
1532    entry.is_dir()
1533        && entry
1534            .unix_mode()
1535            .is_none_or(|mode| matches!(mode & FILE_TYPE_MASK, 0 | DIRECTORY))
1536}
1537
1538fn read_manifest<R: Read + Seek>(
1539    archive: &mut zip::ZipArchive<R>,
1540    index: usize,
1541    limits: DecodeLimits,
1542) -> Result<Vec<u8>, DecodeError> {
1543    let mut entry = archive.by_index(index)?;
1544    let name = entry.name().to_owned();
1545    let size = entry.size();
1546    read_bounded(
1547        &mut entry,
1548        size,
1549        limits.manifest_bytes(),
1550        DecodeResource::ManifestBytes,
1551        name,
1552    )
1553}
1554
1555fn preflight_content<R: Read + Seek>(
1556    archive: &mut zip::ZipArchive<R>,
1557    indices: &[usize],
1558    limits: DecodeLimits,
1559) -> Result<(), DecodeError> {
1560    let mut total = 0u64;
1561    for &index in indices {
1562        let size = archive.by_index_raw(index)?.size();
1563        check_exceeded(DecodeResource::MemberBytes, limits.member_bytes(), size)?;
1564        total = total
1565            .checked_add(size)
1566            .ok_or(DecodeLimitError::AccountingOverflow {
1567                resource: DecodeResource::TotalContentBytes,
1568            })?;
1569        check_exceeded(
1570            DecodeResource::TotalContentBytes,
1571            limits.total_content_bytes(),
1572            total,
1573        )?;
1574    }
1575    Ok(())
1576}
1577
1578fn read_content<R: Read + Seek>(
1579    archive: &mut zip::ZipArchive<R>,
1580    index: usize,
1581    limits: DecodeLimits,
1582    total: &mut u64,
1583) -> Result<Vec<u8>, DecodeError> {
1584    let entry = archive.by_index(index)?;
1585    let name = entry.name().to_owned();
1586    let size = entry.size();
1587    let total_remaining = limits.total_content_bytes().checked_sub(*total).ok_or(
1588        DecodeLimitError::AccountingOverflow {
1589            resource: DecodeResource::TotalContentBytes,
1590        },
1591    )?;
1592    let probe_ceiling = limits.member_bytes().min(total_remaining);
1593    let capacity = usize::try_from(size.min(probe_ceiling).min(64 * 1024)).unwrap();
1594    let mut data = Vec::with_capacity(capacity);
1595    entry
1596        .take(probe_ceiling + 1)
1597        .read_to_end(&mut data)
1598        .map_err(|source| ArchiveError::MemberUnreadable {
1599            member: name,
1600            source,
1601        })?;
1602    let actual_member_bytes =
1603        u64::try_from(data.len()).map_err(|_| DecodeLimitError::AccountingOverflow {
1604            resource: DecodeResource::MemberBytes,
1605        })?;
1606    check_exceeded(
1607        DecodeResource::MemberBytes,
1608        limits.member_bytes(),
1609        actual_member_bytes,
1610    )?;
1611    let actual_total =
1612        total
1613            .checked_add(actual_member_bytes)
1614            .ok_or(DecodeLimitError::AccountingOverflow {
1615                resource: DecodeResource::TotalContentBytes,
1616            })?;
1617    check_exceeded(
1618        DecodeResource::TotalContentBytes,
1619        limits.total_content_bytes(),
1620        actual_total,
1621    )?;
1622    *total = actual_total;
1623    Ok(data)
1624}
1625
1626fn read_bounded(
1627    reader: &mut impl Read,
1628    declared_size: u64,
1629    ceiling: u64,
1630    resource: DecodeResource,
1631    member: String,
1632) -> Result<Vec<u8>, DecodeError> {
1633    check_exceeded(resource, ceiling, declared_size)?;
1634    let capacity = usize::try_from(declared_size.min(ceiling).min(64 * 1024)).unwrap();
1635    let mut bytes = Vec::with_capacity(capacity);
1636    reader
1637        .take(ceiling + 1)
1638        .read_to_end(&mut bytes)
1639        .map_err(|source| ArchiveError::MemberUnreadable { member, source })?;
1640    check_exceeded(
1641        resource,
1642        ceiling,
1643        u64::try_from(bytes.len())
1644            .map_err(|_| DecodeLimitError::AccountingOverflow { resource })?,
1645    )?;
1646    Ok(bytes)
1647}
1648
1649fn check_exceeded(
1650    resource: DecodeResource,
1651    ceiling: u64,
1652    observed: u64,
1653) -> Result<(), DecodeLimitError> {
1654    if observed > ceiling {
1655        return Err(DecodeLimitError::exceeded(resource, ceiling));
1656    }
1657    Ok(())
1658}
1659
1660fn split_package_entry(rest: &str, member: &str) -> Result<(PackageSpec, String), ArchiveError> {
1661    let mut parts = rest.splitn(4, '/');
1662    let (Some(namespace), Some(name), Some(version), Some(path)) =
1663        (parts.next(), parts.next(), parts.next(), parts.next())
1664    else {
1665        return Err(ArchiveError::MalformedPackageMember(member.to_owned()));
1666    };
1667    let serialized = format!("@{namespace}/{name}:{version}");
1668    let spec =
1669        PackageSpec::from_str(&serialized).map_err(|error| ArchiveError::InvalidPackageSpec {
1670            member: member.to_owned(),
1671            spec: serialized,
1672            message: error.to_string(),
1673        })?;
1674    Ok((spec, path.trim_start_matches('/').to_owned()))
1675}
1676
1677fn canonical_archive_name(path: &str) -> Result<String, ArchiveError> {
1678    let prefix_normalized_path = strip_current_directory_prefix(path);
1679    if path.is_empty()
1680        || path.starts_with('/')
1681        || path.starts_with('\\')
1682        || path.contains('\\')
1683        || path.contains('\0')
1684        || has_windows_drive_prefix(prefix_normalized_path)
1685    {
1686        return Err(ArchiveError::UnsafeMemberName(path.to_owned()));
1687    }
1688    let canonical = canonical_relative_path(path)
1689        .map_err(|_| ArchiveError::UnsafeMemberName(path.to_owned()))?
1690        .into_string();
1691    if has_windows_drive_prefix(&canonical) {
1692        return Err(ArchiveError::UnsafeMemberName(path.to_owned()));
1693    }
1694    Ok(canonical)
1695}
1696
1697fn register_archive_identity(
1698    entries: &mut BTreeMap<String, Vec<u8>>,
1699    canonical: String,
1700    raw_name: &[u8],
1701) -> Result<(), ArchiveError> {
1702    if let Some(first_entry) = entries.get(&canonical) {
1703        if first_entry == raw_name {
1704            return Ok(());
1705        }
1706        return Err(ArchiveError::AmbiguousMemberNames);
1707    }
1708    entries.insert(canonical, raw_name.to_owned());
1709    Ok(())
1710}
1711
1712fn strip_current_directory_prefix(mut path: &str) -> &str {
1713    while let Some(rest) = path.strip_prefix("./") {
1714        path = rest;
1715    }
1716    path
1717}
1718
1719fn reserved_font_archive_role(path: &str) -> Option<ReservedMemberRole> {
1720    if is_same_or_descendant(path, MANIFEST_PATH) {
1721        Some(ReservedMemberRole::Manifest)
1722    } else if is_same_or_descendant(path, PROJECT_PREFIX.trim_end_matches('/')) {
1723        Some(ReservedMemberRole::Project)
1724    } else if is_same_or_descendant(path, PACKAGES_PREFIX.trim_end_matches('/')) {
1725        Some(ReservedMemberRole::Package)
1726    } else {
1727        None
1728    }
1729}
1730
1731fn is_same_or_descendant(path: &str, ancestor: &str) -> bool {
1732    path == ancestor
1733        || path
1734            .strip_prefix(ancestor)
1735            .is_some_and(|suffix| suffix.starts_with('/'))
1736}