<?php
/**
 * Safe serialized value reader.
 *
 * @package LocalGuardScanner
 */

namespace LocalGuardScanner\Detection\Database;

use RuntimeException;

/**
 * Decodes only scalar and array graphs without permitting PHP classes.
 */
final class SafeSerializedReader {
	/**
	 * @param string $value Potentially serialized value.
	 * @return mixed
	 */
	public function decode( string $value ) {
		if ( ! $this->looks_serialized( $value ) ) {
			return $value;
		}
		if ( preg_match( '/^(?:O|C):/s', $value ) ) {
			throw new RuntimeException( 'Serialized objects are not permitted.' );
		}
		$previous = set_error_handler( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler
			static function () {
				return true;
			}
		);
		try {
			$decoded = unserialize( $value, array( 'allowed_classes' => false ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
		} finally {
			restore_error_handler();
		}
		unset( $previous );
		if ( false === $decoded && 'b:0;' !== $value ) {
			throw new RuntimeException( 'Malformed serialized value.' );
		}
		if ( ! $this->is_safe_graph( $decoded ) ) {
			throw new RuntimeException( 'Serialized value contains unsupported types.' );
		}
		return $decoded;
	}

	/** @return bool */
	private function looks_serialized( string $value ): bool {
		return 'N;' === $value || 1 === preg_match( '/^[aObisdC]:/s', $value );
	}

	/**
	 * Check a decoded graph recursively.
	 *
	 * @param mixed $value Decoded value.
	 * @return bool
	 */
	private function is_safe_graph( $value ) {
		if ( is_null( $value ) || is_scalar( $value ) ) {
			return true;
		}
		if ( ! is_array( $value ) ) {
			return false;
		}
		foreach ( $value as $item ) {
			if ( ! $this->is_safe_graph( $item ) ) {
				return false;
			}
		}
		return true;
	}
}
