use unicode_general_category::{get_general_category, GeneralCategory};
use unicode_normalization::UnicodeNormalization;
use vitaminc_aead::plaintext::kinds::{timestamp_nanos, MAX_DATE_DAYS, MIN_DATE_DAYS};
use vitaminc_aead::{Float, Int, Logical, Physical, TimeUnit};
use vitaminc_protected::{Controlled, OpaqueDebug, Protected};
pub mod domain {
pub const INTEGER: &str = "vitaminc/prf/value/integer-orderable/v1";
pub const FLOAT: &str = "vitaminc/prf/value/float-orderable/v1";
pub const DATE: &str = "vitaminc/prf/value/date-orderable/v1";
pub const TIMESTAMP: &str = "vitaminc/prf/value/timestamp-micros-orderable/v1";
pub const DECIMAL: &str = "vitaminc/prf/value/decimal-orderable/v1";
pub const TEXT: &str = "vitaminc/prf/value/text-nfc/unicode-16/v1";
pub const BYTES: &str = "vitaminc/prf/value/bytes/v1";
pub const UUID: &str = "vitaminc/prf/value/uuid/v1";
pub const ALL: &[&str] = &[INTEGER, FLOAT, DATE, TIMESTAMP, DECIMAL, TEXT, BYTES, UUID];
}
#[derive(OpaqueDebug)]
pub struct Normalized {
#[non_sensitive]
pub domain: &'static str,
pub bytes: Protected<Vec<u8>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum NormalizeError {
#[error("no domain for this kind of value")]
NoDomain,
#[error("the value is not valid for its logical type")]
Invalid,
#[error("text contains a code point unassigned in Unicode 16")]
UnassignedCodePoint,
#[error("the value is not of the kind the index declares")]
Mismatch,
#[error("the value does not fit the type the index declares")]
OutOfRange,
}
pub fn domain_of(ty: Option<Logical>, v: &Physical<'_>) -> Option<&'static str> {
match (ty, v) {
(None, Physical::Int(_)) => Some(domain::INTEGER),
(None, Physical::Float(_)) => Some(domain::FLOAT),
(None, Physical::Text(_)) => Some(domain::TEXT),
(None, Physical::Bytes(_)) => Some(domain::BYTES),
(None, Physical::Null | Physical::Bool(_)) => None,
(Some(Logical::Date), _) => Some(domain::DATE),
(Some(Logical::Timestamp(_)), _) => Some(domain::TIMESTAMP),
(Some(Logical::Decimal { .. }), _) => Some(domain::DECIMAL),
(Some(Logical::Uuid), _) => Some(domain::UUID),
(Some(_), _) => None,
}
}
pub fn normalize(ty: Option<Logical>, v: Physical<'_>) -> Result<Normalized, NormalizeError> {
if ty.is_some_and(|ty| !ty.admits(&v)) {
return Err(NormalizeError::Invalid);
}
let domain = domain_of(ty, &v).ok_or(NormalizeError::NoDomain)?;
let bytes = match (ty, v) {
(None, Physical::Int(i)) => Some(integer(i)),
(None, Physical::Float(f)) => Some(float(f)),
(None, Physical::Text(s)) => Some(text(s)?),
(None, Physical::Bytes(b)) => Some(bytes(b)),
(Some(Logical::Date), Physical::Int(days)) => date(days),
(Some(Logical::Timestamp(unit)), Physical::Int(count)) => timestamp(unit, count),
(Some(Logical::Decimal { scale }), Physical::Int(mantissa)) => decimal(scale, mantissa),
(Some(Logical::Uuid), Physical::Bytes(b)) => uuid(b),
_ => None,
}
.ok_or(NormalizeError::Invalid)?;
Ok(Normalized { domain, bytes })
}
pub fn integer(value: Int) -> Protected<Vec<u8>> {
let (negative, word) = value.sign_and_word();
let word = Protected::new(word.to_be_bytes());
let mut bytes = Protected::new(Vec::with_capacity(17));
bytes.risky_inner_mut().push(u8::from(!negative));
bytes.risky_inner_mut().extend_from_slice(word.risky_ref());
bytes
}
pub fn float(value: Float) -> Protected<Vec<u8>> {
let value = match value {
Float::F32(f) => widen_f32(f),
Float::F64(f) => f,
};
Protected::new(orderable_f64(canonical_f64(value)).to_vec())
}
pub fn date(days: Int) -> Option<Protected<Vec<u8>>> {
let days = i32::try_from(days.to_i128()?).ok()?;
if !(MIN_DATE_DAYS..=MAX_DATE_DAYS).contains(&days) {
return None;
}
let from_ce = days.checked_add(EPOCH_DAYS_FROM_CE)?;
Some(Protected::new(
((from_ce as u32) ^ (1u32 << 31)).to_be_bytes().to_vec(),
))
}
pub fn timestamp(unit: TimeUnit, count: Int) -> Option<Protected<Vec<u8>>> {
let nanos = timestamp_nanos(unit, count)?;
let seconds = i64::try_from(nanos.div_euclid(NANOS_PER_SECOND)).ok()?;
let sub = (nanos.rem_euclid(NANOS_PER_SECOND) as u32) / 1_000 * 1_000;
let mut bytes = Protected::new(Vec::with_capacity(12));
bytes
.risky_inner_mut()
.extend_from_slice(&((seconds as u64) ^ (1u64 << 63)).to_be_bytes());
bytes
.risky_inner_mut()
.extend_from_slice(&sub.to_be_bytes());
Some(bytes)
}
pub fn decimal(scale: u8, mantissa: Int) -> Option<Protected<Vec<u8>>> {
let bytes = Protected::new(crate::decimal::orderable(mantissa.to_i128()?, scale)?);
Some(Protected::new(bytes.risky_ref().to_vec()))
}
pub fn text(text: &str) -> Result<Protected<Vec<u8>>, NormalizeError> {
if text
.chars()
.any(|c| get_general_category(c) == GeneralCategory::Unassigned)
{
return Err(NormalizeError::UnassignedCodePoint);
}
let len = text.nfc().map(char::len_utf8).sum();
let mut result = Protected::new(Vec::with_capacity(len));
for c in text.nfc() {
let mut encoded = Protected::new([0u8; 4]);
result
.risky_inner_mut()
.extend_from_slice(c.encode_utf8(encoded.risky_inner_mut()).as_bytes());
}
Ok(result)
}
pub fn bytes(value: &[u8]) -> Protected<Vec<u8>> {
Protected::new(value.to_vec())
}
pub fn uuid(value: &[u8]) -> Option<Protected<Vec<u8>>> {
(value.len() == 16).then(|| Protected::new(value.to_vec()))
}
const EPOCH_DAYS_FROM_CE: i32 = 719_163;
const NANOS_PER_SECOND: i128 = 1_000_000_000;
fn orderable_f64(value: f64) -> [u8; 8] {
let bits = value.to_bits();
let sign_extension = ((bits as i64) >> 63) as u64;
(bits ^ (sign_extension | (1u64 << 63))).to_be_bytes()
}
fn widen_f32(value: f32) -> f64 {
let bits = value.to_bits();
let sign = u64::from(bits >> 31) << 63;
let exponent = (bits >> 23) & 0xff;
let mantissa = u64::from(bits & 0x7f_ffff);
let zero_exponent = 0u64.wrapping_sub((exponent == 0) as u64);
let max_exponent = 0u64.wrapping_sub((exponent == 0xff) as u64);
let nonzero = 0u64.wrapping_sub((mantissa != 0) as u64);
let rebiased = ((u64::from(exponent) + 896) & !max_exponent) | (0x7ff & max_exponent);
let normal = (rebiased << 52) | (mantissa << 29);
let top = highest_set_bit(mantissa);
let subnormal = ((top + 874) << 52) | ((mantissa << (52 - top)) & ((1 << 52) - 1));
f64::from_bits(sign | (normal & !zero_exponent) | (subnormal & zero_exponent & nonzero))
}
fn highest_set_bit(mantissa: u64) -> u64 {
u64::from(63 - (mantissa | 1).leading_zeros())
}
fn canonical_f64(value: f64) -> f64 {
let bits = value.to_bits();
let magnitude = bits & 0x7fff_ffff_ffff_ffff;
let nan = 0u64.wrapping_sub((magnitude > 0x7ff0_0000_0000_0000) as u64);
let nonzero = 0u64.wrapping_sub((magnitude != 0) as u64);
f64::from_bits((bits & !nan).wrapping_add(0x7ff8_0000_0000_0000 & nan) & nonzero)
}
#[cfg(test)]
#[path = "scalar_tests.rs"]
mod tests;