From 64820f0cd7cb4f2469e13dd2c573e757020b5ba4 Mon Sep 17 00:00:00 2001 From: prismaman Date: Wed, 17 Jun 2026 16:58:28 +0200 Subject: [PATCH] feature: ct_lookup (+macro, docs, testing) --- src/lib.rs | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ tests/mod.rs | 8 ++++++++ 2 files changed, 59 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index b6e42c4..651415c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -470,6 +470,44 @@ pub trait ConditionallySelectable: Copy { a.conditional_assign(&b, choice); b.conditional_assign(&t, choice); } + + /// Constant-time indexing via a linear scan. + /// + /// This function should execute in constant time. + /// Returns None when given an invalid index. + /// + /// # Example + /// + /// ``` + /// use subtle::{Choice, ConditionallySelectable, ConstantTimeEq}; + /// # + /// # fn main() { + /// let x: [u8; 3] = [13, 42, 67]; + /// + /// let y = u8::ct_lookup(&x, &1); + /// let z = u8::ct_lookup(&x, &4); + /// assert_eq!(y.unwrap_or(u8::default()), 42); + /// assert!(bool::from(z.is_none())); + /// # } + /// ``` + #[inline] + fn ct_lookup(array: &[Self], target: &usize) -> CtOption + where + Self: Default, + { + let mut target_entry = CtOption { + value: Self::default(), + is_some: Choice::from(0), + }; + for (index, entry) in array.iter().enumerate() { + let is_target_entry = index.ct_eq(target); + target_entry + .value + .conditional_assign(entry, is_target_entry); + target_entry.is_some |= is_target_entry; + } + target_entry + } } macro_rules! to_signed_int { @@ -533,6 +571,19 @@ macro_rules! generate_integer_conditional_select { *a ^= t; *b ^= t; } + #[inline] + fn ct_lookup(array: &[Self], target: &usize) -> CtOption { + let mut target_entry = CtOption { value: Self::default(), is_some: Choice::from(0),}; + for (index, entry) in array.iter().enumerate() { + // if choice = 0, mask = (-0) = 0000...0000 + // if choice = 1, mask = (-1) = 1111...1111 + let found = index.ct_eq(target); + let mask = -(found.unwrap_u8() as to_signed_int!($t)) as $t; + target_entry.value ^= (entry & mask); + target_entry.is_some ^= found; + } + target_entry + } } )*) } diff --git a/tests/mod.rs b/tests/mod.rs index 888b9d0..435756b 100644 --- a/tests/mod.rs +++ b/tests/mod.rs @@ -75,6 +75,14 @@ macro_rules! generate_integer_conditional_select_tests { w.conditional_assign(&y, 0.into()); assert_eq!(z, x); assert_eq!(w, x); + + let xarr: [$t; 3] = [x, y, x]; + let t = <$t>::ct_lookup(&xarr, &1); + let u = <$t>::ct_lookup(&xarr, &2); + let v = <$t>::ct_lookup(&xarr, &4); + assert_eq!(t.unwrap(), y); + assert_eq!(u.unwrap(), x); + assert!(bool::from(v.is_none())); )*) }