mirror of
https://github.com/crazywhalecc/static-php-cli.git
synced 2026-03-19 13:24:51 +08:00
377 lines
16 KiB
PHP
377 lines
16 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace StaticPHP\Config;
|
|
|
|
use StaticPHP\Exception\ValidationException;
|
|
|
|
class ConfigValidator
|
|
{
|
|
/**
|
|
* Global field type definitions
|
|
* Maps field names to their expected types and validation rules
|
|
* Note: This only includes fields used in config files (source.json, lib.json, ext.json, pkg.json, pre-built.json)
|
|
*/
|
|
public const array PACKAGE_FIELD_TYPES = [
|
|
// package fields
|
|
'type' => ConfigType::STRING,
|
|
'depends' => ConfigType::LIST_ARRAY, // @
|
|
'suggests' => ConfigType::LIST_ARRAY, // @
|
|
'artifact' => ConfigType::STRING,
|
|
'license' => [ConfigType::class, 'validateLicenseField'],
|
|
'lang' => ConfigType::STRING,
|
|
'frameworks' => ConfigType::LIST_ARRAY, // @
|
|
|
|
// php-extension type fields
|
|
'php-extension' => ConfigType::ASSOC_ARRAY,
|
|
'zend-extension' => ConfigType::BOOL,
|
|
'support' => ConfigType::ASSOC_ARRAY,
|
|
'arg-type' => ConfigType::STRING,
|
|
'build-shared' => ConfigType::BOOL,
|
|
'build-static' => ConfigType::BOOL,
|
|
'build-with-php' => ConfigType::BOOL,
|
|
'notes' => ConfigType::BOOL,
|
|
|
|
// library and target fields
|
|
'headers' => ConfigType::LIST_ARRAY, // @
|
|
'static-libs' => ConfigType::LIST_ARRAY, // @
|
|
'pkg-configs' => ConfigType::LIST_ARRAY,
|
|
'static-bins' => ConfigType::LIST_ARRAY, // @
|
|
];
|
|
|
|
public const array PACKAGE_FIELDS = [
|
|
'type' => true,
|
|
'depends' => false, // @
|
|
'suggests' => false, // @
|
|
'artifact' => false,
|
|
'license' => false,
|
|
'lang' => false,
|
|
'frameworks' => false, // @
|
|
|
|
// php-extension type fields
|
|
'php-extension' => false,
|
|
|
|
// library and target fields
|
|
'headers' => false, // @
|
|
'static-libs' => false, // @
|
|
'pkg-configs' => false,
|
|
'static-bins' => false, // @
|
|
];
|
|
|
|
public const array SUFFIX_ALLOWED_FIELDS = [
|
|
'depends',
|
|
'suggests',
|
|
'headers',
|
|
'static-libs',
|
|
'static-bins',
|
|
];
|
|
|
|
public const array PHP_EXTENSION_FIELDS = [
|
|
'zend-extension' => false,
|
|
'support' => false,
|
|
'arg-type' => false, // @
|
|
'build-shared' => false,
|
|
'build-static' => false,
|
|
'build-with-php' => false,
|
|
'notes' => false,
|
|
];
|
|
|
|
public const array ARTIFACT_TYPE_FIELDS = [ // [required_fields, optional_fields]
|
|
'filelist' => [['url', 'regex'], ['extract']],
|
|
'git' => [['url', 'rev'], ['extract', 'submodules']],
|
|
'ghtagtar' => [['repo'], ['extract', 'prefer-stable', 'match']],
|
|
'ghtar' => [['repo'], ['extract', 'prefer-stable', 'match']],
|
|
'ghrel' => [['repo', 'match'], ['extract', 'prefer-stable']],
|
|
'url' => [['url'], ['filename', 'extract', 'version']],
|
|
'bitbuckettag' => [['repo'], ['extract']],
|
|
'local' => [['dirname'], ['extract']],
|
|
'pie' => [['repo'], ['extract']],
|
|
'php-release' => [[], ['extract']],
|
|
'custom' => [[], ['func']],
|
|
];
|
|
|
|
/**
|
|
* Validate and standardize artifacts configuration data.
|
|
*
|
|
* @param string $config_file_name Name of the configuration file (for error messages)
|
|
* @param mixed $data The configuration data to validate
|
|
*/
|
|
public static function validateAndLintArtifacts(string $config_file_name, mixed &$data): void
|
|
{
|
|
if (!is_array($data)) {
|
|
throw new ValidationException("{$config_file_name} is broken");
|
|
}
|
|
foreach ($data as $name => $artifact) {
|
|
foreach ($artifact as $k => $v) {
|
|
// check source field
|
|
if ($k === 'source' || $k === 'source-mirror') {
|
|
// source === custom is allowed
|
|
if ($v === 'custom') {
|
|
continue;
|
|
}
|
|
// expand string to url type (start with http:// or https://)
|
|
if (is_string($v) && (str_starts_with($v, 'http://') || str_starts_with($v, 'https://'))) {
|
|
$data[$name][$k] = [
|
|
'type' => 'url',
|
|
'url' => $v,
|
|
];
|
|
continue;
|
|
}
|
|
// source: object with type field
|
|
if (is_assoc_array($v)) {
|
|
self::validateArtifactObjectField($name, $v);
|
|
}
|
|
continue;
|
|
}
|
|
// check binary field
|
|
if ($k === 'binary') {
|
|
// binary === custom is allowed
|
|
if ($v === 'custom') {
|
|
$data[$name][$k] = [
|
|
'linux-x86_64' => ['type' => 'custom'],
|
|
'linux-aarch64' => ['type' => 'custom'],
|
|
'windows-x86_64' => ['type' => 'custom'],
|
|
'macos-x86_64' => ['type' => 'custom'],
|
|
'macos-aarch64' => ['type' => 'custom'],
|
|
];
|
|
continue;
|
|
}
|
|
if ($v === 'hosted') {
|
|
$data[$name][$k] = [
|
|
'linux-x86_64' => ['type' => 'hosted'],
|
|
'linux-aarch64' => ['type' => 'hosted'],
|
|
'windows-x86_64' => ['type' => 'hosted'],
|
|
'macos-x86_64' => ['type' => 'hosted'],
|
|
'macos-aarch64' => ['type' => 'hosted'],
|
|
];
|
|
continue;
|
|
}
|
|
if (is_assoc_array($v)) {
|
|
foreach ($v as $platform => $v_obj) {
|
|
self::validatePlatformString($platform);
|
|
// expand string to url type (start with http:// or https://)
|
|
if (is_string($v_obj) && (str_starts_with($v_obj, 'http://') || str_starts_with($v_obj, 'https://'))) {
|
|
$data[$name][$k][$platform] = [
|
|
'type' => 'url',
|
|
'url' => $v_obj,
|
|
];
|
|
continue;
|
|
}
|
|
// binary: object with type field
|
|
if (is_assoc_array($v_obj)) {
|
|
self::validateArtifactObjectField("{$name}::{$platform}", $v_obj);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate packages configuration data.
|
|
*
|
|
* @param string $config_file_name Name of the configuration file (for error messages)
|
|
* @param mixed $data The configuration data to validate
|
|
*/
|
|
public static function validateAndLintPackages(string $config_file_name, mixed &$data): void
|
|
{
|
|
if (!is_array($data)) {
|
|
throw new ValidationException("{$config_file_name} is broken");
|
|
}
|
|
foreach ($data as $name => $pkg) {
|
|
if (!is_assoc_array($pkg)) {
|
|
throw new ValidationException("Package [{$name}] in {$config_file_name} is not a valid associative array");
|
|
}
|
|
// check if package has valid type
|
|
if (!isset($pkg['type']) || !in_array($pkg['type'], ConfigType::PACKAGE_TYPES)) {
|
|
throw new ValidationException("Package [{$name}] in {$config_file_name} has invalid or missing 'type' field");
|
|
}
|
|
|
|
// validate basic fields using unified method
|
|
self::validatePackageFields($name, $pkg);
|
|
|
|
// validate list of suffix-allowed fields
|
|
$suffixes = ['', '@windows', '@unix', '@macos', '@linux'];
|
|
$fields = self::SUFFIX_ALLOWED_FIELDS;
|
|
self::validateSuffixAllowedFields($name, $pkg, $fields, $suffixes);
|
|
|
|
// check if "library|target" package has artifact field for target and library types
|
|
if (in_array($pkg['type'], ['target', 'library']) && !isset($pkg['artifact'])) {
|
|
throw new ValidationException("Package [{$name}] in {$config_file_name} of type '{$pkg['type']}' must have an 'artifact' field");
|
|
}
|
|
|
|
// check if "php-extension" package has php-extension specific fields and validate inside
|
|
if ($pkg['type'] === 'php-extension') {
|
|
self::validatePhpExtensionFields($name, $pkg);
|
|
}
|
|
|
|
// check for unknown fields
|
|
self::validateNoInvalidFields('package', $name, $pkg, array_keys(self::PACKAGE_FIELD_TYPES));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate platform string format.
|
|
*
|
|
* @param string $platform Platform string, like windows-x86_64
|
|
*/
|
|
public static function validatePlatformString(string $platform): void
|
|
{
|
|
$valid_platforms = ['windows', 'linux', 'macos'];
|
|
$valid_arch = ['x86_64', 'aarch64'];
|
|
$parts = explode('-', $platform);
|
|
if (count($parts) !== 2) {
|
|
throw new ValidationException("Invalid platform format '{$platform}', expected format 'os-arch'");
|
|
}
|
|
[$os, $arch] = $parts;
|
|
if (!in_array($os, $valid_platforms)) {
|
|
throw new ValidationException("Invalid platform OS '{$os}' in platform '{$platform}'");
|
|
}
|
|
if (!in_array($arch, $valid_arch)) {
|
|
throw new ValidationException("Invalid platform architecture '{$arch}' in platform '{$platform}'");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate an artifact download object field.
|
|
*
|
|
* @param string $item_name Artifact name (for error messages)
|
|
* @param array $data Artifact source object data
|
|
*/
|
|
private static function validateArtifactObjectField(string $item_name, array $data): void
|
|
{
|
|
if (!isset($data['type']) || !is_string($data['type'])) {
|
|
throw new ValidationException("Artifact source object must have a valid 'type' field");
|
|
}
|
|
$type = $data['type'];
|
|
if (!isset(self::ARTIFACT_TYPE_FIELDS[$type])) {
|
|
throw new ValidationException("Artifact source object has unknown type '{$type}'");
|
|
}
|
|
[$required_fields, $optional_fields] = self::ARTIFACT_TYPE_FIELDS[$type];
|
|
// check required fields
|
|
foreach ($required_fields as $field) {
|
|
if (!isset($data[$field])) {
|
|
throw new ValidationException("Artifact source object of type '{$type}' must have required field '{$field}'");
|
|
}
|
|
}
|
|
// check for unknown fields
|
|
$allowed_fields = array_merge(['type'], $required_fields, $optional_fields);
|
|
self::validateNoInvalidFields('artifact object', $item_name, $data, $allowed_fields);
|
|
}
|
|
|
|
/**
|
|
* Unified method to validate config fields based on field definitions
|
|
*
|
|
* @param string $package_name Package name
|
|
* @param mixed $pkg The package configuration array
|
|
*/
|
|
private static function validatePackageFields(string $package_name, mixed $pkg): void
|
|
{
|
|
foreach (self::PACKAGE_FIELDS as $field => $required) {
|
|
if ($required && !isset($pkg[$field])) {
|
|
throw new ValidationException("Package {$package_name} must have [{$field}] field");
|
|
}
|
|
|
|
if (isset($pkg[$field])) {
|
|
self::validatePackageFieldType($field, $pkg[$field], $package_name);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate a field based on its global type definition
|
|
*
|
|
* @param string $field Field name
|
|
* @param mixed $value Field value
|
|
* @param string $package_name Package name (for error messages)
|
|
*/
|
|
private static function validatePackageFieldType(string $field, mixed $value, string $package_name): void
|
|
{
|
|
// Check if field exists in FIELD_TYPES
|
|
if (!isset(self::PACKAGE_FIELD_TYPES[$field])) {
|
|
// Try to strip suffix and check base field name
|
|
$suffixes = ['@windows', '@unix', '@macos', '@linux'];
|
|
$base_field = $field;
|
|
foreach ($suffixes as $suffix) {
|
|
if (str_ends_with($field, $suffix)) {
|
|
$base_field = substr($field, 0, -strlen($suffix));
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!isset(self::PACKAGE_FIELD_TYPES[$base_field])) {
|
|
// Unknown field is not allowed - strict validation
|
|
throw new ValidationException("Package {$package_name} has unknown field [{$field}]");
|
|
}
|
|
|
|
// Use base field type for validation
|
|
$expected_type = self::PACKAGE_FIELD_TYPES[$base_field];
|
|
} else {
|
|
$expected_type = self::PACKAGE_FIELD_TYPES[$field];
|
|
}
|
|
|
|
match ($expected_type) {
|
|
ConfigType::STRING => is_string($value) ?: throw new ValidationException("Package {$package_name} [{$field}] must be string"),
|
|
ConfigType::BOOL => is_bool($value) ?: throw new ValidationException("Package {$package_name} [{$field}] must be boolean"),
|
|
ConfigType::LIST_ARRAY => is_list_array($value) ?: throw new ValidationException("Package {$package_name} [{$field}] must be a list"),
|
|
ConfigType::ASSOC_ARRAY => is_assoc_array($value) ?: throw new ValidationException("Package {$package_name} [{$field}] must be an object"),
|
|
default => $expected_type($value) ?: throw new ValidationException("Package {$package_name} [{$field}] has invalid type specification"),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Validate that fields with suffixes are list arrays
|
|
*/
|
|
private static function validateSuffixAllowedFields(int|string $name, mixed $item, array $fields, array $suffixes): void
|
|
{
|
|
foreach ($fields as $field) {
|
|
foreach ($suffixes as $suffix) {
|
|
$key = $field . $suffix;
|
|
if (isset($item[$key])) {
|
|
self::validatePackageFieldType($key, $item[$key], $name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate php-extension specific fields for php-extension package
|
|
*/
|
|
private static function validatePhpExtensionFields(int|string $name, mixed $pkg): void
|
|
{
|
|
if (!isset($pkg['php-extension'])) {
|
|
return;
|
|
}
|
|
if (!is_assoc_array($pkg['php-extension'])) {
|
|
throw new ValidationException("Package {$name} [php-extension] must be an object");
|
|
}
|
|
foreach (self::PHP_EXTENSION_FIELDS as $field => $required) {
|
|
if (isset($pkg['php-extension'][$field])) {
|
|
self::validatePackageFieldType($field, $pkg['php-extension'][$field], $name);
|
|
}
|
|
}
|
|
// check for unknown fields in php-extension
|
|
self::validateNoInvalidFields('php-extension', $name, $pkg['php-extension'], array_keys(self::PHP_EXTENSION_FIELDS));
|
|
}
|
|
|
|
private static function validateNoInvalidFields(string $config_type, int|string $item_name, mixed $item_content, array $allowed_fields): void
|
|
{
|
|
foreach ($item_content as $k => $v) {
|
|
// remove suffixes for checking
|
|
$base_k = $k;
|
|
$suffixes = ['@windows', '@unix', '@macos', '@linux'];
|
|
foreach ($suffixes as $suffix) {
|
|
if (str_ends_with($k, $suffix)) {
|
|
$base_k = substr($k, 0, -strlen($suffix));
|
|
break;
|
|
}
|
|
}
|
|
if (!in_array($base_k, $allowed_fields)) {
|
|
throw new ValidationException("{$config_type} [{$item_name}] has invalid field [{$base_k}]");
|
|
}
|
|
}
|
|
}
|
|
}
|