feat(phase111): FileHandleBox append+metadata実装(修正案統合版)
Task 2: FsApi / Ring0FsFileIo 拡張 - FsApi trait に append_all(path, data) メソッド追加 - StdFsApi で append_all() を std::fs::OpenOptions で実装 - Ring0FsFileIo に mode を保持、write() で truncate/append を切り替え - Ring0FsFileIo に内部 metadata() ヘルパ追加(FsApi.metadata() 呼び出し) Task 3: FileHandleBox API 実装 - open(path, mode) で "r"/"w"/"a" 3モードをサポート - write_all() で read-only mode チェック - 内部 Rust API:size / exists / is_file / is_dir メソッド実装 (NyashBox 公開は Phase 112+ に延期) Task 5: テスト + ドキュメント - 4つの新テスト PASS: - test_filehandlebox_append_mode(write→append→内容確認) - test_filehandlebox_metadata_size(size() 取得) - test_filehandlebox_metadata_is_file(is_file()/is_dir()) - test_filehandlebox_write_readonly_error("r"で write 拒否) 統計: - 9ファイル修正(+316行, -35行) - 4つの新テスト追加(既存15テスト全PASS) - cargo build --release: SUCCESS - 11個のチェックリスト: ✅ ALL PASS 次フェーズ(Phase 112-114)の backlog も指示書で整理済み 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -32,7 +32,7 @@ pub fn not_open() -> String {
|
|||||||
|
|
||||||
/// Unsupported mode error (with mode name)
|
/// Unsupported mode error (with mode name)
|
||||||
pub fn unsupported_mode(mode: &str) -> String {
|
pub fn unsupported_mode(mode: &str) -> String {
|
||||||
format!("Unsupported mode: {}. Use 'r' or 'w'", mode)
|
format!("Unsupported mode: {}. Use 'r', 'w', or 'a'", mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read not supported by provider
|
/// Read not supported by provider
|
||||||
|
|||||||
@ -80,19 +80,18 @@ impl FileHandleBox {
|
|||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// - path: File path to open
|
/// - path: File path to open
|
||||||
/// - mode: "r" (read) or "w" (write)
|
/// - mode: "r" (read), "w" (write/truncate), or "a" (append)
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// - Already open: "FileHandleBox is already open. Call close() first."
|
/// - Already open: "FileHandleBox is already open. Call close() first."
|
||||||
/// - Unsupported mode: "Unsupported mode: X. Use 'r' or 'w'"
|
/// - Unsupported mode: "Unsupported mode: X. Use 'r', 'w', or 'a'"
|
||||||
/// - NoFs profile: "File I/O disabled in no-fs profile. FileHandleBox is not available."
|
/// - NoFs profile: "File I/O disabled in no-fs profile. FileHandleBox is not available."
|
||||||
/// - File not found (mode="r"): "File not found: PATH"
|
/// - File not found (mode="r"): "File not found: PATH"
|
||||||
///
|
///
|
||||||
/// # Design Notes
|
/// # Design Notes
|
||||||
///
|
///
|
||||||
/// - Phase 110: Supports "r" and "w" only
|
/// - Phase 111: Supports "r", "w", and "a" modes
|
||||||
/// - Phase 111: Will add "a" (append) mode
|
|
||||||
/// - Each FileHandleBox instance gets its own FileIo (independent)
|
/// - Each FileHandleBox instance gets its own FileIo (independent)
|
||||||
pub fn open(&mut self, path: &str, mode: &str) -> Result<(), String> {
|
pub fn open(&mut self, path: &str, mode: &str) -> Result<(), String> {
|
||||||
// Fail-Fast: Check for double open
|
// Fail-Fast: Check for double open
|
||||||
@ -100,8 +99,8 @@ impl FileHandleBox {
|
|||||||
return Err(already_open());
|
return Err(already_open());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate mode
|
// Validate mode (Phase 111: "a" added)
|
||||||
if mode != "r" && mode != "w" {
|
if mode != "r" && mode != "w" && mode != "a" {
|
||||||
return Err(unsupported_mode(mode));
|
return Err(unsupported_mode(mode));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,13 +125,13 @@ impl FileHandleBox {
|
|||||||
|
|
||||||
let ring0 = get_global_ring0();
|
let ring0 = get_global_ring0();
|
||||||
|
|
||||||
// For write mode, create the file if it doesn't exist
|
// For write/append mode, create the file if it doesn't exist
|
||||||
// Ring0FsFileIo expects the file to exist, so we create it first
|
// Ring0FsFileIo expects the file to exist for open(), so we create it first
|
||||||
if mode == "w" {
|
if mode == "w" || mode == "a" {
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
let path_obj = Path::new(path);
|
let path_obj = Path::new(path);
|
||||||
if !ring0.fs.exists(path_obj) {
|
if !ring0.fs.exists(path_obj) {
|
||||||
// Create empty file for write mode
|
// Create empty file for write/append mode
|
||||||
ring0
|
ring0
|
||||||
.fs
|
.fs
|
||||||
.write_all(path_obj, &[])
|
.write_all(path_obj, &[])
|
||||||
@ -140,7 +139,12 @@ impl FileHandleBox {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let io: Arc<dyn FileIo> = Arc::new(Ring0FsFileIo::new(ring0.clone()));
|
let file_io = Ring0FsFileIo::new(ring0.clone());
|
||||||
|
|
||||||
|
// Set mode BEFORE opening (Phase 111)
|
||||||
|
file_io.set_mode(mode.to_string());
|
||||||
|
|
||||||
|
let io: Arc<dyn FileIo> = Arc::new(file_io);
|
||||||
|
|
||||||
// Now open the file with the new instance
|
// Now open the file with the new instance
|
||||||
io.open(path)
|
io.open(path)
|
||||||
@ -175,10 +179,15 @@ impl FileHandleBox {
|
|||||||
/// - Not open: "FileHandleBox is not open"
|
/// - Not open: "FileHandleBox is not open"
|
||||||
/// - Wrong mode: "FileHandleBox opened in read mode"
|
/// - Wrong mode: "FileHandleBox opened in read mode"
|
||||||
/// - Write failed: "Write failed: ERROR"
|
/// - Write failed: "Write failed: ERROR"
|
||||||
|
///
|
||||||
|
/// # Phase 111
|
||||||
|
///
|
||||||
|
/// Supports both "w" (truncate) and "a" (append) modes.
|
||||||
|
/// Mode "r" returns error.
|
||||||
pub fn write_all(&self, content: &str) -> Result<(), String> {
|
pub fn write_all(&self, content: &str) -> Result<(), String> {
|
||||||
// Fail-Fast: Check mode
|
// Fail-Fast: Check mode (Phase 111: allow "w" and "a")
|
||||||
if self.mode != "w" {
|
if self.mode == "r" {
|
||||||
return Err(opened_in_read_mode());
|
return Err("FileHandleBox is opened in read-only mode".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
self.io
|
self.io
|
||||||
@ -214,6 +223,75 @@ impl FileHandleBox {
|
|||||||
pub fn is_open(&self) -> bool {
|
pub fn is_open(&self) -> bool {
|
||||||
self.io.is_some()
|
self.io.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Phase 111: Metadata methods (internal Rust API) =====
|
||||||
|
|
||||||
|
/// Internal helper: Get FsMetadata from Ring0FsFileIo
|
||||||
|
fn metadata_internal(&self) -> Result<crate::runtime::ring0::FsMetadata, String> {
|
||||||
|
if self.path.is_empty() {
|
||||||
|
return Err("FileHandleBox path not set".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Ring0FsFileIo and call metadata()
|
||||||
|
self.io
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| "FileHandleBox not open".to_string())?
|
||||||
|
.as_any()
|
||||||
|
.downcast_ref::<crate::providers::ring1::file::ring0_fs_fileio::Ring0FsFileIo>()
|
||||||
|
.ok_or_else(|| "FileIo is not Ring0FsFileIo".to_string())?
|
||||||
|
.metadata()
|
||||||
|
.map_err(|e| format!("Metadata failed: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get file size in bytes
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// - Path not set: "FileHandleBox path not set"
|
||||||
|
/// - Metadata failed: "Metadata failed: ERROR"
|
||||||
|
pub fn size(&self) -> Result<u64, String> {
|
||||||
|
self.metadata_internal().map(|meta| meta.len)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if file exists
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// - Path not set: "FileHandleBox path not set"
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// Returns false if file not found (no error)
|
||||||
|
pub fn exists(&self) -> Result<bool, String> {
|
||||||
|
if self.path.is_empty() {
|
||||||
|
return Err("FileHandleBox path not set".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.metadata_internal() {
|
||||||
|
Ok(_) => Ok(true),
|
||||||
|
Err(_) => Ok(false), // not found → false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if path is a file
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// - Path not set: "FileHandleBox path not set"
|
||||||
|
/// - Metadata failed: "Metadata failed: ERROR"
|
||||||
|
pub fn is_file(&self) -> Result<bool, String> {
|
||||||
|
self.metadata_internal().map(|meta| meta.is_file)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if path is a directory
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// - Path not set: "FileHandleBox path not set"
|
||||||
|
/// - Metadata failed: "Metadata failed: ERROR"
|
||||||
|
pub fn is_dir(&self) -> Result<bool, String> {
|
||||||
|
self.metadata_internal().map(|meta| meta.is_dir)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BoxCore for FileHandleBox {
|
impl BoxCore for FileHandleBox {
|
||||||
@ -402,7 +480,7 @@ mod tests {
|
|||||||
// Write in read mode should fail
|
// Write in read mode should fail
|
||||||
let result = h.write_all("data");
|
let result = h.write_all("data");
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
assert!(result.unwrap_err().contains("read mode"));
|
assert!(result.unwrap_err().contains("read-only"));
|
||||||
|
|
||||||
h.close().expect("close");
|
h.close().expect("close");
|
||||||
cleanup_test_file(tmp_path);
|
cleanup_test_file(tmp_path);
|
||||||
@ -436,7 +514,8 @@ mod tests {
|
|||||||
init_test_provider();
|
init_test_provider();
|
||||||
|
|
||||||
let mut h = FileHandleBox::new();
|
let mut h = FileHandleBox::new();
|
||||||
let result = h.open("/tmp/test.txt", "a");
|
// Phase 111: "a" is now supported, test with "x" instead
|
||||||
|
let result = h.open("/tmp/test.txt", "x");
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
assert!(result.unwrap_err().contains("Unsupported mode"));
|
assert!(result.unwrap_err().contains("Unsupported mode"));
|
||||||
}
|
}
|
||||||
@ -575,4 +654,120 @@ mod tests {
|
|||||||
|
|
||||||
cleanup_test_file(tmp_path);
|
cleanup_test_file(tmp_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Phase 111: Append mode + metadata tests =====
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_filehandlebox_append_mode() {
|
||||||
|
init_test_provider();
|
||||||
|
|
||||||
|
let path = "/tmp/phase111_append_test.txt";
|
||||||
|
let _ = fs::remove_file(path); // cleanup
|
||||||
|
|
||||||
|
// First write (truncate)
|
||||||
|
let mut handle = FileHandleBox::new();
|
||||||
|
handle.open(path, "w").unwrap();
|
||||||
|
handle.write_all("hello\n").unwrap();
|
||||||
|
handle.close().unwrap();
|
||||||
|
|
||||||
|
// Append
|
||||||
|
let mut handle = FileHandleBox::new();
|
||||||
|
handle.open(path, "a").unwrap();
|
||||||
|
handle.write_all("world\n").unwrap();
|
||||||
|
handle.close().unwrap();
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
let content = fs::read_to_string(path).unwrap();
|
||||||
|
assert_eq!(content, "hello\nworld\n");
|
||||||
|
|
||||||
|
let _ = fs::remove_file(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_filehandlebox_metadata_size() {
|
||||||
|
init_test_provider();
|
||||||
|
|
||||||
|
let path = "/tmp/phase111_metadata_test.txt";
|
||||||
|
let _ = fs::remove_file(path);
|
||||||
|
|
||||||
|
// Write test file
|
||||||
|
let mut handle = FileHandleBox::new();
|
||||||
|
handle.open(path, "w").unwrap();
|
||||||
|
handle.write_all("hello").unwrap(); // 5 bytes
|
||||||
|
handle.close().unwrap();
|
||||||
|
|
||||||
|
// Check size
|
||||||
|
let mut handle = FileHandleBox::new();
|
||||||
|
handle.open(path, "r").unwrap();
|
||||||
|
let size = handle.size().unwrap();
|
||||||
|
assert_eq!(size, 5);
|
||||||
|
handle.close().unwrap();
|
||||||
|
|
||||||
|
let _ = fs::remove_file(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_filehandlebox_metadata_is_file() {
|
||||||
|
init_test_provider();
|
||||||
|
|
||||||
|
let path = "/tmp/phase111_file_test.txt";
|
||||||
|
let _ = fs::remove_file(path);
|
||||||
|
|
||||||
|
// Create file
|
||||||
|
let mut handle = FileHandleBox::new();
|
||||||
|
handle.open(path, "w").unwrap();
|
||||||
|
handle.close().unwrap();
|
||||||
|
|
||||||
|
// Check is_file
|
||||||
|
let mut handle = FileHandleBox::new();
|
||||||
|
handle.open(path, "r").unwrap();
|
||||||
|
let is_file = handle.is_file().unwrap();
|
||||||
|
assert!(is_file);
|
||||||
|
|
||||||
|
let is_dir = handle.is_dir().unwrap();
|
||||||
|
assert!(!is_dir);
|
||||||
|
handle.close().unwrap();
|
||||||
|
|
||||||
|
let _ = fs::remove_file(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_filehandlebox_write_readonly_error() {
|
||||||
|
init_test_provider();
|
||||||
|
|
||||||
|
let path = "/tmp/phase111_readonly_test.txt";
|
||||||
|
let _ = fs::remove_file(path);
|
||||||
|
|
||||||
|
// Create file
|
||||||
|
fs::write(path, "content").unwrap();
|
||||||
|
|
||||||
|
// Open in read mode
|
||||||
|
let mut handle = FileHandleBox::new();
|
||||||
|
handle.open(path, "r").unwrap();
|
||||||
|
|
||||||
|
// Try to write → Error
|
||||||
|
let result = handle.write_all("new");
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().contains("read-only"));
|
||||||
|
|
||||||
|
handle.close().unwrap();
|
||||||
|
let _ = fs::remove_file(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore] // This test requires NYASH_RUNTIME_PROFILE=no-fs environment variable
|
||||||
|
fn test_filehandlebox_nofs_profile_error() {
|
||||||
|
// Note: This test should be run with NYASH_RUNTIME_PROFILE=no-fs
|
||||||
|
// For now, we test that open() fails with disabled message when provider is None
|
||||||
|
|
||||||
|
// Cannot directly test NoFs profile in unit tests without setting env var
|
||||||
|
// and reinitializing runtime. This test is marked as ignored and should be
|
||||||
|
// run separately with proper profile configuration.
|
||||||
|
|
||||||
|
// The test would look like:
|
||||||
|
// let mut handle = FileHandleBox::new();
|
||||||
|
// let result = handle.open("/tmp/test.txt", "w");
|
||||||
|
// assert!(result.is_err());
|
||||||
|
// assert!(result.unwrap_err().contains("disabled"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,11 +37,11 @@ impl FileCaps {
|
|||||||
|
|
||||||
/// Phase 110.5: Capability check helper
|
/// Phase 110.5: Capability check helper
|
||||||
///
|
///
|
||||||
/// Validates if the given mode ("r" or "w") is supported by this provider.
|
/// Validates if the given mode ("r", "w", or "a") is supported by this provider.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// - mode: "r" (read) or "w" (write)
|
/// - mode: "r" (read), "w" (write), or "a" (append)
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
///
|
///
|
||||||
@ -60,7 +60,7 @@ impl FileCaps {
|
|||||||
return Err("Read not supported by FileBox provider".to_string());
|
return Err("Read not supported by FileBox provider".to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"w" => {
|
"w" | "a" => { // Phase 111: "a" added
|
||||||
if !self.write {
|
if !self.write {
|
||||||
return Err("Write not supported by FileBox provider".to_string());
|
return Err("Write not supported by FileBox provider".to_string());
|
||||||
}
|
}
|
||||||
@ -78,8 +78,8 @@ impl FileCaps {
|
|||||||
pub enum FileError {
|
pub enum FileError {
|
||||||
#[error("io error: {0}")]
|
#[error("io error: {0}")]
|
||||||
Io(String),
|
Io(String),
|
||||||
#[error("unsupported operation")]
|
#[error("unsupported operation: {0}")]
|
||||||
Unsupported,
|
Unsupported(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type FileResult<T> = Result<T, FileError>;
|
pub type FileResult<T> = Result<T, FileError>;
|
||||||
@ -91,6 +91,9 @@ pub trait FileIo: Send + Sync {
|
|||||||
fn read(&self) -> FileResult<String>;
|
fn read(&self) -> FileResult<String>;
|
||||||
fn write(&self, text: &str) -> FileResult<()>; // Phase 108: write support
|
fn write(&self, text: &str) -> FileResult<()>; // Phase 108: write support
|
||||||
fn close(&self) -> FileResult<()>;
|
fn close(&self) -> FileResult<()>;
|
||||||
|
|
||||||
|
/// Phase 111: Downcast support for metadata access
|
||||||
|
fn as_any(&self) -> &dyn std::any::Any;
|
||||||
// Future: exists/stat …
|
// Future: exists/stat …
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -44,13 +44,17 @@ impl FileIo for CoreRoFileIo {
|
|||||||
|
|
||||||
fn write(&self, _text: &str) -> FileResult<()> {
|
fn write(&self, _text: &str) -> FileResult<()> {
|
||||||
// CoreRoFileIo is read-only, write is not supported
|
// CoreRoFileIo is read-only, write is not supported
|
||||||
Err(FileError::Unsupported)
|
Err(FileError::Unsupported("CoreRoFileIo is read-only".to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn close(&self) -> FileResult<()> {
|
fn close(&self) -> FileResult<()> {
|
||||||
*self.handle.write().unwrap() = None;
|
*self.handle.write().unwrap() = None;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn as_any(&self) -> &dyn std::any::Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -65,7 +69,7 @@ mod tests {
|
|||||||
let result = fileio.write("test");
|
let result = fileio.write("test");
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
match result.unwrap_err() {
|
match result.unwrap_err() {
|
||||||
FileError::Unsupported => { /* expected */ }
|
FileError::Unsupported(_) => { /* expected */ }
|
||||||
_ => panic!("Expected Unsupported error"),
|
_ => panic!("Expected Unsupported error"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,19 +36,23 @@ impl FileIo for NoFsFileIo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn open(&self, _path: &str) -> FileResult<()> {
|
fn open(&self, _path: &str) -> FileResult<()> {
|
||||||
Err(FileError::Unsupported)
|
Err(FileError::Unsupported("FileBox disabled in NoFs profile".to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read(&self) -> FileResult<String> {
|
fn read(&self) -> FileResult<String> {
|
||||||
Err(FileError::Unsupported)
|
Err(FileError::Unsupported("FileBox disabled in NoFs profile".to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write(&self, _text: &str) -> FileResult<()> {
|
fn write(&self, _text: &str) -> FileResult<()> {
|
||||||
Err(FileError::Unsupported)
|
Err(FileError::Unsupported("FileBox disabled in NoFs profile".to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn close(&self) -> FileResult<()> {
|
fn close(&self) -> FileResult<()> {
|
||||||
Err(FileError::Unsupported)
|
Err(FileError::Unsupported("FileBox disabled in NoFs profile".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any(&self) -> &dyn std::any::Any {
|
||||||
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,11 +20,13 @@ use std::sync::{Arc, RwLock};
|
|||||||
/// **Design decisions**:
|
/// **Design decisions**:
|
||||||
/// - UTF-8 handling: Uses `read_to_string()` which handles UTF-8 internally
|
/// - UTF-8 handling: Uses `read_to_string()` which handles UTF-8 internally
|
||||||
/// - One file at a time: Calling open() twice without close() returns Err
|
/// - One file at a time: Calling open() twice without close() returns Err
|
||||||
/// - Read-only: Phase 107 focuses on read operations only
|
/// - Phase 111: Supports "r", "w", "a" modes
|
||||||
pub struct Ring0FsFileIo {
|
pub struct Ring0FsFileIo {
|
||||||
ring0: Arc<Ring0Context>,
|
ring0: Arc<Ring0Context>,
|
||||||
/// Current opened file path (None if no file is open)
|
/// Current opened file path (None if no file is open)
|
||||||
path: RwLock<Option<String>>,
|
path: RwLock<Option<String>>,
|
||||||
|
/// File mode ("r", "w", "a")
|
||||||
|
mode: RwLock<Option<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ring0FsFileIo {
|
impl Ring0FsFileIo {
|
||||||
@ -33,6 +35,27 @@ impl Ring0FsFileIo {
|
|||||||
Self {
|
Self {
|
||||||
ring0,
|
ring0,
|
||||||
path: RwLock::new(None),
|
path: RwLock::new(None),
|
||||||
|
mode: RwLock::new(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set file mode (internal helper for FileHandleBox)
|
||||||
|
pub fn set_mode(&self, mode: String) {
|
||||||
|
*self.mode.write().unwrap() = Some(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get metadata (internal helper for FileHandleBox)
|
||||||
|
pub fn metadata(&self) -> FileResult<crate::runtime::ring0::FsMetadata> {
|
||||||
|
let current_path = self.path.read().unwrap();
|
||||||
|
match current_path.as_ref() {
|
||||||
|
Some(path) => {
|
||||||
|
let path_obj = Path::new(path);
|
||||||
|
self.ring0.fs.metadata(path_obj)
|
||||||
|
.map_err(|e| FileError::Io(format!("Metadata failed: {}", e)))
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
Err(FileError::Io("No file path set. Call open() first.".to_string()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -82,25 +105,57 @@ impl FileIo for Ring0FsFileIo {
|
|||||||
|
|
||||||
fn write(&self, text: &str) -> FileResult<()> {
|
fn write(&self, text: &str) -> FileResult<()> {
|
||||||
let current_path = self.path.read().unwrap();
|
let current_path = self.path.read().unwrap();
|
||||||
|
let current_mode = self.mode.read().unwrap();
|
||||||
|
|
||||||
match current_path.as_ref() {
|
match (current_path.as_ref(), current_mode.as_ref()) {
|
||||||
Some(path) => {
|
(Some(path), Some(mode)) => {
|
||||||
// Delegate to Ring0.FsApi (truncate mode: overwrite existing file)
|
|
||||||
let path_obj = Path::new(path);
|
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)))
|
// Mode-based write behavior (Phase 111)
|
||||||
|
match mode.as_str() {
|
||||||
|
"w" => {
|
||||||
|
// Truncate mode: overwrite existing file
|
||||||
|
self.ring0.fs.write_all(path_obj, text.as_bytes())
|
||||||
|
.map_err(|e| FileError::Io(format!("Write failed: {}", e)))
|
||||||
|
}
|
||||||
|
"a" => {
|
||||||
|
// Append mode: append to end of file
|
||||||
|
self.ring0.fs.append_all(path_obj, text.as_bytes())
|
||||||
|
.map_err(|e| FileError::Io(format!("Append failed: {}", e)))
|
||||||
|
}
|
||||||
|
"r" => {
|
||||||
|
// Read-only mode: cannot write
|
||||||
|
Err(FileError::Unsupported(
|
||||||
|
"Cannot write in read-only mode".to_string()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
Err(FileError::Unsupported(
|
||||||
|
format!("Unsupported mode: {}", mode)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
None => {
|
(None, _) => {
|
||||||
Err(FileError::Io("No file is currently open. Call open() first.".to_string()))
|
Err(FileError::Io("No file is currently open. Call open() first.".to_string()))
|
||||||
}
|
}
|
||||||
|
(Some(_), None) => {
|
||||||
|
Err(FileError::Io("File mode not set. Internal error.".to_string()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn close(&self) -> FileResult<()> {
|
fn close(&self) -> FileResult<()> {
|
||||||
let mut current_path = self.path.write().unwrap();
|
let mut current_path = self.path.write().unwrap();
|
||||||
|
let mut current_mode = self.mode.write().unwrap();
|
||||||
*current_path = None;
|
*current_path = None;
|
||||||
|
*current_mode = None;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn as_any(&self) -> &dyn std::any::Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@ -182,6 +182,20 @@ impl FsApi for StdFs {
|
|||||||
.map_err(|e| IoError::WriteFailed(format!("write({}): {}", path.display(), e)))
|
.map_err(|e| IoError::WriteFailed(format!("write({}): {}", path.display(), e)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn append_all(&self, path: &Path, data: &[u8]) -> Result<(), IoError> {
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
let mut file = OpenOptions::new()
|
||||||
|
.create(true) // 存在しなければ作成
|
||||||
|
.append(true) // append モードで開く
|
||||||
|
.open(path)
|
||||||
|
.map_err(|e| IoError::WriteFailed(format!("append_all({}): {}", path.display(), e)))?;
|
||||||
|
|
||||||
|
file.write_all(data)
|
||||||
|
.map_err(|e| IoError::WriteFailed(format!("append write({}): {}", path.display(), e)))
|
||||||
|
}
|
||||||
|
|
||||||
fn exists(&self, path: &Path) -> bool {
|
fn exists(&self, path: &Path) -> bool {
|
||||||
path.exists()
|
path.exists()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -105,6 +105,12 @@ pub trait FsApi: Send + Sync {
|
|||||||
/// ファイルに書き込む
|
/// ファイルに書き込む
|
||||||
fn write_all(&self, path: &Path, data: &[u8]) -> Result<(), IoError>;
|
fn write_all(&self, path: &Path, data: &[u8]) -> Result<(), IoError>;
|
||||||
|
|
||||||
|
/// ファイルに追記(append)
|
||||||
|
///
|
||||||
|
/// ファイルが存在しない場合は新規作成、存在する場合は末尾に追記。
|
||||||
|
/// Phase 111: write_all と対称的に提供。
|
||||||
|
fn append_all(&self, path: &Path, data: &[u8]) -> Result<(), IoError>;
|
||||||
|
|
||||||
/// パスが存在するか確認
|
/// パスが存在するか確認
|
||||||
fn exists(&self, path: &Path) -> bool;
|
fn exists(&self, path: &Path) -> bool;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user