1use parking_lot::RwLock;
48use serde::{Deserialize, Serialize};
49use std::collections::HashMap;
50
51use crate::vfs::{DirEntry, FileType, Inode, InodeId, Permissions, Superblock};
52use crate::{Result, SochDBError};
53
54pub const SYSTEM_TABLE_INODES: &str = "_sys_fs_inodes";
56pub const SYSTEM_TABLE_DIRS: &str = "_sys_fs_dirs";
57pub const SYSTEM_TABLE_SUPERBLOCK: &str = "_sys_fs_superblock";
58
59pub const ROOT_INODE: InodeId = 1;
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct InodeRow {
65 pub inode_id: u64,
66 pub file_type: u8,
67 pub size: u64,
68 pub blocks: Vec<u64>,
69 pub permissions: u16,
70 pub created_us: u64,
71 pub modified_us: u64,
72 pub accessed_us: u64,
73 pub nlink: u32,
74 pub symlink_target: Option<String>,
75 pub soch_schema: Option<String>,
76}
77
78impl From<&Inode> for InodeRow {
79 fn from(inode: &Inode) -> Self {
80 Self {
81 inode_id: inode.id,
82 file_type: inode.file_type as u8,
83 size: inode.size,
84 blocks: inode.blocks.clone(),
85 permissions: inode.permissions.to_mode() as u16,
86 created_us: inode.created_us,
87 modified_us: inode.modified_us,
88 accessed_us: inode.accessed_us,
89 nlink: inode.nlink,
90 symlink_target: inode.symlink_target.clone(),
91 soch_schema: inode.soch_schema.clone(),
92 }
93 }
94}
95
96impl InodeRow {
97 pub fn to_inode(&self) -> Inode {
98 Inode {
99 id: self.inode_id,
100 file_type: match self.file_type {
101 1 => FileType::Regular,
102 2 => FileType::Directory,
103 3 => FileType::Symlink,
104 4 => FileType::SochDocument,
105 _ => FileType::Regular,
106 },
107 size: self.size,
108 blocks: self.blocks.clone(),
109 permissions: Permissions::from_mode(self.permissions as u8),
110 created_us: self.created_us,
111 modified_us: self.modified_us,
112 accessed_us: self.accessed_us,
113 nlink: self.nlink,
114 symlink_target: self.symlink_target.clone(),
115 soch_schema: self.soch_schema.clone(),
116 }
117 }
118
119 pub fn to_bytes(&self) -> Vec<u8> {
121 bincode::serialize(self).unwrap_or_default()
122 }
123
124 pub fn from_bytes(data: &[u8]) -> Result<Self> {
126 bincode::deserialize(data).map_err(|e| SochDBError::Serialization(e.to_string()))
127 }
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct DirEntryRow {
133 pub parent_id: u64,
134 pub name: String,
135 pub child_inode: u64,
136 pub file_type: u8,
137}
138
139impl DirEntryRow {
140 pub fn new(
141 parent_id: InodeId,
142 name: String,
143 child_inode: InodeId,
144 file_type: FileType,
145 ) -> Self {
146 Self {
147 parent_id,
148 name,
149 child_inode,
150 file_type: file_type as u8,
151 }
152 }
153
154 pub fn to_bytes(&self) -> Vec<u8> {
156 bincode::serialize(self).unwrap_or_default()
157 }
158
159 pub fn from_bytes(data: &[u8]) -> Result<Self> {
161 bincode::deserialize(data).map_err(|e| SochDBError::Serialization(e.to_string()))
162 }
163
164 pub fn to_key(&self) -> Vec<u8> {
166 let mut key = Vec::with_capacity(8 + self.name.len());
167 key.extend_from_slice(&self.parent_id.to_le_bytes());
168 key.extend_from_slice(self.name.as_bytes());
169 key
170 }
171
172 pub fn make_key(parent_id: InodeId, name: &str) -> Vec<u8> {
174 let mut key = Vec::with_capacity(8 + name.len());
175 key.extend_from_slice(&parent_id.to_le_bytes());
176 key.extend_from_slice(name.as_bytes());
177 key
178 }
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
183pub enum FsWalOp {
184 CreateInode(InodeRow),
186 UpdateInode(InodeRow),
188 DeleteInode(u64),
190 AddDirEntry(DirEntryRow),
192 RemoveDirEntry { parent_id: u64, name: String },
194 UpdateSuperblock(Superblock),
196}
197
198impl FsWalOp {
199 pub fn to_bytes(&self) -> Vec<u8> {
201 bincode::serialize(self).unwrap_or_default()
202 }
203
204 pub fn from_bytes(data: &[u8]) -> Result<Self> {
206 bincode::deserialize(data).map_err(|e| SochDBError::Serialization(e.to_string()))
207 }
208}
209
210#[allow(clippy::type_complexity)]
214pub struct FsMetadataStore {
215 inodes: RwLock<HashMap<InodeId, Inode>>,
217 directories: RwLock<HashMap<InodeId, Vec<DirEntryRow>>>,
219 superblock: RwLock<Superblock>,
221 write_fn: Box<dyn Fn(&[u8], &[u8]) -> Result<()> + Send + Sync>,
223 wal_fn: Box<dyn Fn(&FsWalOp) -> Result<()> + Send + Sync>,
225 #[allow(dead_code)]
227 dirty_inodes: RwLock<Vec<InodeId>>,
228}
229
230impl FsMetadataStore {
231 pub fn new<W, L>(write_fn: W, wal_fn: L) -> Self
233 where
234 W: Fn(&[u8], &[u8]) -> Result<()> + Send + Sync + 'static,
235 L: Fn(&FsWalOp) -> Result<()> + Send + Sync + 'static,
236 {
237 let superblock = Superblock::new("toonfs");
238 let root_inode = Inode::new_directory(ROOT_INODE);
239
240 let mut inodes = HashMap::new();
241 inodes.insert(ROOT_INODE, root_inode);
242
243 Self {
244 inodes: RwLock::new(inodes),
245 directories: RwLock::new(HashMap::new()),
246 superblock: RwLock::new(superblock),
247 write_fn: Box::new(write_fn),
248 wal_fn: Box::new(wal_fn),
249 dirty_inodes: RwLock::new(Vec::new()),
250 }
251 }
252
253 pub fn init(&self) -> Result<()> {
255 let sb = self.superblock.read();
256 let root = Inode::new_directory(sb.root_inode);
257 drop(sb);
258
259 let row = InodeRow::from(&root);
261 (self.wal_fn)(&FsWalOp::CreateInode(row.clone()))?;
262
263 let key = root.id.to_le_bytes();
264 (self.write_fn)(&key, &row.to_bytes())?;
265
266 self.inodes.write().insert(root.id, root);
267 Ok(())
268 }
269
270 pub fn get_inode(&self, id: InodeId) -> Option<Inode> {
272 self.inodes.read().get(&id).cloned()
273 }
274
275 pub fn create_inode(&self, file_type: FileType) -> Result<Inode> {
277 let id = {
278 let mut sb = self.superblock.write();
279 sb.alloc_inode()
280 };
281
282 let inode = match file_type {
283 FileType::Regular => Inode::new_file(id),
284 FileType::Directory => Inode::new_directory(id),
285 FileType::Symlink => Inode::new_symlink(id, String::new()),
286 FileType::SochDocument => Inode::new_toon(id, String::new()),
287 };
288
289 let row = InodeRow::from(&inode);
291 (self.wal_fn)(&FsWalOp::CreateInode(row.clone()))?;
292
293 let key = inode.id.to_le_bytes();
295 (self.write_fn)(&key, &row.to_bytes())?;
296
297 self.inodes.write().insert(id, inode.clone());
299
300 Ok(inode)
301 }
302
303 pub fn update_inode(&self, inode: &Inode) -> Result<()> {
305 let row = InodeRow::from(inode);
306
307 (self.wal_fn)(&FsWalOp::UpdateInode(row.clone()))?;
309
310 let key = inode.id.to_le_bytes();
312 (self.write_fn)(&key, &row.to_bytes())?;
313
314 self.inodes.write().insert(inode.id, inode.clone());
316
317 Ok(())
318 }
319
320 pub fn delete_inode(&self, id: InodeId) -> Result<()> {
322 (self.wal_fn)(&FsWalOp::DeleteInode(id))?;
324
325 self.inodes.write().remove(&id);
327
328 Ok(())
329 }
330
331 pub fn add_dir_entry(
333 &self,
334 parent_id: InodeId,
335 name: &str,
336 child_id: InodeId,
337 file_type: FileType,
338 ) -> Result<()> {
339 let entry = DirEntryRow::new(parent_id, name.to_string(), child_id, file_type);
340
341 (self.wal_fn)(&FsWalOp::AddDirEntry(entry.clone()))?;
343
344 let key = entry.to_key();
346 (self.write_fn)(&key, &entry.to_bytes())?;
347
348 self.directories
350 .write()
351 .entry(parent_id)
352 .or_default()
353 .push(entry);
354
355 Ok(())
356 }
357
358 pub fn remove_dir_entry(&self, parent_id: InodeId, name: &str) -> Result<()> {
360 (self.wal_fn)(&FsWalOp::RemoveDirEntry {
362 parent_id,
363 name: name.to_string(),
364 })?;
365
366 let mut dirs = self.directories.write();
368 if let Some(entries) = dirs.get_mut(&parent_id) {
369 entries.retain(|e| e.name != name);
370 }
371
372 Ok(())
373 }
374
375 pub fn list_dir(&self, parent_id: InodeId) -> Vec<DirEntryRow> {
377 self.directories
378 .read()
379 .get(&parent_id)
380 .cloned()
381 .unwrap_or_default()
382 }
383
384 pub fn lookup(&self, parent_id: InodeId, name: &str) -> Option<InodeId> {
386 self.directories
387 .read()
388 .get(&parent_id)
389 .and_then(|entries| entries.iter().find(|e| e.name == name))
390 .map(|e| e.child_inode)
391 }
392
393 pub fn resolve_path(&self, path: &str) -> Result<InodeId> {
397 let path = path.trim_start_matches('/');
398 if path.is_empty() {
399 return Ok(ROOT_INODE);
400 }
401
402 let mut current = ROOT_INODE;
403 for component in path.split('/') {
404 if component.is_empty() || component == "." {
405 continue;
406 }
407 if component == ".." {
408 if let Some(inode) = self.get_inode(current)
410 && inode.is_dir()
411 {
412 }
415 continue;
416 }
417
418 current = self.lookup(current, component).ok_or_else(|| {
419 SochDBError::NotFound(format!("Path component not found: {}", component))
420 })?;
421 }
422
423 Ok(current)
424 }
425
426 pub fn create_file(&self, parent_id: InodeId, name: &str) -> Result<Inode> {
428 let parent = self
430 .get_inode(parent_id)
431 .ok_or_else(|| SochDBError::NotFound("Parent not found".into()))?;
432
433 if !parent.is_dir() {
434 return Err(SochDBError::InvalidArgument(
435 "Parent is not a directory".into(),
436 ));
437 }
438
439 if self.lookup(parent_id, name).is_some() {
441 return Err(SochDBError::InvalidArgument("File already exists".into()));
442 }
443
444 let inode = self.create_inode(FileType::Regular)?;
446
447 self.add_dir_entry(parent_id, name, inode.id, FileType::Regular)?;
449
450 Ok(inode)
451 }
452
453 pub fn create_dir(&self, parent_id: InodeId, name: &str) -> Result<Inode> {
455 let parent = self
457 .get_inode(parent_id)
458 .ok_or_else(|| SochDBError::NotFound("Parent not found".into()))?;
459
460 if !parent.is_dir() {
461 return Err(SochDBError::InvalidArgument(
462 "Parent is not a directory".into(),
463 ));
464 }
465
466 if self.lookup(parent_id, name).is_some() {
468 return Err(SochDBError::InvalidArgument(
469 "Directory already exists".into(),
470 ));
471 }
472
473 let inode = self.create_inode(FileType::Directory)?;
475
476 self.add_dir_entry(parent_id, name, inode.id, FileType::Directory)?;
478
479 Ok(inode)
480 }
481
482 pub fn delete(&self, parent_id: InodeId, name: &str) -> Result<()> {
484 let child_id = self
485 .lookup(parent_id, name)
486 .ok_or_else(|| SochDBError::NotFound("Entry not found".into()))?;
487
488 let child = self
489 .get_inode(child_id)
490 .ok_or_else(|| SochDBError::NotFound("Inode not found".into()))?;
491
492 if child.is_dir() {
494 let entries = self.list_dir(child_id);
495 if !entries.is_empty() {
496 return Err(SochDBError::InvalidArgument("Directory not empty".into()));
497 }
498 }
499
500 self.remove_dir_entry(parent_id, name)?;
502
503 self.delete_inode(child_id)?;
505
506 Ok(())
507 }
508
509 pub fn superblock(&self) -> Superblock {
511 self.superblock.read().clone()
512 }
513
514 pub fn update_superblock(&self, sb: &Superblock) -> Result<()> {
516 (self.wal_fn)(&FsWalOp::UpdateSuperblock(sb.clone()))?;
517 *self.superblock.write() = sb.clone();
518 Ok(())
519 }
520
521 pub fn replay_wal_op(&self, op: &FsWalOp) -> Result<()> {
523 match op {
524 FsWalOp::CreateInode(row) => {
525 self.inodes.write().insert(row.inode_id, row.to_inode());
526 }
527 FsWalOp::UpdateInode(row) => {
528 self.inodes.write().insert(row.inode_id, row.to_inode());
529 }
530 FsWalOp::DeleteInode(id) => {
531 self.inodes.write().remove(id);
532 }
533 FsWalOp::AddDirEntry(entry) => {
534 self.directories
535 .write()
536 .entry(entry.parent_id)
537 .or_default()
538 .push(entry.clone());
539 }
540 FsWalOp::RemoveDirEntry { parent_id, name } => {
541 let mut dirs = self.directories.write();
542 if let Some(entries) = dirs.get_mut(parent_id) {
543 entries.retain(|e| &e.name != name);
544 }
545 }
546 FsWalOp::UpdateSuperblock(sb) => {
547 *self.superblock.write() = sb.clone();
548 }
549 }
550 Ok(())
551 }
552}
553
554#[allow(clippy::type_complexity)]
556pub struct SochFS {
557 metadata: FsMetadataStore,
559 block_write_fn: Box<dyn Fn(u64, &[u8]) -> Result<u64> + Send + Sync>,
561 block_read_fn: Box<dyn Fn(u64, usize) -> Result<Vec<u8>> + Send + Sync>,
563}
564
565impl SochFS {
566 pub fn new<W, L, BW, BR>(write_fn: W, wal_fn: L, block_write_fn: BW, block_read_fn: BR) -> Self
568 where
569 W: Fn(&[u8], &[u8]) -> Result<()> + Send + Sync + 'static,
570 L: Fn(&FsWalOp) -> Result<()> + Send + Sync + 'static,
571 BW: Fn(u64, &[u8]) -> Result<u64> + Send + Sync + 'static,
572 BR: Fn(u64, usize) -> Result<Vec<u8>> + Send + Sync + 'static,
573 {
574 Self {
575 metadata: FsMetadataStore::new(write_fn, wal_fn),
576 block_write_fn: Box::new(block_write_fn),
577 block_read_fn: Box::new(block_read_fn),
578 }
579 }
580
581 pub fn init(&self) -> Result<()> {
583 self.metadata.init()
584 }
585
586 pub fn resolve(&self, path: &str) -> Result<InodeId> {
588 self.metadata.resolve_path(path)
589 }
590
591 pub fn get_inode(&self, id: InodeId) -> Option<Inode> {
593 self.metadata.get_inode(id)
594 }
595
596 pub fn create_file(&self, path: &str) -> Result<Inode> {
598 let (parent_path, name) = split_path(path);
599 let parent_id = self.metadata.resolve_path(&parent_path)?;
600 self.metadata.create_file(parent_id, &name)
601 }
602
603 pub fn mkdir(&self, path: &str) -> Result<Inode> {
605 let (parent_path, name) = split_path(path);
606 let parent_id = self.metadata.resolve_path(&parent_path)?;
607 self.metadata.create_dir(parent_id, &name)
608 }
609
610 pub fn delete(&self, path: &str) -> Result<()> {
612 let (parent_path, name) = split_path(path);
613 let parent_id = self.metadata.resolve_path(&parent_path)?;
614 self.metadata.delete(parent_id, &name)
615 }
616
617 pub fn readdir(&self, path: &str) -> Result<Vec<DirEntry>> {
619 let inode_id = self.metadata.resolve_path(path)?;
620 let inode = self
621 .metadata
622 .get_inode(inode_id)
623 .ok_or_else(|| SochDBError::NotFound("Directory not found".into()))?;
624
625 if !inode.is_dir() {
626 return Err(SochDBError::InvalidArgument("Not a directory".into()));
627 }
628
629 let entries = self.metadata.list_dir(inode_id);
630 Ok(entries
631 .into_iter()
632 .map(|e| DirEntry {
633 name: e.name,
634 inode: e.child_inode,
635 file_type: match e.file_type {
636 1 => FileType::Regular,
637 2 => FileType::Directory,
638 3 => FileType::Symlink,
639 4 => FileType::SochDocument,
640 _ => FileType::Regular,
641 },
642 })
643 .collect())
644 }
645
646 pub fn write_file(&self, path: &str, data: &[u8]) -> Result<usize> {
648 let inode_id = self.metadata.resolve_path(path)?;
649 let mut inode = self
650 .metadata
651 .get_inode(inode_id)
652 .ok_or_else(|| SochDBError::NotFound("File not found".into()))?;
653
654 if !inode.is_file() && !inode.is_toon() {
655 return Err(SochDBError::InvalidArgument("Not a regular file".into()));
656 }
657
658 let block_id = (self.block_write_fn)(inode_id, data)?;
660
661 inode.blocks = vec![block_id];
663 inode.size = data.len() as u64;
664 inode.touch();
665
666 self.metadata.update_inode(&inode)?;
667
668 Ok(data.len())
669 }
670
671 pub fn read_file(&self, path: &str) -> Result<Vec<u8>> {
673 let inode_id = self.metadata.resolve_path(path)?;
674 let inode = self
675 .metadata
676 .get_inode(inode_id)
677 .ok_or_else(|| SochDBError::NotFound("File not found".into()))?;
678
679 if !inode.is_file() && !inode.is_toon() {
680 return Err(SochDBError::InvalidArgument("Not a regular file".into()));
681 }
682
683 if inode.blocks.is_empty() {
684 return Ok(Vec::new());
685 }
686
687 let mut data = Vec::new();
689 for &block_id in &inode.blocks {
690 let block_data = (self.block_read_fn)(block_id, inode.size as usize)?;
691 data.extend(block_data);
692 }
693
694 Ok(data)
695 }
696
697 pub fn stat(&self, path: &str) -> Result<Inode> {
699 let inode_id = self.metadata.resolve_path(path)?;
700 self.metadata
701 .get_inode(inode_id)
702 .ok_or_else(|| SochDBError::NotFound("File not found".into()))
703 }
704}
705
706fn split_path(path: &str) -> (String, String) {
708 let path = path.trim_end_matches('/');
709 if let Some(pos) = path.rfind('/') {
710 let parent = if pos == 0 { "/" } else { &path[..pos] };
711 let name = &path[pos + 1..];
712 (parent.to_string(), name.to_string())
713 } else {
714 ("/".to_string(), path.to_string())
715 }
716}
717
718#[cfg(test)]
719mod tests {
720 use super::*;
721 use std::sync::atomic::{AtomicU64, Ordering};
722
723 #[test]
724 fn test_inode_row_serialization() {
725 let inode = Inode::new_file(42);
726 let row = InodeRow::from(&inode);
727 let bytes = row.to_bytes();
728 let recovered = InodeRow::from_bytes(&bytes).unwrap();
729
730 assert_eq!(recovered.inode_id, 42);
731 assert_eq!(recovered.file_type, FileType::Regular as u8);
732 }
733
734 #[test]
735 fn test_dir_entry_key() {
736 let entry = DirEntryRow::new(1, "test.txt".to_string(), 42, FileType::Regular);
737 let key = entry.to_key();
738 let expected_key = DirEntryRow::make_key(1, "test.txt");
739 assert_eq!(key, expected_key);
740 }
741
742 #[test]
743 fn test_path_split() {
744 assert_eq!(
745 split_path("/foo/bar"),
746 ("/foo".to_string(), "bar".to_string())
747 );
748 assert_eq!(split_path("/foo"), ("/".to_string(), "foo".to_string()));
749 assert_eq!(split_path("foo"), ("/".to_string(), "foo".to_string()));
750 }
751
752 #[test]
753 fn test_metadata_store() {
754 let store = FsMetadataStore::new(|_, _| Ok(()), |_| Ok(()));
755 store.init().unwrap();
756
757 let file = store.create_file(ROOT_INODE, "test.txt").unwrap();
759 assert!(file.is_file());
760
761 let found = store.lookup(ROOT_INODE, "test.txt");
763 assert_eq!(found, Some(file.id));
764
765 let entries = store.list_dir(ROOT_INODE);
767 assert_eq!(entries.len(), 1);
768 assert_eq!(entries[0].name, "test.txt");
769 }
770
771 #[test]
772 fn test_path_resolution() {
773 let store = FsMetadataStore::new(|_, _| Ok(()), |_| Ok(()));
774 store.init().unwrap();
775
776 let docs = store.create_dir(ROOT_INODE, "docs").unwrap();
778 let reports = store.create_dir(docs.id, "reports").unwrap();
779 let _summary = store.create_file(reports.id, "summary.txt").unwrap();
780
781 assert_eq!(store.resolve_path("/").unwrap(), ROOT_INODE);
783 assert_eq!(store.resolve_path("/docs").unwrap(), docs.id);
784 assert_eq!(store.resolve_path("/docs/reports").unwrap(), reports.id);
785 }
786
787 #[test]
788 fn test_toonfs() {
789 let block_counter = AtomicU64::new(0);
790 let blocks: std::sync::Arc<RwLock<HashMap<u64, Vec<u8>>>> =
791 std::sync::Arc::new(RwLock::new(HashMap::new()));
792 let blocks_write = blocks.clone();
793 let blocks_read = blocks.clone();
794
795 let fs = SochFS::new(
796 |_, _| Ok(()),
797 |_| Ok(()),
798 move |_inode, data: &[u8]| {
799 let id = block_counter.fetch_add(1, Ordering::SeqCst);
800 blocks_write.write().insert(id, data.to_vec());
801 Ok(id)
802 },
803 move |id, _size| {
804 blocks_read
805 .read()
806 .get(&id)
807 .cloned()
808 .ok_or_else(|| SochDBError::NotFound("Block not found".into()))
809 },
810 );
811
812 fs.init().unwrap();
813
814 fs.create_file("/test.txt").unwrap();
816 fs.write_file("/test.txt", b"Hello, SochFS!").unwrap();
817
818 let data = fs.read_file("/test.txt").unwrap();
820 assert_eq!(data, b"Hello, SochFS!");
821
822 let stat = fs.stat("/test.txt").unwrap();
824 assert_eq!(stat.size, 14);
825 }
826}