sochdb_core/
vfs.rs

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