diff --git a/src/boxes/file/errors.rs b/src/boxes/file/errors.rs index af636edd..6cbe0768 100644 --- a/src/boxes/file/errors.rs +++ b/src/boxes/file/errors.rs @@ -32,7 +32,7 @@ pub fn not_open() -> String { /// Unsupported mode error (with mode name) 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 diff --git a/src/boxes/file/handle_box.rs b/src/boxes/file/handle_box.rs index 0c82b9f5..5813f4d2 100644 --- a/src/boxes/file/handle_box.rs +++ b/src/boxes/file/handle_box.rs @@ -80,19 +80,18 @@ impl FileHandleBox { /// # Arguments /// /// - path: File path to open - /// - mode: "r" (read) or "w" (write) + /// - mode: "r" (read), "w" (write/truncate), or "a" (append) /// /// # Errors /// /// - 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." /// - 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 + /// - Phase 111: Supports "r", "w", and "a" modes /// - 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 @@ -100,8 +99,8 @@ impl FileHandleBox { return Err(already_open()); } - // Validate mode - if mode != "r" && mode != "w" { + // Validate mode (Phase 111: "a" added) + if mode != "r" && mode != "w" && mode != "a" { return Err(unsupported_mode(mode)); } @@ -126,13 +125,13 @@ impl FileHandleBox { 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" { + // For write/append mode, create the file if it doesn't exist + // Ring0FsFileIo expects the file to exist for open(), so we create it first + if mode == "w" || mode == "a" { use std::path::Path; let path_obj = Path::new(path); if !ring0.fs.exists(path_obj) { - // Create empty file for write mode + // Create empty file for write/append mode ring0 .fs .write_all(path_obj, &[]) @@ -140,7 +139,12 @@ impl FileHandleBox { } } - let io: Arc = 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 = Arc::new(file_io); // Now open the file with the new instance io.open(path) @@ -175,10 +179,15 @@ impl FileHandleBox { /// - Not open: "FileHandleBox is not open" /// - Wrong mode: "FileHandleBox opened in read mode" /// - 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> { - // Fail-Fast: Check mode - if self.mode != "w" { - return Err(opened_in_read_mode()); + // Fail-Fast: Check mode (Phase 111: allow "w" and "a") + if self.mode == "r" { + return Err("FileHandleBox is opened in read-only mode".to_string()); } self.io @@ -214,6 +223,75 @@ impl FileHandleBox { pub fn is_open(&self) -> bool { self.io.is_some() } + + // ===== Phase 111: Metadata methods (internal Rust API) ===== + + /// Internal helper: Get FsMetadata from Ring0FsFileIo + fn metadata_internal(&self) -> Result { + 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::() + .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 { + 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 { + 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 { + 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 { + self.metadata_internal().map(|meta| meta.is_dir) + } } impl BoxCore for FileHandleBox { @@ -402,7 +480,7 @@ mod tests { // Write in read mode should fail let result = h.write_all("data"); assert!(result.is_err()); - assert!(result.unwrap_err().contains("read mode")); + assert!(result.unwrap_err().contains("read-only")); h.close().expect("close"); cleanup_test_file(tmp_path); @@ -436,7 +514,8 @@ mod tests { init_test_provider(); 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.unwrap_err().contains("Unsupported mode")); } @@ -575,4 +654,120 @@ mod tests { 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")); + } } diff --git a/src/boxes/file/provider.rs b/src/boxes/file/provider.rs index 549b9870..1c1c2847 100644 --- a/src/boxes/file/provider.rs +++ b/src/boxes/file/provider.rs @@ -37,11 +37,11 @@ impl FileCaps { /// 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 /// - /// - mode: "r" (read) or "w" (write) + /// - mode: "r" (read), "w" (write), or "a" (append) /// /// # Returns /// @@ -60,7 +60,7 @@ impl FileCaps { return Err("Read not supported by FileBox provider".to_string()); } } - "w" => { + "w" | "a" => { // Phase 111: "a" added if !self.write { return Err("Write not supported by FileBox provider".to_string()); } @@ -78,8 +78,8 @@ impl FileCaps { pub enum FileError { #[error("io error: {0}")] Io(String), - #[error("unsupported operation")] - Unsupported, + #[error("unsupported operation: {0}")] + Unsupported(String), } pub type FileResult = Result; @@ -91,6 +91,9 @@ pub trait FileIo: Send + Sync { fn read(&self) -> FileResult; fn write(&self, text: &str) -> FileResult<()>; // Phase 108: write support fn close(&self) -> FileResult<()>; + + /// Phase 111: Downcast support for metadata access + fn as_any(&self) -> &dyn std::any::Any; // Future: exists/stat … } diff --git a/src/providers/ring1/file/core_ro.rs b/src/providers/ring1/file/core_ro.rs index 2374003c..e46d05a5 100644 --- a/src/providers/ring1/file/core_ro.rs +++ b/src/providers/ring1/file/core_ro.rs @@ -44,13 +44,17 @@ impl FileIo for CoreRoFileIo { fn write(&self, _text: &str) -> FileResult<()> { // CoreRoFileIo is read-only, write is not supported - Err(FileError::Unsupported) + Err(FileError::Unsupported("CoreRoFileIo is read-only".to_string())) } fn close(&self) -> FileResult<()> { *self.handle.write().unwrap() = None; Ok(()) } + + fn as_any(&self) -> &dyn std::any::Any { + self + } } #[cfg(test)] @@ -65,7 +69,7 @@ mod tests { let result = fileio.write("test"); assert!(result.is_err()); match result.unwrap_err() { - FileError::Unsupported => { /* expected */ } + FileError::Unsupported(_) => { /* expected */ } _ => panic!("Expected Unsupported error"), } } diff --git a/src/providers/ring1/file/nofs_fileio.rs b/src/providers/ring1/file/nofs_fileio.rs index 276739d8..a3c1ab06 100644 --- a/src/providers/ring1/file/nofs_fileio.rs +++ b/src/providers/ring1/file/nofs_fileio.rs @@ -36,19 +36,23 @@ impl FileIo for NoFsFileIo { } fn open(&self, _path: &str) -> FileResult<()> { - Err(FileError::Unsupported) + Err(FileError::Unsupported("FileBox disabled in NoFs profile".to_string())) } fn read(&self) -> FileResult { - Err(FileError::Unsupported) + Err(FileError::Unsupported("FileBox disabled in NoFs profile".to_string())) } fn write(&self, _text: &str) -> FileResult<()> { - Err(FileError::Unsupported) + Err(FileError::Unsupported("FileBox disabled in NoFs profile".to_string())) } 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 } } diff --git a/src/providers/ring1/file/ring0_fs_fileio.rs b/src/providers/ring1/file/ring0_fs_fileio.rs index 1a57c462..37bdf7da 100644 --- a/src/providers/ring1/file/ring0_fs_fileio.rs +++ b/src/providers/ring1/file/ring0_fs_fileio.rs @@ -20,11 +20,13 @@ use std::sync::{Arc, RwLock}; /// **Design decisions**: /// - UTF-8 handling: Uses `read_to_string()` which handles UTF-8 internally /// - 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 { ring0: Arc, /// Current opened file path (None if no file is open) path: RwLock>, + /// File mode ("r", "w", "a") + mode: RwLock>, } impl Ring0FsFileIo { @@ -33,6 +35,27 @@ impl Ring0FsFileIo { Self { ring0, 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 { + 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<()> { let current_path = self.path.read().unwrap(); + let current_mode = self.mode.read().unwrap(); - match current_path.as_ref() { - Some(path) => { - // Delegate to Ring0.FsApi (truncate mode: overwrite existing file) + match (current_path.as_ref(), current_mode.as_ref()) { + (Some(path), Some(mode)) => { 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())) } + (Some(_), None) => { + Err(FileError::Io("File mode not set. Internal error.".to_string())) + } } } fn close(&self) -> FileResult<()> { let mut current_path = self.path.write().unwrap(); + let mut current_mode = self.mode.write().unwrap(); *current_path = None; + *current_mode = None; Ok(()) } + + fn as_any(&self) -> &dyn std::any::Any { + self + } } #[cfg(test)] diff --git a/src/runtime/ring0/std_impls.rs b/src/runtime/ring0/std_impls.rs index 212f961c..c1fa7852 100644 --- a/src/runtime/ring0/std_impls.rs +++ b/src/runtime/ring0/std_impls.rs @@ -182,6 +182,20 @@ impl FsApi for StdFs { .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 { path.exists() } diff --git a/src/runtime/ring0/traits.rs b/src/runtime/ring0/traits.rs index d1389b67..424edab9 100644 --- a/src/runtime/ring0/traits.rs +++ b/src/runtime/ring0/traits.rs @@ -105,6 +105,12 @@ pub trait FsApi: Send + Sync { /// ファイルに書き込む 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;