config = $config ?? new ParserConfig(); $this->ffi = $this->getFFI(); /** @phpstan-ignore-next-line */ $this->handle = $this->ffi->NewParser( $this->config->isNetworkDisabled(), $this->config->areExternalChecksDisabled(), $this->config->getBranch(), $this->config->getBaseURL(), $this->config->getHTTPTimeout() * 1_000_000_000, ); if ($this->handle == 0) { throw new ParserException('Failed to create parser'); } } public function __destruct() { if ($this->handle != 0) { /** @phpstan-ignore-next-line */ $this->ffi->FreeParser($this->handle); $this->handle = 0; } } /** * Parse publiccode.yml content * * @param string $content YAML content * @return PublicCodeV0 * @throws ParserException * @throws ValidationException */ public function parse(string $content): PublicCodeV0 { /** @phpstan-ignore-next-line */ $result = $this->ffi->ParseString($this->handle, $content); if ($result === null) { throw new ParserException('Failed to parse publiccode.yml content'); } return $this->processResult($result); } /** * Parse a publiccode.yml file * * @param string $filePath Path to publiccode.yml * @return PublicCodeV0 * @throws ParserException * @throws ValidationException */ public function parseFile(string $filePath): PublicCodeV0 { /** @phpstan-ignore-next-line */ $result = $this->ffi->ParseFile($this->handle, $filePath); if ($result === null) { throw new ParserException("Failed to parse $filePath"); } return $this->processResult($result); } /** * Helper function to validate publiccode.yml file, with no error details * * @param string $filePath * @return bool */ public function isValid(string $filePath): bool { try { $this->parseFile($filePath); return true; } catch (ParserException | ValidationException $e) { return false; } } /** * Get or create FFI instance * * @return FFI * @throws ParserException */ private function getFFI(): FFI { if (self::$ffiInstance !== null) { return self::$ffiInstance; } $libraryPath = $this->findLibrary(); $cdef = <<<'CDEF' typedef struct { char* Data; char* Error; int ErrorCount; char** Errors; int WarningCount; char** Warnings; } ParseResult; typedef uintptr_t ParserHandle; ParserHandle NewParser(bool disableNetwork, bool disableExternalChecks, const char* branch, const char* baseURL, int64_t httpTimeout); ParseResult* ParseString(ParserHandle handle, const char* content); ParseResult* ParseFile(ParserHandle handle, const char* uri); void FreeResult(ParseResult* result); void FreeParser(ParserHandle handle); CDEF; try { self::$ffiInstance = FFI::cdef($cdef, $libraryPath); return self::$ffiInstance; } catch (\FFI\Exception $e) { throw new ParserException( 'Failed to load publiccode-parser library: ' . $e->getMessage(), 0, $e, ); } } /** * Find the publiccode-parser shared library * * @return string * @throws ParserException */ private function findLibrary(): string { $os = $this->detectOS(); $arch = $this->detectArchitecture(); $ext = $os === 'darwin' ? 'dylib' : 'so'; $libraryName = "libpubliccode-parser-{$os}-{$arch}.{$ext}"; $possiblePaths = [ __DIR__ . '/', __DIR__ . '/../lib/', __DIR__ . '/../vendor/lib/', '/usr/local/lib/', '/usr/lib/', ]; foreach ($possiblePaths as $path) { $filePath = $path . $libraryName; if (file_exists($filePath)) { return $filePath; } } throw new ParserException( "libpubliccode-parser library not found for platform: {$os}-{$arch}. " . "Searched for: {$libraryName}", ); } /** * Detect operating system * * @return 'linux'|'darwin' * @throws ParserException */ private function detectOS(): string { $osFamily = PHP_OS_FAMILY; if ($osFamily === 'Linux') { return 'linux'; } if ($osFamily === 'Darwin') { return 'darwin'; } throw new ParserException("Unsupported operating system: {$osFamily}"); } /** * Detect CPU architecture * * @return 'amd64'|'arm64' * @throws ParserException */ private function detectArchitecture(): string { $machine = php_uname('m'); if (in_array($machine, ['x86_64', 'amd64', 'AMD64'], true)) { return 'amd64'; } if (in_array($machine, ['arm64', 'aarch64', 'ARM64'], true)) { return 'arm64'; } throw new ParserException("Unsupported architecture: {$machine}"); } /** * Process FFI result and convert to PublicCode object * * @param mixed $result FFI ParseResult pointer * @return PublicCodeV0 * @throws ValidationException * @throws ParserException */ private function processResult($result): PublicCodeV0 { [$warnings, $validationWarnings] = $this->parseValidationWarnings( $result->Warnings, $result->WarningCount, ); if ($result->ErrorCount > 0) { [$errors, $validationErrors] = $this->parseValidationErrors( $result->Errors, $result->ErrorCount, ); /** @phpstan-ignore-next-line */ $this->ffi->FreeResult($result); throw new ValidationException( message: implode("\n", $errors), errors: $errors, validationErrors: $validationErrors, warnings: $warnings, validationWarnings: $validationWarnings, ); } if ($result->Error !== null) { $message = FFI::string($result->Error); /** @phpstan-ignore-next-line */ $this->ffi->FreeResult($result); throw new ParserException($message); } if ($result->Data === null) { /** @phpstan-ignore-next-line */ $this->ffi->FreeResult($result); throw new ParserException('No data returned from parser'); } $jsonData = FFI::string($result->Data); /** @phpstan-ignore-next-line */ $this->ffi->FreeResult($result); $data = json_decode($jsonData, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new ParserException('Failed to decode JSON data: ' . json_last_error_msg()); } return new PublicCodeV0($data, $warnings, $validationWarnings); } /** * @param mixed $cArray FFI pointer to char** (may be null) * @return array{list, list} */ private function parseValidationErrors($cArray, int $count): array { $messages = []; $errors = []; if ($cArray === null || $count === 0) { return [$messages, $errors]; } for ($i = 0; $i < $count; $i++) { $payload = FFI::string($cArray[$i]); $data = json_decode($payload, true); if (is_array($data)) { $error = ValidationError::fromArray($data); $messages[] = (string) $error; } else { $error = ValidationError::fromString($payload); $messages[] = $payload; } $errors[] = $error; } return [$messages, $errors]; } /** * @param mixed $cArray FFI pointer to char** (may be null) * @return array{list, list} */ private function parseValidationWarnings($cArray, int $count): array { $messages = []; $warnings = []; if ($cArray === null || $count === 0) { return [$messages, $warnings]; } for ($i = 0; $i < $count; $i++) { $payload = FFI::string($cArray[$i]); $data = json_decode($payload, true); if (is_array($data)) { $warning = ValidationWarning::fromArray($data); $messages[] = (string) $warning; } else { $warning = ValidationWarning::fromString($payload); $messages[] = $payload; } $warnings[] = $warning; } return [$messages, $warnings]; } }