use prikk_error::{PrikkError, Result};
use prikk_object::{
NodeId, NodeKind, ObjectId, PATCH_PARENT_IDS_RETIRED_SCHEMA, PatchPurpose,
TEXT_SPAN_HASH_BYTES, WireType,
};
mod operations;
mod tlv;
use operations::{
decode_change_perm, decode_create_file, decode_create_symlink, decode_delete_node,
decode_edit_text, decode_rename_path, decode_replace_binary,
};
use tlv::TlvCursor;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DecodedPatchOperation {
pub(crate) op_seq: u32,
pub(crate) kind: DecodedOperationKind,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum DecodedOperationKind {
CreateFile {
path: String,
node_id: NodeId,
blob_id: ObjectId,
mode: u32,
},
DeleteNode {
path: String,
node_id: NodeId,
preimage: DecodedDeletePreimage,
},
EditText {
node_id: NodeId,
span_id: [u8; TEXT_SPAN_HASH_BYTES],
old_span_hash: [u8; TEXT_SPAN_HASH_BYTES],
left_anchor_hash: [u8; TEXT_SPAN_HASH_BYTES],
right_anchor_hash: [u8; TEXT_SPAN_HASH_BYTES],
replacement_text: Vec<u8>,
old_span_text: Vec<u8>,
},
ReplaceBinary {
node_id: NodeId,
old_blob_id: ObjectId,
new_blob_id: ObjectId,
},
RenamePath {
node_id: NodeId,
old_path: String,
new_path: String,
},
ChangePerm {
node_id: NodeId,
old_mode: u32,
new_mode: u32,
},
CreateSymlink {
path: String,
node_id: NodeId,
target: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum DecodedDeletePreimage {
File {
old_node_kind: NodeKind,
old_blob_id: ObjectId,
old_mode: u32,
},
Symlink { old_target: String },
}
pub(crate) fn ensure_apply_supported(operation: &DecodedPatchOperation) -> Result<()> {
match &operation.kind {
DecodedOperationKind::CreateFile { .. }
| DecodedOperationKind::DeleteNode {
preimage: DecodedDeletePreimage::File { .. },
..
}
| DecodedOperationKind::EditText { .. }
| DecodedOperationKind::ReplaceBinary { .. }
| DecodedOperationKind::ChangePerm { .. } => Ok(()),
DecodedOperationKind::DeleteNode {
preimage: DecodedDeletePreimage::Symlink { .. },
..
} => Err(unsupported_operation("DeleteNode(symlink)")),
DecodedOperationKind::RenamePath { .. } => Err(unsupported_operation(
"RenamePath (node-addressed apply pending a rename authoring path)",
)),
DecodedOperationKind::CreateSymlink { .. } => Err(unsupported_operation(
"CreateSymlink (apply pending a symlink authoring path)",
)),
}
}
pub(crate) fn decode_patch_operations(
bytes: &[u8],
schema_version: u32,
) -> Result<Vec<DecodedPatchOperation>> {
PatchPurpose::decode_from_patch_payload(bytes).map_err(|err| {
PrikkError::MalformedData(format!("invalid PatchPurpose canonical form: {err}"))
})?;
let mut cursor = TlvCursor::new(bytes);
let mut operations = Vec::new();
while let Some(field) = cursor.next_field()? {
match field.tag {
1 => {
field.require_wire(WireType::RecordListItem)?;
let index = operations.len();
operations.push(decode_operation(field.value, index)?);
}
2 => {
if schema_version >= PATCH_PARENT_IDS_RETIRED_SCHEMA {
return Err(PrikkError::MalformedData(format!(
"Patch schema {schema_version} must not carry parent_patch_ids (tag 2); \
retired at schema {PATCH_PARENT_IDS_RETIRED_SCHEMA}"
)));
}
}
3 | 4 => {}
5 => {
field.require_wire(WireType::EnumU16)?;
let _ = field.read_u16()?;
}
other => {
return Err(PrikkError::MalformedData(format!(
"unknown Patch field tag: {other}"
)));
}
}
}
if operations.is_empty() {
return Err(PrikkError::MalformedData(
"Patch missing operations".to_string(),
));
}
Ok(operations)
}
pub(crate) fn decode_patch_parent_ids(bytes: &[u8]) -> Result<Vec<ObjectId>> {
let mut cursor = TlvCursor::new(bytes);
let mut parent_patch_ids = Vec::new();
while let Some(field) = cursor.next_field()? {
if field.tag == 2 {
parent_patch_ids.push(field.read_object_id_typed()?);
}
}
Ok(parent_patch_ids)
}
fn decode_operation(bytes: &[u8], index: usize) -> Result<DecodedPatchOperation> {
let mut cursor = TlvCursor::new(bytes);
let mut op_seq = None;
let mut kind: Option<(u16, &[u8])> = None;
while let Some(field) = cursor.next_field()? {
match field.tag {
1 => op_seq = Some(field.read_u32()?),
2 | 3 => {}
10..=16 => {
if let Some((first, _)) = kind {
return Err(PrikkError::MalformedData(format!(
"Operation carries multiple kind records: tag {first} and tag {}",
field.tag
)));
}
field.require_wire(WireType::Record)?;
kind = Some((field.tag, field.value));
}
other => {
return Err(PrikkError::MalformedData(format!(
"unknown Operation field tag: {other}"
)));
}
}
}
let op_seq =
op_seq.ok_or_else(|| PrikkError::MalformedData("Operation missing op_seq".to_string()))?;
if (op_seq as usize) != index + 1 {
return Err(PrikkError::MalformedData(format!(
"operation op_seq {op_seq} does not match physical position {} (expected {})",
index,
index + 1
)));
}
let (kind_tag, value) =
kind.ok_or_else(|| PrikkError::MalformedData("Operation missing kind".to_string()))?;
let kind = match kind_tag {
10 => decode_create_file(value),
11 => decode_delete_node(value),
12 => decode_edit_text(value),
13 => decode_rename_path(value),
14 => decode_change_perm(value),
15 => decode_create_symlink(value),
16 => decode_replace_binary(value),
_ => unreachable!("kind tag is constrained to 10..=16 above"),
}?;
Ok(DecodedPatchOperation { op_seq, kind })
}
fn unsupported_operation(name: &str) -> PrikkError {
PrikkError::UnsupportedObjectType(format!(
"patch replay plan does not yet support {name}; patch algebra remains a later increment"
))
}