diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index 017d5571..0932ab0f 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -278,6 +278,24 @@ - **スコープ**: 設計+ドキュメントのみ(Rust実装なし、Phase 106+で実装) - **成果**: ConsoleBox基盤の構造化ロギングフレームワーク確立 - **次のステップ**: Phase 106(FileBox provider_lock & Fail-Fast)、Phase 107(FsApi 統合 or Logger 出力先拡張) + - **Phase 106: FileBox provider_lock & Fail-Fast**(2025-12-03) + - ✅ FileBox provider_lock 機構実装(OnceLock ベース) + - ✅ CoreBoxId.is_core_required() による Fail-Fast チェック + - ✅ 設計統一(案B): provider_lock SSOT 化 + - ✅ テスト完了: plugin_host tests 全PASS + - **Phase 107: FsApi / FileIo 統合**(2025-12-03) + - ✅ Ring0FsFileIo 実装(Ring0.FsApi 経由の FileIo) + - ✅ FileBox → Ring0FsFileIo → Ring0.FsApi → std::fs パイプライン確立 + - ✅ 自動登録機構: with_core_from_registry() で Ring0FsFileIo を自動登録 + - ✅ テスト完了: ring0_fs_fileio tests 全PASS + - **Phase 108: FileBox write/write_all 実装**(2025-12-03)✅ **完了** + - ✅ FileIo trait に write() メソッド追加 + - ✅ Ring0FsFileIo に write() 実装(truncate mode) + - ✅ FileBox.write() / write_all() が Ring0.FsApi 経由で動作 + - ✅ FileCaps.write = true(標準プロファイルで書き込み対応) + - ✅ テスト完了: Round-trip / truncate / read-only provider 拒否 全PASS + - **設計決定**: write mode は truncate(毎回上書き)、append は Phase 109+ + - **次のステップ**: Phase 109(minimal/no-fs プロファイル)、Phase 110(FileHandleBox) 12. **Phase 86: BoxFactory Priority 正常化** ✅ **完了**(2025-12-02) - **目的**: BoxFactory のデフォルトポリシーを `BuiltinFirst` から `StrictPluginFirst` に変更し、プラグイン版 Box が正常に使用できるよう正常化。 diff --git a/docs/development/current/main/core_boxes_design.md b/docs/development/current/main/core_boxes_design.md index e5d21863..729593dd 100644 --- a/docs/development/current/main/core_boxes_design.md +++ b/docs/development/current/main/core_boxes_design.md @@ -256,7 +256,7 @@ FileBox は selfhost/通常ランタイムでは事実上必須(ログ・ツ - **PluginHost**: startup 時に CoreBoxId.is_core_required() で provider をチェック - 未登録なら CoreInitError::MissingService で fail-fast -### Ring0.FsApi との関係(Phase 107 完了)✅ +### Ring0.FsApi との関係(Phase 107-108 完了)✅ **Phase 107 統合完了(2025-12-03)**: @@ -266,16 +266,22 @@ FileBox の実体 I/O は、以下の層構造で Ring0.FsApi を通す設計が [FileBox (Ring1)] ↓ provider 経由 [Ring0FsFileIo] (FileIo 実装) - ↓ read_to_string/read 呼び出し + ↓ read_to_string/write_all 呼び出し [Ring0.FsApi] (OS I/O 抽象) ↓ [std::fs] ``` +**Phase 108 実装完了(2025-12-03)**: +- FileBox は Ring0FsFileIo 経由で **read/write 両対応** +- write は **truncate mode**(毎回上書き) +- append モードは Phase 109+ で予定 + **設計原則**: - **FileIo = stateful**(現在開いているファイルハンドルに対する操作) - open() でファイルを開く - read() で内容を読み込む + - write() で内容を書き込む(Phase 108 追加) - close() でファイルを閉じる - **FsApi = stateless**(Path → データの直接変換) - read_to_string(path) / write_all(path, data) diff --git a/docs/development/current/main/phase107_fsapi_fileio_bridge.md b/docs/development/current/main/phase107_fsapi_fileio_bridge.md index f441f327..6c15e7a1 100644 --- a/docs/development/current/main/phase107_fsapi_fileio_bridge.md +++ b/docs/development/current/main/phase107_fsapi_fileio_bridge.md @@ -316,7 +316,28 @@ --- -## 8. 設計原則(Phase 107 で確立) +## 8. Phase 108 以降の進展 + +### Phase 108 実装完了(2025-12-03) + +**write 実装完了**: +- Ring0FsFileIo に write() メソッド実装 +- FileBox.write() / write_all() が Ring0.FsApi.write_all() 経由で動作 +- FileCaps.write = true(標準プロファイルで書き込み対応) + +**設計決定**: +- **Write mode**: truncate(既存ファイル毎回上書き) +- **テキスト前提**: UTF-8 変換(from_utf8_lossy) +- **append mode**: Phase 109+ で予定 + +**テスト**: +- Round-trip テスト(write → read)✅ +- Truncate mode 検証✅ +- Read-only provider 拒否テスト✅ + +--- + +## 9. 設計原則(Phase 107 で確立) ### 層の棲み分けが完全化 diff --git a/src/boxes/file/mod.rs b/src/boxes/file/mod.rs index 02e91c4b..d0114951 100644 --- a/src/boxes/file/mod.rs +++ b/src/boxes/file/mod.rs @@ -87,19 +87,19 @@ impl FileBox { } } - pub fn write_all(&self, _buf: &[u8]) -> Result<(), String> { - // Fail-Fast by capability: consult provider caps - let caps = self - .provider - .as_ref() - .map(|p| p.caps()) - .or_else(|| provider_lock::get_filebox_caps()) - .unwrap_or_else(|| provider::FileCaps::read_only()); - if !caps.write { - return Err("Write unsupported by current FileBox provider (read-only)".to_string()); + pub fn write_all(&self, buf: &[u8]) -> Result<(), String> { + if let Some(ref provider) = self.provider { + let caps = provider.caps(); + if !caps.write { + return Err("Write not supported by FileBox provider".to_string()); + } + // Phase 108: UTF-8 conversion (text-oriented design) + let text = String::from_utf8_lossy(buf).to_string(); + provider.write(&text) + .map_err(|e| format!("Write failed: {:?}", e)) + } else { + Err("No provider available".to_string()) } - // Write-capable provider not wired yet - Err("Write supported by provider but not implemented in this build".to_string()) } /// ファイルの内容を読み取る @@ -111,21 +111,28 @@ impl FileBox { } /// ファイルに内容を書き込む - pub fn write(&self, _content: Box) -> Box { - let caps = self - .provider - .as_ref() - .map(|p| p.caps()) - .or_else(|| provider_lock::get_filebox_caps()) - .unwrap_or_else(|| provider::FileCaps::read_only()); - if !caps.write { - return Box::new(StringBox::new( - "Error: write unsupported by provider (read-only)", - )); + pub fn write(&self, content: Box) -> Box { + if let Some(ref provider) = self.provider { + let caps = provider.caps(); + if !caps.write { + return Box::new(StringBox::new( + "Error: write not supported by provider (read-only)".to_string() + )); + } + // Phase 108: Convert content to text + let text = if let Some(str_box) = content.as_any().downcast_ref::() { + str_box.to_string_box().value + } else { + content.to_string_box().value + }; + + match provider.write(&text) { + Ok(()) => Box::new(StringBox::new("OK".to_string())), + Err(e) => Box::new(StringBox::new(format!("Error: {:?}", e))), + } + } else { + Box::new(StringBox::new("Error: no provider available".to_string())) } - Box::new(StringBox::new( - "Error: write supported but not implemented in this build", - )) } /// ファイルが存在するかチェック diff --git a/src/boxes/file/provider.rs b/src/boxes/file/provider.rs index d579df3d..2402d583 100644 --- a/src/boxes/file/provider.rs +++ b/src/boxes/file/provider.rs @@ -52,8 +52,9 @@ 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<()>; // Phase 108: write support fn close(&self) -> FileResult<()>; - // Future: write/exists/stat … + // Future: exists/stat … } /// 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 1194a21d..2374003c 100644 --- a/src/providers/ring1/file/core_ro.rs +++ b/src/providers/ring1/file/core_ro.rs @@ -42,8 +42,31 @@ impl FileIo for CoreRoFileIo { } } + fn write(&self, _text: &str) -> FileResult<()> { + // CoreRoFileIo is read-only, write is not supported + Err(FileError::Unsupported) + } + fn close(&self) -> FileResult<()> { *self.handle.write().unwrap() = None; Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_core_ro_write_unsupported() { + let fileio = CoreRoFileIo::new(); + + // Write should fail with Unsupported + let result = fileio.write("test"); + assert!(result.is_err()); + match result.unwrap_err() { + FileError::Unsupported => { /* expected */ } + _ => panic!("Expected Unsupported error"), + } + } +} diff --git a/src/providers/ring1/file/ring0_fs_fileio.rs b/src/providers/ring1/file/ring0_fs_fileio.rs index 71c11b1f..1a57c462 100644 --- a/src/providers/ring1/file/ring0_fs_fileio.rs +++ b/src/providers/ring1/file/ring0_fs_fileio.rs @@ -39,8 +39,8 @@ impl Ring0FsFileIo { impl FileIo for Ring0FsFileIo { fn caps(&self) -> FileCaps { - // Phase 107: Read-only - FileCaps::read_only() + // Phase 108: Read/write support + FileCaps { read: true, write: true } } fn open(&self, path: &str) -> FileResult<()> { @@ -80,6 +80,22 @@ impl FileIo for Ring0FsFileIo { } } + fn write(&self, text: &str) -> FileResult<()> { + let current_path = self.path.read().unwrap(); + + match current_path.as_ref() { + Some(path) => { + // Delegate to Ring0.FsApi (truncate mode: overwrite existing file) + let path_obj = Path::new(path); + self.ring0.fs.write_all(path_obj, text.as_bytes()) + .map_err(|e| FileError::Io(format!("Write failed: {}", e))) + } + None => { + Err(FileError::Io("No file is currently open. Call open() first.".to_string())) + } + } + } + fn close(&self) -> FileResult<()> { let mut current_path = self.path.write().unwrap(); *current_path = None; @@ -113,10 +129,10 @@ mod tests { let ring0 = Arc::new(default_ring0()); let fileio = Ring0FsFileIo::new(ring0); - // Test capabilities + // Test capabilities (Phase 108: write support added) let caps = fileio.caps(); assert!(caps.read); - assert!(!caps.write); + assert!(caps.write); // Test open assert!(fileio.open(test_path).is_ok()); @@ -175,4 +191,70 @@ mod tests { assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("not found")); } + + // ===== Phase 108: Write tests ===== + + #[test] + fn test_filebox_write_read_roundtrip() { + let test_path = "/tmp/phase108_roundtrip.txt"; + let test_content = "Hello, Phase 108!"; + + // Setup: Create file first (open() requires file to exist) + setup_test_file(test_path, "initial"); + + let ring0 = Arc::new(default_ring0()); + let fileio = Ring0FsFileIo::new(ring0); + + // Test capabilities + let caps = fileio.caps(); + assert!(caps.read); + assert!(caps.write); + + // Write content (truncate mode) + assert!(fileio.open(test_path).is_ok()); + assert!(fileio.write(test_content).is_ok()); + assert!(fileio.close().is_ok()); + + // Read back and verify + assert!(fileio.open(test_path).is_ok()); + let content = fileio.read().unwrap(); + assert_eq!(content, test_content); + assert!(fileio.close().is_ok()); + + // Cleanup + cleanup_test_file(test_path); + } + + #[test] + fn test_filebox_write_truncate_mode() { + let test_path = "/tmp/phase108_truncate.txt"; + setup_test_file(test_path, "Original content"); + + let ring0 = Arc::new(default_ring0()); + let fileio = Ring0FsFileIo::new(ring0); + + // Overwrite with new content (truncate mode) + assert!(fileio.open(test_path).is_ok()); + assert!(fileio.write("New content").is_ok()); + assert!(fileio.close().is_ok()); + + // Verify truncate behavior + assert!(fileio.open(test_path).is_ok()); + let content = fileio.read().unwrap(); + assert_eq!(content, "New content"); + assert!(fileio.close().is_ok()); + + cleanup_test_file(test_path); + } + + #[test] + fn test_filebox_write_without_open() { + let ring0 = Arc::new(default_ring0()); + let fileio = Ring0FsFileIo::new(ring0); + + // Write without open should fail + let result = fileio.write("test"); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("No file is currently open")); + } } diff --git a/src/runtime/plugin_host.rs b/src/runtime/plugin_host.rs index 590e0847..725f1493 100644 --- a/src/runtime/plugin_host.rs +++ b/src/runtime/plugin_host.rs @@ -423,37 +423,31 @@ mod tests { } #[test] - fn test_with_core_from_registry_filebox_required() { - // Phase 106: FileBox provider なし → エラー - // Note: この test は provider_lock::get_filebox_provider() が None を返す場合のみ有効。 - // OnceLock の性質上、同一プロセス内で他のテストが先に provider を set すると - // このテストは期待通りに動作しない(provider が既に存在するため)。 - // そのため、provider が既に set されている場合は test を skip する。 + fn test_with_core_from_registry_filebox_auto_registered() { + // Phase 107/108: with_core_from_registry() は Ring0FsFileIo を自動登録するため、 + // FileBox は常に利用可能になる use crate::runtime::ring0::default_ring0; use crate::box_factory::builtin::BuiltinBoxFactory; - use crate::runtime::provider_lock; - - // provider が既に set されている場合は test skip - if provider_lock::get_filebox_provider().is_some() { - eprintln!("Skipping test_with_core_from_registry_filebox_required: provider already set by another test"); - return; - } let ring0 = Arc::new(default_ring0()); let mut registry = UnifiedBoxRegistry::new(); registry.register(Arc::new(BuiltinBoxFactory::new())); - // provider_lock を初期化せず(呼び出さず) - // provider が無い状態で with_core_from_registry() を呼ぶ - + // Phase 107: with_core_from_registry() は Ring0FsFileIo を自動登録 let result = PluginHost::with_core_from_registry(ring0, ®istry); - assert!(result.is_err()); - if let Err(CoreInitError::MissingService { box_id, .. }) = result { - assert_eq!(box_id, CoreBoxId::File); + // Phase 107/108: FileBox provider は自動登録されるため、成功するはず + assert!(result.is_ok(), "Expected success with auto-registered FileBox provider"); + + // Phase 108: 登録された provider は read/write 両対応 + use crate::runtime::provider_lock; + if let Some(provider) = provider_lock::get_filebox_provider() { + let caps = provider.caps(); + assert!(caps.read, "FileBox provider should support read"); + assert!(caps.write, "FileBox provider should support write (Phase 108)"); } else { - panic!("Expected MissingService error for FileBox"); + panic!("FileBox provider should be registered after with_core_from_registry"); } } }