use prikk_error::{PrikkError, Result};
use prikk_object::ObjectId;
use std::path::Path;
use crate::byte_cursor::ByteCursor;
use crate::file_codec::push_bytes_u64;
use crate::fsutil::{
EntryKind, ensure_directory_required, list_directory, read_file_if_exists,
remove_file_if_present_required, write_file_atomically,
};
use crate::layout::RepositoryLayout;
const REF_POINTER_MAGIC: &[u8; 8] = b"PREFPTR1";
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct RefPointer {
pub ref_name: String,
pub ref_state_id: ObjectId,
}
pub(crate) fn read_ref_pointer(
layout: &RepositoryLayout,
path: &Path,
) -> Result<Option<RefPointer>> {
let relative = layout.repository_relative(path)?;
read_file_if_exists(layout.repository_mutation_root(), &relative)?
.map(|bytes| decode_ref_pointer(&bytes))
.transpose()
}
pub(crate) fn write_ref_pointer_candidate(
layout: &RepositoryLayout,
ref_name: &str,
ref_state_id: ObjectId,
) -> Result<()> {
let candidate = layout.repository_relative(&layout.ref_tmp_path(ref_name))?;
let Some(parent) = candidate.parent() else {
return Err(PrikkError::Io(
"ref pointer candidate path has no parent directory".to_string(),
));
};
ensure_directory_required(layout.repository_mutation_root(), parent)?;
let bytes = encode_ref_pointer(ref_name, ref_state_id)?;
write_file_atomically(layout.repository_mutation_root(), &candidate, &bytes)
}
pub(crate) fn remove_candidate_write_temps(
layout: &RepositoryLayout,
ref_name: &str,
) -> Result<()> {
let candidate = layout.repository_relative(&layout.ref_tmp_path(ref_name))?;
let parent = candidate.parent().ok_or_else(|| {
PrikkError::Io("ref pointer candidate path has no parent directory".to_string())
})?;
let candidate_name = candidate
.file_name()
.and_then(|value| value.to_str())
.ok_or_else(|| PrikkError::Integrity("candidate name is not valid UTF-8".to_string()))?;
let prefix = format!("{candidate_name}.tmp.");
for entry in list_directory(layout.repository_mutation_root(), parent)? {
let Some(name) = entry.name.to_str() else {
continue;
};
if !name.starts_with(&prefix) {
continue;
}
if !is_generated_candidate_temp(name, &prefix) {
return Err(PrikkError::Integrity(
"unrecognized same-prefix ref candidate temp remains".to_string(),
));
}
if entry.kind != EntryKind::Regular {
return Err(PrikkError::Integrity(
"ref candidate temp is not a regular file".to_string(),
));
}
remove_file_if_present_required(
layout.repository_mutation_root(),
&parent.join(&entry.name),
)?;
}
Ok(())
}
fn is_generated_candidate_temp(name: &str, prefix: &str) -> bool {
let Some(suffix) = name.strip_prefix(prefix) else {
return false;
};
let Some((pid, random)) = suffix.split_once('.') else {
return false;
};
!pid.is_empty()
&& pid.bytes().all(|byte| byte.is_ascii_digit())
&& random.len() == 32
&& random
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
}
fn encode_ref_pointer(ref_name: &str, ref_state_id: ObjectId) -> Result<Vec<u8>> {
let mut out = Vec::new();
out.extend_from_slice(REF_POINTER_MAGIC);
push_bytes_u64(&mut out, ref_name.as_bytes())?;
out.extend_from_slice(ref_state_id.as_bytes());
Ok(out)
}
fn decode_ref_pointer(bytes: &[u8]) -> Result<RefPointer> {
let mut cursor = ByteCursor::new(bytes);
let magic = cursor.read_array::<8>()?;
if &magic != REF_POINTER_MAGIC {
return Err(PrikkError::MalformedData(
"invalid ref pointer magic".to_string(),
));
}
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 ref pointer".to_string(),
));
}
Ok(RefPointer {
ref_name,
ref_state_id,
})
}