1use std::cmp::Ordering;
14use std::fmt;
15
16use crate::{AdministrationSequence, CommitSequence};
17
18pub const DUAL_FRONTIER_BYTES: usize = 18;
20
21const COMPONENT_ABSENT: u8 = 0x00;
23const COMPONENT_PRESENT: u8 = 0x01;
25
26#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
40pub struct DualFrontier {
41 application: Option<CommitSequence>,
42 administration: Option<AdministrationSequence>,
43}
44
45impl DualFrontier {
46 pub const INITIAL: Self = Self {
48 application: None,
49 administration: None,
50 };
51
52 #[must_use]
54 pub const fn new(
55 application: Option<CommitSequence>,
56 administration: Option<AdministrationSequence>,
57 ) -> Self {
58 Self {
59 application,
60 administration,
61 }
62 }
63
64 #[must_use]
66 pub const fn application(self) -> Option<CommitSequence> {
67 self.application
68 }
69
70 #[must_use]
72 pub const fn administration(self) -> Option<AdministrationSequence> {
73 self.administration
74 }
75
76 #[must_use]
78 pub const fn is_initial(self) -> bool {
79 self.application.is_none() && self.administration.is_none()
80 }
81
82 #[must_use]
88 pub fn advances_from(self, predecessor: Self) -> bool {
89 self.partial_cmp(&predecessor) == Some(Ordering::Greater)
90 }
91
92 #[must_use]
98 pub const fn to_canonical_bytes(self) -> [u8; DUAL_FRONTIER_BYTES] {
99 let mut encoded = [0_u8; DUAL_FRONTIER_BYTES];
100 if let Some(application) = self.application {
101 encoded[0] = COMPONENT_PRESENT;
102 let value = application.to_be_bytes();
103 let mut index = 0;
104 while index < 8 {
105 encoded[1 + index] = value[index];
106 index += 1;
107 }
108 } else {
109 encoded[0] = COMPONENT_ABSENT;
110 }
111 if let Some(administration) = self.administration {
112 encoded[9] = COMPONENT_PRESENT;
113 let value = administration.to_be_bytes();
114 let mut index = 0;
115 while index < 8 {
116 encoded[10 + index] = value[index];
117 index += 1;
118 }
119 } else {
120 encoded[9] = COMPONENT_ABSENT;
121 }
122 encoded
123 }
124
125 pub fn from_canonical_bytes(
127 bytes: [u8; DUAL_FRONTIER_BYTES],
128 ) -> Result<Self, DualFrontierError> {
129 let application = decode_component(bytes[0], &bytes[1..9])?
130 .map(|value| CommitSequence::new(value).ok_or(DualFrontierError::ZeroSequence))
131 .transpose()?;
132 let administration = decode_component(bytes[9], &bytes[10..18])?
133 .map(|value| AdministrationSequence::new(value).ok_or(DualFrontierError::ZeroSequence))
134 .transpose()?;
135 Ok(Self::new(application, administration))
136 }
137}
138
139fn decode_component(presence: u8, value: &[u8]) -> Result<Option<u64>, DualFrontierError> {
140 let raw = u64::from_be_bytes(
141 value
142 .try_into()
143 .map_err(|_| DualFrontierError::InvalidShape)?,
144 );
145 match presence {
146 COMPONENT_ABSENT if raw == 0 => Ok(None),
147 COMPONENT_ABSENT => Err(DualFrontierError::NonCanonicalAbsence),
148 COMPONENT_PRESENT => Ok(Some(raw)),
149 _ => Err(DualFrontierError::InvalidShape),
150 }
151}
152
153impl PartialOrd for DualFrontier {
154 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
155 let application = component_cmp(
156 self.application.map(CommitSequence::get),
157 other.application.map(CommitSequence::get),
158 );
159 let administration = component_cmp(
160 self.administration.map(AdministrationSequence::get),
161 other.administration.map(AdministrationSequence::get),
162 );
163 match (application, administration) {
164 (Ordering::Equal, value) | (value, Ordering::Equal) => Some(value),
165 (Ordering::Less, Ordering::Less) => Some(Ordering::Less),
166 (Ordering::Greater, Ordering::Greater) => Some(Ordering::Greater),
167 _ => None,
168 }
169 }
170}
171
172fn component_cmp(left: Option<u64>, right: Option<u64>) -> Ordering {
173 left.unwrap_or(0).cmp(&right.unwrap_or(0))
174}
175
176impl fmt::Display for DualFrontier {
177 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
178 match (self.application, self.administration) {
179 (Some(application), Some(administration)) => {
180 write!(formatter, "(app {application}, admin {administration})")
181 }
182 (Some(application), None) => write!(formatter, "(app {application}, admin none)"),
183 (None, Some(administration)) => write!(formatter, "(app none, admin {administration})"),
184 (None, None) => formatter.write_str("(app none, admin none)"),
185 }
186 }
187}
188
189#[derive(Clone, Copy, Debug, Eq, PartialEq)]
191pub enum DualFrontierError {
192 InvalidShape,
194 NonCanonicalAbsence,
196 ZeroSequence,
198}
199
200impl fmt::Display for DualFrontierError {
201 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
202 formatter.write_str(match self {
203 Self::InvalidShape => "dual frontier bytes have an invalid shape",
204 Self::NonCanonicalAbsence => {
205 "an absent dual frontier component is not canonically zero"
206 }
207 Self::ZeroSequence => "a present dual frontier component must be nonzero",
208 })
209 }
210}
211
212impl std::error::Error for DualFrontierError {}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 fn application(value: u64) -> Option<CommitSequence> {
219 CommitSequence::new(value)
220 }
221
222 fn administration(value: u64) -> Option<AdministrationSequence> {
223 AdministrationSequence::new(value)
224 }
225
226 fn pair(app: u64, admin: u64) -> DualFrontier {
227 DualFrontier::new(application(app), administration(admin))
228 }
229
230 #[test]
231 fn initial_is_both_components_absent() {
232 assert!(DualFrontier::INITIAL.is_initial());
233 assert_eq!(DualFrontier::INITIAL.application(), None);
234 assert_eq!(DualFrontier::INITIAL.administration(), None);
235 assert!(!pair(1, 0).is_initial());
236 }
237
238 #[test]
239 fn canonical_bytes_round_trip_over_every_presence_combination() {
240 for frontier in [
241 DualFrontier::INITIAL,
242 pair(1, 0),
243 pair(0, 1),
244 pair(7, 9),
245 pair(u64::MAX, u64::MAX),
246 ] {
247 let encoded = frontier.to_canonical_bytes();
248 assert_eq!(encoded.len(), DUAL_FRONTIER_BYTES);
249 assert_eq!(
250 DualFrontier::from_canonical_bytes(encoded).expect("canonical round trip"),
251 frontier
252 );
253 }
254 }
255
256 #[test]
257 fn absence_is_encoded_explicitly_and_never_confused_with_zero() {
258 let encoded = DualFrontier::INITIAL.to_canonical_bytes();
259 assert_eq!(encoded, [0_u8; DUAL_FRONTIER_BYTES]);
260 let present_zero = {
261 let mut bytes = encoded;
262 bytes[0] = COMPONENT_PRESENT;
263 bytes
264 };
265 assert_eq!(
266 DualFrontier::from_canonical_bytes(present_zero),
267 Err(DualFrontierError::ZeroSequence)
268 );
269 let absent_nonzero = {
270 let mut bytes = encoded;
271 bytes[8] = 1;
272 bytes
273 };
274 assert_eq!(
275 DualFrontier::from_canonical_bytes(absent_nonzero),
276 Err(DualFrontierError::NonCanonicalAbsence)
277 );
278 let unknown_tag = {
279 let mut bytes = encoded;
280 bytes[9] = 0x02;
281 bytes
282 };
283 assert_eq!(
284 DualFrontier::from_canonical_bytes(unknown_tag),
285 Err(DualFrontierError::InvalidShape)
286 );
287 }
288
289 #[test]
290 fn a_command_frame_advances_only_the_application_component() {
291 let predecessor = pair(3, 4);
292 let covered = pair(5, 4);
293 assert!(covered.advances_from(predecessor));
294 assert!(!predecessor.advances_from(covered));
295 assert_eq!(covered.partial_cmp(&predecessor), Some(Ordering::Greater));
296 }
297
298 #[test]
299 fn a_service_audit_frame_advances_only_the_administration_component() {
300 let predecessor = pair(5, 4);
301 let covered = pair(5, 6);
302 assert!(covered.advances_from(predecessor));
303 assert!(!predecessor.advances_from(covered));
304 }
305
306 #[test]
307 fn an_equal_pair_never_advances() {
308 assert!(!pair(5, 4).advances_from(pair(5, 4)));
309 assert!(!DualFrontier::INITIAL.advances_from(DualFrontier::INITIAL));
310 assert_eq!(pair(5, 4).partial_cmp(&pair(5, 4)), Some(Ordering::Equal));
311 }
312
313 #[test]
314 fn the_first_frame_advances_from_the_initial_frontier() {
315 assert!(pair(1, 0).advances_from(DualFrontier::INITIAL));
316 assert!(pair(0, 1).advances_from(DualFrontier::INITIAL));
317 assert!(pair(1, 1).advances_from(DualFrontier::INITIAL));
318 }
319
320 #[test]
321 fn an_incomparable_pair_is_never_an_advance_in_either_direction() {
322 let left = pair(9, 2);
323 let right = pair(3, 7);
324 assert_eq!(left.partial_cmp(&right), None);
325 assert_eq!(right.partial_cmp(&left), None);
326 assert!(!left.advances_from(right));
327 assert!(!right.advances_from(left));
328 }
329
330 #[test]
331 fn a_regressed_component_is_never_an_advance() {
332 assert!(!pair(4, 4).advances_from(pair(5, 4)));
333 assert!(!pair(5, 3).advances_from(pair(5, 4)));
334 assert!(!DualFrontier::INITIAL.advances_from(pair(1, 0)));
335 }
336
337 #[test]
338 fn display_names_both_components_without_leaking_anything_else() {
339 assert_eq!(pair(5, 4).to_string(), "(app 5, admin 4)");
340 assert_eq!(pair(5, 0).to_string(), "(app 5, admin none)");
341 assert_eq!(pair(0, 4).to_string(), "(app none, admin 4)");
342 assert_eq!(DualFrontier::INITIAL.to_string(), "(app none, admin none)");
343 }
344}