mod clock;
mod direct;
mod execution_error;
mod fingerprint;
mod shared;
mod state;
pub use clock::PermitClock;
pub use direct::{CostPermit, DestructivePermit, MutationPermit};
pub use execution_error::PermitExecutionError;
pub use fingerprint::{
CanonicalPlanFingerprint, PlanConfirmation, PlanFingerprintBuildError, PlanFingerprintDigest,
PlanFingerprintRef, PlanSubject, build_canonical_plan, build_plan_digest,
};
pub use shared::{
SharedCostPermit, SharedDestructivePermit, SharedMutationPermit, SharedPermitState,
};
pub use state::PermitAttempt;
use core::fmt;
use subtle::{Choice, ConstantTimeEq};
pub const MAX_PLAN_SCOPE_BYTES: usize = 1024;
pub const MIN_PERMIT_IDEMPOTENCY_BYTES: usize = 16;
pub const MAX_PERMIT_IDEMPOTENCY_BYTES: usize = 64;
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum PermitScope {
Mutation,
Destructive,
Cost,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PlanChange {
NoOp,
ChangesState,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ReplayPolicy {
SingleAttempt,
RecoverNotSent,
ReconcileThenRetry,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AttemptBudgetError {
Zero,
}
impl_static_error!(AttemptBudgetError, Self::Zero => "permit attempt budget must be nonzero");
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct AttemptBudget(u16);
impl AttemptBudget {
pub const fn new(value: u16) -> Result<Self, AttemptBudgetError> {
if value == 0 {
return Err(AttemptBudgetError::Zero);
}
Ok(Self(value))
}
#[must_use]
pub const fn get(self) -> u16 {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct PermitTimestamp(u64);
impl PermitTimestamp {
#[must_use]
pub const fn from_seconds(seconds: u64) -> Self {
Self(seconds)
}
#[must_use]
pub const fn as_seconds(self) -> u64 {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PermitValidityError {
Empty,
TooLong,
}
impl_static_error!(PermitValidityError,
Self::Empty => "permit expiry must follow issuance",
Self::TooLong => "permit validity interval is too long",
);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PermitValidity {
issued_at: PermitTimestamp,
expires_at: PermitTimestamp,
duration: u32,
}
impl PermitValidity {
pub fn new(
issued_at: PermitTimestamp,
expires_at: PermitTimestamp,
) -> Result<Self, PermitValidityError> {
let duration = expires_at
.0
.checked_sub(issued_at.0)
.ok_or(PermitValidityError::Empty)?;
if duration == 0 {
return Err(PermitValidityError::Empty);
}
let duration = u32::try_from(duration).map_err(|_| PermitValidityError::TooLong)?;
Ok(Self {
issued_at,
expires_at,
duration,
})
}
#[must_use]
pub const fn issued_at(self) -> PermitTimestamp {
self.issued_at
}
#[must_use]
pub const fn expires_at(self) -> PermitTimestamp {
self.expires_at
}
pub(crate) fn offset(self, now: PermitTimestamp) -> Result<u32, ExecutionPermitError> {
let elapsed = now
.0
.checked_sub(self.issued_at.0)
.ok_or(ExecutionPermitError::NotYetValid)?;
if elapsed >= u64::from(self.duration) {
return Err(ExecutionPermitError::Expired);
}
u32::try_from(elapsed).map_err(|_| ExecutionPermitError::Expired)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CurrencyCodeError {
Invalid,
}
impl_static_error!(CurrencyCodeError, Self::Invalid => "currency code must be three uppercase ASCII letters");
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct CurrencyCode([u8; 3]);
impl CurrencyCode {
pub fn new(value: &str) -> Result<Self, CurrencyCodeError> {
let bytes: [u8; 3] = value
.as_bytes()
.try_into()
.map_err(|_| CurrencyCodeError::Invalid)?;
if !bytes.iter().all(u8::is_ascii_uppercase) {
return Err(CurrencyCodeError::Invalid);
}
Ok(Self(bytes))
}
#[must_use]
pub const fn as_bytes(self) -> [u8; 3] {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PlanCostError {
ZeroObservedPrice,
SpendingCeilingExceeded,
}
impl_static_error!(PlanCostError,
Self::ZeroObservedPrice => "observed price must be nonzero",
Self::SpendingCeilingExceeded => "observed price exceeds spending ceiling",
);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PlanCost {
currency: CurrencyCode,
scale: u8,
observed_units: u128,
ceiling_units: u128,
}
impl PlanCost {
pub fn new(
currency: CurrencyCode,
scale: u8,
observed_units: u128,
ceiling_units: u128,
) -> Result<Self, PlanCostError> {
if observed_units == 0 {
return Err(PlanCostError::ZeroObservedPrice);
}
if observed_units > ceiling_units {
return Err(PlanCostError::SpendingCeilingExceeded);
}
Ok(Self {
currency,
scale,
observed_units,
ceiling_units,
})
}
pub(crate) const fn fields(self) -> (CurrencyCode, u8, u128, u128) {
(
self.currency,
self.scale,
self.observed_units,
self.ceiling_units,
)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PermitContextError {
Empty,
TooLong,
}
impl_static_error!(PermitContextError,
Self::Empty => "permit context is empty",
Self::TooLong => "permit context exceeds the length limit",
);
#[derive(Clone, Copy)]
pub struct PermitContext<'a>(&'a [u8]);
impl<'a> PermitContext<'a> {
pub fn new(value: &'a [u8]) -> Result<Self, PermitContextError> {
if value.is_empty() {
return Err(PermitContextError::Empty);
}
if value.len() > MAX_PLAN_SCOPE_BYTES {
return Err(PermitContextError::TooLong);
}
Ok(Self(value))
}
pub(crate) const fn bytes(self) -> &'a [u8] {
self.0
}
}
impl fmt::Debug for PermitContext<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("PermitContext([redacted])")
}
}
#[derive(Clone, Copy, Debug)]
pub enum PlanFingerprintScope<'a> {
Absent,
Value(&'a [u8]),
}
impl<'a> PlanFingerprintScope<'a> {
pub(crate) fn bytes(self) -> Result<Option<&'a [u8]>, PermitContextError> {
match self {
Self::Absent => Ok(None),
Self::Value([]) => Err(PermitContextError::Empty),
Self::Value(value) if value.len() > MAX_PLAN_SCOPE_BYTES => {
Err(PermitContextError::TooLong)
}
Self::Value(value) => Ok(Some(value)),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PermitIdempotencyKeyError {
TooShort,
TooLong,
AllZero,
}
impl_static_error!(PermitIdempotencyKeyError,
Self::TooShort => "permit idempotency identity is too short",
Self::TooLong => "permit idempotency identity is too long",
Self::AllZero => "permit idempotency identity cannot be all zero",
);
#[derive(Clone, Copy)]
pub struct PermitIdempotencyKey<'a>(&'a [u8]);
impl<'a> PermitIdempotencyKey<'a> {
pub fn new(value: &'a [u8]) -> Result<Self, PermitIdempotencyKeyError> {
if value.len() < MIN_PERMIT_IDEMPOTENCY_BYTES {
return Err(PermitIdempotencyKeyError::TooShort);
}
if value.len() > MAX_PERMIT_IDEMPOTENCY_BYTES {
return Err(PermitIdempotencyKeyError::TooLong);
}
let mut nonzero = Choice::from(0);
for byte in value {
nonzero |= !byte.ct_eq(&0);
}
if !bool::from(nonzero) {
return Err(PermitIdempotencyKeyError::AllZero);
}
Ok(Self(value))
}
pub(crate) const fn bytes(self) -> &'a [u8] {
self.0
}
pub(crate) fn matches(self, other: Self) -> bool {
self.0.len() == other.0.len() && bool::from(self.0.ct_eq(other.0))
}
}
impl fmt::Debug for PermitIdempotencyKey<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("PermitIdempotencyKey([redacted])")
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PermitState {
Ready,
InFlight,
Recoverable,
PendingReconciliation,
Spent,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct RecoveryToken(pub(crate) u16);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ReconciliationToken(pub(crate) u16);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PermitDisposition {
Spent,
Recoverable(RecoveryToken),
PendingReconciliation(ReconciliationToken),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ExecutionPermitError {
ScopeMismatch,
NotYetValid,
Expired,
ClockRollback,
AttemptInFlight,
RecoveryRequired,
ReconciliationRequired,
Spent,
StaleGeneration,
FingerprintMismatch,
IdempotencyMismatch,
ReplayForbidden,
GenerationExhausted,
}
impl_static_error!(ExecutionPermitError,
Self::ScopeMismatch => "execution permit scope does not match the plan",
Self::NotYetValid => "execution permit is not valid yet",
Self::Expired => "execution permit has expired",
Self::ClockRollback => "execution permit clock moved backward",
Self::AttemptInFlight => "execution permit already has an in-flight attempt",
Self::RecoveryRequired => "execution permit requires not-sent recovery",
Self::ReconciliationRequired => "execution permit requires reconciliation",
Self::Spent => "execution permit is spent",
Self::StaleGeneration => "execution permit generation is stale",
Self::FingerprintMismatch => "execution permit fingerprint does not match",
Self::IdempotencyMismatch => "execution permit idempotency identity does not match",
Self::ReplayForbidden => "execution permit replay policy forbids repetition",
Self::GenerationExhausted => "execution permit generation is exhausted",
);
#[cfg(test)]
mod tests;