use super::codec::{CanonicalDecodeError, Decoder, Encoder};
use super::volume::{decode_config, encode_config};
use crate::foundation::{AuthorityId, Digest, VolumeId};
use crate::model::{VolumeConfig, VolumeConfigError};
use crate::storage::{ObjectId, ObjectKind};
use thiserror::Error;
const DOMAIN: &[u8] = b"acyclic-fs-retention-created-v1\0";
const ID_DOMAIN: &[u8] = b"acyclic-fs-retention-authority-v1\0";
const DELETED_DOMAIN: &[u8] = b"acyclic-fs-workspace-deleted-v1\0";
const VERSION: u16 = 1;
const MAXIMUM_LABEL_BYTES: u32 = 255;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RetentionKind {
Checkpoint,
Pin,
ForkBase,
}
impl RetentionKind {
const fn tag(self) -> u8 {
match self {
Self::Checkpoint => 1,
Self::Pin => 2,
Self::ForkBase => 3,
}
}
fn from_tag(tag: u8) -> Result<Self, CanonicalDecodeError> {
match tag {
1 => Ok(Self::Checkpoint),
2 => Ok(Self::Pin),
3 => Ok(Self::ForkBase),
_ => Err(invariant("unknown retention kind")),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RetentionCreated {
pub volume_id: VolumeId,
pub kind: RetentionKind,
pub label: String,
pub generation_root: ObjectId,
pub config: VolumeConfig,
}
#[derive(Debug, Error)]
pub enum RetentionCreatedError {
#[error("retention label is invalid")]
InvalidLabel,
#[error("retention target is not a generation root")]
WrongObjectKind,
#[error(transparent)]
Config(#[from] VolumeConfigError),
#[error(transparent)]
Decode(#[from] CanonicalDecodeError),
}
#[must_use]
pub fn retention_authority_id(
volume_id: VolumeId,
kind: RetentionKind,
label: &str,
) -> AuthorityId {
let mut hasher = blake3::Hasher::new();
hasher.update(ID_DOMAIN);
hasher.update(&volume_id.into_bytes());
hasher.update(&[kind.tag()]);
hasher.update(&u64::try_from(label.len()).unwrap_or(u64::MAX).to_le_bytes());
hasher.update(label.as_bytes());
let mut bytes = [0_u8; 16];
bytes.copy_from_slice(&hasher.finalize().as_bytes()[..16]);
AuthorityId::from_bytes(bytes)
}
pub fn encode_retention_created(
value: &RetentionCreated,
) -> Result<Vec<u8>, RetentionCreatedError> {
validate(value)?;
let mut encoder = Encoder::new(DOMAIN, VERSION);
encoder.fixed(&value.volume_id.into_bytes());
encoder.u8(value.kind.tag());
encoder.bounded_bytes(value.label.as_bytes())?;
encoder.u8(value.generation_root.kind.canonical_tag());
encoder.fixed(value.generation_root.digest.as_bytes());
encode_config(&mut encoder, value.config);
Ok(encoder.finish())
}
pub fn decode_retention_created(
bytes: &[u8],
maximum_payload_bytes: u64,
) -> Result<RetentionCreated, RetentionCreatedError> {
let mut decoder = Decoder::new(bytes, DOMAIN, VERSION, maximum_payload_bytes)?;
let volume_id = VolumeId::from_bytes(decoder.fixed()?);
let kind = RetentionKind::from_tag(decoder.u8()?)?;
let label = String::from_utf8(decoder.bounded_bytes(MAXIMUM_LABEL_BYTES)?)
.map_err(|_| RetentionCreatedError::InvalidLabel)?;
let object_kind = ObjectKind::from_canonical_tag(decoder.u8()?)
.map_err(|_| RetentionCreatedError::WrongObjectKind)?;
let generation_root = ObjectId {
kind: object_kind,
digest: Digest::from_bytes(decoder.fixed()?),
};
let config = decode_config(&mut decoder)?.validate()?;
decoder.finish()?;
let value = RetentionCreated {
volume_id,
kind,
label,
generation_root,
config,
};
validate(&value)?;
Ok(value)
}
#[must_use]
pub fn encode_workspace_deleted(volume_id: VolumeId) -> Vec<u8> {
let mut encoder = Encoder::new(DELETED_DOMAIN, VERSION);
encoder.fixed(&volume_id.into_bytes());
encoder.finish()
}
pub fn decode_workspace_deleted(
bytes: &[u8],
maximum_payload_bytes: u64,
) -> Result<VolumeId, CanonicalDecodeError> {
let mut decoder = Decoder::new(bytes, DELETED_DOMAIN, VERSION, maximum_payload_bytes)?;
let volume_id = VolumeId::from_bytes(decoder.fixed()?);
decoder.finish()?;
Ok(volume_id)
}
fn validate(value: &RetentionCreated) -> Result<(), RetentionCreatedError> {
value.config.validate()?;
if value.generation_root.kind != ObjectKind::GenerationRoot {
return Err(RetentionCreatedError::WrongObjectKind);
}
if value.label.is_empty()
|| u32::try_from(value.label.len()).unwrap_or(u32::MAX) > MAXIMUM_LABEL_BYTES
|| value
.label
.chars()
.any(|character| character.is_control() || matches!(character, '/' | '\\'))
{
return Err(RetentionCreatedError::InvalidLabel);
}
Ok(())
}
fn invariant(message: &str) -> CanonicalDecodeError {
CanonicalDecodeError::Invariant(message.to_owned())
}
#[cfg(test)]
#[path = "tests/retention.rs"]
mod tests;