goosedump 0.12.49

Browse, search, summarize, compact, and learn from coding-agent sessions
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (C) Jarkko Sakkinen 2026

//! Cached integrity verification for immutable model weights.

use std::fs::File;
#[cfg(target_os = "linux")]
use std::fs::{self, DirBuilder, OpenOptions};
use std::io::Read as _;
#[cfg(target_os = "linux")]
use std::io::Write as _;
#[cfg(target_os = "linux")]
use std::os::unix::fs::{DirBuilderExt as _, MetadataExt as _, OpenOptionsExt as _};
use std::path::Path;

use anyhow::{Context as _, bail};
#[cfg(target_os = "linux")]
use serde::{Deserialize, Serialize};
use sha2::{Digest as _, Sha256};
#[cfg(target_os = "linux")]
use uuid::Uuid;

#[cfg(target_os = "linux")]
const INTEGRITY_STAMP_VERSION: u32 = 1;
#[cfg(target_os = "linux")]
const MAX_INTEGRITY_STAMP_BYTES: u64 = 4_096;

/// Verify `path`, using a metadata-bound stamp on Linux.
///
/// The result is `true` when hashing was skipped.
pub(super) fn verify_cached_sha256(
    path: &Path,
    stamp_path: Option<&Path>,
    expected: &str,
) -> anyhow::Result<bool> {
    #[cfg(target_os = "linux")]
    {
        if stamp_path.is_some_and(|stamp_path| stamp_matches(path, stamp_path, expected)) {
            return Ok(true);
        }
        verify_and_stamp(path, stamp_path, expected)?;
        Ok(false)
    }
    #[cfg(not(target_os = "linux"))]
    {
        let _ = stamp_path;
        verify_sha256(path, expected)?;
        Ok(false)
    }
}

#[cfg(not(target_os = "linux"))]
fn verify_sha256(path: &Path, expected: &str) -> anyhow::Result<()> {
    let mut file = File::open(path).with_context(|| format!("open {}", path.display()))?;
    verify_reader(&mut file, path, expected)
}

fn verify_reader(file: &mut File, path: &Path, expected: &str) -> anyhow::Result<()> {
    let mut digest = Sha256::new();
    let mut buffer = vec![0_u8; 64 * 1_024];
    loop {
        let read = file
            .read(&mut buffer)
            .with_context(|| format!("read {}", path.display()))?;
        if read == 0 {
            break;
        }
        digest.update(&buffer[..read]);
    }
    let actual = format!("{:x}", digest.finalize());
    if actual != expected {
        bail!(
            "model {} has SHA-256 {actual}, expected {expected}; remove the file and retry",
            path.display()
        );
    }
    Ok(())
}

#[cfg(target_os = "linux")]
fn verify_and_stamp(path: &Path, stamp_path: Option<&Path>, expected: &str) -> anyhow::Result<()> {
    let mut file = File::open(path).with_context(|| format!("open {}", path.display()))?;
    let before = FileIdentity::from_metadata(
        &file
            .metadata()
            .with_context(|| format!("inspect {}", path.display()))?,
    );
    verify_reader(&mut file, path, expected)?;
    let identity = FileIdentity::from_metadata(
        &file
            .metadata()
            .with_context(|| format!("inspect {}", path.display()))?,
    );
    if identity != before || identity != path_identity(path)? {
        bail!("model {} changed while it was verified", path.display());
    }

    if let Some(stamp_path) = stamp_path {
        let stamp = IntegrityStamp {
            version: INTEGRITY_STAMP_VERSION,
            sha256: expected.to_owned(),
            identity,
        };
        let _ = write_stamp(stamp_path, &stamp);
        if stamp.identity != path_identity(path)? {
            bail!("model {} changed while it was verified", path.display());
        }
    }
    Ok(())
}

#[cfg(target_os = "linux")]
fn stamp_matches(path: &Path, stamp_path: &Path, expected: &str) -> bool {
    let Ok(file) = File::open(stamp_path) else {
        return false;
    };
    let mut encoded = Vec::new();
    let mut reader = file.take(MAX_INTEGRITY_STAMP_BYTES + 1);
    if reader.read_to_end(&mut encoded).is_err() {
        return false;
    }
    let Ok(encoded_len) = u64::try_from(encoded.len()) else {
        return false;
    };
    if encoded_len > MAX_INTEGRITY_STAMP_BYTES {
        return false;
    }
    let Ok(stamp) = serde_json::from_slice::<IntegrityStamp>(&encoded) else {
        return false;
    };
    let Ok(identity) = path_identity(path) else {
        return false;
    };
    stamp.version == INTEGRITY_STAMP_VERSION
        && stamp.sha256 == expected
        && stamp.identity == identity
}

#[cfg(target_os = "linux")]
fn path_identity(path: &Path) -> anyhow::Result<FileIdentity> {
    let metadata = fs::metadata(path).with_context(|| format!("inspect {}", path.display()))?;
    Ok(FileIdentity::from_metadata(&metadata))
}

#[cfg(target_os = "linux")]
fn write_stamp(path: &Path, stamp: &IntegrityStamp) -> anyhow::Result<()> {
    let encoded = serde_json::to_vec(stamp).context("encode model integrity stamp")?;
    if let Some(parent) = path
        .parent()
        .filter(|parent| !parent.as_os_str().is_empty())
    {
        let mut builder = DirBuilder::new();
        builder.recursive(true).mode(0o700);
        builder
            .create(parent)
            .with_context(|| format!("create {}", parent.display()))?;
    }

    let temporary = path.with_extension(format!("tmp.{}", Uuid::new_v4()));
    let result: anyhow::Result<()> = (|| {
        let mut file = OpenOptions::new()
            .write(true)
            .create_new(true)
            .mode(0o600)
            .open(&temporary)
            .with_context(|| format!("create {}", temporary.display()))?;
        file.write_all(&encoded)
            .with_context(|| format!("write {}", temporary.display()))?;
        file.sync_all()
            .with_context(|| format!("sync {}", temporary.display()))?;
        drop(file);
        fs::rename(&temporary, path)
            .with_context(|| format!("install integrity stamp {}", path.display()))?;
        Ok(())
    })();
    if result.is_err() {
        let _ = fs::remove_file(&temporary);
    }
    result
}

#[cfg(target_os = "linux")]
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
struct IntegrityStamp {
    version: u32,
    sha256: String,
    identity: FileIdentity,
}

#[cfg(target_os = "linux")]
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
struct FileIdentity {
    device: u64,
    inode: u64,
    size: u64,
    modified_seconds: i64,
    modified_nanoseconds: i64,
    changed_seconds: i64,
    changed_nanoseconds: i64,
}

#[cfg(target_os = "linux")]
impl FileIdentity {
    fn from_metadata(metadata: &fs::Metadata) -> Self {
        Self {
            device: metadata.dev(),
            inode: metadata.ino(),
            size: metadata.size(),
            modified_seconds: metadata.mtime(),
            modified_nanoseconds: metadata.mtime_nsec(),
            changed_seconds: metadata.ctime(),
            changed_nanoseconds: metadata.ctime_nsec(),
        }
    }
}