diff --git a/.gitignore b/.gitignore index f357fd7..12ba1de 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ -/vendor/ +/build/ /tools/ -/composer.lock +/vendor/ /.cs-check.json -/build +/.phpunit.result.cache +/composer.lock diff --git a/src/DotGit.php b/src/DotGit.php index e7c45d0..e6811c8 100644 --- a/src/DotGit.php +++ b/src/DotGit.php @@ -57,15 +57,17 @@ private function __construct(string $pathToDotGit) $this->gitDir = $pathToDotGit; return; } - // additional worktree with a .git file referencing the original .git directory + // Additional worktree with a .git file referencing the original .git directory. // The referenced path is not required to be named '.git'; a worktree of a // bare repository, or of one created with --separate-git-dir, points at a // directory with any name. is_dir() below is the actual validation. + // This code aligns with the git source code in 'setup.c'. if (is_file($pathToDotGit)) { $dotGitContent = (string) file_get_contents($pathToDotGit); - $match = []; - preg_match('#^gitdir:\s*(?.+)$#m', $dotGitContent, $match); - $dir = rtrim($match['gitdir'] ?? ''); + if (!str_starts_with($dotGitContent, 'gitdir: ')) { + throw new RuntimeException('invalid .git file'); + } + $dir = rtrim(substr($dotGitContent, 8), "\r\n"); if (is_dir($dir)) { $this->gitDir = $dir; $this->isAdditionalWorktree = true; diff --git a/tests/unit/DotGitTest.php b/tests/unit/DotGitTest.php deleted file mode 100644 index fd41748..0000000 --- a/tests/unit/DotGitTest.php +++ /dev/null @@ -1,22 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace CaptainHook\HookInstaller; - -use PHPUnit\Framework\TestCase; - -class DotGitTest extends Testcase -{ - public function testDummy(): void - { - $this->assertTrue(true); - } -} diff --git a/tests/unit/GitDirParseTest.php b/tests/unit/GitDirParseTest.php new file mode 100644 index 0000000..d2c1c9d --- /dev/null +++ b/tests/unit/GitDirParseTest.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace CaptainHook\HookInstaller; + +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\TestCase; +use RuntimeException; + +class GitDirParseTest extends TestCase +{ + /** + * @link https://github.com/captainhook-git/hook-installer/pull/5#discussion_r3729235534 + */ + public static function gitDirFormatsInvalidData(): array + { + return [ + 'no space after colon' => [ + "gitdir:/foo/.bare\r", + ], + + 'invalid whitespace after colon' => [ + "gitdir:\n/foo/.bare\n\n", + ], + + 'text before gitdir' => [ + "header\ngitdir: /foo/.bare", + ], + ]; + } + + #[DataProvider('gitDirFormatsInvalidData')] + public function testParseGitDirInvalid(string $input): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('invalid .git file'); + + $path = $this->writeTempDotGit($input); + + DotGit::searchInPath($path); + } + + private function writeTempDotGit(string $content): string + { + $dir = $this->createTempDir(); + + if (file_put_contents("$dir/.git", $content) === false) { + throw new RuntimeException('Failed to create temporary git path'); + } + + return $dir; + } + + private function createTempDir(): string + { + $path = tempnam(sys_get_temp_dir(), 'captainhook-hook-installer-'); + + if ($path === false) { + throw new RuntimeException('Failed to create temporary path'); + } + + if (unlink($path) === false) { + throw new RuntimeException('Failed to delete temporary path'); + } + + if (mkdir($path, 0700) === false) { + throw new RuntimeException('Failed to create temporary directory'); + } + + return $path; + } +}