use prikk_error::{PrikkError, Result};
use prikk_hash::sha256;
use prikk_object::{ObjectEnvelope, ObjectType, RefUpdatePayload};
use crate::byte_cursor::ByteCursor;
use crate::file_codec::{decode_envelope_file, encode_envelope_file, push_u16, push_u64};
use crate::frame_resync::resync_to_next_magic;
use crate::fsutil::{append_file_required, len_to_u64, read_file_if_exists};
use crate::layout::RepositoryLayout;
use crate::refs::require_signed_type;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RefLogRecord {
pub envelope: ObjectEnvelope,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RefLogRecordStatus {
Evaluated,
Failed {
message: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RefLogRecordOutcome {
pub offset: usize,
pub status: RefLogRecordStatus,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RefLogReplay {
pub records: Vec<RefLogRecord>,
pub trailing_partial_bytes: usize,
pub record_outcomes: Vec<RefLogRecordOutcome>,
}
impl RefLogReplay {
#[must_use]
pub fn has_item_failure(&self) -> bool {
self.record_outcomes
.iter()
.any(|outcome| matches!(outcome.status, RefLogRecordStatus::Failed { .. }))
}
}
const REF_CONTAINER_MAGIC: &[u8; 8] = b"PREFCON1";
const REF_CONTAINER_VERSION: u16 = 1;
const REF_CONTAINER_HEADER_LEN: usize = 8 + 2 + 32 + 8 + 32;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct RefContainerRecord {
pub(crate) ref_name_key: [u8; 32],
pub(crate) envelope: ObjectEnvelope,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum RefContainerRecordStatus {
Evaluated,
Failed {
message: String,
claimed_ref_name_key: Option<[u8; 32]>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct RefContainerRecordOutcome {
pub(crate) offset: usize,
pub(crate) status: RefContainerRecordStatus,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct RefContainerReplay {
pub(crate) records: Vec<RefContainerRecord>,
pub(crate) trailing_partial_bytes: usize,
pub(crate) record_outcomes: Vec<RefContainerRecordOutcome>,
}
pub(crate) fn encode_ref_container_record(
ref_name_key: [u8; 32],
envelope: &ObjectEnvelope,
) -> Result<Vec<u8>> {
require_signed_type(envelope, ObjectType::RefUpdate)?;
let body = encode_envelope_file(envelope)?;
frame_record(ref_name_key, &body)
}
#[cfg(test)]
pub(crate) fn encode_ref_container_record_for_test(
ref_name_key: [u8; 32],
envelope: &ObjectEnvelope,
) -> Result<Vec<u8>> {
require_signed_type(envelope, ObjectType::RefUpdate)?;
let body = crate::file_codec::encode_envelope_file_structural(envelope)?;
frame_record(ref_name_key, &body)
}
fn frame_record(ref_name_key: [u8; 32], body: &[u8]) -> Result<Vec<u8>> {
let body_len = len_to_u64(body.len())?;
let checksum = record_checksum(ref_name_key, body_len, body);
let mut out = Vec::with_capacity(REF_CONTAINER_HEADER_LEN + body.len());
out.extend_from_slice(REF_CONTAINER_MAGIC);
push_u16(&mut out, REF_CONTAINER_VERSION);
out.extend_from_slice(&ref_name_key);
push_u64(&mut out, body_len);
out.extend_from_slice(&checksum);
out.extend_from_slice(body);
Ok(out)
}
enum FrameAttempt {
Record {
record: RefContainerRecord,
next_offset: usize,
},
TrailingPartial {
remaining: usize,
},
Invalid {
message: String,
claimed_ref_name_key: Option<[u8; 32]>,
},
}
fn parse_frame_at(bytes: &[u8], offset: usize) -> FrameAttempt {
let remaining = bytes.len().saturating_sub(offset);
if remaining < REF_CONTAINER_HEADER_LEN {
return FrameAttempt::TrailingPartial { remaining };
}
let header_end = offset + REF_CONTAINER_HEADER_LEN;
let Some(header) = bytes.get(offset..header_end) else {
return FrameAttempt::TrailingPartial { remaining };
};
let header_values = match parse_header(header) {
Ok(values) => values,
Err(err) => {
return FrameAttempt::Invalid {
message: err.to_string(),
claimed_ref_name_key: None,
};
}
};
let claimed = Some(header_values.ref_name_key);
let Ok(body_len) = usize::try_from(header_values.body_len) else {
return FrameAttempt::Invalid {
message: "ref container body length does not fit usize".to_string(),
claimed_ref_name_key: claimed,
};
};
let Some(body_end) = header_end.checked_add(body_len) else {
return FrameAttempt::Invalid {
message: "ref container body end overflow".to_string(),
claimed_ref_name_key: claimed,
};
};
let Some(body) = bytes.get(header_end..body_end) else {
return FrameAttempt::TrailingPartial { remaining };
};
let expected = record_checksum(header_values.ref_name_key, header_values.body_len, body);
if expected != header_values.checksum {
return FrameAttempt::Invalid {
message: format!("ref container checksum mismatch at byte offset {offset}"),
claimed_ref_name_key: claimed,
};
}
let envelope = match decode_envelope_file(body) {
Ok(envelope) => envelope,
Err(err) => {
return FrameAttempt::Invalid {
message: err.to_string(),
claimed_ref_name_key: claimed,
};
}
};
if let Err(err) = require_signed_type(&envelope, ObjectType::RefUpdate) {
return FrameAttempt::Invalid {
message: err.to_string(),
claimed_ref_name_key: claimed,
};
}
FrameAttempt::Record {
record: RefContainerRecord {
ref_name_key: header_values.ref_name_key,
envelope,
},
next_offset: body_end,
}
}
pub(crate) fn decode_ref_container_records(bytes: &[u8]) -> Result<RefContainerReplay> {
let mut records = Vec::new();
let mut record_outcomes = Vec::new();
let mut offset = 0_usize;
loop {
match parse_frame_at(bytes, offset) {
FrameAttempt::Record {
record,
next_offset,
} => {
match record.envelope.validate_strict() {
Ok(()) => {
record_outcomes.push(RefContainerRecordOutcome {
offset,
status: RefContainerRecordStatus::Evaluated,
});
records.push(record);
}
Err(err) => {
record_outcomes.push(RefContainerRecordOutcome {
offset,
status: RefContainerRecordStatus::Failed {
message: err.to_string(),
claimed_ref_name_key: Some(record.ref_name_key),
},
});
}
}
offset = next_offset;
}
FrameAttempt::TrailingPartial { remaining } => {
return Ok(RefContainerReplay {
records,
trailing_partial_bytes: remaining,
record_outcomes,
});
}
FrameAttempt::Invalid {
message,
claimed_ref_name_key,
} => {
record_outcomes.push(RefContainerRecordOutcome {
offset,
status: RefContainerRecordStatus::Failed {
message,
claimed_ref_name_key,
},
});
match resync_to_next_magic(bytes, offset + 1, REF_CONTAINER_MAGIC.as_slice()) {
Some(next) => offset = next,
None => {
return Ok(RefContainerReplay {
records,
trailing_partial_bytes: 0,
record_outcomes,
});
}
}
}
}
}
}
pub(crate) fn append_ref_container_record(
layout: &RepositoryLayout,
ref_name_key: [u8; 32],
envelope: &ObjectEnvelope,
) -> Result<()> {
let update = RefUpdatePayload::decode_canonical(&envelope.canonical_payload)?;
if update.created_at != 0 {
return Err(PrikkError::MalformedData(
"format-2 RefUpdate requires created_at == 0".to_string(),
));
}
let relative = layout.repository_relative(
&layout.ref_log_container_slot_path(crate::layout::ContainerSlot::A),
)?;
let existing = replay_ref_subsequence(layout, ref_name_key)?;
if existing
.records
.last()
.is_some_and(|last| last.envelope == *envelope)
{
return append_file_required(layout.repository_mutation_root(), &relative, &[]);
}
let record = encode_ref_container_record(ref_name_key, envelope)?;
append_file_required(layout.repository_mutation_root(), &relative, &record)
}
pub(crate) fn replay_ref_subsequence(
layout: &RepositoryLayout,
ref_name_key: [u8; 32],
) -> Result<RefLogReplay> {
let relative = layout.repository_relative(
&layout.ref_log_container_slot_path(crate::layout::ContainerSlot::A),
)?;
let Some(bytes) = read_file_if_exists(layout.repository_mutation_root(), &relative)? else {
return Ok(RefLogReplay {
records: Vec::new(),
trailing_partial_bytes: 0,
record_outcomes: Vec::new(),
});
};
let replay = decode_ref_container_records(&bytes)?;
let mut records = replay.records.iter();
let mut ref_records = Vec::new();
let mut ref_outcomes = Vec::new();
for outcome in &replay.record_outcomes {
match &outcome.status {
RefContainerRecordStatus::Evaluated => {
let Some(record) = records.next() else {
return Err(PrikkError::Integrity(
"ref container replay outcome/record count mismatch".to_string(),
));
};
if record.ref_name_key != ref_name_key {
continue;
}
ref_outcomes.push(RefLogRecordOutcome {
offset: outcome.offset,
status: RefLogRecordStatus::Evaluated,
});
ref_records.push(RefLogRecord {
envelope: record.envelope.clone(),
});
}
RefContainerRecordStatus::Failed {
message,
claimed_ref_name_key,
} => {
if *claimed_ref_name_key != Some(ref_name_key) {
continue;
}
ref_outcomes.push(RefLogRecordOutcome {
offset: outcome.offset,
status: RefLogRecordStatus::Failed {
message: message.clone(),
},
});
}
}
}
let attributed_trailing = trailing_tail_ref_name_key(&bytes, replay.trailing_partial_bytes);
let trailing_partial_bytes = if attributed_trailing == Some(ref_name_key) {
replay.trailing_partial_bytes
} else {
0
};
Ok(RefLogReplay {
records: ref_records,
trailing_partial_bytes,
record_outcomes: ref_outcomes,
})
}
fn trailing_tail_ref_name_key(bytes: &[u8], trailing_partial_bytes: usize) -> Option<[u8; 32]> {
if trailing_partial_bytes == 0 {
return None;
}
let start = bytes.len().checked_sub(trailing_partial_bytes)?;
let key_start = start.checked_add(10)?;
let key_end = key_start.checked_add(32)?;
bytes
.get(key_start..key_end)
.map(|slice| slice.try_into().unwrap_or([0_u8; 32]))
}
pub(crate) fn incomplete_tail_matches(
layout: &RepositoryLayout,
ref_name_key: [u8; 32],
expected: &ObjectEnvelope,
) -> Result<bool> {
let relative = layout.repository_relative(
&layout.ref_log_container_slot_path(crate::layout::ContainerSlot::A),
)?;
let bytes =
read_file_if_exists(layout.repository_mutation_root(), &relative)?.unwrap_or_default();
let replay = decode_ref_container_records(&bytes)?;
if replay.trailing_partial_bytes == 0 {
return Ok(false);
}
let retained = bytes
.len()
.checked_sub(replay.trailing_partial_bytes)
.ok_or_else(|| {
PrikkError::Integrity("ref container retained length underflow".to_string())
})?;
let expected_record = encode_ref_container_record(ref_name_key, expected)?;
let suffix = bytes.get(retained..).ok_or_else(|| {
PrikkError::Integrity("ref container incomplete suffix range overflow".to_string())
})?;
Ok(expected_record.starts_with(suffix))
}
pub(crate) fn truncate_incomplete_tail(layout: &RepositoryLayout) -> Result<usize> {
let relative = layout.repository_relative(
&layout.ref_log_container_slot_path(crate::layout::ContainerSlot::A),
)?;
let bytes =
read_file_if_exists(layout.repository_mutation_root(), &relative)?.unwrap_or_default();
let replay = decode_ref_container_records(&bytes)?;
if replay.trailing_partial_bytes == 0 {
return Ok(0);
}
let retained = bytes
.len()
.checked_sub(replay.trailing_partial_bytes)
.ok_or_else(|| {
PrikkError::Integrity("ref container retained length underflow".to_string())
})?;
crate::fsutil::truncate_existing_file_required(
layout.repository_mutation_root(),
&relative,
u64::try_from(retained)
.map_err(|_| PrikkError::Integrity("ref container length exceeds u64".to_string()))?,
)?;
Ok(replay.trailing_partial_bytes)
}
#[cfg(test)]
pub(crate) fn append_torn_ref_log_tail_for_test(
layout: &RepositoryLayout,
ref_name_key: [u8; 32],
envelope: &ObjectEnvelope,
) -> Result<()> {
let relative = layout.repository_relative(
&layout.ref_log_container_slot_path(crate::layout::ContainerSlot::A),
)?;
let full = encode_ref_container_record_for_test(ref_name_key, envelope)?;
let torn_len = (REF_CONTAINER_HEADER_LEN + 8).min(full.len().saturating_sub(1));
let torn = full.get(..torn_len).ok_or_else(|| {
PrikkError::Integrity("torn tail length exceeds encoded record".to_string())
})?;
crate::fsutil::append_file_required(layout.repository_mutation_root(), &relative, torn)
}
struct RefContainerHeader {
ref_name_key: [u8; 32],
body_len: u64,
checksum: [u8; 32],
}
fn parse_header(header: &[u8]) -> Result<RefContainerHeader> {
let mut cursor = ByteCursor::new(header);
let magic = cursor.read_array::<8>()?;
if &magic != REF_CONTAINER_MAGIC {
return Err(PrikkError::MalformedData(
"invalid ref container record magic".to_string(),
));
}
let version = cursor.read_u16()?;
if version != REF_CONTAINER_VERSION {
return Err(PrikkError::UnsupportedFormatVersion(u32::from(version)));
}
let ref_name_key = cursor.read_array::<32>()?;
let body_len = cursor.read_u64()?;
let checksum = cursor.read_array::<32>()?;
if !cursor.is_finished() {
return Err(PrikkError::MalformedData(
"trailing bytes in ref container header".to_string(),
));
}
Ok(RefContainerHeader {
ref_name_key,
body_len,
checksum,
})
}
fn record_checksum(ref_name_key: [u8; 32], body_len: u64, body: &[u8]) -> [u8; 32] {
let mut preimage = Vec::new();
preimage.extend_from_slice(REF_CONTAINER_MAGIC);
preimage.extend_from_slice(&REF_CONTAINER_VERSION.to_be_bytes());
preimage.extend_from_slice(&ref_name_key);
preimage.extend_from_slice(&body_len.to_be_bytes());
preimage.extend_from_slice(body);
sha256(&preimage)
}
#[cfg(test)]
mod tests;