Skip to main content

vitaminc_normalize/
scalar.rs

1//! One normalized equality term per scalar.
2//!
3//! [`normalize`] turns a physical value and its optional logical type into
4//! the bytes an equality index derives under, and the frozen domain label
5//! those bytes belong to. The encoders are public, one per kind, so an index
6//! layer that already knows a value's kind can call them directly.
7//!
8//! Every integer width shares one 17-byte encoding and domain, and both
9//! float widths share one 8-byte encoding and domain, so equal numbers
10//! match whichever width wrote them. Text is Unicode 16 NFC. These bytes
11//! and their [`domain`] labels are protocol data: changing either requires
12//! a new domain. Text processing is variable-time; this module makes no
13//! constant-time claim for Unicode normalization.
14//!
15//! Logical types opt in. A date, a timestamp, a decimal and a uuid each
16//! have a domain of their own, separate from the physical value they are
17//! stored as, so a date's day count never matches the integer it equals and
18//! a uuid never matches the same sixteen bytes written as bytes. Every other
19//! logical type, [`Logical::Unknown`] included, has no term and is refused
20//! with [`NormalizeError::NoDomain`].
21//!
22//! A null has a term too, under [`domain::NULL`]: one byte, the same in
23//! every column, so an absent value is indexed like any other value and
24//! the nulls show only as one more tied group. A boolean and an
25//! `undefined` have none.
26
27use unicode_general_category::{get_general_category, GeneralCategory};
28use unicode_normalization::UnicodeNormalization;
29use vitaminc_aead::plaintext::kinds::{timestamp_nanos, MAX_DATE_DAYS, MIN_DATE_DAYS};
30use vitaminc_aead::{Float, Int, Logical, Physical, TimeUnit};
31use vitaminc_protected::{Controlled, OpaqueDebug, Protected};
32
33/// Frozen domain labels for equality terms, one per kind that has one.
34pub mod domain {
35    /// Every integer width, `i8` through `u128`.
36    pub const INTEGER: &str = "vitaminc/prf/value/integer-orderable/v1";
37    /// `f32` and `f64`, as `f64`.
38    pub const FLOAT: &str = "vitaminc/prf/value/float-orderable/v1";
39    /// A date, over its day count.
40    pub const DATE: &str = "vitaminc/prf/value/date-orderable/v1";
41    /// A timestamp, truncated to microseconds.
42    pub const TIMESTAMP: &str = "vitaminc/prf/value/timestamp-micros-orderable/v1";
43    /// A decimal, with scale and signed zero normalized.
44    pub const DECIMAL: &str = "vitaminc/prf/value/decimal-orderable/v1";
45    /// Text, as Unicode 16 NFC.
46    pub const TEXT: &str = "vitaminc/prf/value/text-nfc/unicode-16/v1";
47    /// Bytes.
48    pub const BYTES: &str = "vitaminc/prf/value/bytes/v1";
49    /// A uuid, over its sixteen raw bytes.
50    pub const UUID: &str = "vitaminc/prf/value/uuid/v1";
51    /// A null, typed or plain, whatever column it sits in: the one byte
52    /// `0x00`.
53    pub const NULL: &str = "vitaminc/prf/value/null/v1";
54    /// Every domain, for a test or an index layer that enumerates them. A
55    /// slice, so a domain added later does not change its type.
56    pub const ALL: &[&str] = &[
57        INTEGER, FLOAT, DATE, TIMESTAMP, DECIMAL, TEXT, BYTES, UUID, NULL,
58    ];
59}
60
61/// The normalized bytes for one equality term and the domain they belong
62/// to.
63///
64/// The fields are public so an index layer can build a term of its own,
65/// and whatever consumes a `Normalized` trusts them: a PRF derives under
66/// the `domain` it is handed. The only way to get the frozen encodings is
67/// through [`normalize`] or the per-kind encoders here.
68///
69/// `Debug` shows the domain and masks the bytes.
70#[derive(OpaqueDebug)]
71pub struct Normalized {
72    /// The frozen domain label, one of the [`domain`] constants.
73    #[non_sensitive]
74    pub domain: &'static str,
75    /// The normalized plaintext, protected.
76    pub bytes: Protected<Vec<u8>>,
77}
78
79/// Why a scalar has no term. Never includes plaintext.
80#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
81#[non_exhaustive]
82pub enum NormalizeError {
83    /// The scalar has no domain under the normalizer: an `undefined`, a
84    /// boolean, a logical type outside its opt-in table, or a null under
85    /// an index that gives nulls no term (an order index whose space does
86    /// not place them). The message does not name an index, because the
87    /// equality and order normalizers both report it; whatever wraps it
88    /// says which.
89    #[error("no domain for this kind of value")]
90    NoDomain,
91    /// The value is not valid for its logical type.
92    #[error("the value is not valid for its logical type")]
93    Invalid,
94    /// Text contains a code point unassigned in Unicode 16.
95    #[error("text contains a code point unassigned in Unicode 16")]
96    UnassignedCodePoint,
97    /// The value is of another kind than the type the index declares: text
98    /// for an integer column, say. Not reported by the equality
99    /// normalizer, which declares no type.
100    #[error("the value is not of the kind the index declares")]
101    Mismatch,
102    /// The value is of the kind the index declares but does not fit it: an
103    /// integer outside the declared width. Not reported by the equality
104    /// normalizer, which declares no type.
105    #[error("the value does not fit the type the index declares")]
106    OutOfRange,
107    /// The value is of the kind the index declares, and fits it, but its
108    /// code is the one the index keeps for nulls: the maximum of an
109    /// integer or uuid target in an order space that places nulls first
110    /// or last. Not reported by the equality normalizer, which gives a
111    /// null a domain of its own.
112    #[error("the value's code is the one this space keeps for the null")]
113    Reserved,
114}
115
116/// The opt-in table: the domain for a scalar, or `None` for one that has
117/// no term. A logical type decides on its own; a value without one goes by
118/// its kind. Neither looks at the value: [`normalize`] checks that.
119pub fn domain_of(ty: Option<Logical>, v: &Physical<'_>) -> Option<&'static str> {
120    match (ty, v) {
121        (None, Physical::Int(_)) => Some(domain::INTEGER),
122        (None, Physical::Float(_)) => Some(domain::FLOAT),
123        (None, Physical::Text(_)) => Some(domain::TEXT),
124        (None, Physical::Bytes(_)) => Some(domain::BYTES),
125        (None, Physical::Null) => Some(domain::NULL),
126        (None, Physical::Bool(_)) => None,
127        (Some(Logical::Date), _) => Some(domain::DATE),
128        (Some(Logical::Timestamp(_)), _) => Some(domain::TIMESTAMP),
129        (Some(Logical::Decimal { .. }), _) => Some(domain::DECIMAL),
130        (Some(Logical::Uuid), _) => Some(domain::UUID),
131        // `Logical` is non-exhaustive: a type this build does not know, and
132        // any added later, has no term until it is given a domain here.
133        (Some(_), _) => None,
134    }
135}
136
137/// Encode one scalar for equality, with the domain its term belongs to.
138///
139/// Integers of every width share [`domain::INTEGER`]: a sign byte (`0x00`
140/// negative, `0x01` otherwise) then the value as a big-endian 128-bit
141/// two's-complement word, which orders correctly across `i128::MIN` to
142/// `u128::MAX`. Both float widths share [`domain::FLOAT`]: an `f32` widens
143/// exactly to `f64`, so it matches an `f64` only when the numbers are equal
144/// (`f32` 1.5 matches, `f32` 0.1 does not match `f64` 0.1). Float zero
145/// signs and NaN payloads are folded. A timestamp in any unit is truncated
146/// to microseconds. A decimal's scale is normalized, so `1.50` and `1.5`
147/// are one term, as are `-0.00` and `0`. A null is [`domain::NULL`] and
148/// the one byte `0x00`; a typed null is walked as a plain one, so it is
149/// the same term.
150///
151/// # Examples
152///
153/// ```
154/// use vitaminc_aead::Physical;
155/// use vitaminc_normalize::{domain, normalize};
156/// use vitaminc_protected::Controlled;
157///
158/// let null = normalize(None, Physical::Null)?;
159/// assert_eq!(null.domain, domain::NULL);
160/// assert_eq!(null.bytes.risky_ref(), &[0x00]);
161/// # Ok::<(), vitaminc_normalize::NormalizeError>(())
162/// ```
163pub fn normalize(ty: Option<Logical>, v: Physical<'_>) -> Result<Normalized, NormalizeError> {
164    // A value that does not fit its logical type is refused as invalid
165    // before the type is looked up, so `undefined` over an int is `Invalid`
166    // rather than "nothing to index".
167    if ty.is_some_and(|ty| !ty.admits(&v)) {
168        return Err(NormalizeError::Invalid);
169    }
170    let domain = domain_of(ty, &v).ok_or(NormalizeError::NoDomain)?;
171    let bytes = match (ty, v) {
172        (None, Physical::Int(i)) => Some(integer(i)),
173        (None, Physical::Float(f)) => Some(float(f)),
174        (None, Physical::Text(s)) => Some(text(s)?),
175        (None, Physical::Bytes(b)) => Some(bytes(b)),
176        (None, Physical::Null) => Some(null()),
177        (Some(Logical::Date), Physical::Int(days)) => date(days),
178        (Some(Logical::Timestamp(unit)), Physical::Int(count)) => timestamp(unit, count),
179        (Some(Logical::Decimal { scale }), Physical::Int(mantissa)) => decimal(scale, mantissa),
180        (Some(Logical::Uuid), Physical::Bytes(b)) => uuid(b),
181        // `admits` has already refused a logical type over the wrong kind,
182        // and `domain_of` everything without a term. Each encoder checks
183        // its own range as well, for a caller that goes to it directly.
184        _ => None,
185    }
186    .ok_or(NormalizeError::Invalid)?;
187    Ok(Normalized { domain, bytes })
188}
189
190/// An integer of any width: a sign byte, then the big-endian 128-bit word.
191pub fn integer(value: Int) -> Protected<Vec<u8>> {
192    let (negative, word) = value.sign_and_word();
193    fixed::<17>(|b| {
194        b[0] = u8::from(!negative);
195        b[1..].copy_from_slice(&word.to_be_bytes());
196    })
197}
198
199/// A float of either width, as the orderable bytes of its `f64` value with
200/// zero signs and NaN payloads folded.
201pub fn float(value: Float) -> Protected<Vec<u8>> {
202    let value = match value {
203        Float::F32(f) => widen_f32(f),
204        Float::F64(f) => f,
205    };
206    orderable_f64(canonical_f64(value))
207}
208
209/// A date's day count since 1970-01-01, as the orderable bytes of its
210/// count of days from 0001-01-01 (the same bytes chrono's `NaiveDate`
211/// orders by). `None` outside the date range.
212pub fn date(days: Int) -> Option<Protected<Vec<u8>>> {
213    let days = i32::try_from(days.to_i128()?).ok()?;
214    if !(MIN_DATE_DAYS..=MAX_DATE_DAYS).contains(&days) {
215        return None;
216    }
217    let from_ce = days.checked_add(EPOCH_DAYS_FROM_CE)?;
218    Some(fixed(|b| {
219        *b = ((from_ce as u32) ^ (1u32 << 31)).to_be_bytes()
220    }))
221}
222
223/// A count of `unit`s since the Unix epoch, as orderable seconds then
224/// big-endian nanoseconds truncated to microseconds (the same bytes
225/// chrono's `DateTime<Utc>` orders by, truncated). `None` outside the
226/// timestamp range.
227pub fn timestamp(unit: TimeUnit, count: Int) -> Option<Protected<Vec<u8>>> {
228    let nanos = timestamp_nanos(unit, count)?;
229    let seconds = i64::try_from(nanos.div_euclid(NANOS_PER_SECOND)).ok()?;
230    let sub = (nanos.rem_euclid(NANOS_PER_SECOND) as u32) / 1_000 * 1_000;
231    Some(fixed::<12>(|b| {
232        b[..8].copy_from_slice(&((seconds as u64) ^ (1u64 << 63)).to_be_bytes());
233        b[8..].copy_from_slice(&sub.to_be_bytes());
234    }))
235}
236
237/// A decimal's mantissa and scale, as the 14-byte orderable encoding of
238/// its value, ported from the frozen `orderable-bytes` release and checked
239/// against it, which normalizes the scale and a signed zero. `None`
240/// if the mantissa does not fit 96 bits or the scale is above 28, the
241/// range `rust_decimal` represents.
242///
243/// # Encoding (scientific form, base 10)
244///
245/// Each non-zero value is re-expressed in signed scientific form:
246///
247/// ```text
248/// value = ±significand × 10^(leading_exp − digits(significand) + 1)
249/// ```
250///
251/// where `significand` is the mantissa with trailing zeros stripped and
252/// `leading_exp` is the decimal exponent of the leading significant digit,
253/// in `[-28, 28]`. The 14 bytes are:
254///
255/// ```text
256/// byte 0       : [sign:1][biased_exp:7]
257/// bytes 1..=13 : padded_significand (104 bits, big-endian)
258/// ```
259///
260/// - The sign bit is `1` for positive and zero, `0` for negative, so every
261///   negative sorts below zero and zero below every positive.
262/// - The biased exponent is `leading_exp + 64`, in `[36, 92]` for a
263///   non-zero value. Zero uses `0`, which no non-zero value reaches.
264/// - The padded significand is the significand left-justified to 29
265///   digits by appending zeros, so values at one exponent compare
266///   byte-wise however many digits they have. A 96-bit mantissa has at
267///   most 29 digits, so the padded value fits 104 bits.
268///
269/// For a negative, the exponent bits and the significand are inverted, so
270/// a larger magnitude sorts lower. `1`, `1.0` and `1.00` reduce to the
271/// same significand and exponent, and `+0` and `-0` to the same zero, so
272/// equal values are equal bytes, as `rust_decimal` compares them.
273pub fn decimal(scale: u8, mantissa: Int) -> Option<Protected<Vec<u8>>> {
274    crate::decimal::orderable(mantissa.to_i128()?, scale)
275}
276
277/// NFC in the pinned Unicode version, without case or accent folding.
278///
279/// The result is written into one protected buffer sized exactly up front,
280/// so it never reallocates and frees an unwiped partial copy.
281///
282/// The `unicode-normalization` iterators keep pending code points in small
283/// buffers of their own, with room for four code points inside the
284/// iterator. Decomposition holds the combining marks that follow a
285/// character in the same buffer as the decomposed form of the next
286/// character, so the buffer moves to the heap as soon as those together
287/// pass four code points: four marks followed by any character is enough.
288/// Composition moves its buffer to the heap when five marks after one
289/// character do not combine with it. Once a buffer is on the heap it stays
290/// there for the rest of the string, so every later character passes
291/// through it, and it is freed without being wiped. That is out of this
292/// crate's control (see #488).
293pub fn text(text: &str) -> Result<Protected<Vec<u8>>, NormalizeError> {
294    if text
295        .chars()
296        .any(|c| get_general_category(c) == GeneralCategory::Unassigned)
297    {
298        return Err(NormalizeError::UnassignedCodePoint);
299    }
300    // NFC can grow UTF-8 (U+0344 is two bytes, its NFC form four), so measure
301    // the output first rather than sizing from the input.
302    let len = text.nfc().map(char::len_utf8).sum();
303    let mut result = Protected::new(Vec::with_capacity(len));
304    for c in text.nfc() {
305        let mut encoded = Protected::new([0u8; 4]);
306        result
307            .risky_inner_mut()
308            .extend_from_slice(c.encode_utf8(encoded.risky_inner_mut()).as_bytes());
309    }
310    Ok(result)
311}
312
313/// Bytes, as they are.
314pub fn bytes(value: &[u8]) -> Protected<Vec<u8>> {
315    Protected::new(value.to_vec())
316}
317
318/// A null, typed or plain: the one byte `0x00`, under [`domain::NULL`].
319/// Every null is this one term, whatever column it sits in, as SQL says
320/// `NULL IS NULL` whatever the column. Spaces that must not tie already
321/// differ in context.
322pub fn null() -> Protected<Vec<u8>> {
323    fixed::<1>(|b| *b = [0x00])
324}
325
326/// A uuid's sixteen raw bytes. `None` for any other length.
327pub fn uuid(value: &[u8]) -> Option<Protected<Vec<u8>>> {
328    (value.len() == 16).then(|| Protected::new(value.to_vec()))
329}
330
331/// A fixed-width encoding, written by `write` straight into the protected
332/// vector that is returned.
333///
334/// The vector is allocated once at its final size, so it never
335/// reallocates, and `write` fills it in place. No separate array of the
336/// encoding is made and copied, so there is no extra copy to wipe. The
337/// compiler can still make short-lived temporaries while `write` runs, for
338/// example the array that `to_be_bytes` returns, and Rust cannot promise
339/// to wipe those.
340///
341/// `vitaminc-ore` has its own private copy of this function rather than
342/// sharing this one, so it stays out of this crate's public API.
343pub(crate) fn fixed<const N: usize>(mut write: impl FnMut(&mut [u8; N])) -> Protected<Vec<u8>> {
344    let mut bytes = Protected::new(vec![0u8; N]);
345    bytes.update(|bytes| {
346        let array = <&mut [u8; N]>::try_from(bytes.as_mut_slice())
347            .expect("the vector was made with exactly N bytes");
348        write(array);
349    });
350    bytes
351}
352
353/// Days from 0001-01-01 (day 1, chrono's "days from CE") to 1970-01-01.
354const EPOCH_DAYS_FROM_CE: i32 = 719_163;
355const NANOS_PER_SECOND: i128 = 1_000_000_000;
356
357/// The orderable bytes of an `f64`: negatives flip every bit, positives
358/// the sign bit, so the bytes sort as the numbers do. Expects a value
359/// [`canonical_f64`] has already folded.
360///
361/// The bytes are written straight into the protected vector that is
362/// returned (see [`fixed`]).
363fn orderable_f64(value: f64) -> Protected<Vec<u8>> {
364    let bits = value.to_bits();
365    let sign_extension = ((bits as i64) >> 63) as u64;
366    fixed(|b| *b = (bits ^ (sign_extension | (1u64 << 63))).to_be_bytes())
367}
368
369/// `f64::from(f32)` in integer operations only. A hardware conversion can
370/// take a slow path on subnormal inputs, which would leak timing; this keeps
371/// the float path free of floating-point arithmetic.
372fn widen_f32(value: f32) -> f64 {
373    let bits = value.to_bits();
374    let sign = u64::from(bits >> 31) << 63;
375    let exponent = (bits >> 23) & 0xff;
376    let mantissa = u64::from(bits & 0x7f_ffff);
377    let zero_exponent = 0u64.wrapping_sub((exponent == 0) as u64);
378    let max_exponent = 0u64.wrapping_sub((exponent == 0xff) as u64);
379    let nonzero = 0u64.wrapping_sub((mantissa != 0) as u64);
380    // Normal, infinite and NaN: rebias the exponent (127 to 1023, or all
381    // ones stays all ones) and left-align the mantissa.
382    let rebiased = ((u64::from(exponent) + 896) & !max_exponent) | (0x7ff & max_exponent);
383    let normal = (rebiased << 52) | (mantissa << 29);
384    // Subnormal: the highest set bit becomes the implicit leading one.
385    let top = highest_set_bit(mantissa);
386    let subnormal = ((top + 874) << 52) | ((mantissa << (52 - top)) & ((1 << 52) - 1));
387    f64::from_bits(sign | (normal & !zero_exponent) | (subnormal & zero_exponent & nonzero))
388}
389
390/// Index of the highest set bit of a subnormal f32 mantissa. `| 1` keeps the
391/// result in range for a zero mantissa, which `widen_f32` masks out.
392///
393/// Kept out of `widen_f32` so the `.cargo/mutants.toml` exclusion of that
394/// function's equivalent `|`-to-`^` mutants cannot reach this `|`, whose
395/// operands overlap and whose mutant a test does catch.
396fn highest_set_bit(mantissa: u64) -> u64 {
397    u64::from(63 - (mantissa | 1).leading_zeros())
398}
399
400/// Folds every NaN to one quiet NaN and `-0.0` to `0.0`, in integer
401/// operations only.
402fn canonical_f64(value: f64) -> f64 {
403    let bits = value.to_bits();
404    let magnitude = bits & 0x7fff_ffff_ffff_ffff;
405    let nan = 0u64.wrapping_sub((magnitude > 0x7ff0_0000_0000_0000) as u64);
406    let nonzero = 0u64.wrapping_sub((magnitude != 0) as u64);
407    f64::from_bits((bits & !nan).wrapping_add(0x7ff8_0000_0000_0000 & nan) & nonzero)
408}
409
410#[cfg(test)]
411#[path = "scalar_tests.rs"]
412mod tests;