Skip to content

[BUGFIX] Survive removing a directory which is already gone - #109

Open
CybotTM wants to merge 1 commit into
TYPO3:mainfrom
CybotTM:bugfix/remove-missing-directory
Open

[BUGFIX] Survive removing a directory which is already gone#109
CybotTM wants to merge 1 commit into
TYPO3:mainfrom
CybotTM:bugfix/remove-missing-directory

Conversation

@CybotTM

@CybotTM CybotTM commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Directory::remove() passes the result of realpath() straight into a RecursiveDirectoryIterator. For a directory that does not exist that result is false, and false given to an internal function under strict_types is a TypeError.

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:

In UploadExtensionVersionCommand.php line 65:

  Directory could not be created.

PHP Fatal error:  Uncaught TypeError: RecursiveDirectoryIterator::__construct(): Argument #1 ($directory) must be of type string, false given in src/Filesystem/Directory.php:42
Stack trace:
#0 src/Filesystem/Directory.php(42): RecursiveDirectoryIterator->__construct()
#1 src/Command/Extension/UploadExtensionVersionCommand.php(148): TYPO3\Tailor\Filesystem\Directory->remove()
#2 [internal function]: TYPO3\Tailor\Command\Extension\UploadExtensionVersionCommand->__destruct()
#3 {main}

$this->transactionPath is 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 false for a path which is not a directory, before the iterator is built. A file is covered as well: realpath() resolves it and RecursiveDirectoryIterator would throw UnexpectedValueException for it, which is just as fatal in a destructor.

Tests

tests/Unit/Filesystem/DirectoryTest.php is new - the class had no test - and covers remove() only: the directory with nested content is removed, a non-existing path returns false, a file returns false and stays where it is. UploadExtensionVersionCommandTest gets 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 TypeError above, the wiring test with exactly the trace from the report. RecursiveIteratorIterator::CHILD_FIRST turned into SELF_FIRST reddens the removal test, so it is wired to the recursion rather than to the return value alone.

composer tests:unit is green (158 tests, 437 assertions) and composer cs reports no findings.

Found while working on #108, unrelated to it and independent of that branch.

Assisted by claude-code:claude-opus-5 — Session

`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
CybotTM marked this pull request as ready for review September 3, 2026 08:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant