@@ -157,6 +157,16 @@ impl ArrayMap {
157157 max_val. wrapping_sub ( min_val)
158158 }
159159
160+ #[ inline]
161+ fn key_to_index ( key : u64 , offset : u64 , data_len : usize ) -> Option < usize > {
162+ let idx = key. wrapping_sub ( offset) ;
163+ if idx < data_len as u64 {
164+ Some ( idx as usize )
165+ } else {
166+ None
167+ }
168+ }
169+
160170 /// Creates a new [`ArrayMap`] from the given array of join keys.
161171 ///
162172 /// Note: This function processes only the non-null values in the input `array`,
@@ -207,10 +217,9 @@ impl ArrayMap {
207217 for ( i, val) in arr. iter ( ) . enumerate ( ) . rev ( ) {
208218 if let Some ( val) = val {
209219 let key: u64 = val. as_ ( ) ;
210- let idx = key. wrapping_sub ( offset_val) as usize ;
211- if idx >= data. len ( ) {
220+ let Some ( idx) = Self :: key_to_index ( key, offset_val, data. len ( ) ) else {
212221 return internal_err ! ( "failed build Array idx >= data.len()" ) ;
213- }
222+ } ;
214223
215224 if data[ idx] != 0 {
216225 if next. is_empty ( ) {
@@ -264,6 +273,16 @@ impl ArrayMap {
264273 )
265274 }
266275
276+ /// Returns whether `key` (a raw probe value cast to `u64`) is present in the
277+ /// build side, i.e. maps to a non-empty bucket.
278+ #[ inline]
279+ fn key_present ( & self , key : u64 ) -> bool {
280+ let Some ( idx) = Self :: key_to_index ( key, self . offset , self . data . len ( ) ) else {
281+ return false ;
282+ } ;
283+ self . data [ idx] != 0
284+ }
285+
267286 fn lookup_and_get_indices < T : ArrowNumericType > (
268287 & self ,
269288 array : & ArrayRef ,
@@ -294,11 +313,13 @@ impl ArrayMap {
294313 }
295314 // SAFETY: prob_idx is guaranteed to be within bounds by the loop range.
296315 let prob_val: u64 = unsafe { arr. value_unchecked ( prob_idx) } . as_ ( ) ;
297- let idx_in_build_side = prob_val. wrapping_sub ( self . offset ) as usize ;
316+ let Some ( idx_in_build_side) =
317+ Self :: key_to_index ( prob_val, self . offset , self . data . len ( ) )
318+ else {
319+ continue ;
320+ } ;
298321
299- if idx_in_build_side >= self . data . len ( )
300- || self . data [ idx_in_build_side] == 0
301- {
322+ if self . data [ idx_in_build_side] == 0 {
302323 continue ;
303324 }
304325 build_indices. push ( ( self . data [ idx_in_build_side] - 1 ) as u64 ) ;
@@ -345,10 +366,12 @@ impl ArrayMap {
345366
346367 // SAFETY: prob_idx is guaranteed to be within bounds by the loop range.
347368 let prob_val: u64 = unsafe { arr. value_unchecked ( prob_side_idx) } . as_ ( ) ;
348- let idx_in_build_side = prob_val. wrapping_sub ( self . offset ) as usize ;
349- if idx_in_build_side >= self . data . len ( )
350- || self . data [ idx_in_build_side] == 0
351- {
369+ let Some ( idx_in_build_side) =
370+ Self :: key_to_index ( prob_val, self . offset , self . data . len ( ) )
371+ else {
372+ continue ;
373+ } ;
374+ if self . data [ idx_in_build_side] == 0 {
352375 continue ;
353376 }
354377
@@ -402,8 +425,7 @@ impl ArrayMap {
402425 }
403426 // SAFETY: i is within bounds [0, arr.len())
404427 let key: u64 = unsafe { arr. value_unchecked ( i) } . as_ ( ) ;
405- let idx = key. wrapping_sub ( self . offset ) as usize ;
406- idx < self . data . len ( ) && self . data [ idx] != 0
428+ self . key_present ( key)
407429 } ) ;
408430 Ok ( BooleanArray :: new ( buffer, None ) )
409431 }
@@ -414,6 +436,7 @@ mod tests {
414436 use super :: * ;
415437 use arrow:: array:: Int32Array ;
416438 use arrow:: array:: Int64Array ;
439+ use arrow:: array:: UInt64Array ;
417440 use std:: sync:: Arc ;
418441
419442 #[ test]
@@ -506,6 +529,50 @@ mod tests {
506529 Ok ( ( ) )
507530 }
508531
532+ #[ test]
533+ fn test_array_map_rejects_large_out_of_range_probe_key ( ) -> Result < ( ) > {
534+ let build: ArrayRef =
535+ Arc :: new ( UInt64Array :: from ( vec ! [ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ] ) ) ;
536+ let map = ArrayMap :: try_new ( & build, 0 , 10 ) ?;
537+
538+ assert_eq ! ( ArrayMap :: key_to_index( 3 , 0 , 11 ) , Some ( 3 ) ) ;
539+
540+ // Pick a key for which the computed bucket offset is larger than
541+ // u32::MAX but has low 32 bits equal to 3. It must be bounds-checked
542+ // before casting to usize, otherwise 32-bit targets can truncate it
543+ // into range.
544+ let out_of_range_key = ( 1_u64 << 32 ) + 3 ;
545+ assert_eq ! ( ArrayMap :: key_to_index( out_of_range_key, 0 , 11 ) , None ) ;
546+
547+ let probe = [ Arc :: new ( UInt64Array :: from ( vec ! [
548+ Some ( 3 ) ,
549+ Some ( out_of_range_key) ,
550+ Some ( 11 ) ,
551+ None ,
552+ ] ) ) as ArrayRef ] ;
553+
554+ let mut matched_probe_indices = vec ! [ ] ;
555+ let mut matched_build_indices = vec ! [ ] ;
556+ let next = map. get_matched_indices_with_limit_offset (
557+ & probe,
558+ 10 ,
559+ ( 0 , None ) ,
560+ & mut matched_probe_indices,
561+ & mut matched_build_indices,
562+ ) ?;
563+ assert_eq ! ( matched_probe_indices, vec![ 0 ] ) ;
564+ assert_eq ! ( matched_build_indices, vec![ 3 ] ) ;
565+ assert ! ( next. is_none( ) ) ;
566+
567+ let contains = map. contain_keys ( & probe) ?;
568+ assert ! ( contains. value( 0 ) ) ;
569+ assert ! ( !contains. value( 1 ) ) ;
570+ assert ! ( !contains. value( 2 ) ) ;
571+ assert ! ( !contains. value( 3 ) ) ;
572+
573+ Ok ( ( ) )
574+ }
575+
509576 #[ test]
510577 fn test_array_map_i64_with_negative_and_positive_numbers ( ) -> Result < ( ) > {
511578 // Build array with a mix of negative and positive i64 values, no duplicates
0 commit comments