use std::fmt;
use base64::Engine as _;
use ed25519_dalek::{Signature as Ed25519Signature, Signer, SigningKey, Verifier, VerifyingKey};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use sha2::{Digest, Sha256};
use time::format_description::well_known::Rfc3339;
use time::OffsetDateTime;
const ATOM_BINARY_MAGIC: &[u8; 4] = b"FATM";
const ATOM_BINARY_VERSION: u8 = 1;
const ATOM_ID_BYTES: usize = 32;
const ED25519_PUBLIC_KEY_BYTES: usize = 32;
const ED25519_SIGNATURE_BYTES: usize = 64;
#[derive(Debug)]
pub enum AtomError {
Json(String),
Binary(String),
ContentHashMismatch { expected: AtomId, actual: AtomId },
InvalidSignature(&'static str),
Apply(String),
Invalid(String),
}
impl fmt::Display for AtomError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AtomError::Json(message) => write!(f, "atom json error: {message}"),
AtomError::Binary(message) => write!(f, "atom binary error: {message}"),
AtomError::ContentHashMismatch { expected, actual } => write!(
f,
"atom id mismatch: expected {expected}, recomputed {actual}",
),
AtomError::InvalidSignature(role) => write!(f, "{role} signature failed verification"),
AtomError::Apply(message) => write!(f, "atom apply/invert error: {message}"),
AtomError::Invalid(message) => write!(f, "atom invalid: {message}"),
}
}
}
impl std::error::Error for AtomError {}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct AtomId(pub [u8; ATOM_ID_BYTES]);
impl AtomId {
pub fn to_hex(&self) -> String {
hex::encode(self.0)
}
pub fn from_hex(raw: &str) -> Result<Self, AtomError> {
let bytes = hex::decode(raw)
.map_err(|error| AtomError::Invalid(format!("invalid AtomId hex: {error}")))?;
if bytes.len() != ATOM_ID_BYTES {
return Err(AtomError::Invalid(format!(
"AtomId must be {ATOM_ID_BYTES} bytes, got {}",
bytes.len()
)));
}
let mut out = [0u8; ATOM_ID_BYTES];
out.copy_from_slice(&bytes);
Ok(Self(out))
}
}
impl fmt::Debug for AtomId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "AtomId({})", self.to_hex())
}
}
impl fmt::Display for AtomId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_hex())
}
}
impl Serialize for AtomId {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_hex())
}
}
impl<'de> Deserialize<'de> for AtomId {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let raw = String::deserialize(deserializer)?;
AtomId::from_hex(&raw).map_err(serde::de::Error::custom)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum TextOp {
Insert { offset: u64, content: String },
Delete { offset: u64, content: String },
}
impl TextOp {
pub fn invert(&self) -> TextOp {
match self {
TextOp::Insert { offset, content } => TextOp::Delete {
offset: *offset,
content: content.clone(),
},
TextOp::Delete { offset, content } => TextOp::Insert {
offset: *offset,
content: content.clone(),
},
}
}
pub fn apply(&self, document: &mut Vec<u8>) -> Result<(), AtomError> {
match self {
TextOp::Insert { offset, content } => {
let offset_usize = usize::try_from(*offset).map_err(|_| {
AtomError::Apply(format!("insert offset {offset} exceeds usize::MAX"))
})?;
if offset_usize > document.len() {
return Err(AtomError::Apply(format!(
"insert offset {offset_usize} > document length {}",
document.len()
)));
}
document.splice(offset_usize..offset_usize, content.bytes());
Ok(())
}
TextOp::Delete { offset, content } => {
let offset_usize = usize::try_from(*offset).map_err(|_| {
AtomError::Apply(format!("delete offset {offset} exceeds usize::MAX"))
})?;
let end = offset_usize.checked_add(content.len()).ok_or_else(|| {
AtomError::Apply(format!(
"delete range overflows: offset {offset_usize} + len {}",
content.len()
))
})?;
if end > document.len() {
return Err(AtomError::Apply(format!(
"delete range {offset_usize}..{end} exceeds document length {}",
document.len()
)));
}
if &document[offset_usize..end] != content.as_bytes() {
return Err(AtomError::Apply(format!(
"delete content mismatch at offset {offset_usize}",
)));
}
document.drain(offset_usize..end);
Ok(())
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Provenance {
pub principal: String,
pub persona: String,
pub agent_run_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
pub trace_id: String,
pub transcript_ref: String,
#[serde(with = "time::serde::rfc3339")]
pub timestamp: OffsetDateTime,
}
impl Provenance {
pub fn new(
principal: impl Into<String>,
persona: impl Into<String>,
agent_run_id: impl Into<String>,
trace_id: impl Into<String>,
transcript_ref: impl Into<String>,
) -> Self {
Self {
principal: principal.into(),
persona: persona.into(),
agent_run_id: agent_run_id.into(),
tool_call_id: None,
trace_id: trace_id.into(),
transcript_ref: transcript_ref.into(),
timestamp: OffsetDateTime::now_utc(),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct AtomSignature {
pub principal_key: [u8; ED25519_PUBLIC_KEY_BYTES],
pub principal_sig: [u8; ED25519_SIGNATURE_BYTES],
pub persona_key: [u8; ED25519_PUBLIC_KEY_BYTES],
pub persona_sig: [u8; ED25519_SIGNATURE_BYTES],
}
impl fmt::Debug for AtomSignature {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AtomSignature")
.field("principal_key", &hex::encode(self.principal_key))
.field("persona_key", &hex::encode(self.persona_key))
.finish_non_exhaustive()
}
}
#[derive(Serialize, Deserialize)]
struct AtomSignatureWire {
principal_key: String,
principal_sig: String,
persona_key: String,
persona_sig: String,
}
impl Serialize for AtomSignature {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let b64 = base64::engine::general_purpose::STANDARD;
AtomSignatureWire {
principal_key: b64.encode(self.principal_key),
principal_sig: b64.encode(self.principal_sig),
persona_key: b64.encode(self.persona_key),
persona_sig: b64.encode(self.persona_sig),
}
.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for AtomSignature {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let wire = AtomSignatureWire::deserialize(deserializer)?;
let b64 = base64::engine::general_purpose::STANDARD;
fn copy_into<const N: usize, E: serde::de::Error>(
label: &str,
raw: Vec<u8>,
) -> Result<[u8; N], E> {
if raw.len() != N {
return Err(serde::de::Error::custom(format!(
"{label} must be {N} bytes, got {}",
raw.len()
)));
}
let mut out = [0u8; N];
out.copy_from_slice(&raw);
Ok(out)
}
let principal_key_bytes = b64
.decode(wire.principal_key.as_bytes())
.map_err(serde::de::Error::custom)?;
let principal_sig_bytes = b64
.decode(wire.principal_sig.as_bytes())
.map_err(serde::de::Error::custom)?;
let persona_key_bytes = b64
.decode(wire.persona_key.as_bytes())
.map_err(serde::de::Error::custom)?;
let persona_sig_bytes = b64
.decode(wire.persona_sig.as_bytes())
.map_err(serde::de::Error::custom)?;
Ok(AtomSignature {
principal_key: copy_into::<ED25519_PUBLIC_KEY_BYTES, D::Error>(
"principal_key",
principal_key_bytes,
)?,
principal_sig: copy_into::<ED25519_SIGNATURE_BYTES, D::Error>(
"principal_sig",
principal_sig_bytes,
)?,
persona_key: copy_into::<ED25519_PUBLIC_KEY_BYTES, D::Error>(
"persona_key",
persona_key_bytes,
)?,
persona_sig: copy_into::<ED25519_SIGNATURE_BYTES, D::Error>(
"persona_sig",
persona_sig_bytes,
)?,
})
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Atom {
pub id: AtomId,
pub ops: Vec<TextOp>,
pub parents: Vec<AtomId>,
pub provenance: Provenance,
pub signature: AtomSignature,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub inverse_of: Option<AtomId>,
}
impl Atom {
pub fn sign(
ops: Vec<TextOp>,
parents: Vec<AtomId>,
provenance: Provenance,
inverse_of: Option<AtomId>,
principal_key: &SigningKey,
persona_key: &SigningKey,
) -> Result<Self, AtomError> {
let body_bytes = encode_body_canonical(&ops, &parents, &provenance, &inverse_of)?;
let id = AtomId(Sha256::digest(&body_bytes).into());
let principal_sig = principal_key.sign(&id.0);
let persona_sig = persona_key.sign(&id.0);
Ok(Atom {
id,
ops,
parents,
provenance,
inverse_of,
signature: AtomSignature {
principal_key: principal_key.verifying_key().to_bytes(),
principal_sig: principal_sig.to_bytes(),
persona_key: persona_key.verifying_key().to_bytes(),
persona_sig: persona_sig.to_bytes(),
},
})
}
pub fn invert(
target: &Atom,
provenance: Provenance,
principal_key: &SigningKey,
persona_key: &SigningKey,
) -> Result<Self, AtomError> {
let ops: Vec<TextOp> = target.ops.iter().rev().map(TextOp::invert).collect();
Atom::sign(
ops,
vec![target.id],
provenance,
Some(target.id),
principal_key,
persona_key,
)
}
pub fn verify_content_hash(&self) -> Result<(), AtomError> {
let body_bytes =
encode_body_canonical(&self.ops, &self.parents, &self.provenance, &self.inverse_of)?;
let recomputed = AtomId(Sha256::digest(&body_bytes).into());
if recomputed != self.id {
return Err(AtomError::ContentHashMismatch {
expected: self.id,
actual: recomputed,
});
}
Ok(())
}
pub fn verify_signatures(&self) -> Result<(), AtomError> {
let signature_payload = self.id.0;
let principal_key = VerifyingKey::from_bytes(&self.signature.principal_key)
.map_err(|error| AtomError::Invalid(format!("principal key: {error}")))?;
let persona_key = VerifyingKey::from_bytes(&self.signature.persona_key)
.map_err(|error| AtomError::Invalid(format!("persona key: {error}")))?;
let principal_sig = Ed25519Signature::from_bytes(&self.signature.principal_sig);
let persona_sig = Ed25519Signature::from_bytes(&self.signature.persona_sig);
principal_key
.verify(&signature_payload, &principal_sig)
.map_err(|_| AtomError::InvalidSignature("principal"))?;
persona_key
.verify(&signature_payload, &persona_sig)
.map_err(|_| AtomError::InvalidSignature("persona"))?;
Ok(())
}
pub fn verify(&self) -> Result<(), AtomError> {
self.verify_content_hash()?;
self.verify_signatures()
}
pub fn apply(&self, document: &mut Vec<u8>) -> Result<(), AtomError> {
for op in &self.ops {
op.apply(document)?;
}
Ok(())
}
pub fn to_json(&self) -> Result<String, AtomError> {
serde_json::to_string(self).map_err(|error| AtomError::Json(error.to_string()))
}
pub fn from_json_slice(bytes: &[u8]) -> Result<Self, AtomError> {
let atom: Atom =
serde_json::from_slice(bytes).map_err(|error| AtomError::Json(error.to_string()))?;
atom.verify_content_hash()?;
Ok(atom)
}
pub fn to_binary(&self) -> Result<Vec<u8>, AtomError> {
encode_atom_binary(self)
}
pub fn from_binary_slice(bytes: &[u8]) -> Result<Self, AtomError> {
let atom = decode_atom_binary(bytes)?;
atom.verify_content_hash()?;
Ok(atom)
}
}
fn encode_body_canonical(
ops: &[TextOp],
parents: &[AtomId],
provenance: &Provenance,
inverse_of: &Option<AtomId>,
) -> Result<Vec<u8>, AtomError> {
let mut out = Vec::new();
out.extend_from_slice(ATOM_BINARY_MAGIC);
out.push(ATOM_BINARY_VERSION);
write_ops(&mut out, ops);
write_parents(&mut out, parents);
write_provenance(&mut out, provenance)?;
write_optional_atom_id(&mut out, inverse_of);
Ok(out)
}
fn encode_atom_binary(atom: &Atom) -> Result<Vec<u8>, AtomError> {
let mut out =
encode_body_canonical(&atom.ops, &atom.parents, &atom.provenance, &atom.inverse_of)?;
out.extend_from_slice(&atom.id.0);
out.extend_from_slice(&atom.signature.principal_key);
out.extend_from_slice(&atom.signature.principal_sig);
out.extend_from_slice(&atom.signature.persona_key);
out.extend_from_slice(&atom.signature.persona_sig);
Ok(out)
}
fn write_ops(out: &mut Vec<u8>, ops: &[TextOp]) {
write_u32(out, ops.len() as u32);
for op in ops {
match op {
TextOp::Insert { offset, content } => {
out.push(0);
write_u64(out, *offset);
write_bytes(out, content.as_bytes());
}
TextOp::Delete { offset, content } => {
out.push(1);
write_u64(out, *offset);
write_bytes(out, content.as_bytes());
}
}
}
}
fn write_parents(out: &mut Vec<u8>, parents: &[AtomId]) {
write_u32(out, parents.len() as u32);
for parent in parents {
out.extend_from_slice(&parent.0);
}
}
fn write_provenance(out: &mut Vec<u8>, provenance: &Provenance) -> Result<(), AtomError> {
write_str(out, &provenance.principal);
write_str(out, &provenance.persona);
write_str(out, &provenance.agent_run_id);
match &provenance.tool_call_id {
Some(id) => {
out.push(1);
write_str(out, id);
}
None => out.push(0),
}
write_str(out, &provenance.trace_id);
write_str(out, &provenance.transcript_ref);
let formatted = provenance
.timestamp
.format(&Rfc3339)
.map_err(|error| AtomError::Invalid(format!("timestamp format: {error}")))?;
write_str(out, &formatted);
Ok(())
}
fn write_optional_atom_id(out: &mut Vec<u8>, value: &Option<AtomId>) {
match value {
Some(id) => {
out.push(1);
out.extend_from_slice(&id.0);
}
None => out.push(0),
}
}
fn write_bytes(out: &mut Vec<u8>, bytes: &[u8]) {
write_u32(out, bytes.len() as u32);
out.extend_from_slice(bytes);
}
fn write_str(out: &mut Vec<u8>, value: &str) {
write_bytes(out, value.as_bytes());
}
fn write_u32(out: &mut Vec<u8>, value: u32) {
out.extend_from_slice(&value.to_le_bytes());
}
fn write_u64(out: &mut Vec<u8>, value: u64) {
out.extend_from_slice(&value.to_le_bytes());
}
struct Cursor<'a> {
bytes: &'a [u8],
offset: usize,
}
impl<'a> Cursor<'a> {
fn new(bytes: &'a [u8]) -> Self {
Self { bytes, offset: 0 }
}
fn remaining(&self) -> usize {
self.bytes.len() - self.offset
}
fn take(&mut self, n: usize) -> Result<&'a [u8], AtomError> {
if self.remaining() < n {
return Err(AtomError::Binary(format!(
"truncated: need {n} bytes, have {}",
self.remaining()
)));
}
let slice = &self.bytes[self.offset..self.offset + n];
self.offset += n;
Ok(slice)
}
fn take_u8(&mut self) -> Result<u8, AtomError> {
Ok(self.take(1)?[0])
}
fn take_u32(&mut self) -> Result<u32, AtomError> {
let bytes = self.take(4)?;
Ok(u32::from_le_bytes(bytes.try_into().unwrap()))
}
fn take_u64(&mut self) -> Result<u64, AtomError> {
let bytes = self.take(8)?;
Ok(u64::from_le_bytes(bytes.try_into().unwrap()))
}
fn take_bytes(&mut self) -> Result<Vec<u8>, AtomError> {
let len = self.take_u32()? as usize;
Ok(self.take(len)?.to_vec())
}
fn take_string(&mut self) -> Result<String, AtomError> {
let bytes = self.take_bytes()?;
String::from_utf8(bytes).map_err(|error| AtomError::Binary(format!("utf8: {error}")))
}
}
fn decode_atom_binary(bytes: &[u8]) -> Result<Atom, AtomError> {
let mut cursor = Cursor::new(bytes);
let magic = cursor.take(ATOM_BINARY_MAGIC.len())?;
if magic != ATOM_BINARY_MAGIC {
return Err(AtomError::Binary("magic mismatch".to_string()));
}
let version = cursor.take_u8()?;
if version != ATOM_BINARY_VERSION {
return Err(AtomError::Binary(format!(
"unsupported version {version}, expected {ATOM_BINARY_VERSION}"
)));
}
let ops_len = cursor.take_u32()? as usize;
let mut ops = Vec::with_capacity(ops_len);
for _ in 0..ops_len {
let tag = cursor.take_u8()?;
let offset = cursor.take_u64()?;
let content = cursor.take_string()?;
ops.push(match tag {
0 => TextOp::Insert { offset, content },
1 => TextOp::Delete { offset, content },
other => return Err(AtomError::Binary(format!("unknown op tag {other}"))),
});
}
let parents_len = cursor.take_u32()? as usize;
let mut parents = Vec::with_capacity(parents_len);
for _ in 0..parents_len {
let parent_bytes = cursor.take(ATOM_ID_BYTES)?;
let mut id = [0u8; ATOM_ID_BYTES];
id.copy_from_slice(parent_bytes);
parents.push(AtomId(id));
}
let principal = cursor.take_string()?;
let persona = cursor.take_string()?;
let agent_run_id = cursor.take_string()?;
let tool_call_id = match cursor.take_u8()? {
0 => None,
1 => Some(cursor.take_string()?),
other => {
return Err(AtomError::Binary(format!(
"invalid tool_call_id tag {other}"
)))
}
};
let trace_id = cursor.take_string()?;
let transcript_ref = cursor.take_string()?;
let timestamp_str = cursor.take_string()?;
let timestamp = OffsetDateTime::parse(×tamp_str, &Rfc3339)
.map_err(|error| AtomError::Binary(format!("timestamp parse: {error}")))?;
let provenance = Provenance {
principal,
persona,
agent_run_id,
tool_call_id,
trace_id,
transcript_ref,
timestamp,
};
let inverse_of = match cursor.take_u8()? {
0 => None,
1 => {
let id_bytes = cursor.take(ATOM_ID_BYTES)?;
let mut id = [0u8; ATOM_ID_BYTES];
id.copy_from_slice(id_bytes);
Some(AtomId(id))
}
other => return Err(AtomError::Binary(format!("invalid inverse_of tag {other}"))),
};
let id_bytes = cursor.take(ATOM_ID_BYTES)?;
let mut id = [0u8; ATOM_ID_BYTES];
id.copy_from_slice(id_bytes);
let id = AtomId(id);
let principal_key_bytes = cursor.take(ED25519_PUBLIC_KEY_BYTES)?;
let mut principal_key = [0u8; ED25519_PUBLIC_KEY_BYTES];
principal_key.copy_from_slice(principal_key_bytes);
let principal_sig_bytes = cursor.take(ED25519_SIGNATURE_BYTES)?;
let mut principal_sig = [0u8; ED25519_SIGNATURE_BYTES];
principal_sig.copy_from_slice(principal_sig_bytes);
let persona_key_bytes = cursor.take(ED25519_PUBLIC_KEY_BYTES)?;
let mut persona_key = [0u8; ED25519_PUBLIC_KEY_BYTES];
persona_key.copy_from_slice(persona_key_bytes);
let persona_sig_bytes = cursor.take(ED25519_SIGNATURE_BYTES)?;
let mut persona_sig = [0u8; ED25519_SIGNATURE_BYTES];
persona_sig.copy_from_slice(persona_sig_bytes);
if cursor.remaining() != 0 {
return Err(AtomError::Binary(format!(
"trailing bytes after atom: {} bytes left",
cursor.remaining()
)));
}
Ok(Atom {
id,
ops,
parents,
provenance,
signature: AtomSignature {
principal_key,
principal_sig,
persona_key,
persona_sig,
},
inverse_of,
})
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::SigningKey;
fn deterministic_signing_key(seed: u8) -> SigningKey {
let mut bytes = [0u8; 32];
for slot in bytes.iter_mut() {
*slot = seed;
}
SigningKey::from_bytes(&bytes)
}
fn fixed_provenance() -> Provenance {
Provenance {
principal: "user:alice".to_string(),
persona: "ship-captain".to_string(),
agent_run_id: "run-0001".to_string(),
tool_call_id: Some("tc-42".to_string()),
trace_id: "trace-abcd".to_string(),
transcript_ref: "transcript:0001#turn-3".to_string(),
timestamp: OffsetDateTime::parse("2026-04-24T12:34:56Z", &Rfc3339).unwrap(),
}
}
fn sample_ops() -> Vec<TextOp> {
vec![
TextOp::Insert {
offset: 0,
content: "Hello, ".to_string(),
},
TextOp::Insert {
offset: 7,
content: "world!".to_string(),
},
]
}
fn make_atom() -> Atom {
let principal = deterministic_signing_key(1);
let persona = deterministic_signing_key(2);
Atom::sign(
sample_ops(),
Vec::new(),
fixed_provenance(),
None,
&principal,
&persona,
)
.unwrap()
}
#[test]
fn signing_produces_atom_with_verifiable_signatures() {
let atom = make_atom();
atom.verify().expect("freshly-signed atom must verify");
}
#[test]
fn tampering_with_ops_invalidates_content_hash() {
let mut atom = make_atom();
atom.ops.push(TextOp::Insert {
offset: 13,
content: "?!".to_string(),
});
let error = atom.verify_content_hash().unwrap_err();
match error {
AtomError::ContentHashMismatch { .. } => {}
other => panic!("unexpected error: {other}"),
}
}
#[test]
fn tampering_with_signature_fails_verification() {
let mut atom = make_atom();
atom.signature.principal_sig[0] ^= 0xff;
let error = atom.verify_signatures().unwrap_err();
assert!(matches!(error, AtomError::InvalidSignature("principal")));
}
#[test]
fn inverse_atom_undoes_apply() {
let principal = deterministic_signing_key(1);
let persona = deterministic_signing_key(2);
let atom = make_atom();
let mut document: Vec<u8> = Vec::new();
atom.apply(&mut document).unwrap();
assert_eq!(std::str::from_utf8(&document).unwrap(), "Hello, world!");
let inverse = Atom::invert(
&atom,
Provenance {
tool_call_id: None,
..fixed_provenance()
},
&principal,
&persona,
)
.unwrap();
inverse.verify().unwrap();
assert_eq!(inverse.inverse_of, Some(atom.id));
assert_eq!(inverse.parents, vec![atom.id]);
inverse.apply(&mut document).unwrap();
assert!(document.is_empty(), "inverse must restore pre-image");
}
#[test]
fn inverse_of_inverse_returns_to_original() {
let principal = deterministic_signing_key(1);
let persona = deterministic_signing_key(2);
let atom = make_atom();
let inverse = Atom::invert(&atom, fixed_provenance(), &principal, &persona).unwrap();
let inv_inv = Atom::invert(&inverse, fixed_provenance(), &principal, &persona).unwrap();
assert_eq!(inv_inv.ops, atom.ops);
}
#[test]
fn json_roundtrip_preserves_atom_id() {
let atom = make_atom();
let json = atom.to_json().unwrap();
let decoded = Atom::from_json_slice(json.as_bytes()).unwrap();
assert_eq!(decoded, atom);
assert_eq!(decoded.id, atom.id);
decoded.verify().unwrap();
}
#[test]
fn binary_roundtrip_preserves_atom_id() {
let atom = make_atom();
let bytes = atom.to_binary().unwrap();
let decoded = Atom::from_binary_slice(&bytes).unwrap();
assert_eq!(decoded, atom);
assert_eq!(decoded.id, atom.id);
decoded.verify().unwrap();
}
#[test]
fn cross_encoding_atom_id_is_stable() {
let atom = make_atom();
let from_json = Atom::from_json_slice(atom.to_json().unwrap().as_bytes()).unwrap();
let from_binary = Atom::from_binary_slice(&atom.to_binary().unwrap()).unwrap();
assert_eq!(from_json.id, atom.id);
assert_eq!(from_binary.id, atom.id);
assert_eq!(from_json.id, from_binary.id);
}
#[test]
fn atom_id_is_deterministic_across_signers() {
let principal_a = deterministic_signing_key(11);
let persona_a = deterministic_signing_key(22);
let principal_b = deterministic_signing_key(33);
let persona_b = deterministic_signing_key(44);
let atom_a = Atom::sign(
sample_ops(),
Vec::new(),
fixed_provenance(),
None,
&principal_a,
&persona_a,
)
.unwrap();
let atom_b = Atom::sign(
sample_ops(),
Vec::new(),
fixed_provenance(),
None,
&principal_b,
&persona_b,
)
.unwrap();
assert_eq!(atom_a.id, atom_b.id);
assert_ne!(atom_a.signature, atom_b.signature);
}
#[test]
fn binary_decode_rejects_truncated_input() {
let atom = make_atom();
let bytes = atom.to_binary().unwrap();
let truncated = &bytes[..bytes.len() - 1];
let error = Atom::from_binary_slice(truncated).unwrap_err();
assert!(matches!(error, AtomError::Binary(_)));
}
#[test]
fn binary_decode_rejects_trailing_bytes() {
let atom = make_atom();
let mut bytes = atom.to_binary().unwrap();
bytes.push(0xff);
let error = Atom::from_binary_slice(&bytes).unwrap_err();
assert!(matches!(error, AtomError::Binary(_)));
}
#[test]
fn json_decode_rejects_mismatched_id() {
let atom = make_atom();
let mut value: serde_json::Value = serde_json::from_str(&atom.to_json().unwrap()).unwrap();
let other_id = AtomId([0xaau8; ATOM_ID_BYTES]);
value["id"] = serde_json::Value::String(other_id.to_hex());
let raw = serde_json::to_vec(&value).unwrap();
let error = Atom::from_json_slice(&raw).unwrap_err();
assert!(matches!(error, AtomError::ContentHashMismatch { .. }));
}
#[test]
fn delete_op_round_trips_apply_and_invert() {
let principal = deterministic_signing_key(1);
let persona = deterministic_signing_key(2);
let mut document = b"abcdef".to_vec();
let atom = Atom::sign(
vec![TextOp::Delete {
offset: 1,
content: "bcd".to_string(),
}],
Vec::new(),
fixed_provenance(),
None,
&principal,
&persona,
)
.unwrap();
atom.apply(&mut document).unwrap();
assert_eq!(document, b"aef");
let inverse = Atom::invert(&atom, fixed_provenance(), &principal, &persona).unwrap();
inverse.apply(&mut document).unwrap();
assert_eq!(document, b"abcdef");
}
#[test]
fn delete_op_rejects_content_mismatch() {
let principal = deterministic_signing_key(1);
let persona = deterministic_signing_key(2);
let mut document = b"abcdef".to_vec();
let atom = Atom::sign(
vec![TextOp::Delete {
offset: 0,
content: "wrong".to_string(),
}],
Vec::new(),
fixed_provenance(),
None,
&principal,
&persona,
)
.unwrap();
let error = atom.apply(&mut document).unwrap_err();
assert!(matches!(error, AtomError::Apply(_)));
}
#[test]
fn provenance_inverse_of_propagation() {
let principal = deterministic_signing_key(1);
let persona = deterministic_signing_key(2);
let target = make_atom();
let inverse = Atom::invert(&target, fixed_provenance(), &principal, &persona).unwrap();
assert_eq!(inverse.inverse_of, Some(target.id));
let from_json = Atom::from_json_slice(inverse.to_json().unwrap().as_bytes()).unwrap();
let from_binary = Atom::from_binary_slice(&inverse.to_binary().unwrap()).unwrap();
assert_eq!(from_json.inverse_of, Some(target.id));
assert_eq!(from_binary.inverse_of, Some(target.id));
}
}