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 POINTER_INDEX_MAGIC: &[u8; 8] = b"PREFPTI1";
const POINTER_INDEX_VERSION: u16 = 1;
const POINTER_INDEX_HEADER_LEN: usize = 8 + 2 + 8 + 32;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PointerIndexEntry {
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 PointerIndexRecordStatus {
Evaluated,
Failed { message: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PointerIndexRecordOutcome {
pub(crate) offset: usize,
pub(crate) status: PointerIndexRecordStatus,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PointerIndexReplay {
pub(crate) entries: Vec<PointerIndexEntry>,
pub(crate) trailing_partial_bytes: usize,
pub(crate) record_outcomes: Vec<PointerIndexRecordOutcome>,
}
impl PointerIndexReplay {
#[must_use]
pub(crate) fn has_item_failure(&self) -> bool {
self.record_outcomes
.iter()
.any(|outcome| matches!(outcome.status, PointerIndexRecordStatus::Failed { .. }))
}
}
fn encode_entry_body(entry: &PointerIndexEntry) -> 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<PointerIndexEntry> {
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 pointer index entry body".to_string(),
));
}
Ok(PointerIndexEntry {
ref_name_key,
ref_name,
ref_state_id,
})
}
pub(crate) fn encode_pointer_index_record(entry: &PointerIndexEntry) -> 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(POINTER_INDEX_HEADER_LEN + body.len());
out.extend_from_slice(POINTER_INDEX_MAGIC);
push_u16(&mut out, POINTER_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(POINTER_INDEX_MAGIC);
preimage.extend_from_slice(&POINTER_INDEX_VERSION.to_be_bytes());
preimage.extend_from_slice(&body_len.to_be_bytes());
preimage.extend_from_slice(body);
sha256(&preimage)
}
struct PointerIndexHeader {
body_len: u64,
checksum: [u8; 32],
}
fn parse_header(header: &[u8]) -> Result<PointerIndexHeader> {
let mut cursor = ByteCursor::new(header);
let magic = cursor.read_array::<8>()?;
if &magic != POINTER_INDEX_MAGIC {
return Err(PrikkError::MalformedData(
"invalid pointer index record magic".to_string(),
));
}
let version = cursor.read_u16()?;
if version != POINTER_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 pointer index header".to_string(),
));
}
Ok(PointerIndexHeader { body_len, checksum })
}
enum FrameAttempt {
Record {
entry: PointerIndexEntry,
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 < POINTER_INDEX_HEADER_LEN {
return FrameAttempt::TrailingPartial { remaining };
}
let header_end = offset + POINTER_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: "pointer index body length does not fit usize".to_string(),
};
};
let Some(body_end) = header_end.checked_add(body_len) else {
return FrameAttempt::Invalid {
message: "pointer 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!("pointer 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_pointer_index_records(bytes: &[u8]) -> Result<PointerIndexReplay> {
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(PointerIndexRecordOutcome {
offset,
status: PointerIndexRecordStatus::Evaluated,
});
entries.push(entry);
offset = next_offset;
}
FrameAttempt::TrailingPartial { remaining } => {
return Ok(PointerIndexReplay {
entries,
trailing_partial_bytes: remaining,
record_outcomes,
});
}
FrameAttempt::Invalid { message } => {
record_outcomes.push(PointerIndexRecordOutcome {
offset,
status: PointerIndexRecordStatus::Failed { message },
});
match resync_to_next_magic(bytes, offset + 1, POINTER_INDEX_MAGIC.as_slice()) {
Some(next) => offset = next,
None => {
return Ok(PointerIndexReplay {
entries,
trailing_partial_bytes: 0,
record_outcomes,
});
}
}
}
}
}
}
pub(crate) fn replay_pointer_index(layout: &RepositoryLayout) -> Result<PointerIndexReplay> {
let slot = resolve_live_slot(layout, &layout.ref_pointer_index_generation_log_path())?;
let relative = layout.repository_relative(&layout.ref_pointer_index_slot_path(slot))?;
let Some(bytes) = read_file_if_exists(layout.repository_mutation_root(), &relative)? else {
return Ok(PointerIndexReplay {
entries: Vec::new(),
trailing_partial_bytes: 0,
record_outcomes: Vec::new(),
});
};
decode_pointer_index_records(&bytes)
}
pub(crate) fn lookup_ref_pointer(
layout: &RepositoryLayout,
ref_name_key: [u8; 32],
) -> Result<Option<PointerIndexEntry>> {
let replay = replay_pointer_index(layout)?;
if replay.has_item_failure() {
return Err(PrikkError::Integrity(
"ref pointer 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))
}
#[cfg(any(test, feature = "test-support"))]
pub(crate) fn write_ref_pointer_candidate_for_test(
layout: &RepositoryLayout,
ref_name: &str,
ref_state_id: ObjectId,
) -> Result<()> {
append_ref_pointer_entry(
layout,
&PointerIndexEntry {
ref_name_key: crate::layout::ref_name_key_bytes(ref_name),
ref_name: ref_name.to_string(),
ref_state_id,
},
)
}
#[cfg(test)]
pub(crate) fn write_ref_pointer_entry_with_explicit_key_for_test(
layout: &RepositoryLayout,
ref_name_key: [u8; 32],
ref_name: &str,
ref_state_id: ObjectId,
) -> Result<()> {
append_ref_pointer_entry(
layout,
&PointerIndexEntry {
ref_name_key,
ref_name: ref_name.to_string(),
ref_state_id,
},
)
}
pub(crate) fn append_ref_pointer_entry(
layout: &RepositoryLayout,
entry: &PointerIndexEntry,
) -> Result<()> {
let record = encode_pointer_index_record(entry)?;
let slot = resolve_live_slot(layout, &layout.ref_pointer_index_generation_log_path())?;
let relative = layout.repository_relative(&layout.ref_pointer_index_slot_path(slot))?;
append_file_required(layout.repository_mutation_root(), &relative, &record)
}
#[cfg(any(test, feature = "test-support"))]
pub(crate) fn remove_pointer_entries_for_test(
layout: &RepositoryLayout,
ref_name_key: [u8; 32],
) -> Result<()> {
let slot = resolve_live_slot(layout, &layout.ref_pointer_index_generation_log_path())?;
let path = layout.ref_pointer_index_slot_path(slot);
let bytes = std::fs::read(&path)?;
let replay = decode_pointer_index_records(&bytes)?;
let mut entries = replay.entries.iter();
let mut retained = Vec::new();
for (index, outcome) in replay.record_outcomes.iter().enumerate() {
let end = replay
.record_outcomes
.get(index + 1)
.map_or(bytes.len() - replay.trailing_partial_bytes, |next| {
next.offset
});
let span = bytes.get(outcome.offset..end).unwrap_or_default();
match &outcome.status {
PointerIndexRecordStatus::Evaluated => {
let Some(entry) = entries.next() else {
return Err(PrikkError::Integrity(
"pointer index replay outcome/entry count mismatch".to_string(),
));
};
if entry.ref_name_key == ref_name_key {
continue;
}
}
PointerIndexRecordStatus::Failed { .. } => {}
}
retained.extend_from_slice(span);
}
retained.extend_from_slice(
bytes
.get(bytes.len() - replay.trailing_partial_bytes..)
.unwrap_or_default(),
);
std::fs::write(&path, retained)?;
Ok(())
}
#[cfg(feature = "test-support")]
pub fn remove_ref_pointer_entry_for_test_support(
layout: &RepositoryLayout,
ref_name: &str,
) -> Result<()> {
remove_pointer_entries_for_test(layout, crate::layout::ref_name_key_bytes(ref_name))
}
#[cfg(feature = "test-support")]
pub fn force_ref_pointer_to_arbitrary_state_for_test_support(
layout: &RepositoryLayout,
ref_name: &str,
ref_state_id: ObjectId,
) -> Result<()> {
write_ref_pointer_candidate_for_test(layout, ref_name, ref_state_id)
}
#[cfg(test)]
mod tests;