Skip to content

Commit 81d99a8

Browse files
author
Gilad Chase
committed
feat(byte_array): add get(usize) and index(usize) to ByteSpan
1 parent e15c764 commit 81d99a8

File tree

3 files changed

+165
-5
lines changed

3 files changed

+165
-5
lines changed

corelib/src/byte_array.cairo

Lines changed: 103 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -840,8 +840,7 @@ pub impl ByteSpanImpl of ByteSpanTrait {
840840
}
841841

842842
/// Gets the element(s) at the given index.
843-
/// Accepts ranges (returns Option<ByteSpan>), and (to-be-implemented) single indices (returns
844-
/// Option<u8>).
843+
/// Accepts ranges (returns Option<ByteSpan>), and single indices (returns Option<u8>).
845844
#[feature("corelib-get-trait")]
846845
fn get<I, impl TGet: crate::ops::Get<ByteSpan, I>, +Drop<I>>(
847846
self: @ByteSpan, index: I,
@@ -911,6 +910,24 @@ impl ByteSpanGetRangeInclusive of crate::ops::Get<ByteSpan, crate::ops::RangeInc
911910
}
912911
}
913912

913+
impl ByteSpanGetUsize of crate::ops::Get<ByteSpan, usize> {
914+
type Output = u8;
915+
916+
/// Returns the byte at the given index.
917+
/// If out of bounds: returns `None`.
918+
fn get(self: @ByteSpan, index: usize) -> Option<u8> {
919+
helpers::byte_at(self, index)
920+
}
921+
}
922+
923+
impl ByteSpanIndex of core::ops::index::IndexView<ByteSpan, usize> {
924+
type Target = u8;
925+
926+
fn index(self: @ByteSpan, index: usize) -> u8 {
927+
ByteSpanTrait::get(self, index).expect('Index out of bounds')
928+
}
929+
}
930+
914931
/// Trait for types that can be converted into a `ByteSpan`.
915932
#[unstable(feature: "byte-span")]
916933
pub trait ToByteSpanTrait<C> {
@@ -955,18 +972,21 @@ fn shift_right(word: felt252, word_len: usize, n_bytes: usize) -> felt252 {
955972

956973
mod helpers {
957974
use core::num::traits::Bounded;
958-
use crate::bytes_31::BYTES_IN_BYTES31;
975+
use crate::bytes_31::{BYTES_IN_BYTES31, Bytes31Trait, u8_at_u256};
959976
#[feature("bounded-int-utils")]
960977
use crate::internal::bounded_int::{
961-
self, AddHelper, BoundedInt, ConstrainHelper, MulHelper, SubHelper, UnitInt, downcast,
962-
upcast,
978+
self, AddHelper, BoundedInt, ConstrainHelper, DivRemHelper, MulHelper, SubHelper, UnitInt,
979+
downcast, upcast,
963980
};
964981
use super::{BYTES_IN_BYTES31_MINUS_ONE, ByteSpan, Bytes31Index};
965982

966983
type BytesInBytes31Typed = UnitInt<{ BYTES_IN_BYTES31.into() }>;
967984

968985
const U32_MAX_TIMES_B31: felt252 = Bounded::<u32>::MAX.into() * BYTES_IN_BYTES31.into();
969986
const BYTES_IN_BYTES31_UNIT_INT: BytesInBytes31Typed = downcast(BYTES_IN_BYTES31).unwrap();
987+
const NZ_BYTES_IN_BYTES31: NonZero<BytesInBytes31Typed> = 31;
988+
const BYTES_IN_BYTES31_MINUS_ONE_TYPED: UnitInt<{ BYTES_IN_BYTES31_MINUS_ONE.into() }> = 30;
989+
const ONE_TYPED: UnitInt<1> = 1;
970990

971991
impl U32ByB31 of MulHelper<u32, BytesInBytes31Typed> {
972992
type Result = BoundedInt<0, U32_MAX_TIMES_B31>;
@@ -984,6 +1004,50 @@ mod helpers {
9841004
>;
9851005
}
9861006

1007+
// For byte_at: usize + BoundedInt<0,30>
1008+
impl UsizeAddBytes31Index of AddHelper<usize, Bytes31Index> {
1009+
type Result =
1010+
BoundedInt<0, { Bounded::<usize>::MAX.into() + BYTES_IN_BYTES31_MINUS_ONE.into() }>;
1011+
}
1012+
1013+
// For byte_at: div_rem of (usize + BoundedInt<0,30>) by 31
1014+
const USIZE_PLUS_30_DIV_31: felt252 = (Bounded::<usize>::MAX / 31 + 1).into();
1015+
impl UsizePlusBytes31IndexDivRemB31 of DivRemHelper<
1016+
UsizeAddBytes31Index::Result, BytesInBytes31Typed,
1017+
> {
1018+
type DivT = BoundedInt<0, USIZE_PLUS_30_DIV_31>;
1019+
type RemT = Bytes31Index;
1020+
}
1021+
1022+
// For byte_at: 30 - BoundedInt<0,30>
1023+
impl B30SubBytes31Index of SubHelper<
1024+
UnitInt<{ BYTES_IN_BYTES31_MINUS_ONE.into() }>, Bytes31Index,
1025+
> {
1026+
type Result = Bytes31Index;
1027+
}
1028+
1029+
// For byte_at: BoundedInt<0,30> - 1
1030+
impl Bytes31IndexSub1 of SubHelper<Bytes31Index, UnitInt<1>> {
1031+
type Result = BoundedInt<-1, { BYTES_IN_BYTES31_MINUS_ONE.into() - 1 }>;
1032+
}
1033+
1034+
// For byte_at: (BoundedInt<0,30> - 1) - BoundedInt<0,30>
1035+
impl Bytes31IndexMinus1SubBytes31Index of SubHelper<Bytes31IndexSub1::Result, Bytes31Index> {
1036+
type Result =
1037+
BoundedInt<
1038+
{ -BYTES_IN_BYTES31_MINUS_ONE.into() - 1 },
1039+
{ BYTES_IN_BYTES31_MINUS_ONE.into() - 1 },
1040+
>;
1041+
}
1042+
1043+
// For byte_at: split BoundedInt<-31, 29> at 0.
1044+
impl ConstrainRemainderIndexAt0 of bounded_int::ConstrainHelper<
1045+
Bytes31IndexMinus1SubBytes31Index::Result, 0,
1046+
> {
1047+
type LowT = BoundedInt<{ -BYTES_IN_BYTES31_MINUS_ONE.into() - 1 }, -1>;
1048+
type HighT = BoundedInt<0, { BYTES_IN_BYTES31_MINUS_ONE.into() - 1 }>;
1049+
}
1050+
9871051
/// Calculates the length of a `ByteSpan` in bytes.
9881052
pub fn calc_bytespan_len(span: ByteSpan) -> usize {
9891053
let data_bytes = bounded_int::mul(span.data.len(), BYTES_IN_BYTES31_UNIT_INT);
@@ -1073,5 +1137,39 @@ mod helpers {
10731137
pub fn length_minus_one(len: BoundedInt<1, 31>) -> Bytes31Index {
10741138
bounded_int::sub(len, 1)
10751139
}
1140+
/// Returns the byte at the given index in the ByteSpan.
1141+
/// If out of bounds: returns `None`.
1142+
pub fn byte_at(self: @ByteSpan, index: usize) -> Option<u8> {
1143+
let absolute_index = bounded_int::add(index, *self.first_char_start_offset);
1144+
let (word_index_bounded, msb_index) = bounded_int::div_rem(
1145+
absolute_index, NZ_BYTES_IN_BYTES31,
1146+
);
1147+
1148+
let word_index = upcast(word_index_bounded);
1149+
match self.data.get(word_index) {
1150+
Some(word) => {
1151+
// Convert from MSB to LSB indexing.
1152+
let lsb_index = bounded_int::sub(BYTES_IN_BYTES31_MINUS_ONE_TYPED, msb_index);
1153+
Some(word.at(upcast(lsb_index)))
1154+
},
1155+
None => {
1156+
// Word index must equal data.len() for remainder word.
1157+
if word_index != self.data.len() {
1158+
return None;
1159+
}
1160+
1161+
// Compute LSB index: remainder_len - 1 - msb_index.
1162+
let lsb_index_bounded = bounded_int::sub(
1163+
bounded_int::sub(*self.remainder_len, ONE_TYPED), msb_index,
1164+
);
1165+
1166+
// Check if in bounds and extract non-negative index.
1167+
let Err(lsb_index) = bounded_int::constrain::<_, 0>(lsb_index_bounded) else {
1168+
return None; // Out of bounds: index >= remainder_len.
1169+
};
1170+
Some(u8_at_u256((*self.remainder_word).into(), upcast(lsb_index)))
1171+
},
1172+
}
1173+
}
10761174
}
10771175
pub(crate) use helpers::len_parts;

corelib/src/ops/get.cairo

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
/// let ba: ByteArray = "hello";
2121
/// let span = ba.span();
2222
///
23+
/// // Using usize.
24+
/// let byte = span.get(1).unwrap();
25+
/// assert!(byte == 'e');
26+
///
2327
/// // Using Range<usize>.
2428
/// let slice = span.get(1..4).unwrap();
2529
/// assert_eq!(slice.to_byte_array(), "ell");
@@ -29,6 +33,7 @@
2933
/// assert_eq!(slice.to_byte_array(), "ell");
3034
///
3135
/// // Out of bounds returns None.
36+
/// assert!(span.get(10).is_none());
3237
/// assert!(span.get(10..20).is_none());
3338
/// ```
3439
// TODO(giladchase): add examples for `usize` once supported.

corelib/src/test/byte_array_test.cairo

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,3 +683,60 @@ fn test_span_multiple_start_offset_slicing() {
683683
assert_eq!(slice2_inc.map(tba), Some("cdef"));
684684
assert_eq!(slice3_inc.map(tba), Some("def"));
685685
}
686+
687+
#[test]
688+
fn test_span_at_and_index() {
689+
// Test simple access.
690+
let ba: ByteArray = "AB";
691+
let span = ba.span();
692+
assert_eq!(span[0], 'A');
693+
assert_eq!(span.get(1_usize), Some('B'));
694+
assert_eq!(span.get(2_usize), None);
695+
696+
// Test with offset and two words.
697+
let ba_33: ByteArray = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefg";
698+
let mut span = ba_33.span();
699+
span = span.get(1..33).unwrap();
700+
assert_eq!(span.get(0_usize), Some('B'));
701+
assert_eq!(span.get(30_usize), Some('f'));
702+
assert_eq!(span[31], 'g');
703+
assert_eq!(span.get(32_usize), None);
704+
705+
// Test with offset and two words.
706+
// 64 bytes: 31 + 31 + 2.
707+
let ba_64: ByteArray = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789#$";
708+
let mut span = ba_64.span();
709+
span = span.get(1..64).unwrap();
710+
assert_eq!(span.get(30_usize), Some('f'));
711+
assert_eq!(span[31], 'g');
712+
assert_eq!(span.get(60_usize), Some('9'));
713+
assert_eq!(span[61], '#');
714+
assert_eq!(span.get(62_usize), Some('$'));
715+
assert_eq!(span.get(63_usize), None);
716+
717+
// Test empty span.
718+
let empty: ByteArray = Default::default();
719+
let empty_span = empty.span();
720+
assert_eq!(empty_span.get(0_usize), None);
721+
}
722+
723+
#[test]
724+
#[should_panic(expected: ('Index out of bounds',))]
725+
fn test_span_index_out_of_bounds() {
726+
let ba: ByteArray = "AB";
727+
let span = ba.span();
728+
let _x = span[2]; // Should panic
729+
}
730+
731+
#[test]
732+
fn test_span_at_overflows() {
733+
// Test overflow protection with large indices.
734+
let ba: ByteArray = "test";
735+
let span = ba.span();
736+
737+
assert_eq!(span.get(Bounded::<usize>::MAX), None);
738+
739+
let sliced = ba.span().get(1..3).unwrap();
740+
assert_eq!(sliced.get(Bounded::<usize>::MAX - 1), None);
741+
assert_eq!(sliced.get(Bounded::<usize>::MAX), None);
742+
}

0 commit comments

Comments
 (0)