use std::collections::BTreeMap;
use prikk_error::{PrikkError, Result};
use prikk_object::{NodeId, NodeKind, ObjectId, text_span_hash};
use crate::object_store::ObjectReader;
use crate::path::RepoPath;
use crate::text_span;
use super::PatchReplayDeletedFile;
use super::decode::{
DecodedDeletePreimage, DecodedOperationKind, DecodedPatchOperation, ensure_apply_supported,
};
use super::read::read_blob_bytes_with_kind;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct ReplayLiveNode {
pub(super) path: String,
pub(super) kind: NodeKind,
pub(super) mode: u32,
}
pub(super) fn apply_decoded_operation(
object_store: &impl ObjectReader,
files: &mut BTreeMap<String, Vec<u8>>,
live_nodes: &mut BTreeMap<NodeId, ReplayLiveNode>,
deleted_files: &mut BTreeMap<String, PatchReplayDeletedFile>,
operation: DecodedPatchOperation,
) -> Result<()> {
ensure_apply_supported(&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 (kind, bytes) = read_blob_bytes_with_kind(object_store, blob_id)?;
deleted_files.remove(&path);
files.insert(path.clone(), bytes);
live_nodes.insert(node_id, ReplayLiveNode { path, kind, 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,
)?;
let repo_path = RepoPath::parse(&path)?;
let deleted = PatchReplayDeletedFile {
path: repo_path,
old_blob_id,
old_bytes: old_bytes.clone(),
};
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(),
));
}
}
deleted_files.insert(path, deleted);
}
DecodedOperationKind::EditText {
node_id,
span_id,
old_span_hash,
left_anchor_hash,
right_anchor_hash,
replacement_text,
old_span_text,
} => {
apply_edit_text(
files,
live_nodes,
node_id,
&span_id,
&old_span_hash,
&left_anchor_hash,
&right_anchor_hash,
&replacement_text,
&old_span_text,
)?;
}
DecodedOperationKind::ReplaceBinary {
node_id,
old_blob_id,
new_blob_id,
} => {
apply_replace_binary(
object_store,
files,
live_nodes,
node_id,
old_blob_id,
new_blob_id,
)?;
}
DecodedOperationKind::ChangePerm {
node_id,
old_mode,
new_mode,
} => {
apply_change_perm(live_nodes, node_id, old_mode, new_mode)?;
}
_ => unreachable!(
"ensure_apply_supported admits only CreateFile, file-DeleteNode, EditText, \
ReplaceBinary, and ChangePerm for replay"
),
}
Ok(())
}
fn apply_replace_binary(
object_store: &impl ObjectReader,
files: &mut BTreeMap<String, Vec<u8>>,
live_nodes: &BTreeMap<NodeId, ReplayLiveNode>,
node_id: NodeId,
old_blob_id: ObjectId,
new_blob_id: ObjectId,
) -> Result<()> {
let live = live_nodes.get(&node_id).ok_or_else(|| {
PrikkError::Integrity(format!(
"ReplaceBinary target node {} is not live",
hex32(node_id.as_bytes())
))
})?;
if live.kind != NodeKind::BinaryFile {
return Err(PrikkError::Integrity(format!(
"ReplaceBinary target node {} is {:?}, not BinaryFile",
hex32(node_id.as_bytes()),
live.kind
)));
}
let current_bytes = files.get(&live.path).ok_or_else(|| {
PrikkError::Integrity(format!(
"ReplaceBinary target path {} is absent for live node {}",
live.path,
hex32(node_id.as_bytes())
))
})?;
crate::blob_access::ensure_blob_matches_node_kind(current_bytes, old_blob_id, live.kind)?;
let (new_kind, new_bytes) = read_blob_bytes_with_kind(object_store, new_blob_id)?;
if new_kind != NodeKind::BinaryFile {
return Err(PrikkError::Integrity(format!(
"ReplaceBinary new blob {new_blob_id} is not a binary-file blob"
)));
}
files.insert(live.path.clone(), new_bytes);
Ok(())
}
fn apply_change_perm(
live_nodes: &mut BTreeMap<NodeId, ReplayLiveNode>,
node_id: NodeId,
old_mode: u32,
new_mode: u32,
) -> Result<()> {
let live = live_nodes.get_mut(&node_id).ok_or_else(|| {
PrikkError::Integrity(format!(
"ChangePerm target node {} is not live",
hex32(node_id.as_bytes())
))
})?;
if live.mode != old_mode {
return Err(PrikkError::Integrity(format!(
"ChangePerm old_mode does not match live node {}'s current mode",
hex32(node_id.as_bytes())
)));
}
live.mode = new_mode;
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn apply_edit_text(
files: &mut BTreeMap<String, Vec<u8>>,
live_nodes: &BTreeMap<NodeId, ReplayLiveNode>,
node_id: NodeId,
span_id: &[u8; 32],
old_span_hash: &[u8; 32],
left_anchor_hash: &[u8; 32],
right_anchor_hash: &[u8; 32],
replacement_text: &[u8],
old_span_text: &[u8],
) -> Result<()> {
if text_span_hash(old_span_text) != *old_span_hash {
return Err(PrikkError::Integrity(format!(
"EditText hash verification failed before localization for node {} span {}",
hex32(node_id.as_bytes()),
hex32(span_id)
)));
}
let live = live_nodes.get(&node_id).ok_or_else(|| {
PrikkError::Integrity(format!(
"EditText failed before blob load: node {} is not live (span {})",
hex32(node_id.as_bytes()),
hex32(span_id)
))
})?;
if live.kind != NodeKind::TextFile {
return Err(PrikkError::Integrity(format!(
"EditText failed before blob load: node {} is {:?}, not TextFile (span {})",
hex32(node_id.as_bytes()),
live.kind,
hex32(span_id)
)));
}
let current_text = files.get(&live.path).ok_or_else(|| {
PrikkError::Integrity(format!(
"EditText failed before blob load: live node {} path {} is absent (span {})",
hex32(node_id.as_bytes()),
live.path,
hex32(span_id)
))
})?;
if core::str::from_utf8(current_text).is_err() {
return Err(PrikkError::Integrity(format!(
"EditText failed during UTF-8 validation for node {} span {}",
hex32(node_id.as_bytes()),
hex32(span_id)
)));
}
let (start, end) = text_span::locate_text_span(
current_text,
old_span_text,
left_anchor_hash,
right_anchor_hash,
span_id,
node_id,
old_span_hash,
)
.map_err(|reason| {
PrikkError::Integrity(format!(
"EditText failed during localization for node {} span {}: {reason}",
hex32(node_id.as_bytes()),
hex32(span_id)
))
})?;
let new_text =
text_span::splice_text(current_text, start, end, replacement_text).map_err(|err| {
PrikkError::Integrity(format!(
"EditText failed during splice for node {} span {}: {err}",
hex32(node_id.as_bytes()),
hex32(span_id)
))
})?;
files.insert(live.path.clone(), new_text);
Ok(())
}
fn hex32(bytes: &[u8; 32]) -> String {
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
}