acyclic_fs/kernel/
retention.rs1use super::codec::{CanonicalDecodeError, Decoder, Encoder};
4use super::volume::{decode_config, encode_config};
5use crate::foundation::{AuthorityId, Digest, VolumeId};
6use crate::model::{VolumeConfig, VolumeConfigError};
7use crate::storage::{ObjectId, ObjectKind};
8use thiserror::Error;
9
10const DOMAIN: &[u8] = b"acyclic-fs-retention-created-v1\0";
11const ID_DOMAIN: &[u8] = b"acyclic-fs-retention-authority-v1\0";
12const DELETED_DOMAIN: &[u8] = b"acyclic-fs-workspace-deleted-v1\0";
13const VERSION: u16 = 1;
14const MAXIMUM_LABEL_BYTES: u32 = 255;
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub enum RetentionKind {
19 Checkpoint,
21 Pin,
23 ForkBase,
25}
26
27impl RetentionKind {
28 const fn tag(self) -> u8 {
29 match self {
30 Self::Checkpoint => 1,
31 Self::Pin => 2,
32 Self::ForkBase => 3,
33 }
34 }
35
36 fn from_tag(tag: u8) -> Result<Self, CanonicalDecodeError> {
37 match tag {
38 1 => Ok(Self::Checkpoint),
39 2 => Ok(Self::Pin),
40 3 => Ok(Self::ForkBase),
41 _ => Err(invariant("unknown retention kind")),
42 }
43 }
44}
45
46#[derive(Clone, Debug, Eq, PartialEq)]
48pub struct RetentionCreated {
49 pub volume_id: VolumeId,
51 pub kind: RetentionKind,
53 pub label: String,
55 pub generation_root: ObjectId,
57 pub config: VolumeConfig,
59}
60
61#[derive(Debug, Error)]
63pub enum RetentionCreatedError {
64 #[error("retention label is invalid")]
66 InvalidLabel,
67 #[error("retention target is not a generation root")]
69 WrongObjectKind,
70 #[error(transparent)]
72 Config(#[from] VolumeConfigError),
73 #[error(transparent)]
75 Decode(#[from] CanonicalDecodeError),
76}
77
78#[must_use]
80pub fn retention_authority_id(
81 volume_id: VolumeId,
82 kind: RetentionKind,
83 label: &str,
84) -> AuthorityId {
85 let mut hasher = blake3::Hasher::new();
86 hasher.update(ID_DOMAIN);
87 hasher.update(&volume_id.into_bytes());
88 hasher.update(&[kind.tag()]);
89 hasher.update(&u64::try_from(label.len()).unwrap_or(u64::MAX).to_le_bytes());
90 hasher.update(label.as_bytes());
91 let mut bytes = [0_u8; 16];
92 bytes.copy_from_slice(&hasher.finalize().as_bytes()[..16]);
93 AuthorityId::from_bytes(bytes)
94}
95
96pub fn encode_retention_created(
102 value: &RetentionCreated,
103) -> Result<Vec<u8>, RetentionCreatedError> {
104 validate(value)?;
105 let mut encoder = Encoder::new(DOMAIN, VERSION);
106 encoder.fixed(&value.volume_id.into_bytes());
107 encoder.u8(value.kind.tag());
108 encoder.bounded_bytes(value.label.as_bytes())?;
109 encoder.u8(value.generation_root.kind.canonical_tag());
110 encoder.fixed(value.generation_root.digest.as_bytes());
111 encode_config(&mut encoder, value.config);
112 Ok(encoder.finish())
113}
114
115pub fn decode_retention_created(
121 bytes: &[u8],
122 maximum_payload_bytes: u64,
123) -> Result<RetentionCreated, RetentionCreatedError> {
124 let mut decoder = Decoder::new(bytes, DOMAIN, VERSION, maximum_payload_bytes)?;
125 let volume_id = VolumeId::from_bytes(decoder.fixed()?);
126 let kind = RetentionKind::from_tag(decoder.u8()?)?;
127 let label = String::from_utf8(decoder.bounded_bytes(MAXIMUM_LABEL_BYTES)?)
128 .map_err(|_| RetentionCreatedError::InvalidLabel)?;
129 let object_kind = ObjectKind::from_canonical_tag(decoder.u8()?)
130 .map_err(|_| RetentionCreatedError::WrongObjectKind)?;
131 let generation_root = ObjectId {
132 kind: object_kind,
133 digest: Digest::from_bytes(decoder.fixed()?),
134 };
135 let config = decode_config(&mut decoder)?.validate()?;
136 decoder.finish()?;
137 let value = RetentionCreated {
138 volume_id,
139 kind,
140 label,
141 generation_root,
142 config,
143 };
144 validate(&value)?;
145 Ok(value)
146}
147
148#[must_use]
150pub fn encode_workspace_deleted(volume_id: VolumeId) -> Vec<u8> {
151 let mut encoder = Encoder::new(DELETED_DOMAIN, VERSION);
152 encoder.fixed(&volume_id.into_bytes());
153 encoder.finish()
154}
155
156pub fn decode_workspace_deleted(
163 bytes: &[u8],
164 maximum_payload_bytes: u64,
165) -> Result<VolumeId, CanonicalDecodeError> {
166 let mut decoder = Decoder::new(bytes, DELETED_DOMAIN, VERSION, maximum_payload_bytes)?;
167 let volume_id = VolumeId::from_bytes(decoder.fixed()?);
168 decoder.finish()?;
169 Ok(volume_id)
170}
171
172fn validate(value: &RetentionCreated) -> Result<(), RetentionCreatedError> {
173 value.config.validate()?;
174 if value.generation_root.kind != ObjectKind::GenerationRoot {
175 return Err(RetentionCreatedError::WrongObjectKind);
176 }
177 if value.label.is_empty()
178 || u32::try_from(value.label.len()).unwrap_or(u32::MAX) > MAXIMUM_LABEL_BYTES
179 || value
180 .label
181 .chars()
182 .any(|character| character.is_control() || matches!(character, '/' | '\\'))
183 {
184 return Err(RetentionCreatedError::InvalidLabel);
185 }
186 Ok(())
187}
188
189fn invariant(message: &str) -> CanonicalDecodeError {
190 CanonicalDecodeError::Invariant(message.to_owned())
191}
192
193#[cfg(test)]
194#[path = "tests/retention.rs"]
195mod tests;