key = $key ? $key : null; } /** * Open + verify the archive. Discovers all volumes (name.aisv[.N]) and validates * each one's magic. * * @param string $path Archive path (volume 1). * @return true|\WP_Error */ public function open( $path ) { $magic_len = strlen( self::MAGIC ); $this->volumes = array(); $this->vlen = array(); $this->cum = array(); $this->total = 0; $index = 1; while ( true ) { $vp = ( 1 === $index ) ? $path : $path . '.' . $index; if ( ! is_file( $vp ) ) { break; } $size = (int) filesize( $vp ); if ( $size < $magic_len ) { return new \WP_Error( 'aisiteark_archive_short', __( 'A backup volume is truncated or empty.', 'ai-siteark' ) ); } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen $fh = @fopen( $vp, 'rb' ); if ( ! $fh ) { return new \WP_Error( 'aisiteark_archive_open', __( 'Could not open the archive.', 'ai-siteark' ) ); } $magic = fread( $fh, $magic_len ); fclose( $fh ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose if ( self::MAGIC !== $magic ) { return new \WP_Error( 'aisiteark_archive_magic', __( 'This file is not a valid AI-SiteArk archive.', 'ai-siteark' ) ); } $this->cum[] = $this->total; $this->volumes[] = $vp; $this->vlen[] = $size - $magic_len; $this->total += $size - $magic_len; ++$index; } if ( empty( $this->volumes ) ) { return new \WP_Error( 'aisiteark_archive_open', __( 'Could not open the archive.', 'ai-siteark' ) ); } $this->pos = 0; $this->next_pos = null; $this->cur_vol = -1; return true; } /** * Total logical size of the archive (sum of all volumes, magics excluded). * * @return int */ public function total_size() { return (int) $this->total; } /** * Number of volumes. * * @return int */ public function volume_count() { return count( $this->volumes ); } /** * The volume index containing logical offset $p, or -1 at/after the end. * * @param int $p Logical offset. * @return int */ private function volume_for( $p ) { if ( $p < 0 || $p >= $this->total ) { return -1; } for ( $i = count( $this->volumes ) - 1; $i >= 0; $i-- ) { if ( $p >= $this->cum[ $i ] ) { return $i; } } return -1; } /** * Ensure volume $i is the open handle. * * @param int $i Volume index. * @return bool */ private function ensure_open( $i ) { if ( $this->cur_vol === $i && $this->fh ) { return true; } if ( $this->fh ) { fclose( $this->fh ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose $this->fh = null; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen $this->fh = @fopen( $this->volumes[ $i ], 'rb' ); if ( ! $this->fh ) { return false; } $this->cur_vol = $i; return true; } /** * Read up to $n bytes from the logical stream starting at $this->pos, spanning * volume boundaries as needed. Advances $this->pos by what was read. * * @param int $n Byte count. * @return string */ private function read_bytes( $n ) { $magic_len = strlen( self::MAGIC ); $out = ''; $need = (int) $n; while ( $need > 0 ) { $i = $this->volume_for( $this->pos ); if ( $i < 0 || ! $this->ensure_open( $i ) ) { break; // EOF or open failure. } $inner = $magic_len + ( $this->pos - $this->cum[ $i ] ); $avail = ( $this->vlen[ $i ] + $magic_len ) - $inner; if ( $avail <= 0 ) { break; } fseek( $this->fh, $inner ); $buf = fread( $this->fh, (int) min( $need, $avail ) ); if ( '' === $buf || false === $buf ) { break; } $got = strlen( $buf ); $out .= $buf; $this->pos += $got; $need -= $got; } return $out; } /** * Advance to the next entry; returns its header, or null at EOF. * * @return array|null */ public function next() { if ( null !== $this->next_pos ) { $this->pos = $this->next_pos; } $len_bytes = $this->read_bytes( 4 ); if ( strlen( $len_bytes ) < 4 ) { return null; // EOF. } $hlen = unpack( 'N', $len_bytes )[1]; $hjson = $this->read_bytes( $hlen ); $header = json_decode( (string) $hjson, true ); $plen_bytes = $this->read_bytes( 8 ); if ( strlen( $plen_bytes ) < 8 ) { return null; } $plen = unpack( 'J', $plen_bytes )[1]; $this->payload_start = $this->pos; $this->payload_len = (int) $plen; $this->next_pos = $this->pos + (int) $plen; $this->cur_header = is_array( $header ) ? $header : array(); return $this->cur_header; } /** * Byte offset (into S) of the entry the next next() call will read. * * @return int */ public function next_entry_pos() { return null === $this->next_pos ? (int) $this->pos : (int) $this->next_pos; } /** * Resume reading from a saved logical offset (from a prior next_entry_pos()). * * @param int $pos Logical offset. * @return void */ public function seek_to( $pos ) { $this->next_pos = (int) $pos; } /** * Read the full payload of the current entry into memory. * * @return string */ public function read_payload() { $this->pos = $this->payload_start; $data = $this->read_bytes( $this->payload_len ); if ( $this->key && ! empty( $this->cur_header['iv'] ) ) { $data = Crypto::dec( $data, $this->key, base64_decode( $this->cur_header['iv'] ) ); } return $data; } /** * Stream the current entry's payload to an open destination handle. * * @param resource $dest_fh Destination file handle. * @param int $chunk Chunk size. * @return void */ public function copy_payload_to( $dest_fh, $chunk = 1048576 ) { $this->pos = $this->payload_start; $remaining = $this->payload_len; $encrypt = ( $this->key && ! empty( $this->cur_header['iv'] ) ); $iv = $encrypt ? base64_decode( $this->cur_header['iv'] ) : ''; $blocks = 0; while ( $remaining > 0 ) { $buf = $this->read_bytes( (int) min( $chunk, $remaining ) ); if ( '' === $buf ) { break; } $remaining -= strlen( $buf ); if ( $encrypt ) { $dec = Crypto::dec( $buf, $this->key, Crypto::iv_add( $iv, $blocks ) ); $blocks += Crypto::blocks( strlen( $buf ) ); $buf = $dec; } fwrite( $dest_fh, $buf ); } } /** * Close the archive. * * @return void */ public function close() { if ( $this->fh ) { fclose( $this->fh ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose $this->fh = null; } $this->cur_vol = -1; } }