use std::fs::File;
#[cfg(target_os = "linux")]
use std::fs::{self, OpenOptions};
use std::io::Read as _;
#[cfg(target_os = "linux")]
use std::io::Write as _;
#[cfg(target_os = "linux")]
use std::os::unix::fs::MetadataExt 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;
pub(super) fn verify_cached_sha256(
path: &Path,
stamp_path: &Path,
expected: &str,
) -> anyhow::Result<bool> {
#[cfg(target_os = "linux")]
{
if 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: &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());
}
let stamp = IntegrityStamp {
version: INTEGRITY_STAMP_VERSION,
sha256: expected.to_owned(),
identity,
};
let _ = write_stamp(stamp_path, &stamp);
if stamp.identity != path_identity(path)? {
let _ = fs::remove_file(stamp_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(encoded) = fs::read(stamp_path) else {
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")?;
let temporary = path.with_extension(format!("tmp.{}", Uuid::new_v4()));
let result = (|| {
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.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(),
}
}
}