Separate MKV class and functions in a separate module.

This commit is contained in:
Frédéric Tronel
2026-08-28 22:57:50 +02:00
parent ff20f31e74
commit 92df1d24d6
5 changed files with 502 additions and 471 deletions
View File
+163
View File
@@ -0,0 +1,163 @@
# SPDX-License-Identifier: GPL-2.0-or-later
#
# Copyright (C) 2026 Frédéric Tronel
import logging
from os import (
SEEK_SET,
fstat,
ftruncate,
lseek,
read,
write,
)
from typing import IO
from typeguard import typechecked
import hexdump
logger = logging.getLogger(__name__)
#MKV is formatted as an EBML file (Extended Binary Markup Langage).
# cf http://matroska-org.github.io/libebml/specs.html
# It is a Type, Length, Value (TLV) kind of binary file.
# Types are encoded as follows:
# 1xxx xxxx - Class A IDs (2^7 -1 possible values)
# 01xx xxxx xxxx xxxx - Class B IDs (2^14-1 possible values)
# 001x xxxx xxxx xxxx xxxx xxxx - Class C IDs (2^21-1 possible values)
# 0001 xxxx xxxx xxxx xxxx xxxx xxxx xxxx - Class D IDs (2^28-1 possible values)
# Lengths are encoded as follows:
# 1xxx xxxx
# value 0 to 2^7-2
# 01xx xxxx xxxx xxxx
# value 0 to 2^14-2
# 001x xxxx xxxx xxxx xxxx xxxx
# value 0 to 2^21-2
# 0001 xxxx xxxx xxxx xxxx xxxx xxxx xxxx
# value 0 to 2^28-2
# 0000 1xxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx
# value 0 to 2^35-2
# 0000 01xx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx
# value 0 to 2^42-2
# 0000 001x xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx
# value 0 to 2^49-2
# 0000 0001 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx
# value 0 to 2^56-2
@typechecked
def get_ebml_length(length:int) -> bytes|None:
if 0 <= length <= 2**7-2:
size = 1
elif length <= 2**14-2:
size = 2
elif length <= 2**21-2:
size = 3
elif length <= 2**28-2:
size = 4
elif length <= 2**35-2:
size = 5
elif length <= 2**42-2:
size = 6
elif length <= 2**49-2:
size = 7
elif length <= 2**56-2:
size = 8
elif length < 0:
logger.error('Impossible to encode a negative length with EBML.')
return None
else:
logger.error('Impossible to encode a length larger than 2^56-2 with EBML.')
return None
encoded_length = length + ((128>>(size-1))<<((size-1)*8))
res = (encoded_length).to_bytes(size, byteorder='big')
return res
@typechecked
def change_ebml_element_size(input_file: IO[bytes], position:int, addendum:int) -> int:
initial_position = position
infd = input_file.fileno()
lseek(infd, position, SEEK_SET)
buf = read(infd, 1)
element_type = int.from_bytes(buf, byteorder='big')
mask=128
found = False
for i in range(1,5):
if element_type&mask:
type_size = i
found = True
break
mask = mask>>1
if not found:
logger.error('Size of element type cannot be determined: %d', element_type)
raise ValueError()
# We seek to size
position+=type_size
lseek(infd, position, SEEK_SET)
buf = read(infd, 1)
size_head = int.from_bytes(buf, byteorder='big')
logger.info('First byte of size: %x', size_head)
mask=128
found = False
for i in range(1,9):
if size_head&mask:
size_of_data_size = i
found = True
break
mask = mask>>1
if not found:
logger.error('Size of data size cannot be determined: %d', size_head)
raise ValueError()
logger.info('Size of data size: %d.', size_of_data_size)
lseek(infd, position, SEEK_SET)
old_size_buf = read(infd, size_of_data_size)
max_size = 2**(size_of_data_size*7)-2
size_of_data = int.from_bytes(old_size_buf, byteorder='big')
logger.info('Size of data with mask: %x mask: %d.', size_of_data, mask)
size_of_data-= (mask<<((size_of_data_size-1)*8))
logger.info('Found element at position: %d, size of type: %d size of data: %d \
maximal size: %d.', initial_position, type_size, size_of_data, max_size)
new_size = size_of_data+addendum
delta = 0
if new_size > max_size:
# TODO: Test this code ...
new_encoded_size = get_ebml_length(new_size)
size_of_new_encoded_size = len(new_encoded_size)
if size_of_new_encoded_size <= size_of_data_size:
logger.error('New encoded size is smaller (%d) or equal than previous size (%d).\
This should not happen.', size_of_new_encoded_size, size_of_data_size)
raise ValueError()
# The difference of length between old size field and new one.
delta = size_of_new_encoded_size - size_of_data_size
file_length = fstat(infd).st_size
# We seek after actual length field
lseek(infd, position+size_of_data_size, SEEK_SET)
# We read the rest of file
tail = read(infd, file_length-(position+size_of_data_size))
# We increase file length
ftruncate(infd, file_length+delta)
# We go to the beginning of length field
lseek(infd, position, SEEK_SET)
# We write the new length field
write(infd, new_encoded_size)
# We overwrite the rest of file with its previous content that has been offset.
write(infd, tail)
else:
size = new_size + ((128>>(size_of_data_size-1))<<((size_of_data_size-1)*8))
new_size_buf = (size).to_bytes(size_of_data_size, byteorder='big')
logger.info('Old encoded size: %s New encoded size: %s', hexdump.dump(old_size_buf,sep=':'),
hexdump.dump(new_size_buf, sep=':'))
lseek(infd, position, SEEK_SET)
write(infd, new_size_buf)
# We return the potential increase in size of the file if the length field had to be increased.
return delta