[BUGFIX] Survive removing a directory which is already gone - #109
Open
CybotTM wants to merge 1 commit into
Open
Conversation
`Directory::remove()` passes the result of `realpath()` straight into a `RecursiveDirectoryIterator`. For a directory that does not exist that result is `false`, which is a `TypeError` under `strict_types`. The only caller is `UploadExtensionVersionCommand::__destruct()`, so the error is thrown while the object is destroyed. Publishing from a working directory tailor may not write to shows it: the command reports "Directory could not be created." as intended and the process then dies with PHP Fatal error: Uncaught TypeError: RecursiveDirectoryIterator::__construct(): Argument TYPO3#1 ($directory) must be of type string, false given leaving exit code 255 instead of the console exit code, and the fatal error covering the message that explains what actually went wrong. The same happens whenever the transaction directory vanishes before the destructor runs. Return `false` for a path which is not a directory instead. A file is covered as well, `realpath()` resolves it and the iterator would throw `UnexpectedValueException` for it. Assisted-by: claude-code:claude-opus-5 Agent-Session: https://claude.ai/code/session_01CFdZJzsCjJ7rmT1u9snkiv Agent-Host: 0493f0 Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
CybotTM
marked this pull request as ready for review
September 3, 2026 08:18
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Directory::remove()passes the result ofrealpath()straight into aRecursiveDirectoryIterator. For a directory that does not exist that result isfalse, andfalsegiven to an internal function understrict_typesis aTypeError.Its only caller is
UploadExtensionVersionCommand::__destruct(), so the error is thrown while the object is being destroyed. Publishing from a working directory tailor may not write to is enough to see it - the command reports what went wrong and the process then dies on something else entirely:$this->transactionPathis assigned before the directory is created, so the destructor tries to clean up a directory that was never there. The exit code is 255 from the fatal error rather than the console's own, and the fatal error is the last thing on screen, above it the message that actually explains the failure. The same happens whenever the transaction directory vanishes between the command finishing and the destructor running.The fix
Return
falsefor a path which is not a directory, before the iterator is built. A file is covered as well:realpath()resolves it andRecursiveDirectoryIteratorwould throwUnexpectedValueExceptionfor it, which is just as fatal in a destructor.Tests
tests/Unit/Filesystem/DirectoryTest.phpis new - the class had no test - and coversremove()only: the directory with nested content is removed, a non-existing path returnsfalse, a file returnsfalseand stays where it is.UploadExtensionVersionCommandTestgets the wiring: the transaction directory is removed behind the command's back, and the destructor still has to run cleanly.All four were seen failing. Without the guard the three new ones error with the
TypeErrorabove, the wiring test with exactly the trace from the report.RecursiveIteratorIterator::CHILD_FIRSTturned intoSELF_FIRSTreddens the removal test, so it is wired to the recursion rather than to the return value alone.composer tests:unitis green (158 tests, 437 assertions) andcomposer csreports no findings.Found while working on #108, unrelated to it and independent of that branch.
Assisted by claude-code:claude-opus-5 — Session