diff --git a/src/ComposerFileStorage.php b/src/ComposerFileStorage.php index 41602c8..c36858c 100644 --- a/src/ComposerFileStorage.php +++ b/src/ComposerFileStorage.php @@ -53,8 +53,30 @@ public static function create(string $url, Config $config): self { $basePath = implode(DIRECTORY_SEPARATOR, [ static::basePath($config), - preg_replace('/[^[:alnum:]\.]/', '-', $url), + static::escapeUrl($url), ]); return new static($basePath); } + + /** + * Converts a repository URL to a unique directory name. + * + * @param string $url + * A repository URL. + * + * @return string + * A version of the URL suitable to use as the name of the persistent + * storage directory. + */ + public static function escapeUrl(string $url): string + { + $escapedUrl = preg_replace('/[^[:alnum:]\.]/', '-', $url); + // Append a partial hash of the unescaped URL, to prevent URLs like + // `https://www.site.coop.info/packages` from colliding with + // `https://www.site.coop/info/packages`, whilst keeping the directory + // names easily distinguishable. + $hashedUrl = hash('sha256', $url); + + return $escapedUrl . '-' . substr($hashedUrl, 0, 8); + } } diff --git a/tests/ComposerFileStorageTest.php b/tests/ComposerFileStorageTest.php index b1208d1..711f0ee 100644 --- a/tests/ComposerFileStorageTest.php +++ b/tests/ComposerFileStorageTest.php @@ -50,23 +50,40 @@ public function testBasePath(): void $this->assertSame($expectedPath, ComposerFileStorage::basePath($config)); } + /** + * @covers ::escapeUrl + */ + public function testEscapeUrl(): void + { + // Ensure that two very similar URLs are converted into unique, but + // readable, directory names. + $url1 = ComposerFileStorage::escapeUrl('https://site.coop/info/packages'); + $url2 = ComposerFileStorage::escapeUrl('https://site.coop.info/packages'); + + $this->assertNotSame($url1, $url2); + $this->assertMatchesRegularExpression('/^https---site\.coop-info-packages-[a-z0-9]{8}$/', $url1); + $this->assertMatchesRegularExpression('/^https---site\.coop\.info-packages-[a-z0-9]{8}$/', $url2); + } + /** * @covers ::__construct * @covers ::create * * @depends testBasePath + * @depends testEscapeUrl */ public function testCreate(): void { + $url = 'https://example.net/packages'; $config = new Config(); $basePath = implode(DIRECTORY_SEPARATOR, [ ComposerFileStorage::basePath($config), - 'https---example.net-packages', + ComposerFileStorage::escapeUrl($url), ]); $this->assertDirectoryDoesNotExist($basePath); - ComposerFileStorage::create('https://example.net/packages', $config); + ComposerFileStorage::create($url, $config); $this->assertDirectoryExists($basePath); } }