use super::*;
pub(super) fn apply_state_effect<R: BlobKindResolver + BlobContentResolver>(
state: &mut NodeLifecycleState,
text_cache: &mut TextCache,
kind: &DecodedOperationKind,
blob_resolver: &R,
) -> Result<(), LifecycleReplayError> {
match kind {
DecodedOperationKind::CreateFile {
path,
node_id,
blob_id,
mode,
} => {
let repo_path = parse_repo_path(path)?;
let blob_kind = blob_resolver
.blob_kind(blob_id)
.map_err(inconsistent)?
.ok_or(LifecycleReplayError::MissingBlobForLifecycleEffect { blob_id: *blob_id })?;
let node_kind = NodeKind::from_file_blob_kind(blob_kind).map_err(inconsistent)?;
let node = LiveNode {
path: repo_path,
kind: node_kind,
content: NodeContent::File {
blob_id: *blob_id,
mode: *mode,
},
};
state.create_node(*node_id, node).map_err(inconsistent)
}
DecodedOperationKind::CreateSymlink {
path,
node_id,
target,
} => {
let repo_path = parse_repo_path(path)?;
let node = LiveNode {
path: repo_path,
kind: NodeKind::Symlink,
content: NodeContent::Symlink {
target: target.clone(),
},
};
state.create_node(*node_id, node).map_err(inconsistent)
}
DecodedOperationKind::DeleteNode {
path,
node_id,
preimage,
} => {
let expected = expected_deleted_node(path, preimage)?;
state
.delete_node_checked(*node_id, &expected)
.map(|_| ())
.map_err(inconsistent)?;
text_cache.remove(node_id);
Ok(())
}
DecodedOperationKind::RenamePath {
node_id,
old_path,
new_path,
} => {
let expected_old = parse_repo_path(old_path)?;
let new = parse_repo_path(new_path)?;
state
.rename_node_checked(*node_id, &expected_old, new)
.map_err(inconsistent)
}
DecodedOperationKind::ChangePerm {
node_id,
old_mode,
new_mode,
} => state
.change_file_mode(*node_id, *old_mode, *new_mode)
.map_err(inconsistent),
DecodedOperationKind::ReplaceBinary {
node_id,
old_blob_id,
new_blob_id,
} => {
require_binary_blob(blob_resolver, *old_blob_id)?;
require_binary_blob(blob_resolver, *new_blob_id)?;
state
.replace_file_blob(*node_id, *old_blob_id, *new_blob_id)
.map_err(inconsistent)
}
DecodedOperationKind::EditText {
node_id,
span_id,
old_span_hash,
left_anchor_hash,
right_anchor_hash,
replacement_text,
old_span_text,
} => apply_edit_text(
state,
text_cache,
blob_resolver,
*node_id,
span_id,
old_span_hash,
left_anchor_hash,
right_anchor_hash,
replacement_text,
old_span_text,
),
}
}
#[allow(clippy::too_many_arguments)]
fn apply_edit_text<R: BlobContentResolver>(
state: &mut NodeLifecycleState,
text_cache: &mut TextCache,
blob_resolver: &R,
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<(), LifecycleReplayError> {
if text_span_hash(old_span_text) != *old_span_hash {
return Err(LifecycleReplayError::InconsistentLifecycleEffect {
detail: "EditText old_span_hash != SHA-256(old_span_text)".to_string(),
});
}
let live = state.live_node(&node_id).ok_or_else(|| {
LifecycleReplayError::InconsistentLifecycleEffect {
detail: "EditText target node_id is not live".to_string(),
}
})?;
if live.kind != NodeKind::TextFile {
return Err(LifecycleReplayError::InconsistentLifecycleEffect {
detail: "EditText target is not a text-file node".to_string(),
});
}
let current_blob_id = match &live.content {
NodeContent::File { blob_id, .. } => *blob_id,
NodeContent::Symlink { .. } => {
return Err(LifecycleReplayError::InconsistentLifecycleEffect {
detail: "EditText target has symlink content".to_string(),
});
}
};
let current_text = match text_cache.get(&node_id) {
Some(text) => text.clone(),
None => {
let (blob_kind, content) = blob_resolver
.blob_content(¤t_blob_id)
.map_err(inconsistent)?
.ok_or(LifecycleReplayError::MissingBlobForLifecycleEffect {
blob_id: current_blob_id,
})?;
if blob_kind != BlobKind::Text {
return Err(LifecycleReplayError::InconsistentLifecycleEffect {
detail: "EditText current content blob is not Text".to_string(),
});
}
content
}
};
let (start, end) = text_span::locate_text_span(
¤t_text,
old_span_text,
left_anchor_hash,
right_anchor_hash,
span_id,
node_id,
old_span_hash,
)
.map_err(|reason| LifecycleReplayError::TextSpanResolutionFailed {
node_id,
span_id: *span_id,
reason,
})?;
let new_text = text_span::splice_text(¤t_text, start, end, replacement_text)
.map_err(inconsistent)?;
let new_blob_id = text_span::text_blob_id(&new_text).map_err(inconsistent)?;
state
.set_text_blob(node_id, new_blob_id)
.map_err(inconsistent)?;
text_cache.insert(node_id, new_text);
Ok(())
}
fn require_binary_blob(
resolver: &impl BlobKindResolver,
blob_id: ObjectId,
) -> Result<(), LifecycleReplayError> {
let kind = resolver
.blob_kind(&blob_id)
.map_err(inconsistent)?
.ok_or(LifecycleReplayError::MissingBlobForLifecycleEffect { blob_id })?;
if kind != BlobKind::Binary {
return Err(LifecycleReplayError::InconsistentLifecycleEffect {
detail: format!("ReplaceBinary blob {blob_id} is not binary ({kind:?})"),
});
}
Ok(())
}
fn expected_deleted_node(
path: &str,
preimage: &DecodedDeletePreimage,
) -> Result<LiveNode, LifecycleReplayError> {
let repo_path = parse_repo_path(path)?;
let (kind, content) = match preimage {
DecodedDeletePreimage::File {
old_node_kind,
old_blob_id,
old_mode,
} => (
*old_node_kind,
NodeContent::File {
blob_id: *old_blob_id,
mode: *old_mode,
},
),
DecodedDeletePreimage::Symlink { old_target } => (
NodeKind::Symlink,
NodeContent::Symlink {
target: old_target.clone(),
},
),
};
Ok(LiveNode {
path: repo_path,
kind,
content,
})
}
fn parse_repo_path(path: &str) -> Result<RepoPath, LifecycleReplayError> {
RepoPath::parse(path).map_err(inconsistent)
}
fn inconsistent<E: fmt::Display>(error: E) -> LifecycleReplayError {
LifecycleReplayError::InconsistentLifecycleEffect {
detail: error.to_string(),
}
}