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_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_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_STRING_BYTES {
135 return Err(Error::invalid_input(
136 "provenance source is empty or too large",
137 ));
138 }
139 if self.data.len() > MAX_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 let characters = self.short_name.chars().count();
167 if !(4..=50).contains(&characters) {
168 return Err(Error::invalid_input(
169 "short_name must contain between 4 and 50 characters",
170 ));
171 }
172 if self.short_description.chars().count() > 200 {
173 return Err(Error::invalid_input(
174 "short_description exceeds 200 characters",
175 ));
176 }
177 if self.short_name.len() > MAX_STRING_BYTES
178 || self.short_description.len() > MAX_STRING_BYTES
179 || self.long_description.len() > MAX_STRING_BYTES
180 {
181 return Err(Error::invalid_input("node text exceeds 1 MiB"));
182 }
183 ensure_unique(&self.fixed_connections, "fixed_connections")?;
184 ensure_unique(&self.recent_connections, "recent_connections")?;
185 ensure_unique(&self.objects, "objects")?;
186 if self.fixed_connections.iter().any(|id| !id.valid_domain())
187 || self.recent_connections.iter().any(|id| !id.valid_domain())
188 || self.objects.iter().any(|id| !id.valid_domain())
189 || matches!(self.owner, Owner::Node(id) if !id.valid_domain())
190 {
191 return Err(Error::invalid_input(
192 "node contains an identifier in the wrong type domain",
193 ));
194 }
195 Ok(())
196 }
197}
198
199fn ensure_unique<T: Copy + Ord>(values: &[T], name: &str) -> Result<()> {
200 let mut ordered = values.to_vec();
201 ordered.sort_unstable();
202 if ordered.windows(2).any(|pair| pair[0] == pair[1]) {
203 return Err(Error::invalid_input(format!(
204 "{name} must contain unique IDs"
205 )));
206 }
207 Ok(())
208}
209
210#[derive(Clone, Debug, Eq, PartialEq)]
211pub struct Node {
212 pub id: NodeId,
213 pub data: NodeData,
214 pub last_author: String,
215 pub committed_at: DateTime<Utc>,
216}
217
218#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
219pub struct MergePair {
220 pub first: TransactionId,
221 pub second: TransactionId,
222}
223
224impl MergePair {
225 pub fn new(first: TransactionId, second: TransactionId) -> Result<Self> {
226 if first == second {
227 return Err(Error::invalid_input(
228 "a merge pair must name two different transactions",
229 ));
230 }
231 let (first, second) = if first < second {
232 (first, second)
233 } else {
234 (second, first)
235 };
236 Ok(Self { first, second })
237 }
238}
239
240#[derive(Clone, Debug, Eq, PartialEq)]
241pub struct HistoryEntry {
242 pub transaction_id: TransactionId,
243 pub writer: WriterId,
244 pub committed_at: DateTime<Utc>,
245 pub provenance: Provenance,
246 pub active: bool,
247 pub created: bool,
248 pub updated: bool,
249 pub data: Option<NodeData>,
250 pub merge_pairs: Vec<MergePair>,
251}
252
253#[derive(Clone, Debug, Eq, PartialEq)]
254pub struct NodeHistory {
255 pub node_id: NodeId,
256 pub frontier: Vec<TransactionId>,
257 pub visible: Option<TransactionId>,
258 pub entries: Vec<HistoryEntry>,
259}
260
261#[derive(Eq, PartialEq)]
262pub struct ObjectPayload {
263 pub id: ObjectId,
264 pub bytes: Vec<u8>,
265}
266
267impl fmt::Debug for ObjectPayload {
268 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
269 formatter
270 .debug_struct("ObjectPayload")
271 .field("id", &self.id)
272 .field("length", &self.bytes.len())
273 .finish()
274 }
275}
276
277#[derive(Eq, PartialEq)]
278pub struct TransactionPackage {
279 pub transaction: Vec<u8>,
280 pub objects: Vec<ObjectPayload>,
281}
282
283impl fmt::Debug for TransactionPackage {
284 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
285 formatter
286 .debug_struct("TransactionPackage")
287 .field("transaction_bytes", &self.transaction.len())
288 .field("objects", &self.objects)
289 .finish()
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
298 fn long_description_has_no_database_word_limit() {
299 let data = NodeData {
300 short_name: "Long description node".into(),
301 short_description: "Database validation must not enforce Chatend policy.".into(),
302 long_description: std::iter::repeat_n("word", 1_001)
303 .collect::<Vec<_>>()
304 .join(" "),
305 owner: Owner::SelfNode,
306 fixed_connections: Vec::new(),
307 recent_connections: Vec::new(),
308 objects: Vec::new(),
309 };
310
311 assert!(data.long_description.split_whitespace().count() > 1_000);
312 data.validate().unwrap();
313 }
314}