19declare(strict_types=1);
24use PHPUnit\Framework\Attributes\DataProvider;
25use PHPUnit\Framework\TestCase;
36 protected function setUp(): void
38 $this->directory = sys_get_temp_dir() .
'/ilias_atomic_' . bin2hex(random_bytes(4));
39 mkdir($this->directory, 0775);
52 return array_values(array_diff(scandir($this->directory) ?: [], [
'.',
'..']));
59 $this->assertSame(
"inhalt\n", file_get_contents($this->directory .
'/target.po'));
65 file_put_contents($this->directory .
'/target.po',
'alt');
69 $this->assertSame(
'neu', file_get_contents($this->directory .
'/target.po'));
77 $this->assertSame(
'', file_get_contents($this->directory .
'/empty.mo'));
89 $this->assertSame(
'inhalt', file_get_contents($this->directory .
'/target.mo'));
99 $outside = dirname($this->directory) .
'/ilias_atomic_outside_' . bin2hex(random_bytes(4));
100 mkdir($outside, 0775);
102 $this->expectException(RuntimeException::class);
116 $previous = umask(0022);
124 $this->assertSame(
'0644', sprintf(
'%04o', fileperms($this->directory .
'/target.mo') & 0777));
136 file_put_contents($this->directory .
'/target.po',
'alt');
137 chmod($this->directory .
'/target.po', 0600);
138 $previous_umask = umask(0022);
142 umask($previous_umask);
146 $this->assertSame(
'0600', sprintf(
'%04o', fileperms($this->directory .
'/target.po') & 0777));
153 #[DataProvider('grantsAtLeastProvider')]
154 public function testGrantsAtLeast(
int $effective_mode,
int $intended_mode,
bool $expected): void
165 '0600 lacks group/other read the intended 0644 needs' => [0600, 0644,
false],
166 '0777 grants everything, including 0644' => [0777, 0644,
true],
167 '0644 grants exactly the identical 0644 it is compared against' => [0644, 0644,
true],
168 '0640 lacks group-write and other-read the intended 0664 needs' => [0640, 0664,
false],
169 '0664 grants everything the less permissive 0644 needs' => [0664, 0644,
true],
181 $this->assertSame(
'x', file_get_contents($this->directory .
'/target.po'));
191 $outside = dirname($this->directory) .
'/ilias_atomic_outside_' . bin2hex(random_bytes(4));
192 mkdir($outside, 0775);
193 $confine_to = $this->directory .
'/allowed';
194 mkdir($confine_to, 0775);
199 $this->fail(
'Expected a RuntimeException');
200 }
catch (RuntimeException
$e) {
201 $this->assertStringContainsString(
'does not resolve to a path below',
$e->getMessage());
204 $this->assertSame([], array_values(array_diff(scandir($outside) ?: [], [
'.',
'..'])),
'nothing must be written outside the confined directory');
205 $this->assertSame([], array_values(array_diff(scandir($confine_to) ?: [], [
'.',
'..'])),
'no temporary file must leak into the confined directory either');
222 set_error_handler(
static fn():
bool =>
true, E_NOTICE | E_WARNING);
224 $this->expectException(RuntimeException::class);
225 $this->expectExceptionMessageMatches(
'/Could not create a temporary file/');
228 restore_error_handler();
238 mkdir($this->directory .
'/target.po');
240 set_error_handler(
static fn():
bool =>
true, E_WARNING);
243 $this->fail(
'Expected a RuntimeException');
244 }
catch (RuntimeException
$e) {
245 $this->assertStringContainsString(
'Could not replace',
$e->getMessage());
247 restore_error_handler();
251 $this->assertDirectoryExists($this->directory .
'/target.po');
260 $real = $this->directory .
'/real';
262 symlink($real, $this->directory .
'/link');
266 $this->assertSame(
'x', file_get_contents($real .
'/target.po'));
267 $this->assertSame([
'target.po'], array_values(array_diff(scandir($real), [
'.',
'..'])));
272 mkdir($this->directory .
'/sub');
276 $this->assertSame(
'x', file_get_contents($this->directory .
'/sub/target.po'));
277 $this->assertSame([
'target.po'], array_values(array_diff(scandir($this->directory .
'/sub'), [
'.',
'..'])));
283 chdir($this->directory);
292 $this->assertSame(
'x', file_get_contents($this->directory .
'/relative.po'));
293 $this->assertSame(
'y', file_get_contents($this->directory .
'/nested/target.po'));
302 file_put_contents($this->directory .
'/file',
'x');
304 set_error_handler(
static fn():
bool =>
true, E_NOTICE | E_WARNING);
306 $this->expectException(RuntimeException::class);
309 restore_error_handler();
Writes a file so that concurrent readers only see the complete old or new content: a temporary file i...
static grantsAtLeast(int $effective_mode, int $intended_mode)
Whether $effective_mode (permission bits 07777 only) grants every permission of $intended_mode.
static write(string $file, string $content, ?string $confine_to_directory=null, bool $flush_to_disk=true)
AtomicFileWriter: the overlay .po/.mo are replaced via a temporary file in the same directory plus re...
testWritesToARelativePath()
testWritesWithADoubleSlashInThePath()
testConfineToDirectoryAllowsAWriteWhoseTargetResolvesInsideIt()
The happy path of $confine_to_directory: a target resolving inside it is written normally - both the ...
testThrowsWhenAPathComponentIsARegularFile()
Writing below a regular file (not a directory) must fail cleanly - also as root.
testWritesANewFileAndLeavesNoTemporaryFileBehind()
testAnExistingTargetKeepsItsOwnModeInsteadOfANewFilesMode()
An existing target keeps its own mode - not the "new file" mode from testTheWrittenFileDoesNotKeepThe...
testFlushToDiskDisabledStillHonoursConfineToDirectory()
$flush_to_disk = false must not disable the $confine_to_directory guard - both parameters are indepen...
testReplacesAnExistingFile()
testThrowsWhenTheTargetCannotBeReplacedAndRemovesTheTemporaryFile()
Root-proof failure of the final rename(): the target is a directory.
testWritesIntoASymlinkedDirectory()
Regression: tempnam() returns the resolved directory; comparing it with the unresolved one rejected e...
testTheWrittenFileDoesNotKeepTheRestrictiveTempnamPermissions()
tempnam() creates 0600 files; the result must have the permissions a plain file_put_contents() would ...
testThrowsWhenTheTargetDirectoryDoesNotExist()
testConfineToDirectoryThrowsWhenTheTargetResolvesOutsideIt()
Mutation: the pre-write confinement check dropped, inverted or applied after writing - a target whose...
testGrantsAtLeast(int $effective_mode, int $intended_mode, bool $expected)
grantsAtLeast() is the pure decision acceptUnchangeableMode() delegates to: whether every permission ...
static grantsAtLeastProvider()
testWritesCompleteContentWithFlushToDiskDisabled()
$flush_to_disk = false (used by ShippedLanguageFilesCompiledObjective for the build artifacts) skips ...
static removeDirectory(string $directory)