Skip to content

Commit b81d5f1

Browse files
committed
refactor(raster): use i64 throughout the view-machinery internals
Per Dewey's review: every `source_axis`/`source_shape`/`visible_shape` in the view helpers should be `i64` to match `ViewEntry`'s signed fields and the stride arithmetic, instead of round-tripping through `u64` and sprinkling `as` casts through helper bodies. Surface kept the same: - `BandRef::shape() -> &[u64]` still returns u64 (matches `width()`, `height()`, and Arrow's `UInt64Array` storage). Bridged via an `unsafe` slice transmute with a documented invariant — `validate_view` guarantees every entry is `>= 0` so the bit-layout reinterpretation is sound. - `BandRef::raw_source_shape() -> &[u64]` unchanged (raw Arrow view). Internals now `i64`: - `validate_view(view, source_shape: &[i64])`, with an added `source_shape[sa] >= 0` check at the boundary. - `is_identity_view(view, source_shape: &[i64])`. - `visible_shape_from_view(view) -> Vec<i64>`. - `BandRefImpl::visible_shape: Vec<i64>`. - `materialize_strided` and `check_view_buffer_bounds` take `visible_shape: &[i64]`. - `NdBuffer.shape: Vec<i64>`, `NdBuffer.offset: i64` (internal struct with no out-of-crate consumers — dropping the `i64 → u64 → i64` round-trip Dewey called out). Conversions concentrated at two well-defined boundaries: - `RasterRefImpl::band()` converts `raw_source_shape() -> &[u64]` into `Vec<i64>` once with a `try_from` overflow check. - `RasterBuilder::start_band_with_view` does the same conversion at its `source_shape: &[u64]` entry point before calling `validate_view`. Tests pass: sedona-raster 131, sedona-raster-functions 143, sedona-raster-gdal 53. Trait surface and Arrow storage unchanged.
1 parent e890f90 commit b81d5f1

3 files changed

Lines changed: 106 additions & 50 deletions

File tree

rust/sedona-raster/src/array.rs

Lines changed: 55 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,10 @@ struct BandRefImpl<'a> {
5050
data_type: BandDataType,
5151
/// Per-visible-axis view, length = ndim
5252
view_entries: Vec<ViewEntry>,
53-
/// Visible shape (== `[v.steps for v in view_entries]`), length = ndim
54-
visible_shape: Vec<u64>,
53+
/// Visible shape (== `[v.steps for v in view_entries]`), length = ndim.
54+
/// `i64` to match the surrounding view-machinery arithmetic
55+
/// (strides, offsets); `validate_view` guarantees `>= 0`.
56+
visible_shape: Vec<i64>,
5557
/// Byte strides per visible axis. May be 0 (broadcast) or negative.
5658
byte_strides: Vec<i64>,
5759
/// Byte offset into `data` of the visible region's `[0,...,0]` element.
@@ -84,7 +86,18 @@ impl<'a> BandRef for BandRefImpl<'a> {
8486
}
8587

8688
fn shape(&self) -> &[u64] {
87-
&self.visible_shape
89+
// SAFETY: `visible_shape` elements are i64 but `validate_view`
90+
// guarantees they are all `>= 0` at construction. i64 and u64
91+
// have identical bit layout for non-negative values, so
92+
// reinterpreting the slice is sound under that invariant. The
93+
// alternative (storing a parallel `Vec<u64>` or allocating on
94+
// every call) is wasteful for a hot accessor.
95+
unsafe {
96+
std::slice::from_raw_parts(
97+
self.visible_shape.as_ptr() as *const u64,
98+
self.visible_shape.len(),
99+
)
100+
}
88101
}
89102

90103
fn raw_source_shape(&self) -> &[u64] {
@@ -168,13 +181,11 @@ impl<'a> BandRef for BandRefImpl<'a> {
168181
}
169182
// shape and strides are owned by NdBuffer (see its doc comment).
170183
// Cloning here is cheap — both vecs are O(ndim), a handful of values.
171-
// Cast offset i64 -> u64: safe because RasterRefImpl::band asserts
172-
// byte_offset >= 0 before storing.
173184
Ok(NdBuffer {
174185
buffer: self.data_array.value(self.band_row),
175186
shape: self.visible_shape.clone(),
176187
strides: self.byte_strides.clone(),
177-
offset: self.byte_offset as u64,
188+
offset: self.byte_offset,
178189
data_type: self.data_type,
179190
})
180191
}
@@ -186,10 +197,8 @@ impl<'a> BandRef for BandRefImpl<'a> {
186197
// ARE the visible bytes. Borrow them.
187198
return Ok(Cow::Borrowed(buf.buffer));
188199
}
189-
// Use self.* layout fields rather than `buf.*` to avoid the
190-
// i64 -> u64 -> i64 round-trip through NdBuffer for `byte_offset`.
191-
// The visible shape and byte strides are precomputed once at
192-
// construction; `buf.shape` / `buf.strides` are clones of those.
200+
// Use self.* layout fields directly; `buf.shape` / `buf.strides`
201+
// are clones of these same fields.
193202
let out = materialize_strided(
194203
buf.buffer,
195204
&self.visible_shape,
@@ -220,7 +229,7 @@ impl<'a> BandRef for BandRefImpl<'a> {
220229
/// and skip the check.
221230
fn check_view_buffer_bounds(
222231
buffer_len: usize,
223-
visible_shape: &[u64],
232+
visible_shape: &[i64],
224233
byte_strides: &[i64],
225234
byte_offset: i64,
226235
dtype_size: usize,
@@ -231,9 +240,9 @@ fn check_view_buffer_bounds(
231240
let mut min_offset = byte_offset;
232241
let mut max_offset = byte_offset;
233242
for (k, &stride) in byte_strides.iter().enumerate() {
234-
let last_idx = i64::try_from(visible_shape[k] - 1).map_err(|_| {
235-
ArrowError::InvalidArgumentError(format!("visible_shape[{k}] - 1 exceeds i64::MAX"))
236-
})?;
243+
// `validate_view` guarantees `steps >= 0`, so `visible_shape[k] >= 0`
244+
// and `visible_shape[k] - 1` is in-range for any non-empty axis.
245+
let last_idx = visible_shape[k] - 1;
237246
let contribution = last_idx.checked_mul(stride).ok_or_else(|| {
238247
ArrowError::InvalidArgumentError(format!(
239248
"max addressable offset on axis {k} overflows i64"
@@ -285,7 +294,7 @@ fn check_view_buffer_bounds(
285294
/// surface the failure.
286295
fn materialize_strided(
287296
buffer: &[u8],
288-
visible_shape: &[u64],
297+
visible_shape: &[i64],
289298
byte_strides: &[i64],
290299
byte_offset: i64,
291300
dtype_size: usize,
@@ -302,7 +311,7 @@ fn materialize_strided(
302311
// we'd otherwise repeat here, so this walk uses plain arithmetic and
303312
// unchecked slice indexing within the buffer.
304313
let ndim = visible_shape.len();
305-
let total: u64 = visible_shape.iter().product();
314+
let total: i64 = visible_shape.iter().product();
306315
if total == 0 {
307316
return Vec::new();
308317
}
@@ -322,13 +331,13 @@ fn materialize_strided(
322331

323332
// Precompute a small index vector for outer axes (everything except
324333
// the innermost). For 1D this is empty and we run a single pass.
325-
let mut outer_idx = vec![0u64; ndim.saturating_sub(1)];
334+
let mut outer_idx = vec![0i64; ndim.saturating_sub(1)];
326335
loop {
327336
// Compute the byte offset of the row's first element from the
328337
// current outer index combination.
329338
let mut row_off = base;
330339
for (k, &i) in outer_idx.iter().enumerate() {
331-
row_off += (i as i64) * byte_strides[k];
340+
row_off += i * byte_strides[k];
332341
}
333342

334343
if row_bytes_contiguous {
@@ -435,18 +444,37 @@ impl<'a> RasterRef for RasterRefImpl<'a> {
435444
// Read source shape slice.
436445
let ss_start = self.band_source_shape_list.value_offsets()[band_row] as usize;
437446
let ss_end = self.band_source_shape_list.value_offsets()[band_row + 1] as usize;
438-
let source_shape: &[u64] = &self.band_source_shape_values.values()[ss_start..ss_end];
447+
let source_shape_u64: &[u64] = &self.band_source_shape_values.values()[ss_start..ss_end];
439448

440449
// Reject 0-D bands at the read boundary. Schema doesn't forbid them
441450
// outright but every consumer assumes ndim >= 1.
442-
if source_shape.is_empty() {
451+
if source_shape_u64.is_empty() {
443452
return Err(ArrowError::ExternalError(Box::new(
444453
sedona_common::sedona_internal_datafusion_err!(
445454
"band {band_row} has empty source_shape; ndim must be >= 1"
446455
),
447456
)));
448457
}
449458

459+
// Convert source_shape u64 → i64 once with overflow check. Every
460+
// downstream consumer in the view machinery wants i64 (matches
461+
// ViewEntry's signed fields and the stride arithmetic). The cast
462+
// is fallible only on cosmically large dims (> 2^63); a clean
463+
// internal error is better than a wrap that silently passes
464+
// later bound checks.
465+
let source_shape: Vec<i64> = source_shape_u64
466+
.iter()
467+
.map(|&s| {
468+
i64::try_from(s).map_err(|_| {
469+
ArrowError::ExternalError(Box::new(
470+
sedona_common::sedona_internal_datafusion_err!(
471+
"band {band_row}: source_shape axis {s} exceeds i64::MAX"
472+
),
473+
))
474+
})
475+
})
476+
.collect::<Result<_, _>>()?;
477+
450478
// Resolve data type up front; an unknown discriminant is a
451479
// schema-corruption bug, not user data, so failing the band loudly
452480
// here is appropriate.
@@ -474,7 +502,7 @@ impl<'a> RasterRef for RasterRefImpl<'a> {
474502
source_axis: i as i64,
475503
start: 0,
476504
step: 1,
477-
steps: s as i64,
505+
steps: s,
478506
})
479507
.collect()
480508
} else {
@@ -492,7 +520,7 @@ impl<'a> RasterRef for RasterRefImpl<'a> {
492520

493521
// Full validation: length match, source_axis permutation, bounds,
494522
// and steps >= 0. Anything malformed is schema-level corruption.
495-
if let Err(e) = validate_view(&view_entries, source_shape) {
523+
if let Err(e) = validate_view(&view_entries, &source_shape) {
496524
return Err(ArrowError::ExternalError(Box::new(
497525
sedona_common::sedona_internal_datafusion_err!(
498526
"band {band_row} has malformed view: {e}"
@@ -517,17 +545,14 @@ impl<'a> RasterRef for RasterRefImpl<'a> {
517545
// C-order byte strides over the source_shape:
518546
// source_strides_bytes[k] = dtype_size * Π_{j>k} source_shape[j]
519547
//
520-
// Computed with checked arithmetic so a corrupt source_shape (a u64
521-
// that doesn't fit in i64, or a product that overflows) is rejected
522-
// here rather than producing a wrapped stride that silently passes
523-
// later bound checks.
548+
// Computed with checked arithmetic so a corrupt source_shape whose
549+
// product overflows i64 is rejected rather than producing a wrapped
550+
// stride that silently passes later bound checks.
524551
let mut source_strides_bytes = vec![0i64; source_shape.len()];
525552
source_strides_bytes[source_shape.len() - 1] = dtype_size;
526553
for k in (0..source_shape.len() - 1).rev() {
527-
let next_axis = i64::try_from(source_shape[k + 1])
528-
.map_err(|_| overflow_err("source_shape axis exceeds i64::MAX"))?;
529554
source_strides_bytes[k] = source_strides_bytes[k + 1]
530-
.checked_mul(next_axis)
555+
.checked_mul(source_shape[k + 1])
531556
.ok_or_else(|| overflow_err("source-stride product overflows i64"))?;
532557
}
533558

@@ -552,7 +577,7 @@ impl<'a> RasterRef for RasterRefImpl<'a> {
552577
.checked_add(start_off)
553578
.ok_or_else(|| overflow_err("view offset accumulation overflows i64"))?;
554579
}
555-
let is_identity_view = is_identity_view(&view_entries, source_shape);
580+
let is_identity_view = is_identity_view(&view_entries, &source_shape);
556581
// byte_offset is non-negative by construction (start >= 0,
557582
// src_stride > 0). Check defensively so a future refactor that
558583
// breaks the invariant surfaces a clean internal error rather than

rust/sedona-raster/src/builder.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,21 @@ impl RasterBuilder {
419419
)));
420420
}
421421

422-
validate_view(view, source_shape)?;
422+
// Convert source_shape u64 → i64 for validation. `start_band_with_view`
423+
// is a builder entry point so producers can express a source_shape
424+
// that doesn't fit in i64 — reject it here rather than letting it
425+
// wrap inside `validate_view`.
426+
let source_shape_i64: Vec<i64> = source_shape
427+
.iter()
428+
.map(|&s| {
429+
i64::try_from(s).map_err(|_| {
430+
ArrowError::InvalidArgumentError(format!(
431+
"start_band_with_view: source_shape axis {s} exceeds i64::MAX"
432+
))
433+
})
434+
})
435+
.collect::<Result<_, _>>()?;
436+
validate_view(view, &source_shape_i64)?;
423437

424438
// Write fields.
425439
match name {
@@ -470,9 +484,16 @@ impl RasterBuilder {
470484
self.band_data_count_at_start = self.band_data.len();
471485

472486
// finish_raster compares visible shape against spatial_shape.
487+
// `visible_shape_from_view` returns Vec<i64>; the entries are
488+
// guaranteed non-negative by `validate_view` above, so the cast
489+
// to u64 is lossless.
490+
let visible_shape_u64: Vec<u64> = visible_shape_from_view(view)
491+
.into_iter()
492+
.map(|v| v as u64)
493+
.collect();
473494
self.current_raster_bands.push((
474495
dim_names.iter().map(|s| s.to_string()).collect(),
475-
visible_shape_from_view(view),
496+
visible_shape_u64,
476497
));
477498

478499
Ok(())

rust/sedona-raster/src/traits.rs

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ use sedona_schema::raster::BandDataType;
3030
/// C-order. Consumers that need a flat row-major buffer should use
3131
/// `BandRef::contiguous_data()` instead.
3232
///
33+
/// `shape` and `offset` are `i64` (not `u64`) to match the surrounding
34+
/// stride arithmetic (`strides: Vec<i64>` to allow negative steps);
35+
/// shape elements are always non-negative — `validate_view` enforces
36+
/// this — and `offset` is non-negative by construction.
37+
///
3338
/// Only `buffer` is tied to the producer's lifetime `'a` (it can be tens of
3439
/// MBs of pixel data and must not be copied). `shape` and `strides` are
3540
/// owned `Vec`s — they're tiny (ndim ≤ a handful) so an allocation here is
@@ -38,9 +43,9 @@ use sedona_schema::raster::BandDataType;
3843
#[derive(Debug)]
3944
pub struct NdBuffer<'a> {
4045
pub buffer: &'a [u8],
41-
pub shape: Vec<u64>,
46+
pub shape: Vec<i64>,
4247
pub strides: Vec<i64>,
43-
pub offset: u64,
48+
pub offset: i64,
4449
pub data_type: BandDataType,
4550
}
4651

@@ -678,13 +683,10 @@ pub trait BandRef {
678683
/// `view[k].steps` is the visible extent along view axis `k` after slicing /
679684
/// broadcasting. Callers should treat the returned shape as authoritative for
680685
/// the visible region; `source_shape` is only meaningful in conjunction with
681-
/// the per-entry `source_axis`.
682-
///
683-
/// `validate_view` guarantees `steps >= 0`, so the `as u64` cast is lossless
684-
/// when the input has already been validated. Callers that haven't validated
685-
/// yet should still call `validate_view` first.
686-
pub(crate) fn visible_shape_from_view(view: &[ViewEntry]) -> Vec<u64> {
687-
view.iter().map(|v| v.steps as u64).collect()
686+
/// the per-entry `source_axis`. `validate_view` guarantees `steps >= 0`,
687+
/// so the entries are all non-negative when input has been validated.
688+
pub(crate) fn visible_shape_from_view(view: &[ViewEntry]) -> Vec<i64> {
689+
view.iter().map(|v| v.steps).collect()
688690
}
689691

690692
/// True iff `view` is the canonical identity over a C-order source buffer:
@@ -698,16 +700,12 @@ pub(crate) fn visible_shape_from_view(view: &[ViewEntry]) -> Vec<u64> {
698700
///
699701
/// Callers should validate the view first; this function checks lengths
700702
/// defensively but trusts the inputs otherwise.
701-
pub(crate) fn is_identity_view(view: &[ViewEntry], source_shape: &[u64]) -> bool {
703+
pub(crate) fn is_identity_view(view: &[ViewEntry], source_shape: &[i64]) -> bool {
702704
if view.len() != source_shape.len() {
703705
return false;
704706
}
705707
view.iter().enumerate().all(|(k, v)| {
706-
v.source_axis == k as i64
707-
&& v.start == 0
708-
&& v.step == 1
709-
&& v.steps >= 0
710-
&& (v.steps as u64) == source_shape[k]
708+
v.source_axis == k as i64 && v.start == 0 && v.step == 1 && v.steps == source_shape[k]
711709
})
712710
}
713711

@@ -789,7 +787,7 @@ pub(crate) fn compose_view(
789787
///
790788
/// Runs implicitly inside `RasterBuilder::with_view` (writer) and
791789
/// `RasterRef::band` (reader); external callers don't need to invoke it.
792-
pub(crate) fn validate_view(view: &[ViewEntry], source_shape: &[u64]) -> Result<(), ArrowError> {
790+
pub(crate) fn validate_view(view: &[ViewEntry], source_shape: &[i64]) -> Result<(), ArrowError> {
793791
let ndim = source_shape.len();
794792
if view.len() != ndim {
795793
return Err(ArrowError::InvalidArgumentError(format!(
@@ -820,8 +818,14 @@ pub(crate) fn validate_view(view: &[ViewEntry], source_shape: &[u64]) -> Result<
820818
v.steps
821819
)));
822820
}
821+
if source_shape[sa] < 0 {
822+
return Err(ArrowError::InvalidArgumentError(format!(
823+
"source_shape[{sa}] = {} must be >= 0",
824+
source_shape[sa]
825+
)));
826+
}
823827
if v.steps > 0 {
824-
let s = source_shape[sa] as i64;
828+
let s = source_shape[sa];
825829
if v.start < 0 || v.start >= s {
826830
return Err(ArrowError::InvalidArgumentError(format!(
827831
"view[{k}].start = {} is out of range [0, {s}) for source axis {sa}",
@@ -1357,7 +1361,13 @@ mod tests {
13571361
StubBand {
13581362
dim_names: dims.iter().map(|s| (*s).to_string()).collect(),
13591363
source_shape: source_shape.to_vec(),
1360-
shape: visible_shape_from_view(view),
1364+
// `visible_shape_from_view` returns Vec<i64>; the StubBand
1365+
// stores `shape: Vec<u64>` to match BandRef::shape()'s return
1366+
// type. validate_view guarantees `steps >= 0`.
1367+
shape: visible_shape_from_view(view)
1368+
.into_iter()
1369+
.map(|v| v as u64)
1370+
.collect(),
13611371
view: view.to_vec(),
13621372
}
13631373
}

0 commit comments

Comments
 (0)