use std::collections::BTreeMap;
use prikk_error::{PrikkError, Result};
use prikk_object::{
CanonicalEncode, CreateFile, DeleteNode, DeleteNodePreimage, NodeId, NodeKind, ObjectEnvelope,
ObjectId, ObjectType, Operation, OperationKind, PatchPayload, PatchPurpose,
};
use crate::layout::RepositoryLayout;
use crate::object_store::FileObjectStore;
use crate::patch_replay::decode::{
DecodedDeletePreimage, DecodedOperationKind, DecodedPatchOperation, decode_patch_operations,
};
use crate::text_span;
mod read;
use read::{
current_target_block, load_snapshot_files, read_blob_bytes_with_kind, read_block, read_patch,
single_parent_chain,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PatchInversePlan {
pub ref_name: String,
pub target_block_id: ObjectId,
pub block_count: usize,
pub patch_count: usize,
pub original_operation_count: usize,
pub inverse_operation_count: usize,
pub inverse_patch_id_hint: ObjectId,
pub inverse_payload: PatchPayload,
pub operations: Vec<PatchInverseOperationSummary>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PatchInverseOperationSummary {
pub op_seq: u32,
pub path: String,
pub kind: PatchInverseOperationKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PatchInverseOperationKind {
CreateFile,
DeleteFile,
ReplaceBinary,
EditText,
}
impl PatchInverseOperationKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::CreateFile => "create-file",
Self::DeleteFile => "delete-file",
Self::ReplaceBinary => "replace-binary",
Self::EditText => "edit-text",
}
}
}
pub fn prepare_patch_inverse_plan(
layout: &RepositoryLayout,
ref_name: &str,
) -> Result<PatchInversePlan> {
let object_store = FileObjectStore::new(layout.clone());
let target_block_id = current_target_block(layout, &object_store, ref_name)?;
let block_ids = single_parent_chain(&object_store, target_block_id)?;
let mut files = BTreeMap::new();
let mut live_nodes = BTreeMap::new();
let mut inverse_operations = Vec::new();
let mut patch_count = 0_usize;
let mut original_operation_count = 0_usize;
for block_id in &block_ids {
let block = read_block(&object_store, *block_id)?;
if let Some(snapshot_blob_ref) = block.snapshot_blob_ref {
files = load_snapshot_files(&object_store, snapshot_blob_ref)?;
live_nodes.clear();
inverse_operations.clear();
patch_count = 0;
original_operation_count = 0;
}
for patch_id in block.patch_ids {
let patch = read_patch(&object_store, patch_id)?;
let operations = decode_patch_operations(&patch.canonical_payload)?;
for operation in operations {
let inverse = derive_inverse_operation(
&object_store,
&mut files,
&mut live_nodes,
operation,
)?;
inverse_operations.push(inverse);
original_operation_count += 1;
}
patch_count += 1;
}
}
inverse_operations.reverse();
renumber_operations(&mut inverse_operations)?;
let summaries = summarize_operations(&inverse_operations);
let inverse_payload = PatchPayload {
operations: inverse_operations,
parent_patch_ids: Vec::new(),
intent: None,
preconditions: Vec::new(),
purpose: PatchPurpose::Normal,
};
let inverse_payload_bytes = inverse_payload.to_canonical_bytes()?;
let inverse_patch_id_hint =
ObjectEnvelope::unsigned(ObjectType::Patch, 1, inverse_payload_bytes).object_id();
Ok(PatchInversePlan {
ref_name: ref_name.to_string(),
target_block_id,
block_count: block_ids.len(),
patch_count,
original_operation_count,
inverse_operation_count: inverse_payload.operations.len(),
inverse_patch_id_hint,
inverse_payload,
operations: summaries,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct InverseLiveNode {
path: String,
kind: NodeKind,
}
fn derive_inverse_operation(
object_store: &FileObjectStore,
files: &mut BTreeMap<String, Vec<u8>>,
live_nodes: &mut BTreeMap<NodeId, InverseLiveNode>,
operation: DecodedPatchOperation,
) -> Result<Operation> {
match operation.kind {
DecodedOperationKind::CreateFile {
path,
node_id,
blob_id,
mode,
} => {
if files.contains_key(&path) {
return Err(PrikkError::Integrity(format!(
"CreateFile would overwrite existing path {path}"
)));
}
if live_nodes.contains_key(&node_id) {
return Err(PrikkError::Integrity(
"CreateFile would introduce an already-live node_id".to_string(),
));
}
let (old_node_kind, bytes) = read_blob_bytes_with_kind(object_store, blob_id)?;
files.insert(path.clone(), bytes);
live_nodes.insert(
node_id,
InverseLiveNode {
path: path.clone(),
kind: old_node_kind,
},
);
Ok(Operation {
op_seq: 0,
op_id: Some(format!("inverse-delete-{path}")),
preconditions: Vec::new(),
kind: OperationKind::DeleteNode(DeleteNode {
path,
node_id,
old_node_kind,
preimage: DeleteNodePreimage::File {
old_blob_id: blob_id,
old_mode: mode,
},
}),
})
}
DecodedOperationKind::DeleteNode {
path,
node_id,
preimage:
DecodedDeletePreimage::File {
old_node_kind,
old_blob_id,
old_mode,
},
} => {
let old_bytes = files.get(&path).ok_or_else(|| {
PrikkError::Integrity(format!("DeleteNode path is absent: {path}"))
})?;
crate::blob_access::ensure_blob_matches_node_kind(
old_bytes,
old_blob_id,
old_node_kind,
)?;
files.remove(&path);
if let Some(live) = live_nodes.remove(&node_id) {
if live.path != path {
return Err(PrikkError::Integrity(format!(
"DeleteNode path {path} does not match live node path {}",
live.path
)));
}
if live.kind != old_node_kind {
return Err(PrikkError::Integrity(
"DeleteNode old_node_kind does not match live node kind".to_string(),
));
}
}
Ok(Operation {
op_seq: 0,
op_id: Some(format!("inverse-create-{path}")),
preconditions: Vec::new(),
kind: OperationKind::CreateFile(CreateFile {
path,
node_id,
blob_id: old_blob_id,
mode: old_mode,
}),
})
}
DecodedOperationKind::EditText {
node_id,
span_id,
old_span_hash,
left_anchor_hash,
right_anchor_hash,
replacement_text,
old_span_text,
} => {
let live = live_nodes.get(&node_id).ok_or_else(|| {
PrikkError::Integrity("EditText inverse target node is not live".to_string())
})?;
if live.kind != NodeKind::TextFile {
return Err(PrikkError::Integrity(
"EditText inverse target node is not TextFile".to_string(),
));
}
let pre_text = files.get(&live.path).ok_or_else(|| {
PrikkError::Integrity(format!(
"EditText inverse target path {} is absent",
live.path
))
})?;
let (inverse, post_text) = text_span::derive_inverse_edit_text(
pre_text,
node_id,
&span_id,
&old_span_hash,
&left_anchor_hash,
&right_anchor_hash,
&replacement_text,
&old_span_text,
)?;
files.insert(live.path.clone(), post_text);
Ok(Operation {
op_seq: 0,
op_id: Some(format!("inverse-edit-text-{}", live.path)),
preconditions: Vec::new(),
kind: OperationKind::EditText(inverse),
})
}
DecodedOperationKind::DeleteNode {
preimage: DecodedDeletePreimage::Symlink { .. },
..
} => Err(PrikkError::UnsupportedObjectType(
"inverse planning for symlink DeleteNode is deferred".to_string(),
)),
DecodedOperationKind::ReplaceBinary { .. }
| DecodedOperationKind::RenamePath { .. }
| DecodedOperationKind::ChangePerm { .. }
| DecodedOperationKind::CreateSymlink { .. } => Err(PrikkError::UnsupportedObjectType(
"inverse planning for this node-addressed operation is deferred".to_string(),
)),
}
}
fn renumber_operations(operations: &mut [Operation]) -> Result<()> {
for (index, operation) in operations.iter_mut().enumerate() {
let next = index
.checked_add(1)
.ok_or_else(|| PrikkError::CanonicalEncoding("operation count overflow".to_string()))?;
operation.op_seq = u32::try_from(next).map_err(|_| {
PrikkError::CanonicalEncoding("operation count exceeds u32".to_string())
})?;
}
Ok(())
}
fn summarize_operations(operations: &[Operation]) -> Vec<PatchInverseOperationSummary> {
operations
.iter()
.map(|operation| {
let (kind, path) = match &operation.kind {
OperationKind::CreateFile(value) => {
(PatchInverseOperationKind::CreateFile, value.path.clone())
}
OperationKind::DeleteNode(value) => {
(PatchInverseOperationKind::DeleteFile, value.path.clone())
}
OperationKind::EditText(_) => (
PatchInverseOperationKind::EditText,
operation
.op_id
.as_deref()
.and_then(|value| value.strip_prefix("inverse-edit-text-"))
.unwrap_or("<unknown>")
.to_string(),
),
OperationKind::ReplaceBinary(_)
| OperationKind::RenamePath(_)
| OperationKind::ChangePerm(_)
| OperationKind::CreateSymlink(_) => {
unreachable!("inverse plan contains unsupported operation kind")
}
};
PatchInverseOperationSummary {
op_seq: operation.op_seq,
path,
kind,
}
})
.collect()
}
#[cfg(test)]
mod tests;