Vitamin C Normalize
Normalized equality terms for the plaintext data model of
vitaminc-aead. Part of the
Vitamin C framework.
An equality index derives a key from a value. Two values that are equal must derive the same key whichever width, scale or spelling wrote them, and two values that merely look alike (a date and the integer it is stored as, a uuid and the same sixteen bytes) must not. This crate decides both: each scalar kind that can be indexed has a frozen domain label and a normalization to bytes, and everything else has no term.
There is no cryptography here and no PAE: the crate's own dependencies
are the Unicode tables. (It builds on vitaminc-aead for the data model,
which brings that crate's dependencies with it.) The encodings are its
own, including the decimal one, which is ported from the frozen
orderable-bytes release and checked against it byte for byte. A PRF crate such as
vitaminc-prf takes a [Normalized]
and derives under its domain.
One scalar
use ;
use ;
use Controlled;
// Every integer width is one term.
let a = normalize?;
let b = normalize?;
assert_eq!;
assert_eq!;
// A date is not the integer it is stored as.
let day = normalize?;
assert_eq!;
assert_ne!;
# Ok::
Logical types opt in: a date, a timestamp, a decimal and a uuid have
domains of their own. A boolean, an undefined and a logical type this
crate does not know have no term, and normalize refuses them with
[NormalizeError::NoDomain].
Every null has a term, typed or plain, under a domain of its own. It is
the same term in every column, as SQL says NULL IS NULL whatever the
column; spaces that must not tie already differ in context. So the term
column of an index holds a term where the value was absent, as it does
where the value was there, and the nulls show only as one more group of
equal terms, which is what any repeated value shows.
use ;
use Physical;
use Controlled;
let null = normalize?;
assert_eq!;
assert_eq!;
# Ok::
A layer that must keep a SQL NULL in the term column for some column
drops the term it was handed. It knows the term is a null's because its
domain is [domain::NULL]. Producing the term is the default, because the
secure path should be the easy one, and dropping it is an explicit act.
A whole value, beside encryption
[Terms] is a sink that collects one [Term] per indexable scalar, with
the path to it. Teed with the encrypting sink it sees the same walk, so
the terms are complete before any cipher call:
use BTreeMap;
use ;
# use ;
# use ;
# /// Keeps the plaintext in the clear, for the example. Any `Cipher` will do.
# ;
#
# let cipher = Clear;
let mut user = new;
user.insert;
user.insert;
let = encrypt_with_terms?;
assert_eq!;
assert_eq!;
assert_eq!;
# Ok::
encrypt_with_terms_async does the same through an async cipher and has
every term before its first await. plan_with_terms walks without
sealing at all, and terms collects the terms with no encryption in the
walk.
terms refuses every value encryption refuses, for the same reason and
at the same path, so an index never holds terms for a value that cannot
be stored. It keeps the same shape rules, through the same code: no
duplicate key, no value nested too deep, every scalar valid for its
logical type, every passthrough framable, and something sealed in every
container with children, even though it seals nothing itself. Read such a
refusal with TermsError::refusal; encryption reports the same one as
EncryptError::Shape.
A null, typed or plain, has its term at its own path. A scalar whose kind
has no domain (a boolean, an undefined) produces no term and does not
stop the walk; passthrough subtrees get none either. A scalar whose
logical type has no domain, one from a newer client, stops the walk:
encrypting it with no term and no error would leave an index silently
incomplete. That refusal and an unassigned code point
arrive as EncryptError::Source carrying a TermError (from terms, as
TermsError::term), which holds the NormalizeError and the path to the
refused field. Downcast to TermError; the NormalizeError is also its
source(), so a reporter that walks the error chain reaches it. The
message names neither the field nor the value, because map keys can be
user data; read the path with TermError::path.
Encodings
These are protocol data. Changing any of them, or a domain label, means a new label.
| Kind | Domain | Bytes |
|---|---|---|
| integer, any width | vitaminc/prf/value/integer-orderable/v1 |
sign byte (0x00 negative, 0x01 otherwise), then the big-endian 128-bit two's-complement word |
| float, either width | vitaminc/prf/value/float-orderable/v1 |
the f64 value's orderable bytes; f32 widened exactly, zero signs and NaN payloads folded |
| date | vitaminc/prf/value/date-orderable/v1 |
orderable days from 0001-01-01 |
| timestamp, any unit | vitaminc/prf/value/timestamp-micros-orderable/v1 |
orderable seconds, then nanoseconds truncated to microseconds |
| decimal | vitaminc/prf/value/decimal-orderable/v1 |
the 14-byte orderable encoding of the value (see [decimal]), scale and signed zero normalized |
| text | vitaminc/prf/value/text-nfc/unicode-16/v1 |
Unicode 16 NFC; a code point unassigned in Unicode 16 is refused |
| bytes | vitaminc/prf/value/bytes/v1 |
as they are |
| uuid | vitaminc/prf/value/uuid/v1 |
the sixteen raw bytes |
| null, typed or plain | vitaminc/prf/value/null/v1 |
one byte, 0x00 |
Text normalization is variable-time. The integer and float paths use no floating-point arithmetic.