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:
477
src/boxes/file/handle_box.rs
Normal file
477
src/boxes/file/handle_box.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -10,8 +10,12 @@
|
||||
pub mod box_shim; // Thin delegating shim
|
||||
pub mod builtin_factory;
|
||||
pub mod core_ro; // Core read‑only 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;
|
||||
|
||||
Reference in New Issue
Block a user