From 9eda1a6b9235b714d9acba836540989ceb48d71d Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Sat, 15 Aug 2026 03:34:06 +0300 Subject: [PATCH 01/16] fix(pkg): validate and follow bounded registry tarball redirect --- crates/commandf-pkg/src/registry.rs | 168 +++++++++++++++++++++++++++- 1 file changed, 162 insertions(+), 6 deletions(-) diff --git a/crates/commandf-pkg/src/registry.rs b/crates/commandf-pkg/src/registry.rs index 212ab569..9f419d8a 100644 --- a/crates/commandf-pkg/src/registry.rs +++ b/crates/commandf-pkg/src/registry.rs @@ -9,9 +9,11 @@ use crate::{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]; #[derive(Debug, Deserialize)] struct RegistryMetadata { @@ -77,14 +79,92 @@ impl FhirRegistrySource { .get(&url) .call() .map_err(|error| error.to_string())?; - let bytes = response - .body_mut() - .with_config() - .limit(ARCHIVE_LIMIT) - .read_to_vec() - .map_err(|error| error.to_string())?; + + let status = response.status().as_u16(); + if (300..400).contains(&status) { + let location = response + .headers() + .get("location") + .and_then(|value| value.to_str().ok()) + .ok_or_else(|| format!("registry redirect from {url} omitted a valid Location header"))?; + let target = validated_secondary_redirect(endpoint, status, name, version, location)?; + return self.direct_archive_from_url(&target); + } + + if !(200..300).contains(&status) { + return Err(format!("registry download from {url} returned HTTP {status}")); + } + + let bytes = read_archive_body(&mut response, &url)?; + validate_gzip_archive(&bytes, &url)?; Ok(PackageArchive { bytes, source: url }) } + + fn direct_archive_from_url(&self, url: &str) -> Result { + let mut response = self + .agent + .get(url) + .call() + .map_err(|error| error.to_string())?; + let status = response.status().as_u16(); + if !(200..300).contains(&status) { + return Err(format!( + "registry tarball download from {url} returned HTTP {status}; redirects are not followed recursively" + )); + } + let bytes = read_archive_body(&mut response, url)?; + validate_gzip_archive(&bytes, url)?; + Ok(PackageArchive { + bytes, + source: url.to_owned(), + }) + } +} + +fn read_archive_body( + response: &mut ureq::http::Response, + url: &str, +) -> Result, String> { + response + .body_mut() + .with_config() + .limit(ARCHIVE_LIMIT) + .read_to_vec() + .map_err(|error| format!("registry archive body from {url} failed: {error}")) +} + +fn validate_gzip_archive(bytes: &[u8], url: &str) -> Result<(), String> { + if !bytes.starts_with(&GZIP_MAGIC) { + return Err(format!( + "registry response from {url} is not a gzip package archive" + )); + } + Ok(()) +} + +fn expected_secondary_tarball(name: &PackageName, version: &Version) -> String { + format!("{SECONDARY_TARBALL_BASE}/{name}-{version}.tgz") +} + +fn validated_secondary_redirect( + endpoint: &str, + status: u16, + name: &PackageName, + version: &Version, + location: &str, +) -> Result { + if endpoint != SECONDARY || status != 302 { + return Err(format!( + "unexpected registry redirect for {name}@{version}: endpoint={endpoint} status={status}" + )); + } + let expected = expected_secondary_tarball(name, version); + if location != expected { + return Err(format!( + "unexpected secondary registry redirect for {name}@{version}: expected {expected}, found {location}" + )); + } + Ok(expected) } impl PackageSource for FhirRegistrySource { @@ -141,3 +221,79 @@ impl PackageSource for FhirRegistrySource { ))) } } + +#[cfg(test)] +mod tests { + use super::*; + + fn package() -> PackageName { + PackageName::parse("hl7.fhir.us.core").unwrap() + } + + fn version() -> Version { + Version::parse("8.0.1").unwrap() + } + + #[test] + fn accepts_expected_secondary_redirect_only() { + let name = package(); + let version = version(); + let expected = "https://packages2.fhir.org/web/hl7.fhir.us.core-8.0.1.tgz"; + assert_eq!( + validated_secondary_redirect(SECONDARY, 302, &name, &version, expected).unwrap(), + expected + ); + } + + #[test] + fn rejects_redirect_from_primary_endpoint() { + let error = validated_secondary_redirect( + PRIMARY, + 302, + &package(), + &version(), + "https://packages2.fhir.org/web/hl7.fhir.us.core-8.0.1.tgz", + ) + .unwrap_err(); + assert!(error.contains("unexpected registry redirect")); + } + + #[test] + fn rejects_unexpected_secondary_redirect_target() { + let error = validated_secondary_redirect( + SECONDARY, + 302, + &package(), + &version(), + "https://example.invalid/hl7.fhir.us.core-8.0.1.tgz", + ) + .unwrap_err(); + assert!(error.contains("unexpected secondary registry redirect")); + } + + #[test] + fn rejects_non_302_secondary_redirect() { + let error = validated_secondary_redirect( + SECONDARY, + 307, + &package(), + &version(), + "https://packages2.fhir.org/web/hl7.fhir.us.core-8.0.1.tgz", + ) + .unwrap_err(); + assert!(error.contains("unexpected registry redirect")); + } + + #[test] + fn rejects_non_gzip_registry_body() { + let error = validate_gzip_archive(b"Found. Redirecting", "https://example.test/package") + .unwrap_err(); + assert!(error.contains("not a gzip package archive")); + } + + #[test] + fn accepts_gzip_magic() { + validate_gzip_archive(&[0x1f, 0x8b, 0x08, 0x00], "https://example.test/package") + .unwrap(); + } +} From 5b8753ccfd7be53d2f62df1c2f58004f8c6f1613 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Sat, 15 Aug 2026 03:35:08 +0300 Subject: [PATCH 02/16] test(pkg): cover real FHIR registry archive paths --- crates/commandf-pkg/src/registry.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/crates/commandf-pkg/src/registry.rs b/crates/commandf-pkg/src/registry.rs index 9f419d8a..00bf7799 100644 --- a/crates/commandf-pkg/src/registry.rs +++ b/crates/commandf-pkg/src/registry.rs @@ -296,4 +296,30 @@ mod tests { validate_gzip_archive(&[0x1f, 0x8b, 0x08, 0x00], "https://example.test/package") .unwrap(); } + + #[test] + #[ignore = "requires public packages.fhir.org network access"] + fn real_primary_us_core_is_direct_gzip() { + let archive = FhirRegistrySource::new() + .archive_from(PRIMARY, &package(), &version()) + .unwrap(); + assert!(archive.bytes.starts_with(&GZIP_MAGIC)); + assert_eq!( + archive.source, + "https://packages.fhir.org/hl7.fhir.us.core/8.0.1" + ); + } + + #[test] + #[ignore = "requires public packages2.fhir.org network access"] + fn real_secondary_us_core_follows_only_expected_tarball() { + let archive = FhirRegistrySource::new() + .archive_from(SECONDARY, &package(), &version()) + .unwrap(); + assert!(archive.bytes.starts_with(&GZIP_MAGIC)); + assert_eq!( + archive.source, + "https://packages2.fhir.org/web/hl7.fhir.us.core-8.0.1.tgz" + ); + } } From c7be69a545dcb71ce8b41c701d672a8d60a3e522 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Sat, 15 Aug 2026 03:35:22 +0300 Subject: [PATCH 03/16] ci(pkg): add bounded real registry download smoke --- .github/workflows/registry-download-smoke.yml | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 .github/workflows/registry-download-smoke.yml diff --git a/.github/workflows/registry-download-smoke.yml b/.github/workflows/registry-download-smoke.yml new file mode 100644 index 00000000..22d2315f --- /dev/null +++ b/.github/workflows/registry-download-smoke.yml @@ -0,0 +1,69 @@ +name: registry-download-smoke + +on: + pull_request: + paths: + - crates/commandf-pkg/src/registry.rs + - .github/workflows/registry-download-smoke.yml + push: + branches: + - main + paths: + - crates/commandf-pkg/src/registry.rs + - .github/workflows/registry-download-smoke.yml + workflow_dispatch: + +permissions: + contents: read + +jobs: + registry-download: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@1.97.1 + with: + components: rustfmt, clippy + + - name: Format + run: cargo fmt --all -- --check + + - name: Focused registry unit tests + run: cargo test --locked -p commandf-pkg registry::tests -- --skip real_primary_us_core_is_direct_gzip --skip real_secondary_us_core_follows_only_expected_tarball + + - name: Real primary US Core archive response + run: cargo test --locked -p commandf-pkg registry::tests::real_primary_us_core_is_direct_gzip -- --ignored --exact + + - name: Real secondary redirect-to-tarball response + run: cargo test --locked -p commandf-pkg registry::tests::real_secondary_us_core_follows_only_expected_tarball -- --ignored --exact + + - name: End-to-end exact US Core resolve and verify + run: | + set -euo pipefail + rm -rf /tmp/commandf-registry-smoke + cargo run --locked --quiet -p commandf -- \ + pkg resolve hl7.fhir.us.core@8.0.1 \ + --cache /tmp/commandf-registry-smoke/cache \ + --lock /tmp/commandf-registry-smoke/commandf.lock + cargo run --locked --quiet -p commandf -- \ + pkg verify \ + --cache /tmp/commandf-registry-smoke/cache \ + --lock /tmp/commandf-registry-smoke/commandf.lock + python - <<'PY' + import json + from pathlib import Path + + lock = json.loads(Path('/tmp/commandf-registry-smoke/commandf.lock').read_text()) + matches = [ + item for item in lock['packages'] + if item['name'] == 'hl7.fhir.us.core' and item['version'] == '8.0.1' + ] + assert len(matches) == 1 + package = matches[0] + assert len(package['sha256']) == 64 + assert package['source'] in { + 'https://packages.fhir.org/hl7.fhir.us.core/8.0.1', + 'https://packages2.fhir.org/web/hl7.fhir.us.core-8.0.1.tgz', + } + PY From ddc7e83fe5df8e6f907f76df0681909ccc220826 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Sat, 15 Aug 2026 03:36:42 +0300 Subject: [PATCH 04/16] style(pkg): apply rustfmt to registry hotfix --- crates/commandf-pkg/src/registry.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/commandf-pkg/src/registry.rs b/crates/commandf-pkg/src/registry.rs index 00bf7799..1f326e6e 100644 --- a/crates/commandf-pkg/src/registry.rs +++ b/crates/commandf-pkg/src/registry.rs @@ -86,13 +86,17 @@ impl FhirRegistrySource { .headers() .get("location") .and_then(|value| value.to_str().ok()) - .ok_or_else(|| format!("registry redirect from {url} omitted a valid Location header"))?; + .ok_or_else(|| { + format!("registry redirect from {url} omitted a valid Location header") + })?; let target = validated_secondary_redirect(endpoint, status, name, version, location)?; return self.direct_archive_from_url(&target); } if !(200..300).contains(&status) { - return Err(format!("registry download from {url} returned HTTP {status}")); + return Err(format!( + "registry download from {url} returned HTTP {status}" + )); } let bytes = read_archive_body(&mut response, &url)?; @@ -293,8 +297,7 @@ mod tests { #[test] fn accepts_gzip_magic() { - validate_gzip_archive(&[0x1f, 0x8b, 0x08, 0x00], "https://example.test/package") - .unwrap(); + validate_gzip_archive(&[0x1f, 0x8b, 0x08, 0x00], "https://example.test/package").unwrap(); } #[test] From be94f92682e9b3ea12f20e89bb80cd0fc65ef6b1 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Sat, 15 Aug 2026 03:52:22 +0300 Subject: [PATCH 05/16] test(pkg): measure official US Core archive layout --- .github/workflows/registry-download-smoke.yml | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/.github/workflows/registry-download-smoke.yml b/.github/workflows/registry-download-smoke.yml index 22d2315f..8229b216 100644 --- a/.github/workflows/registry-download-smoke.yml +++ b/.github/workflows/registry-download-smoke.yml @@ -38,6 +38,66 @@ jobs: - name: Real secondary redirect-to-tarball response run: cargo test --locked -p commandf-pkg registry::tests::real_secondary_us_core_follows_only_expected_tarball -- --ignored --exact + - name: Diagnostic US Core archive layout + run: | + set -euo pipefail + python - <<'PY' + import gzip + import io + import json + import tarfile + import urllib.request + + LIMIT = 128 * 1024 * 1024 + url = 'https://packages.fhir.org/hl7.fhir.us.core/8.0.1' + with urllib.request.urlopen(url, timeout=30) as response: + data = response.read(LIMIT + 1) + if len(data) > LIMIT: + raise SystemExit('compressed archive exceeds 128 MiB probe bound') + if not data.startswith(b'\x1f\x8b'): + raise SystemExit('not gzip') + + class CountingReader: + def __init__(self, inner): + self.inner = inner + self.read_bytes = 0 + def read(self, size=-1): + chunk = self.inner.read(size) + self.read_bytes += len(chunk) + return chunk + + gz = gzip.GzipFile(fileobj=io.BytesIO(data), mode='rb') + counted = CountingReader(gz) + entries_before = 0 + payload_before = 0 + max_entry = ('', 0) + manifest = None + with tarfile.open(fileobj=counted, mode='r|') as tf: + for member in tf: + normalized = member.name[2:] if member.name.startswith('./') else member.name + if member.size > max_entry[1]: + max_entry = (member.name, member.size) + if normalized == 'package/package.json': + manifest = { + 'index': entries_before, + 'size': member.size, + 'decompressed_bytes_read_to_header': counted.read_bytes, + } + break + entries_before += 1 + payload_before += member.size + if manifest is None: + raise SystemExit('package/package.json not found') + print(json.dumps({ + 'compressed_bytes': len(data), + 'entries_before_package_json': entries_before, + 'payload_bytes_before_package_json': payload_before, + 'manifest': manifest, + 'largest_entry_seen': {'name': max_entry[0], 'bytes': max_entry[1]}, + 'read_to_manifest_ratio': round(manifest['decompressed_bytes_read_to_header'] / len(data), 3), + }, indent=2)) + PY + - name: End-to-end exact US Core resolve and verify run: | set -euo pipefail From 6456adbf4fa766f4814b0d2b00963dd225e0f889 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Sat, 15 Aug 2026 03:56:28 +0300 Subject: [PATCH 06/16] test(pkg): locate dependency manifest bound failure --- .github/workflows/registry-download-smoke.yml | 114 ++++++++++++------ 1 file changed, 75 insertions(+), 39 deletions(-) diff --git a/.github/workflows/registry-download-smoke.yml b/.github/workflows/registry-download-smoke.yml index 8229b216..b1ba9408 100644 --- a/.github/workflows/registry-download-smoke.yml +++ b/.github/workflows/registry-download-smoke.yml @@ -38,7 +38,7 @@ jobs: - name: Real secondary redirect-to-tarball response run: cargo test --locked -p commandf-pkg registry::tests::real_secondary_us_core_follows_only_expected_tarball -- --ignored --exact - - name: Diagnostic US Core archive layout + - name: Diagnostic dependency manifest layout run: | set -euo pipefail python - <<'PY' @@ -48,54 +48,90 @@ jobs: import tarfile import urllib.request - LIMIT = 128 * 1024 * 1024 - url = 'https://packages.fhir.org/hl7.fhir.us.core/8.0.1' - with urllib.request.urlopen(url, timeout=30) as response: - data = response.read(LIMIT + 1) - if len(data) > LIMIT: - raise SystemExit('compressed archive exceeds 128 MiB probe bound') - if not data.startswith(b'\x1f\x8b'): - raise SystemExit('not gzip') + COMPRESSED_LIMIT = 128 * 1024 * 1024 + DIAGNOSTIC_DECOMPRESSED_LIMIT = 768 * 1024 * 1024 + + def read_url(url, limit=COMPRESSED_LIMIT): + with urllib.request.urlopen(url, timeout=30) as response: + data = response.read(limit + 1) + if len(data) > limit: + raise RuntimeError(f'compressed response exceeds diagnostic bound: {url}') + return data class CountingReader: def __init__(self, inner): self.inner = inner self.read_bytes = 0 def read(self, size=-1): + remaining = DIAGNOSTIC_DECOMPRESSED_LIMIT - self.read_bytes + if remaining <= 0: + raise RuntimeError('diagnostic decompressed bound exceeded before manifest') + if size < 0 or size > remaining: + size = remaining chunk = self.inner.read(size) self.read_bytes += len(chunk) return chunk - gz = gzip.GzipFile(fileobj=io.BytesIO(data), mode='rb') - counted = CountingReader(gz) - entries_before = 0 - payload_before = 0 - max_entry = ('', 0) - manifest = None - with tarfile.open(fileobj=counted, mode='r|') as tf: - for member in tf: - normalized = member.name[2:] if member.name.startswith('./') else member.name - if member.size > max_entry[1]: - max_entry = (member.name, member.size) - if normalized == 'package/package.json': - manifest = { - 'index': entries_before, - 'size': member.size, - 'decompressed_bytes_read_to_header': counted.read_bytes, - } - break - entries_before += 1 - payload_before += member.size - if manifest is None: - raise SystemExit('package/package.json not found') - print(json.dumps({ - 'compressed_bytes': len(data), - 'entries_before_package_json': entries_before, - 'payload_bytes_before_package_json': payload_before, - 'manifest': manifest, - 'largest_entry_seen': {'name': max_entry[0], 'bytes': max_entry[1]}, - 'read_to_manifest_ratio': round(manifest['decompressed_bytes_read_to_header'] / len(data), 3), - }, indent=2)) + def parse_version(text): + core = text.split('-', 1)[0].split('+', 1)[0] + parts = core.split('.') + if len(parts) != 3 or not all(p.isdigit() for p in parts): + return None + if '-' in text: + return None + return tuple(map(int, parts)) + + def select_version(name, constraint): + if not constraint.endswith('.x'): + return constraint + major, minor, _ = constraint.split('.') + metadata = json.loads(read_url(f'https://packages.fhir.org/{name}', 4 * 1024 * 1024)) + matches = [] + for raw in metadata.get('versions', {}): + parsed = parse_version(raw) + if parsed and parsed[0] == int(major) and parsed[1] == int(minor): + matches.append((parsed, raw)) + if not matches: + raise RuntimeError(f'no patch wildcard match for {name}@{constraint}') + return max(matches)[1] + + def inspect(name, version): + url = f'https://packages.fhir.org/{name}/{version}' + data = read_url(url) + if not data.startswith(b'\x1f\x8b'): + raise RuntimeError(f'not gzip: {name}@{version}') + counted = CountingReader(gzip.GzipFile(fileobj=io.BytesIO(data), mode='rb')) + entries_before = 0 + payload_before = 0 + with tarfile.open(fileobj=counted, mode='r|') as tf: + for member in tf: + normalized = member.name[2:] if member.name.startswith('./') else member.name + if normalized == 'package/package.json': + body = tf.extractfile(member).read() + manifest = json.loads(body) + return { + 'name': name, + 'version': version, + 'compressed_bytes': len(data), + 'entries_before_manifest': entries_before, + 'payload_bytes_before_manifest': payload_before, + 'decompressed_bytes_read_to_manifest': counted.read_bytes, + 'manifest_bytes': member.size, + 'dependencies': manifest.get('dependencies', {}), + } + entries_before += 1 + payload_before += member.size + raise RuntimeError(f'manifest not found: {name}@{version}') + + root = inspect('hl7.fhir.us.core', '8.0.1') + print('ROOT') + print(json.dumps(root, indent=2, sort_keys=True)) + print('DIRECT DEPENDENCIES') + for name, constraint in sorted(root['dependencies'].items()): + version = select_version(name, constraint) + result = inspect(name, version) + result['constraint'] = constraint + print(json.dumps(result, indent=2, sort_keys=True)) PY - name: End-to-end exact US Core resolve and verify From 77cea4fc9977e1bcf060b3b80df7d8a3a1c47059 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Sat, 15 Aug 2026 03:58:43 +0300 Subject: [PATCH 07/16] test(pkg): measure VSAC fallback manifest position --- .github/workflows/registry-download-smoke.yml | 106 ++++++------------ 1 file changed, 37 insertions(+), 69 deletions(-) diff --git a/.github/workflows/registry-download-smoke.yml b/.github/workflows/registry-download-smoke.yml index b1ba9408..80c0e2a0 100644 --- a/.github/workflows/registry-download-smoke.yml +++ b/.github/workflows/registry-download-smoke.yml @@ -38,7 +38,7 @@ jobs: - name: Real secondary redirect-to-tarball response run: cargo test --locked -p commandf-pkg registry::tests::real_secondary_us_core_follows_only_expected_tarball -- --ignored --exact - - name: Diagnostic dependency manifest layout + - name: Diagnostic VSAC fallback manifest layout run: | set -euo pipefail python - <<'PY' @@ -49,14 +49,14 @@ jobs: import urllib.request COMPRESSED_LIMIT = 128 * 1024 * 1024 - DIAGNOSTIC_DECOMPRESSED_LIMIT = 768 * 1024 * 1024 - - def read_url(url, limit=COMPRESSED_LIMIT): - with urllib.request.urlopen(url, timeout=30) as response: - data = response.read(limit + 1) - if len(data) > limit: - raise RuntimeError(f'compressed response exceeds diagnostic bound: {url}') - return data + DIAGNOSTIC_DECOMPRESSED_LIMIT = 1024 * 1024 * 1024 + url = 'https://packages2.fhir.org/web/us.nlm.vsac-0.24.0.tgz' + with urllib.request.urlopen(url, timeout=30) as response: + data = response.read(COMPRESSED_LIMIT + 1) + if len(data) > COMPRESSED_LIMIT: + raise SystemExit('compressed archive exceeds 128 MiB diagnostic bound') + if not data.startswith(b'\x1f\x8b'): + raise SystemExit('VSAC fallback tarball is not gzip') class CountingReader: def __init__(self, inner): @@ -72,66 +72,34 @@ jobs: self.read_bytes += len(chunk) return chunk - def parse_version(text): - core = text.split('-', 1)[0].split('+', 1)[0] - parts = core.split('.') - if len(parts) != 3 or not all(p.isdigit() for p in parts): - return None - if '-' in text: - return None - return tuple(map(int, parts)) - - def select_version(name, constraint): - if not constraint.endswith('.x'): - return constraint - major, minor, _ = constraint.split('.') - metadata = json.loads(read_url(f'https://packages.fhir.org/{name}', 4 * 1024 * 1024)) - matches = [] - for raw in metadata.get('versions', {}): - parsed = parse_version(raw) - if parsed and parsed[0] == int(major) and parsed[1] == int(minor): - matches.append((parsed, raw)) - if not matches: - raise RuntimeError(f'no patch wildcard match for {name}@{constraint}') - return max(matches)[1] - - def inspect(name, version): - url = f'https://packages.fhir.org/{name}/{version}' - data = read_url(url) - if not data.startswith(b'\x1f\x8b'): - raise RuntimeError(f'not gzip: {name}@{version}') - counted = CountingReader(gzip.GzipFile(fileobj=io.BytesIO(data), mode='rb')) - entries_before = 0 - payload_before = 0 - with tarfile.open(fileobj=counted, mode='r|') as tf: - for member in tf: - normalized = member.name[2:] if member.name.startswith('./') else member.name - if normalized == 'package/package.json': - body = tf.extractfile(member).read() - manifest = json.loads(body) - return { - 'name': name, - 'version': version, - 'compressed_bytes': len(data), - 'entries_before_manifest': entries_before, - 'payload_bytes_before_manifest': payload_before, - 'decompressed_bytes_read_to_manifest': counted.read_bytes, - 'manifest_bytes': member.size, - 'dependencies': manifest.get('dependencies', {}), - } - entries_before += 1 - payload_before += member.size - raise RuntimeError(f'manifest not found: {name}@{version}') - - root = inspect('hl7.fhir.us.core', '8.0.1') - print('ROOT') - print(json.dumps(root, indent=2, sort_keys=True)) - print('DIRECT DEPENDENCIES') - for name, constraint in sorted(root['dependencies'].items()): - version = select_version(name, constraint) - result = inspect(name, version) - result['constraint'] = constraint - print(json.dumps(result, indent=2, sort_keys=True)) + counted = CountingReader(gzip.GzipFile(fileobj=io.BytesIO(data), mode='rb')) + entries_before = 0 + payload_before = 0 + max_entry = ('', 0) + with tarfile.open(fileobj=counted, mode='r|') as tf: + for member in tf: + normalized = member.name[2:] if member.name.startswith('./') else member.name + if member.size > max_entry[1]: + max_entry = (member.name, member.size) + if normalized == 'package/package.json': + body = tf.extractfile(member).read() + manifest = json.loads(body) + print(json.dumps({ + 'compressed_bytes': len(data), + 'entries_before_manifest': entries_before, + 'payload_bytes_before_manifest': payload_before, + 'decompressed_bytes_read_to_manifest': counted.read_bytes, + 'manifest_bytes': member.size, + 'largest_entry_seen_before_manifest': {'name': max_entry[0], 'bytes': max_entry[1]}, + 'manifest_name': manifest.get('name'), + 'manifest_version': manifest.get('version'), + 'dependencies': manifest.get('dependencies', {}), + }, indent=2, sort_keys=True)) + break + entries_before += 1 + payload_before += member.size + else: + raise SystemExit('package/package.json not found') PY - name: End-to-end exact US Core resolve and verify From 8da4a423ff2c0ed512ed8a4f883967138c4c67b6 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Sat, 15 Aug 2026 04:01:02 +0300 Subject: [PATCH 08/16] fix(pkg): bound late-manifest decompression adaptively --- crates/commandf-pkg/src/archive.rs | 40 ++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/crates/commandf-pkg/src/archive.rs b/crates/commandf-pkg/src/archive.rs index f1127060..58cca035 100644 --- a/crates/commandf-pkg/src/archive.rs +++ b/crates/commandf-pkg/src/archive.rs @@ -7,7 +7,9 @@ use tar::Archive; use crate::{model::PackageManifest, PackageError}; const MAX_MANIFEST_BYTES: u64 = 1024 * 1024; -const MAX_ARCHIVE_DECOMPRESSED_BYTES: u64 = 512 * 1024 * 1024; +const MIN_MANIFEST_SCAN_DECOMPRESSED_BYTES: u64 = 512 * 1024 * 1024; +const MAX_MANIFEST_SCAN_DECOMPRESSED_BYTES: u64 = 1024 * 1024 * 1024; +const MANIFEST_SCAN_EXPANSION_RATIO: u64 = 16; const MAX_ARCHIVE_ENTRIES: usize = 50_000; struct BoundedReader { @@ -52,7 +54,21 @@ impl Read for BoundedReader { } pub(crate) fn read_manifest(bytes: &[u8]) -> Result { - read_manifest_with_limits(bytes, MAX_ARCHIVE_DECOMPRESSED_BYTES, MAX_ARCHIVE_ENTRIES) + read_manifest_with_limits( + bytes, + manifest_scan_decompressed_limit(bytes.len()), + MAX_ARCHIVE_ENTRIES, + ) +} + +fn manifest_scan_decompressed_limit(compressed_bytes: usize) -> u64 { + let compressed_bytes = u64::try_from(compressed_bytes).unwrap_or(u64::MAX); + compressed_bytes + .saturating_mul(MANIFEST_SCAN_EXPANSION_RATIO) + .clamp( + MIN_MANIFEST_SCAN_DECOMPRESSED_BYTES, + MAX_MANIFEST_SCAN_DECOMPRESSED_BYTES, + ) } fn read_manifest_with_limits( @@ -120,6 +136,26 @@ mod tests { encoder.finish().unwrap() } + #[test] + fn manifest_scan_budget_preserves_floor_scales_and_caps() { + assert_eq!( + manifest_scan_decompressed_limit(1), + MIN_MANIFEST_SCAN_DECOMPRESSED_BYTES + ); + assert_eq!( + manifest_scan_decompressed_limit(40 * 1024 * 1024), + 640 * 1024 * 1024 + ); + assert_eq!( + manifest_scan_decompressed_limit(78_238_082), + MAX_MANIFEST_SCAN_DECOMPRESSED_BYTES + ); + assert_eq!( + manifest_scan_decompressed_limit(usize::MAX), + MAX_MANIFEST_SCAN_DECOMPRESSED_BYTES + ); + } + #[test] fn rejects_excessive_entry_count_before_manifest() { let bytes = archive_with_entries(&[("one", b""), ("two", b""), ("three", b"")]); From 7d52cad79c72cd4e4968ccf12eea49efb04762d0 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Sat, 15 Aug 2026 04:01:17 +0300 Subject: [PATCH 09/16] test(pkg): finalize registry archive smoke --- .github/workflows/registry-download-smoke.yml | 69 ++----------------- 1 file changed, 5 insertions(+), 64 deletions(-) diff --git a/.github/workflows/registry-download-smoke.yml b/.github/workflows/registry-download-smoke.yml index 80c0e2a0..ddbd9282 100644 --- a/.github/workflows/registry-download-smoke.yml +++ b/.github/workflows/registry-download-smoke.yml @@ -3,12 +3,14 @@ name: registry-download-smoke on: pull_request: paths: + - crates/commandf-pkg/src/archive.rs - crates/commandf-pkg/src/registry.rs - .github/workflows/registry-download-smoke.yml push: branches: - main paths: + - crates/commandf-pkg/src/archive.rs - crates/commandf-pkg/src/registry.rs - .github/workflows/registry-download-smoke.yml workflow_dispatch: @@ -32,76 +34,15 @@ jobs: - name: Focused registry unit tests run: cargo test --locked -p commandf-pkg registry::tests -- --skip real_primary_us_core_is_direct_gzip --skip real_secondary_us_core_follows_only_expected_tarball + - name: Focused archive bound tests + run: cargo test --locked -p commandf-pkg archive::tests + - name: Real primary US Core archive response run: cargo test --locked -p commandf-pkg registry::tests::real_primary_us_core_is_direct_gzip -- --ignored --exact - name: Real secondary redirect-to-tarball response run: cargo test --locked -p commandf-pkg registry::tests::real_secondary_us_core_follows_only_expected_tarball -- --ignored --exact - - name: Diagnostic VSAC fallback manifest layout - run: | - set -euo pipefail - python - <<'PY' - import gzip - import io - import json - import tarfile - import urllib.request - - COMPRESSED_LIMIT = 128 * 1024 * 1024 - DIAGNOSTIC_DECOMPRESSED_LIMIT = 1024 * 1024 * 1024 - url = 'https://packages2.fhir.org/web/us.nlm.vsac-0.24.0.tgz' - with urllib.request.urlopen(url, timeout=30) as response: - data = response.read(COMPRESSED_LIMIT + 1) - if len(data) > COMPRESSED_LIMIT: - raise SystemExit('compressed archive exceeds 128 MiB diagnostic bound') - if not data.startswith(b'\x1f\x8b'): - raise SystemExit('VSAC fallback tarball is not gzip') - - class CountingReader: - def __init__(self, inner): - self.inner = inner - self.read_bytes = 0 - def read(self, size=-1): - remaining = DIAGNOSTIC_DECOMPRESSED_LIMIT - self.read_bytes - if remaining <= 0: - raise RuntimeError('diagnostic decompressed bound exceeded before manifest') - if size < 0 or size > remaining: - size = remaining - chunk = self.inner.read(size) - self.read_bytes += len(chunk) - return chunk - - counted = CountingReader(gzip.GzipFile(fileobj=io.BytesIO(data), mode='rb')) - entries_before = 0 - payload_before = 0 - max_entry = ('', 0) - with tarfile.open(fileobj=counted, mode='r|') as tf: - for member in tf: - normalized = member.name[2:] if member.name.startswith('./') else member.name - if member.size > max_entry[1]: - max_entry = (member.name, member.size) - if normalized == 'package/package.json': - body = tf.extractfile(member).read() - manifest = json.loads(body) - print(json.dumps({ - 'compressed_bytes': len(data), - 'entries_before_manifest': entries_before, - 'payload_bytes_before_manifest': payload_before, - 'decompressed_bytes_read_to_manifest': counted.read_bytes, - 'manifest_bytes': member.size, - 'largest_entry_seen_before_manifest': {'name': max_entry[0], 'bytes': max_entry[1]}, - 'manifest_name': manifest.get('name'), - 'manifest_version': manifest.get('version'), - 'dependencies': manifest.get('dependencies', {}), - }, indent=2, sort_keys=True)) - break - entries_before += 1 - payload_before += member.size - else: - raise SystemExit('package/package.json not found') - PY - - name: End-to-end exact US Core resolve and verify run: | set -euo pipefail From 85a1ae3bd3a9041b7e60f2245c6402ee48bcf55a Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Sat, 15 Aug 2026 04:04:20 +0300 Subject: [PATCH 10/16] test(pkg): isolate registry fallback end-to-end smoke --- .github/workflows/registry-download-smoke.yml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/registry-download-smoke.yml b/.github/workflows/registry-download-smoke.yml index ddbd9282..0ebce2c4 100644 --- a/.github/workflows/registry-download-smoke.yml +++ b/.github/workflows/registry-download-smoke.yml @@ -43,12 +43,12 @@ jobs: - name: Real secondary redirect-to-tarball response run: cargo test --locked -p commandf-pkg registry::tests::real_secondary_us_core_follows_only_expected_tarball -- --ignored --exact - - name: End-to-end exact US Core resolve and verify + - name: End-to-end exact VSAC fallback resolve and verify run: | set -euo pipefail rm -rf /tmp/commandf-registry-smoke cargo run --locked --quiet -p commandf -- \ - pkg resolve hl7.fhir.us.core@8.0.1 \ + pkg resolve us.nlm.vsac@0.24.0 \ --cache /tmp/commandf-registry-smoke/cache \ --lock /tmp/commandf-registry-smoke/commandf.lock cargo run --locked --quiet -p commandf -- \ @@ -62,13 +62,10 @@ jobs: lock = json.loads(Path('/tmp/commandf-registry-smoke/commandf.lock').read_text()) matches = [ item for item in lock['packages'] - if item['name'] == 'hl7.fhir.us.core' and item['version'] == '8.0.1' + if item['name'] == 'us.nlm.vsac' and item['version'] == '0.24.0' ] assert len(matches) == 1 package = matches[0] assert len(package['sha256']) == 64 - assert package['source'] in { - 'https://packages.fhir.org/hl7.fhir.us.core/8.0.1', - 'https://packages2.fhir.org/web/hl7.fhir.us.core-8.0.1.tgz', - } + assert package['source'] == 'https://packages2.fhir.org/web/us.nlm.vsac-0.24.0.tgz' PY From df319c64711c8051c7df36f02c388ecd56eff6e3 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Sat, 15 Aug 2026 04:13:24 +0300 Subject: [PATCH 11/16] fix(pkg): preserve redirect timeout budget --- crates/commandf-pkg/src/registry.rs | 76 ++++++++++++++++++++--------- 1 file changed, 52 insertions(+), 24 deletions(-) diff --git a/crates/commandf-pkg/src/registry.rs b/crates/commandf-pkg/src/registry.rs index 1f326e6e..73f1f9d1 100644 --- a/crates/commandf-pkg/src/registry.rs +++ b/crates/commandf-pkg/src/registry.rs @@ -1,5 +1,5 @@ use std::collections::BTreeMap; -use std::time::Duration; +use std::time::{Duration, Instant}; use semver::Version; use serde::Deserialize; @@ -28,12 +28,8 @@ pub struct FhirRegistrySource { impl Default for FhirRegistrySource { fn default() -> Self { - let config = Agent::config_builder() - .timeout_global(Some(REQUEST_TIMEOUT)) - .max_redirects(0) - .build(); Self { - agent: config.into(), + agent: agent_with_timeout(REQUEST_TIMEOUT), } } } @@ -73,6 +69,7 @@ impl FhirRegistrySource { name: &PackageName, version: &Version, ) -> Result { + let started = Instant::now(); let url = format!("{endpoint}/{name}/{version}"); let mut response = self .agent @@ -90,7 +87,8 @@ impl FhirRegistrySource { format!("registry redirect from {url} omitted a valid Location header") })?; let target = validated_secondary_redirect(endpoint, status, name, version, location)?; - return self.direct_archive_from_url(&target); + let remaining = remaining_request_timeout(started.elapsed())?; + return direct_archive_from_url(&target, remaining); } if !(200..300).contains(&status) { @@ -103,26 +101,46 @@ impl FhirRegistrySource { validate_gzip_archive(&bytes, &url)?; Ok(PackageArchive { bytes, source: url }) } +} - fn direct_archive_from_url(&self, url: &str) -> Result { - let mut response = self - .agent - .get(url) - .call() - .map_err(|error| error.to_string())?; - let status = response.status().as_u16(); - if !(200..300).contains(&status) { - return Err(format!( - "registry tarball download from {url} returned HTTP {status}; redirects are not followed recursively" - )); - } - let bytes = read_archive_body(&mut response, url)?; - validate_gzip_archive(&bytes, url)?; - Ok(PackageArchive { - bytes, - source: url.to_owned(), +fn agent_with_timeout(timeout: Duration) -> Agent { + Agent::config_builder() + .timeout_global(Some(timeout)) + .max_redirects(0) + .build() + .into() +} + +fn remaining_request_timeout(elapsed: Duration) -> Result { + REQUEST_TIMEOUT + .checked_sub(elapsed) + .filter(|remaining| !remaining.is_zero()) + .ok_or_else(|| { + format!( + "registry secondary redirect exhausted the {} second acquisition timeout", + REQUEST_TIMEOUT.as_secs() + ) }) +} + +fn direct_archive_from_url(url: &str, timeout: Duration) -> Result { + let agent = agent_with_timeout(timeout); + let mut response = agent + .get(url) + .call() + .map_err(|error| error.to_string())?; + let status = response.status().as_u16(); + if !(200..300).contains(&status) { + return Err(format!( + "registry tarball download from {url} returned HTTP {status}; redirects are not followed recursively" + )); } + let bytes = read_archive_body(&mut response, url)?; + validate_gzip_archive(&bytes, url)?; + Ok(PackageArchive { + bytes, + source: url.to_owned(), + }) } fn read_archive_body( @@ -300,6 +318,16 @@ mod tests { validate_gzip_archive(&[0x1f, 0x8b, 0x08, 0x00], "https://example.test/package").unwrap(); } + #[test] + fn secondary_redirect_reuses_remaining_timeout_budget() { + assert_eq!( + remaining_request_timeout(Duration::from_secs(29)).unwrap(), + Duration::from_secs(1) + ); + assert!(remaining_request_timeout(REQUEST_TIMEOUT).is_err()); + assert!(remaining_request_timeout(REQUEST_TIMEOUT + Duration::from_millis(1)).is_err()); + } + #[test] #[ignore = "requires public packages.fhir.org network access"] fn real_primary_us_core_is_direct_gzip() { From 68c950290431739117db9775712f0689bbb79eb4 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Sat, 15 Aug 2026 04:13:40 +0300 Subject: [PATCH 12/16] ci(pkg): pin registry smoke actions --- .github/workflows/registry-download-smoke.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/registry-download-smoke.yml b/.github/workflows/registry-download-smoke.yml index 0ebce2c4..ae305910 100644 --- a/.github/workflows/registry-download-smoke.yml +++ b/.github/workflows/registry-download-smoke.yml @@ -23,8 +23,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.97.1 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: dtolnay/rust-toolchain@032958afbdc797a9164d3bc0b56325c1308924a5 # 1.97.1 with: components: rustfmt, clippy From b30715414163aed96b211bf65105e40715c5f947 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Sat, 15 Aug 2026 04:14:59 +0300 Subject: [PATCH 13/16] style(pkg): apply rustfmt to timeout fix --- crates/commandf-pkg/src/registry.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/commandf-pkg/src/registry.rs b/crates/commandf-pkg/src/registry.rs index 73f1f9d1..781cb41c 100644 --- a/crates/commandf-pkg/src/registry.rs +++ b/crates/commandf-pkg/src/registry.rs @@ -125,10 +125,7 @@ fn remaining_request_timeout(elapsed: Duration) -> Result { fn direct_archive_from_url(url: &str, timeout: Duration) -> Result { let agent = agent_with_timeout(timeout); - let mut response = agent - .get(url) - .call() - .map_err(|error| error.to_string())?; + let mut response = agent.get(url).call().map_err(|error| error.to_string())?; let status = response.status().as_u16(); if !(200..300).contains(&status) { return Err(format!( From e93b886696a6cc9f70d82a33c1d17ad1290a310b Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Sat, 15 Aug 2026 04:15:12 +0300 Subject: [PATCH 14/16] ci(pkg): pin node24 checkout revision --- .github/workflows/registry-download-smoke.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/registry-download-smoke.yml b/.github/workflows/registry-download-smoke.yml index ae305910..f639c1a0 100644 --- a/.github/workflows/registry-download-smoke.yml +++ b/.github/workflows/registry-download-smoke.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 / node24 - uses: dtolnay/rust-toolchain@032958afbdc797a9164d3bc0b56325c1308924a5 # 1.97.1 with: components: rustfmt, clippy From f4f12d026888ae49ce81fc91d1fb0441841c15ac Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Sat, 15 Aug 2026 04:16:43 +0300 Subject: [PATCH 15/16] fix(pkg): tighten manifest scan amplification bound --- crates/commandf-pkg/src/archive.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/crates/commandf-pkg/src/archive.rs b/crates/commandf-pkg/src/archive.rs index 58cca035..c360cadc 100644 --- a/crates/commandf-pkg/src/archive.rs +++ b/crates/commandf-pkg/src/archive.rs @@ -8,8 +8,12 @@ use crate::{model::PackageManifest, PackageError}; const MAX_MANIFEST_BYTES: u64 = 1024 * 1024; const MIN_MANIFEST_SCAN_DECOMPRESSED_BYTES: u64 = 512 * 1024 * 1024; -const MAX_MANIFEST_SCAN_DECOMPRESSED_BYTES: u64 = 1024 * 1024 * 1024; -const MANIFEST_SCAN_EXPANSION_RATIO: u64 = 16; +// The official us.nlm.vsac@0.24.0 fallback tarball was measured at 78,238,082 +// compressed bytes and requires 905,712,128 decompressed bytes to reach +// package/package.json. A 12x budget gives that immutable version ~33 MiB of +// headroom, while this 896 MiB absolute cap keeps worst-case work below 1 GiB. +const MAX_MANIFEST_SCAN_DECOMPRESSED_BYTES: u64 = 896 * 1024 * 1024; +const MANIFEST_SCAN_EXPANSION_RATIO: u64 = 12; const MAX_ARCHIVE_ENTRIES: usize = 50_000; struct BoundedReader { @@ -144,12 +148,15 @@ mod tests { ); assert_eq!( manifest_scan_decompressed_limit(40 * 1024 * 1024), - 640 * 1024 * 1024 + MIN_MANIFEST_SCAN_DECOMPRESSED_BYTES ); assert_eq!( - manifest_scan_decompressed_limit(78_238_082), - MAX_MANIFEST_SCAN_DECOMPRESSED_BYTES + manifest_scan_decompressed_limit(50 * 1024 * 1024), + 600 * 1024 * 1024 ); + let vsac_budget = manifest_scan_decompressed_limit(78_238_082); + assert_eq!(vsac_budget, 938_856_984); + assert!(vsac_budget > 905_712_128); assert_eq!( manifest_scan_decompressed_limit(usize::MAX), MAX_MANIFEST_SCAN_DECOMPRESSED_BYTES From 14054ae06fb219af94d2761c6dcd45fbcde666f8 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Sat, 15 Aug 2026 04:17:11 +0300 Subject: [PATCH 16/16] ci(pkg): harden live registry evidence --- .github/workflows/registry-download-smoke.yml | 68 +++++++++++++++---- 1 file changed, 56 insertions(+), 12 deletions(-) diff --git a/.github/workflows/registry-download-smoke.yml b/.github/workflows/registry-download-smoke.yml index f639c1a0..6a951d91 100644 --- a/.github/workflows/registry-download-smoke.yml +++ b/.github/workflows/registry-download-smoke.yml @@ -24,6 +24,8 @@ jobs: timeout-minutes: 15 steps: - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 / node24 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@032958afbdc797a9164d3bc0b56325c1308924a5 # 1.97.1 with: components: rustfmt, clippy @@ -38,24 +40,56 @@ jobs: run: cargo test --locked -p commandf-pkg archive::tests - name: Real primary US Core archive response - run: cargo test --locked -p commandf-pkg registry::tests::real_primary_us_core_is_direct_gzip -- --ignored --exact + shell: bash + run: | + set -euo pipefail + retry() { + local attempt=1 + until "$@"; do + if (( attempt >= 3 )); then + echo "real primary registry probe failed after ${attempt} attempts" >&2 + return 1 + fi + echo "real primary registry probe failed on attempt ${attempt}; retrying" >&2 + sleep $((attempt * 5)) + attempt=$((attempt + 1)) + done + } + retry cargo test --locked -p commandf-pkg registry::tests::real_primary_us_core_is_direct_gzip -- --ignored --exact - name: Real secondary redirect-to-tarball response - run: cargo test --locked -p commandf-pkg registry::tests::real_secondary_us_core_follows_only_expected_tarball -- --ignored --exact + shell: bash + run: | + set -euo pipefail + retry() { + local attempt=1 + until "$@"; do + if (( attempt >= 3 )); then + echo "real secondary registry probe failed after ${attempt} attempts" >&2 + return 1 + fi + echo "real secondary registry probe failed on attempt ${attempt}; retrying" >&2 + sleep $((attempt * 5)) + attempt=$((attempt + 1)) + done + } + retry cargo test --locked -p commandf-pkg registry::tests::real_secondary_us_core_follows_only_expected_tarball -- --ignored --exact - name: End-to-end exact VSAC fallback resolve and verify + shell: bash run: | set -euo pipefail - rm -rf /tmp/commandf-registry-smoke - cargo run --locked --quiet -p commandf -- \ - pkg resolve us.nlm.vsac@0.24.0 \ - --cache /tmp/commandf-registry-smoke/cache \ - --lock /tmp/commandf-registry-smoke/commandf.lock - cargo run --locked --quiet -p commandf -- \ - pkg verify \ - --cache /tmp/commandf-registry-smoke/cache \ - --lock /tmp/commandf-registry-smoke/commandf.lock - python - <<'PY' + for attempt in 1 2 3; do + rm -rf /tmp/commandf-registry-smoke + if cargo run --locked --quiet -p commandf -- \ + pkg resolve us.nlm.vsac@0.24.0 \ + --cache /tmp/commandf-registry-smoke/cache \ + --lock /tmp/commandf-registry-smoke/commandf.lock \ + && cargo run --locked --quiet -p commandf -- \ + pkg verify \ + --cache /tmp/commandf-registry-smoke/cache \ + --lock /tmp/commandf-registry-smoke/commandf.lock \ + && python - <<'PY' import json from pathlib import Path @@ -69,3 +103,13 @@ jobs: assert len(package['sha256']) == 64 assert package['source'] == 'https://packages2.fhir.org/web/us.nlm.vsac-0.24.0.tgz' PY + then + exit 0 + fi + if (( attempt >= 3 )); then + echo "VSAC fallback resolve/verify failed after ${attempt} attempts" >&2 + exit 1 + fi + echo "VSAC fallback resolve/verify failed on attempt ${attempt}; retrying" >&2 + sleep $((attempt * 5)) + done