diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index 8b01dda6f..9a49d595b 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -91,7 +91,7 @@ EOT $allowlist = ['name', 'description', 'author', 'type', 'homepage', 'require', 'require-dev', 'stability', 'license', 'autoload']; $options = array_filter(array_intersect_key($input->getOptions(), array_flip($allowlist)), function ($val) { return $val !== null && $val !== []; }); - if (isset($options['name']) && !Preg::isMatch('{^[a-z0-9_.-]+/[a-z0-9_.-]+$}D', $options['name'])) { + if (isset($options['name']) && !Preg::isMatch('{^[a-z0-9]([_.-]?[a-z0-9]+)*\/[a-z0-9](([_.]|-{1,2})?[a-z0-9]+)*$}D', $options['name'])) { throw new \InvalidArgumentException( 'The package name '.$options['name'].' is invalid, it should be lowercase and have a vendor name, a forward slash, and a package name, matching: [a-z0-9_.-]+/[a-z0-9_.-]+' ); @@ -274,23 +274,24 @@ EOT $name = $input->getOption('name'); if (null === $name) { $name = basename($cwd); - $name = Preg::replace('{(?:([a-z])([A-Z])|([A-Z])([A-Z][a-z]))}', '\\1\\3-\\2\\4', $name); - $name = strtolower($name); + $name = $this->sanitizePackageNameComponent($name); + + $vendor = $name; if (!empty($_SERVER['COMPOSER_DEFAULT_VENDOR'])) { - $name = $_SERVER['COMPOSER_DEFAULT_VENDOR'] . '/' . $name; + $vendor = $_SERVER['COMPOSER_DEFAULT_VENDOR']; } elseif (isset($git['github.user'])) { - $name = $git['github.user'] . '/' . $name; + $vendor = $git['github.user']; } elseif (!empty($_SERVER['USERNAME'])) { - $name = $_SERVER['USERNAME'] . '/' . $name; + $vendor = $_SERVER['USERNAME']; } elseif (!empty($_SERVER['USER'])) { - $name = $_SERVER['USER'] . '/' . $name; + $vendor = $_SERVER['USER']; } elseif (get_current_user()) { - $name = get_current_user() . '/' . $name; - } else { - // package names must be in the format foo/bar - $name .= '/' . $name; + $vendor = get_current_user(); } - $name = strtolower($name); + + $vendor = $this->sanitizePackageNameComponent($vendor); + + $name = $vendor . '/' . $name; } $name = $io->askAndValidate( @@ -300,7 +301,7 @@ EOT return $name; } - if (!Preg::isMatch('{^[a-z0-9_.-]+/[a-z0-9_.-]+$}D', $value)) { + if (!Preg::isMatch('{^[a-z0-9]([_.-]?[a-z0-9]+)*\/[a-z0-9](([_.]|-{1,2})?[a-z0-9]+)*$}D', $value)) { throw new \InvalidArgumentException( 'The package name '.$value.' is invalid, it should be lowercase and have a vendor name, a forward slash, and a package name, matching: [a-z0-9_.-]+/[a-z0-9_.-]+' ); @@ -636,4 +637,14 @@ EOT return !empty($requires) || !empty($devRequires); } + + private function sanitizePackageNameComponent(string $name): string + { + $name = Preg::replace('{(?:([a-z])([A-Z])|([A-Z])([A-Z][a-z]))}', '\\1\\3-\\2\\4', $name); + $name = strtolower($name); + $name = Preg::replace('{^[_.-]+|[_.-]+$|[^a-z0-9_.-]}u', '', $name); + $name = Preg::replace('{([_.-]){2,}}u', '$1', $name); + + return $name; + } } diff --git a/tests/Composer/Test/Command/InitCommandTest.php b/tests/Composer/Test/Command/InitCommandTest.php index 3e50fbdb4..d2f1f4f2b 100644 --- a/tests/Composer/Test/Command/InitCommandTest.php +++ b/tests/Composer/Test/Command/InitCommandTest.php @@ -179,6 +179,31 @@ class InitCommandTest extends TestCase self::assertEquals($expected, $file->read()); } + public function testRunGuessNameFromDirSanitizesDir(): void + { + $dir = $this->initTempComposer(); + mkdir($dirName = '_foo_--bar__baz.--..qux__'); + chdir($dirName); + + $_SERVER['COMPOSER_DEFAULT_VENDOR'] = '.vendorName'; + + $appTester = $this->getApplicationTester(); + $appTester->setInputs(['', '', 'n', '', '', '', 'no', 'no', 'n', 'yes']); + $appTester->run(['command' => 'init']); + + self::assertSame(0, $appTester->getStatusCode()); + + $expected = [ + 'name' => 'vendor-name/foo-bar_baz.qux', + 'require' => [], + ]; + + $file = new JsonFile('./composer.json'); + self::assertEquals($expected, $file->read()); + + unset($_SERVER['COMPOSER_DEFAULT_VENDOR']); + } + public function testRunInvalidAuthorArgumentInvalidEmail(): void { $this->expectException(\InvalidArgumentException::class);