Something went wrong. Try again.
Monorepo for Tangled
Something went wrong. Try again.
11 kB · 352 lines
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353pub mod blocks;pub mod chain;pub mod comment;pub mod git_ref;pub mod issue;pub mod mst;pub mod proof;pub mod reaction;
use bytes::Bytes;use cid::Cid;use knot_types::{AccountDid, RecordBody, RecordBodyError, UnixSeconds};use serde::{Serialize, Serializer};#[derive(Debug, thiserror::Error)]pub enum RecordError { #[error("atproto block error: {0}")] Block(#[from] jacquard_repo::RepoError), #[error("covering proof failed to invert the commit cold")] ColdInversion, #[error("malformed atproto record: {0}")] Malformed(String), #[error("createdAt {0} is outside the rfc3339 range")] CreatedAtUnrenderable(i64),}
pub type Result<T, E = RecordError> = std::result::Result<T, E>;
pub fn member_invite_collection() -> knot_types::RecordCollection { InviteCollection::Member.collection()}
pub fn collaborator_invite_collection() -> knot_types::RecordCollection { InviteCollection::Collaborator.collection()}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum InviteCollection { Member, Collaborator,}
impl InviteCollection { pub const fn as_str(self) -> &'static str { match self { Self::Member => "sh.tangled.knot.memberInvite", Self::Collaborator => "sh.tangled.repo.collaboratorInvite", } }
pub fn collection(self) -> knot_types::RecordCollection { knot_types::RecordCollection::new(self.as_str()).expect("invite collection is a valid nsid") }}
#[derive(Debug, Clone, PartialEq, Eq)]pub struct InviteRecord { kind: InviteCollection, created_at: CreatedAt, editor: AccountDid,}
impl InviteRecord { pub fn new( kind: InviteCollection, created_at: UnixSeconds, editor: AccountDid, ) -> Result<Self, RecordError> { Ok(Self { kind, created_at: CreatedAt::at(created_at)?, editor, }) }
pub fn bytes(&self) -> Vec<u8> { dag_cbor(self) }
pub fn body(&self) -> Result<RecordBody, RecordBodyError> { encoded(self) }}
#[derive(Serialize)]struct InviteEncoding<'a> { #[serde(rename = "$type")] record_type: knot_types::RecordCollection, #[serde(rename = "createdAt")] created_at: &'a CreatedAt, #[serde(rename = "x-tngl-editor")] editor: &'a AccountDid,}
impl Serialize for InviteRecord { fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { InviteEncoding { record_type: self.kind.collection(), created_at: &self.created_at, editor: &self.editor, } .serialize(serializer) }}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]#[serde(transparent)]pub struct CreatedAt(String);
impl CreatedAt { pub fn at(time: UnixSeconds) -> Result<Self, RecordError> { chrono::DateTime::from_timestamp(time.get(), 0) .map(|at| Self(at.to_rfc3339_opts(chrono::SecondsFormat::Secs, true))) .ok_or(RecordError::CreatedAtUnrenderable(time.get())) }}
pub(crate) fn dag_cbor<T: Serialize>(value: &T) -> Vec<u8> { serde_ipld_dagcbor::to_vec(value).expect("record text fields and lists encode as dag-cbor")}
pub(crate) fn encoded<T: Serialize>(value: &T) -> Result<RecordBody, RecordBodyError> { RecordBody::new(dag_cbor(value))}
#[cfg(test)]pub(crate) fn frozen(hex: &str) -> Vec<u8> { hex.as_bytes() .chunks_exact(2) .map(|pair| u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).unwrap()) .collect()}
pub fn cid_of(block: &[u8]) -> Cid { knot_types::dag_cbor_cid(block)}
pub fn car_bytes(root: Cid, blocks: &std::collections::BTreeMap<Cid, Bytes>) -> Vec<u8> { let header = iroh_car::CarHeader::new_v1(vec![root]); let header_bytes = header.encode().expect("a car header encodes"); let mut out = Vec::new(); varint(&mut out, header_bytes.len()); out.extend_from_slice(&header_bytes); blocks.iter().for_each(|(cid, data)| { let cid_bytes = cid.to_bytes(); varint(&mut out, cid_bytes.len() + data.len()); out.extend_from_slice(&cid_bytes); out.extend_from_slice(data); }); out}
fn varint(out: &mut Vec<u8>, value: usize) { let chunks = std::iter::successors(Some(value), |value| (*value >= 0x80).then(|| value >> 7)); out.extend(chunks.map(|value| ((value & 0x7f) as u8) | u8::from(value >= 0x80) << 7));}
#[derive(Debug, thiserror::Error)]pub enum CarError { #[error("car is cut short at byte {0}")] Truncated(usize), #[error("car varint at byte {0} doesn't fit a length")] Varint(usize), #[error("car header doesn't decode: {0}")] Header(String), #[error("car section at byte {at} is missing its cid: {reason}")] Cid { at: usize, reason: String },}
#[derive(Debug, Clone, PartialEq, Eq)]pub struct Car { pub roots: Vec<Cid>, pub blocks: std::collections::BTreeMap<Cid, Bytes>,}
struct Varint { value: usize, after: usize,}
fn read_varint(bytes: &[u8], at: usize) -> Result<Varint, CarError> { let tail = bytes.get(at..).ok_or(CarError::Truncated(at))?; let last = tail .iter() .take(10) .position(|byte| byte & 0x80 == 0) .ok_or(CarError::Truncated(at))?; let value = tail[..=last] .iter() .enumerate() .try_fold(0usize, |value, (index, byte)| { let part = usize::from(byte & 0x7f); let shift = (7 * index) as u32; part.checked_shl(shift) .filter(|shifted| shifted >> shift == part) .map(|shifted| value | shifted) }) .ok_or(CarError::Varint(at))?; Ok(Varint { value, after: at + last + 1, })}
pub fn car_blocks(bytes: &[u8]) -> Result<Car, CarError> { let framed = read_varint(bytes, 0)?; let header = bytes .get(framed.after..framed.after + framed.value) .ok_or(CarError::Truncated(framed.after))?; let roots = iroh_car::CarHeader::decode(header) .map_err(|error| CarError::Header(error.to_string()))? .roots() .to_vec(); let mut at = framed.after + framed.value; let blocks = std::iter::from_fn(|| { (at < bytes.len()).then(|| { let framed = read_varint(bytes, at)?; let section = bytes .get(framed.after..framed.after + framed.value) .ok_or(CarError::Truncated(framed.after))?; let mut cursor = std::io::Cursor::new(§ion); let cid = Cid::read_bytes(&mut cursor).map_err(|error| CarError::Cid { at: framed.after, reason: error.to_string(), })?; at = framed.after + framed.value; Ok(( cid, Bytes::copy_from_slice(§ion[cursor.position() as usize..]), )) }) }) .collect::<Result<std::collections::BTreeMap<_, _>, CarError>>()?; Ok(Car { roots, blocks })}
#[cfg(test)]mod car_tests { use super::*; use std::collections::BTreeMap;
#[tokio::test] async fn the_sync_car_writer_matches_jacquards_byte_for_byte() { let one = cid_of(b"a record body"); let two = cid_of(b"another record body"); let root = cid_of(b"a commit"); let blocks = BTreeMap::from([ (one, Bytes::from_static(b"a record body")), (two, Bytes::from_static(b"another record body")), (root, Bytes::from_static(b"a commit")), ]); assert_eq!( car_bytes(root, &blocks), jacquard_repo::car::write_car_bytes(root, blocks.clone()) .await .unwrap() ); }
#[test] fn car_reader_inverts_car_writer() { let one = cid_of(b"a record body"); let root = cid_of(b"a commit"); let blocks = BTreeMap::from([ (one, Bytes::from_static(b"a record body")), (root, Bytes::from_static(b"a commit")), ]); let written = car_bytes(root, &blocks); assert_eq!( car_blocks(&written).unwrap(), Car { roots: vec![root], blocks: blocks.clone() } ); assert_eq!( car_bytes(root, &car_blocks(&written).unwrap().blocks), written, "a read then written car is the same bytes" ); assert!(matches!( car_blocks(&written[..written.len() - 3]), Err(CarError::Truncated(_)) )); }}
pub struct CommitSigner<'a>(&'a dyn knot_runtime::Signer);
impl<'a> CommitSigner<'a> { pub fn new(signer: &'a dyn knot_runtime::Signer) -> Self { Self(signer) }}
impl jacquard_repo::commit::SigningKey for CommitSigner<'_> { fn sign_bytes(&self, data: &[u8]) -> jacquard_repo::Result<Bytes> { Ok(Bytes::from(self.0.sign(data).as_bytes().to_vec())) }
fn public_key(&self) -> Vec<u8> { self.0.public_key().as_bytes().to_vec() }}
#[cfg(test)]mod tests { use super::*;
const MEMBER: &str = "a3652474797065781c73682e74616e676c65642e6b6e6f742e6d656d626572496e766974656963726561746564417474323032352d30382d31335431333a30313a33365a6d782d746e676c2d656469746f7278206469643a706c633a3366776563646e76746373636a6e72783270346e37616c7a";
fn editor() -> AccountDid { AccountDid::new("did:plc:3fwecdnvtcscjnrx2p4n7alz").unwrap() }
#[test] fn the_invite_record_bytes_are_frozen() { let member = InviteRecord::new( InviteCollection::Member, UnixSeconds::new(1_755_090_096), editor(), ) .unwrap(); assert_eq!( member.bytes(), frozen(MEMBER), "the first emitted record freezes these bytes: $type first, length-first key order, seconds precision, utc z" ); let collaborator = InviteRecord::new( InviteCollection::Collaborator, UnixSeconds::new(1_755_090_096), editor(), ) .unwrap(); assert_eq!( collaborator.bytes()[7..9], [0x78, 0x22], "the collaborator variant differs only in carrying its own 34 character $type" ); }
#[test] fn a_timestamp_outside_the_rfc3339_range_is_refused_at_construction() { assert!(matches!( InviteRecord::new( InviteCollection::Member, UnixSeconds::new(i64::MAX), editor() ), Err(RecordError::CreatedAtUnrenderable(i64::MAX)) )); }}