From 054fe38cb74a24d2e337bcf618df1cdcf70ec878 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 28 Jul 2025 14:57:29 +0800 Subject: [PATCH] Add full config validator for all configs --- src/SPC/util/ConfigValidator.php | 313 ++++++++++++++++++++++++++++--- 1 file changed, 283 insertions(+), 30 deletions(-) diff --git a/src/SPC/util/ConfigValidator.php b/src/SPC/util/ConfigValidator.php index b908efce..cfb9c2c2 100644 --- a/src/SPC/util/ConfigValidator.php +++ b/src/SPC/util/ConfigValidator.php @@ -20,33 +20,48 @@ class ConfigValidator public static function validateSource(array $data): void { foreach ($data as $name => $src) { - isset($src['type']) || throw new ValidationException("source {$name} must have prop: [type]"); - is_string($src['type']) || throw new ValidationException("source {$name} type prop must be string"); - in_array($src['type'], ['filelist', 'git', 'ghtagtar', 'ghtar', 'ghrel', 'url', 'custom']) || throw new ValidationException("source {$name} type [{$src['type']}] is invalid"); - switch ($src['type']) { - case 'filelist': - isset($src['url'], $src['regex']) || throw new ValidationException("source {$name} needs [url] and [regex] props"); - is_string($src['url']) && is_string($src['regex']) || throw new ValidationException("source {$name} [url] and [regex] must be string"); - break; - case 'git': - isset($src['url'], $src['rev']) || throw new ValidationException("source {$name} needs [url] and [rev] props"); - is_string($src['url']) && is_string($src['rev']) || throw new ValidationException("source {$name} [url] and [rev] must be string"); - is_string($src['path'] ?? '') || throw new ValidationException("source {$name} [path] must be string"); - break; - case 'ghtagtar': - case 'ghtar': - isset($src['repo']) || throw new ValidationException("source {$name} needs [repo] prop"); - is_string($src['repo']) || throw new ValidationException("source {$name} [repo] must be string"); - is_string($src['path'] ?? '') || throw new ValidationException("source {$name} [path] must be string"); - break; - case 'ghrel': - isset($src['repo'], $src['match']) || throw new ValidationException("source {$name} needs [repo] and [match] props"); - is_string($src['repo']) && is_string($src['match']) || throw new ValidationException("source {$name} [repo] and [match] must be string"); - break; - case 'url': - isset($src['url']) || throw new ValidationException("source {$name} needs [url] prop"); - is_string($src['url']) || throw new ValidationException("source {$name} [url] must be string"); - break; + // Validate basic source type configuration + self::validateSourceTypeConfig($src, $name, 'source'); + + // Check source-specific fields + // check if alt is valid + if (isset($src['alt'])) { + if (!is_assoc_array($src['alt']) && !is_bool($src['alt'])) { + throw new ValidationException("source {$name} alt must be object or boolean"); + } + if (is_assoc_array($src['alt'])) { + // validate alt source recursively + self::validateSource([$name . '_alt' => $src['alt']]); + } + } + + // check if provide-pre-built is boolean + if (isset($src['provide-pre-built']) && !is_bool($src['provide-pre-built'])) { + throw new ValidationException("source {$name} provide-pre-built must be boolean"); + } + + // check if prefer-stable is boolean + if (isset($src['prefer-stable']) && !is_bool($src['prefer-stable'])) { + throw new ValidationException("source {$name} prefer-stable must be boolean"); + } + + // check if license is valid + if (isset($src['license'])) { + if (!is_assoc_array($src['license'])) { + throw new ValidationException("source {$name} license must be object"); + } + if (!isset($src['license']['type'])) { + throw new ValidationException("source {$name} license must have type"); + } + if (!in_array($src['license']['type'], ['file', 'text'])) { + throw new ValidationException("source {$name} license type is invalid"); + } + if ($src['license']['type'] === 'file' && !isset($src['license']['path'])) { + throw new ValidationException("source {$name} license file must have path"); + } + if ($src['license']['type'] === 'text' && !isset($src['license']['text'])) { + throw new ValidationException("source {$name} license text must have text"); + } } } } @@ -78,7 +93,11 @@ class ConfigValidator if (isset($lib['source']) && !empty($source_data) && !isset($source_data[$lib['source']])) { throw new ValidationException("lib {$name} assigns an invalid source: {$lib['source']}"); } - // check if [lib-depends|lib-suggests|static-libs][-windows|-unix|-macos|-linux] are valid list array + // check if source is string + if (isset($lib['source']) && !is_string($lib['source'])) { + throw new ValidationException("lib {$name} source must be string"); + } + // check if [lib-depends|lib-suggests|static-libs|headers|bin][-windows|-unix|-macos|-linux] are valid list array $suffixes = ['', '-windows', '-unix', '-macos', '-linux']; foreach ($suffixes as $suffix) { if (isset($lib['lib-depends' . $suffix]) && !is_list_array($lib['lib-depends' . $suffix])) { @@ -93,6 +112,12 @@ class ConfigValidator if (isset($lib['pkg-configs' . $suffix]) && !is_list_array($lib['pkg-configs' . $suffix])) { throw new ValidationException("lib {$name} pkg-configs must be a list"); } + if (isset($lib['headers' . $suffix]) && !is_list_array($lib['headers' . $suffix])) { + throw new ValidationException("lib {$name} headers must be a list"); + } + if (isset($lib['bin' . $suffix]) && !is_list_array($lib['bin' . $suffix])) { + throw new ValidationException("lib {$name} bin must be a list"); + } } // check if frameworks is a list array if (isset($lib['frameworks']) && !is_list_array($lib['frameworks'])) { @@ -106,7 +131,65 @@ class ConfigValidator */ public static function validateExts(mixed $data): void { - is_array($data) || throw new ValidationException('ext.json is broken'); + if (!is_array($data)) { + throw new ValidationException('ext.json is broken'); + } + // check each extension + foreach ($data as $name => $ext) { + // check if ext is an assoc array + if (!is_assoc_array($ext)) { + throw new ValidationException("ext {$name} is not an object"); + } + // check if ext has valid type + if (!in_array($ext['type'] ?? '', ['builtin', 'external', 'addon', 'wip'])) { + throw new ValidationException("ext {$name} type is invalid"); + } + // check if external ext has source + if (($ext['type'] ?? '') === 'external' && !isset($ext['source'])) { + throw new ValidationException("ext {$name} does not assign any source"); + } + // check if source is string + if (isset($ext['source']) && !is_string($ext['source'])) { + throw new ValidationException("ext {$name} source must be string"); + } + // check if support is valid + if (isset($ext['support']) && !is_assoc_array($ext['support'])) { + throw new ValidationException("ext {$name} support must be an object"); + } + // check if notes is boolean + if (isset($ext['notes']) && !is_bool($ext['notes'])) { + throw new ValidationException("ext {$name} notes must be boolean"); + } + // check if [lib-depends|lib-suggests|ext-depends][-windows|-unix|-macos|-linux] are valid list array + $suffixes = ['', '-windows', '-unix', '-macos', '-linux']; + foreach ($suffixes as $suffix) { + if (isset($ext['lib-depends' . $suffix]) && !is_list_array($ext['lib-depends' . $suffix])) { + throw new ValidationException("ext {$name} lib-depends must be a list"); + } + if (isset($ext['lib-suggests' . $suffix]) && !is_list_array($ext['lib-suggests' . $suffix])) { + throw new ValidationException("ext {$name} lib-suggests must be a list"); + } + if (isset($ext['ext-depends' . $suffix]) && !is_list_array($ext['ext-depends' . $suffix])) { + throw new ValidationException("ext {$name} ext-depends must be a list"); + } + } + // check if arg-type is valid + if (isset($ext['arg-type'])) { + $valid_arg_types = ['enable', 'with', 'with-path', 'custom', 'none', 'enable-path']; + if (!in_array($ext['arg-type'], $valid_arg_types)) { + throw new ValidationException("ext {$name} arg-type is invalid"); + } + } + // check if arg-type with suffix is valid + foreach ($suffixes as $suffix) { + if (isset($ext['arg-type' . $suffix])) { + $valid_arg_types = ['enable', 'with', 'with-path', 'custom', 'none', 'enable-path']; + if (!in_array($ext['arg-type' . $suffix], $valid_arg_types)) { + throw new ValidationException("ext {$name} arg-type{$suffix} is invalid"); + } + } + } + } } /** @@ -114,7 +197,96 @@ class ConfigValidator */ public static function validatePkgs(mixed $data): void { - is_array($data) || throw new ValidationException('pkg.json is broken'); + if (!is_array($data)) { + throw new ValidationException('pkg.json is broken'); + } + // check each package + foreach ($data as $name => $pkg) { + // check if pkg is an assoc array + if (!is_assoc_array($pkg)) { + throw new ValidationException("pkg {$name} is not an object"); + } + + // Validate basic source type configuration (reuse from source validation) + self::validateSourceTypeConfig($pkg, $name, 'pkg'); + + // Check pkg-specific fields + // check if extract-files is valid + if (isset($pkg['extract-files'])) { + if (!is_assoc_array($pkg['extract-files'])) { + throw new ValidationException("pkg {$name} extract-files must be an object"); + } + // check each extract file mapping + foreach ($pkg['extract-files'] as $source => $target) { + if (!is_string($source) || !is_string($target)) { + throw new ValidationException("pkg {$name} extract-files mapping must be string to string"); + } + } + } + } + } + + /** + * Validate pre-built.json configuration + * + * @param mixed $data pre-built.json loaded data + * @throws ValidationException + */ + public static function validatePreBuilt(mixed $data): void + { + if (!is_array($data)) { + throw new ValidationException('pre-built.json is broken'); + } + + // Check required fields + if (!isset($data['repo'])) { + throw new ValidationException('pre-built.json must have [repo] field'); + } + if (!is_string($data['repo'])) { + throw new ValidationException('pre-built.json [repo] must be string'); + } + + // Check optional prefer-stable field + if (isset($data['prefer-stable']) && !is_bool($data['prefer-stable'])) { + throw new ValidationException('pre-built.json [prefer-stable] must be boolean'); + } + + // Check match pattern fields (at least one must exist) + $pattern_fields = ['match-pattern-linux', 'match-pattern-macos', 'match-pattern-windows']; + $has_pattern = false; + + foreach ($pattern_fields as $field) { + if (isset($data[$field])) { + $has_pattern = true; + if (!is_string($data[$field])) { + throw new ValidationException("pre-built.json [{$field}] must be string"); + } + // Validate pattern contains required placeholders + if (!str_contains($data[$field], '{name}')) { + throw new ValidationException("pre-built.json [{$field}] must contain {name} placeholder"); + } + if (!str_contains($data[$field], '{arch}')) { + throw new ValidationException("pre-built.json [{$field}] must contain {arch} placeholder"); + } + if (!str_contains($data[$field], '{os}')) { + throw new ValidationException("pre-built.json [{$field}] must contain {os} placeholder"); + } + + // Linux pattern should have libc-related placeholders + if ($field === 'match-pattern-linux') { + if (!str_contains($data[$field], '{libc}')) { + throw new ValidationException('pre-built.json [match-pattern-linux] must contain {libc} placeholder'); + } + if (!str_contains($data[$field], '{libcver}')) { + throw new ValidationException('pre-built.json [match-pattern-linux] must contain {libcver} placeholder'); + } + } + } + } + + if (!$has_pattern) { + throw new ValidationException('pre-built.json must have at least one match-pattern field'); + } } /** @@ -242,4 +414,85 @@ class ConfigValidator $craft['craft-options']['build'] ??= true; return $craft; } + + /** + * Validate source type configuration (shared between source.json and pkg.json) + * + * @param array $item The source/package item to validate + * @param string $name The name of the item for error messages + * @param string $config_type The type of config file ("source" or "pkg") + * @throws ValidationException + */ + private static function validateSourceTypeConfig(array $item, string $name, string $config_type): void + { + if (!isset($item['type'])) { + throw new ValidationException("{$config_type} {$name} must have prop: [type]"); + } + if (!is_string($item['type'])) { + throw new ValidationException("{$config_type} {$name} type prop must be string"); + } + if (!in_array($item['type'], ['filelist', 'git', 'ghtagtar', 'ghtar', 'ghrel', 'url', 'custom'])) { + throw new ValidationException("{$config_type} {$name} type [{$item['type']}] is invalid"); + } + + // Validate type-specific requirements + switch ($item['type']) { + case 'filelist': + if (!isset($item['url'], $item['regex'])) { + throw new ValidationException("{$config_type} {$name} needs [url] and [regex] props"); + } + if (!is_string($item['url']) || !is_string($item['regex'])) { + throw new ValidationException("{$config_type} {$name} [url] and [regex] must be string"); + } + break; + case 'git': + if (!isset($item['url'], $item['rev'])) { + throw new ValidationException("{$config_type} {$name} needs [url] and [rev] props"); + } + if (!is_string($item['url']) || !is_string($item['rev'])) { + throw new ValidationException("{$config_type} {$name} [url] and [rev] must be string"); + } + if (isset($item['path']) && !is_string($item['path'])) { + throw new ValidationException("{$config_type} {$name} [path] must be string"); + } + break; + case 'ghtagtar': + case 'ghtar': + if (!isset($item['repo'])) { + throw new ValidationException("{$config_type} {$name} needs [repo] prop"); + } + if (!is_string($item['repo'])) { + throw new ValidationException("{$config_type} {$name} [repo] must be string"); + } + if (isset($item['path']) && !is_string($item['path'])) { + throw new ValidationException("{$config_type} {$name} [path] must be string"); + } + break; + case 'ghrel': + if (!isset($item['repo'], $item['match'])) { + throw new ValidationException("{$config_type} {$name} needs [repo] and [match] props"); + } + if (!is_string($item['repo']) || !is_string($item['match'])) { + throw new ValidationException("{$config_type} {$name} [repo] and [match] must be string"); + } + break; + case 'url': + if (!isset($item['url'])) { + throw new ValidationException("{$config_type} {$name} needs [url] prop"); + } + if (!is_string($item['url'])) { + throw new ValidationException("{$config_type} {$name} [url] must be string"); + } + if (isset($item['filename']) && !is_string($item['filename'])) { + throw new ValidationException("{$config_type} {$name} [filename] must be string"); + } + if (isset($item['path']) && !is_string($item['path'])) { + throw new ValidationException("{$config_type} {$name} [path] must be string"); + } + break; + case 'custom': + // custom type has no specific requirements + break; + } + } }