1use std::cell::RefCell;
13use std::io::Read;
14use std::rc::Rc;
15
16use typst::foundations::Bytes;
17use typst::syntax::package::PackageSpec;
18
19use crate::Pack;
20use crate::limits::{LimitError, Limits, ResourceKind};
21use crate::package_catalog::{PackageTree, PackageTreeError};
22use crate::read_layout;
23
24#[derive(Debug, thiserror::Error)]
26#[non_exhaustive]
27pub enum PackageArchiveReadError {
28 #[error(transparent)]
29 Limit(#[from] PackageExpansionLimitError),
30 #[error("failed to read Package Archive bytes: {0}")]
31 Read(#[source] std::io::Error),
32}
33
34pub const PACKAGE_REGISTRY_URL: &str = read_layout::PACKAGE_REGISTRY_URL;
38
39pub const PACKAGE_REGISTRY_NAMESPACE: &str = read_layout::PACKAGE_REGISTRY_NAMESPACE;
43
44pub type PackageExpansionResource = ResourceKind<6>;
46
47#[allow(non_upper_case_globals)]
48impl ResourceKind<6> {
49 pub const CompressedArchiveBytes: Self = Self::new(0);
50 pub const Members: Self = Self::new(1);
51 pub const MemberNameBytes: Self = Self::new(2);
52 pub const MemberBytes: Self = Self::new(3);
53 pub const TotalExpandedBytes: Self = Self::new(4);
54}
55
56pub type PackageExpansionLimitError = LimitError<PackageExpansionResource>;
58
59pub type PackageExpansionLimits = Limits<PackageExpansionResource>;
61
62impl Limits<PackageExpansionResource> {
63 #[track_caller]
65 pub fn new(
66 compressed_archive_bytes: u64,
67 members: u64,
68 member_name_bytes: u64,
69 member_bytes: u64,
70 total_expanded_bytes: u64,
71 ) -> Self {
72 Self::from_ceilings([
73 compressed_archive_bytes,
74 members,
75 member_name_bytes,
76 member_bytes,
77 total_expanded_bytes,
78 0,
79 0,
80 ])
81 .assert_probe_resources([
82 PackageExpansionResource::CompressedArchiveBytes,
83 PackageExpansionResource::Members,
84 PackageExpansionResource::MemberNameBytes,
85 PackageExpansionResource::MemberBytes,
86 PackageExpansionResource::TotalExpandedBytes,
87 ])
88 }
89
90 pub const fn reference_v1() -> Self {
92 Self::from_ceilings([
93 128 * 1024 * 1024,
94 50_000,
95 8 * 1024 * 1024,
96 64 * 1024 * 1024,
97 512 * 1024 * 1024,
98 0,
99 0,
100 ])
101 }
102
103 pub const fn compressed_archive_bytes(&self) -> u64 {
104 self.ceilings[0]
105 }
106
107 pub const fn members(&self) -> u64 {
108 self.ceilings[1]
109 }
110
111 pub const fn member_name_bytes(&self) -> u64 {
112 self.ceilings[2]
113 }
114
115 pub const fn member_bytes(&self) -> u64 {
116 self.ceilings[3]
117 }
118
119 pub const fn total_expanded_bytes(&self) -> u64 {
120 self.ceilings[4]
121 }
122}
123
124pub fn read_package_archive(
131 mut reader: impl Read,
132 known_size: Option<u64>,
133 limits: PackageExpansionLimits,
134) -> Result<Vec<u8>, PackageArchiveReadError> {
135 let resource = PackageExpansionResource::CompressedArchiveBytes;
136 if let Some(size) = known_size {
137 check_expansion_limit(resource, limits.compressed_archive_bytes(), size)?;
138 }
139
140 let mut bytes = Vec::new();
141 reader
142 .by_ref()
143 .take(limits.compressed_archive_bytes() + 1)
144 .read_to_end(&mut bytes)
145 .map_err(PackageArchiveReadError::Read)?;
146 let observed = u64::try_from(bytes.len())
147 .map_err(|_| PackageExpansionLimitError::AccountingOverflow { resource })?;
148 check_expansion_limit(resource, limits.compressed_archive_bytes(), observed)?;
149 Ok(bytes)
150}
151
152pub fn package_archive_url(spec: &PackageSpec) -> Result<String, PackageReadError> {
159 read_layout::official_registry_archive_url(spec)
160 .ok_or_else(|| PackageReadError::UnservedNamespace { spec: spec.clone() })
161}
162
163pub fn expand_package_archive(
179 spec: PackageSpec,
180 archive: &[u8],
181 limits: PackageExpansionLimits,
182) -> Result<PackageTree, PackageReadError> {
183 let malformed = |message: String| PackageReadError::MalformedArchive {
184 spec: spec.clone(),
185 message,
186 };
187 let limited = |source| PackageReadError::ExpansionLimit {
188 spec: spec.clone(),
189 source,
190 };
191
192 let compressed_archive_bytes = u64::try_from(archive.len()).map_err(|_| {
193 limited(PackageExpansionLimitError::AccountingOverflow {
194 resource: PackageExpansionResource::CompressedArchiveBytes,
195 })
196 })?;
197 check_expansion_limit(
198 PackageExpansionResource::CompressedArchiveBytes,
199 limits.compressed_archive_bytes(),
200 compressed_archive_bytes,
201 )
202 .map_err(limited)?;
203
204 let tar_state = Rc::new(RefCell::new(TarReadState::default()));
205 let reader = ObservedTarReader {
206 inner: flate2::read::GzDecoder::new(archive),
207 state: Rc::clone(&tar_state),
208 };
209 let mut archive = tar::Archive::new(reader);
210 let mut files: Vec<(String, Bytes)> = Vec::new();
211 let mut members = 0u64;
212 let mut member_name_bytes = 0u64;
213 let mut total_expanded_bytes = 0u64;
214 let mut gnu_long_name: Option<Vec<u8>> = None;
215 let mut gnu_long_link = false;
216 let mut pax_local = false;
217 let mut pax_path: Option<Vec<u8>> = None;
218 let mut pax_size: Option<u64> = None;
219
220 for entry in archive
221 .entries()
222 .map_err(|error| malformed(error.to_string()))?
223 .raw(true)
224 {
225 let mut entry = entry.map_err(|error| malformed(error.to_string()))?;
226 check_tar_padding(&tar_state).map_err(&malformed)?;
227
228 members = checked_add(members, 1, PackageExpansionResource::Members).map_err(limited)?;
229 check_expansion_limit(PackageExpansionResource::Members, limits.members(), members)
230 .map_err(limited)?;
231
232 let raw_name = entry.header().path_bytes().into_owned();
233 member_name_bytes = checked_add_usize(
234 member_name_bytes,
235 raw_name.len(),
236 PackageExpansionResource::MemberNameBytes,
237 )
238 .map_err(limited)?;
239 check_expansion_limit(
240 PackageExpansionResource::MemberNameBytes,
241 limits.member_name_bytes(),
242 member_name_bytes,
243 )
244 .map_err(limited)?;
245
246 let entry_type = entry.header().entry_type();
247 let extension = entry_type.is_gnu_longname()
248 || entry_type.is_gnu_longlink()
249 || entry_type.is_pax_local_extensions()
250 || entry_type.is_pax_global_extensions();
251 if !extension
252 && let Some(pax_size) = pax_size.take()
253 && pax_size != entry.size()
254 {
255 return Err(malformed(format!(
256 "PAX size {pax_size} conflicts with member size {}",
257 entry.size()
258 )));
259 }
260 if entry_type.is_gnu_longname() {
261 let observed = checked_add(
262 member_name_bytes,
263 entry.size(),
264 PackageExpansionResource::MemberNameBytes,
265 )
266 .map_err(limited)?;
267 check_expansion_limit(
268 PackageExpansionResource::MemberNameBytes,
269 limits.member_name_bytes(),
270 observed,
271 )
272 .map_err(limited)?;
273 }
274
275 check_expansion_limit(
276 PackageExpansionResource::MemberBytes,
277 limits.member_bytes(),
278 entry.size(),
279 )
280 .map_err(limited)?;
281 let observed_total = checked_add(
282 total_expanded_bytes,
283 entry.size(),
284 PackageExpansionResource::TotalExpandedBytes,
285 )
286 .map_err(limited)?;
287 check_expansion_limit(
288 PackageExpansionResource::TotalExpandedBytes,
289 limits.total_expanded_bytes(),
290 observed_total,
291 )
292 .map_err(limited)?;
293
294 let declared_size = entry.size();
295 if entry_type.is_pax_global_extensions() {
296 return Err(malformed(
297 "global PAX extensions make package member names ambiguous".to_owned(),
298 ));
299 }
300 if entry_type.is_pax_local_extensions() {
301 if pax_local {
302 return Err(malformed(
303 "multiple local PAX extensions describe one archive member".to_owned(),
304 ));
305 }
306 let name_remaining = limits.member_name_bytes() - member_name_bytes;
307 let parsed = match read_pax(&mut entry, declared_size, name_remaining) {
308 Ok(parsed) => parsed,
309 Err(PaxReadError::Io(error) | PaxReadError::Malformed(error)) => {
310 return Err(malformed(error));
311 }
312 Err(PaxReadError::NameLimit { observed }) => {
313 checked_add(
314 member_name_bytes,
315 observed,
316 PackageExpansionResource::MemberNameBytes,
317 )
318 .map_err(limited)?;
319 return Err(limited(PackageExpansionLimitError::exceeded(
320 PackageExpansionResource::MemberNameBytes,
321 limits.member_name_bytes(),
322 )));
323 }
324 };
325 register_tar_padding(&tar_state, entry.raw_file_position(), declared_size)
326 .map_err(&malformed)?;
327 total_expanded_bytes = checked_add(
328 total_expanded_bytes,
329 declared_size,
330 PackageExpansionResource::TotalExpandedBytes,
331 )
332 .map_err(limited)?;
333 if let Some(path) = &parsed.path {
334 member_name_bytes = checked_add_usize(
335 member_name_bytes,
336 path.len(),
337 PackageExpansionResource::MemberNameBytes,
338 )
339 .map_err(limited)?;
340 }
341 if let Some(size) = parsed.size {
342 check_expansion_limit(
343 PackageExpansionResource::MemberBytes,
344 limits.member_bytes(),
345 size,
346 )
347 .map_err(limited)?;
348 let observed_total = checked_add(
349 total_expanded_bytes,
350 size,
351 PackageExpansionResource::TotalExpandedBytes,
352 )
353 .map_err(limited)?;
354 check_expansion_limit(
355 PackageExpansionResource::TotalExpandedBytes,
356 limits.total_expanded_bytes(),
357 observed_total,
358 )
359 .map_err(limited)?;
360 }
361 pax_path = parsed.path;
362 pax_size = parsed.size;
363 pax_local = true;
364 continue;
365 }
366
367 let probe_ceiling = limits
368 .member_bytes()
369 .min(limits.total_expanded_bytes() - total_expanded_bytes);
370 let mut reader = entry.by_ref().take(probe_ceiling + 1);
371 let retain = entry_type.is_file() || entry_type.is_gnu_longname();
372 let capacity = usize::try_from(declared_size.min(64 * 1024)).unwrap();
373 let mut data = Vec::with_capacity(if retain { capacity } else { 0 });
374 let mut observed_size = 0u64;
375 let mut buffer = [0; 16 * 1024];
376 loop {
377 let read = reader
378 .read(&mut buffer)
379 .map_err(|error| malformed(error.to_string()))?;
380 if read == 0 {
381 break;
382 }
383 observed_size =
384 checked_add_usize(observed_size, read, PackageExpansionResource::MemberBytes)
385 .map_err(limited)?;
386 check_expansion_limit(
387 PackageExpansionResource::MemberBytes,
388 limits.member_bytes(),
389 observed_size,
390 )
391 .map_err(limited)?;
392 let observed_total = checked_add(
393 total_expanded_bytes,
394 observed_size,
395 PackageExpansionResource::TotalExpandedBytes,
396 )
397 .map_err(limited)?;
398 check_expansion_limit(
399 PackageExpansionResource::TotalExpandedBytes,
400 limits.total_expanded_bytes(),
401 observed_total,
402 )
403 .map_err(limited)?;
404 if retain {
405 data.extend_from_slice(&buffer[..read]);
406 }
407 }
408 if observed_size != declared_size {
409 return Err(malformed(format!(
410 "archive member declared {declared_size} byte(s) but yielded {observed_size}"
411 )));
412 }
413 register_tar_padding(&tar_state, entry.raw_file_position(), declared_size)
414 .map_err(&malformed)?;
415 total_expanded_bytes = checked_add(
416 total_expanded_bytes,
417 observed_size,
418 PackageExpansionResource::TotalExpandedBytes,
419 )
420 .map_err(limited)?;
421
422 if entry_type.is_gnu_longname() {
423 if gnu_long_name.is_some() {
424 return Err(malformed(
425 "multiple GNU long names describe one archive member".to_owned(),
426 ));
427 }
428 member_name_bytes = checked_add(
429 member_name_bytes,
430 observed_size,
431 PackageExpansionResource::MemberNameBytes,
432 )
433 .map_err(limited)?;
434 gnu_long_name = Some(strip_gnu_terminator(data));
435 continue;
436 }
437 if entry_type.is_gnu_longlink() {
438 if gnu_long_link {
439 return Err(malformed(
440 "multiple GNU long links describe one archive member".to_owned(),
441 ));
442 }
443 gnu_long_link = true;
444 continue;
445 }
446 let effective_name = match (gnu_long_name.take(), pax_path.take()) {
447 (Some(_), Some(_)) => {
448 return Err(malformed(
449 "GNU and PAX extensions provide ambiguous member names".to_owned(),
450 ));
451 }
452 (Some(name), None) | (None, Some(name)) => name,
453 (None, None) => raw_name,
454 };
455 gnu_long_link = false;
456 pax_local = false;
457 let path = std::str::from_utf8(&effective_name)
458 .map_err(|_| malformed(format!("member name {effective_name:?} is not valid UTF-8")))?
459 .to_owned();
460
461 if entry_type.is_file() {
462 files.push((path, Bytes::new(data)));
463 } else {
464 Pack::canonical_package_path(&path).map_err(|message| {
465 malformed(format!(
466 "archive member {path:?} does not name a package file: {message}"
467 ))
468 })?;
469 }
470 }
471
472 check_tar_padding(&tar_state).map_err(&malformed)?;
473
474 if gnu_long_name.is_some() || gnu_long_link || pax_local {
475 return Err(malformed(
476 "archive metadata describes a future member that is missing".to_owned(),
477 ));
478 }
479
480 PackageTree::from_typst_entries(files)
481 .map_err(|source| PackageReadError::InvalidPackageTree { spec, source })
482}
483
484#[derive(Default)]
485struct TarReadState {
486 position: u64,
487 padding: Option<(u64, u64)>,
488 nonzero_padding: bool,
489}
490
491struct ObservedTarReader<R> {
492 inner: R,
493 state: Rc<RefCell<TarReadState>>,
494}
495
496impl<R: Read> Read for ObservedTarReader<R> {
497 fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
498 let read = self.inner.read(buffer)?;
499 let mut state = self.state.borrow_mut();
500 let start = state.position;
501 state.position = state
502 .position
503 .checked_add(read as u64)
504 .ok_or_else(|| std::io::Error::other("tar stream position overflowed"))?;
505 if let Some((padding_start, padding_end)) = state.padding {
506 let overlap_start = start.max(padding_start);
507 let overlap_end = state.position.min(padding_end);
508 if overlap_start < overlap_end {
509 let buffer_start = usize::try_from(overlap_start - start).unwrap();
510 let buffer_end = usize::try_from(overlap_end - start).unwrap();
511 state.nonzero_padding |= buffer[buffer_start..buffer_end]
512 .iter()
513 .any(|byte| *byte != 0);
514 }
515 if state.position >= padding_end {
516 state.padding = None;
517 }
518 }
519 Ok(read)
520 }
521}
522
523fn register_tar_padding(
524 state: &Rc<RefCell<TarReadState>>,
525 file_position: u64,
526 declared_size: u64,
527) -> Result<(), String> {
528 let padding_start = file_position
529 .checked_add(declared_size)
530 .ok_or_else(|| "tar member position overflowed".to_owned())?;
531 let padding_end = padding_start
532 .checked_add(511)
533 .map(|end| end & !511)
534 .ok_or_else(|| "tar member padding position overflowed".to_owned())?;
535 state.borrow_mut().padding =
536 (padding_start < padding_end).then_some((padding_start, padding_end));
537 Ok(())
538}
539
540fn check_tar_padding(state: &Rc<RefCell<TarReadState>>) -> Result<(), String> {
541 if state.borrow().nonzero_padding {
542 return Err("archive member contains non-zero bytes past its declared size".to_owned());
543 }
544 Ok(())
545}
546
547fn strip_gnu_terminator(mut name: Vec<u8>) -> Vec<u8> {
548 if name.last() == Some(&0) {
549 name.pop();
550 }
551 name
552}
553
554struct PaxMetadata {
555 path: Option<Vec<u8>>,
556 size: Option<u64>,
557}
558
559enum PaxReadError {
560 Io(String),
561 Malformed(String),
562 NameLimit { observed: u64 },
563}
564
565fn read_pax(
566 reader: &mut impl Read,
567 declared_size: u64,
568 name_remaining: u64,
569) -> Result<PaxMetadata, PaxReadError> {
570 let mut remaining = declared_size;
571 let mut path = None;
572 let mut size = None;
573 while remaining > 0 {
574 let mut prefix = Vec::with_capacity(21);
575 loop {
576 let byte = read_pax_byte(reader)?;
577 prefix.push(byte);
578 if byte == b' ' {
579 break;
580 }
581 if !byte.is_ascii_digit() || prefix.len() > 20 {
582 return Err(PaxReadError::Malformed(
583 "malformed PAX record length".to_owned(),
584 ));
585 }
586 }
587 let prefix_length = u64::try_from(prefix.len()).unwrap();
588 let record_length = std::str::from_utf8(&prefix[..prefix.len() - 1])
589 .ok()
590 .and_then(|length| length.parse::<u64>().ok())
591 .filter(|length| *length > prefix_length + 2 && *length <= remaining)
592 .ok_or_else(|| PaxReadError::Malformed("invalid PAX record length".to_owned()))?;
593
594 let mut key = Vec::with_capacity(16);
595 let mut key_length = 0u64;
596 loop {
597 let byte = read_pax_byte(reader)?;
598 key_length += 1;
599 if byte == b'=' {
600 break;
601 }
602 if key.len() < 16 {
603 key.push(byte);
604 }
605 if prefix_length + key_length + 1 >= record_length {
606 return Err(PaxReadError::Malformed(
607 "malformed PAX key-value record".to_owned(),
608 ));
609 }
610 }
611 let value_length = record_length - prefix_length - key_length - 1;
612 match key.as_slice() {
613 b"path" if path.is_some() => {
614 return Err(PaxReadError::Malformed(
615 "multiple PAX paths describe one archive member".to_owned(),
616 ));
617 }
618 b"path" => {
619 if value_length > name_remaining {
620 return Err(PaxReadError::NameLimit {
621 observed: value_length,
622 });
623 }
624 let length = usize::try_from(value_length).map_err(|_| {
625 PaxReadError::Malformed("PAX path length is not addressable".to_owned())
626 })?;
627 let mut value = vec![0; length];
628 read_pax_exact(reader, &mut value)?;
629 path = Some(value);
630 }
631 b"size" if size.is_some() => {
632 return Err(PaxReadError::Malformed(
633 "multiple PAX sizes describe one archive member".to_owned(),
634 ));
635 }
636 b"size" => {
637 if value_length > 20 {
638 return Err(PaxReadError::Malformed(
639 "PAX size is not an unsigned integer".to_owned(),
640 ));
641 }
642 let mut value = vec![0; value_length as usize];
643 read_pax_exact(reader, &mut value)?;
644 size = Some(
645 std::str::from_utf8(&value)
646 .ok()
647 .and_then(|value| value.parse().ok())
648 .ok_or_else(|| {
649 PaxReadError::Malformed(
650 "PAX size is not an unsigned integer".to_owned(),
651 )
652 })?,
653 );
654 }
655 _ => discard_pax_bytes(reader, value_length)?,
656 }
657 if read_pax_byte(reader)? != b'\n' {
658 return Err(PaxReadError::Malformed(
659 "PAX record does not end with a newline".to_owned(),
660 ));
661 }
662 remaining -= record_length;
663 }
664 Ok(PaxMetadata { path, size })
665}
666
667fn read_pax_byte(reader: &mut impl Read) -> Result<u8, PaxReadError> {
668 let mut byte = [0];
669 read_pax_exact(reader, &mut byte)?;
670 Ok(byte[0])
671}
672
673fn read_pax_exact(reader: &mut impl Read, buffer: &mut [u8]) -> Result<(), PaxReadError> {
674 reader
675 .read_exact(buffer)
676 .map_err(|error| PaxReadError::Io(error.to_string()))
677}
678
679fn discard_pax_bytes(reader: &mut impl Read, mut remaining: u64) -> Result<(), PaxReadError> {
680 let mut buffer = [0; 8 * 1024];
681 while remaining > 0 {
682 let length = usize::try_from(remaining.min(buffer.len() as u64)).unwrap();
683 read_pax_exact(reader, &mut buffer[..length])?;
684 remaining -= length as u64;
685 }
686 Ok(())
687}
688
689fn checked_add(
690 total: u64,
691 value: u64,
692 resource: PackageExpansionResource,
693) -> Result<u64, PackageExpansionLimitError> {
694 total
695 .checked_add(value)
696 .ok_or(PackageExpansionLimitError::AccountingOverflow { resource })
697}
698
699fn checked_add_usize(
700 total: u64,
701 value: usize,
702 resource: PackageExpansionResource,
703) -> Result<u64, PackageExpansionLimitError> {
704 let value = u64::try_from(value)
705 .map_err(|_| PackageExpansionLimitError::AccountingOverflow { resource })?;
706 checked_add(total, value, resource)
707}
708
709fn check_expansion_limit(
710 resource: PackageExpansionResource,
711 ceiling: u64,
712 observed: u64,
713) -> Result<(), PackageExpansionLimitError> {
714 if observed > ceiling {
715 return Err(PackageExpansionLimitError::exceeded(resource, ceiling));
716 }
717 Ok(())
718}
719
720#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
722#[non_exhaustive]
723pub enum PackageReadError {
724 #[error(
727 "the registry serves only the `{PACKAGE_REGISTRY_NAMESPACE}` namespace, and {spec} is not in it"
728 )]
729 UnservedNamespace { spec: PackageSpec },
730 #[error("the archive for {spec} exceeded an expansion limit: {source}")]
732 ExpansionLimit {
733 spec: PackageSpec,
734 source: PackageExpansionLimitError,
735 },
736 #[error("the archive for {spec} is malformed: {message:?}")]
738 MalformedArchive { spec: PackageSpec, message: String },
739 #[error("the archive for {spec} does not contain a valid package tree: {source}")]
741 InvalidPackageTree {
742 spec: PackageSpec,
743 source: PackageTreeError,
744 },
745}
746
747#[cfg(test)]
748mod tests {
749 use super::*;
750
751 #[test]
752 fn package_expansion_accounting_overflow_is_typed() {
753 assert_eq!(
754 checked_add(u64::MAX, 1, PackageExpansionResource::TotalExpandedBytes),
755 Err(PackageExpansionLimitError::AccountingOverflow {
756 resource: PackageExpansionResource::TotalExpandedBytes,
757 })
758 );
759 }
760}