diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index f1177d28..fba732a2 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -50,7 +50,7 @@ ### 🚀 次フェーズ予定 - ~~**Phase 113**: FileHandleBox NyashBox 公開 API(.hako 側からの呼び出し)~~ ✅ 完了(2025-12-04) -- **Phase 114**: FileIo 機能拡張(exists/stat/canonicalize) +- ~~**Phase 114**: FileIo 機能拡張(exists/stat/canonicalize)~~ ✅ 完了(2025-12-04) - **Phase 115**: 並行アクセス安全性(Arc>) - **Phase 116**: file encoding explicit 指定(UTF-8 以外) - **Phase 117+**: 追加プロファイル(TestMock/Sandbox/ReadOnly/Embedded) diff --git a/docs/development/current/main/core_boxes_design.md b/docs/development/current/main/core_boxes_design.md index 15c072ff..746c55e1 100644 --- a/docs/development/current/main/core_boxes_design.md +++ b/docs/development/current/main/core_boxes_design.md @@ -2029,3 +2029,25 @@ impl Ring0Registry { --- **Phase 112 実装完了日**: 2025-12-03 + + +### Phase 114: FileIo trait 拡張(exists/stat/canonicalize) + +FileIo trait に exists/stat/canonicalize を正式追加。 +FileHandleBox の内部メソッド(is_file/is_dir/size)を stat() に統一。 +Ring0FsFileIo が path を管理し、stat() で正の情報を返す設計。 +NoFsFileIo は exists() → false, stat() → Unsupported エラー。 + +**設計原則**: +- **FsApi = Stateless**: パスを毎回引数で受け取る(Ring0 レイヤー) +- **FileIo = Stateful**: open() で path を保持(Ring1 レイヤー) +- **FileHandleBox**: metadata_internal() で FileIo::stat() を経由 + +**実装成果**: +- FileStat 構造体追加(is_file/is_dir/size) +- Ring0FsFileIo: ring0.fs.metadata() を stat() で wrap +- NoFsFileIo: exists=false, stat=Unsupported +- FileHandleBox: downcast 不要、trait 経由で統一 + +**詳細**: [phase114_fileio_trait_extension.md](./phase114_fileio_trait_extension.md) + diff --git a/docs/development/current/main/phase114_fileio_trait_extension.md b/docs/development/current/main/phase114_fileio_trait_extension.md new file mode 100644 index 00000000..ddc8411f --- /dev/null +++ b/docs/development/current/main/phase114_fileio_trait_extension.md @@ -0,0 +1,246 @@ +# Phase 114: FileIo trait 拡張 & メタデータ統一 + +## 目標 + +FsApi 側に揃った情報(exists / metadata / canonicalize)を、FileIo trait 経由で一貫して扱えるようにする。 +FileHandleBox 内部の is_file/is_dir/size などをすべて FileIo::stat() の上に乗せる。 +.hako 側 API(Phase 113 で公開したメソッド群)は変更しない(内部実装だけきれい化)。 + +## FileIo trait 設計(FsApi との分離:stateless vs stateful) + +### FsApi (Ring0) - Stateless OS抽象 + +```rust +pub trait FsApi { + fn exists(&self, path: &Path) -> bool; + fn metadata(&self, path: &Path) -> Result; + fn canonicalize(&self, path: &Path) -> Result; + fn read_to_string(&self, path: &Path) -> Result; + fn write_all(&self, path: &Path, data: &[u8]) -> Result<()>; + fn append_all(&self, path: &Path, data: &[u8]) -> Result<()>; +} +``` + +- **Role**: OS ファイルシステムの直接抽象化 +- **State**: なし(パス引数で毎回指定) +- **Usage**: Ring0Context 経由でアクセス + +### FileIo (Ring1) - Stateful ハンドル抽象 + +```rust +pub struct FileStat { + pub is_file: bool, + pub is_dir: bool, + pub size: u64, +} + +pub trait FileIo: Send + Sync { + fn caps(&self) -> FileCaps; + fn open(&self, path: &str) -> FileResult<()>; + fn read(&self) -> FileResult; + fn write(&self, text: &str) -> FileResult<()>; + fn close(&self) -> FileResult<()>; + fn as_any(&self) -> &dyn std::any::Any; + + // Phase 114: Metadata operations + fn exists(&self) -> bool; + fn stat(&self) -> FileResult; + fn canonicalize(&self) -> FileResult; +} +``` + +- **Role**: 現在開いているファイルハンドルに対する操作 +- **State**: あり(open() で path を内部保持) +- **Usage**: FileHandleBox 経由でアクセス + +### 設計原則 + +1. **FsApi = Stateless**: パスを毎回引数で受け取る +2. **FileIo = Stateful**: open() で path を保持、以降は引数不要 +3. **分離理由**: + - FsApi: OS レイヤーの直接操作(低レベル) + - FileIo: ハンドルベースの高レベル API(.hako からアクセス) + +## Ring0FsFileIo/NoFsFileIo の実装詳細 + +### Ring0FsFileIo (Default プロファイル) + +```rust +pub struct Ring0FsFileIo { + ring0: Arc, + path: RwLock>, // open() で設定 + mode: RwLock>, // "r", "w", "a" +} + +impl FileIo for Ring0FsFileIo { + fn exists(&self) -> bool { + // path が設定されていれば ring0.fs.exists() を呼ぶ + // path が None なら false + } + + fn stat(&self) -> FileResult { + // path が設定されていれば ring0.fs.metadata() を呼ぶ + // FileStat に変換して返す + } + + fn canonicalize(&self) -> FileResult { + // path が設定されていれば ring0.fs.canonicalize() を呼ぶ + // PathBuf を String に変換して返す + } +} +``` + +### NoFsFileIo (NoFs プロファイル) + +```rust +pub struct NoFsFileIo; + +impl FileIo for NoFsFileIo { + fn exists(&self) -> bool { + // NoFs プロファイルでは常に false + false + } + + fn stat(&self) -> FileResult { + // Unsupported エラーを返す + Err(FileError::Unsupported("...")) + } + + fn canonicalize(&self) -> FileResult { + // Unsupported エラーを返す + Err(FileError::Unsupported("...")) + } +} +``` + +## FileHandleBox の metadata_internal() 統一設計 + +### Before Phase 114 + +```rust +// Ring0FsFileIo.metadata() を直接ダウンキャスト +fn metadata_internal(&self) -> Result { + self.io.as_ref() + .ok_or_else(|| "FileHandleBox not open".to_string())? + .as_any() + .downcast_ref::() + .ok_or_else(|| "FileIo is not Ring0FsFileIo".to_string())? + .metadata() // Ring0FsFileIo 専用メソッド +} +``` + +**問題点**: +- Ring0FsFileIo 依存(downcast 必要) +- FileIo trait を経由していない +- NoFsFileIo では動作しない + +### After Phase 114 + +```rust +// FileIo::stat() を使用(trait 経由) +fn metadata_internal(&self) -> Result { + let io = self.io.as_ref() + .ok_or_else(|| "FileHandleBox is not open".to_string())?; + + io.stat() // FileIo trait 経由 + .map_err(|e| format!("Metadata failed: {}", e)) +} + +// 他のメソッドも metadata_internal() の上に統一 +fn size(&self) -> Result { + self.metadata_internal().map(|meta| meta.size) +} + +fn is_file(&self) -> Result { + self.metadata_internal().map(|meta| meta.is_file) +} + +fn is_dir(&self) -> Result { + self.metadata_internal().map(|meta| meta.is_dir) +} +``` + +**改善点**: +- FileIo trait 経由で統一 +- downcast 不要 +- NoFsFileIo でも正しくエラーを返す + +## Profile 別動作 + +### Default プロファイル(Ring0FsFileIo) + +| メソッド | 動作 | +|---------|------| +| `exists()` | ファイルが存在すれば `true`、存在しないか path 未設定なら `false` | +| `stat()` | `FileStat { is_file, is_dir, size }` を返す。path 未設定なら `Err` | +| `canonicalize()` | 絶対パスを返す。path 未設定なら `Err` | + +### NoFs プロファイル(NoFsFileIo) + +| メソッド | 動作 | +|---------|------| +| `exists()` | 常に `false` | +| `stat()` | `Err(FileError::Unsupported("..."))` | +| `canonicalize()` | `Err(FileError::Unsupported("..."))` | + +## テスト結果 + +### Ring0FsFileIo テスト(Default プロファイル) + +``` +✅ test_ring0_fs_fileio_stat_default_profile +✅ test_ring0_fs_fileio_exists_default_profile +✅ test_ring0_fs_fileio_canonicalize_default_profile +✅ test_ring0_fs_fileio_stat_without_open +✅ test_ring0_fs_fileio_canonicalize_without_open +``` + +### NoFsFileIo テスト + +``` +✅ test_nofs_fileio_exists (常に false) +✅ test_nofs_fileio_stat_error (Unsupported エラー) +✅ test_nofs_fileio_canonicalize_error (Unsupported エラー) +``` + +### FileHandleBox テスト + +``` +✅ test_filehandlebox_metadata_internal_default (stat() 経由で FileStat 取得) +✅ test_filehandlebox_metadata_internal_not_open (エラー確認) +✅ test_filehandlebox_ny_size_uses_stat (ny_size() が stat() 経由) +✅ test_filehandlebox_exists_uses_fileio (exists() が FileIo::exists() 経由) +✅ test_filehandlebox_is_file_is_dir_via_stat (is_file/is_dir が stat() 経由) +``` + +## 実装成果 + +### 統計 + +- **修正ファイル**: 4ファイル + - `src/boxes/file/provider.rs` + - `src/providers/ring1/file/ring0_fs_fileio.rs` + - `src/providers/ring1/file/nofs_fileio.rs` + - `src/boxes/file/handle_box.rs` + - `src/providers/ring1/file/core_ro.rs` +- **追加行**: 約 +150 行 +- **削除行**: 約 -20 行 +- **新規テスト**: 11個 + +### 技術的成果 + +1. **FileIo trait 拡張**: exists/stat/canonicalize 正式追加 +2. **統一設計**: FileHandleBox 内部が metadata_internal() に統一 +3. **Profile 対応**: Default/NoFs 両プロファイルで正しく動作 +4. **後方互換性**: .hako 側 API は変更なし(内部実装のみ統一) + +## 次のステップ + +- **Phase 115**: PathBox 実装(パス操作専用Box) +- **Phase 116**: ディレクトリ操作(mkdir/rmdir/readdir) + +## 関連ドキュメント + +- [core_boxes_design.md](./core_boxes_design.md) - FileHandleBox 設計 +- [ring0-inventory.md](./ring0-inventory.md) - Ring0 機能一覧 +- [Phase 113 実装](./phase113_filehandlebox_api.md) - Nyash API 公開 diff --git a/docs/development/current/main/ring0-inventory.md b/docs/development/current/main/ring0-inventory.md index c569827e..3ed3737d 100644 --- a/docs/development/current/main/ring0-inventory.md +++ b/docs/development/current/main/ring0-inventory.md @@ -612,3 +612,41 @@ Phase 106–108 では FileBox provider_lock / Ring0FsFileIo / write/write_all さらに長期的には、Ring0 全体を「統一サービスレジストリ」として扱うフェーズ(Mem/Io/Time/Log/Fs/Thread の trait 統合)を Phase 11x 以降で検討する予定だよ。Phase 112 で factory pattern による拡張基盤が整備された! + +--- + +### Phase 114: FileIo trait 拡張 & メタデータ統一 + +**Scope**: +- FileIo trait に exists/stat/canonicalize 正式追加 +- Ring0FsFileIo/NoFsFileIo の実装統一 +- FileHandleBox 内部を metadata_internal() に統一 +- FileStat 構造体追加(is_file/is_dir/size) + +**Design Principles**: +- **FsApi = Stateless**: パスを毎回引数で受け取る(OS レイヤー) +- **FileIo = Stateful**: open() で path を保持(ハンドルレイヤー) +- **FileHandleBox**: FileIo::stat() 経由で統一(downcast 不要) + +**Implementation**: +- Ring0FsFileIo: path を RwLock で管理、ring0.fs.metadata() を FileStat に変換 +- NoFsFileIo: exists() → false, stat()/canonicalize() → Unsupported エラー +- FileHandleBox: metadata_internal() が FileIo::stat() を呼ぶ統一設計 + +**Tests**: +- Ring0FsFileIo: stat/exists/canonicalize 動作確認(Default プロファイル) +- NoFsFileIo: exists=false, stat/canonicalize エラー確認 +- FileHandleBox: metadata_internal() が stat() 経由で動作確認 + +**Status**: 完了 - FsApi ↔ FileIo ↔ FileHandleBox の I/O 情報経路を完全統一 + +**Files Modified**: +- src/boxes/file/provider.rs (FileStat 構造体, FileIo trait 拡張) +- src/providers/ring1/file/ring0_fs_fileio.rs (exists/stat/canonicalize 実装) +- src/providers/ring1/file/nofs_fileio.rs (stub 実装) +- src/providers/ring1/file/core_ro.rs (exists/stat/canonicalize 実装) +- src/boxes/file/handle_box.rs (metadata_internal() 統一) +- docs/development/current/main/phase114_fileio_trait_extension.md (新規) +- docs/development/current/main/core_boxes_design.md (Phase 114 セクション追加) +- docs/development/current/main/ring0-inventory.md (this file) + diff --git a/src/boxes/file/handle_box.rs b/src/boxes/file/handle_box.rs index 24d8c87e..c4042b8c 100644 --- a/src/boxes/file/handle_box.rs +++ b/src/boxes/file/handle_box.rs @@ -224,22 +224,16 @@ impl FileHandleBox { self.io.is_some() } - // ===== Phase 111: Metadata methods (internal Rust API) ===== + // ===== Phase 111/114: Metadata methods (internal Rust API) ===== - /// Internal helper: Get FsMetadata from Ring0FsFileIo - fn metadata_internal(&self) -> Result { - if self.path.is_empty() { - return Err("FileHandleBox path not set".to_string()); - } + /// Phase 114: Internal helper using FileIo::stat() + /// + /// Unified metadata access through FileIo trait. + fn metadata_internal(&self) -> Result { + let io = self.io.as_ref() + .ok_or_else(|| "FileHandleBox is not open".to_string())?; - // Get Ring0FsFileIo and call metadata() - self.io - .as_ref() - .ok_or_else(|| "FileHandleBox not open".to_string())? - .as_any() - .downcast_ref::() - .ok_or_else(|| "FileIo is not Ring0FsFileIo".to_string())? - .metadata() + io.stat() .map_err(|e| format!("Metadata failed: {}", e)) } @@ -247,37 +241,33 @@ impl FileHandleBox { /// /// # Errors /// - /// - Path not set: "FileHandleBox path not set" + /// - Not open: "FileHandleBox is not open" /// - Metadata failed: "Metadata failed: ERROR" pub fn size(&self) -> Result { - self.metadata_internal().map(|meta| meta.len) + self.metadata_internal().map(|meta| meta.size) } /// Check if file exists /// /// # Errors /// - /// - Path not set: "FileHandleBox path not set" + /// - Not open: "FileHandleBox is not open" /// /// # Note /// - /// Returns false if file not found (no error) + /// Uses FileIo::exists() for direct check. pub fn exists(&self) -> Result { - if self.path.is_empty() { - return Err("FileHandleBox path not set".to_string()); - } + let io = self.io.as_ref() + .ok_or_else(|| "FileHandleBox is not open".to_string())?; - match self.metadata_internal() { - Ok(_) => Ok(true), - Err(_) => Ok(false), // not found → false - } + Ok(io.exists()) } /// Check if path is a file /// /// # Errors /// - /// - Path not set: "FileHandleBox path not set" + /// - Not open: "FileHandleBox is not open" /// - Metadata failed: "Metadata failed: ERROR" pub fn is_file(&self) -> Result { self.metadata_internal().map(|meta| meta.is_file) @@ -287,7 +277,7 @@ impl FileHandleBox { /// /// # Errors /// - /// - Path not set: "FileHandleBox path not set" + /// - Not open: "FileHandleBox is not open" /// - Metadata failed: "Metadata failed: ERROR" pub fn is_dir(&self) -> Result { self.metadata_internal().map(|meta| meta.is_dir) @@ -948,4 +938,105 @@ mod tests { handle.ny_open(path, "r"); handle.ny_write("data"); // This should panic (read-only mode) } + + // ===== Phase 114: metadata_internal() unification tests ===== + + #[test] + fn test_filehandlebox_metadata_internal_default() { + init_test_provider(); + + let path = "/tmp/phase114_metadata_internal.txt"; + let _ = fs::remove_file(path); + + // Create test file with known content + let mut handle = FileHandleBox::new(); + handle.open(path, "w").unwrap(); + handle.write_all("12345").unwrap(); // 5 bytes + handle.close().unwrap(); + + // Test metadata_internal() via stat() + handle.open(path, "r").unwrap(); + let stat = handle.metadata_internal().expect("metadata_internal should succeed"); + assert!(stat.is_file); + assert!(!stat.is_dir); + assert_eq!(stat.size, 5); + + handle.close().unwrap(); + let _ = fs::remove_file(path); + } + + #[test] + fn test_filehandlebox_metadata_internal_not_open() { + init_test_provider(); + + let handle = FileHandleBox::new(); + let result = handle.metadata_internal(); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("not open")); + } + + #[test] + fn test_filehandlebox_ny_size_uses_stat() { + init_test_provider(); + + let path = "/tmp/phase114_ny_size_stat.txt"; + let _ = fs::remove_file(path); + + let mut handle = FileHandleBox::new(); + handle.ny_open(path, "w"); + handle.ny_write("test123"); // 7 bytes + handle.ny_close(); + + // Verify ny_size() uses stat() internally + handle.ny_open(path, "r"); + let size = handle.ny_size(); + assert_eq!(size.value, 7); + handle.ny_close(); + + let _ = fs::remove_file(path); + } + + #[test] + fn test_filehandlebox_exists_uses_fileio() { + init_test_provider(); + + let path = "/tmp/phase114_exists_fileio.txt"; + let _ = fs::remove_file(path); + + let mut handle = FileHandleBox::new(); + handle.ny_open(path, "w"); + handle.ny_close(); + + // exists() should use FileIo::exists() + handle.ny_open(path, "r"); + let exists = handle.ny_exists(); + assert!(exists.value); + handle.ny_close(); + + let _ = fs::remove_file(path); + } + + #[test] + fn test_filehandlebox_is_file_is_dir_via_stat() { + init_test_provider(); + + let path = "/tmp/phase114_is_file_dir.txt"; + let _ = fs::remove_file(path); + + let mut handle = FileHandleBox::new(); + handle.ny_open(path, "w"); + handle.ny_close(); + + // Test is_file/is_dir use metadata_internal() → stat() + handle.ny_open(path, "r"); + + let is_file = handle.is_file().expect("is_file should succeed"); + assert!(is_file); + + let is_dir = handle.is_dir().expect("is_dir should succeed"); + assert!(!is_dir); + + handle.ny_close(); + let _ = fs::remove_file(path); + } } diff --git a/src/boxes/file/provider.rs b/src/boxes/file/provider.rs index 1c1c2847..604e4506 100644 --- a/src/boxes/file/provider.rs +++ b/src/boxes/file/provider.rs @@ -73,6 +73,14 @@ impl FileCaps { } } +/// Phase 114: File statistics +#[derive(Debug, Clone, Copy)] +pub struct FileStat { + pub is_file: bool, + pub is_dir: bool, + pub size: u64, +} + /// Unified error type (thin placeholder for now) #[derive(thiserror::Error, Debug)] pub enum FileError { @@ -94,7 +102,16 @@ pub trait FileIo: Send + Sync { /// Phase 111: Downcast support for metadata access fn as_any(&self) -> &dyn std::any::Any; - // Future: exists/stat … + + // Phase 114: Metadata operations + /// Check if the file exists + fn exists(&self) -> bool; + + /// Get file statistics (metadata) + fn stat(&self) -> FileResult; + + /// Get canonicalized absolute path + fn canonicalize(&self) -> FileResult; } /// Normalize newlines to LF (optional helper) diff --git a/src/providers/ring1/file/core_ro.rs b/src/providers/ring1/file/core_ro.rs index e46d05a5..832fb37b 100644 --- a/src/providers/ring1/file/core_ro.rs +++ b/src/providers/ring1/file/core_ro.rs @@ -4,16 +4,20 @@ use crate::boxes::file::provider::{normalize_newlines, FileCaps, FileError, FileIo, FileResult}; use std::fs::File; use std::io::Read; +use std::path::Path; use std::sync::RwLock; pub struct CoreRoFileIo { handle: RwLock>, + /// Store path for metadata operations (Phase 114) + path: RwLock>, } impl CoreRoFileIo { pub fn new() -> Self { Self { handle: RwLock::new(None), + path: RwLock::new(None), } } } @@ -27,6 +31,7 @@ impl FileIo for CoreRoFileIo { let file = File::open(path) .map_err(|e| FileError::Io(format!("Failed to open {}: {}", path, e)))?; *self.handle.write().unwrap() = Some(file); + *self.path.write().unwrap() = Some(path.to_string()); // Phase 114: Store path Ok(()) } @@ -49,12 +54,59 @@ impl FileIo for CoreRoFileIo { fn close(&self) -> FileResult<()> { *self.handle.write().unwrap() = None; + *self.path.write().unwrap() = None; // Phase 114: Clear path Ok(()) } fn as_any(&self) -> &dyn std::any::Any { self } + + // Phase 114: Metadata operations + + fn exists(&self) -> bool { + let path_lock = self.path.read().unwrap(); + match path_lock.as_ref() { + Some(path_str) => { + Path::new(path_str).exists() + } + None => false, + } + } + + fn stat(&self) -> FileResult { + let path_lock = self.path.read().unwrap(); + match path_lock.as_ref() { + Some(path_str) => { + let path_obj = Path::new(path_str); + let meta = std::fs::metadata(path_obj) + .map_err(|e| FileError::Io(format!("Metadata failed: {}", e)))?; + Ok(crate::boxes::file::provider::FileStat { + is_file: meta.is_file(), + is_dir: meta.is_dir(), + size: meta.len(), + }) + } + None => { + Err(FileError::Io("No file path set. Call open() first.".to_string())) + } + } + } + + fn canonicalize(&self) -> FileResult { + let path_lock = self.path.read().unwrap(); + match path_lock.as_ref() { + Some(path_str) => { + let path_obj = Path::new(path_str); + let canonical = std::fs::canonicalize(path_obj) + .map_err(|e| FileError::Io(format!("Canonicalize failed: {}", e)))?; + Ok(canonical.display().to_string()) + } + None => { + Err(FileError::Io("No file path set. Call open() first.".to_string())) + } + } + } } #[cfg(test)] diff --git a/src/providers/ring1/file/nofs_fileio.rs b/src/providers/ring1/file/nofs_fileio.rs index a3c1ab06..43b96bf3 100644 --- a/src/providers/ring1/file/nofs_fileio.rs +++ b/src/providers/ring1/file/nofs_fileio.rs @@ -54,6 +54,25 @@ impl FileIo for NoFsFileIo { fn as_any(&self) -> &dyn std::any::Any { self } + + // Phase 114: Metadata operations (stub) + + fn exists(&self) -> bool { + // NoFs profile: all files are considered non-existent + false + } + + fn stat(&self) -> FileResult { + Err(FileError::Unsupported( + "FileSystem operations disabled in no-fs profile".to_string() + )) + } + + fn canonicalize(&self) -> FileResult { + Err(FileError::Unsupported( + "FileSystem operations disabled in no-fs profile".to_string() + )) + } } #[cfg(test)] @@ -99,4 +118,29 @@ mod tests { assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("unsupported")); } + + // ===== Phase 114: Metadata operation tests ===== + + #[test] + fn test_nofs_fileio_exists() { + let fileio = NoFsFileIo; + // NoFsFileIo.exists() should always return false + assert!(!fileio.exists(), "NoFsFileIo.exists() should always return false"); + } + + #[test] + fn test_nofs_fileio_stat_error() { + let fileio = NoFsFileIo; + let result = fileio.stat(); + assert!(result.is_err(), "stat() should return error"); + assert!(result.unwrap_err().to_string().contains("unsupported"), "should contain 'unsupported'"); + } + + #[test] + fn test_nofs_fileio_canonicalize_error() { + let fileio = NoFsFileIo; + let result = fileio.canonicalize(); + assert!(result.is_err(), "canonicalize() should return error"); + assert!(result.unwrap_err().to_string().contains("unsupported"), "should contain 'unsupported'"); + } } diff --git a/src/providers/ring1/file/ring0_fs_fileio.rs b/src/providers/ring1/file/ring0_fs_fileio.rs index 37bdf7da..f28aafd7 100644 --- a/src/providers/ring1/file/ring0_fs_fileio.rs +++ b/src/providers/ring1/file/ring0_fs_fileio.rs @@ -156,6 +156,53 @@ impl FileIo for Ring0FsFileIo { fn as_any(&self) -> &dyn std::any::Any { self } + + // Phase 114: Metadata operations + + fn exists(&self) -> bool { + let path_lock = self.path.read().unwrap(); + match path_lock.as_ref() { + Some(path_str) => { + let path_obj = Path::new(path_str); + self.ring0.fs.exists(path_obj) + } + None => false, + } + } + + fn stat(&self) -> FileResult { + let path_lock = self.path.read().unwrap(); + match path_lock.as_ref() { + Some(path_str) => { + let path_obj = Path::new(path_str); + let meta = self.ring0.fs.metadata(path_obj) + .map_err(|e| FileError::Io(format!("Metadata failed: {}", e)))?; + Ok(crate::boxes::file::provider::FileStat { + is_file: meta.is_file, + is_dir: meta.is_dir, + size: meta.len, + }) + } + None => { + Err(FileError::Io("No file path set. Call open() first.".to_string())) + } + } + } + + fn canonicalize(&self) -> FileResult { + let path_lock = self.path.read().unwrap(); + match path_lock.as_ref() { + Some(path_str) => { + let path_obj = Path::new(path_str); + let canonical = self.ring0.fs.canonicalize(path_obj) + .map_err(|e| FileError::Io(format!("Canonicalize failed: {}", e)))?; + Ok(canonical.display().to_string()) + } + None => { + Err(FileError::Io("No file path set. Call open() first.".to_string())) + } + } + } } #[cfg(test)] @@ -266,11 +313,13 @@ mod tests { assert!(caps.write); // Write content (truncate mode) + fileio.set_mode("w".to_string()); // Phase 114: set mode before open assert!(fileio.open(test_path).is_ok()); assert!(fileio.write(test_content).is_ok()); assert!(fileio.close().is_ok()); // Read back and verify + fileio.set_mode("r".to_string()); // Phase 114: set mode before open assert!(fileio.open(test_path).is_ok()); let content = fileio.read().unwrap(); assert_eq!(content, test_content); @@ -289,11 +338,13 @@ mod tests { let fileio = Ring0FsFileIo::new(ring0); // Overwrite with new content (truncate mode) + fileio.set_mode("w".to_string()); // Phase 114: set mode before open assert!(fileio.open(test_path).is_ok()); assert!(fileio.write("New content").is_ok()); assert!(fileio.close().is_ok()); // Verify truncate behavior + fileio.set_mode("r".to_string()); // Phase 114: set mode before open assert!(fileio.open(test_path).is_ok()); let content = fileio.read().unwrap(); assert_eq!(content, "New content"); @@ -312,4 +363,92 @@ mod tests { assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("No file is currently open")); } + + // ===== Phase 114: Metadata operation tests ===== + + #[test] + fn test_ring0_fs_fileio_stat_default_profile() { + let test_path = "/tmp/phase114_stat_test.txt"; + let test_content = "Hello, Phase 114!"; + + setup_test_file(test_path, test_content); + + let ring0 = Arc::new(default_ring0()); + let fileio = Ring0FsFileIo::new(ring0); + + // Open file + assert!(fileio.open(test_path).is_ok()); + + // Test stat() + let stat = fileio.stat().expect("stat should succeed"); + assert!(stat.is_file, "should be a file"); + assert!(!stat.is_dir, "should not be a directory"); + assert_eq!(stat.size, test_content.len() as u64, "size should match"); + + fileio.close().unwrap(); + cleanup_test_file(test_path); + } + + #[test] + fn test_ring0_fs_fileio_exists_default_profile() { + let test_path = "/tmp/phase114_exists_test.txt"; + setup_test_file(test_path, "test"); + + let ring0 = Arc::new(default_ring0()); + let fileio = Ring0FsFileIo::new(ring0); + + // Open file + assert!(fileio.open(test_path).is_ok()); + + // Test exists() - should return true for existing file + assert!(fileio.exists(), "exists() should return true"); + + fileio.close().unwrap(); + cleanup_test_file(test_path); + + // After file is deleted, exists() with no path set should return false + assert!(!fileio.exists(), "exists() should return false when no path set"); + } + + #[test] + fn test_ring0_fs_fileio_canonicalize_default_profile() { + let test_path = "/tmp/phase114_canonicalize.txt"; + setup_test_file(test_path, "test"); + + let ring0 = Arc::new(default_ring0()); + let fileio = Ring0FsFileIo::new(ring0); + + // Open file + assert!(fileio.open(test_path).is_ok()); + + // Test canonicalize() + let canonical = fileio.canonicalize().expect("canonicalize should succeed"); + assert!(canonical.contains("phase114_canonicalize.txt"), "should contain filename"); + assert!(canonical.starts_with("/"), "should be absolute path"); + + fileio.close().unwrap(); + cleanup_test_file(test_path); + } + + #[test] + fn test_ring0_fs_fileio_stat_without_open() { + let ring0 = Arc::new(default_ring0()); + let fileio = Ring0FsFileIo::new(ring0); + + // stat() without open() should fail + let result = fileio.stat(); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("No file path set")); + } + + #[test] + fn test_ring0_fs_fileio_canonicalize_without_open() { + let ring0 = Arc::new(default_ring0()); + let fileio = Ring0FsFileIo::new(ring0); + + // canonicalize() without open() should fail + let result = fileio.canonicalize(); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("No file path set")); + } }