feat(filebox): Phase 108 FileBox write/write_all implementation
Implement write functionality for FileBox following Phase 108 specification,
completing the read/write pipeline via Ring0.FsApi.
## Implementation Summary
### Task 2: FsApi / Ring0FsFileIo write implementation
- Added write() method to FileIo trait
- Implemented write() in Ring0FsFileIo (truncate mode via Ring0.FsApi.write_all)
- Updated FileCaps to { read: true, write: true } for standard profile
- Added write() stub to CoreRoFileIo (returns Unsupported)
### Task 3: FileBox write/write_all implementation
- Updated FileBox.write_all() to delegate to provider.write()
- Updated FileBox.write() to convert content to text and call provider.write()
- UTF-8 conversion via String::from_utf8_lossy (text-oriented design)
- Returns "OK" on success, "Error: ..." on failure
### Task 4: Test coverage
- Round-trip test (write → read): ✅ PASS
- Truncate mode verification: ✅ PASS
- Write without open error: ✅ PASS
- Read-only provider rejection: ✅ PASS
- Auto-registration test updated: ✅ PASS
### Task 5: Documentation updates
- phase107_fsapi_fileio_bridge.md: Added Phase 108 section
- core_boxes_design.md: Updated Ring0.FsApi relationship section
- CURRENT_TASK.md: Added Phase 108 completion entry
## Design Decisions (from phase108_filebox_write_semantics.md)
- **Write mode**: truncate (overwrite existing file each time)
- **Text-oriented**: UTF-8 conversion via from_utf8_lossy
- **Append mode**: Planned for Phase 109+
- **Error handling**: FileError::Io for failures, Fail-Fast on caps.write=false
## Test Results
```
cargo test --release --lib filebox
test result: ok. 5 passed; 0 failed; 1 ignored
```
All FileBox tests pass, including Phase 107 compatibility tests.
## Pipeline Complete
```
FileBox.write(content)
↓
FileBox.write_all(buf)
↓
provider.write(text) ← Ring0FsFileIo implementation
↓
Ring0.FsApi.write_all()
↓
std::fs::write()
```
## Next Steps (Backlog)
- Phase 109: minimal/no-fs profile
- Phase 110: FileHandleBox (multiple files simultaneously)
- Phase 111: append mode implementation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -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<dyn NyashBox>) -> Box<dyn NyashBox> {
|
||||
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<dyn NyashBox>) -> Box<dyn NyashBox> {
|
||||
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::<StringBox>() {
|
||||
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",
|
||||
))
|
||||
}
|
||||
|
||||
/// ファイルが存在するかチェック
|
||||
|
||||
@ -52,8 +52,9 @@ pub trait FileIo: Send + Sync {
|
||||
fn caps(&self) -> FileCaps;
|
||||
fn open(&self, path: &str) -> FileResult<()>;
|
||||
fn read(&self) -> FileResult<String>;
|
||||
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)
|
||||
|
||||
@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user