267 lines
12 KiB
Python
267 lines
12 KiB
Python
|
# **************************************************************
|
||
|
# * LZKN64 Compression and Decompression Utility *
|
||
|
# * Original repo at https://github.com/Fluvian/lzkn64, *
|
||
|
# * converted from C to Python with permission from Fluvian. *
|
||
|
# **************************************************************
|
||
|
|
||
|
TYPE_COMPRESS = 1
|
||
|
TYPE_DECOMPRESS = 2
|
||
|
|
||
|
MODE_NONE = 0x7F
|
||
|
MODE_WINDOW_COPY = 0x00
|
||
|
MODE_RAW_COPY = 0x80
|
||
|
MODE_RLE_WRITE_A = 0xC0
|
||
|
MODE_RLE_WRITE_B = 0xE0
|
||
|
MODE_RLE_WRITE_C = 0xFF
|
||
|
|
||
|
WINDOW_SIZE = 0x3FF
|
||
|
COPY_SIZE = 0x21
|
||
|
RLE_SIZE = 0x101
|
||
|
|
||
|
|
||
|
# Compresses the data in the buffer specified in the arguments.
|
||
|
def compress_buffer(file_buffer: bytearray) -> bytearray:
|
||
|
# Size of the buffer to compress
|
||
|
buffer_size = len(file_buffer) - 1
|
||
|
|
||
|
# Position of the current read location in the buffer.
|
||
|
buffer_position = 0
|
||
|
|
||
|
# Position of the current write location in the written buffer.
|
||
|
write_position = 4
|
||
|
|
||
|
# Allocate write_buffer with size of 0xFFFFFF (24-bit).
|
||
|
write_buffer = bytearray(0xFFFFFF)
|
||
|
|
||
|
# Position in the input buffer of the last time one of the copy modes was used.
|
||
|
buffer_last_copy_position = 0
|
||
|
|
||
|
while buffer_position < buffer_size:
|
||
|
# Calculate maximum length we are able to copy without going out of bounds.
|
||
|
if COPY_SIZE < (buffer_size - 1) - buffer_position:
|
||
|
sliding_window_maximum_length = COPY_SIZE
|
||
|
else:
|
||
|
sliding_window_maximum_length = (buffer_size - 1) - buffer_position
|
||
|
|
||
|
# Calculate how far we are able to look back without going behind the start of the uncompressed buffer.
|
||
|
if buffer_position - WINDOW_SIZE > 0:
|
||
|
sliding_window_maximum_offset = buffer_position - WINDOW_SIZE
|
||
|
else:
|
||
|
sliding_window_maximum_offset = 0
|
||
|
|
||
|
# Calculate maximum length the forwarding looking window is able to search.
|
||
|
if RLE_SIZE < (buffer_size - 1) - buffer_position:
|
||
|
forward_window_maximum_length = RLE_SIZE
|
||
|
else:
|
||
|
forward_window_maximum_length = (buffer_size - 1) - buffer_position
|
||
|
|
||
|
sliding_window_match_position = -1
|
||
|
sliding_window_match_size = 0
|
||
|
|
||
|
forward_window_match_value = 0
|
||
|
forward_window_match_size = 0
|
||
|
|
||
|
# The current mode the compression algorithm prefers. (0x7F == None)
|
||
|
current_mode = MODE_NONE
|
||
|
|
||
|
# The current submode the compression algorithm prefers.
|
||
|
current_submode = MODE_NONE
|
||
|
|
||
|
# How many bytes will have to be copied in the raw copy command.
|
||
|
raw_copy_size = buffer_position - buffer_last_copy_position
|
||
|
|
||
|
# How many bytes we still have to copy in RLE matches with more than 0x21 bytes.
|
||
|
rle_bytes_left = 0
|
||
|
|
||
|
"""Go backwards in the buffer, is there a matching value?
|
||
|
If yes, search forward and check for more matching values in a loop.
|
||
|
If no, go further back and repeat."""
|
||
|
for search_position in range(buffer_position - 1, sliding_window_maximum_offset - 1, -1):
|
||
|
matching_sequence_size = 0
|
||
|
|
||
|
while file_buffer[search_position + matching_sequence_size] == file_buffer[buffer_position +
|
||
|
matching_sequence_size]:
|
||
|
matching_sequence_size += 1
|
||
|
|
||
|
if matching_sequence_size >= sliding_window_maximum_length:
|
||
|
break
|
||
|
|
||
|
# Once we find a match or a match that is bigger than the match before it, we save its position and length.
|
||
|
if matching_sequence_size > sliding_window_match_size:
|
||
|
sliding_window_match_position = search_position
|
||
|
sliding_window_match_size = matching_sequence_size
|
||
|
|
||
|
"""Look one step forward in the buffer, is there a matching value?
|
||
|
If yes, search further and check for a repeating value in a loop.
|
||
|
If no, continue to the rest of the function."""
|
||
|
matching_sequence_value = file_buffer[buffer_position]
|
||
|
matching_sequence_size = 0
|
||
|
|
||
|
while file_buffer[buffer_position + matching_sequence_size] == matching_sequence_value:
|
||
|
matching_sequence_size += 1
|
||
|
|
||
|
if matching_sequence_size >= forward_window_maximum_length:
|
||
|
break
|
||
|
|
||
|
# If we find a sequence of matching values, save them.
|
||
|
if matching_sequence_size >= 1:
|
||
|
forward_window_match_value = matching_sequence_value
|
||
|
forward_window_match_size = matching_sequence_size
|
||
|
|
||
|
# Try to pick which mode works best with the current values.
|
||
|
if sliding_window_match_size >= 3:
|
||
|
current_mode = MODE_WINDOW_COPY
|
||
|
elif forward_window_match_size >= 3:
|
||
|
current_mode = MODE_RLE_WRITE_A
|
||
|
|
||
|
if forward_window_match_value != 0x00 and forward_window_match_size <= COPY_SIZE:
|
||
|
current_submode = MODE_RLE_WRITE_A
|
||
|
elif forward_window_match_value != 0x00 and forward_window_match_size > COPY_SIZE:
|
||
|
current_submode = MODE_RLE_WRITE_A
|
||
|
rle_bytes_left = forward_window_match_size
|
||
|
elif forward_window_match_value == 0x00 and forward_window_match_size <= COPY_SIZE:
|
||
|
current_submode = MODE_RLE_WRITE_B
|
||
|
elif forward_window_match_value == 0x00 and forward_window_match_size > COPY_SIZE:
|
||
|
current_submode = MODE_RLE_WRITE_C
|
||
|
elif forward_window_match_size >= 2 and forward_window_match_value == 0x00:
|
||
|
current_mode = MODE_RLE_WRITE_A
|
||
|
current_submode = MODE_RLE_WRITE_B
|
||
|
|
||
|
"""Write a raw copy command when these following conditions are met:
|
||
|
The current mode is set and there are raw bytes available to be copied.
|
||
|
The raw byte length exceeds the maximum length that can be stored.
|
||
|
Raw bytes need to be written due to the proximity to the end of the buffer."""
|
||
|
if (current_mode != MODE_NONE and raw_copy_size >= 1) or raw_copy_size >= 0x1F or \
|
||
|
(buffer_position + 1) == buffer_size:
|
||
|
if buffer_position + 1 == buffer_size:
|
||
|
raw_copy_size = buffer_size - buffer_last_copy_position
|
||
|
|
||
|
write_buffer[write_position] = MODE_RAW_COPY | raw_copy_size & 0x1F
|
||
|
write_position += 1
|
||
|
|
||
|
for written_bytes in range(raw_copy_size):
|
||
|
write_buffer[write_position] = file_buffer[buffer_last_copy_position]
|
||
|
write_position += 1
|
||
|
buffer_last_copy_position += 1
|
||
|
|
||
|
if current_mode == MODE_WINDOW_COPY:
|
||
|
write_buffer[write_position] = MODE_WINDOW_COPY | ((sliding_window_match_size - 2) & 0x1F) << 2 | \
|
||
|
(((buffer_position - sliding_window_match_position) & 0x300) >> 8)
|
||
|
write_position += 1
|
||
|
write_buffer[write_position] = (buffer_position - sliding_window_match_position) & 0xFF
|
||
|
write_position += 1
|
||
|
|
||
|
buffer_position += sliding_window_match_size
|
||
|
buffer_last_copy_position = buffer_position
|
||
|
elif current_mode == MODE_RLE_WRITE_A:
|
||
|
if current_submode == MODE_RLE_WRITE_A:
|
||
|
if rle_bytes_left > 0:
|
||
|
while rle_bytes_left > 0:
|
||
|
# Dump raw bytes if we have less than two bytes left, not doing so would cause an underflow
|
||
|
# error.
|
||
|
if rle_bytes_left < 2:
|
||
|
write_buffer[write_position] = MODE_RAW_COPY | rle_bytes_left & 0x1F
|
||
|
write_position += 1
|
||
|
|
||
|
for writtenBytes in range(rle_bytes_left):
|
||
|
write_buffer[write_position] = forward_window_match_value & 0xFF
|
||
|
write_position += 1
|
||
|
|
||
|
rle_bytes_left = 0
|
||
|
break
|
||
|
|
||
|
if rle_bytes_left < COPY_SIZE:
|
||
|
write_buffer[write_position] = MODE_RLE_WRITE_A | (rle_bytes_left - 2) & 0x1F
|
||
|
write_position += 1
|
||
|
else:
|
||
|
write_buffer[write_position] = MODE_RLE_WRITE_A | (COPY_SIZE - 2) & 0x1F
|
||
|
write_position += 1
|
||
|
write_buffer[write_position] = forward_window_match_value & 0xFF
|
||
|
write_position += 1
|
||
|
rle_bytes_left -= COPY_SIZE
|
||
|
else:
|
||
|
write_buffer[write_position] = MODE_RLE_WRITE_A | (forward_window_match_size - 2) & 0x1F
|
||
|
write_position += 1
|
||
|
write_buffer[write_position] = forward_window_match_value & 0xFF
|
||
|
write_position += 1
|
||
|
|
||
|
elif current_submode == MODE_RLE_WRITE_B:
|
||
|
write_buffer[write_position] = MODE_RLE_WRITE_B | (forward_window_match_size - 2) & 0x1F
|
||
|
write_position += 1
|
||
|
elif current_submode == MODE_RLE_WRITE_C:
|
||
|
write_buffer[write_position] = MODE_RLE_WRITE_C
|
||
|
write_position += 1
|
||
|
write_buffer[write_position] = (forward_window_match_size - 2) & 0xFF
|
||
|
write_position += 1
|
||
|
|
||
|
buffer_position += forward_window_match_size
|
||
|
buffer_last_copy_position = buffer_position
|
||
|
else:
|
||
|
buffer_position += 1
|
||
|
|
||
|
# Write the compressed size.
|
||
|
write_buffer[1] = 0x00
|
||
|
write_buffer[1] = write_position >> 16 & 0xFF
|
||
|
write_buffer[2] = write_position >> 8 & 0xFF
|
||
|
write_buffer[3] = write_position & 0xFF
|
||
|
|
||
|
# Return the compressed write buffer.
|
||
|
return write_buffer[0:write_position]
|
||
|
|
||
|
|
||
|
# Decompresses the data in the buffer specified in the arguments.
|
||
|
def decompress_buffer(file_buffer: bytearray) -> bytearray:
|
||
|
# Position of the current read location in the buffer.
|
||
|
buffer_position = 4
|
||
|
|
||
|
# Position of the current write location in the written buffer.
|
||
|
write_position = 0
|
||
|
|
||
|
# Get compressed size.
|
||
|
compressed_size = (file_buffer[1] << 16) + (file_buffer[2] << 8) + file_buffer[3] - 1
|
||
|
|
||
|
# Allocate writeBuffer with size of 0xFFFFFF (24-bit).
|
||
|
write_buffer = bytearray(0xFFFFFF)
|
||
|
|
||
|
while buffer_position < compressed_size:
|
||
|
mode_command = file_buffer[buffer_position]
|
||
|
buffer_position += 1
|
||
|
|
||
|
if MODE_WINDOW_COPY <= mode_command < MODE_RAW_COPY:
|
||
|
copy_length = (mode_command >> 2) + 2
|
||
|
copy_offset = file_buffer[buffer_position] + (mode_command << 8) & 0x3FF
|
||
|
buffer_position += 1
|
||
|
|
||
|
for current_length in range(copy_length, 0, -1):
|
||
|
write_buffer[write_position] = write_buffer[write_position - copy_offset]
|
||
|
write_position += 1
|
||
|
elif MODE_RAW_COPY <= mode_command < MODE_RLE_WRITE_A:
|
||
|
copy_length = mode_command & 0x1F
|
||
|
|
||
|
for current_length in range(copy_length, 0, -1):
|
||
|
write_buffer[write_position] = file_buffer[buffer_position]
|
||
|
write_position += 1
|
||
|
buffer_position += 1
|
||
|
elif MODE_RLE_WRITE_A <= mode_command <= MODE_RLE_WRITE_C:
|
||
|
write_length = 0
|
||
|
write_value = 0x00
|
||
|
|
||
|
if MODE_RLE_WRITE_A <= mode_command < MODE_RLE_WRITE_B:
|
||
|
write_length = (mode_command & 0x1F) + 2
|
||
|
write_value = file_buffer[buffer_position]
|
||
|
buffer_position += 1
|
||
|
elif MODE_RLE_WRITE_B <= mode_command < MODE_RLE_WRITE_C:
|
||
|
write_length = (mode_command & 0x1F) + 2
|
||
|
elif mode_command == MODE_RLE_WRITE_C:
|
||
|
write_length = file_buffer[buffer_position] + 2
|
||
|
buffer_position += 1
|
||
|
|
||
|
for current_length in range(write_length, 0, -1):
|
||
|
write_buffer[write_position] = write_value
|
||
|
write_position += 1
|
||
|
|
||
|
# Return the current position of the write buffer, essentially giving us the size of the write buffer.
|
||
|
while write_position % 16 != 0:
|
||
|
write_position += 1
|
||
|
return write_buffer[0:write_position]
|