Skip to content

add retries to OCI network hiccups#8

Open
bennyz wants to merge 1 commit intojumpstarter-dev:mainfrom
bennyz:oci-retries
Open

add retries to OCI network hiccups#8
bennyz wants to merge 1 commit intojumpstarter-dev:mainfrom
bennyz:oci-retries

Conversation

@bennyz
Copy link
Member

@bennyz bennyz commented Mar 16, 2026

Summary by CodeRabbit

  • New Features

    • Retryable downloads with configurable max retries and delay
    • HTTP Range-based resume for interrupted blob downloads
    • Automatic decompressor selection for streamed archives
  • Improvements

    • Centralized retry handling, clearer diagnostic logging, and unified download pipeline
    • More robust resume-and-retry behavior for blob downloads and extraction
    • Exposed retry defaults for easier configuration
  • Tests

    • Added tests simulating truncated downloads and successful resume+retry
  • Chores

    • Added an HTTP streaming helper as a dev dependency

@coderabbitai
Copy link

coderabbitai bot commented Mar 16, 2026

Important

Review skipped

This PR was authored by the user configured for CodeRabbit reviews. CodeRabbit does not review PRs authored by this user. It's recommended to use a dedicated user account to post CodeRabbit review feedback.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7367f894-7dc3-43de-82f0-86c7742be4dc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

Adds configurable OCI download retries with Range-resume, centralizes download error mapping and retry decisions, performs initial blob pre-read for compression detection, abstracts decompressor selection/spawning, narrows stream receiver API to byte-bounded channels, and adds tests plus a dev-dependency.

Changes

Cohort / File(s) Summary
Manifest
Cargo.toml
Add dev-dependency http-body = "1.0.1".
Retry & Options
src/fls/options.rs, src/fls/mod.rs, src/lib.rs, src/main.rs
Introduce DEFAULT_MAX_RETRIES and DEFAULT_RETRY_DELAY_SECS; add max_retries and retry_delay_secs to OciOptions; re-export constants and propagate options to CLI/fastboot.
Download error & retry logic
src/fls/download_error.rs, src/fls/from_url.rs
Add from_reqwest/from_reqwest_ref and public handle_download_retry to centralize reqwest->DownloadError mapping and retry decision; replace local retry handlers with shared handler.
OCI streaming & registry client
src/fls/oci/from_oci.rs, src/fls/oci/registry.rs
Add fetch_blob_with_detection, retry_download_loop, run_blob_pipeline; change stream_blob_to_file/stream_blob_to_tar_files to accept RegistryClient + OciOptions; add get_blob_stream(_range) with Range support and 206 handling; integrate retry/resume logic.
Decompression orchestration
src/fls/decompress.rs
Add decompressor_for_compression, start_decompressor_for_compression, private spawn_decompressor, and diagnostic logging to centralize decompressor mapping and spawning.
Stream utilities
src/fls/stream_utils.rs
Remove plain mpsc::Receiver variant; unify ChannelReader to use ByteBoundedReceiver<Bytes> only and expose new_byte_bounded.
Fastboot & integration
src/fls/fastboot.rs
Populate OCI options with max_retries and retry_delay_secs in fastboot flows.
Tests
tests/oci_extract.rs
Add spawn_oci_server_with_blob_truncation and test_blob_truncation_triggers_retry_and_succeeds; update tests to use new retry defaults and StreamBody, assert Range-resume + retry behavior.

Sequence Diagram

sequenceDiagram
    participant Client as Client
    participant Registry as RegistryClient
    participant Blob as BlobStream
    participant Retry as RetryHandler
    participant Decomp as Decompressor

    Client->>Registry: fetch_blob_with_detection(digest, options)
    activate Registry
    Registry->>Blob: get_blob_stream_range(digest, None)
    activate Blob
    Blob-->>Registry: response stream + initial bytes
    deactivate Blob
    Registry-->>Client: (stream, initial_chunks, compression)
    deactivate Registry

    Client->>Client: start retry_download_loop()
    loop until success or max_retries
        Client->>Blob: read bytes from stream
        alt connection/truncation detected
            Client->>Retry: handle_download_retry(error, &mut retry_count, max_retries, delay)
            Retry-->>Client: Some(delay) or None
            alt Some(delay)
                Client->>Client: wait(delay)
                Client->>Registry: get_blob_stream_range(digest, bytes_received)
                Registry->>Blob: resume with Range header
                Blob-->>Client: 206 Partial Content + bytes
            end
        end
    end

    Client->>Decomp: start_decompressor_for_compression(compression)
    Client->>Decomp: pipe downloaded bytes
    Decomp-->>Client: decompressed output
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • mangelajo

Poem

🐰 I nibble bytes and mend the trail,

When chunks cut short I hop and prevail,
I ask for Ranges, retry with cheer,
Decompress and stitch each tarball near,
Hooray — full blobs restored, give a cheer!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'add retries to OCI network hiccups' accurately describes the main objective of the changeset, which adds comprehensive retry logic for OCI blob downloads with error handling, range-based resumption, and configurable retry parameters.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
src/fls/options.rs (1)

65-66: Consider adding a Default impl for OciOptions.

BlockFlashOptions has a Default implementation that uses the centralized constants, but OciOptions does not. For API consistency and to reduce boilerplate at call sites, consider implementing Default for OciOptions as well.

♻️ Suggested Default implementation
impl Default for OciOptions {
    fn default() -> Self {
        Self {
            common: FlashOptions::default(),
            username: None,
            password: None,
            file_pattern: None,
            max_retries: DEFAULT_MAX_RETRIES,
            retry_delay_secs: DEFAULT_RETRY_DELAY_SECS,
        }
    }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fls/options.rs` around lines 65 - 66, Add a Default implementation for
OciOptions to match BlockFlashOptions and centralize defaults: implement impl
Default for OciOptions { fn default() -> Self { ... } } returning OciOptions
with common: FlashOptions::default(), username: None, password: None,
file_pattern: None, max_retries: DEFAULT_MAX_RETRIES, retry_delay_secs:
DEFAULT_RETRY_DELAY_SECS; this provides API consistency and reduces boilerplate
when constructing OciOptions.
tests/oci_extract.rs (1)

502-540: Make the truncation fixture require Range on retries.

After the first truncated response, this helper still serves a full 200 OK body for later non-Range requests. That means the new test proves “some retry happened”, but not that resume support stayed intact. Reject the retry unless it includes a valid Range header, or record/assert the header value explicitly.

🧪 Tighten the fixture
-                            } else {
-                                Response::builder()
-                                    .status(StatusCode::OK)
-                                    .header("Content-Length", blob_body.len().to_string())
-                                    .body(full(Bytes::copy_from_slice(&blob_body)))
-                                    .unwrap()
-                            }
+                            } else {
+                                Response::builder()
+                                    .status(StatusCode::RANGE_NOT_SATISFIABLE)
+                                    .body(full(Bytes::new()))
+                                    .unwrap()
+                            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/oci_extract.rs` around lines 502 - 540, The retry fixture must reject
non-resumed retries: when req_num != 0, check the incoming request's "Range"
header (e.g., via req.headers().get("range")) and only return the full-body 200
if a valid Range requesting the remainder (starting at split_point, e.g.,
"bytes={split_point}-") is present; otherwise return an error response (or
416/400) so the test ensures resume used Range. Update the branch using
Response::builder()/body(full(...)) to inspect the Range header and
conditionally respond based on its value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Cargo.toml`:
- Line 24: The http-body crate is only used in tests, so remove the http-body =
"1.0.1" entry from the normal dependencies section and add the same entry under
[dev-dependencies] in Cargo.toml; verify tests that reference http_body::Frame
(e.g., tests/oci_extract.rs) still compile and run, and ensure there are no
other production uses of the http_body symbol before committing the change.

In `@src/fls/oci/from_oci.rs`:
- Around line 133-144: The current match wraps every Err(e) from
client.get_blob_stream into DownloadError::Other which loses HTTP status info
and prevents handle_download_retry from applying correct policies; instead,
inspect the error returned by get_blob_stream (preserving any HTTP status/code
or mapping it to a DownloadError variant that includes status) before calling
handle_download_retry so 401/404 are treated non-retryable and 429/5xx get the
proper backoff; update the error conversion in the Err branch (around
client.get_blob_stream handling) to extract status (or convert to a
DownloadError::Status/code variant) and pass that to handle_download_retry while
only returning the original error e when handle_download_retry indicates no
retry.
- Around line 335-343: The closure passed into retry_download_loop must not
treat writer_handle.is_finished() or decompressor_writer_handle.is_finished() as
a sign of pipeline failure because stream_blob_to_tar_files intentionally lets
the writer exit successfully; remove those is_finished checks from the liveness
predicate and instead let retry_download_loop run based on its existing
download/stream conditions, then after retry_download_loop returns await/join
writer_handle and decompressor_writer_handle to inspect their actual Result/Err
values and handle/log real errors; update references in from_oci.rs to stop
using writer_handle.is_finished()/decompressor_writer_handle.is_finished() in
the closure and perform proper join/error handling of those handles once
download completes.
- Around line 275-301: When client.get_blob_stream_range(digest,
Some(bytes_received)).await returns Err(e), the code currently continues leaving
`stream` pointing at the stale/broken body which can later be treated as EOF;
change this to fail closed: on Err from get_blob_stream_range (in the match arm
handling the Err) return an Err (or propagate the received error) immediately
instead of `continue`, so that the function does not reuse the stale `stream`
and does not return a false-success bytes_received; update any error message to
include context (digest/bytes_received) as needed.

---

Nitpick comments:
In `@src/fls/options.rs`:
- Around line 65-66: Add a Default implementation for OciOptions to match
BlockFlashOptions and centralize defaults: implement impl Default for OciOptions
{ fn default() -> Self { ... } } returning OciOptions with common:
FlashOptions::default(), username: None, password: None, file_pattern: None,
max_retries: DEFAULT_MAX_RETRIES, retry_delay_secs: DEFAULT_RETRY_DELAY_SECS;
this provides API consistency and reduces boilerplate when constructing
OciOptions.

In `@tests/oci_extract.rs`:
- Around line 502-540: The retry fixture must reject non-resumed retries: when
req_num != 0, check the incoming request's "Range" header (e.g., via
req.headers().get("range")) and only return the full-body 200 if a valid Range
requesting the remainder (starting at split_point, e.g., "bytes={split_point}-")
is present; otherwise return an error response (or 416/400) so the test ensures
resume used Range. Update the branch using Response::builder()/body(full(...))
to inspect the Range header and conditionally respond based on its value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 66d1ccc8-cc7f-4acb-b0a5-ed088343583e

📥 Commits

Reviewing files that changed from the base of the PR and between 60e5a9b and 0a60116.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • Cargo.toml
  • src/fls/decompress.rs
  • src/fls/download_error.rs
  • src/fls/fastboot.rs
  • src/fls/from_url.rs
  • src/fls/mod.rs
  • src/fls/oci/from_oci.rs
  • src/fls/oci/registry.rs
  • src/fls/options.rs
  • src/lib.rs
  • src/main.rs
  • tests/oci_extract.rs

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/fls/oci/from_oci.rs (2)

303-306: ⚠️ Potential issue | 🔴 Critical

Resume failure allows stale stream to be reused, risking false success.

When get_blob_stream_range() fails, the code continues to the outer loop, but stream still references the old broken response. On the next iteration, stream.next() may return Ok(None) (EOF), causing the function to return Ok(bytes_received) with truncated data.

This matches the concern from the previous review. The resume failure should fail closed:

🔧 Proposed fix
         Err(e) => {
-            eprintln!("Retry connection failed: {}", e);
-            continue;
+            return Err(format!(
+                "Failed to resume blob download from byte {}: {}",
+                bytes_received, e
+            )
+            .into());
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fls/oci/from_oci.rs` around lines 303 - 306, When get_blob_stream_range()
returns Err(e) while attempting to resume, do not continue and reuse the old
broken stream; instead fail closed by returning an Err so the stale stream
cannot be used to produce a false success. Update the Err(e) branch in the
retry/resume loop (the block that currently eprintln! and continue) to return an
appropriate Err value from the current function (propagate or map the error) so
that the function does not proceed to call stream.next() and cannot return
Ok(bytes_received) with truncated data; reference get_blob_stream_range, stream,
stream.next(), and bytes_received to locate and change the handling.

133-150: ⚠️ Potential issue | 🟠 Major

HTTP status information lost during error classification.

When get_blob_stream() fails, all errors are wrapped as DownloadError::Other(e.to_string()), which marks them as retryable. This means permanent failures like HTTP 401/403/404 (which are caught in registry.rs but returned as boxed errors) will be retried unnecessarily.

Consider extracting the reqwest error from the boxed error to use DownloadError::from_reqwest() for proper classification:

💡 Suggested approach
         Err(e) => {
-            // Classify connection-level errors for retry
-            let dl_err = DownloadError::Other(e.to_string());
+            // Try to extract reqwest error for proper classification
+            let dl_err = e
+                .downcast_ref::<reqwest::Error>()
+                .map(|re| DownloadError::from_reqwest(re.clone()))
+                .unwrap_or_else(|| DownloadError::Other(e.to_string()));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fls/oci/from_oci.rs` around lines 133 - 150, When
client.get_blob_stream(digest) fails in the get_blob_stream match arm, don't
always wrap the boxed error into DownloadError::Other; instead try to extract
the underlying reqwest error from the boxed error and convert it via
DownloadError::from_reqwest(...) so HTTP status-based cases (401/403/404) are
classified correctly before calling handle_download_retry(&dl_err, &mut
retry_count, max_retries, retry_delay_secs); if extraction fails, fall back to
DownloadError::Other(e.to_string()), keep using
retry_count/max_retries/retry_delay_secs and tokio::time::sleep(delay).await on
Some(delay), and on None return the original boxed error as before.
🧹 Nitpick comments (1)
src/fls/decompress.rs (1)

62-62: Consider using eprintln! or a proper logging framework.

Diagnostic output using println! can interfere with stdout-based pipelines. Since this is informational/diagnostic, eprintln! (consistent with line 62 in from_oci.rs: eprintln!("Downloaded blob to {}")) or a structured logging crate like tracing would be more appropriate.

This also applies to line 39.

♻️ Suggested fix
-    println!("Using decompressor: {}", cmd);
+    eprintln!("Using decompressor: {}", cmd);

And similarly for line 39.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fls/decompress.rs` at line 62, Replace the diagnostic println! calls in
this module with stderr or a logging facility: change the println!("Using
decompressor: {}", cmd) (and the other println! at the earlier occurrence around
line 39) to eprintln! or wire them into the project's logging/tracing crate so
informational output doesn't pollute stdout-based pipelines; ensure the same
formatting and message text is preserved when switching to eprintln! or the
chosen logger.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/fls/oci/from_oci.rs`:
- Around line 341-349: The pipeline liveness closure passed into
retry_download_loop incorrectly treats an early writer exit as failure; update
the closure in the call inside stream_blob_to_tar_files so it only checks
decompressor_writer_handle.is_finished() (i.e., use &||
!decompressor_writer_handle.is_finished()) rather than the current AND of both
writer_handle and decompressor_writer_handle, leaving send/error checks as-is;
this ensures retry_download_loop treats normal early writer termination (when
requested files are found) as successful while still detecting decompressor
failures.

---

Duplicate comments:
In `@src/fls/oci/from_oci.rs`:
- Around line 303-306: When get_blob_stream_range() returns Err(e) while
attempting to resume, do not continue and reuse the old broken stream; instead
fail closed by returning an Err so the stale stream cannot be used to produce a
false success. Update the Err(e) branch in the retry/resume loop (the block that
currently eprintln! and continue) to return an appropriate Err value from the
current function (propagate or map the error) so that the function does not
proceed to call stream.next() and cannot return Ok(bytes_received) with
truncated data; reference get_blob_stream_range, stream, stream.next(), and
bytes_received to locate and change the handling.
- Around line 133-150: When client.get_blob_stream(digest) fails in the
get_blob_stream match arm, don't always wrap the boxed error into
DownloadError::Other; instead try to extract the underlying reqwest error from
the boxed error and convert it via DownloadError::from_reqwest(...) so HTTP
status-based cases (401/403/404) are classified correctly before calling
handle_download_retry(&dl_err, &mut retry_count, max_retries, retry_delay_secs);
if extraction fails, fall back to DownloadError::Other(e.to_string()), keep
using retry_count/max_retries/retry_delay_secs and
tokio::time::sleep(delay).await on Some(delay), and on None return the original
boxed error as before.

---

Nitpick comments:
In `@src/fls/decompress.rs`:
- Line 62: Replace the diagnostic println! calls in this module with stderr or a
logging facility: change the println!("Using decompressor: {}", cmd) (and the
other println! at the earlier occurrence around line 39) to eprintln! or wire
them into the project's logging/tracing crate so informational output doesn't
pollute stdout-based pipelines; ensure the same formatting and message text is
preserved when switching to eprintln! or the chosen logger.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 031b2370-9a74-4bba-a16f-06a8390b42fc

📥 Commits

Reviewing files that changed from the base of the PR and between 0a60116 and f0e336b.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • Cargo.toml
  • src/fls/decompress.rs
  • src/fls/download_error.rs
  • src/fls/fastboot.rs
  • src/fls/from_url.rs
  • src/fls/mod.rs
  • src/fls/oci/from_oci.rs
  • src/fls/oci/registry.rs
  • src/fls/options.rs
  • src/fls/stream_utils.rs
  • src/lib.rs
  • src/main.rs
  • tests/oci_extract.rs
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/fls/options.rs
  • src/fls/from_url.rs
  • src/main.rs
  • src/fls/mod.rs
  • Cargo.toml
  • src/lib.rs

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/fls/oci/registry.rs (1)

245-248: ⚠️ Potential issue | 🟠 Major

Preserve structured HTTP failure details instead of stringifying them.

At Line 245–Line 248, converting blob fetch failures to String loses status semantics (e.g., 401/404 vs 429/5xx), so upstream retry logic can misclassify permanent failures as retryable.

Suggested fix
+use crate::fls::download_error::DownloadError;
...
-        if !response.status().is_success() && response.status() != StatusCode::PARTIAL_CONTENT {
-            let status = response.status();
-            let body = response.text().await.unwrap_or_default();
-            return Err(format!("Failed to fetch blob: {} - {}", status, body).into());
-        }
+        if !response.status().is_success() && response.status() != StatusCode::PARTIAL_CONTENT {
+            return Err(DownloadError::from_http_response(&response).into());
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fls/oci/registry.rs` around lines 245 - 248, The code currently turns
non-success HTTP responses into a String error (using response.status() and
response.text()) which loses structured status semantics; instead, preserve the
status and body by returning a typed error that includes the StatusCode and
optional body (e.g., a new RegistryError::Http { status: StatusCode, body:
String } or similar) rather than stringifying them—replace the
Err(format!(...)).into() return with constructing and returning that structured
error so upstream retry/handling can inspect response.status() (use the existing
response, status and body variables when building the error).
🧹 Nitpick comments (1)
src/fls/stream_utils.rs (1)

9-16: Optional: Consider removing the ReceiverVariant wrapper.

Since there's now only a single variant, ReceiverVariant is just a passthrough newtype. You could simplify by using ByteBoundedReceiver<Bytes> directly as the rx field in ChannelReader, eliminating this indirection.

That said, keeping the wrapper is fine if you anticipate adding other receiver types in the future or prefer the explicit abstraction boundary.

♻️ Proposed simplification (optional)
-/// Wrapper over a byte-bounded receiver for use in synchronous readers
-struct ReceiverVariant(ByteBoundedReceiver<Bytes>);
-
-impl ReceiverVariant {
-    fn blocking_recv(&mut self) -> Option<Bytes> {
-        self.0.blocking_recv()
-    }
-}
-
 // ...
 
 pub struct ChannelReader {
-    rx: ReceiverVariant,
+    rx: ByteBoundedReceiver<Bytes>,
     current: Option<Bytes>,
     offset: usize,
 }

 impl ChannelReader {
     pub fn new_byte_bounded(rx: ByteBoundedReceiver<Bytes>) -> Self {
         Self {
-            rx: ReceiverVariant(rx),
+            rx,
             current: None,
             offset: 0,
         }
     }
 }

 // In Read::read, change self.rx.blocking_recv() to self.rx.blocking_recv()
 // (no change needed since method name is the same)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fls/stream_utils.rs` around lines 9 - 16, The ReceiverVariant newtype is
unnecessary since it only forwards blocking_recv to the inner
ByteBoundedReceiver<Bytes>; simplify by replacing ReceiverVariant with
ByteBoundedReceiver<Bytes> directly (use it as the rx field type in
ChannelReader) and call its blocking_recv method where
ReceiverVariant::blocking_recv was used; update any constructor/field accesses
for ChannelReader and remove the ReceiverVariant struct and impl to eliminate
the passthrough indirection.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/fls/oci/from_oci.rs`:
- Around line 283-299: When handling a 206 Partial Content response in the
resume logic, validate the response's "Content-Range" header actually starts at
bytes_received to avoid corruption: read the "Content-Range" header from
response, parse the leading start value (e.g., "bytes {start}-{end}/..."),
ensure start == bytes_received, and return an error if the header is missing or
the start mismatches; update the branch that checks response.status() ==
StatusCode::PARTIAL_CONTENT (and before assigning stream =
Box::pin(response.bytes_stream())) to perform this validation and only proceed
to set stream when the range start matches.
- Around line 341-349: The download_result is propagated early with
download_result? which can return before the decompressor child
(decompressor_writer_handle) is awaited and leave it unreaped; change the
control flow so you always await the spawned handles (writer_handle and
decompressor_writer_handle via tokio::join! or awaiting each) before returning
the download_result, e.g., run let (wr, dw) = tokio::join!(writer_handle,
decompressor_writer_handle) and then handle their results, and only after both
handles have completed propagate retry_download_loop's result (download_result)
so the decompressor child is always reaped even on errors.

---

Duplicate comments:
In `@src/fls/oci/registry.rs`:
- Around line 245-248: The code currently turns non-success HTTP responses into
a String error (using response.status() and response.text()) which loses
structured status semantics; instead, preserve the status and body by returning
a typed error that includes the StatusCode and optional body (e.g., a new
RegistryError::Http { status: StatusCode, body: String } or similar) rather than
stringifying them—replace the Err(format!(...)).into() return with constructing
and returning that structured error so upstream retry/handling can inspect
response.status() (use the existing response, status and body variables when
building the error).

---

Nitpick comments:
In `@src/fls/stream_utils.rs`:
- Around line 9-16: The ReceiverVariant newtype is unnecessary since it only
forwards blocking_recv to the inner ByteBoundedReceiver<Bytes>; simplify by
replacing ReceiverVariant with ByteBoundedReceiver<Bytes> directly (use it as
the rx field type in ChannelReader) and call its blocking_recv method where
ReceiverVariant::blocking_recv was used; update any constructor/field accesses
for ChannelReader and remove the ReceiverVariant struct and impl to eliminate
the passthrough indirection.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 768c5280-d09c-4542-998b-1aacef811816

📥 Commits

Reviewing files that changed from the base of the PR and between f0e336b and 96876b6.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • Cargo.toml
  • src/fls/decompress.rs
  • src/fls/download_error.rs
  • src/fls/fastboot.rs
  • src/fls/from_url.rs
  • src/fls/mod.rs
  • src/fls/oci/from_oci.rs
  • src/fls/oci/registry.rs
  • src/fls/options.rs
  • src/fls/stream_utils.rs
  • src/lib.rs
  • src/main.rs
  • tests/oci_extract.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/fls/fastboot.rs
  • src/fls/mod.rs
  • src/lib.rs
  • Cargo.toml

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/fls/oci/from_oci.rs (1)

292-311: ⚠️ Potential issue | 🟠 Major

Fail closed on missing/invalid Content-Range in 206 responses.

The current check only validates when parsing succeeds; missing or malformed headers still pass. Resume should reject 206 responses unless Content-Range is present and starts exactly at bytes_received.

Suggested fix
-                    if let Some(cr) = response
-                        .headers()
-                        .get("content-range")
-                        .and_then(|v| v.to_str().ok())
-                    {
-                        // Expected: "bytes <start>-<end>/<total>"
-                        if let Some(start_str) =
-                            cr.strip_prefix("bytes ").and_then(|s| s.split('-').next())
-                        {
-                            if let Ok(start) = start_str.parse::<u64>() {
-                                if start != bytes_received {
-                                    return Err(format!(
-                                        "Content-Range mismatch: expected start={}, got start={}",
-                                        bytes_received, start
-                                    )
-                                    .into());
-                                }
-                            }
-                        }
-                    }
+                    let cr = response
+                        .headers()
+                        .get(reqwest::header::CONTENT_RANGE)
+                        .and_then(|v| v.to_str().ok())
+                        .ok_or("Missing Content-Range on 206 response")?;
+
+                    let start = cr
+                        .strip_prefix("bytes ")
+                        .and_then(|s| s.split('-').next())
+                        .and_then(|s| s.parse::<u64>().ok())
+                        .ok_or_else(|| format!("Invalid Content-Range: {}", cr))?;
+
+                    if start != bytes_received {
+                        return Err(format!(
+                            "Content-Range mismatch: expected start={}, got start={} ({})",
+                            bytes_received, start, cr
+                        )
+                        .into());
+                    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fls/oci/from_oci.rs` around lines 292 - 311, The resume logic currently
only errors if Content-Range parses and start mismatches; instead, when handling
a 206 response you must require a valid Content-Range header that parses and
whose start equals bytes_received. In the block referring to response and
bytes_received, change the check so that if headers().get("content-range") is
missing or to_str()/parse::<u64>() fails, you return an Err indicating
missing/invalid Content-Range, and if parsed start != bytes_received you return
the existing mismatch Err; ensure this applies for 206 responses
(response.status() == 206) so malformed or absent headers fail closed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/oci_extract.rs`:
- Around line 482-484: The test reads start_str.parse::<usize>() into start and
then uses blob_body[start..], which panics if start > blob_body.len(); update
the logic around start_str/start parse to validate the parsed start against
blob_body.len() and handle out-of-range values by returning/setting an
appropriate HTTP error or using a safe slice (e.g., skip the range handling)
instead of slicing past the end; specifically guard the code that computes
remaining = &blob_body[start..] and the subsequent content_range formatting so
that if start > blob_body.len() you produce a controlled HTTP 416/400 response
or clamp start to blob_body.len() to avoid panics.

---

Duplicate comments:
In `@src/fls/oci/from_oci.rs`:
- Around line 292-311: The resume logic currently only errors if Content-Range
parses and start mismatches; instead, when handling a 206 response you must
require a valid Content-Range header that parses and whose start equals
bytes_received. In the block referring to response and bytes_received, change
the check so that if headers().get("content-range") is missing or
to_str()/parse::<u64>() fails, you return an Err indicating missing/invalid
Content-Range, and if parsed start != bytes_received you return the existing
mismatch Err; ensure this applies for 206 responses (response.status() == 206)
so malformed or absent headers fail closed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 80ccbadd-d300-4701-bcc0-c2aa24fcca08

📥 Commits

Reviewing files that changed from the base of the PR and between 96876b6 and 8b73749.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • Cargo.toml
  • src/fls/decompress.rs
  • src/fls/download_error.rs
  • src/fls/fastboot.rs
  • src/fls/from_url.rs
  • src/fls/mod.rs
  • src/fls/oci/from_oci.rs
  • src/fls/oci/registry.rs
  • src/fls/options.rs
  • src/fls/stream_utils.rs
  • src/lib.rs
  • src/main.rs
  • tests/oci_extract.rs
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/fls/from_url.rs
  • Cargo.toml
  • src/main.rs
  • src/fls/decompress.rs
  • src/fls/fastboot.rs
  • src/fls/oci/registry.rs

Comment on lines +482 to +484
if let Ok(start) = start_str.parse::<usize>() {
let remaining = &blob_body[start..];
let content_range = format!(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard invalid Range offsets to avoid test-server panics.

blob_body[start..] will panic if start > blob_body.len(), which can turn protocol bugs into opaque task aborts instead of clear HTTP behavior.

Suggested fix
-                                    if let Ok(start) = start_str.parse::<usize>() {
-                                        let remaining = &blob_body[start..];
+                                    if let Ok(start) = start_str.parse::<usize>() {
+                                        if start > blob_body.len() {
+                                            let resp: Response<BoxBody> = Response::builder()
+                                                .status(StatusCode::RANGE_NOT_SATISFIABLE)
+                                                .body(full(Bytes::from("Invalid range start")))
+                                                .unwrap();
+                                            return Ok(resp);
+                                        }
+                                        let remaining = &blob_body[start..];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if let Ok(start) = start_str.parse::<usize>() {
let remaining = &blob_body[start..];
let content_range = format!(
if let Ok(start) = start_str.parse::<usize>() {
if start > blob_body.len() {
let resp: Response<BoxBody> = Response::builder()
.status(StatusCode::RANGE_NOT_SATISFIABLE)
.body(full(Bytes::from("Invalid range start")))
.unwrap();
return Ok(resp);
}
let remaining = &blob_body[start..];
let content_range = format!(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/oci_extract.rs` around lines 482 - 484, The test reads
start_str.parse::<usize>() into start and then uses blob_body[start..], which
panics if start > blob_body.len(); update the logic around start_str/start parse
to validate the parsed start against blob_body.len() and handle out-of-range
values by returning/setting an appropriate HTTP error or using a safe slice
(e.g., skip the range handling) instead of slicing past the end; specifically
guard the code that computes remaining = &blob_body[start..] and the subsequent
content_range formatting so that if start > blob_body.len() you produce a
controlled HTTP 416/400 response or clamp start to blob_body.len() to avoid
panics.

Signed-off-by: Benny Zlotnik <bzlotnik@redhat.com>
Assisted-by: claude-opus-4.6
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
tests/oci_extract.rs (1)

482-484: ⚠️ Potential issue | 🟡 Minor

Guard invalid Range offsets to avoid test-server panics.

blob_body[start..] will panic if start > blob_body.len(). While this is test code, it can turn protocol bugs into opaque task aborts instead of clear HTTP 416 responses.

Suggested fix
                                    if let Ok(start) = start_str.parse::<usize>() {
+                                        if start > blob_body.len() {
+                                            let resp: Response<BoxBody> = Response::builder()
+                                                .status(StatusCode::RANGE_NOT_SATISFIABLE)
+                                                .body(full(Bytes::from("Invalid range start")))
+                                                .unwrap();
+                                            return Ok(resp);
+                                        }
                                         let remaining = &blob_body[start..];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/oci_extract.rs` around lines 482 - 484, The code uses
start_str.parse::<usize>() and then unconditionally slices blob_body[start..],
which panics when start > blob_body.len(); update the logic around the parsing
branch (the start variable and the creation of remaining/content_range) to first
check that start <= blob_body.len() and handle the out-of-range case by
returning an appropriate HTTP 416-style error (or test assertion) instead of
slicing; adjust the branch that sets remaining and content_range to only run
when the guard passes so tests produce a clear 416-like failure rather than a
panic.
src/fls/oci/from_oci.rs (1)

288-304: ⚠️ Potential issue | 🟠 Major

Missing Content-Range validation when resuming downloads.

When the registry returns 206 Partial Content, the code proceeds without verifying that the returned range actually starts at bytes_received. A misbehaving registry or proxy could return a mismatched range, causing data corruption in the reconstructed blob stream.

Suggested fix
             Ok(response) => {
                 if response.status() == StatusCode::PARTIAL_CONTENT {
+                    // Validate Content-Range header matches requested offset
+                    let content_range = response
+                        .headers()
+                        .get(reqwest::header::CONTENT_RANGE)
+                        .and_then(|v| v.to_str().ok());
+
+                    if let Some(range_str) = content_range {
+                        // Parse "bytes START-END/TOTAL" format
+                        if let Some(start) = range_str
+                            .strip_prefix("bytes ")
+                            .and_then(|s| s.split('-').next())
+                            .and_then(|s| s.parse::<u64>().ok())
+                        {
+                            if start != bytes_received {
+                                return Err(format!(
+                                    "Resume offset mismatch: requested {}, got {} ({})",
+                                    bytes_received, start, range_str
+                                )
+                                .into());
+                            }
+                        }
+                    }
+
                     if debug {
                         eprintln!(
                             "[DEBUG] Registry supports Range, resuming from byte {}",
                             bytes_received
                         );
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fls/oci/from_oci.rs` around lines 288 - 304, When handling a 206 Partial
Content response in the blob download logic, validate the response's
Content-Range header to ensure the returned range actually starts at
bytes_received before assigning stream; specifically, read
response.headers().get("content-range"), parse the range start (e.g., the "bytes
X-Y/..." format) and verify X == bytes_received, returning an Err if the header
is missing or the start does not match; add a debug eprintln showing the
Content-Range when debug is true and fail early instead of using
response.bytes_stream() if the range is inconsistent to avoid corrupting the
reconstructed blob.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/fls/oci/from_oci.rs`:
- Around line 288-304: When handling a 206 Partial Content response in the blob
download logic, validate the response's Content-Range header to ensure the
returned range actually starts at bytes_received before assigning stream;
specifically, read response.headers().get("content-range"), parse the range
start (e.g., the "bytes X-Y/..." format) and verify X == bytes_received,
returning an Err if the header is missing or the start does not match; add a
debug eprintln showing the Content-Range when debug is true and fail early
instead of using response.bytes_stream() if the range is inconsistent to avoid
corrupting the reconstructed blob.

In `@tests/oci_extract.rs`:
- Around line 482-484: The code uses start_str.parse::<usize>() and then
unconditionally slices blob_body[start..], which panics when start >
blob_body.len(); update the logic around the parsing branch (the start variable
and the creation of remaining/content_range) to first check that start <=
blob_body.len() and handle the out-of-range case by returning an appropriate
HTTP 416-style error (or test assertion) instead of slicing; adjust the branch
that sets remaining and content_range to only run when the guard passes so tests
produce a clear 416-like failure rather than a panic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d3d23570-3bed-4b94-babd-84fe8b487375

📥 Commits

Reviewing files that changed from the base of the PR and between 8b73749 and 81d4e19.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • Cargo.toml
  • src/fls/decompress.rs
  • src/fls/download_error.rs
  • src/fls/fastboot.rs
  • src/fls/from_url.rs
  • src/fls/mod.rs
  • src/fls/oci/from_oci.rs
  • src/fls/oci/registry.rs
  • src/fls/options.rs
  • src/fls/stream_utils.rs
  • src/lib.rs
  • src/main.rs
  • tests/oci_extract.rs
🚧 Files skipped from review as they are similar to previous changes (6)
  • Cargo.toml
  • src/fls/mod.rs
  • src/main.rs
  • src/fls/options.rs
  • src/fls/stream_utils.rs
  • src/fls/from_url.rs

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
tests/oci_extract.rs (1)

482-498: ⚠️ Potential issue | 🟡 Minor

Guard invalid Range offsets to avoid test-server panics.

blob_body[start..] at line 483 will panic if start > blob_body.len(). While this is test code, a protocol bug could cause opaque task aborts instead of clear HTTP 416 responses.

Suggested fix
                                    if let Ok(start) = start_str.parse::<usize>() {
+                                        if start > blob_body.len() {
+                                            let resp: Response<BoxBody> = Response::builder()
+                                                .status(StatusCode::RANGE_NOT_SATISFIABLE)
+                                                .body(full(Bytes::from("Invalid range start")))
+                                                .unwrap();
+                                            return Ok(resp);
+                                        }
                                         let remaining = &blob_body[start..];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/oci_extract.rs` around lines 482 - 498, The code currently parses
start_str into start and then slices blob_body[start..], which will panic if
start > blob_body.len(); update the Range-handling in the block that parses
start_str (the start variable and subsequent slicing of blob_body) to first
check if start <= blob_body.len() and only proceed to build the PARTIAL_CONTENT
Response when valid, otherwise return a 416 Range Not Satisfiable Response (use
StatusCode::RANGE_NOT_SATISFIABLE) with an appropriate Content-Range header like
"bytes */{blob_body.len()}" and no body; ensure you adjust the
Response::builder() branch that currently sets
Content-Length/Content-Range/Accept-Ranges so the invalid-case returns the 416
response instead of panicking.
src/fls/oci/from_oci.rs (1)

288-304: ⚠️ Potential issue | 🟠 Major

Validate Content-Range start when resuming to prevent data corruption.

When receiving a 206 response, the code assumes the registry honored the requested offset, but a misbehaving registry or proxy could return a different range. Without validation, the reconstructed blob would be corrupted silently.

Suggested fix
             Ok(response) => {
                 if response.status() == StatusCode::PARTIAL_CONTENT {
+                    // Validate Content-Range header matches our requested offset
+                    if let Some(content_range) = response
+                        .headers()
+                        .get(reqwest::header::CONTENT_RANGE)
+                        .and_then(|v| v.to_str().ok())
+                    {
+                        // Parse "bytes START-END/TOTAL"
+                        if let Some(start) = content_range
+                            .strip_prefix("bytes ")
+                            .and_then(|s| s.split('-').next())
+                            .and_then(|s| s.parse::<u64>().ok())
+                        {
+                            if start != bytes_received {
+                                return Err(format!(
+                                    "Resume offset mismatch: requested {}, got {} ({})",
+                                    bytes_received, start, content_range
+                                )
+                                .into());
+                            }
+                        }
+                    }
                     if debug {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fls/oci/from_oci.rs` around lines 288 - 304, When handling a 206 response
for resuming downloads (the branch checking response.status() ==
StatusCode::PARTIAL_CONTENT), validate the Content-Range header before accepting
the stream: read response.headers().get("content-range"), parse the range start
(from the "bytes start-end/total" format), ensure that the parsed start equals
bytes_received, and return an error if the header is missing, malformed, or the
start does not match bytes_received; only then set stream =
Box::pin(response.bytes_stream()). This prevents silent corruption from
misbehaving registries or proxies.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/fls/oci/from_oci.rs`:
- Around line 288-304: When handling a 206 response for resuming downloads (the
branch checking response.status() == StatusCode::PARTIAL_CONTENT), validate the
Content-Range header before accepting the stream: read
response.headers().get("content-range"), parse the range start (from the "bytes
start-end/total" format), ensure that the parsed start equals bytes_received,
and return an error if the header is missing, malformed, or the start does not
match bytes_received; only then set stream = Box::pin(response.bytes_stream()).
This prevents silent corruption from misbehaving registries or proxies.

In `@tests/oci_extract.rs`:
- Around line 482-498: The code currently parses start_str into start and then
slices blob_body[start..], which will panic if start > blob_body.len(); update
the Range-handling in the block that parses start_str (the start variable and
subsequent slicing of blob_body) to first check if start <= blob_body.len() and
only proceed to build the PARTIAL_CONTENT Response when valid, otherwise return
a 416 Range Not Satisfiable Response (use StatusCode::RANGE_NOT_SATISFIABLE)
with an appropriate Content-Range header like "bytes */{blob_body.len()}" and no
body; ensure you adjust the Response::builder() branch that currently sets
Content-Length/Content-Range/Accept-Ranges so the invalid-case returns the 416
response instead of panicking.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 167aaf70-b02f-4501-ba22-807eba603756

📥 Commits

Reviewing files that changed from the base of the PR and between 8b73749 and 81d4e19.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • Cargo.toml
  • src/fls/decompress.rs
  • src/fls/download_error.rs
  • src/fls/fastboot.rs
  • src/fls/from_url.rs
  • src/fls/mod.rs
  • src/fls/oci/from_oci.rs
  • src/fls/oci/registry.rs
  • src/fls/options.rs
  • src/fls/stream_utils.rs
  • src/lib.rs
  • src/main.rs
  • tests/oci_extract.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/fls/mod.rs
  • src/main.rs
  • src/fls/from_url.rs
  • src/fls/fastboot.rs

@bennyz bennyz requested a review from mangelajo March 18, 2026 18:19
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/fls/oci/from_oci.rs (1)

288-304: ⚠️ Potential issue | 🟠 Major

Missing Content-Range validation when resuming downloads.

When the server responds with 206 Partial Content, the code doesn't verify that the returned range actually starts at bytes_received. A misbehaving registry or proxy could return a mismatched range, leading to data corruption in the reconstructed blob.

🛡️ Suggested fix to validate Content-Range
             Ok(response) => {
                 if response.status() == StatusCode::PARTIAL_CONTENT {
+                    // Validate Content-Range header to prevent data corruption
+                    let content_range = response
+                        .headers()
+                        .get(reqwest::header::CONTENT_RANGE)
+                        .and_then(|v| v.to_str().ok());
+                    
+                    if let Some(range_str) = content_range {
+                        // Parse "bytes START-END/TOTAL" format
+                        if let Some(start) = range_str
+                            .strip_prefix("bytes ")
+                            .and_then(|s| s.split('-').next())
+                            .and_then(|s| s.parse::<u64>().ok())
+                        {
+                            if start != bytes_received {
+                                return Err(format!(
+                                    "Resume offset mismatch: requested {}, got {} (Content-Range: {})",
+                                    bytes_received, start, range_str
+                                ).into());
+                            }
+                        }
+                    }
+                    
                     if debug {
                         eprintln!(
                             "[DEBUG] Registry supports Range, resuming from byte {}",
                             bytes_received
                         );
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fls/oci/from_oci.rs` around lines 288 - 304, The code handling a 206
Partial Content response (the branch checking response.status() ==
StatusCode::PARTIAL_CONTENT) does not validate the Content-Range header against
bytes_received, so a mismatched range could corrupt the blob; update the handler
to read response.headers().get("Content-Range"), parse the returned start byte
and ensure it equals bytes_received (or return an Err if missing/mismatched),
log a debug message when it matches, and only then set stream =
Box::pin(response.bytes_stream()); reference the symbols response,
StatusCode::PARTIAL_CONTENT, bytes_received, Content-Range header, and stream to
find where to insert the validation and error path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/fls/oci/from_oci.rs`:
- Around line 453-456: Wrap the Unix-specific conversion of decompressor_stdout
into a std::fs::File with conditional compilation: enclose the existing call
using decompressor_stdout.into_owned_fd().map_err(...).into() inside a
#[cfg(unix)] block (referencing decompressor_stdout and into_owned_fd and the
std_stdout binding), and add a #[cfg(not(unix))] branch that provides a
platform-appropriate fallback (for example return a descriptive Err or an
alternative conversion) so the code compiles on non-Unix targets; ensure both
branches assign/return the same std_stdout symbol or equivalent result type.

---

Duplicate comments:
In `@src/fls/oci/from_oci.rs`:
- Around line 288-304: The code handling a 206 Partial Content response (the
branch checking response.status() == StatusCode::PARTIAL_CONTENT) does not
validate the Content-Range header against bytes_received, so a mismatched range
could corrupt the blob; update the handler to read
response.headers().get("Content-Range"), parse the returned start byte and
ensure it equals bytes_received (or return an Err if missing/mismatched), log a
debug message when it matches, and only then set stream =
Box::pin(response.bytes_stream()); reference the symbols response,
StatusCode::PARTIAL_CONTENT, bytes_received, Content-Range header, and stream to
find where to insert the validation and error path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: faadd08c-4e24-4db7-b277-08dc77d88c1a

📥 Commits

Reviewing files that changed from the base of the PR and between 8b73749 and 81d4e19.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • Cargo.toml
  • src/fls/decompress.rs
  • src/fls/download_error.rs
  • src/fls/fastboot.rs
  • src/fls/from_url.rs
  • src/fls/mod.rs
  • src/fls/oci/from_oci.rs
  • src/fls/oci/registry.rs
  • src/fls/options.rs
  • src/fls/stream_utils.rs
  • src/lib.rs
  • src/main.rs
  • tests/oci_extract.rs
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/main.rs
  • src/fls/from_url.rs
  • Cargo.toml
  • src/fls/stream_utils.rs
  • src/fls/fastboot.rs

Comment on lines +453 to +456
let std_stdout: std::fs::File = decompressor_stdout
.into_owned_fd()
.map_err(|e| format!("Failed to convert decompressor stdout: {}", e))?
.into();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there are any platform guards or documentation about Unix-only support
rg -n "cfg.*unix|unix.*only|linux.*only" src/ --type rust -B 1 -A 1

# Check if there are other uses of Unix-specific APIs
rg -n "into_owned_fd|from_raw_fd|as_raw_fd" src/ --type rust -B 1 -A 1

Repository: jumpstarter-dev/fls

Length of output: 3568


Add platform guards around Unix-specific API usage at lines 453-456.

The into_owned_fd() call is Unix-specific and will fail to compile on non-Unix platforms (e.g., Windows). The same file already demonstrates the correct pattern with platform guards at lines 1447–1459. Wrap the conversion in a #[cfg(unix)] block, and provide a #[cfg(not(unix))] fallback implementation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fls/oci/from_oci.rs` around lines 453 - 456, Wrap the Unix-specific
conversion of decompressor_stdout into a std::fs::File with conditional
compilation: enclose the existing call using
decompressor_stdout.into_owned_fd().map_err(...).into() inside a #[cfg(unix)]
block (referencing decompressor_stdout and into_owned_fd and the std_stdout
binding), and add a #[cfg(not(unix))] branch that provides a
platform-appropriate fallback (for example return a descriptive Err or an
alternative conversion) so the code compiles on non-Unix targets; ensure both
branches assign/return the same std_stdout symbol or equivalent result type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant