feat(phase110): FileHandleBox実装(ハンドルベース複数回アクセス)

Phase 110 完全実装完了:
- FileHandleBox struct で open → read/write → close の複数回アクセスをサポート
- 各インスタンスが独立した Ring0FsFileIo を保持
- Fail-Fast 原則:二重 open、close後アクセス、NoFsプロファイル全て即座にエラー
- RuntimeProfile 対応:Default で使用可能、NoFs で禁止

実装ファイル:
- src/boxes/file/handle_box.rs (新規, 450行)
- src/boxes/file/mod.rs (修正, export追加)

テスト: 7/7 PASS 
- test_filehandlebox_basic_write_read
- test_filehandlebox_double_open_error
- test_filehandlebox_closed_access_error
- test_filehandlebox_write_wrong_mode
- test_filehandlebox_multiple_writes
- test_filehandlebox_unsupported_mode
- test_filehandlebox_independent_instances

ドキュメント更新:
- core_boxes_design.md (Section 16 追加)
- ring0-inventory.md (Phase 110完了)
- CURRENT_TASK.md (Phase 110完了セクション)

設計原則:
- FileBox: ワンショット I/O(open/close隠蔽)
- FileHandleBox: ハンドルベース I/O(複数回アクセス対応)
- Ring0FsFileIo を内部で再利用

次フェーズ: Phase 111(append mode + metadata)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-03 20:28:33 +09:00
parent bc1fce3dfe
commit acd1b54ef9
5 changed files with 695 additions and 5 deletions

View File

@ -0,0 +1,477 @@
//! Phase 110: FileHandleBox - Handle-based file I/O
//!
//! Provides multiple-access file I/O pattern:
//! - open(path, mode) → read/write (multiple times) → close()
//!
//! Complements FileBox (one-shot I/O) by allowing multiple reads/writes
//! to the same file handle.
use crate::box_trait::{BoolBox, BoxBase, BoxCore, NyashBox, StringBox};
use crate::boxes::file::provider::FileIo;
use crate::runtime::provider_lock;
use std::any::Any;
use std::sync::Arc;
/// Phase 110: FileHandleBox
///
/// Handle-based file I/O for multiple-access patterns.
///
/// # Lifecycle
///
/// 1. new() - Create handle (file not yet opened)
/// 2. open(path, mode) - Open file (stores FileIo instance)
/// 3. read_to_string() / write_all() - Multiple accesses allowed
/// 4. close() - Close file (resets FileIo)
///
/// # Design Principles
///
/// - **Fail-Fast**: Double open() returns Err
/// - **Independent instances**: Each FileHandleBox has its own FileIo
/// - **Profile-aware**: NoFs profile → open() returns Err
/// - **Ring0 reuse**: Uses Ring0FsFileIo internally
pub struct FileHandleBox {
base: BoxBase,
/// Current file path (empty if not open)
path: String,
/// Current file mode ("r" or "w", empty if not open)
mode: String,
/// FileIo instance (None if not open)
io: Option<Arc<dyn FileIo>>,
}
impl std::fmt::Debug for FileHandleBox {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FileHandleBox")
.field("path", &self.path)
.field("mode", &self.mode)
.field("is_open", &self.is_open())
.finish()
}
}
impl Clone for FileHandleBox {
fn clone(&self) -> Self {
// Clone creates a new independent handle (not open)
// Design decision: Cloning an open handle creates a closed handle
// Rationale: Prevents accidental file handle leaks
FileHandleBox {
base: BoxBase::new(),
path: String::new(),
mode: String::new(),
io: None,
}
}
}
impl FileHandleBox {
/// Create new FileHandleBox (file not yet opened)
pub fn new() -> Self {
FileHandleBox {
base: BoxBase::new(),
path: String::new(),
mode: String::new(),
io: None,
}
}
/// Open a file
///
/// # Arguments
///
/// - path: File path to open
/// - mode: "r" (read) or "w" (write)
///
/// # Errors
///
/// - Already open: "FileHandleBox is already open. Call close() first."
/// - Unsupported mode: "Unsupported mode: X. Use 'r' or 'w'"
/// - NoFs profile: "File I/O disabled in no-fs profile. FileHandleBox is not available."
/// - File not found (mode="r"): "File not found: PATH"
///
/// # Design Notes
///
/// - Phase 110: Supports "r" and "w" only
/// - Phase 111: Will add "a" (append) mode
/// - Each FileHandleBox instance gets its own FileIo (independent)
pub fn open(&mut self, path: &str, mode: &str) -> Result<(), String> {
// Fail-Fast: Check for double open
if self.io.is_some() {
return Err("FileHandleBox is already open. Call close() first.".to_string());
}
// Validate mode
if mode != "r" && mode != "w" {
return Err(format!("Unsupported mode: {}. Use 'r' or 'w'", mode));
}
// Get FileIo provider to check capabilities
let provider = provider_lock::get_filebox_provider()
.ok_or("FileBox provider not initialized")?;
// NoFs profile check (Fail-Fast)
let caps = provider.caps();
if !caps.read && !caps.write {
return Err("File I/O disabled in no-fs profile. FileHandleBox is not available."
.to_string());
}
// Mode-specific capability check
if mode == "r" && !caps.read {
return Err("Read not supported by FileBox provider".to_string());
}
if mode == "w" && !caps.write {
return Err("Write not supported by FileBox provider".to_string());
}
// Create NEW independent Ring0FsFileIo instance for this handle
// IMPORTANT: We must create a new instance, not clone the Arc
// because Ring0FsFileIo has internal state (path field)
use crate::runtime::get_global_ring0;
use crate::providers::ring1::file::ring0_fs_fileio::Ring0FsFileIo;
let ring0 = get_global_ring0();
// For write mode, create the file if it doesn't exist
// Ring0FsFileIo expects the file to exist, so we create it first
if mode == "w" {
use std::path::Path;
let path_obj = Path::new(path);
if !ring0.fs.exists(path_obj) {
// Create empty file for write mode
ring0
.fs
.write_all(path_obj, &[])
.map_err(|e| format!("Failed to create file: {}", e))?;
}
}
let io: Arc<dyn FileIo> = Arc::new(Ring0FsFileIo::new(ring0.clone()));
// Now open the file with the new instance
io.open(path)
.map_err(|e| format!("Failed to open file: {}", e))?;
// Store state
self.path = path.to_string();
self.mode = mode.to_string();
self.io = Some(io);
Ok(())
}
/// Read file contents to string
///
/// # Errors
///
/// - Not open: "FileHandleBox is not open"
/// - Read failed: "Read failed: ERROR"
pub fn read_to_string(&self) -> Result<String, String> {
self.io
.as_ref()
.ok_or("FileHandleBox is not open".to_string())?
.read()
.map_err(|e| format!("Read failed: {}", e))
}
/// Write content to file
///
/// # Errors
///
/// - Not open: "FileHandleBox is not open"
/// - Wrong mode: "FileHandleBox opened in read mode"
/// - Write failed: "Write failed: ERROR"
pub fn write_all(&self, content: &str) -> Result<(), String> {
// Fail-Fast: Check mode
if self.mode != "w" {
return Err("FileHandleBox opened in read mode".to_string());
}
self.io
.as_ref()
.ok_or("FileHandleBox is not open".to_string())?
.write(content)
.map_err(|e| format!("Write failed: {}", e))
}
/// Close the file
///
/// # Errors
///
/// - Not open: "FileHandleBox is not open"
///
/// # Post-condition
///
/// After close(), is_open() returns false and read/write will fail.
pub fn close(&mut self) -> Result<(), String> {
if self.io.is_none() {
return Err("FileHandleBox is not open".to_string());
}
// Drop the FileIo instance
self.io.take();
self.path.clear();
self.mode.clear();
Ok(())
}
/// Check if file is currently open
pub fn is_open(&self) -> bool {
self.io.is_some()
}
}
impl BoxCore for FileHandleBox {
fn box_id(&self) -> u64 {
self.base.id
}
fn parent_type_id(&self) -> Option<std::any::TypeId> {
self.base.parent_type_id
}
fn fmt_box(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"FileHandleBox(path={}, mode={}, open={})",
if self.path.is_empty() {
"<none>"
} else {
&self.path
},
if self.mode.is_empty() {
"<none>"
} else {
&self.mode
},
self.is_open()
)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl NyashBox for FileHandleBox {
fn clone_box(&self) -> Box<dyn NyashBox> {
Box::new(self.clone())
}
fn share_box(&self) -> Box<dyn NyashBox> {
self.clone_box()
}
fn to_string_box(&self) -> StringBox {
StringBox::new(format!(
"FileHandleBox(path={}, mode={}, open={})",
self.path, self.mode, self.is_open()
))
}
fn type_name(&self) -> &'static str {
"FileHandleBox"
}
fn equals(&self, other: &dyn NyashBox) -> BoolBox {
if let Some(other_handle) = other.as_any().downcast_ref::<FileHandleBox>() {
// Equality: Same path and mode
// Note: Two independent handles to same file are equal if path/mode match
BoolBox::new(self.path == other_handle.path && self.mode == other_handle.mode)
} else {
BoolBox::new(false)
}
}
}
impl std::fmt::Display for FileHandleBox {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.fmt_box(f)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::runtime::ring0::default_ring0;
use crate::providers::ring1::file::ring0_fs_fileio::Ring0FsFileIo;
use std::fs;
use std::io::Write;
use std::sync::Arc;
fn setup_test_file(path: &str, content: &str) {
let mut file = fs::File::create(path).unwrap();
file.write_all(content.as_bytes()).unwrap();
}
fn cleanup_test_file(path: &str) {
let _ = fs::remove_file(path);
}
/// Helper: Initialize FileBox provider for tests
fn init_test_provider() {
// Try to initialize Ring0 (ignore if already initialized)
use crate::runtime::ring0::init_global_ring0;
use std::panic;
let _ = panic::catch_unwind(|| {
let ring0 = default_ring0();
init_global_ring0(ring0);
});
// Set provider if not already set (ignore errors from re-initialization)
let ring0_arc = Arc::new(default_ring0());
let provider = Arc::new(Ring0FsFileIo::new(ring0_arc));
let _ = provider_lock::set_filebox_provider(provider);
}
#[test]
fn test_filehandlebox_basic_write_read() {
init_test_provider();
let tmp_path = "/tmp/phase110_test_write_read.txt";
// Write to file
let mut h = FileHandleBox::new();
assert!(!h.is_open());
h.open(tmp_path, "w").expect("open for write failed");
assert!(h.is_open());
h.write_all("hello world").expect("write failed");
h.close().expect("close failed");
assert!(!h.is_open());
// Read from file (separate instance)
let mut h2 = FileHandleBox::new();
h2.open(tmp_path, "r").expect("open for read failed");
let content = h2.read_to_string().expect("read failed");
assert_eq!(content, "hello world");
h2.close().expect("close failed");
cleanup_test_file(tmp_path);
}
#[test]
fn test_filehandlebox_double_open_error() {
init_test_provider();
let tmp_path = "/tmp/phase110_test_double_open.txt";
setup_test_file(tmp_path, "test");
let mut h = FileHandleBox::new();
h.open(tmp_path, "r").expect("first open");
// Second open should fail
let result = h.open(tmp_path, "r");
assert!(result.is_err());
assert!(result.unwrap_err().contains("already open"));
h.close().expect("close");
cleanup_test_file(tmp_path);
}
#[test]
fn test_filehandlebox_closed_access_error() {
init_test_provider();
let tmp_path = "/tmp/phase110_test_closed_access.txt";
setup_test_file(tmp_path, "test");
let mut h = FileHandleBox::new();
h.open(tmp_path, "r").expect("open");
h.close().expect("close");
// Read after close should fail
let result = h.read_to_string();
assert!(result.is_err());
assert!(result.unwrap_err().contains("not open"));
cleanup_test_file(tmp_path);
}
#[test]
fn test_filehandlebox_write_wrong_mode() {
init_test_provider();
let tmp_path = "/tmp/phase110_test_write_wrong_mode.txt";
setup_test_file(tmp_path, "test");
let mut h = FileHandleBox::new();
h.open(tmp_path, "r").expect("open for read");
// Write in read mode should fail
let result = h.write_all("data");
assert!(result.is_err());
assert!(result.unwrap_err().contains("read mode"));
h.close().expect("close");
cleanup_test_file(tmp_path);
}
#[test]
fn test_filehandlebox_multiple_writes() {
init_test_provider();
let tmp_path = "/tmp/phase110_test_multiple_writes.txt";
let mut h = FileHandleBox::new();
h.open(tmp_path, "w").expect("open");
// Multiple writes (note: current design is truncate mode)
h.write_all("first write").expect("first write");
// Second write will overwrite (truncate mode)
h.write_all("second write").expect("second write");
h.close().expect("close");
// Verify final content
let content = fs::read_to_string(tmp_path).unwrap();
assert_eq!(content, "second write");
cleanup_test_file(tmp_path);
}
#[test]
fn test_filehandlebox_unsupported_mode() {
init_test_provider();
let mut h = FileHandleBox::new();
let result = h.open("/tmp/test.txt", "a");
assert!(result.is_err());
assert!(result.unwrap_err().contains("Unsupported mode"));
}
#[test]
fn test_filehandlebox_independent_instances() {
init_test_provider();
let tmp_path1 = "/tmp/phase110_test_independent1.txt";
let tmp_path2 = "/tmp/phase110_test_independent2.txt";
setup_test_file(tmp_path1, "file1");
setup_test_file(tmp_path2, "file2");
// Two independent handles
let mut h1 = FileHandleBox::new();
let mut h2 = FileHandleBox::new();
h1.open(tmp_path1, "r").expect("h1 open");
h2.open(tmp_path2, "r").expect("h2 open");
let content1 = h1.read_to_string().expect("h1 read");
let content2 = h2.read_to_string().expect("h2 read");
assert_eq!(content1, "file1");
assert_eq!(content2, "file2");
h1.close().expect("h1 close");
h2.close().expect("h2 close");
cleanup_test_file(tmp_path1);
cleanup_test_file(tmp_path2);
}
}

View File

@ -10,8 +10,12 @@
pub mod box_shim; // Thin delegating shim
pub mod builtin_factory;
pub mod core_ro; // Core readonly provider
pub mod handle_box; // Phase 110: FileHandleBox (handle-based multiple-access I/O)
pub mod provider; // trait FileIo / FileCaps / FileError // Builtin FileBox ProviderFactory
// Re-export FileHandleBox for easier access
pub use handle_box::FileHandleBox;
use crate::box_trait::{BoolBox, BoxBase, BoxCore, NyashBox, StringBox};
use crate::runtime::provider_lock;
use std::any::Any;