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 NULL: &str = "vitaminc/prf/value/null/v1";
pub const ALL: &[&str] = &[
INTEGER, FLOAT, DATE, TIMESTAMP, DECIMAL, TEXT, BYTES, UUID, NULL,
];
}
#[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,
#[error("the value's code is the one this space keeps for the null")]
Reserved,
}
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) => Some(domain::NULL),
(None, 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)),
(None, Physical::Null) => Some(null()),
(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();
fixed::<17>(|b| {
b[0] = u8::from(!negative);
b[1..].copy_from_slice(&word.to_be_bytes());
})
}
pub fn float(value: Float) -> Protected<Vec<u8>> {
let value = match value {
Float::F32(f) => widen_f32(f),
Float::F64(f) => f,
};
orderable_f64(canonical_f64(value))
}
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(fixed(|b| {
*b = ((from_ce as u32) ^ (1u32 << 31)).to_be_bytes()
}))
}
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;
Some(fixed::<12>(|b| {
b[..8].copy_from_slice(&((seconds as u64) ^ (1u64 << 63)).to_be_bytes());
b[8..].copy_from_slice(&sub.to_be_bytes());
}))
}
pub fn decimal(scale: u8, mantissa: Int) -> Option<Protected<Vec<u8>>> {
crate::decimal::orderable(mantissa.to_i128()?, scale)
}
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 null() -> Protected<Vec<u8>> {
fixed::<1>(|b| *b = [0x00])
}
pub fn uuid(value: &[u8]) -> Option<Protected<Vec<u8>>> {
(value.len() == 16).then(|| Protected::new(value.to_vec()))
}
pub(crate) fn fixed<const N: usize>(mut write: impl FnMut(&mut [u8; N])) -> Protected<Vec<u8>> {
let mut bytes = Protected::new(vec![0u8; N]);
bytes.update(|bytes| {
let array = <&mut [u8; N]>::try_from(bytes.as_mut_slice())
.expect("the vector was made with exactly N bytes");
write(array);
});
bytes
}
const EPOCH_DAYS_FROM_CE: i32 = 719_163;
const NANOS_PER_SECOND: i128 = 1_000_000_000;
fn orderable_f64(value: f64) -> Protected<Vec<u8>> {
let bits = value.to_bits();
let sign_extension = ((bits as i64) >> 63) as u64;
fixed(|b| *b = (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;