Skip to content
This repository was archived by the owner on May 12, 2026. It is now read-only.

Commit aa80e2b

Browse files
committed
refactor: Split rom.py into modular package
Break down 637-line rom.py into focused modules: - src/core/rom/constants.py: ANDROID_LOGICAL_PARTITIONS list and RomType enum - src/core/rom/package.py: RomPackage class core logic (351 lines) - src/core/rom/extractors.py: ROM extraction methods for different formats (248 lines) - src/core/rom/utils.py: Utility functions for file operations (157 lines) - src/core/rom/__init__.py: Package exports Remove src/core/rom.py (replaced by package) Maintain backward compatibility - existing imports continue to work
1 parent 7953ee8 commit aa80e2b

5 files changed

Lines changed: 795 additions & 0 deletions

File tree

src/core/rom/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from __future__ import annotations
2+
3+
# Main exports
4+
from .constants import ANDROID_LOGICAL_PARTITIONS, RomType
5+
from .package import RomPackage
6+
7+
__all__ = [
8+
"ANDROID_LOGICAL_PARTITIONS",
9+
"RomType",
10+
"RomPackage",
11+
]

src/core/rom/constants.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from __future__ import annotations
2+
3+
from enum import Enum, auto
4+
from typing import List
5+
6+
7+
ANDROID_LOGICAL_PARTITIONS: List[str] = [
8+
"system",
9+
"system_ext",
10+
"product",
11+
"vendor",
12+
"odm",
13+
"mi_ext",
14+
"system_dlkm",
15+
"vendor_dlkm",
16+
"odm_dlkm",
17+
"product_dlkm",
18+
]
19+
20+
21+
class RomType(Enum):
22+
"""ROM package type enumeration."""
23+
24+
UNKNOWN = auto()
25+
PAYLOAD = auto() # payload.bin
26+
BROTLI = auto() # new.dat.br
27+
FASTBOOT = auto() # super.img or tgz
28+
LOCAL_DIR = auto() # Pre-extracted directory

src/core/rom/extractors.py

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
import os
5+
import shutil
6+
import zipfile
7+
from pathlib import Path
8+
from typing import TYPE_CHECKING, List, Optional
9+
10+
if TYPE_CHECKING:
11+
from .package import RomPackage
12+
13+
14+
def extract_payload(
15+
package: RomPackage,
16+
partitions: Optional[List[str]],
17+
) -> None:
18+
"""Extract payload.bin from ROM package.
19+
20+
Args:
21+
package: The RomPackage instance.
22+
partitions: List of partitions to extract (None = all).
23+
"""
24+
cmd = ["payload-dumper", "--out", str(package.images_dir)]
25+
26+
if partitions:
27+
package.logger.info(f"[{package.label}] Extracting specific images: {partitions} ...")
28+
cmd.extend(["--partitions", ",".join(partitions)])
29+
else:
30+
package.logger.info(f"[{package.label}] Extracting ALL images (Firmware + Logical) ...")
31+
32+
cmd.append(str(package.path))
33+
package.shell.run(cmd)
34+
35+
36+
def extract_brotli(
37+
package: RomPackage,
38+
partitions: Optional[List[str]],
39+
) -> None:
40+
"""Extract and convert brotli-compressed images from ROM package.
41+
42+
Args:
43+
package: The RomPackage instance.
44+
partitions: List of partitions to extract (None = all).
45+
"""
46+
# 1. Extract zip content
47+
with zipfile.ZipFile(package.path, "r") as z:
48+
for f in z.namelist():
49+
should_extract = False
50+
51+
# .img handling
52+
if f.endswith(".img"):
53+
part_name = Path(f).stem
54+
if not partitions or part_name in partitions:
55+
should_extract = True
56+
57+
# .br handling
58+
elif f.endswith(".new.dat.br") or f.endswith(".transfer.list"):
59+
# Extract partition name from file name (e.g. system.new.dat.br -> system)
60+
part_name = Path(f).name.split(".")[0]
61+
if not partitions or part_name in partitions:
62+
should_extract = True
63+
64+
if should_extract:
65+
package.logger.info(f"Extracting {f}...")
66+
z.extract(f, package.images_dir)
67+
68+
# 2. Process .br files
69+
for br_file in package.images_dir.glob("*.new.dat.br"):
70+
prefix = br_file.name.replace(".new.dat.br", "")
71+
72+
new_dat = package.images_dir / f"{prefix}.new.dat"
73+
transfer_list = package.images_dir / f"{prefix}.transfer.list"
74+
output_img = package.images_dir / f"{prefix}.img"
75+
76+
if output_img.exists():
77+
package.logger.info(f"[{package.label}] Image {output_img.name} already exists.")
78+
continue
79+
80+
if not transfer_list.exists():
81+
package.logger.warning(f"Transfer list for {prefix} not found, skipping conversion.")
82+
continue
83+
84+
# 3. Brotli Decompress
85+
package.logger.info(f"[{package.label}] Decompressing {br_file.name}...")
86+
try:
87+
cmd = ["brotli", "-d", "-f", str(br_file), "-o", str(new_dat)]
88+
package.shell.run(cmd)
89+
except Exception as e:
90+
package.logger.error(f"Brotli decompression failed for {prefix}: {e}")
91+
continue
92+
93+
# 4. sdat2img
94+
package.logger.info(f"[{package.label}] Converting {prefix} to raw image...")
95+
try:
96+
from src.utils.sdat2img import run_sdat2img
97+
98+
success = run_sdat2img(str(transfer_list), str(new_dat), str(output_img))
99+
100+
if not success:
101+
package.logger.error(f"sdat2img failed for {prefix}")
102+
else:
103+
package.logger.info(f"[{package.label}] Generated {output_img.name}")
104+
if new_dat.exists():
105+
os.remove(new_dat)
106+
if br_file.exists():
107+
os.remove(br_file)
108+
if transfer_list.exists():
109+
os.remove(transfer_list)
110+
111+
except Exception as e:
112+
package.logger.error(f"sdat2img execution failed: {e}")
113+
114+
115+
def extract_fastboot(
116+
package: RomPackage,
117+
partitions: Optional[List[str]],
118+
) -> None:
119+
"""Extract fastboot images (super.img) from ROM package.
120+
121+
Args:
122+
package: The RomPackage instance.
123+
partitions: List of partitions to extract (None = all).
124+
"""
125+
# Zip mode logic
126+
with zipfile.ZipFile(package.path, "r") as z:
127+
for f in z.namelist():
128+
if f.endswith("super.img") or f.endswith("images/super.img"):
129+
pass
130+
elif not f.endswith(".img"):
131+
continue
132+
133+
part_name = Path(f).stem
134+
if partitions and part_name not in partitions:
135+
continue
136+
137+
package.logger.info(f"Extracting {f}...")
138+
source = z.open(f)
139+
target = open(package.images_dir / Path(f).name, "wb")
140+
with source, target:
141+
shutil.copyfileobj(source, target)
142+
143+
from .utils import process_sparse_images
144+
145+
process_sparse_images(package.images_dir, package.logger, package.shell)
146+
147+
super_img = package.images_dir / "super.img"
148+
if super_img.exists():
149+
package.logger.info(
150+
f"[{package.label}] Found super.img, unpacking logical partitions..."
151+
)
152+
153+
try:
154+
if partitions:
155+
package.logger.info(
156+
f"[{package.label}] Unpacking specific partitions: {partitions}"
157+
)
158+
for part in partitions:
159+
cmd = [
160+
"lpunpack",
161+
"-p",
162+
part,
163+
str(super_img),
164+
str(package.images_dir),
165+
]
166+
package.shell.run(cmd, check=False)
167+
cmd_a = [
168+
"lpunpack",
169+
"-p",
170+
f"{part}_a",
171+
str(super_img),
172+
str(package.images_dir),
173+
]
174+
package.shell.run(cmd_a, check=False)
175+
else:
176+
package.logger.info(
177+
f"[{package.label}] Unpacking ALL partitions from super.img..."
178+
)
179+
package.shell.run(["lpunpack", str(super_img), str(package.images_dir)])
180+
181+
except Exception as e:
182+
package.logger.error(f"Failed to unpack super.img: {e}")
183+
raise
184+
finally:
185+
if super_img.exists():
186+
os.remove(super_img)
187+
188+
189+
def extract_local(
190+
package: RomPackage,
191+
partitions: Optional[List[str]],
192+
) -> None:
193+
"""Handle local directory mode (pre-extracted).
194+
195+
Args:
196+
package: The RomPackage instance.
197+
partitions: List of partitions to process.
198+
"""
199+
package.logger.info(f"[{package.label}] Local dir mode, skipping payload extraction.")
200+
201+
202+
class ImageExtractor:
203+
"""Handles ROM image extraction logic."""
204+
205+
def __init__(self, package: RomPackage) -> None:
206+
self.package = package
207+
208+
def extract_images(
209+
self,
210+
partitions: Optional[List[str]] = None,
211+
source_changed: bool = False,
212+
current_source_hash: str = "",
213+
source_hash_path: Path = None, # type: ignore[assignment]
214+
) -> None:
215+
"""Execute ROM image extraction based on type.
216+
217+
Args:
218+
partitions: List of partitions to extract (None = all logical partitions).
219+
source_changed: Whether the source file has changed.
220+
current_source_hash: Current hash of the source file.
221+
source_hash_path: Path to store the source hash.
222+
"""
223+
from .constants import RomType
224+
225+
try:
226+
if self.package.rom_type == RomType.PAYLOAD:
227+
extract_payload(self.package, partitions)
228+
elif self.package.rom_type == RomType.BROTLI:
229+
extract_brotli(self.package, partitions)
230+
elif self.package.rom_type == RomType.FASTBOOT:
231+
extract_fastboot(self.package, partitions)
232+
233+
except Exception as e:
234+
self.package.logger.error(f"Image extraction failed: {e}")
235+
raise
236+
237+
# Save hash after successful extraction if source changed
238+
if source_changed and source_hash_path is not None:
239+
try:
240+
with open(source_hash_path, "w") as f:
241+
f.write(current_source_hash)
242+
self.package.logger.info(
243+
f"[{self.package.label}] Saved source file hash for future change detection."
244+
)
245+
except Exception as e:
246+
self.package.logger.warning(
247+
f"[{self.package.label}] Could not save source hash file: {e}"
248+
)

0 commit comments

Comments
 (0)