1mod 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
30pub 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
43pub type EncodeLimitError = LimitError<EncodeResource>;
45
46#[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
60pub type EncodeLimits = Limits<EncodeResource>;
62
63impl Limits<EncodeResource> {
64 #[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 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#[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
155pub fn encode(pack: &Pack) -> Result<PackArchiveBytes, EncodeError> {
157 encode_with_limits(pack, EncodeLimits::reference_v1())
158}
159
160pub 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 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
663pub 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
676pub type DecodeLimitError = LimitError<DecodeResource>;
678
679#[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#[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#[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
768pub type DecodeLimits = Limits<DecodeResource>;
770
771impl Limits<DecodeResource> {
772 #[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 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
839pub 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}