1use crate::{NodeId, ObjectId, TransactionId, WriterId};
2use chrono::{DateTime, Utc};
3use std::{fmt, sync::Arc};
4
5pub const MAX_OBJECT_BYTES: u64 = 32 * 1024 * 1024 * 1024;
6pub const MAX_TRANSACTION_OBJECT_BYTES: u64 = MAX_OBJECT_BYTES;
7pub(crate) const MAX_PROVENANCE_STRING_BYTES: usize = 1024 * 1024;
8pub(crate) const MAX_TRANSACTION_BYTES: usize = 64 * 1024 * 1024;
9
10pub type Result<T> = std::result::Result<T, Error>;
11
12#[derive(Debug)]
13pub enum Error {
14 Io(std::io::Error),
15 Busy(String),
16 Corrupt(String),
17 InvalidConfig(String),
18 InvalidInput(String),
19 InvalidTransaction(String),
20 NotFound(String),
21 OfflineUpgradeRequired(String),
22}
23
24impl Error {
25 pub(crate) fn corrupt(message: impl Into<String>) -> Self {
26 Self::Corrupt(message.into())
27 }
28
29 pub(crate) fn invalid_config(message: impl Into<String>) -> Self {
30 Self::InvalidConfig(message.into())
31 }
32
33 pub(crate) fn invalid_input(message: impl Into<String>) -> Self {
34 Self::InvalidInput(message.into())
35 }
36
37 pub(crate) fn invalid_transaction(message: impl Into<String>) -> Self {
38 Self::InvalidTransaction(message.into())
39 }
40
41 pub(crate) fn not_found(message: impl Into<String>) -> Self {
42 Self::NotFound(message.into())
43 }
44
45 pub(crate) fn offline_upgrade(message: impl Into<String>) -> Self {
46 Self::OfflineUpgradeRequired(message.into())
47 }
48}
49
50impl fmt::Display for Error {
51 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
52 match self {
53 Self::Io(error) => write!(formatter, "I/O error: {error}"),
54 Self::Busy(message) => write!(formatter, "database is busy: {message}"),
55 Self::Corrupt(message) => write!(formatter, "database corruption: {message}"),
56 Self::InvalidConfig(message) => write!(formatter, "invalid configuration: {message}"),
57 Self::InvalidInput(message) => write!(formatter, "invalid input: {message}"),
58 Self::InvalidTransaction(message) => {
59 write!(formatter, "invalid transaction: {message}")
60 }
61 Self::NotFound(message) => write!(formatter, "not found: {message}"),
62 Self::OfflineUpgradeRequired(message) => {
63 write!(formatter, "offline upgrade required: {message}")
64 }
65 }
66 }
67}
68
69impl std::error::Error for Error {
70 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
71 match self {
72 Self::Io(error) => Some(error),
73 _ => None,
74 }
75 }
76}
77
78impl From<std::io::Error> for Error {
79 fn from(error: std::io::Error) -> Self {
80 Self::Io(error)
81 }
82}
83
84pub trait Gossip: Send + Sync + 'static {
85 fn announce(&self, package: TransactionPackage) -> bool;
86}
87
88pub trait TransactionSource: Send + Sync {
89 fn request_transactions(&self, transactions: Vec<TransactionId>);
90}
91
92#[derive(Clone, Copy, Debug, Default)]
93pub struct NoopGossip;
94
95impl Gossip for NoopGossip {
96 fn announce(&self, _package: TransactionPackage) -> bool {
97 true
98 }
99}
100
101#[derive(Clone)]
102pub struct Config {
103 pub signing_key: [u8; 32],
104 pub writers_by_priority: Vec<WriterId>,
105 pub gossip: Arc<dyn Gossip>,
106}
107
108impl fmt::Debug for Config {
109 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
110 formatter
111 .debug_struct("Config")
112 .field("signing_key", &"[redacted]")
113 .field("writers_by_priority", &self.writers_by_priority)
114 .field("gossip", &"dyn Gossip")
115 .finish()
116 }
117}
118
119#[derive(Clone, Debug, Eq, PartialEq)]
120pub struct Provenance {
121 pub author: String,
122 pub source: String,
123 pub source_created_at: DateTime<Utc>,
124 pub data: String,
125}
126
127impl Provenance {
128 pub(crate) fn validate(&self) -> Result<()> {
129 if self.author.trim().is_empty() || self.author.len() > MAX_PROVENANCE_STRING_BYTES {
130 return Err(Error::invalid_input(
131 "provenance author is empty or too large",
132 ));
133 }
134 if self.source.trim().is_empty() || self.source.len() > MAX_PROVENANCE_STRING_BYTES {
135 return Err(Error::invalid_input(
136 "provenance source is empty or too large",
137 ));
138 }
139 if self.data.len() > MAX_PROVENANCE_STRING_BYTES {
140 return Err(Error::invalid_input("provenance data exceeds 1 MiB"));
141 }
142 Ok(())
143 }
144}
145
146#[derive(Clone, Copy, Debug, Eq, PartialEq)]
147pub enum Owner {
148 Unowned,
149 SelfNode,
150 Node(NodeId),
151}
152
153#[derive(Clone, Debug, Eq, PartialEq)]
154pub struct NodeData {
155 pub short_name: String,
156 pub short_description: String,
157 pub long_description: String,
158 pub owner: Owner,
159 pub fixed_connections: Vec<NodeId>,
160 pub recent_connections: Vec<NodeId>,
161 pub objects: Vec<ObjectId>,
162}
163
164impl NodeData {
165 pub(crate) fn validate(&self) -> Result<()> {
166 ensure_unique(&self.fixed_connections, "fixed_connections")?;
167 ensure_unique(&self.recent_connections, "recent_connections")?;
168 ensure_unique(&self.objects, "objects")?;
169 if self.fixed_connections.iter().any(|id| !id.valid_domain())
170 || self.recent_connections.iter().any(|id| !id.valid_domain())
171 || self.objects.iter().any(|id| !id.valid_domain())
172 || matches!(self.owner, Owner::Node(id) if !id.valid_domain())
173 {
174 return Err(Error::invalid_input(
175 "node contains an identifier in the wrong type domain",
176 ));
177 }
178 Ok(())
179 }
180}
181
182fn ensure_unique<T: Copy + Ord>(values: &[T], name: &str) -> Result<()> {
183 let mut ordered = values.to_vec();
184 ordered.sort_unstable();
185 if ordered.windows(2).any(|pair| pair[0] == pair[1]) {
186 return Err(Error::invalid_input(format!(
187 "{name} must contain unique IDs"
188 )));
189 }
190 Ok(())
191}
192
193#[derive(Clone, Debug, Eq, PartialEq)]
194pub struct Node {
195 pub id: NodeId,
196 pub data: NodeData,
197 pub last_author: String,
198 pub committed_at: DateTime<Utc>,
199}
200
201#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
202pub struct MergePair {
203 pub first: TransactionId,
204 pub second: TransactionId,
205}
206
207impl MergePair {
208 pub fn new(first: TransactionId, second: TransactionId) -> Result<Self> {
209 if first == second {
210 return Err(Error::invalid_input(
211 "a merge pair must name two different transactions",
212 ));
213 }
214 let (first, second) = if first < second {
215 (first, second)
216 } else {
217 (second, first)
218 };
219 Ok(Self { first, second })
220 }
221}
222
223#[derive(Clone, Debug, Eq, PartialEq)]
224pub struct HistoryEntry {
225 pub transaction_id: TransactionId,
226 pub writer: WriterId,
227 pub committed_at: DateTime<Utc>,
228 pub provenance: Provenance,
229 pub active: bool,
230 pub created: bool,
231 pub updated: bool,
232 pub data: Option<NodeData>,
233 pub merge_pairs: Vec<MergePair>,
234}
235
236#[derive(Clone, Debug, Eq, PartialEq)]
237pub struct NodeHistory {
238 pub node_id: NodeId,
239 pub frontier: Vec<TransactionId>,
240 pub visible: Option<TransactionId>,
241 pub entries: Vec<HistoryEntry>,
242}
243
244#[derive(Eq, PartialEq)]
245pub struct ObjectPayload {
246 pub id: ObjectId,
247 pub bytes: Vec<u8>,
248}
249
250impl fmt::Debug for ObjectPayload {
251 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
252 formatter
253 .debug_struct("ObjectPayload")
254 .field("id", &self.id)
255 .field("length", &self.bytes.len())
256 .finish()
257 }
258}
259
260#[derive(Eq, PartialEq)]
261pub struct TransactionPackage {
262 pub transaction: Vec<u8>,
263 pub objects: Vec<ObjectPayload>,
264}
265
266impl fmt::Debug for TransactionPackage {
267 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
268 formatter
269 .debug_struct("TransactionPackage")
270 .field("transaction_bytes", &self.transaction.len())
271 .field("objects", &self.objects)
272 .finish()
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 #[test]
281 fn node_text_has_no_database_policy_limits() {
282 let mut data = NodeData {
283 short_name: String::new(),
284 short_description: "x".repeat(201),
285 long_description: std::iter::repeat_n("word", 1_001)
286 .collect::<Vec<_>>()
287 .join(" "),
288 owner: Owner::SelfNode,
289 fixed_connections: Vec::new(),
290 recent_connections: Vec::new(),
291 objects: Vec::new(),
292 };
293
294 assert!(data.short_description.chars().count() > 200);
295 assert!(data.long_description.split_whitespace().count() > 1_000);
296 data.validate().unwrap();
297
298 data.short_name = "x".repeat(51);
299 assert!(data.short_name.chars().count() > 50);
300 data.validate().unwrap();
301
302 data.short_description = "x".repeat(MAX_PROVENANCE_STRING_BYTES + 1);
303 data.validate().unwrap();
304 }
305}