prikk-store 0.18.4

Prikk storage crate scaffold.
Documentation
//! Canonical object-tree verification and temp-debris classification.

use std::path::{Path, PathBuf};
use std::str::FromStr;

use prikk_error::{PrikkError, Result};
use prikk_object::{ObjectId, ObjectType};

use super::{ObjectVerification, PublicationTrustVerifier, verify_block_payload};
use crate::file_codec::decode_envelope_file;
use crate::fsutil::{EntryKind, inspect_entry, list_directory, read_file_required};
use crate::layout::{RepositoryLayout, persisted_object_types};
use crate::object_store::FileObjectStore;
use crate::signature_diagnostics::{
    SignatureEnvelopeIssue, SignatureEnvelopeSource, classify_signature_envelope,
};

pub(super) struct ObjectSummary {
    pub(super) object_count: usize,
    pub(super) block_count: usize,
    pub(super) rollback_block_count: usize,
    pub(super) rollback_patch_count: usize,
    pub(super) temp_paths: Vec<PathBuf>,
    pub(super) signature_issues: Vec<SignatureEnvelopeIssue>,
}

impl ObjectSummary {
    const fn empty() -> Self {
        Self {
            object_count: 0,
            block_count: 0,
            rollback_block_count: 0,
            rollback_patch_count: 0,
            temp_paths: Vec::new(),
            signature_issues: Vec::new(),
        }
    }

    fn add(&mut self, other: Self) -> Result<()> {
        self.object_count = checked_add(self.object_count, other.object_count, "object")?;
        self.block_count = checked_add(self.block_count, other.block_count, "block")?;
        self.rollback_block_count = checked_add(
            self.rollback_block_count,
            other.rollback_block_count,
            "rollback block",
        )?;
        self.rollback_patch_count = checked_add(
            self.rollback_patch_count,
            other.rollback_patch_count,
            "rollback patch",
        )?;
        self.temp_paths.extend(other.temp_paths);
        self.signature_issues.extend(other.signature_issues);
        Ok(())
    }
}

pub(super) fn verify_objects(
    layout: &RepositoryLayout,
    object_store: &FileObjectStore,
    trust_verifier: &mut PublicationTrustVerifier<'_>,
) -> Result<ObjectSummary> {
    let mut summary = ObjectSummary::empty();
    for object_type in persisted_object_types() {
        summary.add(verify_object_type(
            layout,
            object_store,
            object_type,
            trust_verifier,
        )?)?;
    }
    Ok(summary)
}

fn verify_object_type(
    layout: &RepositoryLayout,
    object_store: &FileObjectStore,
    object_type: ObjectType,
    trust_verifier: &mut PublicationTrustVerifier<'_>,
) -> Result<ObjectSummary> {
    let dir = layout.object_type_dir(object_type);
    let relative_dir = layout.repository_relative(&dir)?;
    match inspect_entry(layout.repository_mutation_root(), &relative_dir)? {
        None => return Ok(ObjectSummary::empty()),
        Some(EntryKind::Directory) => {}
        Some(_) => {
            return Err(PrikkError::Integrity(format!(
                "unexpected non-directory in object type directory: {}",
                dir.display()
            )));
        }
    }
    let mut summary = ObjectSummary::empty();
    let mut entries = list_directory(layout.repository_mutation_root(), &relative_dir)?;
    entries.sort_by(|left, right| {
        left.name
            .as_encoded_bytes()
            .cmp(right.name.as_encoded_bytes())
    });
    for entry in entries {
        let prefix_path = dir.join(&entry.name);
        if entry.kind != EntryKind::Directory {
            return Err(PrikkError::Integrity(format!(
                "unexpected non-directory in object type directory: {}",
                prefix_path.display()
            )));
        }
        summary.add(verify_prefix_dir(
            layout,
            object_store,
            object_type,
            &prefix_path,
            trust_verifier,
        )?)?;
    }
    Ok(summary)
}

fn verify_prefix_dir(
    layout: &RepositoryLayout,
    object_store: &FileObjectStore,
    object_type: ObjectType,
    prefix_path: &Path,
    trust_verifier: &mut PublicationTrustVerifier<'_>,
) -> Result<ObjectSummary> {
    let mut summary = ObjectSummary::empty();
    let relative_prefix = layout.repository_relative(prefix_path)?;
    let mut entries = list_directory(layout.repository_mutation_root(), &relative_prefix)?;
    entries.sort_by(|left, right| {
        left.name
            .as_encoded_bytes()
            .cmp(right.name.as_encoded_bytes())
    });
    for entry in entries {
        let path = prefix_path.join(&entry.name);
        if entry.kind != EntryKind::Regular {
            return Err(PrikkError::Integrity(format!(
                "unexpected non-file in object prefix directory: {}",
                path.display()
            )));
        }
        if is_object_temp_path(&path) {
            summary.temp_paths.push(path);
            continue;
        }
        let (object, signature_issues) =
            verify_object_file(layout, object_store, object_type, &path, trust_verifier)?;
        summary.signature_issues.extend(signature_issues);
        summary.object_count = checked_add(summary.object_count, 1, "object")?;
        if object.object_type == ObjectType::Block {
            summary.block_count = checked_add(summary.block_count, 1, "block")?;
            if object.rollback_patch_count != 0 {
                summary.rollback_block_count =
                    checked_add(summary.rollback_block_count, 1, "rollback block")?;
                summary.rollback_patch_count = checked_add(
                    summary.rollback_patch_count,
                    object.rollback_patch_count,
                    "rollback patch",
                )?;
            }
        }
    }
    Ok(summary)
}

fn verify_object_file(
    layout: &RepositoryLayout,
    object_store: &FileObjectStore,
    object_type: ObjectType,
    path: &Path,
    trust_verifier: &mut PublicationTrustVerifier<'_>,
) -> Result<(ObjectVerification, Vec<SignatureEnvelopeIssue>)> {
    let object_id = object_id_from_path(path)?;
    let expected_path = layout.object_path(object_type, object_id);
    if path != expected_path {
        return Err(PrikkError::Integrity(format!(
            "object path {} does not match canonical path {}",
            path.display(),
            expected_path.display()
        )));
    }
    let relative = layout.repository_relative(path)?;
    let envelope = decode_envelope_file(&read_file_required(
        layout.repository_mutation_root(),
        &relative,
    )?)?;
    if envelope.object_type != object_type {
        return Err(PrikkError::Integrity(format!(
            "object file {} is under type {} but envelope type is {}",
            path.display(),
            object_type,
            envelope.object_type
        )));
    }
    crate::format::validate_read_schema(layout.format(), &envelope)?;
    let computed = envelope.object_id();
    if computed != object_id {
        return Err(PrikkError::Integrity(format!(
            "object file {} has id {} but computed id is {}",
            path.display(),
            object_id,
            computed
        )));
    }
    let signature_issues = classify_signature_envelope(
        &envelope,
        SignatureEnvelopeSource::Object {
            object_type,
            object_id,
        },
    )?;
    if matches!(object_type, ObjectType::Block | ObjectType::RefState) {
        trust_verifier.verify(&envelope)?;
    }
    let rollback_patch_count = if object_type == ObjectType::Block {
        verify_block_payload(
            object_store,
            object_id,
            layout.format(),
            &envelope.canonical_payload,
        )?
    } else {
        0
    };
    Ok((
        ObjectVerification {
            object_id,
            object_type,
            path: path.to_path_buf(),
            rollback_patch_count,
        },
        signature_issues,
    ))
}

fn object_id_from_path(path: &Path) -> Result<ObjectId> {
    let Some(file_name) = path.file_name().and_then(|value| value.to_str()) else {
        return Err(PrikkError::Integrity(format!(
            "object file path is not valid UTF-8: {}",
            path.display()
        )));
    };
    let Some(hex) = file_name.strip_suffix(".pobj") else {
        return Err(PrikkError::Integrity(format!(
            "object file does not use .pobj extension: {}",
            path.display()
        )));
    };
    ObjectId::from_str(hex)
}

fn is_object_temp_path(path: &Path) -> bool {
    let Some(name) = path.file_name().and_then(|value| value.to_str()) else {
        return false;
    };
    let Some((object_name, suffix)) = name.split_once(".pobj.tmp.") else {
        return false;
    };
    let Some((pid, random)) = suffix.split_once('.') else {
        return false;
    };
    object_name.len() == 64
        && object_name.bytes().all(|byte| byte.is_ascii_hexdigit())
        && !pid.is_empty()
        && pid.bytes().all(|byte| byte.is_ascii_digit())
        && random.len() == 32
        && random.bytes().all(|byte| byte.is_ascii_hexdigit())
}

fn checked_add(left: usize, right: usize, label: &str) -> Result<usize> {
    left.checked_add(right)
        .ok_or_else(|| PrikkError::Integrity(format!("{label} verification count overflow")))
}