0, 'member_in' => 0, 'member_out' => 0, 'done' => false]; if ($c['done']) { return $c; } clearstatcache(true, $gz); $size = is_file($gz) ? filesize($gz) : false; $in = Fs::open($gz, 'rb'); if ($in === false || $size === false || fseek($in, $c['member_in']) !== 0) { throw self::damaged(); } clearstatcache(true, $sql); $out = Fs::open($sql, is_file($sql) ? 'r+b' : 'wb'); ftruncate($out, $c['written']); fseek($out, $c['written']); $ctx = self::context(); $pos = $c['member_in']; // offset in the .gz of the next piece $produced = $c['member_out']; // SQL inflated so far, counting what was already written try { while (!$budget->exhausted()) { $piece = (string) Fs::read($in, self::PIECE); if ($piece === '') { throw self::damaged(); // the file ends inside a member: it was cut off } $at = 0; while ($at < strlen($piece)) { $data = @inflate_add($ctx, $at === 0 ? $piece : substr($piece, $at), ZLIB_SYNC_FLUSH); // Damaged data makes PHP warn as well as return false; false is handled here, and PHP has no way to try it without the warning. if ($data === false) { throw self::damaged(); } $produced = self::write($out, $data, $produced, $c, $budget); if (inflate_get_status($ctx) !== ZLIB_STREAM_END) { break; } // This member is complete; the next one, if any, starts right after it. $c['member_in'] += inflate_get_read_len($ctx); $c['member_out'] = $produced; if ($c['member_in'] >= $size) { $c['done'] = true; return $c; } if ($budget->exhausted()) { return $c; // the next request starts at this member boundary } $at = $c['member_in'] - $pos; $ctx = self::context(); } $pos += strlen($piece); } } finally { Fs::close($in); Fs::close($out); } return $c; } /** * Writes the part of $data past what the .sql file already has. * * @param resource $out the .sql file * @return int SQL inflated after $data */ private static function write($out, string $data, int $produced, array &$c, Budget $budget): int { $end = $produced + strlen($data); if ($end > $c['written']) { $new = substr($data, max(0, $c['written'] - $produced)); if (Fs::write($out, $new) !== strlen($new)) { throw new RelayError('import.write_failed', esc_html__('Could not unpack the SQL file. The disk may be full.', 'siterelay')); } $c['written'] += strlen($new); $budget->consume(strlen($new)); } return $end; } /** @return resource|object a resource on PHP 7.4, an InflateContext from PHP 8.0 */ private static function context() // @phpstan-ignore return.unusedType (analysed as PHP 7.4; from PHP 8.0 it is an object) { $ctx = inflate_init(ZLIB_ENCODING_GZIP); if ($ctx === false) { throw self::damaged(); } return $ctx; } private static function damaged(): RelayError { return new RelayError('import.bad_gzip', esc_html__('The .gz file is damaged or not a gzip file.', 'siterelay')); } }