use std::collections::{BTreeMap, HashSet};
use prikk_error::{PrikkError, Result};
use prikk_object::{
BlobKind, BlobPayload, BlockKind, BlockPayload, NodeId, NodeKind, ObjectEnvelope, ObjectId,
ObjectType, RefStatePayload,
};
use crate::layout::RepositoryLayout;
use crate::object_store::ObjectReader;
use crate::path::RepoPath;
use crate::refs::RefStore;
use crate::snapshot::{SnapshotEntry, SnapshotManifest};
use super::apply::ReplayLiveNode;
use super::{ReplayManifest, ReplayManifestEntry};
pub(super) fn current_target_block(
layout: &RepositoryLayout,
object_store: &impl ObjectReader,
ref_name: &str,
) -> Result<ObjectId> {
let ref_store = RefStore::new(layout.clone());
let ref_state_id = ref_store
.read_current_ref_state_id(ref_name)?
.ok_or_else(|| PrikkError::Integrity(format!("ref {ref_name} is not published")))?;
let envelope = object_store
.read_typed(ref_state_id, ObjectType::RefState)?
.ok_or_else(|| {
PrikkError::Integrity(format!(
"ref {ref_name} points to missing RefState {ref_state_id}"
))
})?;
let ref_state =
RefStatePayload::decode_canonical(&envelope.canonical_payload, envelope.schema_version)?;
if ref_state.ref_name != ref_name {
return Err(PrikkError::Integrity(format!(
"RefState name mismatch: expected {ref_name}, got {}",
ref_state.ref_name
)));
}
Ok(ref_state.target_object_id)
}
pub(super) fn single_parent_chain(
object_store: &impl ObjectReader,
target: ObjectId,
) -> Result<Vec<ObjectId>> {
let mut newest_first = Vec::new();
let mut seen = HashSet::new();
let mut current = Some(target);
while let Some(block_id) = current {
if !seen.insert(block_id) {
return Err(PrikkError::Integrity(format!(
"block parent chain contains a cycle at {block_id}"
)));
}
let block = read_block(object_store, block_id)?;
let next = mainline_or_sole_parent(&block).ok_or_else(|| {
PrikkError::UnsupportedObjectType(format!(
"patch replay supports only single-parent chains; block {block_id} has {} parents",
block.parent_block_ids.len()
))
})?;
newest_first.push(block_id);
current = next;
}
newest_first.reverse();
Ok(newest_first)
}
fn mainline_or_sole_parent(block: &BlockPayload) -> Option<Option<ObjectId>> {
if block.kind == BlockKind::Merge {
let mainline = block.mainline_parent_id?;
if !block.parent_block_ids.contains(&mainline) {
return None;
}
return Some(Some(mainline));
}
match block.parent_block_ids.as_slice() {
[] => Some(None),
[parent] => Some(Some(*parent)),
_ => None,
}
}
pub(super) fn read_block(
object_store: &impl ObjectReader,
block_id: ObjectId,
) -> Result<BlockPayload> {
let envelope = object_store
.read_typed(block_id, ObjectType::Block)?
.ok_or_else(|| PrikkError::Integrity(format!("missing Block {block_id}")))?;
BlockPayload::decode_canonical(&envelope.canonical_payload)
}
pub(super) fn read_patch(
object_store: &impl ObjectReader,
patch_id: ObjectId,
) -> Result<ObjectEnvelope> {
object_store
.read_typed(patch_id, ObjectType::Patch)?
.ok_or_else(|| PrikkError::Integrity(format!("missing Patch {patch_id}")))
}
pub(super) fn load_snapshot_files(
object_store: &impl ObjectReader,
snapshot_blob_ref: ObjectId,
) -> Result<BTreeMap<String, Vec<u8>>> {
let envelope = object_store
.read_typed(snapshot_blob_ref, ObjectType::Blob)?
.ok_or_else(|| {
PrikkError::Integrity(format!("missing snapshot Blob {snapshot_blob_ref}"))
})?;
let snapshot_content = crate::blob_access::decode_snapshot_blob(&envelope.canonical_payload)?;
let manifest = SnapshotManifest::decode(&snapshot_content)?;
let mut files = BTreeMap::new();
for entry in manifest.files {
files.insert(entry.path.as_str().to_string(), entry.bytes);
}
Ok(files)
}
pub(super) fn files_to_manifest(files: BTreeMap<String, Vec<u8>>) -> Result<SnapshotManifest> {
let mut entries = Vec::with_capacity(files.len());
for (path, bytes) in files {
entries.push(SnapshotEntry {
path: RepoPath::parse(&path)?,
bytes,
});
}
Ok(SnapshotManifest { files: entries })
}
const SNAPSHOT_SEEDED_FALLBACK_MODE: u32 = 0o600;
pub(super) fn files_to_replay_manifest(
files: BTreeMap<String, Vec<u8>>,
live_nodes: &BTreeMap<NodeId, ReplayLiveNode>,
) -> Result<ReplayManifest> {
let modes_by_path: BTreeMap<&str, u32> = live_nodes
.values()
.map(|node| (node.path.as_str(), node.mode))
.collect();
let mut entries = Vec::with_capacity(files.len());
for (path, bytes) in files {
let mode = modes_by_path
.get(path.as_str())
.copied()
.unwrap_or(SNAPSHOT_SEEDED_FALLBACK_MODE);
entries.push(ReplayManifestEntry {
path: RepoPath::parse(&path)?,
bytes,
mode,
});
}
Ok(ReplayManifest { files: entries })
}
pub(super) fn read_blob_bytes_with_kind(
object_store: &impl ObjectReader,
blob_id: ObjectId,
) -> Result<(NodeKind, Vec<u8>)> {
let envelope = object_store
.read_typed(blob_id, ObjectType::Blob)?
.ok_or_else(|| PrikkError::Integrity(format!("missing Blob {blob_id}")))?;
let blob = BlobPayload::decode_canonical(&envelope.canonical_payload)?;
if blob.blob_kind == BlobKind::Snapshot {
return Err(PrikkError::Integrity(
"file content reference points to a SNAPSHOT blob".to_string(),
));
}
let kind = NodeKind::from_file_blob_kind(blob.blob_kind)?;
Ok((kind, blob.content))
}