#![forbid(unsafe_code)]
mod export;
mod file;
mod import;
pub use export::{ExportError, PersistError, export_commits, export_entries, persist_entries};
pub use file::{FileStore, FileStoreError};
pub use import::{
Checkpoint, Genesis, LoadError, load_from_checkpoint, load_principal,
load_principal_from_commits,
};
use cyphr::state::PrincipalGenesis;
use serde_json::value::RawValue;
pub trait Store {
type Error: std::error::Error + Send + Sync + 'static;
fn append_entry(&self, pr: &PrincipalGenesis, entry: &Entry) -> Result<(), Self::Error>;
fn get_entries(&self, pr: &PrincipalGenesis) -> Result<Vec<Entry>, Self::Error>;
fn get_entries_range(
&self,
pr: &PrincipalGenesis,
opts: &QueryOpts,
) -> Result<Vec<Entry>, Self::Error>;
fn exists(&self, pr: &PrincipalGenesis) -> Result<bool, Self::Error>;
}
#[derive(Default, Debug, Clone)]
pub struct QueryOpts {
pub after: Option<i64>,
pub before: Option<i64>,
pub limit: Option<usize>,
}
#[derive(Debug, Clone)]
pub struct Entry {
raw_json: Box<RawValue>,
pub now: i64,
}
impl Entry {
pub fn from_json(json: String) -> Result<Self, EntryError> {
let raw_json: Box<RawValue> =
serde_json::from_str(&json).map_err(|_| EntryError::InvalidJson)?;
let now = Self::extract_now(&json)?;
Ok(Self { raw_json, now })
}
pub fn from_raw_value(raw: Box<RawValue>) -> Result<Self, EntryError> {
let now = Self::extract_now(raw.get())?;
Ok(Self { raw_json: raw, now })
}
pub fn from_value(value: &serde_json::Value) -> Result<Self, EntryError> {
let json = serde_json::to_string(value).map_err(|_| EntryError::InvalidJson)?;
Self::from_json(json)
}
pub fn raw_json(&self) -> &str {
self.raw_json.get()
}
pub fn as_value(&self) -> Result<serde_json::Value, EntryError> {
serde_json::from_str(self.raw_json.get()).map_err(|_| EntryError::InvalidJson)
}
pub fn pay_bytes(&self) -> Result<Vec<u8>, EntryError> {
#[derive(serde::Deserialize)]
struct PayExtractor<'a> {
#[serde(borrow)]
pay: &'a RawValue,
}
let extractor: PayExtractor =
serde_json::from_str(self.raw_json.get()).map_err(|_| EntryError::MissingPay)?;
Ok(extractor.pay.get().as_bytes().to_vec())
}
fn extract_now(json: &str) -> Result<i64, EntryError> {
#[derive(serde::Deserialize)]
struct PayNow {
now: i64,
}
#[derive(serde::Deserialize)]
struct NowExtractor {
pay: PayNow,
}
let extractor: NowExtractor =
serde_json::from_str(json).map_err(|_| EntryError::MissingNow)?;
Ok(extractor.pay.now)
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct KeyEntry {
pub alg: String,
#[serde(rename = "pub")]
pub pub_key: String,
pub tmb: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub tag: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub now: Option<i64>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CommitEntry {
#[serde(rename = "txs")]
pub cozies: Vec<serde_json::Value>,
pub keys: Vec<KeyEntry>,
#[serde(alias = "ts")]
pub commit_id: String,
#[serde(rename = "ar")]
pub auth_root: String,
#[serde(alias = "cs")]
pub sr: String,
pub pr: String,
}
impl CommitEntry {
pub fn new(
cozies: Vec<serde_json::Value>,
keys: Vec<KeyEntry>,
commit_id: String,
auth_root: String,
sr: String,
pr: String,
) -> Self {
Self {
cozies,
keys,
commit_id,
auth_root,
sr,
pr,
}
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
}
#[derive(Debug, thiserror::Error)]
pub enum EntryError {
#[error("invalid JSON")]
InvalidJson,
#[error("entry missing pay.now field")]
MissingNow,
#[error("entry missing pay field")]
MissingPay,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn entry_from_json_extracts_now() {
let json = r#"{"pay":{"now":12345,"typ":"test"},"sig":"AAAA"}"#.to_string();
let entry = Entry::from_json(json).unwrap();
assert_eq!(entry.now, 12345);
}
#[test]
fn entry_raw_json_preserves_bytes() {
let json = r#"{"pay":{"now":12345,"typ":"test"},"sig":"AAAA"}"#.to_string();
let entry = Entry::from_json(json.clone()).unwrap();
assert_eq!(entry.raw_json(), json);
}
#[test]
fn entry_pay_bytes_extracts_exact_bytes() {
let json = r#"{"pay":{"now":12345,"typ":"test"},"sig":"AAAA"}"#.to_string();
let entry = Entry::from_json(json).unwrap();
let pay_bytes = entry.pay_bytes().unwrap();
assert_eq!(
String::from_utf8(pay_bytes).unwrap(),
r#"{"now":12345,"typ":"test"}"#
);
}
#[test]
fn entry_missing_now_fails() {
let json = r#"{"pay":{"typ":"test"},"sig":"AAAA"}"#.to_string();
let result = Entry::from_json(json);
assert!(matches!(result, Err(EntryError::MissingNow)));
}
#[test]
fn entry_missing_pay_fails() {
let json = r#"{"sig":"AAAA"}"#.to_string();
let result = Entry::from_json(json);
assert!(matches!(result, Err(EntryError::MissingNow)));
}
#[test]
fn entry_invalid_json_fails() {
let json = "not json".to_string();
let result = Entry::from_json(json);
assert!(matches!(result, Err(EntryError::InvalidJson)));
}
}