use vitaminc_aead::{Encrypt, Int, Logical, Physical};
use vitaminc_normalize::{terms_by, Normalize, NormalizeError, Term, TermsError};
use vitaminc_protected::{Controlled, Protected};
use crate::{fold, OrderPlaintext};
pub const VARIABLE_WIDTH_MARKER: u8 = 0x01;
pub mod domain {
pub const INT16: &str = "vitaminc/ore/value/int16/v1";
pub const UINT16: &str = "vitaminc/ore/value/uint16/v1";
pub const INT32: &str = "vitaminc/ore/value/int32/v1";
pub const UINT32: &str = "vitaminc/ore/value/uint32/v1";
pub const INT64: &str = "vitaminc/ore/value/int64/v1";
pub const UINT64: &str = "vitaminc/ore/value/uint64/v1";
pub const INT128: &str = "vitaminc/ore/value/int128/v1";
pub const UINT128: &str = "vitaminc/ore/value/uint128/v1";
pub const FLOAT64: &str = "vitaminc/ore/value/float64/v1";
pub const DATE: &str = "vitaminc/ore/value/date/v1";
pub const TIMESTAMP: &str = "vitaminc/ore/value/timestamp-micros/v1";
pub const DECIMAL: &str = "vitaminc/ore/value/decimal/v1";
pub const TEXT: &str = "vitaminc/ore/value/text-folded/unicode-16/v1";
pub const BYTES: &str = "vitaminc/ore/value/bytes/v1";
pub const UUID: &str = "vitaminc/ore/value/uuid/v1";
pub const INT16_NULLS_FIRST: &str = "vitaminc/ore/value/int16/nulls-first/v1";
pub const INT16_NULLS_LAST: &str = "vitaminc/ore/value/int16/nulls-last/v1";
pub const UINT16_NULLS_FIRST: &str = "vitaminc/ore/value/uint16/nulls-first/v1";
pub const UINT16_NULLS_LAST: &str = "vitaminc/ore/value/uint16/nulls-last/v1";
pub const INT32_NULLS_FIRST: &str = "vitaminc/ore/value/int32/nulls-first/v1";
pub const INT32_NULLS_LAST: &str = "vitaminc/ore/value/int32/nulls-last/v1";
pub const UINT32_NULLS_FIRST: &str = "vitaminc/ore/value/uint32/nulls-first/v1";
pub const UINT32_NULLS_LAST: &str = "vitaminc/ore/value/uint32/nulls-last/v1";
pub const INT64_NULLS_FIRST: &str = "vitaminc/ore/value/int64/nulls-first/v1";
pub const INT64_NULLS_LAST: &str = "vitaminc/ore/value/int64/nulls-last/v1";
pub const UINT64_NULLS_FIRST: &str = "vitaminc/ore/value/uint64/nulls-first/v1";
pub const UINT64_NULLS_LAST: &str = "vitaminc/ore/value/uint64/nulls-last/v1";
pub const INT128_NULLS_FIRST: &str = "vitaminc/ore/value/int128/nulls-first/v1";
pub const INT128_NULLS_LAST: &str = "vitaminc/ore/value/int128/nulls-last/v1";
pub const UINT128_NULLS_FIRST: &str = "vitaminc/ore/value/uint128/nulls-first/v1";
pub const UINT128_NULLS_LAST: &str = "vitaminc/ore/value/uint128/nulls-last/v1";
pub const FLOAT64_NULLS_FIRST: &str = "vitaminc/ore/value/float64/nulls-first/v1";
pub const FLOAT64_NULLS_LAST: &str = "vitaminc/ore/value/float64/nulls-last/v1";
pub const DATE_NULLS_FIRST: &str = "vitaminc/ore/value/date/nulls-first/v1";
pub const DATE_NULLS_LAST: &str = "vitaminc/ore/value/date/nulls-last/v1";
pub const TIMESTAMP_NULLS_FIRST: &str = "vitaminc/ore/value/timestamp-micros/nulls-first/v1";
pub const TIMESTAMP_NULLS_LAST: &str = "vitaminc/ore/value/timestamp-micros/nulls-last/v1";
pub const DECIMAL_NULLS_FIRST: &str = "vitaminc/ore/value/decimal/nulls-first/v1";
pub const DECIMAL_NULLS_LAST: &str = "vitaminc/ore/value/decimal/nulls-last/v1";
pub const UUID_NULLS_FIRST: &str = "vitaminc/ore/value/uuid/nulls-first/v1";
pub const UUID_NULLS_LAST: &str = "vitaminc/ore/value/uuid/nulls-last/v1";
pub const ALL: &[&str] = &[
INT16,
UINT16,
INT32,
UINT32,
INT64,
UINT64,
INT128,
UINT128,
FLOAT64,
DATE,
TIMESTAMP,
DECIMAL,
TEXT,
BYTES,
UUID,
INT16_NULLS_FIRST,
UINT16_NULLS_FIRST,
INT32_NULLS_FIRST,
UINT32_NULLS_FIRST,
INT64_NULLS_FIRST,
UINT64_NULLS_FIRST,
INT128_NULLS_FIRST,
UINT128_NULLS_FIRST,
FLOAT64_NULLS_FIRST,
DATE_NULLS_FIRST,
TIMESTAMP_NULLS_FIRST,
DECIMAL_NULLS_FIRST,
UUID_NULLS_FIRST,
INT16_NULLS_LAST,
UINT16_NULLS_LAST,
INT32_NULLS_LAST,
UINT32_NULLS_LAST,
INT64_NULLS_LAST,
UINT64_NULLS_LAST,
INT128_NULLS_LAST,
UINT128_NULLS_LAST,
FLOAT64_NULLS_LAST,
DATE_NULLS_LAST,
TIMESTAMP_NULLS_LAST,
DECIMAL_NULLS_LAST,
UUID_NULLS_LAST,
];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum NullOrder {
First,
Last,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum OrderTarget {
I16,
U16,
I32,
U32,
I64,
U64,
I128,
U128,
F64,
Date,
Timestamp,
Decimal,
Text,
Bytes,
Uuid,
}
impl OrderTarget {
pub const ALL: &[OrderTarget] = &[
Self::I16,
Self::U16,
Self::I32,
Self::U32,
Self::I64,
Self::U64,
Self::I128,
Self::U128,
Self::F64,
Self::Date,
Self::Timestamp,
Self::Decimal,
Self::Text,
Self::Bytes,
Self::Uuid,
];
pub fn domain(self) -> &'static str {
match self {
Self::I16 => domain::INT16,
Self::U16 => domain::UINT16,
Self::I32 => domain::INT32,
Self::U32 => domain::UINT32,
Self::I64 => domain::INT64,
Self::U64 => domain::UINT64,
Self::I128 => domain::INT128,
Self::U128 => domain::UINT128,
Self::F64 => domain::FLOAT64,
Self::Date => domain::DATE,
Self::Timestamp => domain::TIMESTAMP,
Self::Decimal => domain::DECIMAL,
Self::Text => domain::TEXT,
Self::Bytes => domain::BYTES,
Self::Uuid => domain::UUID,
}
}
pub fn domain_with_nulls(self, nulls: NullOrder) -> Option<&'static str> {
Some(match (self, nulls) {
(Self::I16, NullOrder::First) => domain::INT16_NULLS_FIRST,
(Self::I16, NullOrder::Last) => domain::INT16_NULLS_LAST,
(Self::U16, NullOrder::First) => domain::UINT16_NULLS_FIRST,
(Self::U16, NullOrder::Last) => domain::UINT16_NULLS_LAST,
(Self::I32, NullOrder::First) => domain::INT32_NULLS_FIRST,
(Self::I32, NullOrder::Last) => domain::INT32_NULLS_LAST,
(Self::U32, NullOrder::First) => domain::UINT32_NULLS_FIRST,
(Self::U32, NullOrder::Last) => domain::UINT32_NULLS_LAST,
(Self::I64, NullOrder::First) => domain::INT64_NULLS_FIRST,
(Self::I64, NullOrder::Last) => domain::INT64_NULLS_LAST,
(Self::U64, NullOrder::First) => domain::UINT64_NULLS_FIRST,
(Self::U64, NullOrder::Last) => domain::UINT64_NULLS_LAST,
(Self::I128, NullOrder::First) => domain::INT128_NULLS_FIRST,
(Self::I128, NullOrder::Last) => domain::INT128_NULLS_LAST,
(Self::U128, NullOrder::First) => domain::UINT128_NULLS_FIRST,
(Self::U128, NullOrder::Last) => domain::UINT128_NULLS_LAST,
(Self::F64, NullOrder::First) => domain::FLOAT64_NULLS_FIRST,
(Self::F64, NullOrder::Last) => domain::FLOAT64_NULLS_LAST,
(Self::Date, NullOrder::First) => domain::DATE_NULLS_FIRST,
(Self::Date, NullOrder::Last) => domain::DATE_NULLS_LAST,
(Self::Timestamp, NullOrder::First) => domain::TIMESTAMP_NULLS_FIRST,
(Self::Timestamp, NullOrder::Last) => domain::TIMESTAMP_NULLS_LAST,
(Self::Decimal, NullOrder::First) => domain::DECIMAL_NULLS_FIRST,
(Self::Decimal, NullOrder::Last) => domain::DECIMAL_NULLS_LAST,
(Self::Uuid, NullOrder::First) => domain::UUID_NULLS_FIRST,
(Self::Uuid, NullOrder::Last) => domain::UUID_NULLS_LAST,
(Self::Text | Self::Bytes, _) => return None,
})
}
pub fn width(self) -> Option<usize> {
match self {
Self::I16 | Self::U16 => Some(2),
Self::I32 | Self::U32 | Self::Date => Some(4),
Self::I64 | Self::U64 | Self::F64 => Some(8),
Self::Timestamp => Some(12),
Self::Decimal => Some(14),
Self::I128 | Self::U128 | Self::Uuid => Some(16),
Self::Text | Self::Bytes => None,
}
}
}
pub fn order(
target: OrderTarget,
ty: Option<Logical>,
v: Physical<'_>,
) -> Result<OrderPlaintext, NormalizeError> {
encode(target, None, ty, v)
}
pub fn order_with_nulls(
target: OrderTarget,
nulls: NullOrder,
ty: Option<Logical>,
v: Physical<'_>,
) -> Result<OrderPlaintext, NormalizeError> {
encode(target, Some(nulls), ty, v)
}
pub(crate) fn label(target: OrderTarget, nulls: Option<NullOrder>) -> Option<&'static str> {
match nulls {
None => Some(target.domain()),
Some(nulls) => target.domain_with_nulls(nulls),
}
}
pub(crate) fn null_plaintext(target: OrderTarget, nulls: NullOrder) -> Option<OrderPlaintext> {
let domain = target.domain_with_nulls(nulls)?;
let width = target.width()?;
Some(OrderPlaintext::new(
target,
Some(nulls),
domain,
sentinel(width, nulls),
))
}
pub(crate) fn encode(
target: OrderTarget,
nulls: Option<NullOrder>,
ty: Option<Logical>,
v: Physical<'_>,
) -> Result<OrderPlaintext, NormalizeError> {
use OrderTarget as T;
if ty.is_some_and(|ty| !ty.admits(&v)) {
return Err(NormalizeError::Invalid);
}
let domain = label(target, nulls).ok_or(NormalizeError::NoDomain)?;
let bytes = match (target, ty, v) {
(_, None, Physical::Null) => {
return match nulls.and_then(|nulls| null_plaintext(target, nulls)) {
Some(sentinel) => Ok(sentinel),
None => Err(NormalizeError::NoDomain),
}
}
(_, None, Physical::Bool(_)) | (_, Some(Logical::Undefined), _) => {
return Err(NormalizeError::NoDomain)
}
(_, Some(Logical::Unknown(_)), _) => return Err(NormalizeError::NoDomain),
(target, None, Physical::Int(i)) if target.is_int() => {
int(convert(target, i).ok_or(NormalizeError::OutOfRange)?)
}
(T::F64, None, Physical::Float(f)) => vitaminc_normalize::float(f),
(T::Text, None, Physical::Text(s)) => text(s)?,
(T::Bytes, None, Physical::Bytes(b)) => variable_width(b),
(T::Date, Some(Logical::Date), Physical::Int(days)) => {
vitaminc_normalize::date(days).ok_or(NormalizeError::Invalid)?
}
(T::Timestamp, Some(Logical::Timestamp(unit)), Physical::Int(count)) => {
vitaminc_normalize::timestamp(unit, count).ok_or(NormalizeError::Invalid)?
}
(T::Decimal, Some(Logical::Decimal { scale }), Physical::Int(mantissa)) => {
vitaminc_normalize::decimal(scale, mantissa).ok_or(NormalizeError::Invalid)?
}
(T::Uuid, Some(Logical::Uuid), Physical::Bytes(b)) => {
vitaminc_normalize::uuid(b).ok_or(NormalizeError::Invalid)?
}
_ => return Err(NormalizeError::Mismatch),
};
let bytes = match nulls {
None => bytes,
Some(nulls) => make_room(target, nulls, bytes)?,
};
Ok(OrderPlaintext::new(target, nulls, domain, bytes))
}
fn sentinel(width: usize, nulls: NullOrder) -> Protected<Vec<u8>> {
let fill = match nulls {
NullOrder::First => 0x00,
NullOrder::Last => 0xFF,
};
Protected::new(vec![fill; width])
}
fn make_room(
target: OrderTarget,
nulls: NullOrder,
mut code: Protected<Vec<u8>>,
) -> Result<Protected<Vec<u8>>, NormalizeError> {
if target.owns_every_code() {
if every_byte_is(code.risky_ref(), 0xFF) {
return Err(NormalizeError::Reserved);
}
if nulls == NullOrder::First {
code.update(|code| increment(code));
}
return Ok(code);
}
let reserved = match nulls {
NullOrder::First => 0x00,
NullOrder::Last => 0xFF,
};
if every_byte_is(code.risky_ref(), reserved) {
return Err(NormalizeError::Reserved);
}
Ok(code)
}
fn every_byte_is(code: &[u8], byte: u8) -> bool {
code.iter().fold(0u8, |differ, b| differ | (b ^ byte)) == 0
}
fn increment(code: &mut [u8]) {
let mut carry = 1u8;
for byte in code.iter_mut().rev() {
let (sum, over) = byte.overflowing_add(carry);
*byte = sum;
carry = u8::from(over);
}
}
impl OrderTarget {
fn owns_every_code(self) -> bool {
self.is_int() || self == Self::Uuid
}
fn is_int(self) -> bool {
matches!(
self,
Self::I16
| Self::U16
| Self::I32
| Self::U32
| Self::I64
| Self::U64
| Self::I128
| Self::U128
)
}
}
fn convert(target: OrderTarget, value: Int) -> Option<Int> {
let Some(v) = value.to_i128() else {
return match (target, value) {
(OrderTarget::U128, Int::U128(v)) => Some(Int::U128(v)),
_ => None,
};
};
match target {
OrderTarget::I16 => i16::try_from(v).ok().map(Int::I16),
OrderTarget::U16 => u16::try_from(v).ok().map(Int::U16),
OrderTarget::I32 => i32::try_from(v).ok().map(Int::I32),
OrderTarget::U32 => u32::try_from(v).ok().map(Int::U32),
OrderTarget::I64 => i64::try_from(v).ok().map(Int::I64),
OrderTarget::U64 => u64::try_from(v).ok().map(Int::U64),
OrderTarget::I128 => Some(Int::I128(v)),
OrderTarget::U128 => u128::try_from(v).ok().map(Int::U128),
_ => None,
}
}
fn int(value: Int) -> Protected<Vec<u8>> {
match value {
Int::I8(_) | Int::U8(_) => unreachable!("convert widens every 8-bit value to its target"),
Int::I16(v) => fixed(|b| *b = ((v as u16) ^ (1 << 15)).to_be_bytes()),
Int::U16(v) => fixed(|b| *b = v.to_be_bytes()),
Int::I32(v) => fixed(|b| *b = ((v as u32) ^ (1 << 31)).to_be_bytes()),
Int::U32(v) => fixed(|b| *b = v.to_be_bytes()),
Int::I64(v) => fixed(|b| *b = ((v as u64) ^ (1 << 63)).to_be_bytes()),
Int::U64(v) => fixed(|b| *b = v.to_be_bytes()),
Int::I128(v) => fixed(|b| *b = ((v as u128) ^ (1 << 127)).to_be_bytes()),
Int::U128(v) => fixed(|b| *b = v.to_be_bytes()),
}
}
pub fn text(text: &str) -> Result<Protected<Vec<u8>>, NormalizeError> {
let folded = fold::folded(text)?;
Ok(variable_width(folded.risky_ref()))
}
fn variable_width(bytes: &[u8]) -> Protected<Vec<u8>> {
let mut out = Protected::new(Vec::with_capacity(bytes.len() + 1));
out.risky_inner_mut().push(VARIABLE_WIDTH_MARKER);
out.risky_inner_mut().extend_from_slice(bytes);
out
}
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
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Order {
target: OrderTarget,
nulls: Option<NullOrder>,
}
impl Order {
pub const fn new(target: OrderTarget) -> Self {
Self {
target,
nulls: None,
}
}
pub fn nulls(self, nulls: NullOrder) -> Result<Self, NormalizeError> {
self.target
.domain_with_nulls(nulls)
.map(|_| Self {
nulls: Some(nulls),
..self
})
.ok_or(NormalizeError::NoDomain)
}
pub fn target(self) -> OrderTarget {
self.target
}
pub fn null_order(self) -> Option<NullOrder> {
self.nulls
}
}
impl Normalize for Order {
type Term = OrderPlaintext;
const INDEX: &'static str = "order";
fn normalize(
&self,
ty: Option<Logical>,
value: Physical<'_>,
) -> Result<OrderPlaintext, NormalizeError> {
encode(self.target, self.nulls, ty, value)
}
}
pub fn order_terms<P: Encrypt>(
target: OrderTarget,
value: P,
) -> Result<Vec<Term<OrderPlaintext>>, TermsError> {
terms_by(Order::new(target), value)
}
#[cfg(test)]
#[path = "order_tests.rs"]
mod tests;