lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

safe_artifact_io.rs (132112B)


      1 //! Descriptor-bound, resource-bounded input handling for release tooling.
      2 
      3 use std::collections::BTreeMap;
      4 use std::ffi::{OsStr, OsString};
      5 use std::fmt;
      6 use std::fs::{self, File};
      7 use std::io::{self, BufRead as _, BufReader, Read, Write};
      8 use std::path::{Component, Path, PathBuf};
      9 
     10 use flate2::{Compression, GzBuilder, bufread::GzDecoder};
     11 use sha2::{Digest as _, Sha256};
     12 use tar::{Builder as TarBuilder, Header as TarHeader};
     13 
     14 const STREAM_BUFFER_BYTES: usize = 64 * 1024;
     15 const HARD_MAX_BUFFERED_READ_BYTES: u64 = 67_108_864;
     16 const HARD_MAX_STREAM_FILE_BYTES: u64 = 17_179_869_184;
     17 const HARD_MAX_TRAVERSAL_ENTRIES: u64 = 65_536;
     18 const HARD_MAX_TRAVERSAL_FILES: u64 = 65_536;
     19 const HARD_MAX_TRAVERSAL_TOTAL_BYTES: u64 = 68_719_476_736;
     20 const HARD_MAX_TRAVERSAL_DEPTH: usize = 64;
     21 const HARD_MAX_PATH_BYTES: usize = 4_096;
     22 const HARD_MAX_ARCHIVE_COMPRESSED_BYTES: u64 = 2_147_483_648;
     23 const HARD_MAX_ARCHIVE_EXPANDED_BYTES: u64 = 17_179_869_184;
     24 const HARD_MAX_ARCHIVE_MEMBERS: u64 = 65_536;
     25 const HARD_MAX_ARCHIVE_MEMBER_BYTES: u64 = 17_179_869_184;
     26 const HARD_MAX_ARCHIVE_PAYLOAD_BYTES: u64 = 17_179_869_184;
     27 const TAR_BLOCK_BYTES: usize = 512;
     28 const TAR_TERMINATOR_BYTES: usize = TAR_BLOCK_BYTES * 2;
     29 const CANONICAL_GZIP_COMPRESSION_LEVEL: u32 = 6;
     30 
     31 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     32 pub(crate) enum LimitKind {
     33     ArchiveCompressedBytes,
     34     ArchiveDepth,
     35     ArchiveExpandedBytes,
     36     ArchiveMemberBytes,
     37     ArchiveMembers,
     38     ArchivePathBytes,
     39     ArchivePayloadBytes,
     40     FileBytes,
     41     TraversalDepth,
     42     TraversalEntries,
     43     TraversalFiles,
     44     TraversalPathBytes,
     45     TraversalTotalBytes,
     46 }
     47 
     48 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     49 pub(crate) enum ArtifactIoFailureKind {
     50     ChangedDuringRead,
     51     InvalidObject,
     52     InvalidRequest,
     53     IoFailure,
     54     LimitExceeded(LimitKind),
     55     MalformedArchive,
     56     #[cfg_attr(unix, allow(dead_code))]
     57     UnsupportedPlatform,
     58 }
     59 
     60 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     61 pub(crate) struct ArtifactIoError {
     62     kind: ArtifactIoFailureKind,
     63 }
     64 
     65 impl ArtifactIoError {
     66     const fn new(kind: ArtifactIoFailureKind) -> Self {
     67         Self { kind }
     68     }
     69 
     70     pub(crate) const fn kind(self) -> ArtifactIoFailureKind {
     71         self.kind
     72     }
     73 }
     74 
     75 impl fmt::Display for ArtifactIoError {
     76     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     77         formatter.write_str(match self.kind {
     78             ArtifactIoFailureKind::ChangedDuringRead => "artifact binding changed during admission",
     79             ArtifactIoFailureKind::InvalidObject => "artifact object type is not admitted",
     80             ArtifactIoFailureKind::InvalidRequest => "artifact I/O request is invalid",
     81             ArtifactIoFailureKind::IoFailure => "artifact I/O operation failed",
     82             ArtifactIoFailureKind::LimitExceeded(_) => "artifact I/O limit was exceeded",
     83             ArtifactIoFailureKind::MalformedArchive => "artifact archive is malformed",
     84             ArtifactIoFailureKind::UnsupportedPlatform => {
     85                 "safe artifact I/O is unsupported on this platform"
     86             }
     87         })
     88     }
     89 }
     90 
     91 impl std::error::Error for ArtifactIoError {}
     92 
     93 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     94 pub(crate) struct TraversalLimits {
     95     pub(crate) max_entries: u64,
     96     pub(crate) max_files: u64,
     97     pub(crate) max_total_bytes: u64,
     98     pub(crate) max_file_bytes: u64,
     99     pub(crate) max_depth: usize,
    100     pub(crate) max_path_bytes: usize,
    101 }
    102 
    103 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    104 pub(crate) struct TarGzipLimits {
    105     pub(crate) max_compressed_bytes: u64,
    106     pub(crate) max_expanded_bytes: u64,
    107     pub(crate) max_members: u64,
    108     pub(crate) max_member_bytes: u64,
    109     pub(crate) max_payload_bytes: u64,
    110     pub(crate) max_depth: usize,
    111     pub(crate) max_path_bytes: usize,
    112 }
    113 
    114 #[derive(Clone, Debug, Eq, PartialEq)]
    115 pub(crate) struct FileEvidence {
    116     pub(crate) byte_length: u64,
    117     pub(crate) sha256: String,
    118 }
    119 
    120 #[derive(Clone, Debug, Eq, PartialEq)]
    121 pub(crate) struct ArchiveEvidence {
    122     pub(crate) compressed: FileEvidence,
    123     pub(crate) expanded_bytes: u64,
    124     pub(crate) member_count: u64,
    125     pub(crate) payload_bytes: u64,
    126 }
    127 
    128 #[derive(Debug)]
    129 pub(crate) struct MaterializedArchive {
    130     root_descriptor: File,
    131     root_identity: OutputDirectoryIdentity,
    132     root_chain: Vec<DirectoryIdentity>,
    133     parent_descriptor: File,
    134     parent_identity: OutputDirectoryIdentity,
    135     parent_path: PathBuf,
    136     parent_chain: Vec<OutputDirectoryIdentity>,
    137     snapshot: TraversalSnapshot,
    138     evidence: ArchiveEvidence,
    139     directory: tempfile::TempDir,
    140 }
    141 
    142 impl MaterializedArchive {
    143     pub(crate) fn root(&self) -> &Path {
    144         self.directory.path()
    145     }
    146 
    147     pub(crate) fn evidence(&self) -> &ArchiveEvidence {
    148         &self.evidence
    149     }
    150 
    151     pub(crate) fn snapshot(&self) -> &TraversalSnapshot {
    152         &self.snapshot
    153     }
    154 
    155     pub(crate) fn revalidate(&self) -> Result<(), ArtifactIoError> {
    156         let changed = || ArtifactIoError::new(ArtifactIoFailureKind::ChangedDuringRead);
    157         if output_directory_identity(identity(&self.parent_descriptor).map_err(|_| changed())?)
    158             != self.parent_identity
    159         {
    160             return Err(ArtifactIoError::new(
    161                 ArtifactIoFailureKind::ChangedDuringRead,
    162             ));
    163         }
    164         if output_directory_identity(identity(&self.root_descriptor).map_err(|_| changed())?)
    165             != self.root_identity
    166         {
    167             return Err(ArtifactIoError::new(
    168                 ArtifactIoFailureKind::ChangedDuringRead,
    169             ));
    170         }
    171         let (current_root, current_root_chain) =
    172             open_absolute_directory(self.root()).map_err(|_| changed())?;
    173         if output_directory_identity(identity(&current_root).map_err(|_| changed())?)
    174             != self.root_identity
    175             || current_root_chain != self.root_chain
    176         {
    177             return Err(ArtifactIoError::new(
    178                 ArtifactIoFailureKind::ChangedDuringRead,
    179             ));
    180         }
    181         let (_, current_parent_chain) =
    182             open_trusted_output_directory(&self.parent_path).map_err(|_| changed())?;
    183         if current_parent_chain != self.parent_chain {
    184             return Err(ArtifactIoError::new(
    185                 ArtifactIoFailureKind::ChangedDuringRead,
    186             ));
    187         }
    188         self.snapshot.revalidate().map_err(|_| changed())
    189     }
    190 }
    191 
    192 #[derive(Clone, Debug)]
    193 pub(crate) struct TraversedFile {
    194     relative: PathBuf,
    195     identity: FileIdentity,
    196 }
    197 
    198 impl TraversedFile {
    199     pub(crate) fn relative_path(&self) -> &Path {
    200         &self.relative
    201     }
    202 
    203     pub(crate) const fn permission_mode(&self) -> u32 {
    204         permission_mode(self.identity)
    205     }
    206 }
    207 
    208 #[derive(Debug)]
    209 pub(crate) struct TraversalSnapshot {
    210     root: PathBuf,
    211     root_chain: Vec<DirectoryIdentity>,
    212     directories: Vec<TraversedDirectory>,
    213     files: Vec<TraversedFile>,
    214     entry_count: u64,
    215     total_bytes: u64,
    216 }
    217 
    218 impl TraversalSnapshot {
    219     pub(crate) fn files(&self) -> &[TraversedFile] {
    220         &self.files
    221     }
    222 
    223     pub(crate) const fn entry_count(&self) -> u64 {
    224         self.entry_count
    225     }
    226 
    227     pub(crate) const fn total_bytes(&self) -> u64 {
    228         self.total_bytes
    229     }
    230 
    231     pub(crate) fn directories(&self) -> impl Iterator<Item = (&Path, u32)> {
    232         self.directories
    233             .iter()
    234             .map(|directory| (directory.relative.as_path(), directory.permission_mode))
    235     }
    236 
    237     pub(crate) fn root_permission_mode(&self) -> u32 {
    238         self.directories
    239             .iter()
    240             .find(|directory| directory.relative.as_os_str().is_empty())
    241             .map_or(0, |directory| directory.permission_mode)
    242     }
    243 
    244     pub(crate) fn read(
    245         &self,
    246         file: &TraversedFile,
    247         maximum: u64,
    248     ) -> Result<Vec<u8>, ArtifactIoError> {
    249         read_regular_impl(
    250             &self.root,
    251             &file.relative,
    252             maximum,
    253             Some(&file.identity),
    254             || {},
    255         )
    256     }
    257 
    258     pub(crate) fn read_evidenced(
    259         &self,
    260         file: &TraversedFile,
    261         maximum: u64,
    262     ) -> Result<(Vec<u8>, FileEvidence), ArtifactIoError> {
    263         read_regular_evidenced_impl(
    264             &self.root,
    265             &file.relative,
    266             maximum,
    267             Some(&file.identity),
    268             || {},
    269         )
    270     }
    271 
    272     pub(crate) fn hash(
    273         &self,
    274         file: &TraversedFile,
    275         maximum: u64,
    276     ) -> Result<FileEvidence, ArtifactIoError> {
    277         with_regular(
    278             &self.root,
    279             &file.relative,
    280             maximum,
    281             Some(&file.identity),
    282             || {},
    283             |input, admitted_length| {
    284                 let evidence = stream_regular(input, maximum, None)?;
    285                 require_observed_length(evidence.byte_length, admitted_length)?;
    286                 Ok(evidence)
    287             },
    288         )
    289     }
    290 
    291     pub(crate) fn admit_deterministic_tar_gzip(
    292         &self,
    293         file: &TraversedFile,
    294         limits: TarGzipLimits,
    295     ) -> Result<ArchiveEvidence, ArtifactIoError> {
    296         admit_tar_gzip_relative(
    297             &self.root,
    298             &file.relative,
    299             limits,
    300             Some(&file.identity),
    301             TarGzipPolicy::DeterministicSnapshot,
    302         )
    303     }
    304 
    305     pub(crate) fn materialize_deterministic_tar_gzip(
    306         &self,
    307         file: &TraversedFile,
    308         trusted_parent: &Path,
    309         limits: TarGzipLimits,
    310     ) -> Result<MaterializedArchive, ArtifactIoError> {
    311         materialize_tar_gzip_relative(
    312             &self.root,
    313             &file.relative,
    314             trusted_parent,
    315             limits,
    316             Some(&file.identity),
    317             TarGzipPolicy::DeterministicSnapshot,
    318         )
    319     }
    320 
    321     pub(crate) fn copy_to_new_path(
    322         &self,
    323         file: &TraversedFile,
    324         output: &Path,
    325         maximum: u64,
    326     ) -> Result<FileEvidence, ArtifactIoError> {
    327         copy_regular_to_new_path_impl(
    328             &self.root.join(&file.relative),
    329             output,
    330             maximum,
    331             Some(&file.identity),
    332             || {},
    333             || {},
    334         )
    335     }
    336 
    337     pub(crate) fn revalidate(&self) -> Result<(), ArtifactIoError> {
    338         let (_, root_chain) = open_absolute_directory(&self.root)?;
    339         if root_chain != self.root_chain {
    340             return Err(ArtifactIoError::new(
    341                 ArtifactIoFailureKind::ChangedDuringRead,
    342             ));
    343         }
    344         for directory in &self.directories {
    345             let opened = if directory.relative.as_os_str().is_empty() {
    346                 let (file, chain) = open_absolute_directory(&self.root)?;
    347                 OpenedObject {
    348                     identity: identity(&file)?,
    349                     file,
    350                     chain,
    351                 }
    352             } else {
    353                 open_relative(&self.root, &directory.relative, ObjectKind::Directory)?
    354             };
    355             if directory_identity(opened.identity) != directory.identity
    356                 || permission_mode(opened.identity) != directory.permission_mode
    357             {
    358                 return Err(ArtifactIoError::new(
    359                     ArtifactIoFailureKind::ChangedDuringRead,
    360                 ));
    361             }
    362             revalidate_directory_members(&opened.file, &directory.members)?;
    363         }
    364         for file in &self.files {
    365             let opened = open_relative(&self.root, &file.relative, ObjectKind::Regular)?;
    366             if opened.identity != file.identity {
    367                 return Err(ArtifactIoError::new(
    368                     ArtifactIoFailureKind::ChangedDuringRead,
    369                 ));
    370             }
    371         }
    372         Ok(())
    373     }
    374 }
    375 
    376 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    377 struct FileIdentity {
    378     device: u64,
    379     inode: u64,
    380     mode: u32,
    381     owner: u32,
    382     links: u64,
    383     length: u64,
    384     modified_seconds: i64,
    385     modified_nanoseconds: i64,
    386     changed_seconds: i64,
    387     changed_nanoseconds: i64,
    388 }
    389 
    390 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    391 struct DirectoryIdentity {
    392     device: u64,
    393     inode: u64,
    394 }
    395 
    396 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    397 struct OutputDirectoryIdentity {
    398     directory: DirectoryIdentity,
    399     mode: u32,
    400     owner: u32,
    401 }
    402 
    403 #[derive(Clone, Debug, Eq, PartialEq)]
    404 struct TraversedDirectory {
    405     relative: PathBuf,
    406     identity: DirectoryIdentity,
    407     permission_mode: u32,
    408     members: Vec<DirectoryMember>,
    409 }
    410 
    411 #[derive(Clone, Debug, Eq, PartialEq)]
    412 struct DirectoryMember {
    413     name: OsString,
    414     identity: MemberIdentity,
    415 }
    416 
    417 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    418 enum MemberIdentity {
    419     Directory(DirectoryIdentity),
    420     Regular(FileIdentity),
    421 }
    422 
    423 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    424 enum ObjectKind {
    425     Directory,
    426     Regular,
    427 }
    428 
    429 struct OpenedObject {
    430     file: File,
    431     identity: FileIdentity,
    432     chain: Vec<DirectoryIdentity>,
    433 }
    434 
    435 pub(crate) fn read_regular_path(path: &Path, maximum: u64) -> Result<Vec<u8>, ArtifactIoError> {
    436     let (root, relative) = split_absolute_file(path)?;
    437     read_regular(&root, &relative, maximum)
    438 }
    439 
    440 pub(crate) fn read_regular(
    441     root: &Path,
    442     relative: &Path,
    443     maximum: u64,
    444 ) -> Result<Vec<u8>, ArtifactIoError> {
    445     read_regular_impl(root, relative, maximum, None, || {})
    446 }
    447 
    448 fn read_regular_impl<F>(
    449     root: &Path,
    450     relative: &Path,
    451     maximum: u64,
    452     expected: Option<&FileIdentity>,
    453     after_open: F,
    454 ) -> Result<Vec<u8>, ArtifactIoError>
    455 where
    456     F: FnOnce(),
    457 {
    458     read_regular_evidenced_impl(root, relative, maximum, expected, after_open)
    459         .map(|(bytes, _)| bytes)
    460 }
    461 
    462 fn read_regular_evidenced_impl<F>(
    463     root: &Path,
    464     relative: &Path,
    465     maximum: u64,
    466     expected: Option<&FileIdentity>,
    467     after_open: F,
    468 ) -> Result<(Vec<u8>, FileEvidence), ArtifactIoError>
    469 where
    470     F: FnOnce(),
    471 {
    472     if maximum == 0 || maximum > HARD_MAX_BUFFERED_READ_BYTES {
    473         return Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidRequest));
    474     }
    475     with_regular(
    476         root,
    477         relative,
    478         maximum,
    479         expected,
    480         after_open,
    481         |file, admitted_length| {
    482             let initial_capacity = usize::try_from(maximum.min(STREAM_BUFFER_BYTES as u64))
    483                 .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::InvalidRequest))?;
    484             let mut bytes = Vec::with_capacity(initial_capacity);
    485             let mut hasher = Sha256::new();
    486             let mut buffer = [0_u8; STREAM_BUFFER_BYTES];
    487             loop {
    488                 let read = file
    489                     .read(&mut buffer)
    490                     .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::IoFailure))?;
    491                 if read == 0 {
    492                     break;
    493                 }
    494                 let next = (bytes.len() as u64)
    495                     .checked_add(read as u64)
    496                     .ok_or_else(|| {
    497                         ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
    498                             LimitKind::FileBytes,
    499                         ))
    500                     })?;
    501                 if next > maximum {
    502                     return Err(ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
    503                         LimitKind::FileBytes,
    504                     )));
    505                 }
    506                 hasher.update(&buffer[..read]);
    507                 bytes.extend_from_slice(&buffer[..read]);
    508             }
    509             let byte_length = bytes.len() as u64;
    510             require_observed_length(byte_length, admitted_length)?;
    511             Ok((
    512                 bytes,
    513                 FileEvidence {
    514                     byte_length,
    515                     sha256: hex::encode(hasher.finalize()),
    516                 },
    517             ))
    518         },
    519     )
    520 }
    521 
    522 pub(crate) fn hash_regular_path(
    523     path: &Path,
    524     maximum: u64,
    525 ) -> Result<FileEvidence, ArtifactIoError> {
    526     let (root, relative) = split_absolute_file(path)?;
    527     hash_regular(&root, &relative, maximum)
    528 }
    529 
    530 pub(crate) fn hash_regular(
    531     root: &Path,
    532     relative: &Path,
    533     maximum: u64,
    534 ) -> Result<FileEvidence, ArtifactIoError> {
    535     with_regular(
    536         root,
    537         relative,
    538         maximum,
    539         None,
    540         || {},
    541         |file, admitted_length| {
    542             let evidence = stream_regular(file, maximum, None)?;
    543             require_observed_length(evidence.byte_length, admitted_length)?;
    544             Ok(evidence)
    545         },
    546     )
    547 }
    548 
    549 pub(crate) fn copy_regular_to_new_path(
    550     source: &Path,
    551     output: &Path,
    552     maximum: u64,
    553 ) -> Result<FileEvidence, ArtifactIoError> {
    554     copy_regular_to_new_path_impl(source, output, maximum, None, || {}, || {})
    555 }
    556 
    557 fn copy_regular_to_new_path_impl<H, J>(
    558     source: &Path,
    559     output: &Path,
    560     maximum: u64,
    561     expected: Option<&FileIdentity>,
    562     after_open: H,
    563     after_install: J,
    564 ) -> Result<FileEvidence, ArtifactIoError>
    565 where
    566     H: FnOnce(),
    567     J: FnOnce(),
    568 {
    569     if !output.is_absolute() || output.file_name().is_none() {
    570         return Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidRequest));
    571     }
    572     let output_parent = output
    573         .parent()
    574         .ok_or_else(|| ArtifactIoError::new(ArtifactIoFailureKind::InvalidRequest))?;
    575     let (root, relative) = split_absolute_file(source)?;
    576     copy_regular_descriptor_relative(
    577         &root,
    578         &relative,
    579         output_parent,
    580         output
    581             .file_name()
    582             .ok_or_else(|| ArtifactIoError::new(ArtifactIoFailureKind::InvalidRequest))?,
    583         maximum,
    584         expected,
    585         after_open,
    586         after_install,
    587     )
    588 }
    589 
    590 #[cfg(unix)]
    591 #[allow(clippy::too_many_arguments)]
    592 fn copy_regular_descriptor_relative<H, J>(
    593     input_root: &Path,
    594     input_relative: &Path,
    595     output_parent: &Path,
    596     output_name: &OsStr,
    597     maximum: u64,
    598     expected: Option<&FileIdentity>,
    599     after_open: H,
    600     after_install: J,
    601 ) -> Result<FileEvidence, ArtifactIoError>
    602 where
    603     H: FnOnce(),
    604     J: FnOnce(),
    605 {
    606     use rustix::fs::{Mode, OFlags, fchmod, openat};
    607 
    608     validate_single_component(output_name)?;
    609     let (parent, parent_chain) = open_trusted_output_directory(output_parent)?;
    610     let parent_identity = *parent_chain
    611         .last()
    612         .ok_or_else(|| ArtifactIoError::new(ArtifactIoFailureKind::InvalidRequest))?;
    613     let output = openat(
    614         &parent,
    615         output_name,
    616         OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::CLOEXEC | OFlags::NOFOLLOW,
    617         Mode::RUSR | Mode::WUSR,
    618     )
    619     .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::InvalidRequest))?;
    620     let mut output = File::from(output);
    621     fchmod(&output, Mode::RUSR | Mode::WUSR)
    622         .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::IoFailure))?;
    623     let initial_output = identity(&output)?;
    624     if initial_output.links != 1 || permission_mode(initial_output) != 0o600 {
    625         return Err(ArtifactIoError::new(
    626             ArtifactIoFailureKind::ChangedDuringRead,
    627         ));
    628     }
    629 
    630     let evidence = with_regular(
    631         input_root,
    632         input_relative,
    633         maximum,
    634         expected,
    635         after_open,
    636         |input, admitted_length| {
    637             let evidence = stream_regular(input, maximum, Some(&mut output))?;
    638             require_observed_length(evidence.byte_length, admitted_length)?;
    639             output
    640                 .sync_all()
    641                 .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::IoFailure))?;
    642             Ok(evidence)
    643         },
    644     )?;
    645     let finalized_identity = identity(&output)?;
    646     if finalized_identity.links != 1
    647         || finalized_identity.length != evidence.byte_length
    648         || permission_mode(finalized_identity) != 0o600
    649     {
    650         return Err(ArtifactIoError::new(
    651             ArtifactIoFailureKind::ChangedDuringRead,
    652         ));
    653     }
    654     if parent.sync_all().is_err() {
    655         return Err(ArtifactIoError::new(ArtifactIoFailureKind::IoFailure));
    656     }
    657     after_install();
    658     let installed_identity = identity(&output)?;
    659     if installed_identity != finalized_identity {
    660         return Err(ArtifactIoError::new(
    661             ArtifactIoFailureKind::ChangedDuringRead,
    662         ));
    663     }
    664     let rebound = open_at(&parent, output_name);
    665     if rebound.as_ref().ok().and_then(|file| identity(file).ok()) != Some(finalized_identity) {
    666         return Err(ArtifactIoError::new(
    667             ArtifactIoFailureKind::ChangedDuringRead,
    668         ));
    669     }
    670     let retained_parent_identity = output_directory_identity(identity(&parent)?);
    671     let current_parent_chain = open_trusted_output_directory(output_parent)
    672         .map(|(_, chain)| chain)
    673         .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::ChangedDuringRead))?;
    674     if retained_parent_identity != parent_identity || current_parent_chain != parent_chain {
    675         return Err(ArtifactIoError::new(
    676             ArtifactIoFailureKind::ChangedDuringRead,
    677         ));
    678     }
    679     Ok(evidence)
    680 }
    681 
    682 #[cfg(not(unix))]
    683 #[allow(clippy::too_many_arguments)]
    684 fn copy_regular_descriptor_relative<H, J>(
    685     _input_root: &Path,
    686     _input_relative: &Path,
    687     _output_parent: &Path,
    688     _output_name: &OsStr,
    689     _maximum: u64,
    690     _expected: Option<&FileIdentity>,
    691     _after_open: H,
    692     _after_install: J,
    693 ) -> Result<FileEvidence, ArtifactIoError>
    694 where
    695     H: FnOnce(),
    696     J: FnOnce(),
    697 {
    698     Err(ArtifactIoError::new(
    699         ArtifactIoFailureKind::UnsupportedPlatform,
    700     ))
    701 }
    702 
    703 fn require_observed_length(observed: u64, admitted: u64) -> Result<(), ArtifactIoError> {
    704     if observed == admitted {
    705         Ok(())
    706     } else {
    707         Err(ArtifactIoError::new(
    708             ArtifactIoFailureKind::ChangedDuringRead,
    709         ))
    710     }
    711 }
    712 
    713 const fn output_directory_identity(identity: FileIdentity) -> OutputDirectoryIdentity {
    714     OutputDirectoryIdentity {
    715         directory: directory_identity(identity),
    716         mode: permission_mode(identity),
    717         owner: identity.owner,
    718     }
    719 }
    720 
    721 #[cfg(unix)]
    722 fn output_directory_is_trusted(identity: OutputDirectoryIdentity) -> bool {
    723     (identity.owner == 0 || identity.owner == rustix::process::geteuid().as_raw())
    724         && identity.mode & 0o022 == 0
    725 }
    726 
    727 fn stream_regular(
    728     file: &mut File,
    729     maximum: u64,
    730     mut output: Option<&mut File>,
    731 ) -> Result<FileEvidence, ArtifactIoError> {
    732     let mut hasher = Sha256::new();
    733     let mut total = 0_u64;
    734     let mut buffer = [0_u8; STREAM_BUFFER_BYTES];
    735     loop {
    736         let read = file
    737             .read(&mut buffer)
    738             .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::IoFailure))?;
    739         if read == 0 {
    740             break;
    741         }
    742         total = total
    743             .checked_add(read as u64)
    744             .ok_or_else(|| ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject))?;
    745         if total > maximum {
    746             return Err(ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
    747                 LimitKind::FileBytes,
    748             )));
    749         }
    750         hasher.update(&buffer[..read]);
    751         if let Some(destination) = output.as_deref_mut() {
    752             destination
    753                 .write_all(&buffer[..read])
    754                 .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::IoFailure))?;
    755         }
    756     }
    757     Ok(FileEvidence {
    758         byte_length: total,
    759         sha256: hex::encode(hasher.finalize()),
    760     })
    761 }
    762 
    763 fn with_regular<T, F, H>(
    764     root: &Path,
    765     relative: &Path,
    766     maximum: u64,
    767     expected: Option<&FileIdentity>,
    768     after_open: H,
    769     operation: F,
    770 ) -> Result<T, ArtifactIoError>
    771 where
    772     F: FnOnce(&mut File, u64) -> Result<T, ArtifactIoError>,
    773     H: FnOnce(),
    774 {
    775     if maximum == 0 || maximum > HARD_MAX_STREAM_FILE_BYTES {
    776         return Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidRequest));
    777     }
    778     let mut opened = open_relative(root, relative, ObjectKind::Regular)?;
    779     if opened.identity.length > maximum {
    780         return Err(ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
    781             LimitKind::FileBytes,
    782         )));
    783     }
    784     if opened.identity.links != 1 {
    785         return Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject));
    786     }
    787     if expected.is_some_and(|identity| *identity != opened.identity) {
    788         return Err(ArtifactIoError::new(
    789             ArtifactIoFailureKind::ChangedDuringRead,
    790         ));
    791     }
    792     let before = opened.identity;
    793     let chain = opened.chain.clone();
    794     after_open();
    795     let result = operation(&mut opened.file, before.length)?;
    796     let after = identity(&opened.file)?;
    797     let rebound = open_relative(root, relative, ObjectKind::Regular)?;
    798     if after != before || rebound.identity != before || rebound.chain != chain {
    799         return Err(ArtifactIoError::new(
    800             ArtifactIoFailureKind::ChangedDuringRead,
    801         ));
    802     }
    803     Ok(result)
    804 }
    805 
    806 pub(crate) fn traverse_regular_files(
    807     root: &Path,
    808     limits: TraversalLimits,
    809     excluded_directory_names: &[&str],
    810 ) -> Result<TraversalSnapshot, ArtifactIoError> {
    811     traverse_regular_files_impl(root, limits, excluded_directory_names, || {})
    812 }
    813 
    814 fn traverse_regular_files_impl<F>(
    815     root: &Path,
    816     limits: TraversalLimits,
    817     excluded_directory_names: &[&str],
    818     after_walk: F,
    819 ) -> Result<TraversalSnapshot, ArtifactIoError>
    820 where
    821     F: FnOnce(),
    822 {
    823     validate_traversal_limits(limits)?;
    824     if excluded_directory_names.iter().any(|name| {
    825         name.is_empty()
    826             || matches!(*name, "." | "..")
    827             || name.as_bytes().contains(&b'/')
    828             || name.as_bytes().contains(&b'\\')
    829             || name.as_bytes().contains(&0)
    830     }) {
    831         return Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidRequest));
    832     }
    833     let (root_file, root_chain) = open_absolute_directory(root)?;
    834     let mut state = TraversalState {
    835         limits,
    836         exclusions: excluded_directory_names,
    837         entries: 0,
    838         files: Vec::new(),
    839         directories: Vec::new(),
    840         total_bytes: 0,
    841     };
    842     let root_identity = identity(&root_file)?;
    843     walk_directory(&root_file, Path::new(""), 0, root_identity, &mut state)?;
    844     after_walk();
    845     let snapshot = TraversalSnapshot {
    846         root: root.to_path_buf(),
    847         root_chain,
    848         directories: state.directories,
    849         files: state.files,
    850         entry_count: state.entries,
    851         total_bytes: state.total_bytes,
    852     };
    853     snapshot.revalidate()?;
    854     Ok(snapshot)
    855 }
    856 
    857 struct TraversalState<'a> {
    858     limits: TraversalLimits,
    859     exclusions: &'a [&'a str],
    860     entries: u64,
    861     files: Vec<TraversedFile>,
    862     directories: Vec<TraversedDirectory>,
    863     total_bytes: u64,
    864 }
    865 
    866 fn validate_traversal_limits(limits: TraversalLimits) -> Result<(), ArtifactIoError> {
    867     if limits.max_entries == 0
    868         || limits.max_entries > HARD_MAX_TRAVERSAL_ENTRIES
    869         || limits.max_files == 0
    870         || limits.max_files > HARD_MAX_TRAVERSAL_FILES
    871         || limits.max_total_bytes == 0
    872         || limits.max_total_bytes > HARD_MAX_TRAVERSAL_TOTAL_BYTES
    873         || limits.max_file_bytes == 0
    874         || limits.max_file_bytes > HARD_MAX_STREAM_FILE_BYTES
    875         || limits.max_depth == 0
    876         || limits.max_depth > HARD_MAX_TRAVERSAL_DEPTH
    877         || limits.max_path_bytes == 0
    878         || limits.max_path_bytes > HARD_MAX_PATH_BYTES
    879     {
    880         Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidRequest))
    881     } else {
    882         Ok(())
    883     }
    884 }
    885 
    886 #[cfg(unix)]
    887 fn walk_directory(
    888     directory: &File,
    889     relative: &Path,
    890     depth: usize,
    891     current_identity: FileIdentity,
    892     state: &mut TraversalState<'_>,
    893 ) -> Result<(), ArtifactIoError> {
    894     use std::ffi::OsStr;
    895     use std::os::unix::ffi::OsStrExt as _;
    896 
    897     let mut reader = rustix::fs::Dir::read_from(directory)
    898         .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::IoFailure))?;
    899     let mut names = Vec::new();
    900     while let Some(entry) = reader.read() {
    901         let entry = entry.map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::IoFailure))?;
    902         let name = entry.file_name().to_bytes();
    903         if matches!(name, b"." | b"..") {
    904             continue;
    905         }
    906         state.entries = state.entries.checked_add(1).ok_or_else(|| {
    907             ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
    908                 LimitKind::TraversalEntries,
    909             ))
    910         })?;
    911         if state.entries > state.limits.max_entries {
    912             return Err(ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
    913                 LimitKind::TraversalEntries,
    914             )));
    915         }
    916         names.push(name.to_vec());
    917     }
    918     names.sort();
    919 
    920     let mut members = Vec::with_capacity(names.len());
    921     for name in names {
    922         if name.is_empty() || name.contains(&0) || name.contains(&b'/') {
    923             return Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject));
    924         }
    925         let component = OsStr::from_bytes(&name);
    926         let child_relative = relative.join(component);
    927         if path_byte_len(&child_relative) > state.limits.max_path_bytes {
    928             return Err(ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
    929                 LimitKind::TraversalPathBytes,
    930             )));
    931         }
    932         let child = open_at(directory, component)?;
    933         let metadata = child
    934             .metadata()
    935             .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::IoFailure))?;
    936         let child_identity = identity_from_metadata(&metadata);
    937         if metadata.is_dir() {
    938             let child_directory_identity = directory_identity(child_identity);
    939             members.push(DirectoryMember {
    940                 name: component.to_os_string(),
    941                 identity: MemberIdentity::Directory(child_directory_identity),
    942             });
    943             let next_depth = depth.checked_add(1).ok_or_else(|| {
    944                 ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
    945                     LimitKind::TraversalDepth,
    946                 ))
    947             })?;
    948             if next_depth > state.limits.max_depth {
    949                 return Err(ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
    950                     LimitKind::TraversalDepth,
    951                 )));
    952             }
    953             if state
    954                 .exclusions
    955                 .iter()
    956                 .any(|excluded| name == excluded.as_bytes())
    957             {
    958                 continue;
    959             }
    960             walk_directory(&child, &child_relative, next_depth, child_identity, state)?;
    961         } else if metadata.is_file() {
    962             if child_identity.links != 1 {
    963                 return Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject));
    964             }
    965             if child_identity.length > state.limits.max_file_bytes {
    966                 return Err(ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
    967                     LimitKind::FileBytes,
    968                 )));
    969             }
    970             let file_count = state.files.len().checked_add(1).ok_or_else(|| {
    971                 ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
    972                     LimitKind::TraversalFiles,
    973                 ))
    974             })?;
    975             if file_count as u64 > state.limits.max_files {
    976                 return Err(ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
    977                     LimitKind::TraversalFiles,
    978                 )));
    979             }
    980             state.total_bytes = state
    981                 .total_bytes
    982                 .checked_add(child_identity.length)
    983                 .ok_or_else(|| {
    984                     ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
    985                         LimitKind::TraversalTotalBytes,
    986                     ))
    987                 })?;
    988             if state.total_bytes > state.limits.max_total_bytes {
    989                 return Err(ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
    990                     LimitKind::TraversalTotalBytes,
    991                 )));
    992             }
    993             state.files.push(TraversedFile {
    994                 relative: child_relative,
    995                 identity: child_identity,
    996             });
    997             members.push(DirectoryMember {
    998                 name: component.to_os_string(),
    999                 identity: MemberIdentity::Regular(child_identity),
   1000             });
   1001         } else {
   1002             return Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject));
   1003         }
   1004     }
   1005     state.directories.push(TraversedDirectory {
   1006         relative: relative.to_path_buf(),
   1007         identity: directory_identity(current_identity),
   1008         permission_mode: permission_mode(current_identity),
   1009         members,
   1010     });
   1011     Ok(())
   1012 }
   1013 
   1014 #[cfg(unix)]
   1015 fn revalidate_directory_members(
   1016     directory: &File,
   1017     expected: &[DirectoryMember],
   1018 ) -> Result<(), ArtifactIoError> {
   1019     use std::os::unix::ffi::OsStrExt as _;
   1020 
   1021     let changed = || ArtifactIoError::new(ArtifactIoFailureKind::ChangedDuringRead);
   1022     let mut reader = rustix::fs::Dir::read_from(directory).map_err(|_| changed())?;
   1023     let mut names = Vec::with_capacity(expected.len());
   1024     while let Some(entry) = reader.read() {
   1025         let entry = entry.map_err(|_| changed())?;
   1026         let name = entry.file_name().to_bytes();
   1027         if matches!(name, b"." | b"..") {
   1028             continue;
   1029         }
   1030         if names.len() == expected.len() {
   1031             return Err(changed());
   1032         }
   1033         names.push(OsString::from(OsStr::from_bytes(name)));
   1034     }
   1035     names.sort();
   1036     if names.len() != expected.len()
   1037         || names
   1038             .iter()
   1039             .zip(expected)
   1040             .any(|(name, member)| name != &member.name)
   1041     {
   1042         return Err(changed());
   1043     }
   1044     for member in expected {
   1045         let child = open_at(directory, &member.name).map_err(|_| changed())?;
   1046         let metadata = child.metadata().map_err(|_| changed())?;
   1047         let identity = identity_from_metadata(&metadata);
   1048         let observed = if metadata.is_dir() {
   1049             MemberIdentity::Directory(directory_identity(identity))
   1050         } else if metadata.is_file() && identity.links == 1 {
   1051             MemberIdentity::Regular(identity)
   1052         } else {
   1053             return Err(changed());
   1054         };
   1055         if observed != member.identity {
   1056             return Err(changed());
   1057         }
   1058     }
   1059     Ok(())
   1060 }
   1061 
   1062 #[cfg(not(unix))]
   1063 fn revalidate_directory_members(
   1064     _directory: &File,
   1065     _expected: &[DirectoryMember],
   1066 ) -> Result<(), ArtifactIoError> {
   1067     Err(ArtifactIoError::new(
   1068         ArtifactIoFailureKind::UnsupportedPlatform,
   1069     ))
   1070 }
   1071 
   1072 #[cfg(not(unix))]
   1073 fn walk_directory(
   1074     _directory: &File,
   1075     _relative: &Path,
   1076     _depth: usize,
   1077     _current_identity: FileIdentity,
   1078     _state: &mut TraversalState<'_>,
   1079 ) -> Result<(), ArtifactIoError> {
   1080     Err(ArtifactIoError::new(
   1081         ArtifactIoFailureKind::UnsupportedPlatform,
   1082     ))
   1083 }
   1084 
   1085 pub(crate) fn admit_tar_gzip_path(
   1086     path: &Path,
   1087     limits: TarGzipLimits,
   1088 ) -> Result<ArchiveEvidence, ArtifactIoError> {
   1089     let (root, relative) = split_absolute_file(path)?;
   1090     admit_tar_gzip_relative(&root, &relative, limits, None, TarGzipPolicy::Generic)
   1091 }
   1092 
   1093 pub(crate) fn materialize_tar_gzip_path(
   1094     path: &Path,
   1095     trusted_parent: &Path,
   1096     limits: TarGzipLimits,
   1097 ) -> Result<MaterializedArchive, ArtifactIoError> {
   1098     let (root, relative) = split_absolute_file(path)?;
   1099     materialize_tar_gzip_relative(
   1100         &root,
   1101         &relative,
   1102         trusted_parent,
   1103         limits,
   1104         None,
   1105         TarGzipPolicy::Generic,
   1106     )
   1107 }
   1108 
   1109 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
   1110 enum TarGzipPolicy {
   1111     Generic,
   1112     DeterministicSnapshot,
   1113 }
   1114 
   1115 fn admit_tar_gzip_relative(
   1116     root: &Path,
   1117     relative: &Path,
   1118     limits: TarGzipLimits,
   1119     expected: Option<&FileIdentity>,
   1120     policy: TarGzipPolicy,
   1121 ) -> Result<ArchiveEvidence, ArtifactIoError> {
   1122     validate_archive_limits(limits)?;
   1123     with_regular(
   1124         root,
   1125         relative,
   1126         limits.max_compressed_bytes,
   1127         expected,
   1128         || {},
   1129         |file, admitted_length| {
   1130             let evidence = admit_tar_gzip_reader(file, limits, policy, None)?;
   1131             require_observed_length(evidence.compressed.byte_length, admitted_length)?;
   1132             Ok(evidence)
   1133         },
   1134     )
   1135     .map_err(|error| match error.kind() {
   1136         ArtifactIoFailureKind::LimitExceeded(LimitKind::FileBytes) => ArtifactIoError::new(
   1137             ArtifactIoFailureKind::LimitExceeded(LimitKind::ArchiveCompressedBytes),
   1138         ),
   1139         _ => error,
   1140     })
   1141 }
   1142 
   1143 fn materialize_tar_gzip_relative(
   1144     root: &Path,
   1145     relative: &Path,
   1146     trusted_parent: &Path,
   1147     limits: TarGzipLimits,
   1148     expected: Option<&FileIdentity>,
   1149     policy: TarGzipPolicy,
   1150 ) -> Result<MaterializedArchive, ArtifactIoError> {
   1151     validate_archive_limits(limits)?;
   1152     let (parent_descriptor, parent_chain) = open_trusted_output_directory(trusted_parent)?;
   1153     let parent_identity = *parent_chain
   1154         .last()
   1155         .ok_or_else(|| ArtifactIoError::new(ArtifactIoFailureKind::InvalidRequest))?;
   1156     let mut directory_builder = tempfile::Builder::new();
   1157     directory_builder.prefix("radroots-advisory-materialized-");
   1158     #[cfg(unix)]
   1159     {
   1160         use std::os::unix::fs::PermissionsExt as _;
   1161 
   1162         directory_builder.permissions(fs::Permissions::from_mode(0o700));
   1163     }
   1164     let directory = directory_builder
   1165         .tempdir_in(trusted_parent)
   1166         .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::IoFailure))?;
   1167     let (_, rebound_parent_chain) = open_trusted_output_directory(trusted_parent)?;
   1168     if rebound_parent_chain != parent_chain
   1169         || output_directory_identity(identity(&parent_descriptor)?) != parent_identity
   1170     {
   1171         return Err(ArtifactIoError::new(
   1172             ArtifactIoFailureKind::ChangedDuringRead,
   1173         ));
   1174     }
   1175     let (output, root_chain) = open_absolute_directory(directory.path())?;
   1176     let output_identity = identity(&output)?;
   1177     let parent_directory_chain = parent_chain
   1178         .iter()
   1179         .map(|identity| identity.directory)
   1180         .collect::<Vec<_>>();
   1181     if root_chain.len() != parent_directory_chain.len().saturating_add(1)
   1182         || root_chain[..parent_directory_chain.len()] != parent_directory_chain
   1183     {
   1184         return Err(ArtifactIoError::new(
   1185             ArtifactIoFailureKind::ChangedDuringRead,
   1186         ));
   1187     }
   1188     #[cfg(unix)]
   1189     if output_identity.owner != rustix::process::geteuid().as_raw()
   1190         || permission_mode(output_identity) != 0o700
   1191     {
   1192         return Err(ArtifactIoError::new(
   1193             ArtifactIoFailureKind::ChangedDuringRead,
   1194         ));
   1195     }
   1196     parent_descriptor
   1197         .sync_all()
   1198         .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::IoFailure))?;
   1199     let evidence = with_regular(
   1200         root,
   1201         relative,
   1202         limits.max_compressed_bytes,
   1203         expected,
   1204         || {},
   1205         |file, admitted_length| {
   1206             let evidence = admit_tar_gzip_reader(file, limits, policy, Some(&output))?;
   1207             require_observed_length(evidence.compressed.byte_length, admitted_length)?;
   1208             output
   1209                 .sync_all()
   1210                 .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::IoFailure))?;
   1211             Ok(evidence)
   1212         },
   1213     )
   1214     .map_err(|error| match error.kind() {
   1215         ArtifactIoFailureKind::LimitExceeded(LimitKind::FileBytes) => ArtifactIoError::new(
   1216             ArtifactIoFailureKind::LimitExceeded(LimitKind::ArchiveCompressedBytes),
   1217         ),
   1218         _ => error,
   1219     })?;
   1220     let materialized_snapshot = traverse_regular_files(
   1221         directory.path(),
   1222         TraversalLimits {
   1223             max_entries: limits.max_members,
   1224             max_files: limits.max_members,
   1225             max_total_bytes: limits.max_payload_bytes,
   1226             max_file_bytes: limits.max_member_bytes,
   1227             max_depth: limits.max_depth,
   1228             max_path_bytes: limits.max_path_bytes,
   1229         },
   1230         &[],
   1231     )?;
   1232     if materialized_snapshot.entry_count() != evidence.member_count
   1233         || materialized_snapshot.total_bytes() != evidence.payload_bytes
   1234     {
   1235         return Err(ArtifactIoError::new(
   1236             ArtifactIoFailureKind::ChangedDuringRead,
   1237         ));
   1238     }
   1239     materialized_snapshot.revalidate()?;
   1240     parent_descriptor
   1241         .sync_all()
   1242         .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::IoFailure))?;
   1243     let materialized = MaterializedArchive {
   1244         root_descriptor: output,
   1245         root_identity: output_directory_identity(output_identity),
   1246         root_chain,
   1247         parent_descriptor,
   1248         parent_identity,
   1249         parent_path: trusted_parent.to_owned(),
   1250         parent_chain,
   1251         snapshot: materialized_snapshot,
   1252         evidence,
   1253         directory,
   1254     };
   1255     materialized.revalidate()?;
   1256     Ok(materialized)
   1257 }
   1258 
   1259 fn validate_archive_limits(limits: TarGzipLimits) -> Result<(), ArtifactIoError> {
   1260     if limits.max_compressed_bytes == 0
   1261         || limits.max_compressed_bytes > HARD_MAX_ARCHIVE_COMPRESSED_BYTES
   1262         || limits.max_expanded_bytes == 0
   1263         || limits.max_expanded_bytes > HARD_MAX_ARCHIVE_EXPANDED_BYTES
   1264         || limits.max_members == 0
   1265         || limits.max_members > HARD_MAX_ARCHIVE_MEMBERS
   1266         || limits.max_member_bytes == 0
   1267         || limits.max_member_bytes > HARD_MAX_ARCHIVE_MEMBER_BYTES
   1268         || limits.max_payload_bytes == 0
   1269         || limits.max_payload_bytes > HARD_MAX_ARCHIVE_PAYLOAD_BYTES
   1270         || limits.max_depth == 0
   1271         || limits.max_depth > HARD_MAX_TRAVERSAL_DEPTH
   1272         || limits.max_path_bytes == 0
   1273         || limits.max_path_bytes > HARD_MAX_PATH_BYTES
   1274     {
   1275         Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidRequest))
   1276     } else {
   1277         Ok(())
   1278     }
   1279 }
   1280 
   1281 fn admit_tar_gzip_reader(
   1282     file: &mut File,
   1283     limits: TarGzipLimits,
   1284     policy: TarGzipPolicy,
   1285     materialization_root: Option<&File>,
   1286 ) -> Result<ArchiveEvidence, ArtifactIoError> {
   1287     let expected_compressed_bytes = file
   1288         .metadata()
   1289         .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::IoFailure))?
   1290         .len();
   1291     let compressed = HashingLimitReader::new(file, limits.max_compressed_bytes);
   1292     let mut buffered = BufReader::new(compressed);
   1293     validate_gzip_header(&mut buffered, policy)?;
   1294     let decoder = GzDecoder::new(buffered);
   1295     let expanded = LimitReader::new(decoder, limits.max_expanded_bytes);
   1296     let mut archive = tar::Archive::new(expanded);
   1297     let mut names = BTreeMap::<Vec<u8>, ArchiveMemberKind>::new();
   1298     let mut members = 0_u64;
   1299     let mut payload = 0_u64;
   1300     let mut previous_name = None::<Vec<u8>>;
   1301     let mut canonical = (policy == TarGzipPolicy::DeterministicSnapshot).then(|| {
   1302         GzBuilder::new().mtime(0).operating_system(255).write(
   1303             HashingLimitWriter::new(limits.max_compressed_bytes),
   1304             Compression::new(CANONICAL_GZIP_COMPRESSION_LEVEL),
   1305         )
   1306     });
   1307 
   1308     let parse_result = (|| {
   1309         let entries = archive
   1310             .entries()
   1311             .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::MalformedArchive))?
   1312             .raw(true);
   1313         for entry in entries {
   1314             let mut entry =
   1315                 entry.map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::MalformedArchive))?;
   1316             members = members.checked_add(1).ok_or_else(|| {
   1317                 ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
   1318                     LimitKind::ArchiveMembers,
   1319                 ))
   1320             })?;
   1321             if members > limits.max_members {
   1322                 return Err(ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
   1323                     LimitKind::ArchiveMembers,
   1324                 )));
   1325             }
   1326             let declared = entry
   1327                 .header()
   1328                 .size()
   1329                 .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::MalformedArchive))?;
   1330             if declared > limits.max_member_bytes {
   1331                 return Err(ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
   1332                     LimitKind::ArchiveMemberBytes,
   1333                 )));
   1334             }
   1335             let member_kind = match entry.header().entry_type().as_byte() {
   1336                 0 | b'0' => ArchiveMemberKind::File,
   1337                 b'5' if declared == 0 => ArchiveMemberKind::Directory,
   1338                 _ => {
   1339                     return Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject));
   1340                 }
   1341             };
   1342             let name = entry.path_bytes().into_owned();
   1343             validate_archive_path(&name, limits.max_depth, limits.max_path_bytes)?;
   1344             let canonical_header = if policy == TarGzipPolicy::DeterministicSnapshot {
   1345                 let header = validate_deterministic_tar_header(
   1346                     entry.header(),
   1347                     member_kind,
   1348                     &name,
   1349                     declared,
   1350                 )?;
   1351                 if previous_name
   1352                     .as_ref()
   1353                     .is_some_and(|previous| previous >= &name)
   1354                 {
   1355                     return Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject));
   1356                 }
   1357                 previous_name = Some(name.clone());
   1358                 Some(header)
   1359             } else {
   1360                 None
   1361             };
   1362             if match member_kind {
   1363                 ArchiveMemberKind::Directory => !name.ends_with(b"/"),
   1364                 ArchiveMemberKind::File => name.ends_with(b"/"),
   1365             } {
   1366                 return Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject));
   1367             }
   1368             admit_archive_name(&mut names, &name, member_kind)?;
   1369             if let (Some(encoder), Some(header)) = (canonical.as_mut(), canonical_header.as_ref()) {
   1370                 write_canonical_archive_bytes(encoder, header.as_bytes())?;
   1371             }
   1372             payload = payload.checked_add(declared).ok_or_else(|| {
   1373                 ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
   1374                     LimitKind::ArchivePayloadBytes,
   1375                 ))
   1376             })?;
   1377             if payload > limits.max_payload_bytes {
   1378                 return Err(ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
   1379                     LimitKind::ArchivePayloadBytes,
   1380                 )));
   1381             }
   1382             let mut materialized = materialization_root
   1383                 .map(|root| materialize_archive_member(root, member_kind, &name))
   1384                 .transpose()?
   1385                 .flatten();
   1386             let mut actual = 0_u64;
   1387             let mut buffer = [0_u8; STREAM_BUFFER_BYTES];
   1388             loop {
   1389                 let read = entry
   1390                     .read(&mut buffer)
   1391                     .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::MalformedArchive))?;
   1392                 if read == 0 {
   1393                     break;
   1394                 }
   1395                 actual = actual.checked_add(read as u64).ok_or_else(|| {
   1396                     ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
   1397                         LimitKind::ArchiveMemberBytes,
   1398                     ))
   1399                 })?;
   1400                 if actual > limits.max_member_bytes {
   1401                     return Err(ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
   1402                         LimitKind::ArchiveMemberBytes,
   1403                     )));
   1404                 }
   1405                 if let Some(destination) = materialized.as_mut() {
   1406                     destination
   1407                         .write_all(&buffer[..read])
   1408                         .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::IoFailure))?;
   1409                 }
   1410                 if let Some(encoder) = canonical.as_mut() {
   1411                     write_canonical_archive_bytes(encoder, &buffer[..read])?;
   1412                 }
   1413             }
   1414             if actual != declared {
   1415                 return Err(ArtifactIoError::new(
   1416                     ArtifactIoFailureKind::MalformedArchive,
   1417                 ));
   1418             }
   1419             if let Some(encoder) = canonical.as_mut() {
   1420                 let padding = (TAR_BLOCK_BYTES as u64 - actual % TAR_BLOCK_BYTES as u64)
   1421                     % TAR_BLOCK_BYTES as u64;
   1422                 write_canonical_archive_bytes(
   1423                     encoder,
   1424                     &[0_u8; TAR_BLOCK_BYTES][..padding as usize],
   1425                 )?;
   1426             }
   1427             if let Some(destination) = materialized {
   1428                 destination
   1429                     .sync_all()
   1430                     .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::IoFailure))?;
   1431             }
   1432         }
   1433         Ok(())
   1434     })();
   1435 
   1436     let mut expanded = archive.into_inner();
   1437     if expanded.exceeded() {
   1438         return Err(ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
   1439             LimitKind::ArchiveExpandedBytes,
   1440         )));
   1441     }
   1442     if compressed_limit_exceeded(&expanded) {
   1443         return Err(ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
   1444             LimitKind::ArchiveCompressedBytes,
   1445         )));
   1446     }
   1447     parse_result?;
   1448 
   1449     if let Some(encoder) = canonical.as_mut() {
   1450         write_canonical_archive_bytes(encoder, &[0_u8; TAR_TERMINATOR_BYTES])?;
   1451     }
   1452 
   1453     let mut trailing = [0_u8; STREAM_BUFFER_BYTES];
   1454     let mut trailing_zero_bytes = 0_u64;
   1455     loop {
   1456         let read = expanded
   1457             .read(&mut trailing)
   1458             .map_err(|_| classify_expanded_error(&expanded))?;
   1459         if read == 0 {
   1460             break;
   1461         }
   1462         if trailing[..read].iter().any(|byte| *byte != 0) {
   1463             return Err(ArtifactIoError::new(
   1464                 ArtifactIoFailureKind::MalformedArchive,
   1465             ));
   1466         }
   1467         trailing_zero_bytes = trailing_zero_bytes
   1468             .checked_add(read as u64)
   1469             .ok_or_else(|| {
   1470                 ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
   1471                     LimitKind::ArchiveExpandedBytes,
   1472                 ))
   1473             })?;
   1474     }
   1475     if expanded.exceeded() {
   1476         return Err(ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
   1477             LimitKind::ArchiveExpandedBytes,
   1478         )));
   1479     }
   1480     if compressed_limit_exceeded(&expanded) {
   1481         return Err(ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
   1482             LimitKind::ArchiveCompressedBytes,
   1483         )));
   1484     }
   1485     if members == 0 || trailing_zero_bytes < 512 {
   1486         return Err(ArtifactIoError::new(
   1487             ArtifactIoFailureKind::MalformedArchive,
   1488         ));
   1489     }
   1490     let expanded_bytes = expanded.total();
   1491     let decoder = expanded.into_inner();
   1492     let mut buffered = decoder.into_inner();
   1493     if !buffered
   1494         .fill_buf()
   1495         .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::MalformedArchive))?
   1496         .is_empty()
   1497     {
   1498         return Err(ArtifactIoError::new(
   1499             ArtifactIoFailureKind::MalformedArchive,
   1500         ));
   1501     }
   1502     let compressed = buffered.into_inner();
   1503     if compressed.exceeded() {
   1504         return Err(ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
   1505             LimitKind::ArchiveCompressedBytes,
   1506         )));
   1507     }
   1508     let compressed_bytes = compressed.total();
   1509     if compressed_bytes != expected_compressed_bytes {
   1510         return Err(ArtifactIoError::new(
   1511             ArtifactIoFailureKind::MalformedArchive,
   1512         ));
   1513     }
   1514     let compressed_evidence = FileEvidence {
   1515         byte_length: compressed_bytes,
   1516         sha256: compressed.finalize(),
   1517     };
   1518     if let Some(encoder) = canonical {
   1519         let canonical_evidence = encoder
   1520             .finish()
   1521             .map_err(|_| {
   1522                 ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
   1523                     LimitKind::ArchiveCompressedBytes,
   1524                 ))
   1525             })?
   1526             .finalize();
   1527         if canonical_evidence != compressed_evidence {
   1528             return Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject));
   1529         }
   1530     }
   1531     Ok(ArchiveEvidence {
   1532         compressed: compressed_evidence,
   1533         expanded_bytes,
   1534         member_count: members,
   1535         payload_bytes: payload,
   1536     })
   1537 }
   1538 
   1539 #[cfg(unix)]
   1540 fn materialize_archive_member(
   1541     root: &File,
   1542     kind: ArchiveMemberKind,
   1543     raw_name: &[u8],
   1544 ) -> Result<Option<File>, ArtifactIoError> {
   1545     use rustix::fs::{Mode, OFlags, fchmod, mkdirat, openat};
   1546 
   1547     let name = std::str::from_utf8(raw_name)
   1548         .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject))?
   1549         .trim_end_matches('/');
   1550     let path = Path::new(name);
   1551     let mut components = path.components().peekable();
   1552     let mut current = root
   1553         .try_clone()
   1554         .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::IoFailure))?;
   1555     while let Some(component) = components.next() {
   1556         let Component::Normal(component) = component else {
   1557             return Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject));
   1558         };
   1559         let is_leaf = components.peek().is_none();
   1560         if !is_leaf {
   1561             current = File::from(
   1562                 openat(
   1563                     &current,
   1564                     component,
   1565                     OFlags::RDONLY
   1566                         | OFlags::DIRECTORY
   1567                         | OFlags::NOFOLLOW
   1568                         | OFlags::CLOEXEC
   1569                         | OFlags::NONBLOCK,
   1570                     Mode::empty(),
   1571                 )
   1572                 .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject))?,
   1573             );
   1574             continue;
   1575         }
   1576         return match kind {
   1577             ArchiveMemberKind::Directory => {
   1578                 mkdirat(&current, component, Mode::RUSR | Mode::WUSR | Mode::XUSR)
   1579                     .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject))?;
   1580                 let directory = File::from(
   1581                     openat(
   1582                         &current,
   1583                         component,
   1584                         OFlags::RDONLY
   1585                             | OFlags::DIRECTORY
   1586                             | OFlags::NOFOLLOW
   1587                             | OFlags::CLOEXEC
   1588                             | OFlags::NONBLOCK,
   1589                         Mode::empty(),
   1590                     )
   1591                     .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject))?,
   1592                 );
   1593                 fchmod(
   1594                     &directory,
   1595                     Mode::RUSR
   1596                         | Mode::WUSR
   1597                         | Mode::XUSR
   1598                         | Mode::RGRP
   1599                         | Mode::XGRP
   1600                         | Mode::ROTH
   1601                         | Mode::XOTH,
   1602                 )
   1603                 .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::IoFailure))?;
   1604                 directory
   1605                     .sync_all()
   1606                     .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::IoFailure))?;
   1607                 current
   1608                     .sync_all()
   1609                     .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::IoFailure))?;
   1610                 Ok(None)
   1611             }
   1612             ArchiveMemberKind::File => {
   1613                 let file = File::from(
   1614                     openat(
   1615                         &current,
   1616                         component,
   1617                         OFlags::WRONLY
   1618                             | OFlags::CREATE
   1619                             | OFlags::EXCL
   1620                             | OFlags::NOFOLLOW
   1621                             | OFlags::CLOEXEC
   1622                             | OFlags::NONBLOCK,
   1623                         Mode::RUSR | Mode::WUSR,
   1624                     )
   1625                     .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject))?,
   1626                 );
   1627                 fchmod(&file, Mode::RUSR | Mode::WUSR | Mode::RGRP | Mode::ROTH)
   1628                     .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::IoFailure))?;
   1629                 current
   1630                     .sync_all()
   1631                     .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::IoFailure))?;
   1632                 Ok(Some(file))
   1633             }
   1634         };
   1635     }
   1636     Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject))
   1637 }
   1638 
   1639 #[cfg(not(unix))]
   1640 fn materialize_archive_member(
   1641     _root: &File,
   1642     _kind: ArchiveMemberKind,
   1643     _raw_name: &[u8],
   1644 ) -> Result<Option<File>, ArtifactIoError> {
   1645     Err(ArtifactIoError::new(
   1646         ArtifactIoFailureKind::UnsupportedPlatform,
   1647     ))
   1648 }
   1649 
   1650 fn validate_gzip_header(
   1651     reader: &mut BufReader<HashingLimitReader<'_>>,
   1652     policy: TarGzipPolicy,
   1653 ) -> Result<(), ArtifactIoError> {
   1654     if reader.fill_buf().is_err() {
   1655         return Err(if reader.get_ref().exceeded() {
   1656             ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
   1657                 LimitKind::ArchiveCompressedBytes,
   1658             ))
   1659         } else {
   1660             ArtifactIoError::new(ArtifactIoFailureKind::MalformedArchive)
   1661         });
   1662     }
   1663     let header = reader.buffer();
   1664     if header.len() < 10 || header[..3] != [0x1f, 0x8b, 8] {
   1665         return Err(ArtifactIoError::new(
   1666             ArtifactIoFailureKind::MalformedArchive,
   1667         ));
   1668     }
   1669     if header[3] != 0 {
   1670         return Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject));
   1671     }
   1672     if policy == TarGzipPolicy::DeterministicSnapshot
   1673         && (header[4..8] != [0, 0, 0, 0] || header[8] != 0 || header[9] != 255)
   1674     {
   1675         return Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject));
   1676     }
   1677     Ok(())
   1678 }
   1679 
   1680 fn validate_deterministic_tar_header(
   1681     header: &tar::Header,
   1682     kind: ArchiveMemberKind,
   1683     name: &[u8],
   1684     size: u64,
   1685 ) -> Result<TarHeader, ArtifactIoError> {
   1686     let path = std::str::from_utf8(name)
   1687         .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject))?;
   1688     let mut expected = TarHeader::new_gnu();
   1689     expected
   1690         .set_path(path)
   1691         .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject))?;
   1692     expected.set_entry_type(match kind {
   1693         ArchiveMemberKind::Directory => tar::EntryType::Directory,
   1694         ArchiveMemberKind::File => tar::EntryType::Regular,
   1695     });
   1696     expected.set_mode(match kind {
   1697         ArchiveMemberKind::Directory => 0o755,
   1698         ArchiveMemberKind::File => 0o644,
   1699     });
   1700     expected.set_uid(0);
   1701     expected.set_gid(0);
   1702     expected.set_mtime(0);
   1703     expected.set_size(size);
   1704     expected.set_cksum();
   1705     if header.as_bytes() == expected.as_bytes() {
   1706         Ok(expected)
   1707     } else {
   1708         Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject))
   1709     }
   1710 }
   1711 
   1712 fn write_canonical_archive_bytes(
   1713     encoder: &mut flate2::write::GzEncoder<HashingLimitWriter>,
   1714     bytes: &[u8],
   1715 ) -> Result<(), ArtifactIoError> {
   1716     encoder.write_all(bytes).map_err(|_| {
   1717         if encoder.get_ref().exceeded() {
   1718             ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
   1719                 LimitKind::ArchiveCompressedBytes,
   1720             ))
   1721         } else {
   1722             ArtifactIoError::new(ArtifactIoFailureKind::IoFailure)
   1723         }
   1724     })
   1725 }
   1726 
   1727 fn classify_expanded_error(
   1728     reader: &LimitReader<GzDecoder<BufReader<HashingLimitReader<'_>>>>,
   1729 ) -> ArtifactIoError {
   1730     if reader.exceeded() {
   1731         ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
   1732             LimitKind::ArchiveExpandedBytes,
   1733         ))
   1734     } else if compressed_limit_exceeded(reader) {
   1735         ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
   1736             LimitKind::ArchiveCompressedBytes,
   1737         ))
   1738     } else {
   1739         ArtifactIoError::new(ArtifactIoFailureKind::MalformedArchive)
   1740     }
   1741 }
   1742 
   1743 fn compressed_limit_exceeded(
   1744     reader: &LimitReader<GzDecoder<BufReader<HashingLimitReader<'_>>>>,
   1745 ) -> bool {
   1746     reader.get_ref().get_ref().get_ref().exceeded()
   1747 }
   1748 
   1749 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
   1750 enum ArchiveMemberKind {
   1751     Directory,
   1752     File,
   1753 }
   1754 
   1755 fn validate_archive_path(
   1756     path: &[u8],
   1757     maximum_depth: usize,
   1758     maximum_bytes: usize,
   1759 ) -> Result<(), ArtifactIoError> {
   1760     if path.len() > maximum_bytes {
   1761         return Err(ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
   1762             LimitKind::ArchivePathBytes,
   1763         )));
   1764     }
   1765     if path.is_empty()
   1766         || path.starts_with(b"/")
   1767         || path.contains(&0)
   1768         || path.contains(&b'\\')
   1769         || std::str::from_utf8(path).is_err()
   1770     {
   1771         return Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject));
   1772     }
   1773     let trimmed = path.strip_suffix(b"/").unwrap_or(path);
   1774     let mut depth = 0_usize;
   1775     for component in trimmed.split(|byte| *byte == b'/') {
   1776         if component.is_empty() || matches!(component, b"." | b"..") {
   1777             return Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject));
   1778         }
   1779         depth = depth.checked_add(1).ok_or_else(|| {
   1780             ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
   1781                 LimitKind::ArchiveDepth,
   1782             ))
   1783         })?;
   1784     }
   1785     if depth > maximum_depth {
   1786         Err(ArtifactIoError::new(ArtifactIoFailureKind::LimitExceeded(
   1787             LimitKind::ArchiveDepth,
   1788         )))
   1789     } else {
   1790         Ok(())
   1791     }
   1792 }
   1793 
   1794 fn admit_archive_name(
   1795     names: &mut BTreeMap<Vec<u8>, ArchiveMemberKind>,
   1796     path: &[u8],
   1797     kind: ArchiveMemberKind,
   1798 ) -> Result<(), ArtifactIoError> {
   1799     let mut path = path.to_vec();
   1800     if kind == ArchiveMemberKind::Directory && path.ends_with(b"/") {
   1801         path.pop();
   1802     }
   1803     if names.contains_key(&path) {
   1804         return Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject));
   1805     }
   1806     let mut offset = 0_usize;
   1807     while let Some(index) = path[offset..].iter().position(|byte| *byte == b'/') {
   1808         offset = offset
   1809             .checked_add(index)
   1810             .ok_or_else(|| ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject))?;
   1811         if names
   1812             .get(&path[..offset])
   1813             .is_some_and(|existing| *existing == ArchiveMemberKind::File)
   1814         {
   1815             return Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject));
   1816         }
   1817         offset = offset
   1818             .checked_add(1)
   1819             .ok_or_else(|| ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject))?;
   1820     }
   1821     if kind == ArchiveMemberKind::File {
   1822         let mut prefix = path.clone();
   1823         prefix.push(b'/');
   1824         if names
   1825             .range(prefix.clone()..)
   1826             .next()
   1827             .is_some_and(|(candidate, _)| candidate.starts_with(&prefix))
   1828         {
   1829             return Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject));
   1830         }
   1831     }
   1832     names.insert(path, kind);
   1833     Ok(())
   1834 }
   1835 
   1836 struct LimitReader<R> {
   1837     inner: R,
   1838     maximum: u64,
   1839     total: u64,
   1840     exceeded: bool,
   1841 }
   1842 
   1843 impl<R> LimitReader<R> {
   1844     const fn new(inner: R, maximum: u64) -> Self {
   1845         Self {
   1846             inner,
   1847             maximum,
   1848             total: 0,
   1849             exceeded: false,
   1850         }
   1851     }
   1852 
   1853     const fn total(&self) -> u64 {
   1854         self.total
   1855     }
   1856 
   1857     const fn exceeded(&self) -> bool {
   1858         self.exceeded
   1859     }
   1860 
   1861     fn into_inner(self) -> R {
   1862         self.inner
   1863     }
   1864 
   1865     const fn get_ref(&self) -> &R {
   1866         &self.inner
   1867     }
   1868 }
   1869 
   1870 impl<R: Read> Read for LimitReader<R> {
   1871     fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
   1872         if output.is_empty() {
   1873             return Ok(0);
   1874         }
   1875         let remaining = self.maximum.saturating_sub(self.total);
   1876         let allowed = usize::try_from(remaining.saturating_add(1))
   1877             .unwrap_or(usize::MAX)
   1878             .min(output.len());
   1879         let read = self.inner.read(&mut output[..allowed])?;
   1880         self.total = self
   1881             .total
   1882             .checked_add(read as u64)
   1883             .ok_or_else(|| io::Error::other("bounded stream length overflow"))?;
   1884         if self.total > self.maximum {
   1885             self.exceeded = true;
   1886             Err(io::Error::other("bounded stream limit exceeded"))
   1887         } else {
   1888             Ok(read)
   1889         }
   1890     }
   1891 }
   1892 
   1893 struct HashingLimitReader<'a> {
   1894     inner: &'a mut File,
   1895     maximum: u64,
   1896     total: u64,
   1897     exceeded: bool,
   1898     hasher: Sha256,
   1899 }
   1900 
   1901 impl<'a> HashingLimitReader<'a> {
   1902     fn new(inner: &'a mut File, maximum: u64) -> Self {
   1903         Self {
   1904             inner,
   1905             maximum,
   1906             total: 0,
   1907             exceeded: false,
   1908             hasher: Sha256::new(),
   1909         }
   1910     }
   1911 
   1912     const fn total(&self) -> u64 {
   1913         self.total
   1914     }
   1915 
   1916     const fn exceeded(&self) -> bool {
   1917         self.exceeded
   1918     }
   1919 
   1920     fn finalize(self) -> String {
   1921         hex::encode(self.hasher.finalize())
   1922     }
   1923 }
   1924 
   1925 impl Read for HashingLimitReader<'_> {
   1926     fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
   1927         if output.is_empty() {
   1928             return Ok(0);
   1929         }
   1930         let remaining = self.maximum.saturating_sub(self.total);
   1931         let allowed = usize::try_from(remaining.saturating_add(1))
   1932             .unwrap_or(usize::MAX)
   1933             .min(output.len());
   1934         let read = self.inner.read(&mut output[..allowed])?;
   1935         self.total = self
   1936             .total
   1937             .checked_add(read as u64)
   1938             .ok_or_else(|| io::Error::other("bounded stream length overflow"))?;
   1939         if self.total > self.maximum {
   1940             self.exceeded = true;
   1941             return Err(io::Error::other("bounded stream limit exceeded"));
   1942         }
   1943         self.hasher.update(&output[..read]);
   1944         Ok(read)
   1945     }
   1946 }
   1947 
   1948 struct HashingLimitWriter {
   1949     maximum: u64,
   1950     total: u64,
   1951     exceeded: bool,
   1952     hasher: Sha256,
   1953 }
   1954 
   1955 impl HashingLimitWriter {
   1956     fn new(maximum: u64) -> Self {
   1957         Self {
   1958             maximum,
   1959             total: 0,
   1960             exceeded: false,
   1961             hasher: Sha256::new(),
   1962         }
   1963     }
   1964 
   1965     const fn exceeded(&self) -> bool {
   1966         self.exceeded
   1967     }
   1968 
   1969     fn finalize(self) -> FileEvidence {
   1970         FileEvidence {
   1971             byte_length: self.total,
   1972             sha256: hex::encode(self.hasher.finalize()),
   1973         }
   1974     }
   1975 }
   1976 
   1977 impl Write for HashingLimitWriter {
   1978     fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
   1979         let next = self
   1980             .total
   1981             .checked_add(bytes.len() as u64)
   1982             .ok_or_else(|| io::Error::other("bounded stream length overflow"))?;
   1983         if next > self.maximum {
   1984             self.exceeded = true;
   1985             return Err(io::Error::other("bounded stream limit exceeded"));
   1986         }
   1987         self.hasher.update(bytes);
   1988         self.total = next;
   1989         Ok(bytes.len())
   1990     }
   1991 
   1992     fn flush(&mut self) -> io::Result<()> {
   1993         Ok(())
   1994     }
   1995 }
   1996 
   1997 fn split_absolute_file(path: &Path) -> Result<(PathBuf, PathBuf), ArtifactIoError> {
   1998     if !path.is_absolute() {
   1999         return Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidRequest));
   2000     }
   2001     let parent = path
   2002         .parent()
   2003         .ok_or_else(|| ArtifactIoError::new(ArtifactIoFailureKind::InvalidRequest))?;
   2004     let name = path
   2005         .file_name()
   2006         .ok_or_else(|| ArtifactIoError::new(ArtifactIoFailureKind::InvalidRequest))?;
   2007     Ok((parent.to_path_buf(), PathBuf::from(name)))
   2008 }
   2009 
   2010 #[cfg(unix)]
   2011 fn open_absolute_directory(path: &Path) -> Result<(File, Vec<DirectoryIdentity>), ArtifactIoError> {
   2012     use rustix::fs::{Mode, OFlags, open, openat};
   2013 
   2014     if !path.is_absolute() {
   2015         return Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidRequest));
   2016     }
   2017     let flags =
   2018         OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::DIRECTORY;
   2019     let root = open("/", flags, Mode::empty())
   2020         .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::IoFailure))?;
   2021     let mut current = File::from(root);
   2022     let mut chain = vec![directory_identity(identity(&current)?)];
   2023     for component in path.components() {
   2024         match component {
   2025             Component::RootDir => {}
   2026             Component::Normal(name) => {
   2027                 let next = openat(&current, name, flags, Mode::empty())
   2028                     .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject))?;
   2029                 current = File::from(next);
   2030                 chain.push(directory_identity(identity(&current)?));
   2031             }
   2032             _ => {
   2033                 return Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidRequest));
   2034             }
   2035         }
   2036     }
   2037     Ok((current, chain))
   2038 }
   2039 
   2040 pub(crate) fn validate_trusted_output_directory(path: &Path) -> Result<(), ArtifactIoError> {
   2041     let _ = open_trusted_output_directory(path)?;
   2042     Ok(())
   2043 }
   2044 
   2045 #[cfg(unix)]
   2046 fn open_trusted_output_directory(
   2047     path: &Path,
   2048 ) -> Result<(File, Vec<OutputDirectoryIdentity>), ArtifactIoError> {
   2049     use rustix::fs::{Mode, OFlags, open, openat};
   2050 
   2051     if !path.is_absolute() {
   2052         return Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidRequest));
   2053     }
   2054     let flags =
   2055         OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::DIRECTORY;
   2056     let root = open("/", flags, Mode::empty())
   2057         .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::IoFailure))?;
   2058     let mut current = File::from(root);
   2059     let mut chain = Vec::new();
   2060     let root_identity = output_directory_identity(identity(&current)?);
   2061     if !output_directory_is_trusted(root_identity) {
   2062         return Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidRequest));
   2063     }
   2064     chain.push(root_identity);
   2065     for component in path.components() {
   2066         match component {
   2067             Component::RootDir => {}
   2068             Component::Normal(name) => {
   2069                 let next = openat(&current, name, flags, Mode::empty())
   2070                     .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject))?;
   2071                 current = File::from(next);
   2072                 let current_identity = output_directory_identity(identity(&current)?);
   2073                 if !output_directory_is_trusted(current_identity) {
   2074                     return Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidRequest));
   2075                 }
   2076                 chain.push(current_identity);
   2077             }
   2078             _ => {
   2079                 return Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidRequest));
   2080             }
   2081         }
   2082     }
   2083     Ok((current, chain))
   2084 }
   2085 
   2086 #[cfg(not(unix))]
   2087 fn open_absolute_directory(
   2088     _path: &Path,
   2089 ) -> Result<(File, Vec<DirectoryIdentity>), ArtifactIoError> {
   2090     Err(ArtifactIoError::new(
   2091         ArtifactIoFailureKind::UnsupportedPlatform,
   2092     ))
   2093 }
   2094 
   2095 #[cfg(not(unix))]
   2096 fn open_trusted_output_directory(
   2097     _path: &Path,
   2098 ) -> Result<(File, Vec<OutputDirectoryIdentity>), ArtifactIoError> {
   2099     Err(ArtifactIoError::new(
   2100         ArtifactIoFailureKind::UnsupportedPlatform,
   2101     ))
   2102 }
   2103 
   2104 fn open_relative(
   2105     root: &Path,
   2106     relative: &Path,
   2107     expected: ObjectKind,
   2108 ) -> Result<OpenedObject, ArtifactIoError> {
   2109     validate_relative(relative)?;
   2110     let (mut current, mut chain) = open_absolute_directory(root)?;
   2111     let components = relative.components().collect::<Vec<_>>();
   2112     let mut object_identity = None;
   2113     for (index, component) in components.iter().enumerate() {
   2114         let Component::Normal(name) = component else {
   2115             return Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidRequest));
   2116         };
   2117         let object = open_at(&current, name)?;
   2118         let metadata = object
   2119             .metadata()
   2120             .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::IoFailure))?;
   2121         let final_component = index + 1 == components.len();
   2122         if (!final_component && !metadata.is_dir())
   2123             || (final_component
   2124                 && match expected {
   2125                     ObjectKind::Directory => !metadata.is_dir(),
   2126                     ObjectKind::Regular => !metadata.is_file(),
   2127                 })
   2128         {
   2129             return Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject));
   2130         }
   2131         let current_identity = identity_from_metadata(&metadata);
   2132         current = object;
   2133         if metadata.is_dir() {
   2134             chain.push(directory_identity(current_identity));
   2135         }
   2136         object_identity = Some(current_identity);
   2137     }
   2138     let object_identity = object_identity
   2139         .ok_or_else(|| ArtifactIoError::new(ArtifactIoFailureKind::InvalidRequest))?;
   2140     Ok(OpenedObject {
   2141         file: current,
   2142         identity: object_identity,
   2143         chain,
   2144     })
   2145 }
   2146 
   2147 #[cfg(unix)]
   2148 fn open_at(directory: &File, name: &std::ffi::OsStr) -> Result<File, ArtifactIoError> {
   2149     use rustix::fs::{Mode, OFlags, openat};
   2150 
   2151     let flags = OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NONBLOCK;
   2152     let descriptor = openat(directory, name, flags, Mode::empty())
   2153         .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::InvalidObject))?;
   2154     Ok(File::from(descriptor))
   2155 }
   2156 
   2157 #[cfg(not(unix))]
   2158 fn open_at(_directory: &File, _name: &std::ffi::OsStr) -> Result<File, ArtifactIoError> {
   2159     Err(ArtifactIoError::new(
   2160         ArtifactIoFailureKind::UnsupportedPlatform,
   2161     ))
   2162 }
   2163 
   2164 fn validate_relative(path: &Path) -> Result<(), ArtifactIoError> {
   2165     if path.as_os_str().is_empty()
   2166         || path.is_absolute()
   2167         || path
   2168             .components()
   2169             .any(|component| !matches!(component, Component::Normal(_)))
   2170     {
   2171         Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidRequest))
   2172     } else {
   2173         Ok(())
   2174     }
   2175 }
   2176 
   2177 fn validate_single_component(name: &OsStr) -> Result<(), ArtifactIoError> {
   2178     let path = Path::new(name);
   2179     let mut components = path.components();
   2180     if matches!(components.next(), Some(Component::Normal(_))) && components.next().is_none() {
   2181         Ok(())
   2182     } else {
   2183         Err(ArtifactIoError::new(ArtifactIoFailureKind::InvalidRequest))
   2184     }
   2185 }
   2186 
   2187 #[cfg(unix)]
   2188 fn path_byte_len(path: &Path) -> usize {
   2189     use std::os::unix::ffi::OsStrExt as _;
   2190     path.as_os_str().as_bytes().len()
   2191 }
   2192 
   2193 #[cfg(not(unix))]
   2194 fn path_byte_len(path: &Path) -> usize {
   2195     path.as_os_str().to_string_lossy().len()
   2196 }
   2197 
   2198 fn identity(file: &File) -> Result<FileIdentity, ArtifactIoError> {
   2199     let metadata = file
   2200         .metadata()
   2201         .map_err(|_| ArtifactIoError::new(ArtifactIoFailureKind::IoFailure))?;
   2202     Ok(identity_from_metadata(&metadata))
   2203 }
   2204 
   2205 const fn directory_identity(identity: FileIdentity) -> DirectoryIdentity {
   2206     DirectoryIdentity {
   2207         device: identity.device,
   2208         inode: identity.inode,
   2209     }
   2210 }
   2211 
   2212 const fn permission_mode(identity: FileIdentity) -> u32 {
   2213     identity.mode & 0o7777
   2214 }
   2215 
   2216 #[cfg(unix)]
   2217 fn identity_from_metadata(metadata: &std::fs::Metadata) -> FileIdentity {
   2218     use std::os::unix::fs::MetadataExt as _;
   2219     FileIdentity {
   2220         device: metadata.dev(),
   2221         inode: metadata.ino(),
   2222         mode: metadata.mode(),
   2223         owner: metadata.uid(),
   2224         links: metadata.nlink(),
   2225         length: metadata.len(),
   2226         modified_seconds: metadata.mtime(),
   2227         modified_nanoseconds: metadata.mtime_nsec(),
   2228         changed_seconds: metadata.ctime(),
   2229         changed_nanoseconds: metadata.ctime_nsec(),
   2230     }
   2231 }
   2232 
   2233 #[cfg(not(unix))]
   2234 fn identity_from_metadata(metadata: &std::fs::Metadata) -> FileIdentity {
   2235     FileIdentity {
   2236         device: 0,
   2237         inode: 0,
   2238         mode: 0,
   2239         owner: 0,
   2240         links: 0,
   2241         length: metadata.len(),
   2242         modified_seconds: 0,
   2243         modified_nanoseconds: 0,
   2244         changed_seconds: 0,
   2245         changed_nanoseconds: 0,
   2246     }
   2247 }
   2248 
   2249 pub(crate) fn self_test() -> Result<(), String> {
   2250     #[cfg(unix)]
   2251     {
   2252         self_test_suite()?;
   2253         Ok(())
   2254     }
   2255     #[cfg(not(unix))]
   2256     {
   2257         Err(ArtifactIoError::new(ArtifactIoFailureKind::UnsupportedPlatform).to_string())
   2258     }
   2259 }
   2260 
   2261 #[cfg(unix)]
   2262 fn self_test_suite() -> Result<(), String> {
   2263     let directory =
   2264         tempfile::tempdir().map_err(|_| "safe artifact I/O self-test setup failed".to_owned())?;
   2265     let root = directory
   2266         .path()
   2267         .canonicalize()
   2268         .map_err(|_| "safe artifact I/O self-test setup failed".to_owned())?;
   2269     fs::write(root.join("input"), b"step-293-safe-artifact")
   2270         .map_err(|_| "safe artifact I/O self-test setup failed".to_owned())?;
   2271 
   2272     let bytes = read_regular(&root, Path::new("input"), 64).map_err(|error| error.to_string())?;
   2273     let evidence =
   2274         hash_regular(&root, Path::new("input"), 64).map_err(|error| error.to_string())?;
   2275     if bytes != b"step-293-safe-artifact"
   2276         || hash_regular(&root, Path::new("input"), 64).map_err(|error| error.to_string())?
   2277             != evidence
   2278     {
   2279         return Err("safe artifact I/O self-test evidence failed".to_owned());
   2280     }
   2281     let traversal = traverse_regular_files(
   2282         &root,
   2283         TraversalLimits {
   2284             max_entries: 4,
   2285             max_files: 4,
   2286             max_total_bytes: 256,
   2287             max_file_bytes: 128,
   2288             max_depth: 2,
   2289             max_path_bytes: 128,
   2290         },
   2291         &[],
   2292     )
   2293     .map_err(|error| error.to_string())?;
   2294     if traversal.entry_count() != 1
   2295         || traversal.total_bytes() != evidence.byte_length
   2296         || traversal
   2297             .read(&traversal.files()[0], 64)
   2298             .map_err(|error| error.to_string())?
   2299             != bytes
   2300         || traversal
   2301             .read_evidenced(&traversal.files()[0], 64)
   2302             .map_err(|error| error.to_string())?
   2303             != (bytes.clone(), evidence.clone())
   2304     {
   2305         return Err("safe artifact I/O self-test traversal failed".to_owned());
   2306     }
   2307 
   2308     let archive_path = root.join("archive.tar.gz");
   2309     let output = File::create(&archive_path)
   2310         .map_err(|_| "safe artifact I/O self-test setup failed".to_owned())?;
   2311     let encoder = GzBuilder::new().write(output, Compression::fast());
   2312     let mut archive = TarBuilder::new(encoder);
   2313     let mut header = TarHeader::new_gnu();
   2314     header.set_size(bytes.len() as u64);
   2315     header.set_mode(0o644);
   2316     header.set_cksum();
   2317     archive
   2318         .append_data(&mut header, "payload", bytes.as_slice())
   2319         .map_err(|_| "safe artifact I/O self-test archive failed".to_owned())?;
   2320     let encoder = archive
   2321         .into_inner()
   2322         .map_err(|_| "safe artifact I/O self-test archive failed".to_owned())?;
   2323     encoder
   2324         .finish()
   2325         .map_err(|_| "safe artifact I/O self-test archive failed".to_owned())?;
   2326     let admitted = admit_tar_gzip_path(
   2327         &archive_path,
   2328         TarGzipLimits {
   2329             max_compressed_bytes: 65_536,
   2330             max_expanded_bytes: 65_536,
   2331             max_members: 4,
   2332             max_member_bytes: 256,
   2333             max_payload_bytes: 256,
   2334             max_depth: 4,
   2335             max_path_bytes: 128,
   2336         },
   2337     )
   2338     .map_err(|error| error.to_string())?;
   2339     if admitted.member_count != 1 || admitted.payload_bytes != bytes.len() as u64 {
   2340         return Err("safe artifact I/O self-test archive failed".to_owned());
   2341     }
   2342     Ok(())
   2343 }
   2344 
   2345 #[cfg(all(test, unix))]
   2346 mod tests {
   2347     use std::fs::{self, OpenOptions};
   2348     use std::os::unix::fs::{FileTypeExt as _, PermissionsExt as _, symlink};
   2349     use std::time::Duration;
   2350 
   2351     use flate2::{Compression, GzBuilder};
   2352     use tar::{Builder, EntryType, Header};
   2353     use tempfile::TempDir;
   2354 
   2355     use super::*;
   2356 
   2357     fn root(directory: &TempDir) -> PathBuf {
   2358         directory
   2359             .path()
   2360             .canonicalize()
   2361             .expect("canonical test root")
   2362     }
   2363 
   2364     fn traversal_limits() -> TraversalLimits {
   2365         TraversalLimits {
   2366             max_entries: 8,
   2367             max_files: 4,
   2368             max_total_bytes: 64,
   2369             max_file_bytes: 32,
   2370             max_depth: 3,
   2371             max_path_bytes: 64,
   2372         }
   2373     }
   2374 
   2375     fn archive_limits() -> TarGzipLimits {
   2376         TarGzipLimits {
   2377             max_compressed_bytes: 32 * 1024,
   2378             max_expanded_bytes: 64 * 1024,
   2379             max_members: 8,
   2380             max_member_bytes: 8 * 1024,
   2381             max_payload_bytes: 16 * 1024,
   2382             max_depth: 4,
   2383             max_path_bytes: 128,
   2384         }
   2385     }
   2386 
   2387     #[test]
   2388     fn zero_limits_and_unsafe_names_fail_before_filesystem_admission() {
   2389         for mutate in [
   2390             |limits: &mut TraversalLimits| limits.max_entries = 0,
   2391             |limits: &mut TraversalLimits| limits.max_files = 0,
   2392             |limits: &mut TraversalLimits| limits.max_total_bytes = 0,
   2393             |limits: &mut TraversalLimits| limits.max_file_bytes = 0,
   2394             |limits: &mut TraversalLimits| limits.max_depth = 0,
   2395             |limits: &mut TraversalLimits| limits.max_path_bytes = 0,
   2396         ] {
   2397             let mut limits = traversal_limits();
   2398             mutate(&mut limits);
   2399             assert_eq!(
   2400                 validate_traversal_limits(limits).unwrap_err().kind(),
   2401                 ArtifactIoFailureKind::InvalidRequest
   2402             );
   2403         }
   2404         for mutate in [
   2405             |limits: &mut TarGzipLimits| limits.max_compressed_bytes = 0,
   2406             |limits: &mut TarGzipLimits| limits.max_expanded_bytes = 0,
   2407             |limits: &mut TarGzipLimits| limits.max_members = 0,
   2408             |limits: &mut TarGzipLimits| limits.max_member_bytes = 0,
   2409             |limits: &mut TarGzipLimits| limits.max_payload_bytes = 0,
   2410             |limits: &mut TarGzipLimits| limits.max_depth = 0,
   2411             |limits: &mut TarGzipLimits| limits.max_path_bytes = 0,
   2412         ] {
   2413             let mut limits = archive_limits();
   2414             mutate(&mut limits);
   2415             assert_eq!(
   2416                 validate_archive_limits(limits).unwrap_err().kind(),
   2417                 ArtifactIoFailureKind::InvalidRequest
   2418             );
   2419         }
   2420         let directory = TempDir::new().unwrap();
   2421         let root = root(&directory);
   2422         fs::write(root.join("input"), b"x").unwrap();
   2423         assert_eq!(
   2424             read_regular(&root, Path::new("input"), 0)
   2425                 .unwrap_err()
   2426                 .kind(),
   2427             ArtifactIoFailureKind::InvalidRequest
   2428         );
   2429         assert_eq!(
   2430             hash_regular(&root, Path::new("input"), 0)
   2431                 .unwrap_err()
   2432                 .kind(),
   2433             ArtifactIoFailureKind::InvalidRequest
   2434         );
   2435         for excluded in ["", "a/b", "a\\b", ".", ".."] {
   2436             assert!(traverse_regular_files(&root, traversal_limits(), &[excluded]).is_err());
   2437         }
   2438         for path in ["", "/absolute", "../escape"] {
   2439             assert!(validate_relative(Path::new(path)).is_err());
   2440         }
   2441         for name in ["", "/absolute", "nested/file", ".", ".."] {
   2442             assert!(validate_single_component(OsStr::new(name)).is_err());
   2443         }
   2444         for path in [Path::new("relative"), Path::new("/")] {
   2445             assert!(copy_regular_to_new_path(&root.join("input"), path, 1).is_err());
   2446         }
   2447         assert!(split_absolute_file(Path::new("relative")).is_err());
   2448         assert_eq!(fs::read(root.join("input")).unwrap(), b"x");
   2449         assert!(open_absolute_directory(Path::new("relative")).is_err());
   2450         assert!(open_trusted_output_directory(Path::new("relative")).is_err());
   2451         assert!(open_relative(&root, Path::new("input/child"), ObjectKind::Regular).is_err());
   2452     }
   2453 
   2454     #[test]
   2455     fn bounded_streams_keep_exact_prefixes_and_fail_on_the_next_byte() {
   2456         let mut reader = LimitReader::new(io::Cursor::new(b"abcd"), 3);
   2457         assert_eq!(reader.read(&mut []).unwrap(), 0);
   2458         let mut exact = [0; 3];
   2459         reader.read_exact(&mut exact).unwrap();
   2460         assert_eq!(&exact, b"abc");
   2461         assert!(!reader.exceeded());
   2462         assert!(reader.read(&mut [0; 1]).is_err());
   2463         assert!(reader.exceeded());
   2464         let mut writer = HashingLimitWriter::new(3);
   2465         writer.write_all(b"abc").unwrap();
   2466         assert_eq!(writer.write(&[]).unwrap(), 0);
   2467         writer.flush().unwrap();
   2468         assert!(writer.write_all(b"d").is_err());
   2469         assert!(writer.exceeded());
   2470         let evidence = writer.finalize();
   2471         assert_eq!(evidence.byte_length, 3);
   2472         assert_eq!(evidence.sha256, hex::encode(Sha256::digest(b"abc")));
   2473         let directory = TempDir::new().unwrap();
   2474         let path = directory.path().join("input");
   2475         fs::write(&path, b"abcd").unwrap();
   2476         let mut file = File::open(&path).unwrap();
   2477         let mut reader = HashingLimitReader::new(&mut file, 3);
   2478         assert_eq!(reader.read(&mut []).unwrap(), 0);
   2479         reader.read_exact(&mut exact).unwrap();
   2480         assert!(reader.read(&mut [0; 1]).is_err());
   2481         assert!(reader.exceeded());
   2482         assert_eq!(reader.finalize(), hex::encode(Sha256::digest(b"abc")));
   2483         assert_eq!(
   2484             stream_regular(&mut File::open(&path).unwrap(), 3, None)
   2485                 .unwrap_err()
   2486                 .kind(),
   2487             ArtifactIoFailureKind::LimitExceeded(LimitKind::FileBytes)
   2488         );
   2489     }
   2490 
   2491     #[test]
   2492     fn archive_names_and_headers_reject_noncanonical_and_conflicting_members() {
   2493         for path in [
   2494             b"".as_slice(),
   2495             b"/root",
   2496             b"a\0b",
   2497             b"a\\b",
   2498             &[255],
   2499             b"a//b",
   2500             b"a/../b",
   2501             b"./a",
   2502         ] {
   2503             assert!(validate_archive_path(path, 4, 128).is_err());
   2504         }
   2505         validate_archive_path(b"dir/file", 2, 8).unwrap();
   2506         assert!(validate_archive_path(b"dir/file", 1, 8).is_err());
   2507         assert!(validate_archive_path(b"dir/file", 2, 7).is_err());
   2508         let mut names = BTreeMap::new();
   2509         admit_archive_name(&mut names, b"dir/", ArchiveMemberKind::Directory).unwrap();
   2510         admit_archive_name(&mut names, b"dir/file", ArchiveMemberKind::File).unwrap();
   2511         assert!(admit_archive_name(&mut names, b"dir/file", ArchiveMemberKind::File).is_err());
   2512         assert!(
   2513             admit_archive_name(&mut names, b"dir/file/child", ArchiveMemberKind::File).is_err()
   2514         );
   2515         let mut reverse = BTreeMap::new();
   2516         admit_archive_name(&mut reverse, b"dir/child", ArchiveMemberKind::File).unwrap();
   2517         assert!(admit_archive_name(&mut reverse, b"dir", ArchiveMemberKind::File).is_err());
   2518         let directory = TempDir::new().unwrap();
   2519         let path = directory.path().join("header");
   2520         let canonical = [0x1f, 0x8b, 8, 0, 0, 0, 0, 0, 0, 255];
   2521         for index in [0, 3, 4, 8, 9] {
   2522             let mut bytes = canonical;
   2523             bytes[index] ^= 1;
   2524             fs::write(&path, bytes).unwrap();
   2525             let mut file = File::open(&path).unwrap();
   2526             let mut reader = BufReader::new(HashingLimitReader::new(&mut file, 10));
   2527             assert!(
   2528                 validate_gzip_header(&mut reader, TarGzipPolicy::DeterministicSnapshot).is_err()
   2529             );
   2530         }
   2531         fs::write(&path, canonical).unwrap();
   2532         let mut file = File::open(&path).unwrap();
   2533         let mut reader = BufReader::new(HashingLimitReader::new(&mut file, 9));
   2534         assert_eq!(
   2535             validate_gzip_header(&mut reader, TarGzipPolicy::DeterministicSnapshot)
   2536                 .unwrap_err()
   2537                 .kind(),
   2538             ArtifactIoFailureKind::LimitExceeded(LimitKind::ArchiveCompressedBytes)
   2539         );
   2540     }
   2541 
   2542     #[test]
   2543     fn hard_maximums_reject_invalid_requests() {
   2544         assert_eq!(HARD_MAX_BUFFERED_READ_BYTES, 67_108_864);
   2545         assert_eq!(HARD_MAX_STREAM_FILE_BYTES, 17_179_869_184);
   2546         assert_eq!(HARD_MAX_TRAVERSAL_ENTRIES, 65_536);
   2547         assert_eq!(HARD_MAX_TRAVERSAL_FILES, 65_536);
   2548         assert_eq!(HARD_MAX_TRAVERSAL_TOTAL_BYTES, 68_719_476_736);
   2549         assert_eq!(HARD_MAX_TRAVERSAL_DEPTH, 64);
   2550         assert_eq!(HARD_MAX_PATH_BYTES, 4_096);
   2551         assert_eq!(HARD_MAX_ARCHIVE_COMPRESSED_BYTES, 2_147_483_648);
   2552         assert_eq!(HARD_MAX_ARCHIVE_EXPANDED_BYTES, 17_179_869_184);
   2553         assert_eq!(HARD_MAX_ARCHIVE_MEMBERS, 65_536);
   2554         assert_eq!(HARD_MAX_ARCHIVE_MEMBER_BYTES, 17_179_869_184);
   2555         assert_eq!(HARD_MAX_ARCHIVE_PAYLOAD_BYTES, 17_179_869_184);
   2556         let directory = TempDir::new().expect("tempdir");
   2557         let root = root(&directory);
   2558         fs::write(root.join("input"), b"x").expect("input");
   2559         assert_eq!(
   2560             read_regular(&root, Path::new("input"), HARD_MAX_BUFFERED_READ_BYTES + 1,)
   2561                 .expect_err("buffered read hard maximum")
   2562                 .kind(),
   2563             ArtifactIoFailureKind::InvalidRequest
   2564         );
   2565         assert_eq!(
   2566             hash_regular(&root, Path::new("input"), HARD_MAX_STREAM_FILE_BYTES + 1,)
   2567                 .expect_err("streaming file hard maximum")
   2568                 .kind(),
   2569             ArtifactIoFailureKind::InvalidRequest
   2570         );
   2571 
   2572         for mutate in [
   2573             |limits: &mut TraversalLimits| limits.max_entries = HARD_MAX_TRAVERSAL_ENTRIES + 1,
   2574             |limits: &mut TraversalLimits| limits.max_files = HARD_MAX_TRAVERSAL_FILES + 1,
   2575             |limits: &mut TraversalLimits| {
   2576                 limits.max_total_bytes = HARD_MAX_TRAVERSAL_TOTAL_BYTES + 1
   2577             },
   2578             |limits: &mut TraversalLimits| limits.max_file_bytes = HARD_MAX_STREAM_FILE_BYTES + 1,
   2579             |limits: &mut TraversalLimits| limits.max_depth = HARD_MAX_TRAVERSAL_DEPTH + 1,
   2580             |limits: &mut TraversalLimits| limits.max_path_bytes = HARD_MAX_PATH_BYTES + 1,
   2581         ] {
   2582             let mut limits = traversal_limits();
   2583             mutate(&mut limits);
   2584             assert_eq!(
   2585                 validate_traversal_limits(limits)
   2586                     .expect_err("traversal hard maximum")
   2587                     .kind(),
   2588                 ArtifactIoFailureKind::InvalidRequest
   2589             );
   2590         }
   2591 
   2592         for mutate in [
   2593             |limits: &mut TarGzipLimits| {
   2594                 limits.max_compressed_bytes = HARD_MAX_ARCHIVE_COMPRESSED_BYTES + 1
   2595             },
   2596             |limits: &mut TarGzipLimits| {
   2597                 limits.max_expanded_bytes = HARD_MAX_ARCHIVE_EXPANDED_BYTES + 1
   2598             },
   2599             |limits: &mut TarGzipLimits| limits.max_members = HARD_MAX_ARCHIVE_MEMBERS + 1,
   2600             |limits: &mut TarGzipLimits| {
   2601                 limits.max_member_bytes = HARD_MAX_ARCHIVE_MEMBER_BYTES + 1
   2602             },
   2603             |limits: &mut TarGzipLimits| {
   2604                 limits.max_payload_bytes = HARD_MAX_ARCHIVE_PAYLOAD_BYTES + 1
   2605             },
   2606             |limits: &mut TarGzipLimits| limits.max_depth = HARD_MAX_TRAVERSAL_DEPTH + 1,
   2607             |limits: &mut TarGzipLimits| limits.max_path_bytes = HARD_MAX_PATH_BYTES + 1,
   2608         ] {
   2609             let mut limits = archive_limits();
   2610             mutate(&mut limits);
   2611             assert_eq!(
   2612                 validate_archive_limits(limits)
   2613                     .expect_err("archive hard maximum")
   2614                     .kind(),
   2615                 ArtifactIoFailureKind::InvalidRequest
   2616             );
   2617         }
   2618     }
   2619 
   2620     #[test]
   2621     fn bounded_read_hash_and_copy_are_streaming_and_exact() {
   2622         let directory = TempDir::new().expect("tempdir");
   2623         let root = root(&directory);
   2624         fs::write(root.join("input"), b"01234567").expect("write input");
   2625 
   2626         assert_eq!(
   2627             read_regular(&root, Path::new("input"), 8).expect("read at cap"),
   2628             b"01234567"
   2629         );
   2630         assert_eq!(
   2631             read_regular(&root, Path::new("input"), 7)
   2632                 .expect_err("read over cap")
   2633                 .kind(),
   2634             ArtifactIoFailureKind::LimitExceeded(LimitKind::FileBytes)
   2635         );
   2636         let evidence = hash_regular(&root, Path::new("input"), 8).expect("hash");
   2637         let output_path = root.join("output");
   2638         let copied = copy_regular_to_new_path(&root.join("input"), &output_path, 8)
   2639             .expect("create-new copy");
   2640         assert_eq!(evidence, copied);
   2641         assert_eq!(fs::read(output_path).expect("output bytes"), b"01234567");
   2642         assert_eq!(
   2643             fs::metadata(root.join("output"))
   2644                 .expect("output metadata")
   2645                 .permissions()
   2646                 .mode()
   2647                 & 0o7777,
   2648             0o600
   2649         );
   2650 
   2651         let failed_output = root.join("failed-output");
   2652         let error = copy_regular_to_new_path_impl(
   2653             &root.join("input"),
   2654             &failed_output,
   2655             8,
   2656             None,
   2657             || {
   2658                 fs::rename(root.join("input"), root.join("moved-input")).expect("move copy input");
   2659                 fs::write(root.join("input"), b"01234567").expect("replace copy input");
   2660             },
   2661             || {},
   2662         )
   2663         .expect_err("copy binding change");
   2664         assert_eq!(error.kind(), ArtifactIoFailureKind::ChangedDuringRead);
   2665         assert_eq!(
   2666             fs::read(&failed_output).expect("untrusted failed output"),
   2667             b"01234567"
   2668         );
   2669         fs::remove_file(&failed_output).expect("remove untrusted failed output");
   2670 
   2671         let open_hardlink_output = root.join("open-hardlink-output");
   2672         let open_hardlink_alias = root.join("open-hardlink-alias");
   2673         let error = copy_regular_to_new_path_impl(
   2674             &root.join("input"),
   2675             &open_hardlink_output,
   2676             8,
   2677             None,
   2678             || {
   2679                 fs::hard_link(&open_hardlink_output, &open_hardlink_alias)
   2680                     .expect("hardlink open output");
   2681             },
   2682             || {},
   2683         )
   2684         .expect_err("open hardlink race");
   2685         assert_eq!(error.kind(), ArtifactIoFailureKind::ChangedDuringRead);
   2686         assert_eq!(
   2687             fs::read(&open_hardlink_output).expect("untrusted output"),
   2688             b"01234567"
   2689         );
   2690         assert_eq!(
   2691             fs::read(&open_hardlink_alias).expect("hardlink alias"),
   2692             b"01234567"
   2693         );
   2694         fs::remove_file(&open_hardlink_output).expect("remove untrusted output");
   2695         fs::remove_file(&open_hardlink_alias).expect("remove hardlink alias");
   2696 
   2697         let installed_hardlink_output = root.join("installed-hardlink-output");
   2698         let installed_hardlink_alias = root.join("installed-hardlink-alias");
   2699         let error = copy_regular_to_new_path_impl(
   2700             &root.join("input"),
   2701             &installed_hardlink_output,
   2702             8,
   2703             None,
   2704             || {},
   2705             || {
   2706                 fs::hard_link(&installed_hardlink_output, &installed_hardlink_alias)
   2707                     .expect("hardlink installed output");
   2708             },
   2709         )
   2710         .expect_err("installed hardlink race");
   2711         assert_eq!(error.kind(), ArtifactIoFailureKind::ChangedDuringRead);
   2712         assert_eq!(
   2713             fs::read(&installed_hardlink_output).expect("untrusted installed output"),
   2714             b"01234567"
   2715         );
   2716         assert!(installed_hardlink_alias.exists());
   2717         fs::remove_file(&installed_hardlink_output).expect("remove untrusted installed output");
   2718         fs::remove_file(&installed_hardlink_alias).expect("remove installed hardlink alias");
   2719 
   2720         let mutated_output = root.join("mutated-output");
   2721         let error = copy_regular_to_new_path_impl(
   2722             &root.join("input"),
   2723             &mutated_output,
   2724             8,
   2725             None,
   2726             || {},
   2727             || fs::write(&mutated_output, b"87654321").expect("mutate installed output"),
   2728         )
   2729         .expect_err("post-sync output mutation");
   2730         assert_eq!(error.kind(), ArtifactIoFailureKind::ChangedDuringRead);
   2731         assert_eq!(
   2732             fs::read(&mutated_output).expect("untrusted mutated output"),
   2733             b"87654321"
   2734         );
   2735         fs::remove_file(&mutated_output).expect("remove untrusted mutated output");
   2736 
   2737         let name_swap_output = root.join("name-swap-output");
   2738         let moved_created_output = root.join("moved-created-output");
   2739         let error = copy_regular_to_new_path_impl(
   2740             &root.join("input"),
   2741             &name_swap_output,
   2742             8,
   2743             None,
   2744             || {
   2745                 fs::rename(&name_swap_output, &moved_created_output).expect("move created output");
   2746                 fs::write(&name_swap_output, b"unrelated").expect("replacement output");
   2747             },
   2748             || {},
   2749         )
   2750         .expect_err("created name swap");
   2751         assert_eq!(error.kind(), ArtifactIoFailureKind::ChangedDuringRead);
   2752         assert_eq!(
   2753             fs::read(&name_swap_output).expect("unrelated replacement preserved"),
   2754             b"unrelated"
   2755         );
   2756         assert_eq!(
   2757             fs::read(&moved_created_output).expect("moved untrusted output"),
   2758             b"01234567"
   2759         );
   2760         fs::remove_file(&name_swap_output).expect("remove replacement output");
   2761         fs::remove_file(&moved_created_output).expect("remove moved output");
   2762 
   2763         fs::create_dir(root.join("output-parent")).expect("output parent");
   2764         fs::write(root.join("input"), b"01234567").expect("restore input");
   2765         let rebound_output = root.join("output-parent/installed");
   2766         let error = copy_regular_to_new_path_impl(
   2767             &root.join("input"),
   2768             &rebound_output,
   2769             8,
   2770             None,
   2771             || {
   2772                 fs::rename(root.join("output-parent"), root.join("old-output-parent"))
   2773                     .expect("move output parent");
   2774                 fs::create_dir(root.join("output-parent")).expect("replace output parent");
   2775             },
   2776             || {},
   2777         )
   2778         .expect_err("output parent replacement");
   2779         assert_eq!(error.kind(), ArtifactIoFailureKind::ChangedDuringRead);
   2780         assert!(!rebound_output.exists());
   2781         assert_eq!(
   2782             fs::read(root.join("old-output-parent/installed"))
   2783                 .expect("untrusted output in moved parent"),
   2784             b"01234567"
   2785         );
   2786 
   2787         fs::set_permissions(&root, fs::Permissions::from_mode(0o777))
   2788             .expect("make output parent untrusted");
   2789         assert_eq!(
   2790             copy_regular_to_new_path(&root.join("input"), &root.join("untrusted-parent"), 8)
   2791                 .expect_err("writable output parent rejected")
   2792                 .kind(),
   2793             ArtifactIoFailureKind::InvalidRequest
   2794         );
   2795         fs::set_permissions(&root, fs::Permissions::from_mode(0o700))
   2796             .expect("restore output parent mode");
   2797 
   2798         let parent_mode_output = root.join("parent-mode-output");
   2799         let error = copy_regular_to_new_path_impl(
   2800             &root.join("input"),
   2801             &parent_mode_output,
   2802             8,
   2803             None,
   2804             || {},
   2805             || {
   2806                 fs::set_permissions(&root, fs::Permissions::from_mode(0o777))
   2807                     .expect("change output parent mode");
   2808             },
   2809         )
   2810         .expect_err("output parent security change");
   2811         assert_eq!(error.kind(), ArtifactIoFailureKind::ChangedDuringRead);
   2812         assert!(parent_mode_output.exists());
   2813         fs::set_permissions(&root, fs::Permissions::from_mode(0o700))
   2814             .expect("restore output parent mode");
   2815         fs::remove_file(&parent_mode_output).expect("remove untrusted parent-mode output");
   2816 
   2817         let secure_ancestor = root.join("secure-ancestor");
   2818         let nested_parent = secure_ancestor.join("output-parent");
   2819         fs::create_dir(&secure_ancestor).expect("secure ancestor");
   2820         fs::create_dir(&nested_parent).expect("nested output parent");
   2821         let ancestor_mode_output = nested_parent.join("installed");
   2822         let error = copy_regular_to_new_path_impl(
   2823             &root.join("input"),
   2824             &ancestor_mode_output,
   2825             8,
   2826             None,
   2827             || {},
   2828             || {
   2829                 fs::set_permissions(&secure_ancestor, fs::Permissions::from_mode(0o777))
   2830                     .expect("change output ancestor mode");
   2831             },
   2832         )
   2833         .expect_err("output ancestor security change");
   2834         assert_eq!(error.kind(), ArtifactIoFailureKind::ChangedDuringRead);
   2835         assert!(ancestor_mode_output.exists());
   2836         fs::set_permissions(&secure_ancestor, fs::Permissions::from_mode(0o755))
   2837             .expect("restore output ancestor mode");
   2838         fs::remove_file(&ancestor_mode_output).expect("remove untrusted ancestor-mode output");
   2839     }
   2840 
   2841     #[test]
   2842     fn no_follow_admission_rejects_symlink_fifo_and_replacement() {
   2843         let directory = TempDir::new().expect("tempdir");
   2844         let root = root(&directory);
   2845         fs::write(root.join("source"), b"original").expect("source");
   2846         symlink("source", root.join("link")).expect("symlink");
   2847         assert_eq!(
   2848             read_regular(&root, Path::new("link"), 32)
   2849                 .expect_err("symlink rejected")
   2850                 .kind(),
   2851             ArtifactIoFailureKind::InvalidObject
   2852         );
   2853 
   2854         let fifo = root.join("fifo");
   2855         let request = crate::bounded_process::ProcessRequest::new("/usr/bin/mkfifo")
   2856             .arg(fifo.as_os_str())
   2857             .deadline(Duration::from_secs(5))
   2858             .output_limits(1024, 1024);
   2859         let output = crate::bounded_process::run(&request).expect("bounded mkfifo");
   2860         assert!(output.status().success());
   2861         assert!(
   2862             fs::symlink_metadata(&fifo)
   2863                 .expect("fifo metadata")
   2864                 .file_type()
   2865                 .is_fifo()
   2866         );
   2867         assert_eq!(
   2868             read_regular(&root, Path::new("fifo"), 32)
   2869                 .expect_err("fifo rejected")
   2870                 .kind(),
   2871             ArtifactIoFailureKind::InvalidObject
   2872         );
   2873 
   2874         let error = read_regular_impl(&root, Path::new("source"), 32, None, || {
   2875             fs::rename(root.join("source"), root.join("old")).expect("move admitted file");
   2876             fs::write(root.join("source"), b"original").expect("replacement");
   2877         })
   2878         .expect_err("replacement rejected");
   2879         assert_eq!(error.kind(), ArtifactIoFailureKind::ChangedDuringRead);
   2880 
   2881         fs::write(root.join("mutable"), b"same-size").expect("mutable");
   2882         let error = read_regular_impl(&root, Path::new("mutable"), 32, None, || {
   2883             fs::write(root.join("mutable"), b"new-bytes").expect("in-place mutation");
   2884         })
   2885         .expect_err("in-place mutation rejected");
   2886         assert_eq!(error.kind(), ArtifactIoFailureKind::ChangedDuringRead);
   2887 
   2888         fs::create_dir(root.join("parent")).expect("parent");
   2889         fs::write(root.join("parent/member"), b"member").expect("member");
   2890         let error = read_regular_impl(&root, Path::new("parent/member"), 32, None, || {
   2891             fs::rename(root.join("parent"), root.join("old-parent")).expect("move parent");
   2892             fs::create_dir(root.join("parent")).expect("replacement parent");
   2893             fs::write(root.join("parent/member"), b"member").expect("replacement member");
   2894         })
   2895         .expect_err("parent replacement rejected");
   2896         assert_eq!(error.kind(), ArtifactIoFailureKind::ChangedDuringRead);
   2897     }
   2898 
   2899     #[test]
   2900     fn traversal_enforces_type_count_byte_and_depth_bounds() {
   2901         let directory = TempDir::new().expect("tempdir");
   2902         let root = root(&directory);
   2903         fs::create_dir(root.join("a")).expect("a");
   2904         fs::write(root.join("a/one"), b"1").expect("one");
   2905         fs::write(root.join("two"), b"22").expect("two");
   2906         let snapshot = traverse_regular_files(&root, traversal_limits(), &[]).expect("traverse");
   2907         assert_eq!(snapshot.entry_count(), 3);
   2908         assert_eq!(snapshot.total_bytes(), 3);
   2909         assert_eq!(
   2910             snapshot
   2911                 .files()
   2912                 .iter()
   2913                 .map(|file| file.relative_path().to_path_buf())
   2914                 .collect::<Vec<_>>(),
   2915             [PathBuf::from("a/one"), PathBuf::from("two")]
   2916         );
   2917         assert_eq!(
   2918             snapshot
   2919                 .read(&snapshot.files()[0], 1)
   2920                 .expect("snapshot read"),
   2921             b"1"
   2922         );
   2923         let (bytes, evidenced) = snapshot
   2924             .read_evidenced(&snapshot.files()[0], 1)
   2925             .expect("snapshot evidenced read");
   2926         assert_eq!(bytes, b"1");
   2927         assert_eq!(
   2928             evidenced,
   2929             snapshot
   2930                 .hash(&snapshot.files()[0], 1)
   2931                 .expect("snapshot hash")
   2932         );
   2933         let output_directory = TempDir::new().expect("snapshot copy output");
   2934         let output_root = output_directory
   2935             .path()
   2936             .canonicalize()
   2937             .expect("canonical snapshot copy output");
   2938         assert_eq!(
   2939             snapshot
   2940                 .copy_to_new_path(&snapshot.files()[0], &output_root.join("copy"), 1)
   2941                 .expect("snapshot copy"),
   2942             evidenced
   2943         );
   2944         assert_eq!(
   2945             fs::read(output_root.join("copy")).expect("copied bytes"),
   2946             b"1"
   2947         );
   2948 
   2949         let late_error = traverse_regular_files_impl(&root, traversal_limits(), &[], || {
   2950             fs::write(root.join("late"), b"late").expect("late insertion");
   2951         })
   2952         .expect_err("late insertion is detected");
   2953         assert_eq!(late_error.kind(), ArtifactIoFailureKind::ChangedDuringRead);
   2954         fs::remove_file(root.join("late")).expect("remove late insertion");
   2955 
   2956         fs::create_dir(root.join("excluded")).expect("excluded directory");
   2957         let excluded = traverse_regular_files(&root, traversal_limits(), &["excluded"])
   2958             .expect("excluded snapshot");
   2959         fs::rename(root.join("excluded"), root.join("old-excluded"))
   2960             .expect("move excluded directory");
   2961         fs::create_dir(root.join("excluded")).expect("replace excluded directory");
   2962         assert_eq!(
   2963             excluded
   2964                 .revalidate()
   2965                 .expect_err("excluded binding change")
   2966                 .kind(),
   2967             ArtifactIoFailureKind::ChangedDuringRead
   2968         );
   2969 
   2970         let mut one_entry = traversal_limits();
   2971         one_entry.max_entries = 1;
   2972         assert_eq!(
   2973             traverse_regular_files(&root, one_entry, &[])
   2974                 .expect_err("entry cap")
   2975                 .kind(),
   2976             ArtifactIoFailureKind::LimitExceeded(LimitKind::TraversalEntries)
   2977         );
   2978         let mut one_file = traversal_limits();
   2979         one_file.max_files = 1;
   2980         assert_eq!(
   2981             traverse_regular_files(&root, one_file, &[])
   2982                 .expect_err("file cap")
   2983                 .kind(),
   2984             ArtifactIoFailureKind::LimitExceeded(LimitKind::TraversalFiles)
   2985         );
   2986         let mut one_file_byte = traversal_limits();
   2987         one_file_byte.max_file_bytes = 1;
   2988         assert_eq!(
   2989             traverse_regular_files(&root, one_file_byte, &[])
   2990                 .expect_err("per-file cap")
   2991                 .kind(),
   2992             ArtifactIoFailureKind::LimitExceeded(LimitKind::FileBytes)
   2993         );
   2994         let mut short_path = traversal_limits();
   2995         short_path.max_path_bytes = 2;
   2996         assert_eq!(
   2997             traverse_regular_files(&root, short_path, &[])
   2998                 .expect_err("path cap")
   2999                 .kind(),
   3000             ArtifactIoFailureKind::LimitExceeded(LimitKind::TraversalPathBytes)
   3001         );
   3002         let mut two_bytes = traversal_limits();
   3003         two_bytes.max_total_bytes = 2;
   3004         assert_eq!(
   3005             traverse_regular_files(&root, two_bytes, &[])
   3006                 .expect_err("byte cap")
   3007                 .kind(),
   3008             ArtifactIoFailureKind::LimitExceeded(LimitKind::TraversalTotalBytes)
   3009         );
   3010         let mut no_depth = traversal_limits();
   3011         no_depth.max_depth = 1;
   3012         fs::create_dir(root.join("a/deep")).expect("deep");
   3013         fs::write(root.join("a/deep/file"), b"x").expect("deep file");
   3014         assert_eq!(
   3015             traverse_regular_files(&root, no_depth, &[])
   3016                 .expect_err("depth cap")
   3017                 .kind(),
   3018             ArtifactIoFailureKind::LimitExceeded(LimitKind::TraversalDepth)
   3019         );
   3020     }
   3021 
   3022     #[test]
   3023     fn retained_traversal_rejects_root_directory_member_and_file_replacement() {
   3024         for case in [
   3025             "root",
   3026             "directory",
   3027             "mode",
   3028             "file",
   3029             "members",
   3030             "member-name",
   3031         ] {
   3032             let directory = TempDir::new().unwrap();
   3033             let base = root(&directory);
   3034             let tree = base.join("tree");
   3035             fs::create_dir_all(tree.join("nested")).unwrap();
   3036             fs::write(tree.join("nested/file"), b"original").unwrap();
   3037             let snapshot = traverse_regular_files(&tree, traversal_limits(), &[]).unwrap();
   3038             snapshot.revalidate().unwrap();
   3039             match case {
   3040                 "root" => {
   3041                     fs::rename(&tree, base.join("old-tree")).unwrap();
   3042                     fs::create_dir_all(tree.join("nested")).unwrap();
   3043                     fs::write(tree.join("nested/file"), b"original").unwrap();
   3044                 }
   3045                 "directory" => {
   3046                     fs::rename(tree.join("nested"), base.join("old-nested")).unwrap();
   3047                     fs::create_dir(tree.join("nested")).unwrap();
   3048                     fs::write(tree.join("nested/file"), b"original").unwrap();
   3049                 }
   3050                 "mode" => {
   3051                     fs::set_permissions(tree.join("nested"), fs::Permissions::from_mode(0o700))
   3052                         .unwrap()
   3053                 }
   3054                 "file" => fs::write(tree.join("nested/file"), b"different length").unwrap(),
   3055                 "members" => fs::write(tree.join("nested/added"), b"new").unwrap(),
   3056                 "member-name" => {
   3057                     fs::rename(tree.join("nested/file"), tree.join("nested/renamed")).unwrap()
   3058                 }
   3059                 _ => unreachable!(),
   3060             }
   3061             assert_eq!(
   3062                 snapshot.revalidate().unwrap_err().kind(),
   3063                 ArtifactIoFailureKind::ChangedDuringRead,
   3064                 "{case}"
   3065             );
   3066         }
   3067     }
   3068 
   3069     fn write_archive(path: &Path, members: &[(&str, EntryType, &[u8])]) {
   3070         let output = File::create(path).expect("archive output");
   3071         let encoder = GzBuilder::new().write(output, Compression::fast());
   3072         let mut builder = Builder::new(encoder);
   3073         for (name, kind, contents) in members {
   3074             let mut header = Header::new_gnu();
   3075             header.set_entry_type(*kind);
   3076             header.set_mode(0o644);
   3077             header.set_size(contents.len() as u64);
   3078             header.set_cksum();
   3079             builder
   3080                 .append_data(&mut header, name, *contents)
   3081                 .expect("archive member");
   3082         }
   3083         let encoder = builder.into_inner().expect("finish tar");
   3084         encoder.finish().expect("finish gzip");
   3085     }
   3086 
   3087     fn write_raw_named_archive(path: &Path, name: &[u8], contents: &[u8]) {
   3088         assert!(name.len() < 100);
   3089         let output = File::create(path).expect("archive output");
   3090         let encoder = GzBuilder::new().write(output, Compression::fast());
   3091         let mut builder = Builder::new(encoder);
   3092         let mut header = Header::new_gnu();
   3093         header.as_mut_bytes()[..name.len()].copy_from_slice(name);
   3094         header.set_entry_type(EntryType::Regular);
   3095         header.set_mode(0o644);
   3096         header.set_size(contents.len() as u64);
   3097         header.set_cksum();
   3098         builder
   3099             .append(&header, contents)
   3100             .expect("raw archive member");
   3101         let encoder = builder.into_inner().expect("finish tar");
   3102         encoder.finish().expect("finish gzip");
   3103     }
   3104 
   3105     fn write_single_terminator_archive(path: &Path) {
   3106         let mut header = Header::new_gnu();
   3107         header.set_path("one").expect("path");
   3108         header.set_entry_type(EntryType::Regular);
   3109         header.set_mode(0o644);
   3110         header.set_size(3);
   3111         header.set_cksum();
   3112         let mut bytes = Vec::from(header.as_bytes());
   3113         bytes.extend_from_slice(b"one");
   3114         bytes.resize(1536, 0);
   3115         let output = File::create(path).expect("archive output");
   3116         let mut encoder = GzBuilder::new().write(output, Compression::fast());
   3117         encoder.write_all(&bytes).expect("write raw tar");
   3118         encoder.finish().expect("finish gzip");
   3119     }
   3120 
   3121     #[test]
   3122     fn tar_gzip_admission_is_parse_only_and_bounded() {
   3123         let directory = TempDir::new().expect("tempdir");
   3124         let root = root(&directory);
   3125         let valid = root.join("valid.tar.gz");
   3126         write_archive(&valid, &[("bin/tool", EntryType::Regular, b"payload")]);
   3127         let evidence = admit_tar_gzip_path(&valid, archive_limits()).expect("valid archive");
   3128         assert_eq!(evidence.member_count, 1);
   3129         assert_eq!(evidence.payload_bytes, 7);
   3130         let valid_bytes = fs::read(&valid).expect("valid archive bytes");
   3131         assert_eq!(evidence.compressed.byte_length, valid_bytes.len() as u64);
   3132         assert_eq!(
   3133             evidence.compressed.sha256,
   3134             hex::encode(Sha256::digest(&valid_bytes))
   3135         );
   3136         assert!(!root.join("bin").exists());
   3137 
   3138         for (name, flags) in [
   3139             ("optional-header.tar.gz", 0x08),
   3140             ("reserved-header.tar.gz", 0x20),
   3141         ] {
   3142             let path = root.join(name);
   3143             let mut bytes = valid_bytes.clone();
   3144             bytes[3] = flags;
   3145             fs::write(&path, bytes).expect("write rejected gzip header");
   3146             assert_eq!(
   3147                 admit_tar_gzip_path(&path, archive_limits())
   3148                     .expect_err("non-minimal gzip header rejected")
   3149                     .kind(),
   3150                 ArtifactIoFailureKind::InvalidObject
   3151             );
   3152         }
   3153 
   3154         let symlink_archive = root.join("symlink.tar.gz");
   3155         write_archive(
   3156             &symlink_archive,
   3157             &[("escape", EntryType::Symlink, b"target")],
   3158         );
   3159         assert_eq!(
   3160             admit_tar_gzip_path(&symlink_archive, archive_limits())
   3161                 .expect_err("special member rejected")
   3162                 .kind(),
   3163             ArtifactIoFailureKind::InvalidObject
   3164         );
   3165 
   3166         let escape_archive = root.join("escape.tar.gz");
   3167         write_raw_named_archive(&escape_archive, b"../escape", b"payload");
   3168         assert_eq!(
   3169             admit_tar_gzip_path(&escape_archive, archive_limits())
   3170                 .expect_err("escape rejected")
   3171                 .kind(),
   3172             ArtifactIoFailureKind::InvalidObject
   3173         );
   3174 
   3175         let bomb = root.join("bomb.tar.gz");
   3176         write_archive(&bomb, &[("large", EntryType::Regular, &[0_u8; 8192])]);
   3177         let mut bounded = archive_limits();
   3178         bounded.max_expanded_bytes = 1024;
   3179         assert_eq!(
   3180             admit_tar_gzip_path(&bomb, bounded)
   3181                 .expect_err("expanded cap")
   3182                 .kind(),
   3183             ArtifactIoFailureKind::LimitExceeded(LimitKind::ArchiveExpandedBytes)
   3184         );
   3185 
   3186         let mut compressed = archive_limits();
   3187         compressed.max_compressed_bytes = fs::metadata(&valid).expect("valid metadata").len() - 1;
   3188         assert_eq!(
   3189             admit_tar_gzip_path(&valid, compressed)
   3190                 .expect_err("compressed cap")
   3191                 .kind(),
   3192             ArtifactIoFailureKind::LimitExceeded(LimitKind::ArchiveCompressedBytes)
   3193         );
   3194 
   3195         let two_members = root.join("two-members.tar.gz");
   3196         write_archive(
   3197             &two_members,
   3198             &[
   3199                 ("one", EntryType::Regular, b"1"),
   3200                 ("two", EntryType::Regular, b"2"),
   3201             ],
   3202         );
   3203         let mut one_member = archive_limits();
   3204         one_member.max_members = 1;
   3205         assert_eq!(
   3206             admit_tar_gzip_path(&two_members, one_member)
   3207                 .expect_err("member count cap")
   3208                 .kind(),
   3209             ArtifactIoFailureKind::LimitExceeded(LimitKind::ArchiveMembers)
   3210         );
   3211 
   3212         let mut one_member_byte = archive_limits();
   3213         one_member_byte.max_member_bytes = 1;
   3214         assert_eq!(
   3215             admit_tar_gzip_path(&valid, one_member_byte)
   3216                 .expect_err("member byte cap")
   3217                 .kind(),
   3218             ArtifactIoFailureKind::LimitExceeded(LimitKind::ArchiveMemberBytes)
   3219         );
   3220 
   3221         let mut one_payload_byte = archive_limits();
   3222         one_payload_byte.max_payload_bytes = 1;
   3223         assert_eq!(
   3224             admit_tar_gzip_path(&two_members, one_payload_byte)
   3225                 .expect_err("payload cap")
   3226                 .kind(),
   3227             ArtifactIoFailureKind::LimitExceeded(LimitKind::ArchivePayloadBytes)
   3228         );
   3229 
   3230         let deep = root.join("deep.tar.gz");
   3231         write_archive(&deep, &[("a/b/c/d/e", EntryType::Regular, b"x")]);
   3232         assert_eq!(
   3233             admit_tar_gzip_path(&deep, archive_limits())
   3234                 .expect_err("archive depth cap")
   3235                 .kind(),
   3236             ArtifactIoFailureKind::LimitExceeded(LimitKind::ArchiveDepth)
   3237         );
   3238 
   3239         let long_path = root.join("long-path.tar.gz");
   3240         write_archive(
   3241             &long_path,
   3242             &[("a-long-member-name", EntryType::Regular, b"x")],
   3243         );
   3244         let mut short_path = archive_limits();
   3245         short_path.max_path_bytes = 4;
   3246         assert_eq!(
   3247             admit_tar_gzip_path(&long_path, short_path)
   3248                 .expect_err("archive path cap")
   3249                 .kind(),
   3250             ArtifactIoFailureKind::LimitExceeded(LimitKind::ArchivePathBytes)
   3251         );
   3252     }
   3253 
   3254     #[test]
   3255     fn archive_rejects_duplicates_prefix_conflicts_and_concatenation() {
   3256         let directory = TempDir::new().expect("tempdir");
   3257         let root = root(&directory);
   3258         let duplicate = root.join("duplicate.tar.gz");
   3259         write_archive(
   3260             &duplicate,
   3261             &[
   3262                 ("same", EntryType::Regular, b"one"),
   3263                 ("same", EntryType::Regular, b"two"),
   3264             ],
   3265         );
   3266         assert_eq!(
   3267             admit_tar_gzip_path(&duplicate, archive_limits())
   3268                 .expect_err("duplicate rejected")
   3269                 .kind(),
   3270             ArtifactIoFailureKind::InvalidObject
   3271         );
   3272 
   3273         let prefix = root.join("prefix.tar.gz");
   3274         write_archive(
   3275             &prefix,
   3276             &[
   3277                 ("a", EntryType::Regular, b"one"),
   3278                 ("a/b", EntryType::Regular, b"two"),
   3279             ],
   3280         );
   3281         assert_eq!(
   3282             admit_tar_gzip_path(&prefix, archive_limits())
   3283                 .expect_err("prefix rejected")
   3284                 .kind(),
   3285             ArtifactIoFailureKind::InvalidObject
   3286         );
   3287 
   3288         let first = root.join("first.tar.gz");
   3289         let second = root.join("second.tar.gz");
   3290         write_archive(&first, &[("one", EntryType::Regular, b"one")]);
   3291         write_archive(&second, &[("two", EntryType::Regular, b"two")]);
   3292         let second_bytes = fs::read(&second).expect("second bytes");
   3293         OpenOptions::new()
   3294             .append(true)
   3295             .open(&first)
   3296             .expect("append archive")
   3297             .write_all(&second_bytes)
   3298             .expect("concatenate");
   3299         assert_eq!(
   3300             admit_tar_gzip_path(&first, archive_limits())
   3301                 .expect_err("concatenated gzip rejected")
   3302                 .kind(),
   3303             ArtifactIoFailureKind::MalformedArchive
   3304         );
   3305 
   3306         let trailing_slash = root.join("trailing-slash.tar.gz");
   3307         write_raw_named_archive(&trailing_slash, b"file/", b"payload");
   3308         assert_eq!(
   3309             admit_tar_gzip_path(&trailing_slash, archive_limits())
   3310                 .expect_err("file trailing slash rejected")
   3311                 .kind(),
   3312             ArtifactIoFailureKind::InvalidObject
   3313         );
   3314 
   3315         let truncated = root.join("truncated-terminator.tar.gz");
   3316         write_single_terminator_archive(&truncated);
   3317         assert_eq!(
   3318             admit_tar_gzip_path(&truncated, archive_limits())
   3319                 .expect_err("single terminator rejected")
   3320                 .kind(),
   3321             ArtifactIoFailureKind::MalformedArchive
   3322         );
   3323 
   3324         let malformed = root.join("malformed.tar.gz");
   3325         fs::write(&malformed, b"not a gzip stream").expect("malformed archive");
   3326         assert_eq!(
   3327             admit_tar_gzip_path(&malformed, archive_limits())
   3328                 .expect_err("malformed gzip rejected")
   3329                 .kind(),
   3330             ArtifactIoFailureKind::MalformedArchive
   3331         );
   3332 
   3333         let gzip_truncated = root.join("gzip-truncated.tar.gz");
   3334         let mut truncated_bytes = fs::read(&second).expect("archive bytes");
   3335         truncated_bytes.truncate(truncated_bytes.len() - 4);
   3336         fs::write(&gzip_truncated, truncated_bytes).expect("truncated gzip");
   3337         assert_eq!(
   3338             admit_tar_gzip_path(&gzip_truncated, archive_limits())
   3339                 .expect_err("truncated gzip rejected")
   3340                 .kind(),
   3341             ArtifactIoFailureKind::MalformedArchive
   3342         );
   3343 
   3344         for (index, name) in [b"/absolute".as_slice(), b"./dot", b"a//b", b"a\\b"]
   3345             .into_iter()
   3346             .enumerate()
   3347         {
   3348             let path = root.join(format!("escape-{index}.tar.gz"));
   3349             write_raw_named_archive(&path, name, b"payload");
   3350             assert_eq!(
   3351                 admit_tar_gzip_path(&path, archive_limits())
   3352                     .expect_err("nonportable path rejected")
   3353                     .kind(),
   3354                 ArtifactIoFailureKind::InvalidObject
   3355             );
   3356         }
   3357 
   3358         for (index, kind) in [
   3359             EntryType::Symlink,
   3360             EntryType::Link,
   3361             EntryType::Fifo,
   3362             EntryType::Char,
   3363             EntryType::Block,
   3364             EntryType::Continuous,
   3365             EntryType::GNULongName,
   3366             EntryType::GNULongLink,
   3367             EntryType::GNUSparse,
   3368             EntryType::XGlobalHeader,
   3369             EntryType::XHeader,
   3370             EntryType::new(b'Z'),
   3371         ]
   3372         .into_iter()
   3373         .enumerate()
   3374         {
   3375             let path = root.join(format!("special-{index}.tar.gz"));
   3376             write_archive(&path, &[("special", kind, b"")]);
   3377             assert!(
   3378                 admit_tar_gzip_path(&path, archive_limits()).is_err(),
   3379                 "special archive member was admitted"
   3380             );
   3381         }
   3382     }
   3383 
   3384     #[test]
   3385     fn hardlinked_regular_inputs_are_rejected() {
   3386         let directory = TempDir::new().expect("tempdir");
   3387         let root = root(&directory);
   3388         fs::write(root.join("original"), b"bytes").expect("original");
   3389         fs::hard_link(root.join("original"), root.join("alias")).expect("hard link");
   3390         assert_eq!(
   3391             read_regular(&root, Path::new("original"), 8)
   3392                 .expect_err("hardlink rejected")
   3393                 .kind(),
   3394             ArtifactIoFailureKind::InvalidObject
   3395         );
   3396         assert_eq!(
   3397             traverse_regular_files(&root, traversal_limits(), &[])
   3398                 .expect_err("traversal hardlink rejected")
   3399                 .kind(),
   3400             ArtifactIoFailureKind::InvalidObject
   3401         );
   3402     }
   3403 
   3404     #[test]
   3405     fn diagnostics_are_redacted() {
   3406         let error = read_regular(
   3407             Path::new("/definitely-not-present-sensitive-root"),
   3408             Path::new("secret-token"),
   3409             1,
   3410         )
   3411         .expect_err("missing input");
   3412         let diagnostic = format!("{error}");
   3413         assert!(!diagnostic.contains("sensitive"));
   3414         assert!(!diagnostic.contains("secret"));
   3415     }
   3416 }
   3417 
   3418 #[cfg(all(test, unix))]
   3419 mod step_294_tests {
   3420     use std::fs::{self, File};
   3421     use std::io::Write as _;
   3422     use std::os::unix::fs::PermissionsExt as _;
   3423     use std::path::{Path, PathBuf};
   3424 
   3425     use flate2::{Compression, GzBuilder};
   3426     use sha2::{Digest as _, Sha256};
   3427     use tar::{Builder, EntryType, Header};
   3428     use tempfile::TempDir;
   3429 
   3430     use super::*;
   3431 
   3432     const REVIEWED_FLATE2_LOCK: &str = concat!(
   3433         "name = \"flate2\"\n",
   3434         "version = \"1.1.9\"\n",
   3435         "source = \"registry+https://github.com/rust-lang/crates.io-index\"\n",
   3436         "checksum = \"843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c\"",
   3437     );
   3438     const REVIEWED_MINIZ_OXIDE_LOCK: &str = concat!(
   3439         "name = \"miniz_oxide\"\n",
   3440         "version = \"0.8.9\"\n",
   3441         "source = \"registry+https://github.com/rust-lang/crates.io-index\"\n",
   3442         "checksum = \"1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316\"",
   3443     );
   3444     const CANONICAL_ARCHIVE_BYTE_LENGTH: u64 = 147;
   3445     const CANONICAL_ARCHIVE_SHA256: &str =
   3446         "6073c70a98ff9ab610085ed74b7886811da3fe64e9ec2771464300a94befd0fb";
   3447 
   3448     fn root(directory: &TempDir) -> PathBuf {
   3449         directory
   3450             .path()
   3451             .canonicalize()
   3452             .expect("canonical Step 294 test root")
   3453     }
   3454 
   3455     fn trusted_tempdir(prefix: &str) -> TempDir {
   3456         let current = std::env::current_dir()
   3457             .expect("Step 294 current directory")
   3458             .canonicalize()
   3459             .expect("canonical Step 294 current directory");
   3460         open_trusted_output_directory(&current).expect("trusted Step 294 current directory");
   3461         let mut builder = tempfile::Builder::new();
   3462         builder
   3463             .prefix(prefix)
   3464             .permissions(fs::Permissions::from_mode(0o700));
   3465         let directory = builder
   3466             .tempdir_in(current)
   3467             .expect("private Step 294 tempdir");
   3468         let canonical = root(&directory);
   3469         let (descriptor, _) =
   3470             open_trusted_output_directory(&canonical).expect("trusted private Step 294 tempdir");
   3471         let actual = identity(&descriptor).expect("Step 294 tempdir identity");
   3472         assert_eq!(actual.owner, rustix::process::geteuid().as_raw());
   3473         assert_eq!(permission_mode(actual), 0o700);
   3474         directory
   3475     }
   3476 
   3477     fn archive_limits() -> TarGzipLimits {
   3478         TarGzipLimits {
   3479             max_compressed_bytes: 64 * 1024,
   3480             max_expanded_bytes: 128 * 1024,
   3481             max_members: 8,
   3482             max_member_bytes: 32 * 1024,
   3483             max_payload_bytes: 64 * 1024,
   3484             max_depth: 4,
   3485             max_path_bytes: 128,
   3486         }
   3487     }
   3488 
   3489     fn write_deterministic_archive(path: &Path, payload: &[u8], extra_terminator: bool) {
   3490         let output = File::create(path).expect("Step 294 archive output");
   3491         let encoder = GzBuilder::new()
   3492             .mtime(0)
   3493             .operating_system(255)
   3494             .write(output, Compression::new(CANONICAL_GZIP_COMPRESSION_LEVEL));
   3495         let mut builder = Builder::new(encoder);
   3496 
   3497         let mut directory = Header::new_gnu();
   3498         directory
   3499             .set_path("snapshot/")
   3500             .expect("Step 294 directory path");
   3501         directory.set_entry_type(EntryType::Directory);
   3502         directory.set_mode(0o755);
   3503         directory.set_uid(0);
   3504         directory.set_gid(0);
   3505         directory.set_mtime(0);
   3506         directory.set_size(0);
   3507         directory.set_cksum();
   3508         builder
   3509             .append(&directory, &[][..])
   3510             .expect("Step 294 directory member");
   3511 
   3512         let mut file = Header::new_gnu();
   3513         file.set_path("snapshot/data.json")
   3514             .expect("Step 294 file path");
   3515         file.set_entry_type(EntryType::Regular);
   3516         file.set_mode(0o644);
   3517         file.set_uid(0);
   3518         file.set_gid(0);
   3519         file.set_mtime(0);
   3520         file.set_size(payload.len() as u64);
   3521         file.set_cksum();
   3522         builder
   3523             .append(&file, payload)
   3524             .expect("Step 294 file member");
   3525 
   3526         let mut encoder = builder.into_inner().expect("finish Step 294 tar");
   3527         if extra_terminator {
   3528             encoder
   3529                 .write_all(&[0_u8; TAR_BLOCK_BYTES])
   3530                 .expect("append noncanonical tar terminator");
   3531         }
   3532         encoder.finish().expect("finish Step 294 gzip");
   3533     }
   3534 
   3535     #[test]
   3536     fn canonical_tar_gzip_is_exactly_reencoded_before_admission() {
   3537         let directory = trusted_tempdir("radroots-step-294-source-");
   3538         let root = root(&directory);
   3539         let canonical = root.join("canonical.tar.gz");
   3540         let noncanonical = root.join("noncanonical.tar.gz");
   3541         let payload = br#"{"schema":"radroots.step-294.test.v1"}"#;
   3542         write_deterministic_archive(&canonical, payload, false);
   3543         write_deterministic_archive(&noncanonical, payload, true);
   3544 
   3545         let evidence = admit_tar_gzip_relative(
   3546             &root,
   3547             Path::new("canonical.tar.gz"),
   3548             archive_limits(),
   3549             None,
   3550             TarGzipPolicy::DeterministicSnapshot,
   3551         )
   3552         .expect("exact canonical archive admitted");
   3553         let cargo_lock = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../Cargo.lock"));
   3554         assert_eq!(cargo_lock.matches(REVIEWED_FLATE2_LOCK).count(), 1);
   3555         assert_eq!(cargo_lock.matches(REVIEWED_MINIZ_OXIDE_LOCK).count(), 1);
   3556         assert_eq!(
   3557             evidence.compressed.byte_length,
   3558             CANONICAL_ARCHIVE_BYTE_LENGTH
   3559         );
   3560         assert_eq!(evidence.compressed.sha256, CANONICAL_ARCHIVE_SHA256);
   3561         assert_eq!(evidence.member_count, 2);
   3562         assert_eq!(evidence.payload_bytes, payload.len() as u64);
   3563         assert_eq!(
   3564             evidence.compressed.sha256,
   3565             hex::encode(Sha256::digest(
   3566                 fs::read(&canonical).expect("canonical bytes")
   3567             ))
   3568         );
   3569 
   3570         admit_tar_gzip_path(&noncanonical, archive_limits())
   3571             .expect("generic parser accepts safe extended terminator");
   3572         assert_eq!(
   3573             admit_tar_gzip_relative(
   3574                 &root,
   3575                 Path::new("noncanonical.tar.gz"),
   3576                 archive_limits(),
   3577                 None,
   3578                 TarGzipPolicy::DeterministicSnapshot,
   3579             )
   3580             .expect_err("byte-noncanonical archive rejected")
   3581             .kind(),
   3582             ArtifactIoFailureKind::InvalidObject
   3583         );
   3584     }
   3585 
   3586     #[test]
   3587     fn materialization_retains_exact_member_and_parent_bindings() {
   3588         let directory = trusted_tempdir("radroots-step-294-source-");
   3589         let source_root = root(&directory);
   3590         let archive = source_root.join("canonical.tar.gz");
   3591         let payload = br#"{"snapshot":"immutable"}"#;
   3592         write_deterministic_archive(&archive, payload, false);
   3593 
   3594         let parent = trusted_tempdir("radroots-step-294-materialization-");
   3595         let parent_root = root(&parent);
   3596         let materialized = materialize_tar_gzip_relative(
   3597             &source_root,
   3598             Path::new("canonical.tar.gz"),
   3599             &parent_root,
   3600             archive_limits(),
   3601             None,
   3602             TarGzipPolicy::DeterministicSnapshot,
   3603         )
   3604         .expect("descriptor-bound materialization");
   3605         let snapshot = materialized.snapshot();
   3606         assert_eq!(snapshot.entry_count(), 2);
   3607         assert_eq!(snapshot.files().len(), 1);
   3608         assert_eq!(snapshot.total_bytes(), payload.len() as u64);
   3609         assert_eq!(
   3610             snapshot
   3611                 .hash(&snapshot.files()[0], payload.len() as u64)
   3612                 .expect("bound member hash")
   3613                 .sha256,
   3614             hex::encode(Sha256::digest(payload))
   3615         );
   3616         materialized
   3617             .revalidate()
   3618             .expect("unchanged materialization");
   3619 
   3620         fs::write(materialized.root().join("snapshot/data.json"), b"changed")
   3621             .expect("mutate materialized member");
   3622         assert_eq!(
   3623             materialized
   3624                 .revalidate()
   3625                 .expect_err("member mutation invalidates materialization")
   3626                 .kind(),
   3627             ArtifactIoFailureKind::ChangedDuringRead
   3628         );
   3629     }
   3630 
   3631     #[test]
   3632     fn materialization_rejects_parent_root_and_ancestor_rebinding() {
   3633         let source = trusted_tempdir("radroots-materialization-source-");
   3634         let source_root = root(&source);
   3635         write_deterministic_archive(&source_root.join("archive.tar.gz"), b"immutable", false);
   3636         for case in [
   3637             "parent-mode",
   3638             "root-mode",
   3639             "root-replacement",
   3640             "ancestor-replacement",
   3641             "ancestor-mode",
   3642         ] {
   3643             let parent = trusted_tempdir("radroots-materialization-parent-");
   3644             let base = root(&parent);
   3645             let ancestor = base.join("ancestor");
   3646             let output = ancestor.join("output");
   3647             fs::create_dir_all(&output).unwrap();
   3648             fs::set_permissions(&ancestor, fs::Permissions::from_mode(0o700)).unwrap();
   3649             fs::set_permissions(&output, fs::Permissions::from_mode(0o700)).unwrap();
   3650             let materialized = materialize_tar_gzip_relative(
   3651                 &source_root,
   3652                 Path::new("archive.tar.gz"),
   3653                 &output,
   3654                 archive_limits(),
   3655                 None,
   3656                 TarGzipPolicy::DeterministicSnapshot,
   3657             )
   3658             .unwrap();
   3659             materialized.revalidate().unwrap();
   3660             match case {
   3661                 "parent-mode" => {
   3662                     fs::set_permissions(&output, fs::Permissions::from_mode(0o750)).unwrap()
   3663                 }
   3664                 "root-mode" => {
   3665                     fs::set_permissions(materialized.root(), fs::Permissions::from_mode(0o750))
   3666                         .unwrap()
   3667                 }
   3668                 "root-replacement" => {
   3669                     fs::rename(materialized.root(), base.join("retained-root")).unwrap();
   3670                     fs::create_dir(materialized.root()).unwrap();
   3671                     fs::set_permissions(materialized.root(), fs::Permissions::from_mode(0o700))
   3672                         .unwrap();
   3673                 }
   3674                 "ancestor-replacement" => {
   3675                     fs::rename(&ancestor, base.join("retained-ancestor")).unwrap();
   3676                     fs::create_dir(&ancestor).unwrap();
   3677                     fs::set_permissions(&ancestor, fs::Permissions::from_mode(0o700)).unwrap();
   3678                     fs::rename(base.join("retained-ancestor/output"), &output).unwrap();
   3679                 }
   3680                 "ancestor-mode" => {
   3681                     fs::set_permissions(&ancestor, fs::Permissions::from_mode(0o750)).unwrap()
   3682                 }
   3683                 _ => unreachable!(),
   3684             }
   3685             assert_eq!(
   3686                 materialized.revalidate().unwrap_err().kind(),
   3687                 ArtifactIoFailureKind::ChangedDuringRead,
   3688                 "{case}"
   3689             );
   3690         }
   3691     }
   3692 }