Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions .github/workflows/registry-download-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
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:

permissions:
contents: read

jobs:
registry-download:
runs-on: ubuntu-latest
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

- 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: Focused archive bound tests
run: cargo test --locked -p commandf-pkg archive::tests

- name: Real primary US Core archive response
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
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
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
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

lock = json.loads(Path('/tmp/commandf-registry-smoke/commandf.lock').read_text())
matches = [
item for item in lock['packages']
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'] == '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
47 changes: 45 additions & 2 deletions crates/commandf-pkg/src/archive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ 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;
// 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<R> {
Expand Down Expand Up @@ -52,7 +58,21 @@ impl<R: Read> Read for BoundedReader<R> {
}

pub(crate) fn read_manifest(bytes: &[u8]) -> Result<PackageManifest, PackageError> {
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(
Expand Down Expand Up @@ -120,6 +140,29 @@ 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),
MIN_MANIFEST_SCAN_DECOMPRESSED_BYTES
);
assert_eq!(
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
);
}

#[test]
fn rejects_excessive_entry_count_before_manifest() {
let bytes = archive_with_entries(&[("one", b""), ("two", b""), ("three", b"")]);
Expand Down
Loading
Loading