Skip to main content

appcore_storage/
manifest.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: manifest.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/06/03 10:17:09 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/07/23 23:50:45 by dnettoRaw
8//      ###########      S: 1.0.1-rc.8
9// =============================================================================
10
11//! Cryptographic manifest and file checksum verifications.
12
13use crate::storage::{StorageError, StorageResult};
14use serde::{Deserialize, Serialize};
15use sha2::{Digest, Sha256};
16use std::collections::HashMap;
17use std::fs;
18use std::io;
19use std::path::{Component, Path, PathBuf};
20
21/// Entry representing a single file size and SHA-256 hash.
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
23pub struct ManifestFileEntry {
24    /// Lowercase SHA-256 digest.
25    pub hash: String,
26    /// File length in bytes.
27    pub size: u64,
28}
29
30// O manifest serve apenas para detectar corrupção acidental de dados (bit rot, falha de disco).
31// Ele NÃO é uma assinatura criptográfica e não protege contra um atacante ativo que
32// consiga alterar os arquivos de dados e regerar o manifest correspondente.
33/// Corruption-detection manifest for a bounded set of local files.
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
35pub struct StorageManifest {
36    /// Manifest schema version.
37    pub schema_version: String,
38    /// Application scope.
39    pub app_id: String,
40    /// Runtime node scope.
41    pub node_id: String,
42    /// Creation timestamp in Unix milliseconds.
43    pub created_at_ms: u64,
44    /// Runtime version that produced the manifest.
45    pub runtime_version: String,
46    /// Relative path to file integrity metadata.
47    pub files: HashMap<String, ManifestFileEntry>,
48}
49
50impl StorageManifest {
51    /// Generates a new `StorageManifest` for the given files under a root directory.
52    pub fn generate(
53        app_id: &str,
54        node_id: &str,
55        runtime_version: &str,
56        created_at_ms: u64,
57        root_dir: &Path,
58        file_paths: &[&str],
59    ) -> StorageResult<Self> {
60        let mut files = HashMap::new();
61        for rel_path in file_paths {
62            let full_path = resolve_path(root_dir, rel_path)?;
63            if !full_path.exists() {
64                return Err(StorageError::RepositoryNotFound((*rel_path).to_string()));
65            }
66            let (hash, size) = compute_file_sha256(&full_path)
67                .map_err(|e| StorageError::TransactionFailed(e.to_string()))?;
68            files.insert((*rel_path).to_string(), ManifestFileEntry { hash, size });
69        }
70        Ok(Self {
71            schema_version: "1".to_string(),
72            app_id: app_id.to_string(),
73            node_id: node_id.to_string(),
74            created_at_ms,
75            runtime_version: runtime_version.to_string(),
76            files,
77        })
78    }
79
80    /// Verifies that all files in the manifest exist under root_dir and match recorded sizes/hashes.
81    pub fn verify(&self, root_dir: &Path) -> StorageResult<()> {
82        if self.schema_version != "1" {
83            return Err(StorageError::MigrationFailed(format!(
84                "Incompatible schema version: expected 1, found {}",
85                self.schema_version
86            )));
87        }
88        for (rel_path, entry) in &self.files {
89            let full_path = resolve_path(root_dir, rel_path)?;
90            if !full_path.exists() {
91                return Err(StorageError::RepositoryNotFound(format!(
92                    "File missing: {}",
93                    rel_path
94                )));
95            }
96            let (actual_hash, actual_size) = compute_file_sha256(&full_path)
97                .map_err(|e| StorageError::TransactionFailed(e.to_string()))?;
98            if actual_size != entry.size {
99                return Err(StorageError::TransactionFailed(format!(
100                    "File size mismatch: {}. Expected {}, got {}",
101                    rel_path, entry.size, actual_size
102                )));
103            }
104            if actual_hash != entry.hash {
105                return Err(StorageError::TransactionFailed(format!(
106                    "File hash mismatch: {}. Expected {}, got {}",
107                    rel_path, entry.hash, actual_hash
108                )));
109            }
110        }
111        Ok(())
112    }
113}
114
115fn resolve_path(root: &Path, relative: &str) -> StorageResult<PathBuf> {
116    let rel = Path::new(relative);
117    if rel.is_absolute() {
118        return Err(StorageError::InvalidPath(relative.to_string()));
119    }
120    for component in rel.components() {
121        if matches!(
122            component,
123            Component::ParentDir | Component::RootDir | Component::Prefix(_)
124        ) {
125            return Err(StorageError::InvalidPath(relative.to_string()));
126        }
127    }
128    Ok(root.join(rel))
129}
130
131fn compute_file_sha256(path: &Path) -> Result<(String, u64), io::Error> {
132    let mut file = fs::File::open(path)?;
133    let mut hasher = Sha256::new();
134    let size = io::copy(&mut file, &mut hasher)?;
135    let hash = format!("{:x}", hasher.finalize());
136    Ok((hash, size))
137}
138
139#[cfg(test)]
140#[path = "manifest_tests.rs"]
141mod tests;