From 91ccd49cf68bca199e7d41fa65a4a3b0d1368127 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Mon, 29 Jun 2026 21:56:39 +0900 Subject: feat(php-shim): implement DirectoryIterator in fs shim Give DirectoryIteratorEntry a backing path (mirroring RecursiveIteratorFileInfo) and make directory_iterator enumerate entries, returning Result to match PHP DirectoryIterator's UnexpectedValueException on an unopenable directory. Wire the new Result through DumpCompletionCommand's get_supported_shells. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../console/command/dump_completion_command.rs | 20 +++--- crates/shirabe-php-shim/Cargo.toml | 3 + crates/shirabe-php-shim/src/fs.rs | 83 ++++++++++++++++++---- 3 files changed, 84 insertions(+), 22 deletions(-) (limited to 'crates') diff --git a/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs b/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs index 588b868..8e2583e 100644 --- a/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs +++ b/crates/shirabe-external-packages/src/symfony/console/command/dump_completion_command.rs @@ -63,12 +63,12 @@ impl DumpCompletionCommand { pub fn complete_impl(&self, input: &CompletionInput, suggestions: &mut CompletionSuggestions) { if input.must_suggest_argument_values_for("shell") { - suggestions.suggest_values( - self.get_supported_shells() - .into_iter() - .map(StringOrSuggestion::String) - .collect(), - ); + // TODO(phase-d): PHP's complete() lets a DirectoryIterator failure propagate, but the + // Command::complete trait is infallible; on error the suggestions are skipped instead. + if let Ok(shells) = self.get_supported_shells() { + suggestions + .suggest_values(shells.into_iter().map(StringOrSuggestion::String).collect()); + } } } @@ -98,14 +98,14 @@ impl DumpCompletionCommand { todo!() } - fn get_supported_shells(&self) -> Vec { + fn get_supported_shells(&self) -> anyhow::Result> { let mut shells = vec![]; // foreach (new \DirectoryIterator(__DIR__.'/../Resources/') as $file) for file in shirabe_php_shim::directory_iterator(&format!( "{}/../Resources/", shirabe_php_shim::dir() - )) { + ))? { if shirabe_php_shim::str_starts_with(&file.get_basename(), "completion.") && file.is_file() { @@ -113,7 +113,7 @@ impl DumpCompletionCommand { } } - shells + Ok(shells) } } @@ -209,7 +209,7 @@ impl Command for DumpCompletionCommand { shell ); if !shirabe_php_shim::file_exists(&completion_file) { - let supported_shells = self.get_supported_shells(); + let supported_shells = self.get_supported_shells()?; // TODO: PHP does `$output instanceof ConsoleOutputInterface ? $output->getErrorOutput() // : $output`. There is no way to test trait membership through `&dyn OutputInterface` diff --git a/crates/shirabe-php-shim/Cargo.toml b/crates/shirabe-php-shim/Cargo.toml index 4226234..f8092ec 100644 --- a/crates/shirabe-php-shim/Cargo.toml +++ b/crates/shirabe-php-shim/Cargo.toml @@ -19,5 +19,8 @@ sha2.workspace = true twox-hash.workspace = true zip.workspace = true +[dev-dependencies] +tempfile.workspace = true + [lints] workspace = true diff --git a/crates/shirabe-php-shim/src/fs.rs b/crates/shirabe-php-shim/src/fs.rs index bd882c2..ba6eef6 100644 --- a/crates/shirabe-php-shim/src/fs.rs +++ b/crates/shirabe-php-shim/src/fs.rs @@ -38,20 +38,29 @@ impl FilesystemIterator { pub const CURRENT_AS_FILEINFO: i64 = 0; } -#[derive(Debug)] -pub struct DirectoryIteratorEntry; +#[derive(Debug, Clone)] +pub struct DirectoryIteratorEntry { + path: std::path::PathBuf, +} + impl DirectoryIteratorEntry { - // TODO(phase-d): DirectoryIterator is a unit struct carrying no entry data; giving it real - // behavior requires the same field redesign as RecursiveIteratorFileInfo. It has no callers, so - // it is left unimplemented. + // SplFileInfo::getBasename(): the trailing name component. pub fn get_basename(&self) -> String { - todo!() + basename(&self.path.to_string_lossy()) } + // SplFileInfo::isFile() follows symlinks. pub fn is_file(&self) -> bool { - todo!() + std::fs::metadata(&self.path) + .map(|m| m.is_file()) + .unwrap_or(false) } + // SplFileInfo::getExtension(): substring after the basename's last '.'. pub fn get_extension(&self) -> String { - todo!() + let base = self.get_basename(); + match base.rfind('.') { + Some(index) => base[index + 1..].to_string(), + None => String::new(), + } } } @@ -232,10 +241,30 @@ fn rii_walk( } } -pub fn directory_iterator(_path: &str) -> Vec { - // TODO(phase-d): see DirectoryIteratorEntry; the entry type carries no data yet and there are no - // callers. - todo!() +pub fn directory_iterator( + path: &str, +) -> Result, UnexpectedValueException> { + let base = std::path::Path::new(path); + let rd = std::fs::read_dir(base).map_err(|_| UnexpectedValueException { + message: format!( + "DirectoryIterator::__construct({}): Failed to open directory", + path + ), + code: 0, + })?; + // PHP's DirectoryIterator yields the "." and ".." entries before the real ones. + let mut entries = vec![ + DirectoryIteratorEntry { + path: base.join("."), + }, + DirectoryIteratorEntry { + path: base.join(".."), + }, + ]; + for entry in rd.flatten() { + entries.push(DirectoryIteratorEntry { path: entry.path() }); + } + Ok(entries) } /// PHP `fopen()`. Returns a stream resource, or an error mirroring PHP's `false`-on-failure @@ -1428,4 +1457,34 @@ mod tests { fn fopen_missing_file_is_err() { assert!(fopen("/nonexistent/shirabe/test/path", "r").is_err()); } + + #[test] + fn directory_iterator_lists_entries() { + let dir = tempfile::tempdir().unwrap(); + std::fs::write(dir.path().join("completion.bash"), b"").unwrap(); + std::fs::create_dir(dir.path().join("sub")).unwrap(); + + let entries = directory_iterator(dir.path().to_str().unwrap()).unwrap(); + let basenames: Vec = entries.iter().map(|e| e.get_basename()).collect(); + // PHP's DirectoryIterator includes the "." and ".." entries. + assert!(basenames.contains(&".".to_string())); + assert!(basenames.contains(&"..".to_string())); + assert!(basenames.contains(&"completion.bash".to_string())); + assert!(basenames.contains(&"sub".to_string())); + + let file = entries + .iter() + .find(|e| e.get_basename() == "completion.bash") + .unwrap(); + assert!(file.is_file()); + assert_eq!(file.get_extension(), "bash"); + + let sub = entries.iter().find(|e| e.get_basename() == "sub").unwrap(); + assert!(!sub.is_file()); + } + + #[test] + fn directory_iterator_missing_dir_is_err() { + assert!(directory_iterator("/nonexistent/shirabe/test/dir").is_err()); + } } -- cgit v1.3.1