use kcode_k1_peering::K1Peering;
use kcode_k1_transaction::{REGISTER_AT_TIP, SubsystemId, Transaction};
pub use kcode_k1_transaction_id::TxId;
use kcode_k1_txn_ordering::{K1TxnOrdering, Subsystem};
use std::sync::Arc;
const PAYLOAD_VERSION: u8 = 1;
const PAYLOAD_HEADER_BYTES: usize = 25;
const OBJECT_SUBSYSTEM_BYTES: [u8; 20] = *b"k1-objects-subsystem";
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Object {
pub filename: String,
pub file_type: String,
pub description: String,
pub data: Vec<u8>,
}
pub trait ObjectSaveObserver: Send + Sync + 'static {
fn saved(&self, id: TxId);
}
pub struct K1Objects {
ordering: Arc<K1TxnOrdering>,
peering: Arc<K1Peering>,
observer: Arc<dyn ObjectSaveObserver>,
}
impl K1Objects {
pub fn open(ordering: Arc<K1TxnOrdering>, peering: Arc<K1Peering>) -> Result<Self, String> {
Self::open_with_save_observer(ordering, peering, Arc::new(NoopSaveObserver))
}
pub fn open_with_save_observer(
ordering: Arc<K1TxnOrdering>,
peering: Arc<K1Peering>,
observer: Arc<dyn ObjectSaveObserver>,
) -> Result<Self, String> {
ordering.register_subsystem(
object_subsystem(),
Some(REGISTER_AT_TIP),
Arc::new(StatelessObjects),
)?;
Ok(Self {
ordering,
peering,
observer,
})
}
pub fn save(
&self,
filename: &str,
file_type: &str,
description: &str,
data: &[u8],
) -> Result<TxId, String> {
let payload = encode_payload(filename, file_type, description, data)?;
let id = self.peering.submit_txn(object_subsystem(), &payload)?;
self.observer.saved(id);
Ok(id)
}
pub fn load(&self, id: TxId) -> Result<Option<Object>, String> {
let Some(bytes) = self.ordering.get_txn(id)? else {
return Ok(None);
};
let transaction = Transaction::parse(&bytes)
.map_err(|message| format!("canonical transaction is invalid: {message}"))?;
if transaction.subsystem() != object_subsystem() {
return Ok(None);
}
decode_payload(transaction.payload()).map(Some)
}
}
struct NoopSaveObserver;
impl ObjectSaveObserver for NoopSaveObserver {
fn saved(&self, _id: TxId) {}
}
struct StatelessObjects;
impl Subsystem for StatelessObjects {
fn submit_txn(&self, _id: TxId, _payload: &[u8]) -> Result<(), String> {
Ok(())
}
fn reorg(&self) -> Result<(), String> {
Ok(())
}
}
fn object_subsystem() -> SubsystemId {
SubsystemId::from_bytes(OBJECT_SUBSYSTEM_BYTES)
.expect("Objects subsystem ID is valid fixed-width UTF-8")
}
fn encode_payload(
filename: &str,
file_type: &str,
description: &str,
data: &[u8],
) -> Result<Vec<u8>, String> {
let filename_len = u64::try_from(filename.len())
.map_err(|_| "filename length exceeds the object wire format".to_owned())?;
let file_type_len = u64::try_from(file_type.len())
.map_err(|_| "file type length exceeds the object wire format".to_owned())?;
let description_len = u64::try_from(description.len())
.map_err(|_| "description length exceeds the object wire format".to_owned())?;
let total_len = PAYLOAD_HEADER_BYTES
.checked_add(filename.len())
.and_then(|length| length.checked_add(file_type.len()))
.and_then(|length| length.checked_add(description.len()))
.and_then(|length| length.checked_add(data.len()))
.ok_or_else(|| "object payload length exceeds usize".to_owned())?;
let mut payload = Vec::new();
payload
.try_reserve_exact(total_len)
.map_err(|error| format!("cannot allocate object payload: {error}"))?;
payload.push(PAYLOAD_VERSION);
payload.extend_from_slice(&filename_len.to_le_bytes());
payload.extend_from_slice(&file_type_len.to_le_bytes());
payload.extend_from_slice(&description_len.to_le_bytes());
payload.extend_from_slice(filename.as_bytes());
payload.extend_from_slice(file_type.as_bytes());
payload.extend_from_slice(description.as_bytes());
payload.extend_from_slice(data);
debug_assert_eq!(payload.len(), total_len);
Ok(payload)
}
fn decode_payload(payload: &[u8]) -> Result<Object, String> {
if payload.len() < PAYLOAD_HEADER_BYTES {
return Err(format!(
"object payload must contain at least {PAYLOAD_HEADER_BYTES} bytes"
));
}
if payload[0] != PAYLOAD_VERSION {
return Err(format!("unsupported object payload version {}", payload[0]));
}
let filename_len = decode_length(payload, 1, "filename")?;
let file_type_len = decode_length(payload, 9, "file type")?;
let description_len = decode_length(payload, 17, "description")?;
let filename_end = PAYLOAD_HEADER_BYTES
.checked_add(filename_len)
.ok_or_else(|| "object metadata length overflow".to_owned())?;
let file_type_end = filename_end
.checked_add(file_type_len)
.ok_or_else(|| "object metadata length overflow".to_owned())?;
let description_end = file_type_end
.checked_add(description_len)
.ok_or_else(|| "object metadata length overflow".to_owned())?;
if description_end > payload.len() {
return Err("object metadata extends past the payload".to_owned());
}
let filename = std::str::from_utf8(&payload[PAYLOAD_HEADER_BYTES..filename_end])
.map_err(|error| format!("object filename is not valid UTF-8: {error}"))?
.to_owned();
let file_type = std::str::from_utf8(&payload[filename_end..file_type_end])
.map_err(|error| format!("object file type is not valid UTF-8: {error}"))?
.to_owned();
let description = std::str::from_utf8(&payload[file_type_end..description_end])
.map_err(|error| format!("object description is not valid UTF-8: {error}"))?
.to_owned();
Ok(Object {
filename,
file_type,
description,
data: payload[description_end..].to_vec(),
})
}
fn decode_length(payload: &[u8], offset: usize, field: &str) -> Result<usize, String> {
let bytes: [u8; 8] = payload[offset..offset + 8]
.try_into()
.expect("object length fields are inside the fixed header");
usize::try_from(u64::from_le_bytes(bytes))
.map_err(|_| format!("object {field} length exceeds usize"))
}