Commit 283f2d9
fix(client): handle unaligned ImageBytes response buffers (#19)
* fix(client): handle unaligned ImageBytes response buffers
`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 #18.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* review: address clippy + Copilot feedback
- 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>
* perf(client): fuse cast_raw_data slow path into single-pass widen
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>
* review: use as_chunks with a fixed-size &[u8; N] widening trait
Address RReverser's review on the cast_raw_data unaligned fallback: swap
chunks_exact for <[u8]>::as_chunks::<N>() and move the per-element
little-endian widening onto a companion WidenLeChunk<const N> trait whose
method takes a fixed-size &[u8; N]. This ties the byte-chunk width to the
element type at the type level (i16/u16 => 2, i32 => 4, u8 => 1) and drops
the per-pixel bounds checks; as_chunks' remainder replaces the manual
is_multiple_of slop check while preserving the OutputSliceWouldHaveSlop
guarantee. The aligned try_cast_slice fast path is unchanged.
WidenLeChunk is gated behind #[cfg(feature = "client")] since it is only
used by the client-gated cast_raw_data. Also reword a stale test doc
comment that referenced pod_collect_to_vec.
Verified: from_reqwest_handles_unaligned_imagebytes_slice passes; clippy
-D warnings clean on --all-features and on the client,camera /
server,camera feature combos.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* review: fold unaligned widening onto AsTransmissionElementType
Per maintainer feedback, replace the separate `WidenLeChunk<const N>`
trait with a single client-gated `widen_unaligned_le` method on the
existing `AsTransmissionElementType` trait, and drop the const generic
from `cast_raw_data` (call sites are now `cast_raw_data::<i16>` etc.).
Each impl hard-codes `as_chunks::<N>()` to its own byte width, so the
slow path still decodes fixed-size `&[u8; N]` chunks with no per-pixel
bounds checks. The "obvious" form (an associated `const WIDTH` driving
`&[u8; Self::WIDTH]` / `as_chunks::<{ T::WIDTH }>()`) needs the unstable
`generic_const_exprs` feature, so the explicit-width method is the
stable-toolchain realization of the same intent.
No behavioural change: the aligned fast path is byte-for-byte identical,
and the slow path preserves the exact error semantics (a non-empty
`as_chunks` remainder -> `OutputSliceWouldHaveSlop`; only the alignment
failure falls back; all other `PodCastError`s propagate verbatim).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* review: collapse unaligned widening into one default method
Fold the four near-identical `widen_unaligned_le` impls into a single
default method on `AsTransmissionElementType`, reading each pixel with
`bytemuck::pod_read_unaligned` over `chunks_exact(size_of::<Self>())`
instead of per-type `as_chunks::<N>` + `from_le_bytes`. The four impls
shrink to just `const TYPE`.
Drop the redundant `'static` supertrait (already implied by
`AnyBitPattern`) and rename the method to `widen_unaligned`: the read is
host-endian, and little-endianness is guaranteed by the crate's
little-endian-only `compile_error!` guard rather than by this function.
The one-time `chunks_exact` remainder check preserves the
`OutputSliceWouldHaveSlop` guarantee and keeps the per-element read's
length-mismatch panic unreachable, so the output `Vec<i32>` is still
built in a single exact-capacity pass. Add a test covering sign/zero
extension across all four element widths, which the i32-only regression
test did not exercise.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Associate types with fixed-size arrays
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Ingvar Stepanyan <me@rreverser.com>1 parent e4a828b commit 283f2d9
3 files changed
Lines changed: 136 additions & 11 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
| 39 | + | |
39 | 40 | | |
40 | 41 | | |
41 | 42 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | | - | |
3 | | - | |
| 2 | + | |
| 3 | + | |
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
12 | 11 | | |
| 12 | + | |
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| |||
83 | 83 | | |
84 | 84 | | |
85 | 85 | | |
86 | | - | |
87 | | - | |
88 | | - | |
89 | | - | |
90 | | - | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
91 | 119 | | |
92 | 120 | | |
93 | 121 | | |
| |||
108 | 136 | | |
109 | 137 | | |
110 | 138 | | |
111 | | - | |
| 139 | + | |
112 | 140 | | |
113 | 141 | | |
114 | | - | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
115 | 150 | | |
116 | 151 | | |
117 | 152 | | |
| |||
168 | 203 | | |
169 | 204 | | |
170 | 205 | | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
62 | 62 | | |
63 | 63 | | |
64 | 64 | | |
65 | | - | |
| 65 | + | |
66 | 66 | | |
| 67 | + | |
67 | 68 | | |
68 | 69 | | |
69 | 70 | | |
70 | 71 | | |
| 72 | + | |
71 | 73 | | |
72 | 74 | | |
73 | 75 | | |
74 | 76 | | |
| 77 | + | |
75 | 78 | | |
76 | 79 | | |
77 | 80 | | |
78 | 81 | | |
| 82 | + | |
79 | 83 | | |
80 | 84 | | |
81 | 85 | | |
82 | 86 | | |
| 87 | + | |
| 88 | + | |
83 | 89 | | |
84 | 90 | | |
85 | 91 | | |
| |||
0 commit comments