use prikk_error::{PrikkError, Result};
use prikk_hash::sha256;
use prikk_object::ObjectId;
use crate::byte_cursor::ByteCursor;
use crate::file_codec::{push_bytes_u64, push_u16};
use crate::frame_resync::resync_to_next_magic;
use crate::fsutil::{append_file_required, len_to_u64, read_file_if_exists};
use crate::generation::resolve_live_slot;
use crate::layout::RepositoryLayout;
const RECEIVED_INDEX_MAGIC: &[u8; 8] = b"PRECVIX1";
const RECEIVED_INDEX_VERSION: u16 = 1;
const RECEIVED_INDEX_HEADER_LEN: usize = 8 + 2 + 8 + 32;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ReceivedIndexEntry {
pub(crate) ref_name_key: [u8; 32],
pub(crate) ref_name: String,
pub(crate) ref_state_id: ObjectId,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ReceivedIndexRecordStatus {
Evaluated,
Failed { message: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ReceivedIndexRecordOutcome {
pub(crate) offset: usize,
pub(crate) status: ReceivedIndexRecordStatus,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ReceivedIndexReplay {
pub(crate) entries: Vec<ReceivedIndexEntry>,
pub(crate) trailing_partial_bytes: usize,
pub(crate) record_outcomes: Vec<ReceivedIndexRecordOutcome>,
}
impl ReceivedIndexReplay {
#[must_use]
pub(crate) fn has_item_failure(&self) -> bool {
self.record_outcomes
.iter()
.any(|outcome| matches!(outcome.status, ReceivedIndexRecordStatus::Failed { .. }))
}
}
fn encode_entry_body(entry: &ReceivedIndexEntry) -> Result<Vec<u8>> {
let mut body = Vec::new();
body.extend_from_slice(&entry.ref_name_key);
push_bytes_u64(&mut body, entry.ref_name.as_bytes())?;
body.extend_from_slice(entry.ref_state_id.as_bytes());
Ok(body)
}
fn decode_entry_body(body: &[u8]) -> Result<ReceivedIndexEntry> {
let mut cursor = ByteCursor::new(body);
let ref_name_key = cursor.read_array::<32>()?;
let ref_name_bytes = cursor.read_bytes_u64()?;
let ref_name = String::from_utf8(ref_name_bytes)
.map_err(|err| PrikkError::MalformedData(format!("invalid ref name utf-8: {err}")))?;
let ref_state_id = ObjectId::from_bytes(cursor.read_array::<32>()?);
if !cursor.is_finished() {
return Err(PrikkError::MalformedData(
"trailing bytes in received index entry body".to_string(),
));
}
Ok(ReceivedIndexEntry {
ref_name_key,
ref_name,
ref_state_id,
})
}
pub(crate) fn encode_received_index_record(entry: &ReceivedIndexEntry) -> Result<Vec<u8>> {
let body = encode_entry_body(entry)?;
let body_len = len_to_u64(body.len())?;
let checksum = record_checksum(body_len, &body);
let mut out = Vec::with_capacity(RECEIVED_INDEX_HEADER_LEN + body.len());
out.extend_from_slice(RECEIVED_INDEX_MAGIC);
push_u16(&mut out, RECEIVED_INDEX_VERSION);
out.extend_from_slice(&body_len.to_be_bytes());
out.extend_from_slice(&checksum);
out.extend_from_slice(&body);
Ok(out)
}
fn record_checksum(body_len: u64, body: &[u8]) -> [u8; 32] {
let mut preimage = Vec::new();
preimage.extend_from_slice(RECEIVED_INDEX_MAGIC);
preimage.extend_from_slice(&RECEIVED_INDEX_VERSION.to_be_bytes());
preimage.extend_from_slice(&body_len.to_be_bytes());
preimage.extend_from_slice(body);
sha256(&preimage)
}
struct ReceivedIndexHeader {
body_len: u64,
checksum: [u8; 32],
}
fn parse_header(header: &[u8]) -> Result<ReceivedIndexHeader> {
let mut cursor = ByteCursor::new(header);
let magic = cursor.read_array::<8>()?;
if &magic != RECEIVED_INDEX_MAGIC {
return Err(PrikkError::MalformedData(
"invalid received index record magic".to_string(),
));
}
let version = cursor.read_u16()?;
if version != RECEIVED_INDEX_VERSION {
return Err(PrikkError::UnsupportedFormatVersion(u32::from(version)));
}
let body_len = cursor.read_u64()?;
let checksum = cursor.read_array::<32>()?;
if !cursor.is_finished() {
return Err(PrikkError::MalformedData(
"trailing bytes in received index header".to_string(),
));
}
Ok(ReceivedIndexHeader { body_len, checksum })
}
enum FrameAttempt {
Record {
entry: ReceivedIndexEntry,
next_offset: usize,
},
TrailingPartial {
remaining: usize,
},
Invalid {
message: String,
},
}
fn parse_frame_at(bytes: &[u8], offset: usize) -> FrameAttempt {
let remaining = bytes.len().saturating_sub(offset);
if remaining < RECEIVED_INDEX_HEADER_LEN {
return FrameAttempt::TrailingPartial { remaining };
}
let header_end = offset + RECEIVED_INDEX_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(),
};
}
};
let Ok(body_len) = usize::try_from(header_values.body_len) else {
return FrameAttempt::Invalid {
message: "received index body length does not fit usize".to_string(),
};
};
let Some(body_end) = header_end.checked_add(body_len) else {
return FrameAttempt::Invalid {
message: "received index body end overflow".to_string(),
};
};
let Some(body) = bytes.get(header_end..body_end) else {
return FrameAttempt::TrailingPartial { remaining };
};
let expected = record_checksum(header_values.body_len, body);
if expected != header_values.checksum {
return FrameAttempt::Invalid {
message: format!("received index checksum mismatch at byte offset {offset}"),
};
}
match decode_entry_body(body) {
Ok(entry) => FrameAttempt::Record {
entry,
next_offset: body_end,
},
Err(err) => FrameAttempt::Invalid {
message: err.to_string(),
},
}
}
pub(crate) fn decode_received_index_records(bytes: &[u8]) -> Result<ReceivedIndexReplay> {
let mut entries = Vec::new();
let mut record_outcomes = Vec::new();
let mut offset = 0_usize;
loop {
match parse_frame_at(bytes, offset) {
FrameAttempt::Record { entry, next_offset } => {
record_outcomes.push(ReceivedIndexRecordOutcome {
offset,
status: ReceivedIndexRecordStatus::Evaluated,
});
entries.push(entry);
offset = next_offset;
}
FrameAttempt::TrailingPartial { remaining } => {
return Ok(ReceivedIndexReplay {
entries,
trailing_partial_bytes: remaining,
record_outcomes,
});
}
FrameAttempt::Invalid { message } => {
record_outcomes.push(ReceivedIndexRecordOutcome {
offset,
status: ReceivedIndexRecordStatus::Failed { message },
});
match resync_to_next_magic(bytes, offset + 1, RECEIVED_INDEX_MAGIC.as_slice()) {
Some(next) => offset = next,
None => {
return Ok(ReceivedIndexReplay {
entries,
trailing_partial_bytes: 0,
record_outcomes,
});
}
}
}
}
}
}
pub(crate) fn replay_received_index(layout: &RepositoryLayout) -> Result<ReceivedIndexReplay> {
let slot = resolve_live_slot(layout, &layout.received_index_generation_log_path())?;
let relative = layout.repository_relative(&layout.received_index_slot_path(slot))?;
let Some(bytes) = read_file_if_exists(layout.repository_mutation_root(), &relative)? else {
return Ok(ReceivedIndexReplay {
entries: Vec::new(),
trailing_partial_bytes: 0,
record_outcomes: Vec::new(),
});
};
decode_received_index_records(&bytes)
}
pub(crate) fn lookup_received_index_entry(
layout: &RepositoryLayout,
ref_name_key: [u8; 32],
) -> Result<Option<ReceivedIndexEntry>> {
let replay = replay_received_index(layout)?;
if replay.has_item_failure() {
return Err(PrikkError::Integrity(
"received-ref index has a damaged entry; run doctor before reading".to_string(),
));
}
Ok(replay
.entries
.into_iter()
.rev()
.find(|entry| entry.ref_name_key == ref_name_key))
}
pub(crate) fn list_resolved_received_entries(
layout: &RepositoryLayout,
) -> Result<Vec<ReceivedIndexEntry>> {
let replay = replay_received_index(layout)?;
if replay.has_item_failure() {
return Err(PrikkError::Integrity(
"received-ref index has a damaged entry; run doctor before reading".to_string(),
));
}
let mut resolved: Vec<ReceivedIndexEntry> = Vec::new();
for entry in replay.entries {
match resolved
.iter_mut()
.find(|existing| existing.ref_name_key == entry.ref_name_key)
{
Some(existing) => *existing = entry,
None => resolved.push(entry),
}
}
Ok(resolved)
}
pub(crate) fn append_received_index_entry(
layout: &RepositoryLayout,
entry: &ReceivedIndexEntry,
) -> Result<()> {
let record = encode_received_index_record(entry)?;
let slot = resolve_live_slot(layout, &layout.received_index_generation_log_path())?;
let relative = layout.repository_relative(&layout.received_index_slot_path(slot))?;
append_file_required(layout.repository_mutation_root(), &relative, &record)
}
#[cfg(test)]
mod tests;