Skip to main content

ozzy_replication/local/
configuration.rs

1use super::Error;
2use crate::{Digest, Scope};
3use ozzy_proto::{GroupId, NodeId, layout::Fixed};
4
5/// Exact encoded single-broker configuration length.
6pub const CONFIGURATION_BYTES: usize = 128;
7const DIGEST_START: usize = 88;
8
9/// Persistent single-broker identity, principal binding and local durability.
10/// Encoding is distinct from fixed-three membership. No implicit conversion exists.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct Configuration {
13    scope: Scope,
14    broker: NodeId,
15    principal: Digest,
16}
17
18impl Configuration {
19    /// Validate nonzero identity, epoch, and principal; bind them in a checksummed scope.
20    pub fn new(
21        group: GroupId,
22        epoch: u64,
23        broker: NodeId,
24        principal: Digest,
25    ) -> Result<Self, Error> {
26        if group.as_bytes() == &[0; 16]
27            || broker.as_bytes() == &[0; 16]
28            || epoch == 0
29            || principal == Digest::ZERO
30        {
31            return Err(Error::Configuration);
32        }
33        let mut configuration = Self {
34            scope: Scope {
35                group_id: group,
36                configuration_epoch: epoch,
37                configuration_digest: Digest::ZERO,
38                view: 0,
39            },
40            broker,
41            principal,
42        };
43        configuration.scope.configuration_digest = checksum(&configuration.encode());
44        Ok(configuration)
45    }
46
47    /// Persistent local scope; its view is always zero.
48    pub const fn scope(self) -> Scope {
49        self.scope
50    }
51    /// The sole configured broker identity.
52    pub const fn broker(self) -> NodeId {
53        self.broker
54    }
55    /// Persistent principal binding; it does not authenticate a connection.
56    pub const fn principal(self) -> Digest {
57        self.principal
58    }
59
60    /// Fixed fields and integers in network byte order. Reserved bytes stay zero.
61    pub fn encode(self) -> [u8; CONFIGURATION_BYTES] {
62        let mut bytes = [0; CONFIGURATION_BYTES];
63        bytes[..8].copy_from_slice(b"OZYLOCAL");
64        bytes[8..12].copy_from_slice(&[
65            ozzy_journal::integrity::PROFILE,
66            ozzy_proto::VERSION,
67            1,
68            1,
69        ]);
70        bytes[16..32].copy_from_slice(self.scope.group_id.as_bytes());
71        bytes[32..40].copy_from_slice(&self.scope.configuration_epoch.to_be_bytes());
72        bytes[40..56].copy_from_slice(self.broker.as_bytes());
73        bytes[56..88].copy_from_slice(self.principal.as_bytes());
74        bytes[DIGEST_START..120].copy_from_slice(self.scope.configuration_digest.as_bytes());
75        bytes
76    }
77
78    /// Reject corruption, unsupported fields, or replicated membership records.
79    pub fn decode(bytes: &[u8]) -> Result<Self, Error> {
80        let bytes: &[u8; CONFIGURATION_BYTES] = bytes.try_into().map_err(|_| Error::Encoding)?;
81        let configuration = Self::new(
82            GroupId::from_bytes(bytes.array::<16, 16>()),
83            bytes.u64_at::<32>(),
84            NodeId::from_bytes(bytes.array::<40, 16>()),
85            Digest::from_bytes(bytes.array::<56, 32>()),
86        )?;
87        if *bytes != configuration.encode() {
88            return Err(Error::Encoding);
89        }
90        Ok(configuration)
91    }
92}
93
94fn checksum(bytes: &[u8; CONFIGURATION_BYTES]) -> Digest {
95    let mut hasher =
96        ozzy_journal::integrity::IntegrityHasher::new("ozzy single broker configuration");
97    hasher.update(&bytes[..DIGEST_START]);
98    hasher.update(&[0; 32]);
99    hasher.update(&bytes[120..]);
100    hasher.finish()
101}