Skip to main content

typst_pack/
package_reading.rs

1//! Package reading for a Pack Assembler that supplies its own transport.
2//!
3//! Creation reports the exact specifications its representative request needs
4//! and was not given. These helpers cover the two transformations between that
5//! report and a resolved tree — constructing the registry URL of a
6//! specification, and expanding the archive bytes fetched from it into a
7//! Package Tree — so a caller reimplements neither the registry layout
8//! nor the archive encoding. Fetching those bytes is the adapter's own
9//! obligation and stays outside: nothing here needs an HTTP client, so the
10//! helpers are usable on a host whose only network access is asynchronous.
11
12use 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/// A failure while reading exact Package Archive bytes from a stream.
25#[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
34/// The URL of the package registry these helpers describe the layout of, the
35/// official Typst Universe registry. There is no standardized registry
36/// protocol, so the layout is this registry's own.
37pub const PACKAGE_REGISTRY_URL: &str = read_layout::PACKAGE_REGISTRY_URL;
38
39/// The one package namespace the registry serves. A specification in any other
40/// namespace is resolved from wherever its namespace lives, which the registry
41/// layout says nothing about.
42pub const PACKAGE_REGISTRY_NAMESPACE: &str = read_layout::PACKAGE_REGISTRY_NAMESPACE;
43
44/// A resource bounded during Package Archive Expansion.
45pub 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
56/// A package archive exceeded a mandatory expansion ceiling.
57pub type PackageExpansionLimitError = LimitError<PackageExpansionResource>;
58
59/// Mandatory finite resource ceilings for Package Archive Expansion.
60pub type PackageExpansionLimits = Limits<PackageExpansionResource>;
61
62impl Limits<PackageExpansionResource> {
63    /// Constructs a validated set of mandatory finite expansion ceilings.
64    #[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    /// The first-party limits for registry package archives.
91    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
124/// Reads exact Package Archive bytes under the expansion profile's
125/// compressed-byte ceiling.
126///
127/// A known size is checked before the reader is touched. The stream is still
128/// incrementally metered with a plus-one probe because a size declaration is
129/// only a hint.
130pub 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
152/// The URL of the archive holding one exact package specification's Package
153/// Tree.
154///
155/// No index lookup is involved: creation only ever reports fully versioned
156/// specifications, because a Typst import specification always carries an exact
157/// version.
158pub 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
163/// Expands the archive bytes served for one exact package specification into
164/// the Package Tree creation accepts as a resolved tree for it.
165///
166/// The bytes are the gzip-compressed tar a registry serves, which the caller
167/// fetched with whatever primitive its host provides; expansion itself needs no
168/// transport. Only addressable regular files become tree entries: directories,
169/// links, and other archive members are not package files. An entry whose path
170/// cannot name one is rejected here rather than carried into a tree, because
171/// expansion is where a hostile archive is met.
172///
173/// Expansion stops at `limits` and fails with
174/// [`PackageReadError::ExpansionLimit`] rather than materializing what
175/// lies past it. Every raw member and its payload is charged before semantic
176/// interpretation, whether or not that member becomes a package file.
177///
178pub 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/// A failure while reading one Package Tree.
721#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
722#[non_exhaustive]
723pub enum PackageReadError {
724    /// The registry does not serve the specification's namespace, so it has no
725    /// URL there.
726    #[error(
727        "the registry serves only the `{PACKAGE_REGISTRY_NAMESPACE}` namespace, and {spec} is not in it"
728    )]
729    UnservedNamespace { spec: PackageSpec },
730    /// The archive exceeds one expansion resource ceiling.
731    #[error("the archive for {spec} exceeded an expansion limit: {source}")]
732    ExpansionLimit {
733        spec: PackageSpec,
734        source: PackageExpansionLimitError,
735    },
736    /// The bytes are not the archive a registry serves for the specification.
737    #[error("the archive for {spec} is malformed: {message:?}")]
738    MalformedArchive { spec: PackageSpec, message: String },
739    /// The archive does not expand into a valid Package Tree.
740    #[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}