mode = $mode; $this->with_crc = $with_crc; if ( 'p' === $mode ) { $this->psha = new Sha256_Portable(); } else { $this->nsha = hash_init( 'sha256' ); $this->ncrc = $with_crc ? hash_init( 'crc32b' ) : null; } } /** * Whether PHP can serialize native hash contexts (PHP 8.0+). */ public static function native_pausable(): bool { if ( null === self::$native_ok ) { self::$native_ok = false; if ( PHP_VERSION_ID >= 80000 ) { try { $ctx = hash_init( 'crc32b' ); hash_update( $ctx, 'x' ); $copy = unserialize( serialize( $ctx ), array( 'allowed_classes' => array( 'HashContext' ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize, WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize self::$native_ok = $copy instanceof \HashContext && hash_final( $copy ) === hash( 'crc32b', 'x' ); } catch ( \Throwable $e ) { self::$native_ok = false; } } } return self::$native_ok; } /** * Whether some pausable mode exists on this server. */ public static function can_pause_any(): bool { return self::native_pausable() || PHP_INT_SIZE >= 8; } /** * Start a hash for a stream of $bytes that must be pausable unless it obviously * fits before $deadline. * * @param int $bytes Bytes still to hash. * @param float $deadline Request deadline (0 = none). * @param bool $with_crc Track CRC32 too. */ public static function start( int $bytes, float $deadline, bool $with_crc = true ): self { if ( self::native_pausable() ) { return new self( 'n', $with_crc ); } if ( PHP_INT_SIZE < 8 || $bytes <= self::SMALL || $deadline <= 0 ) { return new self( 'o', $with_crc ); } $left = $deadline - microtime( true ); if ( $left > 0 && $bytes <= (int) ( $left * self::NATIVE_RATE * 0.25 ) ) { return new self( 'o', $with_crc ); } return new self( 'p', $with_crc ); } /** * Restore a saved state. * * @param mixed $s Saved state. */ public static function import( $s ): ?self { if ( ! is_array( $s ) || ! isset( $s['m'] ) ) { return null; } $with_crc = ! empty( $s['c'] ); if ( 'n' === $s['m'] ) { if ( ! self::native_pausable() ) { return null; } $o = new self( 'n', $with_crc ); $o->nsha = unserialize( base64_decode( (string) ( $s['hs'] ?? '' ) ), array( 'allowed_classes' => array( 'HashContext' ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize, WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode if ( ! ( $o->nsha instanceof \HashContext ) ) { return null; } if ( $with_crc ) { $o->ncrc = unserialize( base64_decode( (string) ( $s['hc'] ?? '' ) ), array( 'allowed_classes' => array( 'HashContext' ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize, WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode if ( ! ( $o->ncrc instanceof \HashContext ) ) { return null; } } return $o; } if ( 'p' === $s['m'] && PHP_INT_SIZE >= 8 ) { $sha = Sha256_Portable::import( isset( $s['ps'] ) && is_array( $s['ps'] ) ? $s['ps'] : array() ); if ( null === $sha ) { return null; } $o = new self( 'p', $with_crc ); $o->psha = $sha; $o->crc = (int) ( $s['cr'] ?? 0 ) & 0xFFFFFFFF; return $o; } return null; } /** * Whether this hash can be saved and continued later. */ public function can_pause(): bool { return 'o' !== $this->mode; } /** * Mode letter (n, p or o). */ public function mode(): string { return $this->mode; } /** * Add data. * * @param string $data Bytes. */ public function update( string $data ): void { if ( '' === $data ) { return; } if ( 'p' === $this->mode ) { $this->psha->update( $data ); if ( $this->with_crc ) { $this->crc = self::crc32_combine( $this->crc, (int) hexdec( hash( 'crc32b', $data ) ), strlen( $data ) ); } return; } hash_update( $this->nsha, $data ); if ( $this->with_crc ) { hash_update( $this->ncrc, $data ); } } /** * Save state. * * @return array * @throws \LogicException In one-shot mode. */ public function export(): array { if ( 'p' === $this->mode ) { return array( 'm' => 'p', 'c' => $this->with_crc ? 1 : 0, 'ps' => $this->psha->export(), 'cr' => $this->crc, ); } if ( 'n' === $this->mode ) { return array( 'm' => 'n', 'c' => $this->with_crc ? 1 : 0, 'hs' => base64_encode( serialize( $this->nsha ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize, WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode 'hc' => $this->with_crc ? base64_encode( serialize( $this->ncrc ) ) : '', // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize, WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode ); } throw new \LogicException( 'One-shot hash cannot be paused.' ); } /** * Final SHA-256 hex. */ public function sha256(): string { return 'p' === $this->mode ? $this->psha->hex() : hash_final( $this->nsha ); } /** * Final CRC32 hex (same format as hash( 'crc32b' )). */ public function crc32b(): string { if ( ! $this->with_crc ) { return ''; } return 'p' === $this->mode ? sprintf( '%08x', $this->crc ) : hash_final( $this->ncrc ); } /** * Combine CRC32 values: crc(A . B) from crc(A), crc(B) and len(B). Port of zlib's crc32_combine(). * * @param int $crc1 CRC of the first part. * @param int $crc2 CRC of the second part. * @param int $len2 Length of the second part. */ public static function crc32_combine( int $crc1, int $crc2, int $len2 ): int { static $cache = array(); if ( $len2 <= 0 ) { return $crc1; } if ( ! isset( $cache[ $len2 ] ) ) { if ( count( $cache ) > 8 ) { $cache = array(); } // Operator matrix that advances a CRC over $len2 zero bytes. $cols = array(); for ( $i = 0; $i < 32; $i++ ) { $cols[ $i ] = self::zero_advance( 1 << $i, $len2 ); } $cache[ $len2 ] = $cols; } return ( self::times( $cache[ $len2 ], $crc1 ) ^ $crc2 ) & 0xFFFFFFFF; } /** * Advance a CRC register over $len zero bytes (zlib algorithm). * * @param int $crc CRC. * @param int $len Bytes. */ private static function zero_advance( int $crc, int $len ): int { $odd = array( 0xedb88320 ); $row = 1; for ( $n = 1; $n < 32; $n++ ) { $odd[ $n ] = $row; $row <<= 1; } $even = self::square( $odd ); $odd = self::square( $even ); do { $even = self::square( $odd ); if ( $len & 1 ) { $crc = self::times( $even, $crc ); } $len >>= 1; if ( 0 === $len ) { break; } $odd = self::square( $even ); if ( $len & 1 ) { $crc = self::times( $odd, $crc ); } $len >>= 1; } while ( 0 !== $len ); return $crc; } /** * GF(2) matrix times vector. * * @param array $mat Matrix. * @param int $vec Vector. */ private static function times( array $mat, int $vec ): int { $sum = 0; $i = 0; while ( 0 !== $vec ) { if ( $vec & 1 ) { $sum ^= $mat[ $i ]; } $vec >>= 1; ++$i; } return $sum; } /** * GF(2) matrix square. * * @param array $mat Matrix. * @return array */ private static function square( array $mat ): array { $sq = array(); for ( $n = 0; $n < 32; $n++ ) { $sq[ $n ] = self::times( $mat, $mat[ $n ] ); } return $sq; } }