Passed
Pull Request — master (#251)
by Gabriel
10:21
created

FileCacheTest::testWindowsPathLengthLimitationsAreCorrectlyRespected()   B

Complexity

Conditions 4
Paths 8

Size

Total Lines 43
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 43
c 0
b 0
f 0
rs 8.5806
cc 4
eloc 26
nc 8
nop 2
1
<?php
2
3
namespace Doctrine\Tests\Common\Cache;
4
5
use Doctrine\Common\Cache\Cache;
6
use Doctrine\Common\Cache\FileCache;
7
use Doctrine\Tests\DoctrineTestCase;
8
use InvalidArgumentException;
9
use const DIRECTORY_SEPARATOR;
10
use const PATHINFO_DIRNAME;
11
use function basename;
12
use function bin2hex;
13
use function define;
14
use function defined;
15
use function floor;
16
use function hash;
17
use function mkdir;
18
use function pathinfo;
19
use function realpath;
20
use function sprintf;
21
use function str_repeat;
22
use function strlen;
23
use function substr;
24
use function sys_get_temp_dir;
25
use function uniqid;
26
27
/**
28
 * @group DCOM-101
29
 */
30
class FileCacheTest extends DoctrineTestCase
31
{
32
    /** @var FileCache */
33
    private $driver;
34
35
    protected function setUp() : void
36
    {
37
        $this->driver = $this
38
            ->getMockBuilder(FileCache::class)
39
            ->setMethods(['doFetch', 'doContains', 'doSave'])
40
            ->disableOriginalConstructor()
41
            ->getMock();
42
    }
43
44
    public function testFilenameShouldCreateThePathWithOneSubDirectory() : void
45
    {
46
        $cache       = $this->driver;
47
        $method      = new \ReflectionMethod($cache, 'getFilename');
48
        $key         = 'item-key';
49
        $expectedDir = '84';
50
51
        $method->setAccessible(true);
52
53
        $path    = $method->invoke($cache, $key);
54
        $dirname = pathinfo($path, PATHINFO_DIRNAME);
55
56
        self::assertEquals(DIRECTORY_SEPARATOR . $expectedDir, $dirname);
57
    }
58
59
    public function testFileExtensionCorrectlyEscaped() : void
60
    {
61
        $driver1 = $this
62
            ->getMockBuilder(FileCache::class)
63
            ->setMethods(['doFetch', 'doContains', 'doSave'])
64
            ->setConstructorArgs([__DIR__, '.*'])
65
            ->getMock();
66
67
        $driver2 = $this
68
            ->getMockBuilder(FileCache::class)
69
            ->setMethods(['doFetch', 'doContains', 'doSave'])
70
            ->setConstructorArgs([__DIR__, '.php'])
71
            ->getMock();
72
73
        $doGetStats = new \ReflectionMethod($driver1, 'doGetStats');
74
75
        $doGetStats->setAccessible(true);
76
77
        $stats1 = $doGetStats->invoke($driver1);
78
        $stats2 = $doGetStats->invoke($driver2);
79
80
        self::assertSame(0, $stats1[Cache::STATS_MEMORY_USAGE]);
81
        self::assertGreaterThan(0, $stats2[Cache::STATS_MEMORY_USAGE]);
82
    }
83
84
    /**
85
     * @group DCOM-266
86
     */
87
    public function testFileExtensionSlashCorrectlyEscaped() : void
88
    {
89
        $driver = $this
90
            ->getMockBuilder(FileCache::class)
91
            ->setMethods(['doFetch', 'doContains', 'doSave'])
92
            ->setConstructorArgs([__DIR__ . '/../', DIRECTORY_SEPARATOR . basename(__FILE__)])
93
            ->getMock();
94
95
        $doGetStats = new \ReflectionMethod($driver, 'doGetStats');
96
97
        $doGetStats->setAccessible(true);
98
99
        $stats = $doGetStats->invoke($driver);
100
101
        self::assertGreaterThan(0, $stats[Cache::STATS_MEMORY_USAGE]);
102
    }
103
104
    public function testNonIntUmaskThrowsInvalidArgumentException() : void
105
    {
106
        $this->expectException(InvalidArgumentException::class);
107
108
        $this
109
            ->getMockBuilder(FileCache::class)
110
            ->setMethods(['doFetch', 'doContains', 'doSave'])
111
            ->setConstructorArgs(['', '', 'invalid'])
112
            ->getMock();
113
    }
114
115
    public function testGetDirectoryReturnsRealpathDirectoryString() : void
116
    {
117
        $directory = __DIR__ . '/../';
118
119
        $driver = $this
120
            ->getMockBuilder(FileCache::class)
121
            ->setMethods(['doFetch', 'doContains', 'doSave'])
122
            ->setConstructorArgs([$directory])
123
            ->getMock();
124
125
        $doGetDirectory = new \ReflectionMethod($driver, 'getDirectory');
126
127
        $actualDirectory   = $doGetDirectory->invoke($driver);
128
        $expectedDirectory = realpath($directory);
129
130
        self::assertEquals($expectedDirectory, $actualDirectory);
131
    }
132
133
    public function testGetExtensionReturnsExtensionString() : void
134
    {
135
        $directory = __DIR__ . '/../';
136
        $extension = DIRECTORY_SEPARATOR . basename(__FILE__);
137
138
        $driver = $this
139
            ->getMockBuilder(FileCache::class)
140
            ->setMethods(['doFetch', 'doContains', 'doSave'])
141
            ->setConstructorArgs([$directory, $extension])
142
            ->getMock();
143
144
        $doGetExtension = new \ReflectionMethod($driver, 'getExtension');
145
146
        $actualExtension = $doGetExtension->invoke($driver);
147
148
        self::assertEquals($extension, $actualExtension);
149
    }
150
151
    public const WIN_MAX_PATH_LEN = 258;
152
153
    public static function getBasePathForWindowsPathLengthTests(int $pathLength) : string
154
    {
155
        // Not using __DIR__ because it can get screwed up when xdebug debugger is attached.
156
        $basePath = realpath(sys_get_temp_dir()) . '/' . uniqid('doctrine-cache', true);
157
158
        /** @noinspection MkdirRaceConditionInspection */
159
        @mkdir($basePath);
160
161
        $basePath = realpath($basePath);
162
163
        // Test whether the desired path length is odd or even.
164
        $desiredPathLengthIsOdd = ($pathLength % 2) == 1;
165
166
        // If the cache key is not too long, the filecache codepath will add
167
        // a slash and bin2hex($key). The length of the added portion will be an odd number.
168
        // len(desired) = len(base path) + len(slash . bin2hex($key))
169
        //          odd = even           + odd
170
        //         even = odd            + odd
171
        $basePathLengthShouldBeOdd = ! $desiredPathLengthIsOdd;
172
173
        $basePathLengthIsOdd = (strlen($basePath) % 2) == 1;
174
175
        // If the base path needs to be odd or even where it is not, we add an odd number of
176
        // characters as a pad. In this case, we're adding '\aa' (or '/aa' depending on platform)
177
        // This is all to make it so that the key we're testing would result in
178
        // a path that is exactly the length we want to test IF the path length limit
179
        // were not in place in FileCache.
180
        if ($basePathLengthIsOdd != $basePathLengthShouldBeOdd) {
181
            $basePath .= DIRECTORY_SEPARATOR . 'aa';
182
        }
183
184
        return $basePath;
185
    }
186
187
    public static function getKeyAndPathFittingLength(int $length, string $basePath) : array
188
    {
189
        $baseDirLength   = strlen($basePath);
190
        $extensionLength = strlen('.doctrine.cache');
191
        $directoryLength = strlen(DIRECTORY_SEPARATOR . 'aa' . DIRECTORY_SEPARATOR);
192
        $keyLength       = $length - ($baseDirLength + $extensionLength + $directoryLength); // - 1 because of slash
193
194
        $key = str_repeat('a', floor($keyLength / 2));
195
196
        $keyHash = hash('sha256', $key);
197
198
        $keyPath = $basePath
199
            . DIRECTORY_SEPARATOR
200
            . substr($keyHash, 0, 2)
201
            . DIRECTORY_SEPARATOR
202
            . bin2hex($key)
203
            . '.doctrine.cache';
204
205
        $hashedKeyPath = $basePath
206
            . DIRECTORY_SEPARATOR
207
            . substr($keyHash, 0, 2)
208
            . DIRECTORY_SEPARATOR
209
            . '_' . $keyHash
210
            . '.doctrine.cache';
211
212
        return [$key, $keyPath, $hashedKeyPath];
213
    }
214
215
    public function getPathLengthsToTest() : array
216
    {
217
        // Windows officially supports 260 bytes including null terminator
218
        // 259 characters is too large due to PHP bug (https://bugs.php.net/bug.php?id=70943)
219
        // 260 characters is too large - null terminator is included in allowable length
220
        return [
221
            [257, false],
222
            [258, false],
223
            [259, true],
224
            [260, true],
225
        ];
226
    }
227
228
    /**
229
     * @dataProvider getPathLengthsToTest
230
     *
231
     * @covers \Doctrine\Common\Cache\FileCache::getFilename
232
     */
233
    public function testWindowsPathLengthLimitationsAreCorrectlyRespected(int $length, bool $pathShouldBeHashed) : void
234
    {
235
        if (! defined('PHP_WINDOWS_VERSION_BUILD')) {
236
            define('PHP_WINDOWS_VERSION_BUILD', 'Yes, this is the "usual suspect", with the usual limitations');
237
        }
238
239
        $basePath = self::getBasePathForWindowsPathLengthTests($length);
240
241
        $fileCache = $this->getMockForAbstractClass(
242
            'Doctrine\Common\Cache\FileCache',
243
            [$basePath, '.doctrine.cache']
244
        );
245
246
        list($key, $keyPath, $hashedKeyPath) = self::getKeyAndPathFittingLength($length, $basePath);
247
248
        $getFileName = new \ReflectionMethod($fileCache, 'getFilename');
249
250
        $getFileName->setAccessible(true);
251
252
        self::assertEquals(
253
            $length,
254
            strlen($keyPath),
255
            sprintf('Path expected to be %d characters long is %d characters long', $length, strlen($keyPath))
256
        );
257
258
        if ($pathShouldBeHashed) {
259
            $keyPath = $hashedKeyPath;
260
        }
261
262
        if ($pathShouldBeHashed) {
263
            self::assertSame(
264
                $hashedKeyPath,
265
                $getFileName->invoke($fileCache, $key),
266
                'Keys should be hashed correctly if they are over the limit.'
267
            );
268
        } else {
269
            self::assertSame(
270
                $keyPath,
271
                $getFileName->invoke($fileCache, $key),
272
                'Keys below limit of the allowed length are used directly, unhashed'
273
            );
274
        }
275
    }
276
}
277