<?php
/**
 * Offline core manifest repository.
 *
 * @package LocalGuardScanner
 */

namespace LocalGuardScanner\Detection\Integrity;

use RuntimeException;

/**
 * Loads bounded SHA-256 manifests bundled with the plugin.
 */
final class CoreManifestRepository {
	/** @var string */
	private $directory;
	/** @var array<string,string> */
	private $signed_hashes;

	/**
	 * @param string               $directory     Manifest directory.
	 * @param array<string,string> $signed_hashes Catalog hashes from the signed rule manifest.
	 */
	public function __construct( $directory, array $signed_hashes ) {
		$this->directory     = rtrim( $directory, '/\\' );
		$this->signed_hashes = $signed_hashes;
	}

	/**
	 * @param string $version Exact WordPress version.
	 * @return array<string,string>|null
	 */
	public function for_version( $version ) {
		$index = $this->read_json( $this->directory . '/index.json', 262144 );
		if ( ! isset( $index['versions'][ $version ] ) || ! is_string( $index['versions'][ $version ] ) ) {
			return null;
		}
		$file = basename( $index['versions'][ $version ] );
		$data = $this->read_json( $this->directory . '/' . $file, 8 * 1024 * 1024 );
		if ( ! isset( $data['version'], $data['files'] ) || $data['version'] !== $version || ! is_array( $data['files'] ) ) {
			throw new RuntimeException( 'Core integrity manifest is malformed.' );
		}
		$files = array();
		foreach ( $data['files'] as $path => $hash ) {
			if ( ! is_string( $path ) || ! is_string( $hash ) || ! preg_match( '/^[a-f0-9]{64}$/', $hash ) ) {
				throw new RuntimeException( 'Core integrity entry is malformed.' );
			}
			$files[ $path ] = $hash;
		}
		return $files;
	}

	/**
	 * @param string $path      JSON path.
	 * @param int    $max_bytes Maximum bytes.
	 * @return array<string,mixed>
	 */
	private function read_json( $path, $max_bytes ) {
		if ( ! is_readable( $path ) || filesize( $path ) > $max_bytes ) {
			throw new RuntimeException( 'Core integrity catalog is unavailable.' );
		}
		$content      = file_get_contents( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
		$catalog_path = 'core/' . basename( $path );
		if ( false === $content || ! isset( $this->signed_hashes[ $catalog_path ] ) || ! hash_equals( $this->signed_hashes[ $catalog_path ], hash( 'sha256', $content ) ) ) {
			throw new RuntimeException( 'Core integrity catalog signature binding is invalid.' );
		}
		$data = json_decode( $content, true );
		if ( ! is_array( $data ) ) {
			throw new RuntimeException( 'Core integrity catalog contains invalid JSON.' );
		}
		return $data;
	}
}
