⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/pkg/
manifest.rs

1//! Package file manifest tracking
2//!
3//! Maps packages to their installed files with checksums for
4//! integrity verification. Each package records the files it installs
5//! so they can be verified, queried, or cleaned up on removal.
6
7#[cfg(feature = "alloc")]
8extern crate alloc;
9
10#[cfg(feature = "alloc")]
11use alloc::{collections::BTreeMap, string::String, vec::Vec};
12
13use crate::error::KernelError;
14
15/// Type classification for files installed by a package.
16#[cfg(feature = "alloc")]
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum FileType {
19    /// Regular binary/executable file
20    Binary,
21    /// Configuration file (preserved on upgrade/remove)
22    Config,
23    /// Documentation file
24    Documentation,
25    /// Static asset (images, fonts, data files)
26    Asset,
27}
28
29#[cfg(feature = "alloc")]
30impl FileType {
31    /// Parse a file type from a string identifier.
32    pub fn parse(s: &str) -> Self {
33        match s {
34            "config" | "conf" | "cfg" => Self::Config,
35            "doc" | "documentation" | "man" => Self::Documentation,
36            "asset" | "data" | "resource" => Self::Asset,
37            _ => Self::Binary,
38        }
39    }
40
41    /// Infer file type from a file path based on common patterns.
42    pub fn from_path(path: &str) -> Self {
43        if path.contains("/etc/")
44            || path.ends_with(".conf")
45            || path.ends_with(".cfg")
46            || path.ends_with(".ini")
47            || path.ends_with(".toml")
48            || path.ends_with(".yaml")
49            || path.ends_with(".json")
50        {
51            Self::Config
52        } else if path.contains("/doc/")
53            || path.contains("/man/")
54            || path.ends_with(".md")
55            || path.ends_with(".txt")
56            || path.ends_with(".html")
57            || path.ends_with(".1")
58        {
59            Self::Documentation
60        } else if path.contains("/share/")
61            || path.ends_with(".png")
62            || path.ends_with(".svg")
63            || path.ends_with(".ico")
64            || path.ends_with(".ttf")
65            || path.ends_with(".otf")
66        {
67            Self::Asset
68        } else {
69            Self::Binary
70        }
71    }
72
73    /// Convert to a byte for serialization.
74    pub fn to_byte(self) -> u8 {
75        match self {
76            Self::Binary => 0,
77            Self::Config => 1,
78            Self::Documentation => 2,
79            Self::Asset => 3,
80        }
81    }
82
83    /// Parse from a byte.
84    pub fn from_byte(b: u8) -> Self {
85        match b {
86            1 => Self::Config,
87            2 => Self::Documentation,
88            3 => Self::Asset,
89            _ => Self::Binary,
90        }
91    }
92}
93
94/// A record of files installed by a package
95#[cfg(feature = "alloc")]
96pub struct FileManifest {
97    /// Map of package name -> list of installed file records
98    entries: BTreeMap<String, Vec<FileRecord>>,
99}
100
101/// Record of a single installed file
102#[cfg(feature = "alloc")]
103#[derive(Clone)]
104pub struct FileRecord {
105    /// Absolute path of the installed file
106    pub path: String,
107    /// Size in bytes
108    pub size: u64,
109    /// Simple hash for integrity checking
110    pub checksum: u64,
111    /// Type classification of this file
112    pub file_type: FileType,
113}
114
115#[cfg(feature = "alloc")]
116impl FileManifest {
117    pub fn new() -> Self {
118        Self {
119            entries: BTreeMap::new(),
120        }
121    }
122
123    /// Record all files installed by a package.
124    ///
125    /// Replaces any previous manifest for this package.
126    pub fn record_installation(&mut self, package: &str, files: Vec<FileRecord>) {
127        self.entries.insert(String::from(package), files);
128    }
129
130    /// Verify installed files against the manifest.
131    ///
132    /// Checks that every recorded file still exists and matches its
133    /// expected size. Returns `Ok(true)` if all files are intact,
134    /// `Ok(false)` if any file is missing or has a different size.
135    pub fn verify_installation(&self, package: &str) -> Result<bool, KernelError> {
136        let records = self.entries.get(package).ok_or(KernelError::NotFound {
137            resource: "package manifest",
138            id: 0,
139        })?;
140
141        for record in records {
142            if let Some(vfs_lock) = crate::fs::try_get_vfs() {
143                let vfs = vfs_lock.read();
144                match vfs.resolve_path(&record.path) {
145                    Ok(node) => {
146                        if let Ok(metadata) = node.metadata() {
147                            if metadata.size as u64 != record.size {
148                                return Ok(false);
149                            }
150                        }
151                    }
152                    Err(_) => return Ok(false),
153                }
154            }
155        }
156
157        Ok(true)
158    }
159
160    /// Get all files belonging to a package.
161    pub fn get_package_files(&self, package: &str) -> Option<&[FileRecord]> {
162        self.entries.get(package).map(|v| v.as_slice())
163    }
164
165    /// Find which package owns a given file path.
166    ///
167    /// Returns the package name if any manifest entry contains the path.
168    pub fn find_file_owner(&self, path: &str) -> Option<String> {
169        for (package, records) in &self.entries {
170            if records.iter().any(|r| r.path == path) {
171                return Some(package.clone());
172            }
173        }
174        None
175    }
176
177    /// Remove manifest entries for a package.
178    ///
179    /// Returns the removed file records so the caller can clean up
180    /// the actual files.
181    pub fn remove_package(&mut self, package: &str) -> Option<Vec<FileRecord>> {
182        self.entries.remove(package)
183    }
184
185    /// List only configuration files for a package.
186    pub fn list_config_files(&self, package: &str) -> Vec<&FileRecord> {
187        self.entries
188            .get(package)
189            .map(|records| {
190                records
191                    .iter()
192                    .filter(|r| r.file_type == FileType::Config)
193                    .collect()
194            })
195            .unwrap_or_else(Vec::new)
196    }
197
198    /// List only documentation files for a package.
199    pub fn list_doc_files(&self, package: &str) -> Vec<&FileRecord> {
200        self.entries
201            .get(package)
202            .map(|records| {
203                records
204                    .iter()
205                    .filter(|r| r.file_type == FileType::Documentation)
206                    .collect()
207            })
208            .unwrap_or_else(Vec::new)
209    }
210
211    /// Return the total number of tracked packages.
212    pub fn package_count(&self) -> usize {
213        self.entries.len()
214    }
215}
216
217#[cfg(feature = "alloc")]
218impl Default for FileManifest {
219    fn default() -> Self {
220        Self::new()
221    }
222}
223
224/// Compute a simple non-cryptographic hash of file data.
225///
226/// Uses FNV-1a (64-bit) for speed. This is not a security hash -- it is
227/// only meant for detecting accidental corruption.
228#[cfg(feature = "alloc")]
229pub fn fnv1a_hash(data: &[u8]) -> u64 {
230    const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
231    const FNV_PRIME: u64 = 0x0100_0000_01b3;
232
233    let mut hash = FNV_OFFSET_BASIS;
234    for &byte in data {
235        hash ^= byte as u64;
236        hash = hash.wrapping_mul(FNV_PRIME);
237    }
238    hash
239}