use std::fs::{self, File, OpenOptions};
use std::io::{Read, Write};
use prikk_error::{PrikkError, Result};
use prikk_hash::sha256;
use prikk_object::{ObjectEnvelope, ObjectType};
use crate::byte_cursor::ByteCursor;
use crate::file_codec::{decode_envelope_file, encode_envelope_file, push_u16, push_u64};
use crate::fsutil::{len_to_u64, sync_directory_best_effort};
use crate::layout::RepositoryLayout;
use crate::refs::require_signed_type;
const REF_LOG_MAGIC: &[u8; 8] = b"PREFLOG1";
const REF_LOG_VERSION: u16 = 1;
const REF_LOG_HEADER_LEN: usize = 8 + 2 + 8 + 32;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RefLogRecord {
pub envelope: ObjectEnvelope,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RefLogReplay {
pub records: Vec<RefLogRecord>,
pub trailing_partial_bytes: usize,
}
pub(crate) fn append_log_record(
layout: &RepositoryLayout,
ref_name: &str,
envelope: &ObjectEnvelope,
) -> Result<()> {
let path = layout.ref_log_path(ref_name);
let Some(parent) = path.parent() else {
return Err(PrikkError::Io(
"ref log path has no parent directory".to_string(),
));
};
fs::create_dir_all(parent)?;
let record = encode_log_record(envelope)?;
let is_new = !path.exists();
let mut file = OpenOptions::new().create(true).append(true).open(&path)?;
file.write_all(&record)?;
file.sync_all()?;
if is_new {
sync_directory_best_effort(parent)?;
}
Ok(())
}
pub(crate) fn replay_log(layout: &RepositoryLayout, ref_name: &str) -> Result<RefLogReplay> {
let path = layout.ref_log_path(ref_name);
if !path.exists() {
return Ok(RefLogReplay {
records: Vec::new(),
trailing_partial_bytes: 0,
});
}
let mut bytes = Vec::new();
File::open(path)?.read_to_end(&mut bytes)?;
decode_log_records(&bytes)
}
pub(crate) fn decode_log_file_bytes(bytes: &[u8]) -> Result<RefLogReplay> {
decode_log_records(bytes)
}
fn encode_log_record(envelope: &ObjectEnvelope) -> Result<Vec<u8>> {
require_signed_type(envelope, ObjectType::RefUpdate)?;
let body = encode_envelope_file(envelope)?;
let body_len = len_to_u64(body.len())?;
let checksum = log_record_checksum(body_len, &body);
let mut out = Vec::new();
out.extend_from_slice(REF_LOG_MAGIC);
push_u16(&mut out, REF_LOG_VERSION);
push_u64(&mut out, body_len);
out.extend_from_slice(&checksum);
out.extend_from_slice(&body);
Ok(out)
}
fn decode_log_records(bytes: &[u8]) -> Result<RefLogReplay> {
let mut records = Vec::new();
let mut offset = 0_usize;
while offset < bytes.len() {
let remaining = bytes.len().saturating_sub(offset);
if remaining < REF_LOG_HEADER_LEN {
return Ok(RefLogReplay {
records,
trailing_partial_bytes: remaining,
});
}
let header_end = offset
.checked_add(REF_LOG_HEADER_LEN)
.ok_or_else(|| PrikkError::MalformedData("ref-log header overflow".to_string()))?;
let header = bytes.get(offset..header_end).ok_or_else(|| {
PrikkError::MalformedData("ref-log header range overflow".to_string())
})?;
let header_values = parse_log_header(header)?;
let body_len = usize::try_from(header_values.body_len).map_err(|_| {
PrikkError::MalformedData("ref-log body length does not fit usize".to_string())
})?;
let body_end = header_end
.checked_add(body_len)
.ok_or_else(|| PrikkError::MalformedData("ref-log body end overflow".to_string()))?;
let Some(body) = bytes.get(header_end..body_end) else {
return Ok(RefLogReplay {
records,
trailing_partial_bytes: remaining,
});
};
let expected = log_record_checksum(header_values.body_len, body);
if expected != header_values.checksum {
return Err(PrikkError::Integrity(format!(
"ref-log checksum mismatch at byte offset {offset}"
)));
}
let envelope = decode_envelope_file(body)?;
require_signed_type(&envelope, ObjectType::RefUpdate)?;
records.push(RefLogRecord { envelope });
offset = body_end;
}
Ok(RefLogReplay {
records,
trailing_partial_bytes: 0,
})
}
struct RefLogHeader {
body_len: u64,
checksum: [u8; 32],
}
fn parse_log_header(header: &[u8]) -> Result<RefLogHeader> {
let mut cursor = ByteCursor::new(header);
let magic = cursor.read_array::<8>()?;
if &magic != REF_LOG_MAGIC {
return Err(PrikkError::MalformedData(
"invalid ref-log record magic".to_string(),
));
}
let version = cursor.read_u16()?;
if version != REF_LOG_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 ref-log header".to_string(),
));
}
Ok(RefLogHeader { body_len, checksum })
}
fn log_record_checksum(body_len: u64, body: &[u8]) -> [u8; 32] {
let mut preimage = Vec::new();
preimage.extend_from_slice(REF_LOG_MAGIC);
preimage.extend_from_slice(&REF_LOG_VERSION.to_be_bytes());
preimage.extend_from_slice(&body_len.to_be_bytes());
preimage.extend_from_slice(body);
sha256(&preimage)
}