1use kcode_k1_peering::K1Peering;
2use kcode_k1_transaction::{REGISTER_AT_TIP, SubsystemId, Transaction};
3pub use kcode_k1_transaction_id::TxId;
4use kcode_k1_txn_ordering::{K1TxnOrdering, Subsystem};
5use std::sync::Arc;
6
7const PAYLOAD_VERSION: u8 = 1;
8const PAYLOAD_HEADER_BYTES: usize = 25;
9const OBJECT_SUBSYSTEM_BYTES: [u8; 20] = *b"k1-objects-subsystem";
10
11#[derive(Clone, Debug, Eq, PartialEq)]
12pub struct Object {
13 pub filename: String,
14 pub file_type: String,
15 pub description: String,
16 pub data: Vec<u8>,
17}
18
19pub trait ObjectSaveObserver: Send + Sync + 'static {
20 fn saved(&self, id: TxId);
21}
22
23pub struct K1Objects {
24 ordering: Arc<K1TxnOrdering>,
25 peering: Arc<K1Peering>,
26 observer: Arc<dyn ObjectSaveObserver>,
27}
28
29impl K1Objects {
30 pub fn open(ordering: Arc<K1TxnOrdering>, peering: Arc<K1Peering>) -> Result<Self, String> {
31 Self::open_with_save_observer(ordering, peering, Arc::new(NoopSaveObserver))
32 }
33
34 pub fn open_with_save_observer(
35 ordering: Arc<K1TxnOrdering>,
36 peering: Arc<K1Peering>,
37 observer: Arc<dyn ObjectSaveObserver>,
38 ) -> Result<Self, String> {
39 ordering.register_subsystem(
40 object_subsystem(),
41 Some(REGISTER_AT_TIP),
42 Arc::new(StatelessObjects),
43 )?;
44 Ok(Self {
45 ordering,
46 peering,
47 observer,
48 })
49 }
50
51 pub fn save(
52 &self,
53 filename: &str,
54 file_type: &str,
55 description: &str,
56 data: &[u8],
57 ) -> Result<TxId, String> {
58 let payload = encode_payload(filename, file_type, description, data)?;
59 let id = self.peering.submit_txn(object_subsystem(), &payload)?;
60 self.observer.saved(id);
61 Ok(id)
62 }
63
64 pub fn load(&self, id: TxId) -> Result<Option<Object>, String> {
65 let Some(bytes) = self.ordering.get_txn(id)? else {
66 return Ok(None);
67 };
68 let transaction = Transaction::parse(&bytes)
69 .map_err(|message| format!("canonical transaction is invalid: {message}"))?;
70 if transaction.subsystem() != object_subsystem() {
71 return Ok(None);
72 }
73 decode_payload(transaction.payload()).map(Some)
74 }
75}
76
77struct NoopSaveObserver;
78
79impl ObjectSaveObserver for NoopSaveObserver {
80 fn saved(&self, _id: TxId) {}
81}
82
83struct StatelessObjects;
84
85impl Subsystem for StatelessObjects {
86 fn submit_txn(&self, _id: TxId, _payload: &[u8]) -> Result<(), String> {
87 Ok(())
88 }
89
90 fn reorg(&self) -> Result<(), String> {
91 Ok(())
92 }
93}
94
95fn object_subsystem() -> SubsystemId {
96 SubsystemId::from_bytes(OBJECT_SUBSYSTEM_BYTES)
97 .expect("Objects subsystem ID is valid fixed-width UTF-8")
98}
99
100fn encode_payload(
101 filename: &str,
102 file_type: &str,
103 description: &str,
104 data: &[u8],
105) -> Result<Vec<u8>, String> {
106 let filename_len = u64::try_from(filename.len())
107 .map_err(|_| "filename length exceeds the object wire format".to_owned())?;
108 let file_type_len = u64::try_from(file_type.len())
109 .map_err(|_| "file type length exceeds the object wire format".to_owned())?;
110 let description_len = u64::try_from(description.len())
111 .map_err(|_| "description length exceeds the object wire format".to_owned())?;
112 let total_len = PAYLOAD_HEADER_BYTES
113 .checked_add(filename.len())
114 .and_then(|length| length.checked_add(file_type.len()))
115 .and_then(|length| length.checked_add(description.len()))
116 .and_then(|length| length.checked_add(data.len()))
117 .ok_or_else(|| "object payload length exceeds usize".to_owned())?;
118
119 let mut payload = Vec::new();
120 payload
121 .try_reserve_exact(total_len)
122 .map_err(|error| format!("cannot allocate object payload: {error}"))?;
123 payload.push(PAYLOAD_VERSION);
124 payload.extend_from_slice(&filename_len.to_le_bytes());
125 payload.extend_from_slice(&file_type_len.to_le_bytes());
126 payload.extend_from_slice(&description_len.to_le_bytes());
127 payload.extend_from_slice(filename.as_bytes());
128 payload.extend_from_slice(file_type.as_bytes());
129 payload.extend_from_slice(description.as_bytes());
130 payload.extend_from_slice(data);
131
132 debug_assert_eq!(payload.len(), total_len);
133 Ok(payload)
134}
135
136fn decode_payload(payload: &[u8]) -> Result<Object, String> {
137 if payload.len() < PAYLOAD_HEADER_BYTES {
138 return Err(format!(
139 "object payload must contain at least {PAYLOAD_HEADER_BYTES} bytes"
140 ));
141 }
142 if payload[0] != PAYLOAD_VERSION {
143 return Err(format!("unsupported object payload version {}", payload[0]));
144 }
145
146 let filename_len = decode_length(payload, 1, "filename")?;
147 let file_type_len = decode_length(payload, 9, "file type")?;
148 let description_len = decode_length(payload, 17, "description")?;
149 let filename_end = PAYLOAD_HEADER_BYTES
150 .checked_add(filename_len)
151 .ok_or_else(|| "object metadata length overflow".to_owned())?;
152 let file_type_end = filename_end
153 .checked_add(file_type_len)
154 .ok_or_else(|| "object metadata length overflow".to_owned())?;
155 let description_end = file_type_end
156 .checked_add(description_len)
157 .ok_or_else(|| "object metadata length overflow".to_owned())?;
158
159 if description_end > payload.len() {
160 return Err("object metadata extends past the payload".to_owned());
161 }
162
163 let filename = std::str::from_utf8(&payload[PAYLOAD_HEADER_BYTES..filename_end])
164 .map_err(|error| format!("object filename is not valid UTF-8: {error}"))?
165 .to_owned();
166 let file_type = std::str::from_utf8(&payload[filename_end..file_type_end])
167 .map_err(|error| format!("object file type is not valid UTF-8: {error}"))?
168 .to_owned();
169 let description = std::str::from_utf8(&payload[file_type_end..description_end])
170 .map_err(|error| format!("object description is not valid UTF-8: {error}"))?
171 .to_owned();
172
173 Ok(Object {
174 filename,
175 file_type,
176 description,
177 data: payload[description_end..].to_vec(),
178 })
179}
180
181fn decode_length(payload: &[u8], offset: usize, field: &str) -> Result<usize, String> {
182 let bytes: [u8; 8] = payload[offset..offset + 8]
183 .try_into()
184 .expect("object length fields are inside the fixed header");
185 usize::try_from(u64::from_le_bytes(bytes))
186 .map_err(|_| format!("object {field} length exceeds usize"))
187}