From 95b9b54f0cbe1cd3327dc420721160c9c4cfc385 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 26 Sep 2024 13:36:25 +0200 Subject: [PATCH] JSON schema updates (#12123) * Add composer-lock-schema, update composer-repository-schema with new properties, add lock schema validation in diagnose Fixes #7823 * Add ref to composer.json schema in the lock one --- res/composer-lock-schema.json | 101 +++++++++++++++++++ res/composer-repository-schema.json | 120 ++++++++++++++++++++--- res/composer-schema.json | 2 +- src/Composer/Command/DiagnoseCommand.php | 29 ++++++ src/Composer/Json/JsonFile.php | 10 +- src/Composer/Package/Locker.php | 8 ++ 6 files changed, 254 insertions(+), 16 deletions(-) create mode 100644 res/composer-lock-schema.json diff --git a/res/composer-lock-schema.json b/res/composer-lock-schema.json new file mode 100644 index 000000000..b1ef31c2b --- /dev/null +++ b/res/composer-lock-schema.json @@ -0,0 +1,101 @@ +{ + "$schema": "https://json-schema.org/draft-04/schema#", + "title": "Composer Lock File", + "type": "object", + "required": [ "content-hash", "packages", "packages-dev" ], + "additionalProperties": true, + "properties": { + "_readme": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Informational text for humans reading the file" + }, + "content-hash": { + "type": "string", + "description": "Hash of all relevant properties of the composer.json that was used to create this lock file." + }, + "packages": { + "type": "array", + "description": "An array of packages that are required.", + "items": { + "$ref": "./composer-schema.json", + "required": ["name", "version"] + } + }, + "packages-dev": { + "type": "array", + "description": "An array of packages that are required in require-dev.", + "items": { + "$ref": "./composer-schema.json" + } + }, + "aliases": { + "type": "array", + "description": "Inline aliases defined in the root package.", + "items": { + "type": "object", + "required": [ "package", "version", "alias", "alias_normalized" ], + "properties": { + "package": { + "type": "string" + }, + "version": { + "type": "string" + }, + "alias": { + "type": "string" + }, + "alias_normalized": { + "type": "string" + } + } + } + }, + "minimum-stability": { + "type": "string", + "description": "The minimum-stability used to generate this lock file." + }, + "stability-flags": { + "type": "object", + "description": "Root package stability flags changing the minimum-stability for specific packages.", + "additionalProperties": { + "type": "integer" + } + }, + "prefer-stable": { + "type": "boolean", + "description": "Whether the --prefer-stable flag was used when building this lock file." + }, + "prefer-lowest": { + "type": "boolean", + "description": "Whether the --prefer-lowest flag was used when building this lock file." + }, + "platform": { + "type": "object", + "description": "Platform requirements of the root package.", + "additionalProperties": { + "type": "string" + } + }, + "platform-dev": { + "type": "object", + "description": "Platform dev-requirements of the root package.", + "additionalProperties": { + "type": "string" + } + }, + "platform-overrides": { + "type": "object", + "description": "Platform config overrides of the root package.", + "additionalProperties": { + "type": "string" + } + }, + "plugin-api-version": { + "type": "string", + "description": "The composer-plugin-api version that was used to generate this lock file." + } + } +} diff --git a/res/composer-repository-schema.json b/res/composer-repository-schema.json index adcc299d6..223f63abf 100644 --- a/res/composer-repository-schema.json +++ b/res/composer-repository-schema.json @@ -1,11 +1,12 @@ { "$schema": "https://json-schema.org/draft-04/schema#", - "description": "A representation of packages metadata.", + "title": "Composer Package Repository", "type": "object", "oneOf": [ { "required": [ "packages" ] }, { "required": [ "providers" ] }, - { "required": [ "provider-includes", "providers-url" ] } + { "required": [ "provider-includes", "providers-url" ] }, + { "required": [ "metadata-url" ] } ], "properties": { "packages": { @@ -13,19 +14,48 @@ "description": "A hashmap of package names in the form of /.", "additionalProperties": { "$ref": "#/definitions/versions" } }, - "providers-url": { + "metadata-url": { "type": "string", - "description": "Endpoint to retrieve provider data from, e.g. '/p/%package%$%hash%.json'." + "description": "Endpoint to retrieve package metadata data from, in Composer v2 format, e.g. '/p2/%package%.json'." }, - "provider-includes": { - "type": "object", - "description": "A hashmap of provider listings.", - "additionalProperties": { "$ref": "#/definitions/provider" } + "available-packages": { + "type": "array", + "items": { + "type": "string" + }, + "description": "If your repository only has a small number of packages, and you want to avoid serving many 404s, specify all the package names that your repository contains here." }, - "providers": { - "type": "object", - "description": "A hashmap of package names in the form of /.", - "additionalProperties": { "$ref": "#/definitions/provider" } + "available-package-patterns": { + "type": "array", + "items": { + "type": "string" + }, + "description": "If your repository only has a small number of packages, and you want to avoid serving many 404s, specify package name patterns containing wildcards (*) that your repository contains here." + }, + "security-advisories": { + "type": "array", + "items": { + "type": "object", + "required": ["metadata", "api-url"], + "properties": { + "metadata": { + "type": "boolean", + "description": "Whether metadata files contain security advisory data or whether it should always be queried using the API URL." + }, + "api-url": { + "type": "string", + "description": "Endpoint to call to retrieve security advisories data." + } + } + } + }, + "metadata-changes-url": { + "type": "string", + "description": "Endpoint to retrieve package metadata updates from. This should receive a timestamp since last call to be able to return new changes. e.g. '/metadata/changes.json'." + }, + "providers-api": { + "type": "string", + "description": "Endpoint to retrieve package names providing a given name from, e.g. '/providers/%package%.json'." }, "notify-batch": { "type": "string", @@ -35,9 +65,73 @@ "type": "string", "description": "Endpoint that provides search capabilities, e.g. '/search.json?q=%query%&type=%type%'." }, + "list": { + "type": "string", + "description": "Endpoint that provides a full list of packages present in the repository. It should accept an optional `?filter=xx` query param, which can contain `*` as wildcards matching any substring. e.g. '/list.json'." + }, + "warnings": { + "type": "array", + "items": { + "type": "object", + "required": ["message", "versions"], + "properties": { + "message": { + "type": "string", + "description": "A message that will be output by Composer as a warning when this source is consulted." + }, + "versions": { + "type": "string", + "description": "A version constraint to limit to which Composer versions the warning should be shown." + } + } + } + }, + "infos": { + "type": "array", + "items": { + "type": "object", + "required": ["message", "versions"], + "properties": { + "message": { + "type": "string", + "description": "A message that will be output by Composer as info when this source is consulted." + }, + "versions": { + "type": "string", + "description": "A version constraint to limit to which Composer versions the info should be shown." + } + } + } + }, + "providers-url": { + "type": "string", + "description": "DEPRECATED: Endpoint to retrieve provider data from, e.g. '/p/%package%$%hash%.json'." + }, + "provider-includes": { + "type": "object", + "description": "DEPRECATED: A hashmap of provider listings.", + "additionalProperties": { "$ref": "#/definitions/provider" } + }, + "providers": { + "type": "object", + "description": "DEPRECATED: A hashmap of package names in the form of /.", + "additionalProperties": { "$ref": "#/definitions/provider" } + }, "warning": { "type": "string", - "description": "A message that will be output by Composer as a warning when this source is consulted." + "description": "DEPRECATED: A message that will be output by Composer as a warning when this source is consulted." + }, + "warning-versions": { + "type": "string", + "description": "DEPRECATED: A version constraint to limit to which Composer versions the warning should be shown." + }, + "info": { + "type": "string", + "description": "DEPRECATED: A message that will be output by Composer as a info when this source is consulted." + }, + "info-versions": { + "type": "string", + "description": "DEPRECATED: A version constraint to limit to which Composer versions the info should be shown." } }, "definitions": { diff --git a/res/composer-schema.json b/res/composer-schema.json index ba62d21a4..9f76bc4b7 100644 --- a/res/composer-schema.json +++ b/res/composer-schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft-04/schema#", - "title": "Package", + "title": "Composer Package", "type": "object", "properties": { "name": { diff --git a/src/Composer/Command/DiagnoseCommand.php b/src/Composer/Command/DiagnoseCommand.php index cbd4a7c60..d06ad4497 100644 --- a/src/Composer/Command/DiagnoseCommand.php +++ b/src/Composer/Command/DiagnoseCommand.php @@ -19,6 +19,8 @@ use Composer\Config; use Composer\Downloader\TransportException; use Composer\IO\BufferIO; use Composer\Json\JsonFile; +use Composer\Json\JsonValidationException; +use Composer\Package\Locker; use Composer\Package\RootPackage; use Composer\Package\Version\VersionParser; use Composer\Pcre\Preg; @@ -89,6 +91,12 @@ EOT $io->write('Checking composer.json: ', false); $this->outputResult($this->checkComposerSchema()); + + if ($composer->getLocker()->isLocked()) { + $io->write('Checking composer.lock: ', false); + $this->outputResult($this->checkComposerLockSchema($composer->getLocker())); + } + $this->process = $composer->getLoop()->getProcessExecutor() ?? new ProcessExecutor($io); } else { $this->process = new ProcessExecutor($io); @@ -267,6 +275,27 @@ EOT return true; } + /** + * @return string|true + */ + private function checkComposerLockSchema(Locker $locker) + { + $json = $locker->getJsonFile(); + + try { + $json->validateSchema(JsonFile::LOCK_SCHEMA); + } catch (JsonValidationException $e) { + $output = ''; + foreach ($e->getErrors() as $error) { + $output .= ''.$error.''.PHP_EOL; + } + + return trim($output); + } + + return true; + } + private function checkGit(): string { if (!function_exists('proc_open')) { diff --git a/src/Composer/Json/JsonFile.php b/src/Composer/Json/JsonFile.php index df4067e26..02ce5196c 100644 --- a/src/Composer/Json/JsonFile.php +++ b/src/Composer/Json/JsonFile.php @@ -32,6 +32,7 @@ class JsonFile public const LAX_SCHEMA = 1; public const STRICT_SCHEMA = 2; public const AUTH_SCHEMA = 3; + public const LOCK_SCHEMA = 4; /** @deprecated Use \JSON_UNESCAPED_SLASHES */ public const JSON_UNESCAPED_SLASHES = 64; @@ -41,6 +42,7 @@ class JsonFile public const JSON_UNESCAPED_UNICODE = 256; public const COMPOSER_SCHEMA_PATH = __DIR__ . '/../../../res/composer-schema.json'; + public const LOCK_SCHEMA_PATH = __DIR__ . '/../../../res/composer-lock-schema.json'; public const INDENT_DEFAULT = ' '; @@ -228,8 +230,12 @@ class JsonFile { $isComposerSchemaFile = false; if (null === $schemaFile) { - $isComposerSchemaFile = true; - $schemaFile = self::COMPOSER_SCHEMA_PATH; + if ($schema === self::LOCK_SCHEMA) { + $schemaFile = self::LOCK_SCHEMA_PATH; + } else { + $isComposerSchemaFile = true; + $schemaFile = self::COMPOSER_SCHEMA_PATH; + } } // Prepend with file:// only when not using a special schema already (e.g. in the phar) diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index dc33a1123..5b5ec466c 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -73,6 +73,14 @@ class Locker $this->process = $process ?? new ProcessExecutor($io); } + /** + * @internal + */ + public function getJsonFile(): JsonFile + { + return $this->lockFile; + } + /** * Returns the md5 hash of the sorted content of the composer file. *