use prikk_error::{PrikkError, Result};
use prikk_hash::sha256;
use crate::byte_cursor::ByteCursor;
use crate::file_codec::push_u16;
use crate::frame_resync::resync_to_next_magic;
use crate::fsutil::{append_file_required, read_file_if_exists};
use crate::layout::{ContainerSlot, RepositoryLayout};
const GENERATION_MAGIC: &[u8; 8] = b"PGENREC1";
const GENERATION_VERSION: u16 = 1;
const GENERATION_HEADER_LEN: usize = 8 + 2 + 8 + 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct GenerationRecord {
pub(crate) live_slot: ContainerSlot,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum GenerationRecordStatus {
Evaluated,
Failed { message: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct GenerationRecordOutcome {
pub(crate) offset: usize,
pub(crate) status: GenerationRecordStatus,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct GenerationReplay {
pub(crate) records: Vec<GenerationRecord>,
pub(crate) trailing_partial_bytes: usize,
pub(crate) record_outcomes: Vec<GenerationRecordOutcome>,
}
impl GenerationReplay {
#[must_use]
pub(crate) fn has_item_failure(&self) -> bool {
self.record_outcomes
.iter()
.any(|outcome| matches!(outcome.status, GenerationRecordStatus::Failed { .. }))
}
}
fn slot_code(slot: ContainerSlot) -> u8 {
match slot {
ContainerSlot::A => 0,
ContainerSlot::B => 1,
}
}
fn slot_from_code(code: u8) -> Result<ContainerSlot> {
match code {
0 => Ok(ContainerSlot::A),
1 => Ok(ContainerSlot::B),
other => Err(PrikkError::MalformedData(format!(
"unrecognized container slot code {other}"
))),
}
}
fn generation_checksum(body_len: u64, body: &[u8]) -> [u8; 32] {
let mut preimage = Vec::new();
preimage.extend_from_slice(GENERATION_MAGIC);
preimage.extend_from_slice(&GENERATION_VERSION.to_be_bytes());
preimage.extend_from_slice(&body_len.to_be_bytes());
preimage.extend_from_slice(body);
sha256(&preimage)
}
pub(crate) fn encode_generation_record(record: &GenerationRecord) -> Vec<u8> {
let body = vec![slot_code(record.live_slot)];
let body_len = u64::try_from(body.len()).unwrap_or(0);
let checksum = generation_checksum(body_len, &body);
let mut out = Vec::with_capacity(GENERATION_HEADER_LEN + body.len());
out.extend_from_slice(GENERATION_MAGIC);
push_u16(&mut out, GENERATION_VERSION);
out.extend_from_slice(&body_len.to_be_bytes());
out.extend_from_slice(&checksum);
out.extend_from_slice(&body);
out
}
pub(crate) fn append_generation_record(
layout: &RepositoryLayout,
generation_log_path: &std::path::Path,
record: &GenerationRecord,
) -> Result<()> {
let relative = layout.repository_relative(generation_log_path)?;
let bytes = encode_generation_record(record);
append_file_required(layout.repository_mutation_root(), &relative, &bytes)
}
fn decode_generation_body(body: &[u8]) -> Result<GenerationRecord> {
let mut cursor = ByteCursor::new(body);
let code = cursor.read_array::<1>()?[0];
if !cursor.is_finished() {
return Err(PrikkError::MalformedData(
"trailing bytes in generation record body".to_string(),
));
}
Ok(GenerationRecord {
live_slot: slot_from_code(code)?,
})
}
enum GenerationFrameAttempt {
Record {
record: GenerationRecord,
next_offset: usize,
},
TrailingPartial {
remaining: usize,
},
Invalid {
message: String,
},
}
fn parse_generation_frame_at(bytes: &[u8], offset: usize) -> GenerationFrameAttempt {
let remaining = bytes.len().saturating_sub(offset);
if remaining < GENERATION_HEADER_LEN {
return GenerationFrameAttempt::TrailingPartial { remaining };
}
let header_end = offset + GENERATION_HEADER_LEN;
let Some(header) = bytes.get(offset..header_end) else {
return GenerationFrameAttempt::TrailingPartial { remaining };
};
let header_values = match parse_generation_header(header) {
Ok(values) => values,
Err(err) => {
return GenerationFrameAttempt::Invalid {
message: err.to_string(),
};
}
};
let Ok(body_len) = usize::try_from(header_values.0) else {
return GenerationFrameAttempt::Invalid {
message: "generation record body length does not fit usize".to_string(),
};
};
let Some(body_end) = header_end.checked_add(body_len) else {
return GenerationFrameAttempt::Invalid {
message: "generation record body end overflow".to_string(),
};
};
let Some(body) = bytes.get(header_end..body_end) else {
return GenerationFrameAttempt::TrailingPartial { remaining };
};
let expected = generation_checksum(header_values.0, body);
if expected != header_values.1 {
return GenerationFrameAttempt::Invalid {
message: format!("generation record checksum mismatch at byte offset {offset}"),
};
}
match decode_generation_body(body) {
Ok(record) => GenerationFrameAttempt::Record {
record,
next_offset: body_end,
},
Err(err) => GenerationFrameAttempt::Invalid {
message: err.to_string(),
},
}
}
fn parse_generation_header(header: &[u8]) -> Result<(u64, [u8; 32])> {
let mut cursor = ByteCursor::new(header);
let magic = cursor.read_array::<8>()?;
if &magic != GENERATION_MAGIC {
return Err(PrikkError::MalformedData(
"invalid generation record magic".to_string(),
));
}
let version = cursor.read_u16()?;
if version != GENERATION_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 generation record header".to_string(),
));
}
Ok((body_len, checksum))
}
pub(crate) fn decode_generation_records(bytes: &[u8]) -> Result<GenerationReplay> {
let mut records = Vec::new();
let mut record_outcomes = Vec::new();
let mut offset = 0_usize;
loop {
match parse_generation_frame_at(bytes, offset) {
GenerationFrameAttempt::Record {
record,
next_offset,
} => {
record_outcomes.push(GenerationRecordOutcome {
offset,
status: GenerationRecordStatus::Evaluated,
});
records.push(record);
offset = next_offset;
}
GenerationFrameAttempt::TrailingPartial { remaining } => {
return Ok(GenerationReplay {
records,
trailing_partial_bytes: remaining,
record_outcomes,
});
}
GenerationFrameAttempt::Invalid { message } => {
record_outcomes.push(GenerationRecordOutcome {
offset,
status: GenerationRecordStatus::Failed { message },
});
match resync_to_next_magic(bytes, offset + 1, GENERATION_MAGIC.as_slice()) {
Some(next) => offset = next,
None => {
return Ok(GenerationReplay {
records,
trailing_partial_bytes: 0,
record_outcomes,
});
}
}
}
}
}
}
fn replay_generation_log(
layout: &RepositoryLayout,
generation_log_path: &std::path::Path,
) -> Result<GenerationReplay> {
let relative = layout.repository_relative(generation_log_path)?;
let Some(bytes) = read_file_if_exists(layout.repository_mutation_root(), &relative)? else {
return Ok(GenerationReplay {
records: Vec::new(),
trailing_partial_bytes: 0,
record_outcomes: Vec::new(),
});
};
decode_generation_records(&bytes)
}
pub(crate) fn resolve_live_slot(
layout: &RepositoryLayout,
generation_log_path: &std::path::Path,
) -> Result<ContainerSlot> {
let replay = replay_generation_log(layout, generation_log_path)?;
if replay.has_item_failure() {
return Err(PrikkError::Integrity(
"generation log has a damaged record; run doctor before reading".to_string(),
));
}
Ok(replay
.records
.last()
.map_or(ContainerSlot::A, |record| record.live_slot))
}
#[cfg(test)]
mod tests;