prikk-store 0.18.4

Prikk storage crate scaffold.
Documentation
//! Ref pointer file codec.

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";

/// Decoded ref pointer file.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct RefPointer {
    /// Human-readable ref name stored inside the pointer.
    pub ref_name: String,
    /// RefState object ID currently selected by this ref.
    pub ref_state_id: ObjectId,
}

/// Read a ref pointer file.
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()
}

/// Write a candidate ref pointer file and fsync it.
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)
}

/// Remove only uniquely named atomic-write temps belonging to this ref's candidate.
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,
    })
}