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

veridian_kernel/pkg/
database.rs

1//! Persistent package database
2//!
3//! Stores package state to VFS-backed storage at `/var/pkg/db`.
4//! Uses a simple binary format for serialization so the package
5//! registry survives reboots.
6//!
7//! ## Binary Format
8//!
9//! The database file is a sequence of records. Each record is:
10//!
11//! ```text
12//! name_len:    u16 (little-endian)
13//! name:        [u8; name_len]
14//! version_len: u16 (little-endian)
15//! version:     [u8; version_len]
16//! installed_at: u64 (little-endian)
17//! files_count: u32 (little-endian)
18//! size_bytes:  u64 (little-endian)
19//! dep_count:   u16 (little-endian)
20//! for each dep:
21//!     dep_len: u16 (little-endian)
22//!     dep:     [u8; dep_len]
23//! ```
24
25#[cfg(feature = "alloc")]
26extern crate alloc;
27
28#[cfg(feature = "alloc")]
29use alloc::{collections::BTreeMap, string::String, vec, vec::Vec};
30
31use crate::error::KernelError;
32
33/// Record of a tracked configuration file
34#[cfg(feature = "alloc")]
35#[derive(Clone)]
36pub struct ConfigRecord {
37    /// Absolute path of the configuration file
38    pub path: String,
39    /// SHA-256 hash at install time
40    pub original_hash: [u8; 32],
41    /// Whether the user has modified this file since install
42    pub is_user_modified: bool,
43}
44
45/// On-disk package database
46#[cfg(feature = "alloc")]
47pub struct PackageDatabase {
48    /// Map of package name -> installed package record
49    packages: BTreeMap<String, DbPackageRecord>,
50    /// Database file path
51    db_path: String,
52    /// Whether database has unsaved changes
53    dirty: bool,
54    /// Tracked configuration files per package
55    config_files: BTreeMap<String, Vec<ConfigRecord>>,
56}
57
58/// A single record in the package database
59#[cfg(feature = "alloc")]
60#[derive(Clone)]
61pub struct DbPackageRecord {
62    /// Package name
63    pub name: String,
64    /// Installed version string (e.g. "1.2.3")
65    pub version: String,
66    /// Timestamp when the package was installed (seconds since boot)
67    pub installed_at: u64,
68    /// Number of files installed by the package
69    pub files_count: u32,
70    /// Total size of installed files in bytes
71    pub size_bytes: u64,
72    /// Names of packages this package depends on
73    pub dependencies: Vec<String>,
74}
75
76#[cfg(feature = "alloc")]
77impl PackageDatabase {
78    pub fn new(db_path: &str) -> Self {
79        Self {
80            packages: BTreeMap::new(),
81            db_path: String::from(db_path),
82            dirty: false,
83            config_files: BTreeMap::new(),
84        }
85    }
86
87    /// Load database from VFS.
88    ///
89    /// If the database file does not exist or the VFS is not available,
90    /// the in-memory database remains empty -- this is not an error.
91    pub fn load(&mut self) -> Result<(), KernelError> {
92        let vfs_lock = match crate::fs::try_get_vfs() {
93            Some(v) => v,
94            None => return Ok(()), // VFS not yet initialised
95        };
96
97        let data = {
98            let vfs = vfs_lock.read();
99            match vfs.resolve_path(&self.db_path) {
100                Ok(node) => {
101                    let meta = node.metadata()?;
102                    let mut buf = vec![0u8; meta.size];
103                    let n = node.read(0, &mut buf)?;
104                    buf.truncate(n);
105                    buf
106                }
107                Err(_) => return Ok(()), // File does not exist yet
108            }
109        };
110
111        self.packages = Self::deserialize(&data)?;
112        self.dirty = false;
113        Ok(())
114    }
115
116    /// Save database to VFS.
117    ///
118    /// Creates parent directories if needed. Silently succeeds if the
119    /// VFS is not available (early boot).
120    pub fn save(&self) -> Result<(), KernelError> {
121        if crate::fs::try_get_vfs().is_none() {
122            return Ok(());
123        }
124
125        let data = self.serialize();
126
127        // Ensure parent directories exist (e.g. /var/pkg)
128        if let Some(parent_end) = self.db_path.rfind('/') {
129            if parent_end > 0 {
130                let parent = &self.db_path[..parent_end];
131                ensure_directories(parent)?;
132            }
133        }
134
135        crate::fs::write_file(&self.db_path, &data)?;
136        Ok(())
137    }
138
139    /// Record a package installation.
140    pub fn record_install(&mut self, record: DbPackageRecord) {
141        self.packages.insert(record.name.clone(), record);
142        self.dirty = true;
143    }
144
145    /// Record a package removal.
146    ///
147    /// Returns the removed record so the caller can log or undo it.
148    pub fn record_remove(&mut self, name: &str) -> Option<DbPackageRecord> {
149        let removed = self.packages.remove(name);
150        if removed.is_some() {
151            self.dirty = true;
152        }
153        removed
154    }
155
156    /// Query all installed packages.
157    pub fn query_installed(&self) -> Vec<&DbPackageRecord> {
158        self.packages.values().collect()
159    }
160
161    /// Check if a package is installed.
162    pub fn is_installed(&self, name: &str) -> bool {
163        self.packages.contains_key(name)
164    }
165
166    /// Get a package record by name.
167    pub fn get(&self, name: &str) -> Option<&DbPackageRecord> {
168        self.packages.get(name)
169    }
170
171    /// Return whether the database has unsaved changes.
172    pub fn is_dirty(&self) -> bool {
173        self.dirty
174    }
175
176    // ------------------------------------------------------------------
177    // Configuration tracking
178    // ------------------------------------------------------------------
179
180    /// Record a configuration file for a package.
181    pub fn track_config_file(&mut self, package: &str, config: ConfigRecord) {
182        let configs = self.config_files.entry(String::from(package)).or_default();
183        // Replace existing entry for the same path
184        if let Some(pos) = configs.iter().position(|c| c.path == config.path) {
185            configs[pos] = config;
186        } else {
187            configs.push(config);
188        }
189        self.dirty = true;
190    }
191
192    /// Check whether a config file has been modified by the user.
193    pub fn is_config_modified(&self, package: &str, path: &str) -> bool {
194        self.config_files
195            .get(package)
196            .and_then(|configs| configs.iter().find(|c| c.path == path))
197            .map(|c| c.is_user_modified)
198            .unwrap_or(false)
199    }
200
201    /// List all tracked config files for a package.
202    pub fn list_config_files(&self, package: &str) -> &[ConfigRecord] {
203        self.config_files.get(package).map_or(&[], |v| v.as_slice())
204    }
205
206    /// Find orphan packages (packages with zero reverse dependencies).
207    ///
208    /// A package is an orphan if no other installed package depends on it.
209    pub fn find_orphans(&self) -> Vec<String> {
210        let mut orphans = Vec::new();
211        for name in self.packages.keys() {
212            let is_depended_on = self
213                .packages
214                .values()
215                .any(|record| record.dependencies.iter().any(|dep| dep == name));
216            if !is_depended_on {
217                orphans.push(name.clone());
218            }
219        }
220        orphans
221    }
222
223    // ------------------------------------------------------------------
224    // Serialization helpers
225    // ------------------------------------------------------------------
226
227    fn serialize(&self) -> Vec<u8> {
228        let mut buf = Vec::new();
229
230        for record in self.packages.values() {
231            Self::write_str(&mut buf, &record.name);
232            Self::write_str(&mut buf, &record.version);
233            buf.extend_from_slice(&record.installed_at.to_le_bytes());
234            buf.extend_from_slice(&record.files_count.to_le_bytes());
235            buf.extend_from_slice(&record.size_bytes.to_le_bytes());
236
237            let dep_count = record.dependencies.len() as u16;
238            buf.extend_from_slice(&dep_count.to_le_bytes());
239            for dep in &record.dependencies {
240                Self::write_str(&mut buf, dep);
241            }
242        }
243
244        buf
245    }
246
247    fn deserialize(data: &[u8]) -> Result<BTreeMap<String, DbPackageRecord>, KernelError> {
248        let mut map = BTreeMap::new();
249        let mut pos = 0;
250
251        while pos < data.len() {
252            let name = Self::read_str(data, &mut pos)?;
253            let version = Self::read_str(data, &mut pos)?;
254
255            let installed_at = Self::read_u64(data, &mut pos)?;
256            let files_count = Self::read_u32(data, &mut pos)?;
257            let size_bytes = Self::read_u64(data, &mut pos)?;
258
259            let dep_count = Self::read_u16(data, &mut pos)? as usize;
260            let mut dependencies = Vec::with_capacity(dep_count);
261            for _ in 0..dep_count {
262                dependencies.push(Self::read_str(data, &mut pos)?);
263            }
264
265            let record = DbPackageRecord {
266                name: name.clone(),
267                version,
268                installed_at,
269                files_count,
270                size_bytes,
271                dependencies,
272            };
273            map.insert(name, record);
274        }
275
276        Ok(map)
277    }
278
279    /// Write a length-prefixed UTF-8 string (u16 length + bytes).
280    fn write_str(buf: &mut Vec<u8>, s: &str) {
281        let len = s.len() as u16;
282        buf.extend_from_slice(&len.to_le_bytes());
283        buf.extend_from_slice(s.as_bytes());
284    }
285
286    /// Read a length-prefixed UTF-8 string.
287    fn read_str(data: &[u8], pos: &mut usize) -> Result<String, KernelError> {
288        let len = Self::read_u16(data, pos)? as usize;
289        if *pos + len > data.len() {
290            return Err(KernelError::InvalidArgument {
291                name: "db_record",
292                value: "truncated_string",
293            });
294        }
295        let s = core::str::from_utf8(&data[*pos..*pos + len]).map_err(|_| {
296            KernelError::InvalidArgument {
297                name: "db_record",
298                value: "invalid_utf8",
299            }
300        })?;
301        *pos += len;
302        Ok(String::from(s))
303    }
304
305    fn read_u16(data: &[u8], pos: &mut usize) -> Result<u16, KernelError> {
306        if *pos + 2 > data.len() {
307            return Err(KernelError::InvalidArgument {
308                name: "db_record",
309                value: "truncated_u16",
310            });
311        }
312        let val = u16::from_le_bytes([data[*pos], data[*pos + 1]]);
313        *pos += 2;
314        Ok(val)
315    }
316
317    fn read_u32(data: &[u8], pos: &mut usize) -> Result<u32, KernelError> {
318        if *pos + 4 > data.len() {
319            return Err(KernelError::InvalidArgument {
320                name: "db_record",
321                value: "truncated_u32",
322            });
323        }
324        let val = u32::from_le_bytes([data[*pos], data[*pos + 1], data[*pos + 2], data[*pos + 3]]);
325        *pos += 4;
326        Ok(val)
327    }
328
329    fn read_u64(data: &[u8], pos: &mut usize) -> Result<u64, KernelError> {
330        if *pos + 8 > data.len() {
331            return Err(KernelError::InvalidArgument {
332                name: "db_record",
333                value: "truncated_u64",
334            });
335        }
336        let val = u64::from_le_bytes([
337            data[*pos],
338            data[*pos + 1],
339            data[*pos + 2],
340            data[*pos + 3],
341            data[*pos + 4],
342            data[*pos + 5],
343            data[*pos + 6],
344            data[*pos + 7],
345        ]);
346        *pos += 8;
347        Ok(val)
348    }
349}
350
351#[cfg(feature = "alloc")]
352impl Default for PackageDatabase {
353    fn default() -> Self {
354        Self::new("/var/pkg/db")
355    }
356}
357
358/// Create directory hierarchy, ignoring errors for existing directories.
359#[cfg(feature = "alloc")]
360fn ensure_directories(path: &str) -> Result<(), KernelError> {
361    use crate::fs::Permissions;
362
363    let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
364
365    let mut current_path = String::new();
366    for component in components {
367        current_path.push('/');
368        current_path.push_str(component);
369
370        if let Some(vfs_lock) = crate::fs::try_get_vfs() {
371            let perms = Permissions::from_mode(0o755);
372            let _ = vfs_lock.write().mkdir(&current_path, perms);
373        }
374    }
375
376    Ok(())
377}