use std::collections::BTreeSet;
use prikk_error::{PrikkError, Result};
use prikk_hash::sha256;
use prikk_object::{MerkleRoot, NodeId, NodeKind, ObjectId};
use crate::node_lifecycle::{NodeContent, NodeLifecycleState};
use crate::path::{RepoPath, validate_no_path_collisions};
const LEAF_DOMAIN: &[u8] = b"PRIKK-STATE-LEAF-v2";
const NODE_DOMAIN: &[u8] = b"PRIKK-STATE-NODE-v2";
const ROOT_DOMAIN: &[u8] = b"PRIKK-STATE-ROOT-v2";
const REGULAR_MODE: u32 = 0o100644;
const EXECUTABLE_MODE: u32 = 0o100755;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StateRootContent {
Blob(ObjectId),
Symlink(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StateRootEntry {
pub path: RepoPath,
pub node_id: NodeId,
pub kind: NodeKind,
pub mode: u32,
pub content: StateRootContent,
}
pub fn state_leaf_preimage(entry: &StateRootEntry) -> Result<Vec<u8>> {
validate_entry(entry)?;
let path = entry.path.as_str().as_bytes();
let path_len = u32::try_from(path.len())
.map_err(|_| PrikkError::Integrity("state-root path length exceeds u32".to_string()))?;
let content = content_bytes(entry);
let content_len = u64::try_from(content.len())
.map_err(|_| PrikkError::Integrity("state-root content length exceeds u64".to_string()))?;
let mut preimage =
Vec::with_capacity(LEAF_DOMAIN.len() + 4 + path.len() + 32 + 2 + 4 + 8 + content.len());
preimage.extend_from_slice(LEAF_DOMAIN);
preimage.extend_from_slice(&path_len.to_be_bytes());
preimage.extend_from_slice(path);
preimage.extend_from_slice(entry.node_id.as_bytes());
preimage.extend_from_slice(&entry.kind.code().to_be_bytes());
preimage.extend_from_slice(&entry.mode.to_be_bytes());
preimage.extend_from_slice(&content_len.to_be_bytes());
preimage.extend_from_slice(content);
Ok(preimage)
}
pub fn state_leaf_hash(entry: &StateRootEntry) -> Result<[u8; 32]> {
Ok(sha256(&state_leaf_preimage(entry)?))
}
pub fn compute_state_root(entries: &[StateRootEntry]) -> Result<MerkleRoot> {
validate_entries(entries)?;
let count = u64::try_from(entries.len())
.map_err(|_| PrikkError::Integrity("state-root entry count exceeds u64".to_string()))?;
if entries.is_empty() {
let mut preimage = Vec::with_capacity(ROOT_DOMAIN.len() + 8);
preimage.extend_from_slice(ROOT_DOMAIN);
preimage.extend_from_slice(&count.to_be_bytes());
return Ok(MerkleRoot(sha256(&preimage)));
}
let mut level = entries
.iter()
.map(state_leaf_hash)
.collect::<Result<Vec<_>>>()?;
while level.len() > 1 {
let mut next = Vec::with_capacity(level.len().div_ceil(2));
for pair in level.chunks(2) {
match pair {
[left, right] => {
let mut preimage = Vec::with_capacity(NODE_DOMAIN.len() + 64);
preimage.extend_from_slice(NODE_DOMAIN);
preimage.extend_from_slice(left);
preimage.extend_from_slice(right);
next.push(sha256(&preimage));
}
[single] => next.push(*single),
_ => {}
}
}
level = next;
}
let top = level.first().ok_or_else(|| {
PrikkError::Integrity("non-empty state-root reduction produced no hash".to_string())
})?;
let mut preimage = Vec::with_capacity(ROOT_DOMAIN.len() + 8 + 32);
preimage.extend_from_slice(ROOT_DOMAIN);
preimage.extend_from_slice(&count.to_be_bytes());
preimage.extend_from_slice(top);
Ok(MerkleRoot(sha256(&preimage)))
}
pub(crate) fn entries_from_state(state: &NodeLifecycleState) -> Result<Vec<StateRootEntry>> {
let mut entries = state
.live_nodes()
.map(|(node_id, node)| {
let (mode, content) = match &node.content {
NodeContent::File { blob_id, mode } => (*mode, StateRootContent::Blob(*blob_id)),
NodeContent::Symlink { target } => (0, StateRootContent::Symlink(target.clone())),
};
StateRootEntry {
path: node.path.clone(),
node_id: *node_id,
kind: node.kind,
mode,
content,
}
})
.collect::<Vec<_>>();
entries.sort_by(|left, right| {
left.path
.as_str()
.as_bytes()
.cmp(right.path.as_str().as_bytes())
});
validate_entries(&entries)?;
Ok(entries)
}
fn validate_entries(entries: &[StateRootEntry]) -> Result<()> {
let paths = entries
.iter()
.map(|entry| entry.path.clone())
.collect::<Vec<_>>();
validate_no_path_collisions(&paths)?;
if !entries.windows(2).all(
|pair| matches!(pair, [left, right] if left.path.as_str().as_bytes() < right.path.as_str().as_bytes()),
) {
return Err(PrikkError::Integrity(
"state-root entries are not in strict canonical path order".to_string(),
));
}
let mut node_ids = BTreeSet::new();
for entry in entries {
validate_entry(entry)?;
if !node_ids.insert(entry.node_id) {
return Err(PrikkError::Integrity(
"state-root entries contain a duplicate node_id".to_string(),
));
}
}
Ok(())
}
fn validate_entry(entry: &StateRootEntry) -> Result<()> {
if entry.node_id.is_zero() {
return Err(PrikkError::Integrity(
"state-root entry node_id must be nonzero".to_string(),
));
}
match (&entry.kind, &entry.content, entry.mode) {
(
NodeKind::TextFile | NodeKind::BinaryFile,
StateRootContent::Blob(_),
REGULAR_MODE | EXECUTABLE_MODE,
) => Ok(()),
(NodeKind::Symlink, StateRootContent::Symlink(_), 0) => Ok(()),
_ => Err(PrikkError::Integrity(
"state-root entry kind, content, or normalized mode is invalid".to_string(),
)),
}
}
fn content_bytes(entry: &StateRootEntry) -> &[u8] {
match &entry.content {
StateRootContent::Blob(blob_id) => blob_id.as_bytes(),
StateRootContent::Symlink(target) => target.as_bytes(),
}
}
#[cfg(test)]
mod tests;