Skip to main content

sochdb_core/
vfs.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// SochDB - LLM-Optimized Embedded Database
3// Copyright (C) 2026 Sushanth Reddy Vanagala (https://github.com/sushanthpy)
4//
5// This program is free software: you can redistribute it and/or modify
6// it under the terms of the GNU Affero General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// This program is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU Affero General Public License for more details.
14//
15// You should have received a copy of the GNU Affero General Public License
16// along with this program. If not, see <https://www.gnu.org/licenses/>.
17
18//! Virtual Filesystem Types for SochDB
19//!
20//! POSIX-like filesystem interface backed by WAL for ACID guarantees.
21
22use serde::{Deserialize, Serialize};
23use std::time::SystemTime;
24
25/// Inode number (unique file identifier)
26pub type InodeId = u64;
27
28/// Block number for data storage
29pub type BlockId = u64;
30
31/// File types in the VFS
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
33#[repr(u8)]
34pub enum FileType {
35    /// Regular file
36    Regular = 1,
37    /// Directory
38    Directory = 2,
39    /// Symbolic link
40    Symlink = 3,
41    /// TOON document (special type for native format)
42    SochDocument = 4,
43}
44
45/// File permissions (Unix-style)
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
47pub struct Permissions {
48    pub read: bool,
49    pub write: bool,
50    pub execute: bool,
51}
52
53impl Permissions {
54    pub fn new(read: bool, write: bool, execute: bool) -> Self {
55        Self {
56            read,
57            write,
58            execute,
59        }
60    }
61
62    pub fn all() -> Self {
63        Self {
64            read: true,
65            write: true,
66            execute: true,
67        }
68    }
69
70    pub fn read_only() -> Self {
71        Self {
72            read: true,
73            write: false,
74            execute: false,
75        }
76    }
77
78    pub fn read_write() -> Self {
79        Self {
80            read: true,
81            write: true,
82            execute: false,
83        }
84    }
85
86    pub fn to_mode(&self) -> u8 {
87        let mut mode = 0u8;
88        if self.read {
89            mode |= 0b100;
90        }
91        if self.write {
92            mode |= 0b010;
93        }
94        if self.execute {
95            mode |= 0b001;
96        }
97        mode
98    }
99
100    pub fn from_mode(mode: u8) -> Self {
101        Self {
102            read: mode & 0b100 != 0,
103            write: mode & 0b010 != 0,
104            execute: mode & 0b001 != 0,
105        }
106    }
107}
108
109impl Default for Permissions {
110    fn default() -> Self {
111        Self::read_write()
112    }
113}
114
115/// Inode structure (file/directory metadata)
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct Inode {
118    /// Unique inode number
119    pub id: InodeId,
120    /// File type
121    pub file_type: FileType,
122    /// File size in bytes
123    pub size: u64,
124    /// Data blocks (for regular files)
125    pub blocks: Vec<BlockId>,
126    /// Permissions
127    pub permissions: Permissions,
128    /// Creation time (microseconds since epoch)
129    pub created_us: u64,
130    /// Modification time (microseconds since epoch)
131    pub modified_us: u64,
132    /// Access time (microseconds since epoch)
133    pub accessed_us: u64,
134    /// Number of hard links
135    pub nlink: u32,
136    /// For symlinks: target path
137    pub symlink_target: Option<String>,
138    /// For TOON documents: schema name
139    pub soch_schema: Option<String>,
140}
141
142impl Inode {
143    pub fn new_file(id: InodeId) -> Self {
144        let now = now_micros();
145        Self {
146            id,
147            file_type: FileType::Regular,
148            size: 0,
149            blocks: Vec::new(),
150            permissions: Permissions::read_write(),
151            created_us: now,
152            modified_us: now,
153            accessed_us: now,
154            nlink: 1,
155            symlink_target: None,
156            soch_schema: None,
157        }
158    }
159
160    pub fn new_directory(id: InodeId) -> Self {
161        let now = now_micros();
162        Self {
163            id,
164            file_type: FileType::Directory,
165            size: 0,
166            blocks: Vec::new(),
167            permissions: Permissions::all(),
168            created_us: now,
169            modified_us: now,
170            accessed_us: now,
171            nlink: 2, // . and ..
172            symlink_target: None,
173            soch_schema: None,
174        }
175    }
176
177    pub fn new_symlink(id: InodeId, target: String) -> Self {
178        let now = now_micros();
179        Self {
180            id,
181            file_type: FileType::Symlink,
182            size: target.len() as u64,
183            blocks: Vec::new(),
184            permissions: Permissions::all(),
185            created_us: now,
186            modified_us: now,
187            accessed_us: now,
188            nlink: 1,
189            symlink_target: Some(target),
190            soch_schema: None,
191        }
192    }
193
194    pub fn new_toon(id: InodeId, schema: String) -> Self {
195        let now = now_micros();
196        Self {
197            id,
198            file_type: FileType::SochDocument,
199            size: 0,
200            blocks: Vec::new(),
201            permissions: Permissions::read_write(),
202            created_us: now,
203            modified_us: now,
204            accessed_us: now,
205            nlink: 1,
206            symlink_target: None,
207            soch_schema: Some(schema),
208        }
209    }
210
211    pub fn is_dir(&self) -> bool {
212        self.file_type == FileType::Directory
213    }
214
215    pub fn is_file(&self) -> bool {
216        self.file_type == FileType::Regular
217    }
218
219    pub fn is_symlink(&self) -> bool {
220        self.file_type == FileType::Symlink
221    }
222
223    pub fn is_toon(&self) -> bool {
224        self.file_type == FileType::SochDocument
225    }
226
227    pub fn touch(&mut self) {
228        self.modified_us = now_micros();
229        self.accessed_us = self.modified_us;
230    }
231
232    pub fn update_access_time(&mut self) {
233        self.accessed_us = now_micros();
234    }
235
236    /// Serialize inode to bytes
237    pub fn to_bytes(&self) -> Result<Vec<u8>, String> {
238        bincode::serialize(self).map_err(|e| e.to_string())
239    }
240
241    /// Deserialize inode from bytes
242    pub fn from_bytes(data: &[u8]) -> Result<Self, String> {
243        bincode::deserialize(data).map_err(|e| e.to_string())
244    }
245}
246
247/// Directory entry
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct DirEntry {
250    /// Entry name (file/subdirectory name)
251    pub name: String,
252    /// Inode number
253    pub inode: InodeId,
254    /// File type (cached for readdir efficiency)
255    pub file_type: FileType,
256}
257
258impl DirEntry {
259    pub fn new(name: impl Into<String>, inode: InodeId, file_type: FileType) -> Self {
260        Self {
261            name: name.into(),
262            inode,
263            file_type,
264        }
265    }
266}
267
268/// Directory contents
269#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct Directory {
271    /// Parent inode (0 for root)
272    pub parent: InodeId,
273    /// Directory entries
274    pub entries: Vec<DirEntry>,
275}
276
277impl Directory {
278    pub fn new(parent: InodeId) -> Self {
279        Self {
280            parent,
281            entries: Vec::new(),
282        }
283    }
284
285    pub fn add_entry(&mut self, entry: DirEntry) {
286        self.entries.push(entry);
287    }
288
289    pub fn remove_entry(&mut self, name: &str) -> Option<DirEntry> {
290        if let Some(pos) = self.entries.iter().position(|e| e.name == name) {
291            Some(self.entries.remove(pos))
292        } else {
293            None
294        }
295    }
296
297    pub fn find_entry(&self, name: &str) -> Option<&DirEntry> {
298        self.entries.iter().find(|e| e.name == name)
299    }
300
301    pub fn contains(&self, name: &str) -> bool {
302        self.entries.iter().any(|e| e.name == name)
303    }
304
305    /// Serialize to bytes
306    pub fn to_bytes(&self) -> Result<Vec<u8>, String> {
307        bincode::serialize(self).map_err(|e| e.to_string())
308    }
309
310    /// Deserialize from bytes
311    pub fn from_bytes(data: &[u8]) -> Result<Self, String> {
312        bincode::deserialize(data).map_err(|e| e.to_string())
313    }
314}
315
316/// Superblock - filesystem metadata
317#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct Superblock {
319    /// Magic number for identification
320    pub magic: [u8; 4],
321    /// Filesystem version
322    pub version: u32,
323    /// Root inode number
324    pub root_inode: InodeId,
325    /// Next free inode number
326    pub next_inode: InodeId,
327    /// Next free block number
328    pub next_block: BlockId,
329    /// Total inodes allocated
330    pub total_inodes: u64,
331    /// Total blocks used
332    pub total_blocks: u64,
333    /// Block size in bytes
334    pub block_size: u32,
335    /// Creation time
336    pub created_us: u64,
337    /// Last mount time
338    pub mounted_us: u64,
339    /// Filesystem label
340    pub label: String,
341}
342
343impl Superblock {
344    pub const MAGIC: [u8; 4] = *b"TOON";
345    pub const VERSION: u32 = 1;
346    pub const DEFAULT_BLOCK_SIZE: u32 = 4096;
347
348    pub fn new(label: impl Into<String>) -> Self {
349        let now = now_micros();
350        Self {
351            magic: Self::MAGIC,
352            version: Self::VERSION,
353            root_inode: 1, // Root directory is always inode 1
354            next_inode: 2,
355            next_block: 0,
356            total_inodes: 1,
357            total_blocks: 0,
358            block_size: Self::DEFAULT_BLOCK_SIZE,
359            created_us: now,
360            mounted_us: now,
361            label: label.into(),
362        }
363    }
364
365    /// Allocate a new inode number
366    pub fn alloc_inode(&mut self) -> InodeId {
367        let id = self.next_inode;
368        self.next_inode += 1;
369        self.total_inodes += 1;
370        id
371    }
372
373    /// Allocate block numbers
374    pub fn alloc_blocks(&mut self, count: u64) -> Vec<BlockId> {
375        let start = self.next_block;
376        self.next_block += count;
377        self.total_blocks += count;
378        (start..start + count).collect()
379    }
380
381    /// Serialize to bytes
382    pub fn to_bytes(&self) -> Result<Vec<u8>, String> {
383        bincode::serialize(self).map_err(|e| e.to_string())
384    }
385
386    /// Deserialize from bytes
387    pub fn from_bytes(data: &[u8]) -> Result<Self, String> {
388        let sb: Self = bincode::deserialize(data).map_err(|e| e.to_string())?;
389        if sb.magic != Self::MAGIC {
390            return Err("Invalid magic number".into());
391        }
392        Ok(sb)
393    }
394}
395
396/// VFS operation types for WAL
397#[derive(Debug, Clone, Serialize, Deserialize)]
398pub enum VfsOp {
399    /// Create file
400    CreateFile {
401        parent: InodeId,
402        name: String,
403        inode: Inode,
404    },
405    /// Create directory
406    CreateDir {
407        parent: InodeId,
408        name: String,
409        inode: Inode,
410    },
411    /// Delete entry
412    Delete {
413        parent: InodeId,
414        name: String,
415        inode: InodeId,
416    },
417    /// Rename/move entry
418    Rename {
419        old_parent: InodeId,
420        old_name: String,
421        new_parent: InodeId,
422        new_name: String,
423        inode: InodeId,
424    },
425    /// Write data block
426    WriteBlock {
427        inode: InodeId,
428        block: BlockId,
429        data: Vec<u8>,
430    },
431    /// Truncate file
432    Truncate { inode: InodeId, new_size: u64 },
433    /// Update inode metadata
434    UpdateInode { inode: Inode },
435    /// Update superblock
436    UpdateSuperblock { superblock: Superblock },
437    /// Create symlink
438    CreateSymlink {
439        parent: InodeId,
440        name: String,
441        inode: Inode,
442        target: String,
443    },
444}
445
446/// File stat information (like POSIX stat)
447#[derive(Debug, Clone)]
448pub struct FileStat {
449    pub inode: InodeId,
450    pub file_type: FileType,
451    pub size: u64,
452    pub blocks: u64,
453    pub block_size: u32,
454    pub nlink: u32,
455    pub permissions: Permissions,
456    pub created: SystemTime,
457    pub modified: SystemTime,
458    pub accessed: SystemTime,
459}
460
461impl From<&Inode> for FileStat {
462    fn from(inode: &Inode) -> Self {
463        Self {
464            inode: inode.id,
465            file_type: inode.file_type,
466            size: inode.size,
467            blocks: inode.blocks.len() as u64,
468            block_size: Superblock::DEFAULT_BLOCK_SIZE,
469            nlink: inode.nlink,
470            permissions: inode.permissions,
471            created: micros_to_system_time(inode.created_us),
472            modified: micros_to_system_time(inode.modified_us),
473            accessed: micros_to_system_time(inode.accessed_us),
474        }
475    }
476}
477
478/// Get current time in microseconds since Unix epoch
479fn now_micros() -> u64 {
480    SystemTime::now()
481        .duration_since(SystemTime::UNIX_EPOCH)
482        .map(|d| d.as_micros() as u64)
483        .unwrap_or(0)
484}
485
486/// Convert microseconds to SystemTime
487fn micros_to_system_time(micros: u64) -> SystemTime {
488    SystemTime::UNIX_EPOCH + std::time::Duration::from_micros(micros)
489}
490
491#[cfg(test)]
492mod tests {
493    use super::*;
494
495    #[test]
496    fn test_inode_serialization() {
497        let inode = Inode::new_file(42);
498        let bytes = inode.to_bytes().expect("Failed to serialize inode");
499        let parsed = Inode::from_bytes(&bytes).unwrap();
500        assert_eq!(parsed.id, 42);
501        assert!(parsed.is_file());
502    }
503
504    #[test]
505    fn test_directory_operations() {
506        let mut dir = Directory::new(1);
507        dir.add_entry(DirEntry::new("file1.txt", 10, FileType::Regular));
508        dir.add_entry(DirEntry::new("subdir", 11, FileType::Directory));
509
510        assert!(dir.contains("file1.txt"));
511        assert!(dir.contains("subdir"));
512        assert!(!dir.contains("nonexistent"));
513
514        let entry = dir.find_entry("file1.txt").unwrap();
515        assert_eq!(entry.inode, 10);
516
517        let removed = dir.remove_entry("file1.txt").unwrap();
518        assert_eq!(removed.inode, 10);
519        assert!(!dir.contains("file1.txt"));
520    }
521
522    #[test]
523    fn test_superblock() {
524        let mut sb = Superblock::new("test-fs");
525        assert_eq!(sb.magic, Superblock::MAGIC);
526        assert_eq!(sb.root_inode, 1);
527
528        let inode1 = sb.alloc_inode();
529        let inode2 = sb.alloc_inode();
530        assert_eq!(inode1, 2);
531        assert_eq!(inode2, 3);
532
533        let blocks = sb.alloc_blocks(5);
534        assert_eq!(blocks.len(), 5);
535        assert_eq!(blocks[0], 0);
536        assert_eq!(blocks[4], 4);
537    }
538
539    #[test]
540    fn test_permissions() {
541        let perms = Permissions::new(true, true, false);
542        assert_eq!(perms.to_mode(), 0b110);
543
544        let from_mode = Permissions::from_mode(0b101);
545        assert!(from_mode.read);
546        assert!(!from_mode.write);
547        assert!(from_mode.execute);
548    }
549}