diff --git a/ctutils/src/ct_option.rs b/ctutils/src/ct_option.rs index 08b383cb..9b306e9a 100644 --- a/ctutils/src/ct_option.rs +++ b/ctutils/src/ct_option.rs @@ -1,6 +1,36 @@ use crate::{Choice, CtEq, CtSelect}; use core::ops::{Deref, DerefMut}; +/// Helper macro for providing behavior like the [`CtOption::map`] combinator that works in +/// `const fn` contexts. +/// +/// Requires a provided `$mapper` function to convert from one type to another, e.g. +/// +/// ```ignore +/// const fn mapper(value: T) -> U +/// ``` +#[macro_export] +macro_rules! map { + ($opt:expr, $mapper:path) => {{ $crate::CtOption::new($mapper($opt.to_inner_unchecked()), $opt.is_some()) }}; +} + +/// Helper macro for providing behavior like the [`CtOption::unwrap_or`] combinator that works in +/// `const fn` contexts. +/// +/// Requires a provided selector function `$select` to perform constant-time selection which takes +/// two `T` values by reference along with a [`Choice`], returning the first `T` for +/// [`Choice::FALSE`], and the second for [`Choice::TRUE`], e.g.: +/// +/// ```ignore +/// const fn ct_select(a: &T, b: &T, condition: Choice) -> T +/// ``` +#[macro_export] +macro_rules! unwrap_or { + ($opt:expr, $default:expr, $select:path) => { + $select(&$default, $opt.as_inner_unchecked(), $opt.is_some()) + }; +} + /// Equivalent of [`Option`] but predicated on a [`Choice`] with combinators that allow for /// constant-time operations which always perform the same sequence of instructions regardless of /// the value of `is_some`. @@ -23,6 +53,21 @@ impl CtOption { Self { value, is_some } } + /// Construct a new [`CtOption`] where `self.is_some()` is [`Choice::TRUE`]. + #[inline] + pub const fn some(value: T) -> CtOption { + Self::new(value, Choice::TRUE) + } + + /// Construct a new [`CtOption`] with the [`Default`] value, and where `self.is_some()` is + /// [`Choice::FALSE`]. + pub fn none() -> CtOption + where + T: Default, + { + Self::new(Default::default(), Choice::FALSE) + } + /// Convert from a `&mut CtOption` to `CtOption<&mut T>`. #[inline] pub const fn as_mut(&mut self) -> CtOption<&mut T> { @@ -80,7 +125,7 @@ impl CtOption { // (needs `const_precise_live_drops`) #[inline] #[track_caller] - pub const fn expect_copied(&self, msg: &str) -> T + pub const fn expect_copied(self, msg: &str) -> T where T: Copy, { @@ -99,7 +144,7 @@ impl CtOption { pub const fn expect_ref(&self, msg: &str) -> &T { // TODO(tarcieri): use `self.is_some().to_bool()` when MSRV is 1.86 assert!(self.is_some.to_bool_vartime(), "{}", msg); - &self.value + self.as_inner_unchecked() } /// Convert the [`CtOption`] wrapper into an [`Option`], depending on whether @@ -124,6 +169,29 @@ impl CtOption { } } + /// Convert the [`CtOption`] wrapper into an [`Option`] in a `const fn`-friendly manner. + /// + /// This is the equivalent of [`CtOption::into_option`] but is `const fn`-friendly by only + /// allowing `Copy` types which are implicitly `!Drop` and don't run into problems with + /// `const fn` and destructors. + /// + ///
+ /// This implementation doesn't intend to be constant-time nor try to protect the leakage of the + /// `T` value since the [`Option`] will do it anyway. + ///
+ #[inline] + pub const fn into_option_copied(self) -> Option + where + T: Copy, + { + // TODO(tarcieri): use `self.is_some().to_bool()` when MSRV is 1.86 + if self.is_some.to_bool_vartime() { + Some(self.value) + } else { + None + } + } + /// Returns [`Choice::TRUE`] if the option is the equivalent of a `Some`. #[inline] #[must_use] @@ -164,6 +232,24 @@ impl CtOption { ret } + /// Obtain a reference to the inner value without first checking that `self.is_some()` is + /// [`Choice::TRUE`]. + /// + /// This method is primarily intended for use in `const fn` scenarios where it's not yet + /// possible to use the safe combinator methods, and returns a reference to avoid issues with + /// `const fn` destructors. + /// + ///
+ /// Use with care! + /// + /// This method does not ensure the `value` is actually valid. Callers of this method should + /// take great care to ensure that `self.is_some()` is checked elsewhere. + ///
+ #[inline] + pub const fn as_inner_unchecked(&self) -> &T { + &self.value + } + /// Calls the provided callback with the wrapped inner value, which computes a [`Choice`], /// and updates `self.is_some()`. /// @@ -178,6 +264,13 @@ impl CtOption { self } + /// Apply an additional [`Choice`] requirement to `is_some`. + #[inline] + pub const fn filter_by(mut self, is_some: Choice) -> Self { + self.is_some = self.is_some.and(is_some); + self + } + /// Maps a `CtOption` to a `CtOption` by unconditionally applying a function to the /// contained `value`, but returning a new option value which inherits `self.is_some()`. #[inline] @@ -247,6 +340,27 @@ impl CtOption { } } + /// Obtain a copy of the inner value without first checking that `self.is_some()` is + /// [`Choice::TRUE`]. + /// + /// This method is primarily intended for use in `const fn` scenarios where it's not yet + /// possible to use the safe combinator methods, and uses a `Copy` bound to avoid issues with + /// `const fn` destructors. + /// + ///
+ /// Use with care! + /// + /// This method does not ensure the `value` is actually valid. Callers of this method should + /// take great care to ensure that `self.is_some()` is checked elsewhere. + ///
+ #[inline] + pub const fn to_inner_unchecked(self) -> T + where + T: Copy, + { + self.value + } + /// Return the contained value, consuming the `self` value. /// /// Use of this function is discouraged due to panic potential. Instead, prefer non-panicking @@ -400,6 +514,12 @@ impl CtSelect for CtOption { } } +impl Default for CtOption { + fn default() -> Self { + Self::none() + } +} + /// Convert the [`CtOption`] wrapper into an [`Option`], depending on whether /// [`CtOption::is_some`] is a truthy or falsy [`Choice`]. /// @@ -460,6 +580,26 @@ mod tests { #[derive(Debug, Eq, PartialEq)] struct Error; + #[test] + fn map_macro() { + assert!(map!(NONE, u16::from).is_none().to_bool()); + assert_eq!(map!(SOME, u16::from).unwrap(), VALUE as u16); + } + + #[test] + fn unwrap_or_macro() { + // Don't actually use this! It's just a test function implemented in variable-time + const fn select_vartime(a: &u8, b: &u8, choice: Choice) -> u8 { + if choice.to_bool_vartime() { *b } else { *a } + } + + assert_eq!( + unwrap_or!(NONE, OTHER.unwrap(), select_vartime), + OTHER.unwrap() + ); + assert_eq!(unwrap_or!(SOME, OTHER.unwrap(), select_vartime), VALUE); + } + #[test] fn ct_eq() { assert!(NONE.ct_eq(&NONE).to_bool()); @@ -477,6 +617,11 @@ mod tests { assert!(SOME.ct_select(&NONE, Choice::TRUE).is_none().to_bool()); } + #[test] + fn default() { + assert!(NONE.ct_eq(&CtOption::default()).to_bool()); + } + #[test] fn expect_some() { assert_eq!(SOME.expect("should succeed"), VALUE); @@ -494,6 +639,12 @@ mod tests { assert_eq!(NONE.into_option(), None) } + #[test] + fn into_option_copied() { + assert_eq!(SOME.into_option_copied(), Some(VALUE)); + assert_eq!(NONE.into_option_copied(), None) + } + #[test] fn is_some() { assert!(SOME.is_some().to_bool()); @@ -537,6 +688,14 @@ mod tests { assert_eq!(ret.unwrap(), VALUE); } + #[test] + fn filter_by() { + assert!(NONE.filter_by(Choice::FALSE).is_none().to_bool()); + assert!(NONE.filter_by(Choice::TRUE).is_none().to_bool()); + assert!(SOME.filter_by(Choice::FALSE).ct_eq(&NONE).to_bool()); + assert_eq!(SOME.filter_by(Choice::TRUE).unwrap(), VALUE); + } + #[test] fn map() { assert!(NONE.map(|value| value + 1).ct_eq(&NONE).to_bool()); @@ -576,6 +735,11 @@ mod tests { assert!(SOME.or(OTHER).ct_eq(&SOME).to_bool()); } + #[test] + fn some() { + assert!(CtOption::some(VALUE).ct_eq(&SOME).to_bool()); + } + #[test] fn unwrap_some() { assert_eq!(SOME.unwrap(), VALUE);