Skip to content

fix(client): handle unaligned ImageBytes response buffers#19

Open
ivonnyssen wants to merge 3 commits into
RReverser:mainfrom
ivonnyssen:fix/image-array-alignment
Open

fix(client): handle unaligned ImageBytes response buffers#19
ivonnyssen wants to merge 3 commits into
RReverser:mainfrom
ivonnyssen:fix/image-array-alignment

Conversation

@ivonnyssen
Copy link
Copy Markdown
Contributor

Summary

  • Replaces bytemuck::try_from_bytes::<ImageBytesMetadata> (line 114) with bytemuck::pod_read_unaligned.
  • Replaces bytemuck::try_cast_slice::<u8, T> in cast_raw_data with a length-checked bytemuck::pod_collect_to_vec that allocates a fresh aligned Vec<T> and memcpys the payload in.
  • Adds a regression test exercising leading-pad offsets 0..4 within the source Vec<u8> so at least three iterations place the body slice on a non-4-byte-aligned start.

Both call sites assumed the HTTP response body buffer (bytes::Bytes slice from reqwest's chunked read) is 4-byte aligned. The OS allocator usually delivers that, but not always — particularly on macOS under bazel's test runner — and clients see intermittent ASCOM error UNSPECIFIED: TargetAlignmentGreaterAndInputNotAligned panics.

Fixes #18.

Test plan

  • cargo test --all-features -p ascom-alpaca --lib api::camera::image_array::client::tests — new regression test passes.
  • cargo check --all-features — clean.
  • No public API change; the only signature change is internal (cast_raw_data gains a bytemuck::NoUninit bound, satisfied by all four impl types).

🤖 Generated with Claude Code

`ASCOMResult<ImageArray>::from_reqwest` reads the 44-byte
`ImageBytesMetadata` header via `bytemuck::try_from_bytes` and the
pixel payload via `bytemuck::try_cast_slice::<u8, T>`. Both APIs
require the source `&[u8]` to be aligned to the target type's
alignment (4 for `i32`, 2 for `i16`/`u16`). The `&[u8]` here is a
slice of the HTTP response body — typically a `bytes::Bytes` chunk
that reqwest assembled from one or more chunked reads — and its
start pointer is not guaranteed to satisfy that alignment. When it
doesn't, both calls return
`PodCastError::TargetAlignmentGreaterAndInputNotAligned` and the
client panics with `ASCOM error UNSPECIFIED:
TargetAlignmentGreaterAndInputNotAligned` — intermittently,
depending on the host allocator's runtime behaviour.

Fix: use the unaligned-friendly equivalents.

- Metadata: `bytemuck::pod_read_unaligned` reads the full struct via
  `ptr::read_unaligned`, no source-alignment requirement.
- Pixel data: `bytemuck::pod_collect_to_vec` allocates a fresh
  `Vec<T>` (which IS aligned) and `memcpy`s the payload in. Adds an
  upfront length-modulo check to keep the existing slop-error
  semantics.

Adds a regression test that constructs an `imagebytes` payload at
each of the four leading-pad offsets (0..4) within a `Vec<u8>`,
guaranteeing at least three of them place the body slice on a
non-4-aligned start. The test fails on the previous code path
(`try_from_bytes` rejects the unaligned metadata immediately) and
passes after the fix.

Closes RReverser#18.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 10, 2026 19:25
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes client-side parsing of application/imagebytes responses when the underlying HTTP body buffer is not properly aligned for i16/i32/u16 reads (issue #18), avoiding intermittent bytemuck alignment failures on some allocators/runners.

Changes:

  • Switch metadata parsing from try_from_bytes to bytemuck::pod_read_unaligned.
  • Replace raw pixel casting with an unaligned-safe path using bytemuck::pod_collect_to_vec (plus a length check).
  • Add a regression test that exercises multiple leading offsets to ensure unaligned slice starts are handled.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/api/camera/image_array/client.rs Outdated
Comment thread src/api/camera/image_array/client.rs Outdated
Comment thread src/api/camera/image_array/client.rs Outdated
ivonnyssen and others added 2 commits May 10, 2026 12:32
- cast_raw_data: keep the zero-copy fast path. Try
  `bytemuck::try_cast_slice::<u8, T>` first; only fall back to the
  copying `pod_collect_to_vec` when it returns
  `TargetAlignmentGreaterAndInputNotAligned`. Slop / length-mismatch
  errors propagate verbatim. Common (aligned) case keeps the
  pre-PR memory and copy profile; unaligned case still works.
- Drop redundant `if !data.len().is_multiple_of(elem_size)` length
  check now that the slop error path is reached via try_cast_slice
  directly.
- Metadata fix doc comment: refer to
  `align_of::<ImageBytesMetadata>()` instead of the hard-coded
  `align_of::<i32>() == 4` so it stays correct if the struct's
  layout grows a wider field.
- Test helper doc comment: clarify the alignment guarantee — the
  `Vec<u8>` base allocation is high-aligned by the system
  allocator, so looping leading_pad over 0..4 covers all four mod-4
  rotations and guarantees at least three unaligned cases.
- Test fn signature: split into a `Result<()>`-returning helper
  (`check_one_offset`) using `eyre::ensure!` for validation and a
  panicking `#[test]` shell that surfaces the failing leading_pad
  in the panic message. Avoids clippy::panic_in_result_fn while
  keeping diagnostic context.
- Replace `data.len() % elem_size != 0` with the
  `is_multiple_of` form (clippy::manual_is_multiple_of),
  superseded by the fast-path refactor above.
- Clean up `as i32` casts in the test helper to `i32::from(...)`
  for the enum reprs and `i32::try_from(...)?` for the
  size_of/len conversions (clippy::cast_lossless / cast_possible_truncation).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the slow path's two-memcpy bytes→Vec<T>→Vec<i32> staging
with a single chunked pass that decodes each `size_of::<T>()`-byte
chunk via a new `widen_from_le_chunk` trait method on
`AsTransmissionElementType` and writes straight into the output
`Vec<i32>`.

Savings:

- One full pixel-buffer memcpy on unaligned input.
- Memory ceiling on the slow path drops from
  `data.len() + sizeof_output` to just `sizeof_output`. For a
  32 MP `i32` image that's ~128 MB peak instead of ~256 MB.
- Drops the `bytemuck::NoUninit` bound on the local function (was
  only required by `pod_collect_to_vec`).

The fast (aligned) path is unchanged from the previous commit —
`try_cast_slice` reinterprets in place, and we iterate-and-widen
into the output. So the common case keeps the pre-PR memory and
copy profile; the slow path is now strictly an improvement.

`from_le_bytes` on `i32` lowers to the same instruction as a normal
aligned `i32` load on little-endian targets, so the per-element
loop in the slow path is essentially identical in cost to a bulk
memcpy + widening loop. For the smaller types (`i16` / `u16` /
`u8`) the upconversion was element-wise anyway, so the staging
buffer was pure overhead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ivonnyssen added a commit to ivonnyssen/ascom-alpaca-rs that referenced this pull request May 10, 2026
…y ceiling

Squashes upstream RReverser#19:

- `cast_raw_data` and the `ImageBytesMetadata` parse no longer assume
  the HTTP response body buffer is `align_of::<T>()`-aligned. The
  fast (aligned) path is unchanged — `try_cast_slice` reinterprets
  in place — and the unaligned path decodes each
  `size_of::<T>()`-byte chunk via a new
  `AsTransmissionElementType::widen_from_le_chunk` trait method,
  going straight from response bytes into `Vec<i32>` in one pass.
- Metadata parse switches to `bytemuck::pod_read_unaligned`, which
  uses `ptr::read_unaligned` and works for any pointer.
- Slow-path peak memory drops from `data.len() + sizeof_output` to
  just `sizeof_output`. For a 32 MP `i32` capture that's ~128 MB
  instead of ~256 MB.
- Adds a regression test that loops `leading_pad` over `0..4` so at
  least three of the four iterations exercise an unaligned source
  slice.

Fixes the intermittent `ASCOM error UNSPECIFIED:
TargetAlignmentGreaterAndInputNotAligned` in ascom-alpaca clients
that depended on the host allocator's runtime alignment behaviour
(observed on macOS under bazel).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ivonnyssen added a commit to ivonnyssen/ascom-alpaca-rs that referenced this pull request May 12, 2026
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.

cast_raw_data fails with TargetAlignmentGreaterAndInputNotAligned when ImageBytes body buffer is not 4-byte aligned

2 participants