From 8ee1d7984ad0e209fc6463b5dad1b67db5be760d Mon Sep 17 00:00:00 2001 From: Nymiro Date: Mon, 20 Oct 2025 11:58:35 +1300 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20Box=20=E7=B1=BB=E5=8F=8A?= =?UTF-8?q?=E5=85=B6=E5=AD=90=E7=B1=BB=20ItemLocationBox=20=E7=9A=84?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=EF=BC=8C=E5=8C=85=E5=90=AB=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96=E5=92=8C=E8=A7=A3=E6=9E=90=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 8196 -> 8196 bytes __pycache__/base.cpython-314.pyc | Bin 0 -> 1526 bytes __pycache__/heic_types.cpython-314.pyc | Bin 0 -> 3925 bytes __pycache__/parser.cpython-314.pyc | Bin 3379 -> 2891 bytes base.py | 15 ++++ heic_types.py | 48 ++++++++++++ main.py | 29 +++++-- parser.py | 101 +++++++++++-------------- 8 files changed, 131 insertions(+), 62 deletions(-) create mode 100644 __pycache__/base.cpython-314.pyc create mode 100644 __pycache__/heic_types.cpython-314.pyc create mode 100644 base.py create mode 100644 heic_types.py diff --git a/.DS_Store b/.DS_Store index 2aab8c4f64d1a634ff4128b75a2fb27fa3bd6bba..7a2552b9f89bc84bdf14c1098323116654d05af4 100644 GIT binary patch delta 277 zcmZp1XmOa}&nU4mU^hRb#AF@;$#~lyW(EcZ76v_rbcRfZlH7b3m!zEhB%l}v5HGnH zbleeDJ_WCQL55*)a(-?BP!9ux{RD=|4uVpX=LxtlDNF-1{|WH0GB7lhU!2S@s0m_m zfmpJWqXhXF4JOYOl$_iocr!VPA(5e&ArO>7%Q6rR~1d!5Glp=lCCP?r`O7KDuo2M%dfm8eS9G|){+4iOn`*5i7Gy=!L1 zqBa*Fkl+v@j^qT#<^;#~#FgH9j7H)F7jBI#NRPZXYsbxnk@uTA_V9_VLB&4^=8ddZnxj^0^p|wCl`$H@DQda0(CV~4miUHNm zdLmVv+j1N_lEE6t5ghnAQ8XmV+O?HEPck@OIa}ldiU&8n&<31QuF*I~;~GsNEU4lZ zQC?Vt3q5V$%pUeuCqmv9T=G;&Kk^^&l#3+xZ*P8b^X{!%{$BR5;@@v1(uZH<%cRwA za=#X-$d9vBMG3f9CSu3{!4HQApAFyOD!>ZulP$7qee?|>HP9}Rd$d7Ax=D8+4}dhZ z&DsI9s(B_bVC~4H$F&C!V5-~Zx{%fDk}K_r=qrW~2yAU9snWJoLc4oi#bsdWLOm0$ zNa-?TaWj&#mc)uNiAP@Q>-VakNiJkH?Y0t;RadjP)56HB?QVl7F@vKzv8wJxl2_VY zJ;_*8RMp8(CSh^9o0!h#gCQe|T zigPG?aU*Hg1WyAO(#xB5<0eYzaOx%TbyskRS+j|;BuyZPdNPkcmrU%hgF~Mj%`NVi zj^>sC=9l)*9WDC%<)gQjzjMAXi#hN(Qys&2$-oE8|5ZnoLM)cRdvrl?)e&hxMHxiU zPQxT-0EWbxr;1lldmdm4EeimK80xOmV}f{aIiJVR0v|K*8biYtEczsbw0@df&fp~U z8dy%L7#qQ2f#sBnGZyT8-_4{R|HyK+vD4hVM< zCW%EbCPP5Gj>*U+6E7AbkTJXPqw`bgp!CDnKQBLe^OxW-c=Xz@Yme{$w)14?@!j8N zpH1I+=G`$FfI7h>jk(Y7##d|vCG9bmMlG!T3C3Dkt<%Ic^u(8)s5!JuGgeQ8R89CJ zNHZ`O8LMS6W8zgj`67V60R0Co1nm_mC6c`kbSikxzSN;HZ#Ef&fDNY@@Qs{OEJlHV z6)$~&7vDmZ$Cl-KCk1FT(_JT1$nL*}V-uO#vH|}P1!#dJeIRhVQ67@qA+PO~Jgn>D b9kAoI5)1aZO)33@ESxx#be#?eqS^Hy>+eY$ literal 0 HcmV?d00001 diff --git a/__pycache__/heic_types.cpython-314.pyc b/__pycache__/heic_types.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0aae52b543f7fbabd00b96f9b4ccc27a117218a5 GIT binary patch literal 3925 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 I>&mqHA7HXvtN;K2 literal 3379 zcmbsrOKcm*b(UPNmOqg+^|Pr&TS<+=Sf9wX6IHU^)UjO)k{FqG$p%)rT`yM_5o)=f zU8R=X6f9uCR4vqUgTN9Jz&bR53>0XO&Z*b-ra}@zY;zFMlW#KARyp>)SyH4NH92&E zeX}!f=KbHBcd{|)BQQcU|DO4YmymzrqB{7>&)#|XDUu9Ha-+l|&E$I|>1riO_i2}N z!r}nOkGih7E%z1P;yXyv19+j8?-YQuT#=+m#x-=0kSy%BldIee zm*lQ-H$ZoQT++?{d4R3~9{75mqdqoo<`y(t1p2ZyZ)mnqn4UH@%ND8fOF64pO5AO` zbGl`_O^e!|oAZ`t#$C2BU7)jyWd~(h%`2vv&8e0wn@F;=Xb%kh)YPaspwG|dXkp-D zL7kgL#RiP|87-&EKx$641`LJ5v3_IT4#={i>jg`(G+DO8V37X$odzIl#sPd?B>TkU ziM>FudTke{r|+`qizu7^=3%%!I80Oz!pcC=z%t_AMffR_bXc!qNLtdXRwd~-^{Ssq z`W+oD%tk-hSLds%BmoO@iMWTNXf}_gqY#gTVtCr3I+M$1siwz0U|=mj&5luYS?!dG z(!GX*hS?=q&gnU@q8)JbcUDaISKvUAl$+X?yyd2L04?oH{&JhN6fC!$DDl4w(kPrb z6dx6R$({t5Z;OusjCU6m;Fv8?&6=ZnoTC9)!fqo>Gz6gHS|%!u;O;R1h&EdRFvqw& zsYhu9}Hc%brh7|l1NfOLD>0l1Q03;J&4qQ<~0>%ZH16LfvxKCk9@e9IP zFrn15(9k1ce4kqoB`2=W+1&ZKn>NEO$0z7CP#1MDF$ikTppcI>XE-aSF9u?f>_l2u z_$S^~@ABQXp7riO#h=F4V}HK#?D}7Bd~xI1#9t#Dp|K5dj9CEUqy;Y3eDCl^DH3t7 zEy}X4%wpvGWO=rboy+4t1*n1r&+JgXZf|kr*sAXqe;9~TmJ4`eh7L*|_@&dtU z;x0~qxc->sS*{vzg;hHonGGmLoZLt`@b~FgpG!Dy0^|AhB(m!auO!zag;ONRgX_K)UOON zeZRL(dBZayzD)1vXi2|af3K!F7IgIPn}sPYhFJpGepq#Z+@S zKwX7e49~Up0X*nXCv6^TOV)PhHQg4RmckS*fO^~xWzY@!!3{f zkNh78i=*W#;$C9M7k+qh@#KTPmH4)=t9-oUNo+Ot=_{qiU4a~HdmMWdTeiRQUJ&Y!pnn8Kiui~{*$|_ch?6${q;tm{j1Qi zjcC_qsC(`9R_OI|xOw+2htV0*bmH;3-=AA++UR+EGdlDvvK<{MT`ZqyFO8LB9owG}B%-2s9-Lb_yXEiM0ZqdW{~!q+Tkc-#-)tNyxyxekVPG+^d~HiS z4uosu;$Mny?D)gQai=2?=qYeaC4QiNgot%sF7}3@)1DINg>{<4vrb=dp8gn6m`v>T d+I4dr_YV@>=Yt&fUI_O)3D>#L!$ZdI{}(p1kg@;( diff --git a/base.py b/base.py new file mode 100644 index 0000000..f122d1a --- /dev/null +++ b/base.py @@ -0,0 +1,15 @@ +from typing import List + +class Box: + """ + Represents a generic ISOBMFF box. This is a simple data container. + """ + def __init__(self, size: int, box_type: str, offset: int, raw_data: bytes): + self.size = size + self.type = box_type + self.offset = offset + self.raw_data = raw_data + self.children: List['Box'] = [] # Initially empty + + def __repr__(self) -> str: + return f"" \ No newline at end of file diff --git a/heic_types.py b/heic_types.py new file mode 100644 index 0000000..739b7f1 --- /dev/null +++ b/heic_types.py @@ -0,0 +1,48 @@ +# In heic_types.py +import struct +from base import Box # <-- THIS IS THE KEY CHANGE + +class ItemLocation: + # ... (rest of this class is unchanged) + def __init__(self, item_id, offset, length): + self.item_id = item_id + self.offset = offset + self.length = length + def __repr__(self): + return f"" + +class ItemLocationBox(Box): + # ... (rest of this class is unchanged) + def __init__(self, size: int, box_type: str, offset: int, raw_data: bytes): + super().__init__(size, box_type, offset, raw_data) + self.locations = [] + self._parse_locations() + def _parse_locations(self): + # ... (all the parsing logic here is unchanged) + stream = self.raw_data + version_flags = struct.unpack('>I', stream[:4])[0] + sizes = struct.unpack('>H', stream[4:6])[0] + offset_size = (sizes >> 12) & 0x0F + length_size = (sizes >> 8) & 0x0F + base_offset_size = (sizes >> 4) & 0x0F + item_count = struct.unpack('>H', stream[6:8])[0] + current_pos = 8 + for _ in range(item_count): + item_id = struct.unpack('>H', stream[current_pos : current_pos+2])[0] + current_pos += 4 # Skip item_id and construction_method + current_pos += 2 # Skip data_reference_index + current_pos += base_offset_size + extent_count = struct.unpack('>H', stream[current_pos : current_pos+2])[0] + current_pos += 2 + if extent_count > 0: + offset = self._read_int(stream, current_pos, offset_size) + current_pos += offset_size + length = self._read_int(stream, current_pos, length_size) + current_pos += length_size + self.locations.append(ItemLocation(item_id, offset, length)) + def _read_int(self, data, pos, size): + if size == 1: return data[pos] + if size == 2: return struct.unpack('>H', data[pos:pos+2])[0] + if size == 4: return struct.unpack('>I', data[pos:pos+4])[0] + if size == 8: return struct.unpack('>Q', data[pos:pos+8])[0] + return 0 \ No newline at end of file diff --git a/main.py b/main.py index 5d017c6..b5dcc9f 100644 --- a/main.py +++ b/main.py @@ -1,13 +1,30 @@ +from base import Box from parser import parse_boxes +from heic_types import ItemLocationBox +from typing import List +import os + +def print_box_tree(boxes: List[Box], indent: int = 0): + for box in boxes: + print(" " * indent + str(box)) + + if isinstance(box, ItemLocationBox): + for loc in box.locations[:5]: + print(" " * (indent + 1) + str(loc)) + if len(box.locations) > 5: + print(" " * (indent + 1) + f"... and {len(box.locations) - 5} more locations") + + if box.children: + print_box_tree(box.children, indent + 1) def main(): # Test with the Apple HEIC file print("--- Analyzing Apple HEIC file ---") try: with open('apple.heic', 'rb') as f: - top_level_boxes = parse_boxes(f) - for box in top_level_boxes: - print(box) + file_size = os.fstat(f.fileno()).st_size + top_level_boxes = parse_boxes(f, file_size) + print_box_tree(top_level_boxes) except FileNotFoundError: print("Error: apple.heic not found.") @@ -17,9 +34,9 @@ def main(): print("--- Analyzing Samsung HEIC file ---") try: with open('samsung.heic', 'rb') as f: - top_level_boxes = parse_boxes(f) - for box in top_level_boxes: - print(box) + file_size = os.fstat(f.fileno()).st_size + top_level_boxes = parse_boxes(f, file_size) + print_box_tree(top_level_boxes) except FileNotFoundError: print("Error: samsung.heic not found.") diff --git a/parser.py b/parser.py index f701a23..26a0c76 100644 --- a/parser.py +++ b/parser.py @@ -1,78 +1,67 @@ import struct - -class Box: - """ - Represents a generic ISOBMFF box. - """ - def __init__(self, size: int, box_type: str, offset: int, raw_data: bytes): - self.size = size - self.type = box_type - self.offset = offset - self.raw_data = raw_data - self.children = [] # Will be populated for container boxes - - def __repr__(self) -> str: - # e.g., - return f"" - -# In parser.py - from typing import List, BinaryIO +from io import BytesIO -def parse_boxes(stream: BinaryIO) -> List[Box]: +from base import Box # Import the base Box class +from heic_types import ItemLocationBox + +# The factory map remains here +BOX_TYPE_MAP = { + 'iloc': ItemLocationBox, +} + +CONTAINER_BOXES = {'meta', 'moov', 'trak', 'iprp', 'ipco', 'dinf', 'fiinf', 'ipro'} +FULL_BOXES = {'meta', 'hdlr', 'pitm', 'iinf', 'iloc'} + +def parse_boxes(stream: BinaryIO, max_size: int) -> List[Box]: """ - Parses all top-level boxes from a file stream, handling special size cases. + Parses boxes from a file stream up to a maximum size. + Now also handles recursive parsing. """ boxes = [] + start_pos_in_stream = stream.tell() - stream.seek(0, 2) - file_size = stream.tell() - stream.seek(0) - - while stream.tell() < file_size: - current_offset = stream.tell() + while stream.tell() - start_pos_in_stream < max_size: + current_offset_in_stream = stream.tell() - # Read the standard 8-byte header header = stream.read(8) - if len(header) < 8: - break + if len(header) < 8: break size = struct.unpack('>I', header[:4])[0] - box_type = header[4:].decode('ascii') + box_type = header[4:].decode('ascii', errors='ignore') header_size = 8 - content_size = 0 - - # --- NEW: Handle special size cases --- if size == 1: - # The actual size is a 64-bit integer following the type largesize_header = stream.read(8) - if len(largesize_header) < 8: - break + if len(largesize_header) < 8: break size = struct.unpack('>Q', largesize_header)[0] header_size = 16 - content_size = size - header_size elif size == 0: - # The box extends to the end of the file - content_size = file_size - current_offset - header_size - size = content_size + header_size - else: - # Standard size - content_size = size - header_size - # --- End of new code --- + size = max_size - (current_offset_in_stream - start_pos_in_stream) - # Read the box's content (raw_data) - # We need to rewind a bit if we read largesize header to get all content - stream.seek(current_offset + header_size) - raw_data = stream.read(content_size) - if len(raw_data) < content_size: - # Avoids errors on truncated files - break - - box = Box(size, box_type, current_offset, raw_data) - boxes.append(box) + if size < header_size: break + + content_size = size - header_size - # Seek to the beginning of the next box - stream.seek(current_offset + size) + stream.seek(current_offset_in_stream + header_size) + raw_data = stream.read(content_size) + if len(raw_data) < content_size: break + + # --- Factory Pattern --- + box_class = BOX_TYPE_MAP.get(box_type, Box) + box = box_class(size, box_type, current_offset_in_stream, raw_data) + + # --- NEW: Recursive parsing logic moved here --- + if box.type in CONTAINER_BOXES: + child_stream = BytesIO(box.raw_data) + parse_size = len(box.raw_data) + if box.type in FULL_BOXES: + child_stream.read(4) # Skip version/flags + parse_size -= 4 + box.children = parse_boxes(child_stream, parse_size) + # --- End of new logic --- + + boxes.append(box) + stream.seek(current_offset_in_stream + size) return boxes \ No newline at end of file