@@ -210,6 +210,132 @@ public function testDeleteMatchingSuffix(): void
210210 $ this ->handler ->clean ();
211211 }
212212
213+ /**
214+ * @param string $pattern "{victim}" is replaced with the name of a file in the parent of the cache directory
215+ */
216+ #[DataProvider('provideDeleteMatchingDoesNotDeleteFilesOutsideCacheDirectory ' )]
217+ public function testDeleteMatchingDoesNotDeleteFilesOutsideCacheDirectory (string $ pattern ): void
218+ {
219+ $ parent = realpath (dirname ($ this ->config ->file ['storePath ' ]));
220+ $ this ->assertNotFalse ($ parent );
221+
222+ $ victim = tempnam ($ parent , 'deleteMatchingVictim ' );
223+ $ this ->assertNotFalse ($ victim );
224+
225+ try {
226+ // tempnam() falls back to the system temp directory on failure.
227+ $ this ->assertSame ($ parent , realpath (dirname ($ victim )));
228+
229+ $ this ->assertTrue ($ this ->handler ->save (self ::$ key1 , 'value ' ));
230+
231+ $ pattern = str_replace ('{victim} ' , basename ($ victim ), $ pattern );
232+
233+ $ this ->assertSame (0 , $ this ->handler ->deleteMatching ($ pattern ));
234+ $ this ->assertFileExists ($ victim );
235+ $ this ->assertSame ('value ' , $ this ->handler ->get (self ::$ key1 ));
236+ } finally {
237+ if (is_file ($ victim )) {
238+ unlink ($ victim );
239+ }
240+ }
241+ }
242+
243+ /**
244+ * @return iterable<string, array{string}>
245+ */
246+ public static function provideDeleteMatchingDoesNotDeleteFilesOutsideCacheDirectory (): iterable
247+ {
248+ yield 'parent directory ' => ['../{victim} ' ];
249+
250+ yield 'parent directory with wildcard ' => ['../{victim}* ' ];
251+
252+ yield 'parent directory with backslash ' => ['.. \\{victim} ' ];
253+
254+ yield 'parent directory only ' => ['.. ' ];
255+ }
256+
257+ public function testDeleteMatchingDoesNotDeleteFilesInSubdirectories (): void
258+ {
259+ $ directoryName = 'dir_ ' . bin2hex (random_bytes (8 ));
260+ $ subdirectory = $ this ->config ->file ['storePath ' ] . '/ ' . $ directoryName . '/ ' ;
261+
262+ $ this ->assertTrue (mkdir ($ subdirectory ));
263+
264+ try {
265+ $ this ->assertNotFalse (file_put_contents ($ subdirectory . 'key_1 ' , 'data ' ));
266+
267+ $ this ->assertSame (0 , $ this ->handler ->deleteMatching ($ directoryName . '/key_* ' ));
268+ $ this ->assertFileExists ($ subdirectory . 'key_1 ' );
269+ } finally {
270+ if (is_file ($ subdirectory . 'key_1 ' )) {
271+ unlink ($ subdirectory . 'key_1 ' );
272+ }
273+
274+ rmdir ($ subdirectory );
275+ }
276+ }
277+
278+ #[RequiresOperatingSystem('Linux|Darwin ' )]
279+ public function testDeleteMatchingSupportsEscapedWildcards (): void
280+ {
281+ $ this ->assertTrue ($ this ->handler ->save ('key*1 ' , 'value ' ));
282+ $ this ->assertTrue ($ this ->handler ->save ('key_1 ' , 'value ' ));
283+
284+ $ this ->assertSame (1 , $ this ->handler ->deleteMatching ('key\*1 ' ));
285+ $ this ->assertNull ($ this ->handler ->get ('key*1 ' ));
286+ $ this ->assertSame ('value ' , $ this ->handler ->get ('key_1 ' ));
287+ }
288+
289+ public function testDeleteMatchingTreatsCachePathLiterally (): void
290+ {
291+ // As a glob pattern, "cache[1]" would match the sibling "cache1" instead of itself.
292+ $ cachePath = $ this ->config ->file ['storePath ' ] . '/cache[1]/ ' ;
293+ $ siblingPath = $ this ->config ->file ['storePath ' ] . '/cache1/ ' ;
294+
295+ $ this ->assertTrue (mkdir ($ cachePath ));
296+ $ this ->assertTrue (mkdir ($ siblingPath ));
297+
298+ try {
299+ $ this ->assertNotFalse (file_put_contents ($ siblingPath . 'key_1 ' , 'data ' ));
300+
301+ $ config = new Cache ();
302+ $ config ->file ['storePath ' ] = $ cachePath ;
303+ $ handler = CacheFactory ::getHandler ($ config , 'file ' );
304+
305+ $ this ->assertTrue ($ handler ->save ('key_1 ' , 'value ' ));
306+
307+ $ this ->assertSame (1 , $ handler ->deleteMatching ('* ' ));
308+ $ this ->assertNull ($ handler ->get ('key_1 ' ));
309+ $ this ->assertFileExists ($ siblingPath . 'key_1 ' );
310+ } finally {
311+ foreach ([$ cachePath , $ siblingPath ] as $ path ) {
312+ if (is_file ($ path . 'key_1 ' )) {
313+ unlink ($ path . 'key_1 ' );
314+ }
315+
316+ rmdir ($ path );
317+ }
318+ }
319+ }
320+
321+ public function testDeleteMatchingWildcardDoesNotMatchHiddenFiles (): void
322+ {
323+ $ hidden = $ this ->config ->file ['storePath ' ] . '/.hidden ' ;
324+
325+ $ this ->assertNotFalse (file_put_contents ($ hidden , 'data ' ));
326+
327+ try {
328+ $ this ->assertTrue ($ this ->handler ->save (self ::$ key1 , 'value ' ));
329+
330+ $ this ->assertSame (1 , $ this ->handler ->deleteMatching ('* ' ));
331+ $ this ->assertFileExists ($ hidden );
332+ } finally {
333+ if (is_file ($ hidden )) {
334+ unlink ($ hidden );
335+ }
336+ }
337+ }
338+
213339 public function testIncrement (): void
214340 {
215341 $ this ->handler ->save (self ::$ key1 , 1 );
0 commit comments