blob: 68931af6e3b86cbfeca4cf182c9032af5a9d3cbf [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Shared block decompression for Java global-index block trailers."""
import struct
import zlib
from pypaimon.globalindex.memory_slice_input import MemorySliceInput
COMPRESSION_NONE = 0
COMPRESSION_ZSTD = 1
COMPRESSION_LZ4 = 2
COMPRESSION_LZO = 3
BLOCK_TRAILER_LENGTH = 5
_HEADER_LENGTH = 8
def crc32c(data: bytes, compression_type: int) -> int:
crc = zlib.crc32(data)
crc = zlib.crc32(bytes([compression_type & 0xFF]), crc)
return crc & 0xFFFFFFFF
def decompress_block(block_bytes: bytes, compression_type: int) -> bytes:
if compression_type == COMPRESSION_NONE:
return block_bytes
if compression_type == COMPRESSION_ZSTD:
return _decompress_zstd(block_bytes)
if compression_type == COMPRESSION_LZ4:
return _decompress_lz4(block_bytes)
if compression_type == COMPRESSION_LZO:
return _decompress_lzo(block_bytes)
raise ValueError("Compression type %s not supported" % compression_type)
def decompress_block_with_trailer(block_and_trailer: bytes, block_length: int) -> bytes:
if len(block_and_trailer) < block_length + BLOCK_TRAILER_LENGTH:
raise ValueError("Block data too short to contain trailer.")
block_bytes = block_and_trailer[:block_length]
trailer = block_and_trailer[block_length:block_length + BLOCK_TRAILER_LENGTH]
compression_type = trailer[0]
expected_crc = struct.unpack('<I', trailer[1:5])[0]
actual_crc = crc32c(block_bytes, compression_type)
if actual_crc != expected_crc:
raise ValueError(
"CRC32 mismatch: expected %s, got %s" % (expected_crc, actual_crc))
return decompress_block(block_bytes, compression_type)
def _decompress_zstd(block_bytes: bytes) -> bytes:
import zstandard as zstd
from io import BytesIO
memory_input = MemorySliceInput(block_bytes)
expected_len = memory_input.read_var_len_int()
compressed = block_bytes[memory_input.position():]
with zstd.ZstdDecompressor().stream_reader(BytesIO(compressed)) as reader:
uncompressed = reader.read()
if len(uncompressed) != expected_len:
raise ValueError("Corrupted block, decompression failed.")
return uncompressed
def _decompress_lz4(block_bytes: bytes) -> bytes:
memory_input = MemorySliceInput(block_bytes)
expected_len = memory_input.read_var_len_int()
payload_start = memory_input.position()
if len(block_bytes) - payload_start < _HEADER_LENGTH:
raise ValueError("Compressed block data too short to contain header.")
compressed_len, original_len = struct.unpack_from('<ii', block_bytes, payload_start)
_validate_lengths(compressed_len, original_len)
if original_len != expected_len:
raise ValueError("Compressed block header has unexpected original length.")
payload_start += _HEADER_LENGTH
payload_end = payload_start + compressed_len
if len(block_bytes) < payload_end:
raise ValueError("Compressed block data is incomplete.")
payload = block_bytes[payload_start:payload_end]
uncompressed = _decompress_lz4_raw(payload, original_len)
if len(uncompressed) != original_len:
raise ValueError("Corrupted block, decompression failed.")
return uncompressed
def _decompress_lzo(block_bytes: bytes) -> bytes:
memory_input = MemorySliceInput(block_bytes)
expected_len = memory_input.read_var_len_int()
payload_start = memory_input.position()
if len(block_bytes) - payload_start < _HEADER_LENGTH:
raise ValueError("Compressed block data too short to contain header.")
compressed_len, original_len = struct.unpack_from('<ii', block_bytes, payload_start)
_validate_lengths(compressed_len, original_len)
if original_len != expected_len:
raise ValueError("Compressed block header has unexpected original length.")
payload_start += _HEADER_LENGTH
payload_end = payload_start + compressed_len
if len(block_bytes) < payload_end:
raise ValueError("Compressed block data is incomplete.")
payload = block_bytes[payload_start:payload_end]
uncompressed = _decompress_lzo_raw(payload, original_len)
if len(uncompressed) != original_len:
raise ValueError("Corrupted block, decompression failed.")
return uncompressed
def _decompress_lz4_raw(payload: bytes, output_len: int) -> bytes:
output = bytearray()
pos = 0
payload_len = len(payload)
while pos < payload_len:
token = payload[pos]
pos += 1
literal_len = token >> 4
if literal_len == 15:
length_part, pos = _read_lz4_length(payload, pos, payload_len)
literal_len += length_part
literal_end = pos + literal_len
if literal_end > payload_len:
raise ValueError("Corrupted LZ4 block, literal exceeds input.")
output.extend(payload[pos:literal_end])
pos = literal_end
if pos == payload_len:
break
if pos + 2 > payload_len:
raise ValueError("Corrupted LZ4 block, missing match offset.")
offset = payload[pos] | (payload[pos + 1] << 8)
pos += 2
if offset <= 0 or offset > len(output):
raise ValueError("Corrupted LZ4 block, invalid match offset.")
match_len = token & 0x0F
if match_len == 15:
length_part, pos = _read_lz4_length(payload, pos, payload_len)
match_len += length_part
match_len += 4
start = len(output) - offset
for _ in range(match_len):
output.append(output[start])
start += 1
if len(output) > output_len:
raise ValueError("Corrupted LZ4 block, output exceeds expected length.")
if len(output) != output_len:
raise ValueError("Corrupted LZ4 block, unexpected output length.")
return bytes(output)
def _decompress_lzo_raw(payload: bytes, output_len: int) -> bytes:
if not payload:
if output_len == 0:
return b''
raise ValueError("Corrupted LZO block, empty payload.")
output = bytearray()
pos = 0
payload_len = len(payload)
first_command = True
last_literal_len = 0
while pos < payload_len:
first_command = True
last_literal_len = 0
while True:
if pos >= payload_len:
raise ValueError("Corrupted LZO block, missing command.")
command = payload[pos]
pos += 1
match_len = 0
match_offset = 0
if (command & 0xF0) == 0:
if last_literal_len == 0:
literal_len = command & 0x0F
if literal_len == 0:
literal_len, pos = _read_lzo_variable_length(
payload, pos, payload_len, 0x0F)
literal_len += 3
elif last_literal_len <= 3:
if pos >= payload_len:
raise ValueError("Corrupted LZO block, missing match offset.")
match_len = 2
match_offset = ((command & 0x0C) >> 2) | (payload[pos] << 2)
pos += 1
literal_len = command & 0x03
else:
if pos >= payload_len:
raise ValueError("Corrupted LZO block, missing match offset.")
match_len = 3
match_offset = (((command & 0x0C) >> 2)
| (payload[pos] << 2)
| 0x0800)
pos += 1
literal_len = command & 0x03
elif first_command:
literal_len = command - 17
elif (command & 0xF0) == 0x10:
match_len = command & 0x07
if match_len == 0:
match_len, pos = _read_lzo_variable_length(
payload, pos, payload_len, 0x07)
match_len += 2
trailer, pos = _read_lzo_short(payload, pos, payload_len)
match_offset = ((command & 0x08) << 11) + (trailer >> 2)
if match_offset == 0:
break
match_offset += 0x3FFF
literal_len = trailer & 0x03
elif (command & 0xE0) == 0x20:
match_len = command & 0x1F
if match_len == 0:
match_len, pos = _read_lzo_variable_length(
payload, pos, payload_len, 0x1F)
match_len += 2
trailer, pos = _read_lzo_short(payload, pos, payload_len)
match_offset = trailer >> 2
literal_len = trailer & 0x03
elif (command & 0xC0) != 0:
if pos >= payload_len:
raise ValueError("Corrupted LZO block, missing match offset.")
match_len = ((command & 0xE0) >> 5) + 1
match_offset = ((command & 0x1C) >> 2) | (payload[pos] << 3)
pos += 1
literal_len = command & 0x03
else:
raise ValueError("Corrupted LZO block, invalid command.")
first_command = False
if match_len < 0 or literal_len < 0:
raise ValueError("Corrupted LZO block, invalid length.")
if match_len:
_copy_lzo_match(output, match_offset + 1, match_len, output_len)
literal_end = pos + literal_len
if literal_end > payload_len:
raise ValueError("Corrupted LZO block, literal exceeds input.")
output.extend(payload[pos:literal_end])
pos = literal_end
if len(output) > output_len:
raise ValueError("Corrupted LZO block, output exceeds expected length.")
last_literal_len = literal_len
if len(output) != output_len:
raise ValueError("Corrupted LZO block, unexpected output length.")
return bytes(output)
def _read_lzo_variable_length(payload: bytes, pos: int, payload_len: int, base: int):
length = base
while True:
if pos >= payload_len:
raise ValueError("Corrupted LZO block, malformed length.")
value = payload[pos]
pos += 1
if value != 0:
return length + value, pos
length += 255
def _read_lzo_short(payload: bytes, pos: int, payload_len: int):
if pos + 2 > payload_len:
raise ValueError("Corrupted LZO block, missing match trailer.")
return payload[pos] | (payload[pos + 1] << 8), pos + 2
def _copy_lzo_match(output: bytearray, offset: int, match_len: int, output_len: int) -> None:
if offset <= 0 or offset > len(output):
raise ValueError("Corrupted LZO block, invalid match offset.")
if len(output) + match_len > output_len:
raise ValueError("Corrupted LZO block, output exceeds expected length.")
start = len(output) - offset
for _ in range(match_len):
output.append(output[start])
start += 1
def _read_lz4_length(payload: bytes, pos: int, payload_len: int):
length = 0
while True:
if pos >= payload_len:
raise ValueError("Corrupted LZ4 block, malformed length.")
value = payload[pos]
pos += 1
length += value
if value != 255:
return length, pos
def _validate_lengths(compressed_len: int, original_len: int) -> None:
if (original_len < 0
or compressed_len < 0
or (original_len == 0 and compressed_len != 0)
or (original_len != 0 and compressed_len == 0)):
raise ValueError("Input is corrupted, invalid length.")