diff --git a/crates/commandf-pkg/src/registry.rs b/crates/commandf-pkg/src/registry.rs index 781cb41c..b1f6518a 100644 --- a/crates/commandf-pkg/src/registry.rs +++ b/crates/commandf-pkg/src/registry.rs @@ -5,13 +5,14 @@ use semver::Version; use serde::Deserialize; use ureq::Agent; -use crate::{PackageArchive, PackageError, PackageName, PackageSource}; +use crate::{ + source::MAX_PACKAGE_ARCHIVE_BYTES, PackageArchive, PackageError, PackageName, PackageSource, +}; const PRIMARY: &str = "https://packages.fhir.org"; const SECONDARY: &str = "https://packages2.fhir.org/packages"; const SECONDARY_TARBALL_BASE: &str = "https://packages2.fhir.org/web"; const METADATA_LIMIT: u64 = 4 * 1024 * 1024; -const ARCHIVE_LIMIT: u64 = 128 * 1024 * 1024; const REQUEST_TIMEOUT: Duration = Duration::from_secs(30); const GZIP_MAGIC: [u8; 2] = [0x1f, 0x8b]; @@ -147,7 +148,7 @@ fn read_archive_body( response .body_mut() .with_config() - .limit(ARCHIVE_LIMIT) + .limit(MAX_PACKAGE_ARCHIVE_BYTES) .read_to_vec() .map_err(|error| format!("registry archive body from {url} failed: {error}")) } diff --git a/crates/commandf-pkg/src/source.rs b/crates/commandf-pkg/src/source.rs index 7da9da5d..89df1186 100644 --- a/crates/commandf-pkg/src/source.rs +++ b/crates/commandf-pkg/src/source.rs @@ -1,10 +1,13 @@ use std::fs; +use std::io::Read; use std::path::{Path, PathBuf}; use semver::Version; use crate::{PackageError, PackageName}; +pub(crate) const MAX_PACKAGE_ARCHIVE_BYTES: u64 = 128 * 1024 * 1024; + #[derive(Clone, Debug, Eq, PartialEq)] pub struct PackageArchive { pub bytes: Vec, @@ -83,7 +86,7 @@ impl PackageSource for LocalMirrorSource { .join(name.as_str()) .join(version.to_string()) .join("package.tgz"); - fs::read(path).map_err(|error| { + let file = fs::File::open(path).map_err(|error| { if error.kind() == std::io::ErrorKind::NotFound { PackageError::PackageNotFound { name: name.to_string(), @@ -92,6 +95,40 @@ impl PackageSource for LocalMirrorSource { } else { PackageError::Io(error) } - }) + })?; + read_bounded_archive(file, MAX_PACKAGE_ARCHIVE_BYTES) + } +} + +fn read_bounded_archive(reader: R, max_bytes: u64) -> Result, PackageError> { + let mut bytes = Vec::new(); + reader + .take(max_bytes.saturating_add(1)) + .read_to_end(&mut bytes)?; + if bytes.len() as u64 > max_bytes { + return Err(PackageError::InvalidRequest(format!( + "package archive exceeds the maximum supported compressed size of {max_bytes} bytes" + ))); + } + Ok(bytes) +} + +#[cfg(test)] +mod tests { + use std::io::Cursor; + + use super::*; + + #[test] + fn bounded_archive_reader_accepts_exact_limit() { + let bytes = read_bounded_archive(Cursor::new(b"abcd"), 4).expect("exact bound"); + assert_eq!(bytes, b"abcd"); + } + + #[test] + fn bounded_archive_reader_rejects_limit_plus_one() { + let error = read_bounded_archive(Cursor::new(b"abcde"), 4) + .expect_err("limit plus one must fail closed"); + assert!(matches!(error, PackageError::InvalidRequest(_))); } }