Skip to content

Commit 681ff63

Browse files
committed
fix(stack-encrypt): a marker's operator methods take the value by value
A marker query took every value by reference, `AGE.between(&18, &65)`, so that a string literal could be copied in through `ToOwned`. A number by reference reads oddly for a `Copy` type, as the review of #1595 said. The methods now take the value by value through `QueryInput`: an owned scalar is moved into the query, and a borrowed text or byte string is copied into an owned one. The scalars are listed, since a blanket over `Zeroize` would overlap the borrowed impls under coherence. The query wipes the owned value as before. The trybuild snapshot follows the bound's new name. Refs #1486, #1590.
1 parent caea3db commit 681ff63

7 files changed

Lines changed: 140 additions & 23 deletions

File tree

‎docs/plans/2026-10-24-stack-encrypt-api-plan.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -350,14 +350,14 @@ EQL query type the marker fixes. Decided on 7 November 2026.
350350
// On `next` since pull request #1595, with the index's query term as
351351
// the output type (`EqualityTerm`, `RangeQuery<OreTerm<u32>>`,
352352
// `MatchQuery`); the EQL query types follow with the EQL owned
353-
// implementations. The bounds are passed by reference.
353+
// implementations. A value is passed by value; a string literal as it is.
354354
let by_email: TextEqQuery = EncryptedMember::EMAIL
355355
.eq("alice@example.com")
356356
.scope(tenant)
357357
.encrypt(&keyset)
358358
.await?;
359359
let adults: IntegerOrdQuery = EncryptedMember::AGE
360-
.between(&18, &65)
360+
.between(18, 65)
361361
.scope(tenant)
362362
.encrypt(&keyset)
363363
.await?;

‎packages/stack-encrypt/CHANGELOG.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2323
`bind` method, and ends with `.encrypt(&keyset)`. The output is the
2424
index's query term. A query from a marker derives under the declaration
2525
the derive wrote, so it is the stored term. No plan is written by hand
26-
beside the record. The query's copy of the value is wiped when it is
26+
beside the record. A method takes its value by value, and a string
27+
literal as it is (`QueryInput`). The query's value is wiped when it is
2728
derived, refused, or dropped. `assert_context!` takes a marker, and
2829
`Marker::contexts()` is the record's contexts narrowed to the field.
2930
Not yet (#1596): a field of several indexes, a nullable field, and a

‎packages/stack-encrypt/src/lib.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,7 @@ pub use contexts::{
586586
};
587587
pub use descriptor::{Describe, Description, Descriptor, Label, LabelError};
588588
pub use keyset::KeysetCipher;
589-
pub use marker::{BetweenQuery, MarkedQuery, Marker, Needs, Ready, ScopeInput};
589+
pub use marker::{BetweenQuery, MarkedQuery, Marker, Needs, QueryInput, Ready, ScopeInput};
590590
pub use plan::{all, Plan, PlanError};
591591
// stack-kms is a public dependency: `StackCipher` is generic over its
592592
// `DataKeySource`, and `StackCipherBuilder::kms` takes its `StackKms`. It is

‎packages/stack-encrypt/src/marker.rs‎

Lines changed: 130 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
//! .await?;
5757
//! assert_eq!(by_email, stored.email.terms);
5858
//! let adults = EncryptedUser::AGE
59-
//! .between(&18u32, &36)
59+
//! .between(18u32, 36)
6060
//! .scope(Scope::new(42u64))
6161
//! .encrypt(&keyset)
6262
//! .await?;
@@ -75,7 +75,7 @@
7575
//! # What a marker does not check
7676
//!
7777
//! An equality marker takes a value of any type that the equality index
78-
//! accepts: `EMAIL.eq(&42u64)` compiles on a text field and matches
78+
//! accepts: `EMAIL.eq(42u64)` compiles on a text field and matches
7979
//! nothing, because the derive sees the record's field types and not the
8080
//! plaintext struct's. An order marker fixes the type, since `OreTerm<S>`
8181
//! names it. A field whose type is several indexes, such as
@@ -203,22 +203,95 @@ fn wipe_pair<S: Zeroize>((mut lo, mut hi): (S, S)) {
203203
hi.zeroize();
204204
}
205205

206+
/// What a marker's operator methods take: an owned scalar, moved into the
207+
/// query, or a borrowed text or byte string, copied into it. A number is
208+
/// given by value, `AGE.between(18, 65)`, and a string literal as it is,
209+
/// `EMAIL.eq("ada@example.com")`. The query holds the owned value and
210+
/// wipes it when it is derived, refused or dropped. The scalars are the
211+
/// ones the indexes accept: the booleans, the integers, the floats, text,
212+
/// bytes, the [`value`](crate::value) types, and an `Option` of each.
213+
pub trait QueryInput {
214+
/// The owned value the query holds.
215+
type Value: Zeroize + 'static;
216+
/// The value, owned.
217+
fn into_value(self) -> Self::Value;
218+
}
219+
macro_rules! owned_query_inputs {
220+
($($ty:ty),+ $(,)?) => {$(
221+
impl QueryInput for $ty {
222+
type Value = $ty;
223+
fn into_value(self) -> $ty {
224+
self
225+
}
226+
}
227+
)+};
228+
}
229+
owned_query_inputs!(
230+
bool,
231+
u8,
232+
u16,
233+
u32,
234+
u64,
235+
u128,
236+
i8,
237+
i16,
238+
i32,
239+
i64,
240+
i128,
241+
f32,
242+
f64,
243+
String,
244+
Vec<u8>,
245+
crate::value::Date,
246+
crate::value::Timestamp,
247+
crate::value::Decimal,
248+
);
249+
impl<V: QueryInput> QueryInput for Option<V> {
250+
type Value = Option<V::Value>;
251+
fn into_value(self) -> Self::Value {
252+
self.map(V::into_value)
253+
}
254+
}
255+
impl QueryInput for &str {
256+
type Value = String;
257+
fn into_value(self) -> String {
258+
self.to_owned()
259+
}
260+
}
261+
impl QueryInput for &String {
262+
type Value = String;
263+
fn into_value(self) -> String {
264+
self.clone()
265+
}
266+
}
267+
impl QueryInput for &[u8] {
268+
type Value = Vec<u8>;
269+
fn into_value(self) -> Vec<u8> {
270+
self.to_vec()
271+
}
272+
}
273+
impl QueryInput for &Vec<u8> {
274+
type Value = Vec<u8>;
275+
fn into_value(self) -> Vec<u8> {
276+
self.clone()
277+
}
278+
}
279+
206280
/// One method per operator. Each exists only where the field's target type
207281
/// answers the operator ([`Answers`]), so a missing one is a compile error
208-
/// that names the field's indexes. The value is copied into the query
209-
/// (`ToOwned`), and the copy is wiped when the query is derived, refused or
282+
/// that names the field's indexes. The value is moved or copied into the
283+
/// query ([`QueryInput`]), and wiped when the query is derived, refused or
210284
/// dropped.
211285
macro_rules! operator_methods {
212286
($($(#[$doc:meta])* $method:ident => $op:ident;)+) => {$(
213287
impl<T, C: ScopeInput> Marker<T, C> {
214288
$(#[$doc])*
215-
pub fn $method<Q>(self, value: &Q) -> MarkedQuery<T, $op, Q::Owned, C::Start>
289+
pub fn $method<Q>(self, value: Q) -> MarkedQuery<T, $op, Q::Value, C::Start>
216290
where
217-
Q: ?Sized + ToOwned,
218-
Q::Owned: Zeroize + 'static,
219-
T: Answers<$op, Q::Owned> + LabelledTarget,
291+
Q: QueryInput,
292+
T: Answers<$op, Q::Value> + LabelledTarget,
220293
{
221-
self.query(value.to_owned(), wipe_one)
294+
self.query(value.into_value(), wipe_one)
222295
}
223296
}
224297
)+};
@@ -244,13 +317,12 @@ impl<T, C: ScopeInput> Marker<T, C> {
244317
/// Rows whose value is between `lo` and `hi`, inclusive
245318
/// ([`op::Between`](Between)): one term carrying both bounds, derived
246319
/// under one context.
247-
pub fn between<Q>(self, lo: &Q, hi: &Q) -> BetweenQuery<T, Q::Owned, C::Start>
320+
pub fn between<Q>(self, lo: Q, hi: Q) -> BetweenQuery<T, Q::Value, C::Start>
248321
where
249-
Q: ?Sized + ToOwned,
250-
Q::Owned: Zeroize + 'static,
251-
T: Answers<Between, (Q::Owned, Q::Owned)> + LabelledTarget,
322+
Q: QueryInput,
323+
T: Answers<Between, (Q::Value, Q::Value)> + LabelledTarget,
252324
{
253-
self.query((lo.to_owned(), hi.to_owned()), wipe_pair)
325+
self.query((lo.into_value(), hi.into_value()), wipe_pair)
254326
}
255327
}
256328

@@ -444,4 +516,48 @@ mod tests {
444516
assert_eq!(wipes.load(Ordering::SeqCst), 0, "nothing left to wipe");
445517
drop(taken);
446518
}
519+
520+
/// An owned scalar is the query's as it is, for each scalar the
521+
/// indexes accept; a borrowed text or byte string is copied into an
522+
/// owned one.
523+
#[test]
524+
fn a_query_input_is_owned_or_copied_in() {
525+
use crate::value::{Date, Decimal, Timestamp};
526+
macro_rules! owned_as_it_is {
527+
($($value:expr),+ $(,)?) => {$(
528+
assert_eq!($value.into_value(), $value);
529+
assert_eq!(Some($value).into_value(), Some($value));
530+
)+};
531+
}
532+
owned_as_it_is!(
533+
true,
534+
7u8,
535+
7u16,
536+
7u32,
537+
7u64,
538+
7u128,
539+
-7i8,
540+
-7i16,
541+
-7i32,
542+
-7i64,
543+
-7i128,
544+
0.5f32,
545+
0.5f64,
546+
String::from("ada"),
547+
b"ada".to_vec(),
548+
);
549+
let date = Date::from_days_since_epoch(7).unwrap();
550+
assert_eq!(date.into_value().days_since_epoch(), 7);
551+
let instant = Timestamp::new(7, 9).unwrap();
552+
let instant = instant.into_value();
553+
assert_eq!((instant.seconds(), instant.nanoseconds()), (7, 9));
554+
let decimal = Decimal::new(75, 1).unwrap();
555+
let decimal = decimal.into_value();
556+
assert_eq!((decimal.mantissa(), decimal.scale()), (75, 1));
557+
558+
assert_eq!("ada".into_value(), String::from("ada"));
559+
assert_eq!((&String::from("ada")).into_value(), String::from("ada"));
560+
assert_eq!(b"ada"[..].into_value(), b"ada".to_vec());
561+
assert_eq!((&b"ada".to_vec()).into_value(), b"ada".to_vec());
562+
}
447563
}

‎packages/stack-encrypt/tests/markers.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ mod a_marker_query {
243243

244244
// A bound equal to the stored value derives the stored term.
245245
let at_36 = EncryptedUser::AGE
246-
.gte(&36u32)
246+
.gte(36u32)
247247
.scope(Scope::new(42u64))
248248
.encrypt(&keyset)
249249
.await
@@ -253,7 +253,7 @@ mod a_marker_query {
253253
// A range carries one term per bound, each as the stored term of
254254
// that value would be.
255255
let adults = EncryptedUser::AGE
256-
.between(&18u32, &36u32)
256+
.between(18u32, 36u32)
257257
.scope(Scope::new(42u64))
258258
.encrypt(&keyset)
259259
.await

‎packages/stack-encrypt/tests/ui/marker_operator_not_answered.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,6 @@ struct EncryptedUser {
2222
fn main() {
2323
let _ = EncryptedUser::EMAIL.eq("ada@example.com");
2424
let _ = EncryptedUser::EMAIL.gt("ada@example.com");
25-
let _ = EncryptedUser::AGE.between(&18u32, &65);
25+
let _ = EncryptedUser::AGE.between(18u32, 65);
2626
let _ = EncryptedUser::AGE.matches("36");
2727
}

‎packages/stack-encrypt/tests/ui/marker_operator_not_answered.stderr‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ help: the trait `Answers<stack_encrypt::op::Gt, _>` is not implemented for `Equa
2020
note: required by a bound in `Marker::<T, C>::gt`
2121
--> src/marker.rs
2222
|
23-
| T: Answers<$op, Q::Owned> + LabelledTarget,
23+
| T: Answers<$op, Q::Value> + LabelledTarget,
2424
| ^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `Marker::<T, C>::gt`
2525
...
2626
| / operator_methods! {
@@ -56,7 +56,7 @@ error[E0277]: `OreTerm<u32>` does not answer `stack_encrypt::op::Matches` given
5656
note: required by a bound in `Marker::<T, C>::matches`
5757
--> src/marker.rs
5858
|
59-
| T: Answers<$op, Q::Owned> + LabelledTarget,
59+
| T: Answers<$op, Q::Value> + LabelledTarget,
6060
| ^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `Marker::<T, C>::matches`
6161
...
6262
| / operator_methods! {

0 commit comments

Comments
 (0)