1use bytes::Bytes;
4use serde::{Deserialize, Serialize};
5
6#[inline]
12pub(crate) fn usize_to_u64(value: usize) -> u64 {
13 value as u64
14}
15use std::fmt;
16use thiserror::Error;
17use uuid::Uuid;
18
19#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
21#[serde(transparent)]
22pub struct AuthorityId(Uuid);
23
24impl AuthorityId {
25 #[must_use]
27 pub fn new() -> Self {
28 Self(Uuid::now_v7())
29 }
30
31 #[must_use]
33 pub const fn from_bytes(bytes: [u8; 16]) -> Self {
34 Self(Uuid::from_bytes(bytes))
35 }
36
37 #[must_use]
39 pub const fn into_bytes(self) -> [u8; 16] {
40 self.0.into_bytes()
41 }
42}
43
44impl Default for AuthorityId {
45 fn default() -> Self {
46 Self::new()
47 }
48}
49
50#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
52#[serde(transparent)]
53pub struct OperationId(Uuid);
54
55impl OperationId {
56 #[must_use]
58 pub fn new() -> Self {
59 Self(Uuid::now_v7())
60 }
61
62 #[must_use]
64 pub const fn from_bytes(bytes: [u8; 16]) -> Self {
65 Self(Uuid::from_bytes(bytes))
66 }
67
68 #[must_use]
70 pub const fn into_bytes(self) -> [u8; 16] {
71 self.0.into_bytes()
72 }
73}
74
75impl Default for OperationId {
76 fn default() -> Self {
77 Self::new()
78 }
79}
80
81#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
83#[serde(transparent)]
84pub struct Epoch(u64);
85
86impl Epoch {
87 pub const GENESIS: Self = Self(1);
89
90 pub const fn new(value: u64) -> Result<Self, IdentityError> {
96 if value == 0 {
97 Err(IdentityError::ZeroEpoch)
98 } else {
99 Ok(Self(value))
100 }
101 }
102
103 #[must_use]
105 pub const fn get(self) -> u64 {
106 self.0
107 }
108}
109
110#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
112#[serde(transparent)]
113pub struct Sequence(u64);
114
115impl Sequence {
116 pub const GENESIS: Self = Self(0);
118
119 #[must_use]
121 pub const fn new(value: u64) -> Self {
122 Self(value)
123 }
124
125 #[must_use]
127 pub const fn get(self) -> u64 {
128 self.0
129 }
130
131 pub const fn checked_next(self) -> Result<Self, IdentityError> {
137 match self.0.checked_add(1) {
138 Some(value) => Ok(Self(value)),
139 None => Err(IdentityError::SequenceExhausted),
140 }
141 }
142}
143
144#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
146#[serde(transparent)]
147pub struct Digest([u8; 32]);
148
149impl Digest {
150 pub const ZERO: Self = Self([0; 32]);
152
153 #[must_use]
155 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
156 Self(bytes)
157 }
158
159 #[must_use]
161 pub const fn as_bytes(&self) -> &[u8; 32] {
162 &self.0
163 }
164
165 #[must_use]
167 pub const fn into_bytes(self) -> [u8; 32] {
168 self.0
169 }
170}
171
172impl fmt::Debug for Digest {
173 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
174 formatter
175 .debug_tuple("Digest")
176 .field(&hex::encode(self.0))
177 .finish()
178 }
179}
180
181macro_rules! uuid_identity {
182 ($name:ident, $description:literal) => {
183 #[doc = $description]
184 #[derive(
185 Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize,
186 )]
187 #[serde(transparent)]
188 pub struct $name(Uuid);
189
190 impl $name {
191 #[must_use]
193 pub fn new() -> Self {
194 Self(Uuid::now_v7())
195 }
196
197 #[must_use]
199 pub const fn from_bytes(bytes: [u8; 16]) -> Self {
200 Self(Uuid::from_bytes(bytes))
201 }
202
203 #[must_use]
205 pub const fn into_bytes(self) -> [u8; 16] {
206 self.0.into_bytes()
207 }
208 }
209
210 impl Default for $name {
211 fn default() -> Self {
212 Self::new()
213 }
214 }
215 };
216}
217
218uuid_identity!(
219 VolumeId,
220 "Stable identity of one independently versioned volume."
221);
222uuid_identity!(
223 CheckoutId,
224 "Process-independent identity of one volume checkout."
225);
226uuid_identity!(MountId, "Stable identity of one mounted-volume binding.");
227uuid_identity!(
228 WatchId,
229 "Process-local identity of one native watcher handle."
230);
231uuid_identity!(
232 FileId,
233 "Stable path-independent identity of one file record."
234);
235
236#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
238#[serde(transparent)]
239pub struct GenerationId(Digest);
240
241impl GenerationId {
242 #[must_use]
244 pub const fn new(digest: Digest) -> Self {
245 Self(digest)
246 }
247
248 #[must_use]
250 pub const fn digest(self) -> Digest {
251 self.0
252 }
253}
254
255#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
257pub struct Head {
258 pub epoch: Epoch,
260 pub sequence: Sequence,
262 pub digest: Digest,
264}
265
266impl Head {
267 #[must_use]
269 pub const fn genesis(epoch: Epoch) -> Self {
270 Self {
271 epoch,
272 sequence: Sequence::GENESIS,
273 digest: Digest::ZERO,
274 }
275 }
276}
277
278#[derive(Clone, Debug, Eq, PartialEq)]
280pub struct ProposedCommit {
281 pub operation_id: OperationId,
283 pub fingerprint: Digest,
285 pub payload: Bytes,
287}
288
289#[derive(Clone, Debug, Eq, PartialEq)]
291pub struct DurableCommit {
292 pub epoch: Epoch,
294 pub sequence: Sequence,
296 pub operation_id: OperationId,
298 pub fingerprint: Digest,
300 pub previous_digest: Digest,
302 pub digest: Digest,
304 pub payload: Bytes,
306}
307
308pub const AUTHORITY_COMMIT_DIGEST_ENVELOPE_BYTES: u64 = 151;
310
311#[must_use]
316pub fn authority_commit_digest(
317 authority_id: AuthorityId,
318 epoch: Epoch,
319 sequence: Sequence,
320 operation_id: OperationId,
321 fingerprint: Digest,
322 previous_digest: Digest,
323 payload: &[u8],
324) -> Digest {
325 const DOMAIN: &[u8] = b"acyclic-fs-authority-commit-v1\0";
326 let mut hasher = blake3::Hasher::new();
327 hasher.update(DOMAIN);
328 hasher.update(&authority_id.into_bytes());
329 hasher.update(&epoch.get().to_le_bytes());
330 hasher.update(&sequence.get().to_le_bytes());
331 hasher.update(&operation_id.into_bytes());
332 hasher.update(fingerprint.as_bytes());
333 hasher.update(previous_digest.as_bytes());
334 hasher.update(
335 &u64::try_from(payload.len())
336 .unwrap_or(u64::MAX)
337 .to_le_bytes(),
338 );
339 hasher.update(payload);
340 Digest::from_bytes(*hasher.finalize().as_bytes())
341}
342
343#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
345pub enum IdentityError {
346 #[error("authority epoch must be non-zero")]
348 ZeroEpoch,
349 #[error("authority sequence exhausted")]
351 SequenceExhausted,
352}
353
354#[cfg(test)]
355#[path = "tests/foundation.rs"]
356mod tests;