vitaminc_ore/scheme.rs
1//! The sealed order plaintext, the trait a scheme implements to encrypt
2//! it, and the space a caller encrypts through.
3//!
4//! A caller opens a space on a scheme, with the target every value in it
5//! is encoded as and the context its terms compare within, and encrypts
6//! values: `scheme.target(I32).context(("orders", "total")).encrypt(42i16)`.
7//! It never sees the encoding. A scheme opens a space once, under the
8//! salt the space derives, and encrypts the sealed [`OrderPlaintext`]s of
9//! that space with what it derived; it never sees bytes that skipped the
10//! encoding (the only ways to make a plaintext are
11//! [`order`](crate::order) and [`order_with_nulls`](crate::order_with_nulls))
12//! or a salt it could derive differently from another scheme.
13
14use core::cmp::Ordering;
15use core::fmt;
16use std::sync::OnceLock;
17
18use vitaminc_aead::plaintext::{self, SourceError};
19use vitaminc_aead::{Encrypt, FixedType, Logical, MapSink, Physical, SeqSink, Sink};
20use vitaminc_context::{Context, IntoContext};
21use vitaminc_normalize::NormalizeError;
22use vitaminc_protected::{Controlled, OpaqueDebug, Protected};
23
24use crate::order::{encode, label, null_plaintext};
25use crate::target::{
26 FitsIn, FixedWidth, NullPolicy, Nulls, OwnTarget, RuntimeNulls, StaticTarget, Supports, Target,
27};
28use crate::{NullOrder, OrderTarget};
29
30/// The label every salt this crate derives leads with.
31const SALT_LABEL: &[u8] = b"vitaminc/ore/v1";
32
33/// One scalar's order term before encryption: bytes whose order is the
34/// value's order, encoded as the target they were made for.
35///
36/// Only [`order`](crate::order) and [`order_with_nulls`](crate::order_with_nulls)
37/// build one, so a scheme can rely on the bytes being the
38/// encoding of their target, and on there being at least one: a
39/// fixed-width target encodes to its width, and text and bytes start with
40/// [`VARIABLE_WIDTH_MARKER`](crate::VARIABLE_WIDTH_MARKER). A caller
41/// never needs one: [`OrderSpace::encrypt`] takes the value itself.
42/// `Debug` shows the domain and width and masks the bytes.
43#[derive(OpaqueDebug)]
44pub struct OrderPlaintext {
45 #[non_sensitive]
46 target: OrderTarget,
47 #[non_sensitive]
48 nulls: Option<NullOrder>,
49 #[non_sensitive]
50 domain: &'static str,
51 bytes: Protected<Vec<u8>>,
52}
53
54impl OrderPlaintext {
55 pub(crate) fn new(
56 target: OrderTarget,
57 nulls: Option<NullOrder>,
58 domain: &'static str,
59 bytes: Protected<Vec<u8>>,
60 ) -> Self {
61 debug_assert_eq!(
62 label(target, nulls),
63 Some(domain),
64 "the domain is the space's label"
65 );
66 debug_assert!(
67 target.width().is_none_or(|w| w == bytes.risky_ref().len()),
68 "{target:?} encodes to its width"
69 );
70 debug_assert!(
71 !bytes.risky_ref().is_empty(),
72 "an order plaintext is never empty"
73 );
74 Self {
75 target,
76 nulls,
77 domain,
78 bytes,
79 }
80 }
81
82 /// The target the bytes were encoded as.
83 pub fn target(&self) -> OrderTarget {
84 self.target
85 }
86
87 /// Where the space the plaintext was encoded for places nulls, or
88 /// `None` for a space that gives them no term.
89 pub fn null_order(&self) -> Option<NullOrder> {
90 self.nulls
91 }
92
93 /// The frozen domain label, one of the [`domain`](crate::domain)
94 /// constants: the target's, or the label of its null policy.
95 pub fn domain(&self) -> &'static str {
96 self.domain
97 }
98
99 /// The width every term of this target has, or `None` for text and
100 /// bytes, whose terms vary in length. A fixed-width scheme says which
101 /// widths it takes in [`Supports::supports`], so it refuses a target
102 /// rather than a value, and never sees a plaintext of another.
103 pub fn width(&self) -> Option<usize> {
104 self.target.width()
105 }
106
107 /// The term's bytes, protected, never empty. A scheme reads them with
108 /// [`risky_ref`](Controlled::risky_ref) and nothing else should.
109 pub fn bytes(&self) -> &Protected<Vec<u8>> {
110 &self.bytes
111 }
112}
113
114/// Why a space produced no term. Never includes plaintext.
115#[derive(Debug, thiserror::Error)]
116#[non_exhaustive]
117pub enum OrderError<E> {
118 /// The scalar has no order term as the space's target: no order for
119 /// its kind, another kind than the target, outside the target, not
120 /// valid for its logical type, or text with a code point unassigned
121 /// in Unicode 16.
122 #[error("no order term: {0}")]
123 Term(#[from] NormalizeError),
124 /// The value is a sequence, a map or a passthrough, which has no
125 /// single term. Walk it with [`order_terms`](crate::order_terms).
126 #[error("a container has no single order term; walk it for one per scalar")]
127 NotScalar,
128 /// The value could not be read: its source failed before any term
129 /// was made.
130 #[error("the value could not be read: {0}")]
131 Source(SourceError),
132 /// The scheme does not take terms of this target, chosen while the
133 /// program ran: a fixed-width scheme given text, or a width above its
134 /// limit. A target named when the program is compiled is checked
135 /// then, through [`Supports`], and never reaches this.
136 #[error("{scheme} has no order term for {domain}")]
137 Unsupported {
138 /// The scheme's [`NAME`](OrderScheme::NAME).
139 scheme: &'static str,
140 /// The target's domain label.
141 domain: &'static str,
142 },
143 /// The scheme's own failure.
144 #[error("the order scheme failed: {0}")]
145 Backend(E),
146}
147
148/// An order scheme: encrypts one sealed plaintext so that the terms still
149/// compare.
150///
151/// A scheme implements [`open`](OrderScheme::open), which derives what it
152/// keeps for one space from the salt, [`encrypt_plaintext`](OrderScheme::encrypt_plaintext)
153/// over that, and [`compare`](OrderScheme::compare); and it says which
154/// targets it takes through [`Supports`]. A caller does not call any of
155/// them: it opens a space with [`OrderEncrypt::target`], which every
156/// scheme has, and encrypts values there. The space derives the salt the
157/// scheme binds, so no scheme can leave a part of it out, opens the
158/// scheme under it once, and refuses a target the scheme does not take
159/// before the scheme sees it.
160///
161/// Terms compare only between terms of the same scheme, key, target and
162/// context: a space. The scheme keeps its own comparison (CLWW ORE and
163/// block ORE do not compare byte by byte) in [`compare`](Self::compare),
164/// and its own stored bytes through `AsRef<[u8]>`. A term is not `Ord`:
165/// that would promise an order between any two terms, and no term carries
166/// which space it came from, so two terms of different spaces would
167/// compare as noise and break every sort and tree that trusted the
168/// promise.
169///
170/// Implemented by the scheme crates behind a `vitaminc` feature, and by a
171/// mock in this crate's tests. An implementation reads the plaintext's
172/// [`bytes`](OrderPlaintext::bytes) and binds the salt through what
173/// [`open`](Self::open) derived from it; it never pads or truncates. The bytes are never empty (a variable-width encoding starts
174/// with [`VARIABLE_WIDTH_MARKER`](crate::VARIABLE_WIDTH_MARKER)), so a
175/// scheme can rely on running its keyed primitive at least once, and
176/// every term it makes must depend on the key and the salt: a term that
177/// is the same under every key could be written into any index by
178/// anyone. `test_support` (behind the `test-support` feature) checks a
179/// real scheme for this.
180pub trait OrderScheme {
181 /// The scheme's name, bound into every term's salt and named in its
182 /// refusals: `"cllw-ore"`, `"cllw-ope"`, `"block-ore"`.
183 const NAME: &'static str;
184
185 /// What one term encrypts to.
186 type Term: AsRef<[u8]>;
187
188 /// The scheme's own failure. [`Infallible`](core::convert::Infallible)
189 /// for a scheme that cannot fail once keyed.
190 type Error: core::error::Error + Send + Sync + 'static;
191
192 /// What the scheme keeps for one space: whatever it derives from the
193 /// salt, so that it is derived for the space rather than for every
194 /// term. Block ORE keeps the keys and cipher it derives under the
195 /// salt; a scheme that passes the salt straight to its primitive, as
196 /// CLWW does, keeps the salt's bytes (`Protected<Vec<u8>>`).
197 ///
198 /// It must wipe itself when it is dropped, because what a scheme
199 /// derives from its key is as secret as the key. A space is dropped
200 /// with its [`OrderSpace`], and also as the loser of a race to open
201 /// (see [`open`](Self::open)), where nothing else would wipe it. Keep
202 /// derived bytes in [`Protected`], hold a cipher whose crate wipes it
203 /// on drop, or use `()` for a scheme that keeps nothing. A `Vec<u8>`
204 /// of derived keys does not compile.
205 ///
206 /// An [`OrderSpace`] holds a reference to the scheme and one of these,
207 /// so it is `Send` when the scheme is `Sync` and this is `Send`, and
208 /// `Sync` when this is `Sync` too. A scheme that keeps a random number
209 /// generator behind a `RefCell` here has a space per thread.
210 type Space: zeroize::ZeroizeOnDrop;
211
212 /// Open a space: derive what [`Space`](Self::Space) keeps, binding
213 /// `salt`: `PAE("vitaminc/ore/v1", NAME, domain, context)`, which the
214 /// space derives. Called when a space's first value is encrypted, or
215 /// from [`OrderSpace::open`], and not for every term.
216 ///
217 /// `target` is the space's, the one every plaintext the space hands
218 /// to [`encrypt_plaintext`](Self::encrypt_plaintext) is encoded as.
219 /// The salt binds its domain but does not give it back, and a scheme
220 /// that keys one of several engines by width, or pads text and not
221 /// bytes, decides that here, once, rather than for every term. A
222 /// scheme with one engine for every target ignores it.
223 ///
224 /// It may run more than once for one space: again after it fails, and
225 /// by more than one thread at once if they race to the first value,
226 /// in which case one result is kept and the rest dropped. So it must
227 /// be a pure derivation from `salt` and the scheme's key, with no
228 /// side effect that assumes it runs once.
229 ///
230 /// # Errors
231 ///
232 /// The scheme's own failure: seeding its random number generator,
233 /// say. The space reports it from every encryption, and tries again
234 /// on the next.
235 fn open(&self, target: OrderTarget, salt: &Context<'_>) -> Result<Self::Space, Self::Error>;
236
237 /// Encrypt a sealed plaintext of `space`, which [`open`](Self::open)
238 /// made under the space's salt.
239 ///
240 /// The plaintext is of a target the scheme [`Supports`]: the space
241 /// checks before calling. So the only failure is the scheme's own.
242 ///
243 /// # Errors
244 ///
245 /// The scheme's own failure: its random number generator, say.
246 fn encrypt_plaintext(
247 &self,
248 space: &Self::Space,
249 plaintext: &OrderPlaintext,
250 ) -> Result<Self::Term, Self::Error>;
251
252 /// The order of two terms of one space, as the order of the values
253 /// they encrypt. Needs no key: that is what makes the scheme an order
254 /// scheme.
255 ///
256 /// Over the terms of one space this is a total order, so a caller may
257 /// sort with it. `None` says the two cannot be compared: one is not a
258 /// term of this scheme, or the two were not made to compare (block
259 /// ORE under different padding). A scheme returns `None` wherever it
260 /// can tell, and never falls back to comparing bytes it cannot order.
261 /// It cannot tell everything: two well-formed terms of different
262 /// spaces may compare as noise, so a caller keeps a space's terms
263 /// together, as [`OrderSpace::compare`] says.
264 ///
265 /// The noise is in the direction, never in equality. `Some(Equal)`
266 /// means the two terms encrypt one value in one space, up to the
267 /// scheme's own collision bound: a term that lacks every block, or
268 /// that ignores its key or salt, compares equal to one of another
269 /// space, and the `test_support` checks fail it for that. Block ORE
270 /// keeps this because equality needs every block's keyed tag to
271 /// agree, and an OPE because equality is equal bytes. A scheme that
272 /// cannot keep it answers `None` across spaces instead.
273 fn compare(a: &Self::Term, b: &Self::Term) -> Option<Ordering>;
274}
275
276/// What a caller does with any [`OrderScheme`]: name a target, a context,
277/// or just a value, and get terms. Implemented for every scheme and for
278/// nothing else, so a scheme cannot change how a space binds its terms.
279///
280/// Every step of the chain is usable for terms. A target alone is a space
281/// under the empty context; a context alone takes values whose Rust type
282/// says the target; a value alone uses both defaults. Queries come only
283/// from a space, through [`OrderSpace::encrypt_query`], so a chain that
284/// makes one names its target.
285///
286/// ```
287/// use std::cmp::Ordering;
288/// use vitaminc_ore::target::I32;
289/// use vitaminc_ore::{OrderEncrypt, OrderTarget};
290/// # use vitaminc_ore::{Context, OrderPlaintext, OrderScheme};
291/// # /// Keeps the bytes as they are, for the example. Any scheme will do.
292/// # struct Clear;
293/// # impl OrderScheme for Clear {
294/// # const NAME: &'static str = "clear";
295/// # type Term = Vec<u8>;
296/// # type Error = core::convert::Infallible;
297/// # type Space = ();
298/// # fn open(&self, _: vitaminc_ore::OrderTarget, _: &Context<'_>) -> Result<(), Self::Error> {
299/// # Ok(())
300/// # }
301/// # fn encrypt_plaintext(&self, _: &(), p: &OrderPlaintext) -> Result<Vec<u8>, Self::Error> {
302/// # use vitaminc_protected::Controlled;
303/// # Ok(p.bytes().risky_ref().clone())
304/// # }
305/// # fn compare(a: &Vec<u8>, b: &Vec<u8>) -> Option<Ordering> {
306/// # Some(a.cmp(b))
307/// # }
308/// # }
309/// # vitaminc_ore::supports_every_target!(Clear);
310/// # let scheme = Clear;
311///
312/// // Held, for many values.
313/// let quantity = scheme.target(I32).context(("orders", "quantity"));
314/// let a = quantity.encrypt(42i16)?; // an i16 always fits an i32
315/// let b = quantity.encrypt_checked(43i64)?; // an i64 is checked
316/// assert_eq!(quantity.compare(&a, &b), Some(Ordering::Less));
317///
318/// // One-shot chains.
319/// scheme.target(I32).encrypt(42i16)?; // context ()
320/// scheme.context("tenant-7").encrypt(42i64)?; // the type's own target, I64
321/// scheme.encrypt(42i64)?; // own target, no context
322///
323/// // A target chosen while the program runs, as an index layer needs.
324/// let index = scheme.target(OrderTarget::I32).context("ctx");
325/// index.encrypt_checked(42i64)?;
326/// # Ok::<(), Box<dyn std::error::Error>>(())
327/// ```
328pub trait OrderEncrypt: OrderScheme {
329 /// A space of `target` values under the empty context: a marker from
330 /// [`target`](crate::target), or an [`OrderTarget`] chosen while the
331 /// program runs. Chain [`context`](OrderSpace::context) to bind one.
332 ///
333 /// Compiles only for a target this scheme [`Supports`].
334 fn target<T: Target>(&self, target: T) -> OrderSpace<'_, Self, T>
335 where
336 Self: Supports<T> + Supports<OrderTarget>,
337 {
338 OrderSpace::new(self, target, &().into_context().encode())
339 }
340
341 /// The terms that compare within `context` (a table and column, a
342 /// tenant, whatever they must compare within): chain
343 /// [`target`](OrderContext::target), or encrypt a value whose type is
344 /// its target.
345 fn context<'a, C: IntoContext<'a>>(&self, context: C) -> OrderContext<'_, Self> {
346 OrderContext {
347 scheme: self,
348 context: context.into_context().encode().into_owned(),
349 }
350 }
351
352 /// Encrypt a value as the target its Rust type is, under the empty
353 /// context: `scheme.target(V::Target).encrypt(value)`. A one-shot
354 /// chain makes a space per call, so the scheme opens per value; hold
355 /// `scheme.target(..).context(..)` to derive once for many values.
356 ///
357 /// # Errors
358 ///
359 /// The scheme's own failure, and nothing else: the value always fits.
360 fn encrypt<V>(&self, value: V) -> Result<OrderTerm<Self::Term, V::Target>, Self::Error>
361 where
362 V: OwnTarget + FitsIn<V::Target>,
363 Self: Supports<V::Target> + Supports<OrderTarget>,
364 {
365 self.target(V::Target::default()).encrypt(value)
366 }
367
368 /// Encrypt a value as the target its Rust type is, checking it while
369 /// the program runs, under the empty context:
370 /// `scheme.target(V::Target).encrypt_checked(value)`.
371 ///
372 /// # Errors
373 ///
374 /// As [`OrderSpace::encrypt_checked`].
375 #[allow(clippy::type_complexity)]
376 fn encrypt_checked<V: OwnTarget>(
377 &self,
378 value: V,
379 ) -> Result<OrderTerm<Self::Term, V::Target>, OrderError<Self::Error>>
380 where
381 Self: Supports<V::Target> + Supports<OrderTarget>,
382 {
383 self.target(V::Target::default()).encrypt_checked(value)
384 }
385
386 /// `scheme.target(target).context(context)`, as the 0.6 API spelt it.
387 #[deprecated(since = "0.7.0", note = "use `scheme.target(target).context(context)`")]
388 fn column<'a, C: IntoContext<'a>>(
389 &self,
390 target: OrderTarget,
391 context: C,
392 ) -> OrderSpace<'_, Self, OrderTarget>
393 where
394 Self: Supports<OrderTarget>,
395 {
396 self.target(target).context(context)
397 }
398}
399
400impl<S: OrderScheme + ?Sized> OrderEncrypt for S {}
401
402/// `PAE("vitaminc/ore/v1", scheme, domain, context)`. Leading with this
403/// crate's own label keeps it apart from any context another crate
404/// derives from the same parts. The domain is the space's label, which
405/// names its null policy, so a space that places nulls never shares a
406/// salt with the plain space of its target.
407fn salt(scheme: &str, domain: &str, context: &Context<'_>) -> Context<'static> {
408 Context::pae(&[
409 SALT_LABEL,
410 scheme.as_bytes(),
411 domain.as_bytes(),
412 context.as_bytes(),
413 ])
414}
415
416/// A scheme and a context, with the target still to come: from
417/// [`OrderEncrypt::context`]. `Debug` masks the scheme, whose key it
418/// holds.
419#[derive(OpaqueDebug)]
420pub struct OrderContext<'s, S: OrderScheme + ?Sized> {
421 scheme: &'s S,
422 #[non_sensitive]
423 context: Context<'static>,
424}
425
426impl<'s, S: OrderScheme + ?Sized> OrderContext<'s, S> {
427 /// The space of `target` values under this context.
428 ///
429 /// Compiles only for a target the scheme [`Supports`].
430 pub fn target<T: Target>(&self, target: T) -> OrderSpace<'s, S, T>
431 where
432 S: Supports<T> + Supports<OrderTarget>,
433 {
434 OrderSpace::new(self.scheme, target, &self.context)
435 }
436
437 /// Encrypt a value as the target its Rust type is, under this context.
438 ///
439 /// # Errors
440 ///
441 /// The scheme's own failure, and nothing else: the value always fits.
442 pub fn encrypt<V>(&self, value: V) -> Result<OrderTerm<S::Term, V::Target>, S::Error>
443 where
444 V: OwnTarget + FitsIn<V::Target>,
445 S: Supports<V::Target> + Supports<OrderTarget>,
446 {
447 self.target(V::Target::default()).encrypt(value)
448 }
449
450 /// Encrypt a value as the target its Rust type is, checking it while
451 /// the program runs, under this context.
452 ///
453 /// # Errors
454 ///
455 /// As [`OrderSpace::encrypt_checked`].
456 #[allow(clippy::type_complexity)]
457 pub fn encrypt_checked<V: OwnTarget>(
458 &self,
459 value: V,
460 ) -> Result<OrderTerm<S::Term, V::Target>, OrderError<S::Error>>
461 where
462 S: Supports<V::Target> + Supports<OrderTarget>,
463 {
464 self.target(V::Target::default()).encrypt_checked(value)
465 }
466}
467
468/// One space of an order index on a scheme: the set of terms that compare
469/// with each other, under one scheme, key, target and context. It holds
470/// the target, the salt its terms are bound under, fixed when it is made
471/// so every term of it agrees, and what the scheme derives under that
472/// salt ([`OrderScheme::Space`]), derived once for every term of the
473/// space. From [`OrderEncrypt::target`] or [`OrderContext::target`].
474/// `Debug` shows the target and domain and masks the scheme, whose key
475/// it holds, the context, the salt and what was derived.
476///
477/// The scheme opens the space when its first value is encrypted, or when
478/// [`open`](Self::open) is called, so making one cannot fail and a chain
479/// never needs a `?` before its value. If the scheme fails to open, every
480/// encryption reports it and the next tries again. A one-shot chain
481/// (`scheme.encrypt(v)`, `scheme.target(I32).encrypt(v)`) makes a space
482/// per call and so opens the scheme per value; hold the space to derive
483/// once for many values.
484///
485/// `T` is the target: a marker from [`target`](crate::target), whose
486/// values the compiler checks through [`encrypt`](Self::encrypt), or
487/// [`OrderTarget`], chosen while the program runs, whose values
488/// [`encrypt_checked`](Self::encrypt_checked) checks then.
489///
490/// A space holds a reference to the scheme and the scheme's
491/// [`Space`](OrderScheme::Space), so it is `Send` when the scheme is
492/// `Sync` and its `Space` is `Send`, and `Sync` when its `Space` is `Sync`
493/// too. A space of a scheme that keeps a random number generator behind
494/// a `RefCell` is opened per thread.
495#[derive(OpaqueDebug)]
496pub struct OrderSpace<'s, S: OrderScheme + ?Sized, T: Target = OrderTarget> {
497 pub(crate) scheme: &'s S,
498 #[non_sensitive]
499 pub(crate) target: T,
500 /// The space's domain label: the target's, or its null policy's.
501 #[non_sensitive]
502 domain: &'static str,
503 /// The context the space was made under, kept so that
504 /// [`nulls`](OrderSpace::nulls) can make the same space with a policy.
505 /// Masked in `Debug` with the salt it is bound into.
506 context: Context<'static>,
507 salt: Context<'static>,
508 /// What the scheme derived under the salt, once it has.
509 opened: OnceLock<S::Space>,
510}
511
512/// `OrderSpace` under a target chosen while the program runs, as the 0.6
513/// API named it.
514#[deprecated(since = "0.7.0", note = "use `OrderSpace<'s, S, OrderTarget>`")]
515pub type OrderColumn<'s, S> = OrderSpace<'s, S, OrderTarget>;
516
517impl<'s, S: Supports<T> + Supports<OrderTarget> + ?Sized, T: Target> OrderSpace<'s, S, T> {
518 fn new(scheme: &'s S, target: T, context: &Context<'_>) -> Self {
519 // A marker impl vouches for its target; the runtime list must
520 // agree, or a space under the marker would hand the scheme a
521 // plaintext it cannot take. `supports_targets!` writes both from
522 // one list; this catches a pair written by hand that disagree.
523 debug_assert!(
524 !<S as Supports<T>>::supports(target)
525 || <S as Supports<OrderTarget>>::supports(target.runtime()),
526 "{} implements Supports<{:?}> but refuses it at runtime",
527 S::NAME,
528 target.runtime()
529 );
530 // Every `Nulls` target is made by a `nulls` method, which takes a
531 // fixed-width target only, so every space's target has a label.
532 let domain = label(target.runtime(), target.null_order())
533 .expect("a null policy is only ever set on a fixed-width target");
534 Self {
535 scheme,
536 target,
537 domain,
538 context: context.clone().into_owned(),
539 salt: salt(S::NAME, domain, context),
540 opened: OnceLock::new(),
541 }
542 }
543
544 /// Open the space now rather than at its first value: have the scheme
545 /// derive what it keeps for the space under the salt. Encrypting does
546 /// this when it has not been done; call it to hear about a scheme
547 /// that cannot open before the first value, or to pay for the
548 /// derivation up front.
549 ///
550 /// # Errors
551 ///
552 /// The scheme's own failure, from [`OrderScheme::open`]. Nothing is
553 /// kept, so the next call tries again.
554 pub fn open(&self) -> Result<(), S::Error> {
555 self.opened().map(|_| ())
556 }
557
558 /// What the scheme keeps for this space, opening it if it has not
559 /// been. Two threads may open it at once; one result is kept and the
560 /// other dropped, and both are as good.
561 pub(crate) fn opened(&self) -> Result<&S::Space, S::Error> {
562 if let Some(opened) = self.opened.get() {
563 return Ok(opened);
564 }
565 let opened = self.scheme.open(self.target.runtime(), &self.salt)?;
566 Ok(self.opened.get_or_init(|| opened))
567 }
568
569 /// This target under `context` instead: a new space, whose terms do
570 /// not compare with this one's.
571 pub fn context<'a, C: IntoContext<'a>>(&self, context: C) -> Self {
572 Self::new(self.scheme, self.target, &context.into_context().encode())
573 }
574
575 /// The type every value of this space is encoded as.
576 pub fn target(&self) -> OrderTarget {
577 self.target.runtime()
578 }
579
580 /// Where this space places nulls, or `None` if it gives them no term.
581 /// Set with [`nulls`](OrderSpace::nulls).
582 pub fn null_order(&self) -> Option<NullOrder> {
583 self.target.null_order()
584 }
585
586 /// The frozen domain label of every term of this space: the target's,
587 /// or the label of its null policy.
588 pub fn domain(&self) -> &'static str {
589 self.domain
590 }
591
592 /// The salt every term of this space is bound under. Not secret.
593 pub fn salt(&self) -> &Context<'static> {
594 &self.salt
595 }
596
597 /// Encrypt one scalar as this space's target, checking it while the
598 /// program runs: an integer of any width it fits, a float, a string,
599 /// bytes, or a typed value such as a date. Any [`Encrypt`] value,
600 /// which is what an index layer holds when the target was chosen
601 /// while the program ran.
602 ///
603 /// A scalar with no term is refused rather than skipped: a caller who
604 /// asks for one term and gets none should hear about it. A null has a
605 /// term only in a space that places nulls, where it is the space's
606 /// sentinel.
607 ///
608 /// # Errors
609 ///
610 /// [`OrderError::Unsupported`] if the scheme does not take this
611 /// target, [`OrderError::Term`] if the scalar has no term as the
612 /// space's target (with [`NormalizeError::Reserved`] for the maximum
613 /// of an integer or uuid target in a space that places nulls),
614 /// [`OrderError::NotScalar`] if the value is a container,
615 /// [`OrderError::Source`] if the value could not be read,
616 /// [`OrderError::Backend`] if the scheme failed.
617 pub fn encrypt_checked<V: Encrypt>(
618 &self,
619 value: V,
620 ) -> Result<OrderTerm<S::Term, T>, OrderError<S::Error>> {
621 self.supported()?;
622 let plaintext = value.visit(Scalar::of(self.target))?;
623 self.encrypt_plaintext(&plaintext)
624 }
625
626 /// Encrypt a plaintext [`order_terms`](crate::order_terms) collected
627 /// from a whole value.
628 ///
629 /// # Errors
630 ///
631 /// [`OrderError::Term`] with [`NormalizeError::Mismatch`] if it was
632 /// encoded for another space than this one (another target, or
633 /// another null policy), else as
634 /// [`encrypt_checked`](Self::encrypt_checked).
635 pub fn encrypt_term(
636 &self,
637 plaintext: &OrderPlaintext,
638 ) -> Result<OrderTerm<S::Term, T>, OrderError<S::Error>> {
639 if plaintext.domain() != self.domain {
640 return Err(OrderError::Term(NormalizeError::Mismatch));
641 }
642 self.supported()?;
643 self.encrypt_plaintext(plaintext)
644 }
645
646 /// A term the scheme already made in this space, read back from
647 /// storage say, as the space's own: it then compares with the space's
648 /// other terms. The caller vouches that it was made here; the space
649 /// cannot tell.
650 pub fn term(&self, raw: S::Term) -> OrderTerm<S::Term, T> {
651 OrderTerm {
652 raw,
653 target: self.target,
654 }
655 }
656
657 /// The order of two of this space's terms, as the order of the values
658 /// they encrypt: [`OrderScheme::compare`]. A total order over the
659 /// space's terms, so they sort by it.
660 ///
661 /// Both terms must be this space's. A term of a target named when the
662 /// program is compiled cannot be another target's or another null
663 /// policy's: that does not compile. A term of one chosen while it ran
664 /// carries its target and its null policy, so two of different
665 /// targets or policies, or of another than this space's, are `None`. The scheme cannot always
666 /// tell two spaces' terms apart otherwise, and may order them as
667 /// noise. `None` where it can tell: a term not of this scheme, or two
668 /// terms not made to compare.
669 pub fn compare(
670 &self,
671 a: &OrderTerm<S::Term, T>,
672 b: &OrderTerm<S::Term, T>,
673 ) -> Option<Ordering> {
674 (same(a.target, self.target) && same(b.target, self.target))
675 .then(|| S::compare(&a.raw, &b.raw))
676 .flatten()
677 }
678
679 /// Refuse a target the scheme does not take, by the scheme's runtime
680 /// list. A marker's impl already vouched for its target when the
681 /// space was opened; asking again costs a match.
682 pub(crate) fn supported(&self) -> Result<(), OrderError<S::Error>> {
683 if <S as Supports<OrderTarget>>::supports(self.target.runtime()) {
684 Ok(())
685 } else {
686 Err(OrderError::Unsupported {
687 scheme: S::NAME,
688 domain: self.domain,
689 })
690 }
691 }
692
693 fn encrypt_plaintext(
694 &self,
695 plaintext: &OrderPlaintext,
696 ) -> Result<OrderTerm<S::Term, T>, OrderError<S::Error>> {
697 self.encrypt_opened(plaintext).map_err(OrderError::Backend)
698 }
699
700 /// Open the space if it is not, then encrypt one plaintext of it.
701 fn encrypt_opened(
702 &self,
703 plaintext: &OrderPlaintext,
704 ) -> Result<OrderTerm<S::Term, T>, S::Error> {
705 let opened = self.opened()?;
706 self.scheme
707 .encrypt_plaintext(opened, plaintext)
708 .map(|raw| self.term(raw))
709 }
710}
711
712impl<S: Supports<T> + Supports<OrderTarget> + ?Sized, T: StaticTarget> OrderSpace<'_, S, T> {
713 /// Encrypt a value that always fits this space's target, as the
714 /// [`FitsIn`] table says: an `i16` into an `I32` space, an `f32` into
715 /// an `F64` one. The compiler refuses a value that might not fit, and
716 /// names [`encrypt_checked`](Self::encrypt_checked) for it.
717 ///
718 /// # Errors
719 ///
720 /// The scheme's own failure, and nothing else: the value always fits,
721 /// and the scheme [`Supports`] the target. A scheme whose `Error` is
722 /// [`Infallible`](core::convert::Infallible) cannot fail at all.
723 pub fn encrypt<V: FitsIn<T>>(&self, value: V) -> Result<OrderTerm<S::Term, T>, S::Error> {
724 let plaintext = match value.visit(Scalar::of(T::default())) {
725 Ok(plaintext) => plaintext,
726 // `FitsIn` is a sealed table of conversions the encoding never
727 // refuses, and this crate's tests encrypt every row at its
728 // extremes, so no value reaches here.
729 Err(refused) => unreachable!("a `FitsIn` value was refused: {refused}"),
730 };
731 self.encrypt_opened(&plaintext)
732 }
733}
734
735/// Whether two targets name one space's target: the same type and the
736/// same null policy.
737pub(crate) fn same<T: Target>(a: T, b: T) -> bool {
738 a.runtime() == b.runtime() && a.null_order() == b.null_order()
739}
740
741impl<'s, S: Supports<T> + Supports<OrderTarget> + ?Sized, T: FixedWidth> OrderSpace<'s, S, T> {
742 /// This target and context in a space that places nulls as the policy
743 /// marker says, [`First`](crate::target::First) or
744 /// [`Last`](crate::target::Last): a new space, whose terms do not
745 /// compare with this one's.
746 ///
747 /// Every null in it, typed or plain, encodes to the space's sentinel,
748 /// and for an integer or uuid target the target's maximum is refused
749 /// with [`NormalizeError::Reserved`]; [`NullOrder`] has the rule and
750 /// what a placed null reveals. A value that might be the maximum goes
751 /// through [`encrypt_checked`](OrderSpace::encrypt_checked), which
752 /// reports it, so a space with a policy has no
753 /// [`encrypt`](OrderSpace::encrypt).
754 ///
755 /// The policy is part of the space's type, `Nulls<T, P>`, so a term of
756 /// a nulls-first space does not compile against a nulls-last space's
757 /// [`compare`](OrderSpace::compare). Compiles only for a fixed-width
758 /// marker: text and bytes take no null policy. A layer that chooses
759 /// the policy while the program runs opens the space with an
760 /// [`OrderTarget`] and passes a [`NullOrder`] instead, as the second
761 /// half of the example shows.
762 ///
763 /// ```
764 /// use vitaminc_ore::target::{Last, I32};
765 /// use vitaminc_ore::{NormalizeError, NullOrder, OrderEncrypt, OrderError, OrderTarget};
766 /// # use vitaminc_ore::{Context, OrderPlaintext, OrderScheme};
767 /// # struct Clear;
768 /// # impl OrderScheme for Clear {
769 /// # const NAME: &'static str = "clear";
770 /// # type Term = Vec<u8>;
771 /// # type Error = core::convert::Infallible;
772 /// # type Space = ();
773 /// # fn open(&self, _: vitaminc_ore::OrderTarget, _: &Context<'_>) -> Result<(), Self::Error> {
774 /// # Ok(())
775 /// # }
776 /// # fn encrypt_plaintext(&self, _: &(), p: &OrderPlaintext) -> Result<Vec<u8>, Self::Error> {
777 /// # use vitaminc_protected::Controlled;
778 /// # Ok(p.bytes().risky_ref().clone())
779 /// # }
780 /// # fn compare(a: &Vec<u8>, b: &Vec<u8>) -> Option<core::cmp::Ordering> {
781 /// # Some(a.cmp(b))
782 /// # }
783 /// # }
784 /// # vitaminc_ore::supports_every_target!(Clear);
785 /// # let scheme = Clear;
786 /// use std::cmp::Ordering;
787 ///
788 /// // A marker target takes a marker policy, checked by the compiler.
789 /// let ages = scheme.context("users.age").target(I32).nulls(Last);
790 /// let (age, absent) = (ages.encrypt_checked(36i32)?, ages.encrypt_checked(None::<i32>)?);
791 /// assert_eq!(ages.compare(&age, &absent), Some(Ordering::Less));
792 /// assert!(matches!(
793 /// ages.encrypt_checked(i32::MAX),
794 /// Err(OrderError::Term(NormalizeError::Reserved))
795 /// ));
796 ///
797 /// // A target chosen while the program runs takes a policy chosen then
798 /// // too, and refuses text and bytes.
799 /// let index = scheme.target(OrderTarget::I32).context("users.age").nulls(NullOrder::Last)?;
800 /// let (age, absent) = (index.encrypt_checked(36i32)?, index.encrypt_null()?);
801 /// assert_eq!(index.compare(&age, &absent), Some(Ordering::Less));
802 /// assert_eq!(
803 /// scheme.target(OrderTarget::Text).nulls(NullOrder::Last).err(),
804 /// Some(NormalizeError::NoDomain)
805 /// );
806 /// # Ok::<(), Box<dyn std::error::Error>>(())
807 /// ```
808 pub fn nulls<P: NullPolicy>(&self, _: P) -> OrderSpace<'s, S, Nulls<T, P>> {
809 OrderSpace::new(self.scheme, Nulls::new(self.target), &self.context)
810 }
811}
812
813impl<'s, S: Supports<OrderTarget> + ?Sized> OrderSpace<'s, S, OrderTarget> {
814 /// This target and context in a space that places nulls as `nulls`
815 /// says, for a target chosen while the program runs: a new space,
816 /// whose terms do not compare with this one's. As
817 /// [`nulls`](OrderSpace::nulls) on a marker target, which has the
818 /// rules, except that the policy is a value: a term carries it, and
819 /// two terms of different policies compare as `None`.
820 ///
821 /// # Errors
822 ///
823 /// [`NormalizeError::NoDomain`] for text and bytes, which take no null
824 /// policy, rather than a space that refuses every value.
825 pub fn nulls(
826 &self,
827 nulls: NullOrder,
828 ) -> Result<OrderSpace<'s, S, RuntimeNulls>, NormalizeError> {
829 if self.target.domain_with_nulls(nulls).is_none() {
830 return Err(NormalizeError::NoDomain);
831 }
832 Ok(OrderSpace::<'s, S, RuntimeNulls>::new(
833 self.scheme,
834 RuntimeNulls::new(self.target, nulls),
835 &self.context,
836 ))
837 }
838}
839
840impl<S: Supports<T> + Supports<OrderTarget> + ?Sized, T: Target> OrderSpace<'_, S, T> {
841 /// The space's sentinel, sealed. Called only on a space that places
842 /// nulls, whose target is a [`Nulls`] or a [`RuntimeNulls`].
843 pub(crate) fn sentinel(&self) -> OrderPlaintext {
844 self.target
845 .null_order()
846 .and_then(|order| null_plaintext(self.target.runtime(), order))
847 .expect("a null policy is only ever set on a fixed-width target")
848 }
849}
850
851impl<S: Supports<T> + Supports<OrderTarget> + ?Sized, T: FixedWidth, P: NullPolicy>
852 OrderSpace<'_, S, Nulls<T, P>>
853{
854 /// The term of a null in this space: its sentinel, encrypted. The same
855 /// as [`encrypt_checked`](OrderSpace::encrypt_checked) of any null.
856 ///
857 /// # Errors
858 ///
859 /// The scheme's own failure, and nothing else: the scheme
860 /// [`Supports`] the target.
861 #[allow(clippy::type_complexity)]
862 pub fn encrypt_null(&self) -> Result<OrderTerm<S::Term, Nulls<T, P>>, S::Error> {
863 self.encrypt_opened(&self.sentinel())
864 }
865}
866
867impl<S: Supports<OrderTarget> + ?Sized> OrderSpace<'_, S, RuntimeNulls> {
868 /// The term of a null in this space, for a target chosen while the
869 /// program runs: its sentinel, encrypted. The same as
870 /// [`encrypt_checked`](OrderSpace::encrypt_checked) of any null.
871 ///
872 /// # Errors
873 ///
874 /// [`OrderError::Unsupported`] if the scheme does not take this
875 /// target, [`OrderError::Backend`] if the scheme failed.
876 #[allow(clippy::type_complexity)]
877 pub fn encrypt_null(&self) -> Result<OrderTerm<S::Term, RuntimeNulls>, OrderError<S::Error>> {
878 self.supported()?;
879 self.encrypt_plaintext(&self.sentinel())
880 }
881}
882
883/// One term of an order space: the scheme's term, labelled with the target
884/// it was made under.
885///
886/// Under a marker from [`target`](crate::target) the label is a zero-sized
887/// type, so two terms of different targets cannot meet in
888/// [`OrderSpace::compare`]: that does not compile, and nor does a term of a
889/// space that places nulls in a plain space's comparison, or a term of a
890/// nulls-first space in a nulls-last space's, since the policy is part of
891/// the [`Nulls`] label's type. Under an [`OrderTarget`] the label is
892/// stored, and two of different targets compare as `None` instead of as
893/// noise. A [`RuntimeNulls`] label stores its policy beside its target,
894/// so a nulls-first and a nulls-last term of it compare as `None` too. Storage reads
895/// the scheme's bytes through `AsRef<[u8]>`, and [`raw`](Self::raw) is the
896/// scheme's term itself.
897///
898/// `==` compares the stored term, byte for byte, not the value: the order
899/// of two values is [`OrderSpace::compare`], and a randomised scheme
900/// (block ORE carries a nonce) gives one value many terms, which compare
901/// `Equal` and are not `==`.
902#[derive(Debug, Clone, PartialEq, Eq)]
903pub struct OrderTerm<R, T: Target> {
904 raw: R,
905 pub(crate) target: T,
906}
907
908impl<R, T: Target> OrderTerm<R, T> {
909 /// The target the term was made under.
910 pub fn target(&self) -> OrderTarget {
911 self.target.runtime()
912 }
913
914 /// Where the space the term was made in places nulls, or `None` for a
915 /// space that gives them no term.
916 pub fn null_order(&self) -> Option<NullOrder> {
917 self.target.null_order()
918 }
919
920 /// The scheme's own term.
921 pub fn raw(&self) -> &R {
922 &self.raw
923 }
924
925 /// The scheme's own term, unlabelled.
926 pub fn into_raw(self) -> R {
927 self.raw
928 }
929}
930
931impl<R: AsRef<[u8]>, T: Target> OrderTerm<R, T> {
932 /// The stored bytes: the scheme's term's.
933 pub fn as_bytes(&self) -> &[u8] {
934 self.raw.as_ref()
935 }
936}
937
938impl<R: AsRef<[u8]>, T: Target> AsRef<[u8]> for OrderTerm<R, T> {
939 fn as_ref(&self) -> &[u8] {
940 self.raw.as_ref()
941 }
942}
943
944/// A scheme keyed from secret bytes, as
945/// [`PrfKeyInit`](https://docs.rs/vitaminc-prf) keys a PRF, so an index
946/// layer can construct any scheme the same way.
947pub trait OrderKeyInit: OrderScheme + Sized {
948 /// The scheme's full-strength key, a protected array.
949 type Key: Controlled;
950 /// Why key material of a length only known at runtime was refused.
951 type KeyError: core::error::Error + Send + Sync + 'static;
952
953 /// Key the scheme with a full-strength key. Cannot fail: the type
954 /// carries the length.
955 fn new(key: Self::Key) -> Self;
956
957 /// Key the scheme with material whose length is only known at runtime,
958 /// such as a KMS response.
959 ///
960 /// # Errors
961 ///
962 /// [`KeyError`](OrderKeyInit::KeyError) if the material is unfit, a
963 /// short key say. Accepting it would produce terms anyone can order.
964 fn try_from_bytes(key: Protected<Vec<u8>>) -> Result<Self, Self::KeyError>;
965}
966
967/// The sink one scalar is walked into: it encodes the scalar as its
968/// space's target, under the space's null policy, and refuses a
969/// container.
970#[derive(Clone, Copy)]
971pub(crate) struct Scalar {
972 target: OrderTarget,
973 nulls: Option<NullOrder>,
974}
975
976impl Scalar {
977 /// The sink for a space of `target`.
978 pub(crate) fn of<T: Target>(target: T) -> Self {
979 Self {
980 target: target.runtime(),
981 nulls: target.null_order(),
982 }
983 }
984}
985
986/// How one scalar fails to become a plaintext.
987#[derive(Debug, thiserror::Error)]
988pub(crate) enum ScalarError {
989 #[error(transparent)]
990 Term(NormalizeError),
991 #[error("not a scalar")]
992 NotScalar,
993 #[error(transparent)]
994 Source(SourceError),
995}
996
997impl plaintext::Error for ScalarError {
998 fn source(error: SourceError) -> Self {
999 Self::Source(error)
1000 }
1001}
1002
1003impl<E> From<ScalarError> for OrderError<E> {
1004 fn from(error: ScalarError) -> Self {
1005 match error {
1006 ScalarError::Term(e) => Self::Term(e),
1007 ScalarError::NotScalar => Self::NotScalar,
1008 ScalarError::Source(e) => Self::Source(e),
1009 }
1010 }
1011}
1012
1013impl Sink for Scalar {
1014 type Ok = OrderPlaintext;
1015 type Error = ScalarError;
1016 type Seq = Never;
1017 type Map = Never;
1018
1019 fn scalar(
1020 self,
1021 ty: Option<Logical>,
1022 value: Physical<'_>,
1023 ) -> Result<OrderPlaintext, ScalarError> {
1024 encode(self.target, self.nulls, ty, value).map_err(ScalarError::Term)
1025 }
1026
1027 /// The same as a plain null: the space's sentinel in a space that
1028 /// places nulls, and no term in one that does not. The sentinel is the
1029 /// extreme code of the space's target, so the null's own type adds
1030 /// nothing.
1031 fn typed_null(self, _: FixedType) -> Result<OrderPlaintext, ScalarError> {
1032 self.scalar(None, Physical::Null)
1033 }
1034
1035 fn seq(self, _: Option<usize>) -> Result<Never, ScalarError> {
1036 Err(ScalarError::NotScalar)
1037 }
1038
1039 fn map(self, _: Option<usize>) -> Result<Never, ScalarError> {
1040 Err(ScalarError::NotScalar)
1041 }
1042
1043 fn passthrough<P: Encrypt + Clone>(self, _: P) -> Result<OrderPlaintext, ScalarError> {
1044 Err(ScalarError::NotScalar)
1045 }
1046}
1047
1048/// The container sink [`Scalar`] never hands out. Uninhabited, so it
1049/// cannot be built.
1050pub(crate) enum Never {}
1051
1052impl SeqSink for Never {
1053 type Ok = OrderPlaintext;
1054 type Error = ScalarError;
1055 type Element<'s>
1056 = Scalar
1057 where
1058 Self: 's;
1059
1060 fn element(&mut self) -> Result<Scalar, ScalarError> {
1061 match *self {}
1062 }
1063
1064 fn end(self) -> Result<OrderPlaintext, ScalarError> {
1065 match self {}
1066 }
1067}
1068
1069impl MapSink for Never {
1070 type Ok = OrderPlaintext;
1071 type Error = ScalarError;
1072 type Entry<'s>
1073 = Scalar
1074 where
1075 Self: 's;
1076
1077 fn entry(&mut self, _: &str) -> Result<Scalar, ScalarError> {
1078 match *self {}
1079 }
1080
1081 fn end(self) -> Result<OrderPlaintext, ScalarError> {
1082 match self {}
1083 }
1084}
1085
1086impl fmt::Display for OrderPlaintext {
1087 /// The domain only: never the bytes.
1088 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1089 f.write_str(self.domain())
1090 }
1091}
1092
1093#[cfg(test)]
1094mod tests {
1095 use super::*;
1096 use crate::{domain, order};
1097 use vitaminc_aead::Int;
1098
1099 #[test]
1100 fn every_target_has_the_width_its_encoding_has() {
1101 for target in OrderTarget::ALL {
1102 let variable = matches!(target, OrderTarget::Text | OrderTarget::Bytes);
1103 assert_eq!(target.width().is_none(), variable, "{target:?}");
1104 }
1105 let p = order(OrderTarget::I32, None, Physical::Int(Int::I8(1))).expect("term");
1106 assert_eq!(p.width(), Some(4));
1107 assert_eq!(p.bytes().risky_ref().len(), 4);
1108 assert_eq!(p.target(), OrderTarget::I32);
1109 assert_eq!(
1110 order(OrderTarget::Text, None, Physical::Text("a"))
1111 .expect("term")
1112 .width(),
1113 None
1114 );
1115 }
1116
1117 #[test]
1118 fn the_salt_binds_label_scheme_domain_and_context() {
1119 let context = ("tenant", 1u64).into_context().encode();
1120 let salted = salt("mock", domain::INT16, &context);
1121 assert_eq!(
1122 salted,
1123 Context::pae(&[
1124 b"vitaminc/ore/v1",
1125 b"mock",
1126 domain::INT16.as_bytes(),
1127 context.as_bytes()
1128 ])
1129 );
1130 assert_ne!(salted, salt("other", domain::INT16, &context));
1131 assert_ne!(salted, salt("mock", domain::UINT16, &context));
1132 assert_ne!(
1133 salted,
1134 salt(
1135 "mock",
1136 domain::INT16,
1137 &("tenant", 2u64).into_context().encode()
1138 )
1139 );
1140 // A null policy is its own label, so its own salt.
1141 assert_ne!(salted, salt("mock", domain::INT16_NULLS_FIRST, &context));
1142 assert_ne!(salted, salt("mock", domain::INT16_NULLS_LAST, &context));
1143 }
1144
1145 #[test]
1146 fn debug_and_display_show_the_domain_and_mask_the_bytes() {
1147 let p = order(OrderTarget::Text, None, Physical::Text("secret")).expect("term");
1148 let shown = format!("{p:?}");
1149 assert!(shown.contains("Text") && shown.contains("***"), "{shown}");
1150 assert!(!shown.contains("secret"), "{shown}");
1151 assert_eq!(p.to_string(), domain::TEXT);
1152 }
1153
1154 #[test]
1155 fn errors_say_what_failed_without_plaintext() {
1156 type E = OrderError<&'static str>;
1157 assert_eq!(
1158 E::Term(NormalizeError::NoDomain).to_string(),
1159 "no order term: no domain for this kind of value"
1160 );
1161 assert_eq!(
1162 E::Term(NormalizeError::Mismatch).to_string(),
1163 "no order term: the value is not of the kind the index declares"
1164 );
1165 assert_eq!(
1166 E::Term(NormalizeError::OutOfRange).to_string(),
1167 "no order term: the value does not fit the type the index declares"
1168 );
1169 assert_eq!(
1170 E::NotScalar.to_string(),
1171 "a container has no single order term; walk it for one per scalar"
1172 );
1173 assert_eq!(
1174 E::Source(SourceError::msg("getter threw")).to_string(),
1175 "the value could not be read: getter threw"
1176 );
1177 assert_eq!(
1178 E::Unsupported {
1179 scheme: "block-ore",
1180 domain: domain::TEXT
1181 }
1182 .to_string(),
1183 "block-ore has no order term for vitaminc/ore/value/text-folded/unicode-16/v1"
1184 );
1185 assert_eq!(
1186 E::Backend("down").to_string(),
1187 "the order scheme failed: down"
1188 );
1189 let from: E = NormalizeError::Invalid.into();
1190 assert!(matches!(from, E::Term(NormalizeError::Invalid)));
1191 }
1192
1193 #[test]
1194 fn the_scalar_sink_encodes_one_scalar_as_its_target_and_refuses_a_container() {
1195 let p = 7i16.visit(Scalar::of(OrderTarget::I64)).expect("scalar");
1196 assert_eq!(p.domain(), domain::INT64);
1197 assert!(matches!(
1198 vec![1u16, 2].visit(Scalar::of(OrderTarget::U16)),
1199 Err(ScalarError::NotScalar)
1200 ));
1201 assert!(matches!(
1202 std::collections::BTreeMap::from([("k", 1u16)]).visit(Scalar::of(OrderTarget::U16)),
1203 Err(ScalarError::NotScalar)
1204 ));
1205 assert!(matches!(
1206 true.visit(Scalar::of(OrderTarget::U16)),
1207 Err(ScalarError::Term(NormalizeError::NoDomain))
1208 ));
1209 }
1210
1211 /// A fixed-width `None` reaches the sink as a typed null. In a space
1212 /// with no null policy it has no order term, as a plain null has none;
1213 /// in a space that places nulls it is the space's sentinel.
1214 #[test]
1215 fn the_scalar_sink_gives_a_typed_null_no_term_without_a_policy_and_the_sentinel_with_one() {
1216 let plain = Scalar::of(OrderTarget::U32);
1217 assert!(
1218 matches!(
1219 None::<u32>.visit(plain),
1220 Err(ScalarError::Term(NormalizeError::NoDomain))
1221 ),
1222 "a typed null must have no order term in a plain space"
1223 );
1224 assert!(
1225 matches!(
1226 None::<u32>.visit(Scalar::of(OrderTarget::Text)),
1227 Err(ScalarError::Term(NormalizeError::NoDomain))
1228 ),
1229 "the target does not give a typed null a term either"
1230 );
1231 for (nulls, fill, label) in [
1232 (NullOrder::First, 0x00, domain::UINT32_NULLS_FIRST),
1233 (NullOrder::Last, 0xFF, domain::UINT32_NULLS_LAST),
1234 ] {
1235 let placed = Scalar::of(RuntimeNulls::new(OrderTarget::U32, nulls));
1236 let typed = None::<u32>
1237 .visit(placed)
1238 .expect("a typed null is the sentinel");
1239 assert_eq!(
1240 typed.domain(),
1241 label,
1242 "the sentinel is under the policy's label"
1243 );
1244 assert_eq!(
1245 typed.null_order(),
1246 Some(nulls),
1247 "the plaintext carries its policy"
1248 );
1249 assert_eq!(
1250 typed.bytes().risky_ref(),
1251 &[fill; 4],
1252 "a typed null is the sentinel's bytes"
1253 );
1254 // The null's own type adds nothing: an `i16` null in a `u32`
1255 // space is the same sentinel, and so is a plain null.
1256 let other = None::<i16>.visit(placed).expect("sentinel");
1257 let raw = vitaminc_aead::Raw(None, Physical::Null)
1258 .visit(placed)
1259 .expect("sentinel");
1260 assert_eq!(other.bytes().risky_ref(), typed.bytes().risky_ref());
1261 assert_eq!(raw.bytes().risky_ref(), typed.bytes().risky_ref());
1262 }
1263 }
1264}