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