Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions system/Config/DotEnv.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,24 @@ public function parse(): ?array
return null;
}

// Ensure the file is readable
if (! is_readable($this->path)) {
throw new InvalidArgumentException("The .env file is not readable: {$this->path}");
$lines = @file($this->path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

// The .env file may have been removed or replaced by a concurrent
// process between the is_file() check above and this read attempt
// (e.g. another test process renaming `.env`). A vanished file is
// treated as absent, so re-check with a fresh stat cache.
if ($lines === false) {
clearstatcache();

if (is_file($this->path)) {
throw new InvalidArgumentException("The .env file is not readable: {$this->path}");
}

return null;
}

$vars = [];

$lines = file($this->path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

foreach ($lines as $line) {
// Is it a comment?
if (str_starts_with(trim($line), '#')) {
Expand Down
62 changes: 62 additions & 0 deletions tests/system/Config/DotEnvTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,68 @@ public function testLoadsUnreadableFile(): void
$dotenv->load();
}

/**
* Regression test: a concurrent process may remove or replace the .env
* file between the is_file() check and the read attempt. In that case the
* file must be treated as absent (parse() returns null) instead of
* throwing an exception.
*/
public function testParseReturnsNullIfFileRemovedBetweenCheckAndRead(): void
{
$scheme = 'vanishenv';

$wrapper = new class () {
public static bool $vanished = false;

/**
* @var resource|null
*/
public $context;

/**
* @return array<string, int>|false
*/
public function url_stat(string $path, int $flags): array|false
{
if (self::$vanished) {
return false;
}

return [
'dev' => 0,
'ino' => 0,
'mode' => 0100644,
'nlink' => 1,
'uid' => 0,
'gid' => 0,
'rdev' => 0,
'size' => 1,
'atime' => 1,
'mtime' => 1,
'ctime' => 1,
'blksize' => 4096,
'blocks' => 8,
];
}

public function stream_open(string $path, string $mode, int $options, ?string &$openedPath): bool
{
self::$vanished = true;

return false;
}
};

stream_wrapper_register($scheme, $wrapper::class, STREAM_IS_URL);

try {
$dotenv = new DotEnv("{$scheme}://dir", '.env');
$this->assertNull($dotenv->parse());
} finally {
stream_wrapper_unregister($scheme);
}
}

public function testQuotedDotenvLoadsEnvironmentVars(): void
{
$dotenv = new DotEnv($this->fixturesFolder, 'quoted.env');
Expand Down
Loading