feat(phase114): FileIo trait 拡張 & メタデータ統一完成
Phase 113 で公開した .hako API は変更なく、内部実装を完全統一化。 FsApi(Ring0 stateless)と FileIo(Ring1 stateful)の設計を確立。 【実装内容】 Task 1: FileIo trait 拡張 - FileStat 構造体追加(is_file/is_dir/size) - exists/stat/canonicalize メソッド追加(FileIo trait) Task 2: Ring0FsFileIo 実装 - exists(): path を Ring0.fs で確認 - stat(): Ring0.fs.metadata() を FileStat に変換 - canonicalize(): Ring0.fs.canonicalize() を String に変換 Task 3: NoFsFileIo stub 実装 - exists() → false(全ファイルが「存在しない」扱い) - stat() → Err(Unsupported)(FS 無効情報を返す) - canonicalize() → Err(Unsupported) Task 4: FileHandleBox 内部統一 - metadata_internal() を新規追加(FileIo::stat() ベース) - is_file/is_dir/size を metadata_internal() 経由に統一 - Nyash 公開 API(ny_exists/ny_size/ny_isFile/ny_isDir)は変更なし Task 5: テスト + ドキュメント - Ring0FsFileIo: 5テスト(stat/exists/canonicalize) - NoFsFileIo: 3テスト(exist/stat/canonicalize error) - FileHandleBox: 5テスト(metadata_internal/exists/is_file/is_dir) - すべてのテスト PASS 【設計原則確立】 FsApi ↔ FileIo の責務分担: - FsApi (Ring0): Stateless(パスを毎回指定) - FileIo (Ring1): Stateful(path を内部保持) - FileHandleBox: FileIo::stat() で一元化 Profile 別動作: - Default: 全機能正常動作 - NoFs: exists=false, stat/canonicalize は Unsupported エラー 【統計】 - 修正ファイル: 9ファイル - 追加行: +432行、削除: -29行 - 新規テスト: 13個(全PASS) - ビルド: SUCCESS 【効果】 - 内部実装が完全統一(二重実装・不一貫性排除) - Phase 115+ での拡張(modified_time/permissions等)が容易に - FsApi と FileIo の設計がクリアに確立 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -4,16 +4,20 @@
|
||||
use crate::boxes::file::provider::{normalize_newlines, FileCaps, FileError, FileIo, FileResult};
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use std::sync::RwLock;
|
||||
|
||||
pub struct CoreRoFileIo {
|
||||
handle: RwLock<Option<File>>,
|
||||
/// Store path for metadata operations (Phase 114)
|
||||
path: RwLock<Option<String>>,
|
||||
}
|
||||
|
||||
impl CoreRoFileIo {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
handle: RwLock::new(None),
|
||||
path: RwLock::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -27,6 +31,7 @@ impl FileIo for CoreRoFileIo {
|
||||
let file = File::open(path)
|
||||
.map_err(|e| FileError::Io(format!("Failed to open {}: {}", path, e)))?;
|
||||
*self.handle.write().unwrap() = Some(file);
|
||||
*self.path.write().unwrap() = Some(path.to_string()); // Phase 114: Store path
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -49,12 +54,59 @@ impl FileIo for CoreRoFileIo {
|
||||
|
||||
fn close(&self) -> FileResult<()> {
|
||||
*self.handle.write().unwrap() = None;
|
||||
*self.path.write().unwrap() = None; // Phase 114: Clear path
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
// Phase 114: Metadata operations
|
||||
|
||||
fn exists(&self) -> bool {
|
||||
let path_lock = self.path.read().unwrap();
|
||||
match path_lock.as_ref() {
|
||||
Some(path_str) => {
|
||||
Path::new(path_str).exists()
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn stat(&self) -> FileResult<crate::boxes::file::provider::FileStat> {
|
||||
let path_lock = self.path.read().unwrap();
|
||||
match path_lock.as_ref() {
|
||||
Some(path_str) => {
|
||||
let path_obj = Path::new(path_str);
|
||||
let meta = std::fs::metadata(path_obj)
|
||||
.map_err(|e| FileError::Io(format!("Metadata failed: {}", e)))?;
|
||||
Ok(crate::boxes::file::provider::FileStat {
|
||||
is_file: meta.is_file(),
|
||||
is_dir: meta.is_dir(),
|
||||
size: meta.len(),
|
||||
})
|
||||
}
|
||||
None => {
|
||||
Err(FileError::Io("No file path set. Call open() first.".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn canonicalize(&self) -> FileResult<String> {
|
||||
let path_lock = self.path.read().unwrap();
|
||||
match path_lock.as_ref() {
|
||||
Some(path_str) => {
|
||||
let path_obj = Path::new(path_str);
|
||||
let canonical = std::fs::canonicalize(path_obj)
|
||||
.map_err(|e| FileError::Io(format!("Canonicalize failed: {}", e)))?;
|
||||
Ok(canonical.display().to_string())
|
||||
}
|
||||
None => {
|
||||
Err(FileError::Io("No file path set. Call open() first.".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@ -54,6 +54,25 @@ impl FileIo for NoFsFileIo {
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
// Phase 114: Metadata operations (stub)
|
||||
|
||||
fn exists(&self) -> bool {
|
||||
// NoFs profile: all files are considered non-existent
|
||||
false
|
||||
}
|
||||
|
||||
fn stat(&self) -> FileResult<crate::boxes::file::provider::FileStat> {
|
||||
Err(FileError::Unsupported(
|
||||
"FileSystem operations disabled in no-fs profile".to_string()
|
||||
))
|
||||
}
|
||||
|
||||
fn canonicalize(&self) -> FileResult<String> {
|
||||
Err(FileError::Unsupported(
|
||||
"FileSystem operations disabled in no-fs profile".to_string()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -99,4 +118,29 @@ mod tests {
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("unsupported"));
|
||||
}
|
||||
|
||||
// ===== Phase 114: Metadata operation tests =====
|
||||
|
||||
#[test]
|
||||
fn test_nofs_fileio_exists() {
|
||||
let fileio = NoFsFileIo;
|
||||
// NoFsFileIo.exists() should always return false
|
||||
assert!(!fileio.exists(), "NoFsFileIo.exists() should always return false");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nofs_fileio_stat_error() {
|
||||
let fileio = NoFsFileIo;
|
||||
let result = fileio.stat();
|
||||
assert!(result.is_err(), "stat() should return error");
|
||||
assert!(result.unwrap_err().to_string().contains("unsupported"), "should contain 'unsupported'");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nofs_fileio_canonicalize_error() {
|
||||
let fileio = NoFsFileIo;
|
||||
let result = fileio.canonicalize();
|
||||
assert!(result.is_err(), "canonicalize() should return error");
|
||||
assert!(result.unwrap_err().to_string().contains("unsupported"), "should contain 'unsupported'");
|
||||
}
|
||||
}
|
||||
|
||||
@ -156,6 +156,53 @@ impl FileIo for Ring0FsFileIo {
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
// Phase 114: Metadata operations
|
||||
|
||||
fn exists(&self) -> bool {
|
||||
let path_lock = self.path.read().unwrap();
|
||||
match path_lock.as_ref() {
|
||||
Some(path_str) => {
|
||||
let path_obj = Path::new(path_str);
|
||||
self.ring0.fs.exists(path_obj)
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn stat(&self) -> FileResult<crate::boxes::file::provider::FileStat> {
|
||||
let path_lock = self.path.read().unwrap();
|
||||
match path_lock.as_ref() {
|
||||
Some(path_str) => {
|
||||
let path_obj = Path::new(path_str);
|
||||
let meta = self.ring0.fs.metadata(path_obj)
|
||||
.map_err(|e| FileError::Io(format!("Metadata failed: {}", e)))?;
|
||||
Ok(crate::boxes::file::provider::FileStat {
|
||||
is_file: meta.is_file,
|
||||
is_dir: meta.is_dir,
|
||||
size: meta.len,
|
||||
})
|
||||
}
|
||||
None => {
|
||||
Err(FileError::Io("No file path set. Call open() first.".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn canonicalize(&self) -> FileResult<String> {
|
||||
let path_lock = self.path.read().unwrap();
|
||||
match path_lock.as_ref() {
|
||||
Some(path_str) => {
|
||||
let path_obj = Path::new(path_str);
|
||||
let canonical = self.ring0.fs.canonicalize(path_obj)
|
||||
.map_err(|e| FileError::Io(format!("Canonicalize failed: {}", e)))?;
|
||||
Ok(canonical.display().to_string())
|
||||
}
|
||||
None => {
|
||||
Err(FileError::Io("No file path set. Call open() first.".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -266,11 +313,13 @@ mod tests {
|
||||
assert!(caps.write);
|
||||
|
||||
// Write content (truncate mode)
|
||||
fileio.set_mode("w".to_string()); // Phase 114: set mode before open
|
||||
assert!(fileio.open(test_path).is_ok());
|
||||
assert!(fileio.write(test_content).is_ok());
|
||||
assert!(fileio.close().is_ok());
|
||||
|
||||
// Read back and verify
|
||||
fileio.set_mode("r".to_string()); // Phase 114: set mode before open
|
||||
assert!(fileio.open(test_path).is_ok());
|
||||
let content = fileio.read().unwrap();
|
||||
assert_eq!(content, test_content);
|
||||
@ -289,11 +338,13 @@ mod tests {
|
||||
let fileio = Ring0FsFileIo::new(ring0);
|
||||
|
||||
// Overwrite with new content (truncate mode)
|
||||
fileio.set_mode("w".to_string()); // Phase 114: set mode before open
|
||||
assert!(fileio.open(test_path).is_ok());
|
||||
assert!(fileio.write("New content").is_ok());
|
||||
assert!(fileio.close().is_ok());
|
||||
|
||||
// Verify truncate behavior
|
||||
fileio.set_mode("r".to_string()); // Phase 114: set mode before open
|
||||
assert!(fileio.open(test_path).is_ok());
|
||||
let content = fileio.read().unwrap();
|
||||
assert_eq!(content, "New content");
|
||||
@ -312,4 +363,92 @@ mod tests {
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("No file is currently open"));
|
||||
}
|
||||
|
||||
// ===== Phase 114: Metadata operation tests =====
|
||||
|
||||
#[test]
|
||||
fn test_ring0_fs_fileio_stat_default_profile() {
|
||||
let test_path = "/tmp/phase114_stat_test.txt";
|
||||
let test_content = "Hello, Phase 114!";
|
||||
|
||||
setup_test_file(test_path, test_content);
|
||||
|
||||
let ring0 = Arc::new(default_ring0());
|
||||
let fileio = Ring0FsFileIo::new(ring0);
|
||||
|
||||
// Open file
|
||||
assert!(fileio.open(test_path).is_ok());
|
||||
|
||||
// Test stat()
|
||||
let stat = fileio.stat().expect("stat should succeed");
|
||||
assert!(stat.is_file, "should be a file");
|
||||
assert!(!stat.is_dir, "should not be a directory");
|
||||
assert_eq!(stat.size, test_content.len() as u64, "size should match");
|
||||
|
||||
fileio.close().unwrap();
|
||||
cleanup_test_file(test_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ring0_fs_fileio_exists_default_profile() {
|
||||
let test_path = "/tmp/phase114_exists_test.txt";
|
||||
setup_test_file(test_path, "test");
|
||||
|
||||
let ring0 = Arc::new(default_ring0());
|
||||
let fileio = Ring0FsFileIo::new(ring0);
|
||||
|
||||
// Open file
|
||||
assert!(fileio.open(test_path).is_ok());
|
||||
|
||||
// Test exists() - should return true for existing file
|
||||
assert!(fileio.exists(), "exists() should return true");
|
||||
|
||||
fileio.close().unwrap();
|
||||
cleanup_test_file(test_path);
|
||||
|
||||
// After file is deleted, exists() with no path set should return false
|
||||
assert!(!fileio.exists(), "exists() should return false when no path set");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ring0_fs_fileio_canonicalize_default_profile() {
|
||||
let test_path = "/tmp/phase114_canonicalize.txt";
|
||||
setup_test_file(test_path, "test");
|
||||
|
||||
let ring0 = Arc::new(default_ring0());
|
||||
let fileio = Ring0FsFileIo::new(ring0);
|
||||
|
||||
// Open file
|
||||
assert!(fileio.open(test_path).is_ok());
|
||||
|
||||
// Test canonicalize()
|
||||
let canonical = fileio.canonicalize().expect("canonicalize should succeed");
|
||||
assert!(canonical.contains("phase114_canonicalize.txt"), "should contain filename");
|
||||
assert!(canonical.starts_with("/"), "should be absolute path");
|
||||
|
||||
fileio.close().unwrap();
|
||||
cleanup_test_file(test_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ring0_fs_fileio_stat_without_open() {
|
||||
let ring0 = Arc::new(default_ring0());
|
||||
let fileio = Ring0FsFileIo::new(ring0);
|
||||
|
||||
// stat() without open() should fail
|
||||
let result = fileio.stat();
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("No file path set"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ring0_fs_fileio_canonicalize_without_open() {
|
||||
let ring0 = Arc::new(default_ring0());
|
||||
let fileio = Ring0FsFileIo::new(ring0);
|
||||
|
||||
// canonicalize() without open() should fail
|
||||
let result = fileio.canonicalize();
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("No file path set"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user