unsuper 1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.4
2
+ Name: unsuper
3
+ Version: 1.0
4
+ Summary: The fastest Android super.img dumper, made in Python.
5
+ Author-email: fl0w <codefl0w@proton.me>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/codefl0w/unsuper
8
+ Project-URL: Repository, https://github.com/codefl0w/unsuper
9
+ Project-URL: Issues, https://github.com/codefl0w/unsuper/issues
10
+ Requires-Python: >=3.6
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Dynamic: license-file
14
+
15
+
16
+ # unsuper
17
+ The fastest Android super.img dumper, made in Python.
18
+
19
+ ![Version](https://img.shields.io/github/v/release/codefl0w/unsuper) ![Downloads](https://img.shields.io/github/downloads/codefl0w/unsuper/total)
20
+
21
+
22
+ ## Features
23
+
24
+ - **Extremely fast** - Best in its class, see speeds below
25
+ - **Unsparse support** - Automatically unsparses images without any dependancies
26
+ - **Smart extraction** - Automatically skips empty partitions
27
+ - **Selective usage** - unsuper can extract only specific partitions or just unsparse the image
28
+
29
+
30
+
31
+ # Speed
32
+ unsuper can can unsparse a 6.67GB super.img in only 11 seconds, and dump all available partitions in ~27 seconds on an average NVMe disk with the default of 4 threads. This puts the total extraction time to just under 40 seconds.
33
+
34
+ Performance may vary based on the amount of threads and disk speeds. More threads doesn't always equal to higher speeds.
35
+
36
+ # Usage
37
+
38
+ unsuper's usage is extremely easy: just state the path to your super.img and start. If no output directory is stated, unsuper will create an "extracted_partitions" folder under your home directory and use it as default.
39
+
40
+ Extra arguements come AFTER the positional arguements.
41
+
42
+ positional arguments:
43
+ super_image Path to super.img file
44
+ output_dir Output directory for extracted partitions (default: extracted_partitions)
45
+
46
+ options:
47
+ -h, --help show this help message and exit
48
+ --version show program's version number and exit
49
+ -p, --partitions PARTITIONS [PARTITIONS ...]
50
+ Specific partition names to extract (vendor, product etc.) (default: extract all)
51
+ -j, --jobs JOBS Number of parallel extraction threads (default: 4)
52
+ -q, --quiet Suppress progress output
53
+ --list List available partitions and exit
54
+ --temp-dir TEMP_DIR Directory for temporary unsparse file (default: system temp directory)
55
+ --unsparse Unsparse image and save to output directory
56
+
57
+ For example, if you want to extract the system_a partition using 6 threads:
58
+
59
+ unsuper.py /path/to/super.img /output_path --partitions system_a --jobs 6
60
+
61
+ And so on.
62
+
63
+ unsuper can also simply unsparse an image and save it to a specified directory:
64
+
65
+ unsuper.py /path/to/super.img /output_path --unsparse
66
+
67
+ Or list all available partitions and clean after:
68
+
69
+ unsuper.py /path/to/super.img --list
70
+
71
+ ### Notes
72
+ If the --list arguement is used with --partitions, it will calculate the specified partitions' sizes and exit.
73
+
74
+ If the --unsparse arguement is used along with --partitions or --list, the unsparse image will be saved to the output directory and reused for extraction rather than creating a temporary one.
75
+
76
+ ### Extras
77
+
78
+ Enjoy my work? Please consider a small donation!
79
+
80
+ <a href="https://buymeacoffee.com/fl0w" target="_blank" rel="noopener noreferrer">
81
+ <img width="350" alt="yellow-button" src="https://github.com/user-attachments/assets/2e6d44c8-9640-4cb3-bcc8-989595d6b7e9"/>
82
+ </a>
83
+
@@ -0,0 +1,7 @@
1
+ unsuper.py,sha256=qQh3fTLHFJKRuZlQJBIWN46WDanODGRr0HwNpuk9N9s,33603
2
+ unsuper-1.0.dist-info/licenses/LICENSE,sha256=OHPrqaGCtOyKrJkJiAwk-GiTGGS_D23hKmYvt5Sczpw,1061
3
+ unsuper-1.0.dist-info/METADATA,sha256=EwhupTR26HQxVJf1Mh13fnrbGXaqLZe1YDnb8OjmVxg,3484
4
+ unsuper-1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
5
+ unsuper-1.0.dist-info/entry_points.txt,sha256=FvNrac_y7Vehjn7206YPl4pZ-ezndk9G9lEre-Tc9b4,41
6
+ unsuper-1.0.dist-info/top_level.txt,sha256=HaeaRAynFdwrPpq5cptT8uXLDJ9xRATulUuAQetKvBY,8
7
+ unsuper-1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ unsuper = unsuper:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 fl0w
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ unsuper
unsuper.py ADDED
@@ -0,0 +1,775 @@
1
+ #!/usr/bin/env python3
2
+
3
+ # unsuper, fastest Android super.img dumper
4
+
5
+ import argparse
6
+ import mmap
7
+ import os
8
+ import struct
9
+ import sys
10
+ import tempfile
11
+ from concurrent.futures import ThreadPoolExecutor, as_completed
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+ from typing import BinaryIO, List, Optional, Tuple
15
+ import time
16
+
17
+ VERSION = 1.0
18
+
19
+ # Constants
20
+ SPARSE_HEADER_MAGIC = 0xED26FF3A
21
+ SPARSE_HEADER_SIZE = 28
22
+ SPARSE_CHUNK_HEADER_SIZE = 12
23
+
24
+ LP_PARTITION_RESERVED_BYTES = 4096
25
+ LP_METADATA_GEOMETRY_MAGIC = 0x616c4467
26
+ LP_METADATA_GEOMETRY_SIZE = 4096
27
+ LP_METADATA_HEADER_MAGIC = 0x414C5030
28
+ LP_SECTOR_SIZE = 512
29
+
30
+ LP_TARGET_TYPE_LINEAR = 0
31
+
32
+ # Optimized buffer size for I/O operations (1MB)
33
+ BUFFER_SIZE = 1024 * 1024
34
+
35
+ class FastDumperError(Exception):
36
+ # Custom exception for dumper errors
37
+ pass
38
+
39
+ @dataclass
40
+ class ExtentInfo:
41
+ # extent information
42
+ offset: int
43
+ size: int
44
+
45
+ @dataclass
46
+ class PartitionInfo:
47
+ # partition information
48
+ name: str
49
+ extents: List[ExtentInfo]
50
+ total_size: int
51
+
52
+ class SparseHeader:
53
+ # Sparse image header parser
54
+ def __init__(self, buffer: bytes):
55
+ fmt = '<I4H4I'
56
+ required_size = struct.calcsize(fmt)
57
+ if len(buffer) < required_size:
58
+ raise FastDumperError(f"Sparse header too short: got {len(buffer)} bytes, need {required_size}")
59
+
60
+ (
61
+ self.magic,
62
+ self.major_version,
63
+ self.minor_version,
64
+ self.file_hdr_sz,
65
+ self.chunk_hdr_sz,
66
+ self.blk_sz,
67
+ self.total_blks,
68
+ self.total_chunks,
69
+ self.image_checksum
70
+ ) = struct.unpack(fmt, buffer[:required_size])
71
+
72
+ class SparseChunkHeader:
73
+ # Sparse chunk header parser
74
+ def __init__(self, buffer: bytes):
75
+ fmt = '<2H2I'
76
+ required_size = struct.calcsize(fmt)
77
+ if len(buffer) < required_size:
78
+ raise FastDumperError(f"Chunk header too short: got {len(buffer)} bytes, need {required_size}")
79
+
80
+ (
81
+ self.chunk_type,
82
+ self.reserved,
83
+ self.chunk_sz,
84
+ self.total_sz,
85
+ ) = struct.unpack(fmt, buffer[:required_size])
86
+
87
+ class FastSparseConverter:
88
+ # sparse image converter
89
+
90
+ def __init__(self, input_file: str, temp_dir: Optional[str] = None):
91
+ self.input_file = input_file
92
+ self.temp_dir = temp_dir
93
+
94
+ def convert(self) -> str:
95
+
96
+ # Create output file in temp directory
97
+ if self.temp_dir:
98
+ temp_dir = Path(self.temp_dir)
99
+ temp_dir.mkdir(parents=True, exist_ok=True)
100
+ output_file = temp_dir / f"{Path(self.input_file).stem}.unsparse.img"
101
+ else:
102
+ # Use OS temp directory
103
+ fd, output_file = tempfile.mkstemp(
104
+ suffix='.unsparse.img',
105
+ prefix=f"{Path(self.input_file).stem}_",
106
+ dir=None
107
+ )
108
+ os.close(fd) # Close the file descriptor, we'll open it normally
109
+ output_file = str(output_file)
110
+
111
+ try:
112
+ with open(self.input_file, 'rb') as infile, open(output_file, 'wb') as outfile:
113
+ # Read and validate header
114
+ header_data = infile.read(SPARSE_HEADER_SIZE)
115
+ if len(header_data) < SPARSE_HEADER_SIZE:
116
+ raise FastDumperError(f"File too short: cannot read sparse header ({len(header_data)} < {SPARSE_HEADER_SIZE})")
117
+
118
+ header = SparseHeader(header_data)
119
+
120
+ if header.magic != SPARSE_HEADER_MAGIC:
121
+ raise FastDumperError(f"Invalid sparse image magic: 0x{header.magic:08x} (expected 0x{SPARSE_HEADER_MAGIC:08x})")
122
+
123
+ # Validate header values
124
+ if header.file_hdr_sz < SPARSE_HEADER_SIZE:
125
+ raise FastDumperError(f"Invalid file header size: {header.file_hdr_sz}")
126
+
127
+ if header.chunk_hdr_sz < SPARSE_CHUNK_HEADER_SIZE:
128
+ raise FastDumperError(f"Invalid chunk header size: {header.chunk_hdr_sz}")
129
+
130
+ if header.blk_sz == 0:
131
+ raise FastDumperError("Invalid block size: 0")
132
+
133
+ # Skip to chunk data
134
+ infile.seek(header.file_hdr_sz)
135
+
136
+ # Process chunks efficiently
137
+ for chunk_idx in range(header.total_chunks):
138
+ try:
139
+ # Read chunk header with bounds checking
140
+ chunk_header_data = infile.read(SPARSE_CHUNK_HEADER_SIZE)
141
+ if len(chunk_header_data) < SPARSE_CHUNK_HEADER_SIZE:
142
+ raise FastDumperError(f"Unexpected EOF while reading chunk {chunk_idx + 1}/{header.total_chunks} header: got {len(chunk_header_data)} bytes")
143
+
144
+ chunk_header = SparseChunkHeader(chunk_header_data)
145
+
146
+ # Validate chunk header
147
+ if chunk_header.total_sz < header.chunk_hdr_sz:
148
+ raise FastDumperError(f"Invalid chunk {chunk_idx + 1} total size: {chunk_header.total_sz} < {header.chunk_hdr_sz}")
149
+
150
+ # Skip extra header data if present
151
+ if header.chunk_hdr_sz > SPARSE_CHUNK_HEADER_SIZE:
152
+ extra_bytes = header.chunk_hdr_sz - SPARSE_CHUNK_HEADER_SIZE
153
+ skipped = infile.read(extra_bytes)
154
+ if len(skipped) < extra_bytes:
155
+ raise FastDumperError(f"Unexpected EOF while reading chunk {chunk_idx + 1} extra header data")
156
+
157
+ chunk_data_size = chunk_header.total_sz - header.chunk_hdr_sz
158
+ output_size = chunk_header.chunk_sz * header.blk_sz
159
+
160
+ if chunk_header.chunk_type == 0xCAC1: # Raw data
161
+ # Copy data in large chunks for better performance
162
+ remaining = chunk_data_size
163
+ while remaining > 0:
164
+ chunk_size = min(BUFFER_SIZE, remaining)
165
+ data = infile.read(chunk_size)
166
+ if len(data) == 0:
167
+ raise FastDumperError(f"Unexpected EOF while reading chunk {chunk_idx + 1} raw data")
168
+ outfile.write(data)
169
+ remaining -= len(data)
170
+
171
+ elif chunk_header.chunk_type == 0xCAC2: # Fill data
172
+ if chunk_data_size < 4:
173
+ raise FastDumperError(f"Incomplete fill chunk {chunk_idx + 1}: data size {chunk_data_size} < 4")
174
+
175
+ fill_data = infile.read(4)
176
+ if len(fill_data) < 4:
177
+ raise FastDumperError(f"Unexpected EOF while reading chunk {chunk_idx + 1} fill data")
178
+
179
+ # Skip remaining fill chunk data if any
180
+ if chunk_data_size > 4:
181
+ remaining_fill = chunk_data_size - 4
182
+ skipped = infile.read(remaining_fill)
183
+ if len(skipped) < remaining_fill:
184
+ raise FastDumperError(f"Unexpected EOF while reading chunk {chunk_idx + 1} remaining fill data")
185
+
186
+ # Write fill pattern efficiently
187
+ if output_size > 0:
188
+ pattern = fill_data * (BUFFER_SIZE // 4)
189
+ remaining = output_size
190
+ while remaining > 0:
191
+ write_size = min(len(pattern), remaining)
192
+ outfile.write(pattern[:write_size])
193
+ remaining -= write_size
194
+
195
+ elif chunk_header.chunk_type == 0xCAC3: # Don't care
196
+ # Skip chunk data and write zeros/seek
197
+ if chunk_data_size > 0:
198
+ skipped = infile.read(chunk_data_size)
199
+ if len(skipped) < chunk_data_size:
200
+ raise FastDumperError(f"Unexpected EOF while skipping chunk {chunk_idx + 1} don't care data")
201
+
202
+ # Seek forward in output file (creates sparse file if supported)
203
+ current_pos = outfile.tell()
204
+ outfile.seek(current_pos + output_size)
205
+
206
+ elif chunk_header.chunk_type == 0xCAC4: # CRC32
207
+ # Skip CRC chunk data
208
+ if chunk_data_size > 0:
209
+ skipped = infile.read(chunk_data_size)
210
+ if len(skipped) < chunk_data_size:
211
+ raise FastDumperError(f"Unexpected EOF while skipping chunk {chunk_idx + 1} CRC data")
212
+ # CRC chunks don't contribute to output
213
+
214
+ else:
215
+ # Unknown chunk type, skip data and output
216
+ print(f"Warning: Unknown chunk type 0x{chunk_header.chunk_type:04x} in chunk {chunk_idx + 1}, skipping...")
217
+ if chunk_data_size > 0:
218
+ skipped = infile.read(chunk_data_size)
219
+ if len(skipped) < chunk_data_size:
220
+ raise FastDumperError(f"Unexpected EOF while skipping chunk {chunk_idx + 1} unknown data")
221
+
222
+ # Seek forward in output
223
+ current_pos = outfile.tell()
224
+ outfile.seek(current_pos + output_size)
225
+
226
+ except FastDumperError:
227
+ raise
228
+ except Exception as e:
229
+ raise FastDumperError(f"Error processing chunk {chunk_idx + 1}: {e}")
230
+
231
+ except Exception as e:
232
+ # Clean up partial output file on error
233
+ if os.path.exists(output_file):
234
+ try:
235
+ os.remove(output_file)
236
+ except:
237
+ pass
238
+ raise
239
+
240
+ return output_file
241
+
242
+ class FastMetadataParser:
243
+ # Optimized metadata parser
244
+
245
+ def __init__(self, file_path: str):
246
+ self.file_path = file_path
247
+ self.file_size = os.path.getsize(file_path)
248
+
249
+ def parse_partitions(self, partition_names: Optional[List[str]] = None) -> List[PartitionInfo]:
250
+ # Parse partition metadata efficiently
251
+
252
+ with open(self.file_path, 'rb') as f:
253
+ # Use memory mapping for better performance on large files
254
+ with mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mm:
255
+ return self._parse_with_mmap(mm, partition_names)
256
+
257
+ def _parse_with_mmap(self, mm: mmap.mmap, partition_names: Optional[List[str]]) -> List[PartitionInfo]:
258
+ # Parse metadata using memory mapping
259
+
260
+ # Check minimum file size
261
+ if len(mm) < LP_PARTITION_RESERVED_BYTES + 40:
262
+ raise FastDumperError("File too small to contain valid metadata")
263
+
264
+ # Read geometry
265
+ mm.seek(LP_PARTITION_RESERVED_BYTES)
266
+ geometry_data = mm.read(40) # Only read what we need
267
+ if len(geometry_data) < 40:
268
+ raise FastDumperError("Cannot read geometry data")
269
+
270
+ magic, struct_size = struct.unpack('<2I', geometry_data[:8])
271
+
272
+ if magic != LP_METADATA_GEOMETRY_MAGIC:
273
+ raise FastDumperError(f"Invalid geometry magic: 0x{magic:08x}")
274
+
275
+ # Read remaining geometry fields
276
+ if len(geometry_data) < 44:
277
+ mm.seek(LP_PARTITION_RESERVED_BYTES + 32)
278
+ extra_data = mm.read(12)
279
+ if len(extra_data) < 12:
280
+ raise FastDumperError("Cannot read complete geometry data")
281
+ geometry_data = geometry_data[:32] + extra_data
282
+
283
+ metadata_max_size, metadata_slot_count, logical_block_size = struct.unpack(
284
+ '<3I', geometry_data[32:44]
285
+ )
286
+
287
+ # Validate geometry values
288
+ if metadata_max_size == 0 or metadata_slot_count == 0:
289
+ raise FastDumperError("Invalid metadata geometry")
290
+
291
+ # Calculate header offset
292
+ base_offset = LP_PARTITION_RESERVED_BYTES + (LP_METADATA_GEOMETRY_SIZE * 2)
293
+ header_offset = base_offset
294
+
295
+ # Check if we have enough space for metadata
296
+ if header_offset + 128 > len(mm):
297
+ raise FastDumperError("File too small for metadata header")
298
+
299
+ # Read metadata header
300
+ mm.seek(header_offset)
301
+ header_data = mm.read(128) # Read enough for header + table descriptors
302
+ if len(header_data) < 128:
303
+ raise FastDumperError("Cannot read metadata header")
304
+
305
+ magic = struct.unpack('<I', header_data[:4])[0]
306
+ if magic != LP_METADATA_HEADER_MAGIC:
307
+ # Try backup header
308
+ backup_offset = base_offset + metadata_max_size * metadata_slot_count
309
+ if backup_offset + 128 > len(mm):
310
+ raise FastDumperError("File too small for backup metadata header")
311
+
312
+ mm.seek(backup_offset)
313
+ header_data = mm.read(128)
314
+ if len(header_data) < 128:
315
+ raise FastDumperError("Cannot read backup metadata header")
316
+
317
+ magic = struct.unpack('<I', header_data[:4])[0]
318
+ if magic != LP_METADATA_HEADER_MAGIC:
319
+ raise FastDumperError(f"Invalid metadata header magic: 0x{magic:08x}")
320
+ header_offset = backup_offset
321
+
322
+ # Parse header and table descriptors
323
+ header_size, tables_size = struct.unpack('<2I', header_data[8:16])
324
+
325
+ if header_size < 80 or tables_size == 0:
326
+ raise FastDumperError("Invalid metadata header values")
327
+
328
+ # Parse table descriptors (starting at offset 80 in header)
329
+ tables_start = header_offset + header_size
330
+
331
+ # Check bounds for table descriptors
332
+ if len(header_data) < 104:
333
+ raise FastDumperError("Header too small for table descriptors")
334
+
335
+ # Partition table descriptor
336
+ part_offset, part_count, part_entry_size = struct.unpack('<3I', header_data[80:92])
337
+
338
+ # Extent table descriptor
339
+ extent_offset, extent_count, extent_entry_size = struct.unpack('<3I', header_data[92:104])
340
+
341
+ # Validate table parameters
342
+ if part_entry_size < 52 or extent_entry_size < 24:
343
+ raise FastDumperError("Invalid table entry sizes")
344
+
345
+ # Check bounds for partition table
346
+ part_table_start = tables_start + part_offset
347
+ part_table_size = part_count * part_entry_size
348
+ if part_table_start + part_table_size > len(mm):
349
+ raise FastDumperError("Partition table extends beyond file")
350
+
351
+ # Check bounds for extent table
352
+ extent_table_start = tables_start + extent_offset
353
+ extent_table_size = extent_count * extent_entry_size
354
+ if extent_table_start + extent_table_size > len(mm):
355
+ raise FastDumperError("Extent table extends beyond file")
356
+
357
+ # Read partition table
358
+ mm.seek(part_table_start)
359
+ partitions_data = mm.read(part_table_size)
360
+ if len(partitions_data) < part_table_size:
361
+ raise FastDumperError("Cannot read complete partition table")
362
+
363
+ # Read extent table
364
+ mm.seek(extent_table_start)
365
+ extents_data = mm.read(extent_table_size)
366
+ if len(extents_data) < extent_table_size:
367
+ raise FastDumperError("Cannot read complete extent table")
368
+
369
+ # Parse partitions and extents efficiently
370
+ partitions = []
371
+
372
+ for i in range(part_count):
373
+ part_start = i * part_entry_size
374
+ part_data = partitions_data[part_start:part_start + part_entry_size]
375
+
376
+ if len(part_data) < 52:
377
+ continue # Skip malformed entries
378
+
379
+ # Parse partition entry
380
+ name_bytes = part_data[:36]
381
+ name = name_bytes.decode('utf-8').strip('\x00')
382
+
383
+ # Skip empty partition names
384
+ if not name:
385
+ continue
386
+
387
+ # Skip if not in requested partitions
388
+ if partition_names and name not in partition_names:
389
+ continue
390
+
391
+ attributes, first_extent_idx, num_extents = struct.unpack(
392
+ '<3I', part_data[36:48]
393
+ )
394
+
395
+ # Check for various empty partition conditions
396
+ if num_extents == 0:
397
+ print(f"Warning: Partition {name} has no extents, skipping...")
398
+ continue
399
+
400
+ # Validate extent indices
401
+ if first_extent_idx >= extent_count or first_extent_idx + num_extents > extent_count:
402
+ print(f"Warning: Invalid extent indices for partition {name}, skipping...")
403
+ continue
404
+
405
+ # Parse extents for this partition
406
+ extents = []
407
+ total_size = 0
408
+ skipped_extents = 0
409
+
410
+ for j in range(num_extents):
411
+ extent_idx = first_extent_idx + j
412
+ extent_start = extent_idx * extent_entry_size
413
+ extent_data = extents_data[extent_start:extent_start + extent_entry_size]
414
+
415
+ if len(extent_data) < 24:
416
+ skipped_extents += 1
417
+ continue # Skip malformed extents
418
+
419
+ num_sectors, target_type, target_data = struct.unpack(
420
+ '<QIQ', extent_data[:20]
421
+ )
422
+
423
+ if target_type != LP_TARGET_TYPE_LINEAR:
424
+ skipped_extents += 1
425
+ continue
426
+
427
+ offset = target_data * LP_SECTOR_SIZE
428
+ size = num_sectors * LP_SECTOR_SIZE
429
+
430
+ # Validate extent bounds
431
+ if offset + size > len(mm):
432
+ print(f"Warning: Extent for {name} extends beyond file, truncating...")
433
+ size = max(0, len(mm) - offset)
434
+
435
+ if size > 0:
436
+ extents.append(ExtentInfo(offset, size))
437
+ total_size += size
438
+ else:
439
+ skipped_extents += 1
440
+
441
+ # Check if partition ended up empty after processing extents
442
+ if not extents:
443
+ if skipped_extents > 0:
444
+ print(f"Warning: Partition {name} has no valid extents (skipped {skipped_extents} invalid extents), skipping...")
445
+ else:
446
+ print(f"Warning: Partition {name} has zero-sized extents, skipping...")
447
+ continue
448
+
449
+ # Only add partitions with valid extents
450
+ partitions.append(PartitionInfo(name, extents, total_size))
451
+
452
+ return partitions
453
+
454
+ class FastExtractor:
455
+ # High-performance partition extractor
456
+
457
+ def __init__(self, input_file: str, output_dir: str, max_workers: int = 4):
458
+ self.input_file = input_file
459
+ self.output_dir = Path(output_dir)
460
+ self.max_workers = max_workers
461
+
462
+ # Create output directory
463
+ self.output_dir.mkdir(parents=True, exist_ok=True)
464
+
465
+ def extract_partition(self, partition: PartitionInfo) -> Tuple[str, float]:
466
+ # Extract a single partition
467
+ start_time = time.time()
468
+ output_file = self.output_dir / f"{partition.name}.img"
469
+
470
+ try:
471
+ with open(self.input_file, 'rb') as infile, open(output_file, 'wb') as outfile:
472
+ # Use memory mapping for input file if it's large
473
+ if os.path.getsize(self.input_file) > 100 * 1024 * 1024: # 100MB threshold
474
+ with mmap.mmap(infile.fileno(), 0, access=mmap.ACCESS_READ) as mm:
475
+ self._extract_with_mmap(mm, outfile, partition)
476
+ else:
477
+ self._extract_with_seek(infile, outfile, partition)
478
+ except Exception as e:
479
+ # Clean up partial output file on error
480
+ if output_file.exists():
481
+ output_file.unlink()
482
+ raise FastDumperError(f"Failed to extract {partition.name}: {e}")
483
+
484
+ elapsed = time.time() - start_time
485
+ return partition.name, elapsed
486
+
487
+ def _extract_with_mmap(self, mm: mmap.mmap, outfile: BinaryIO, partition: PartitionInfo):
488
+ # Extract using memory mapping
489
+ for extent in partition.extents:
490
+ # Validate extent bounds
491
+ if extent.offset >= len(mm):
492
+ continue
493
+
494
+ mm.seek(extent.offset)
495
+ remaining = min(extent.size, len(mm) - extent.offset)
496
+
497
+ while remaining > 0:
498
+ chunk_size = min(BUFFER_SIZE, remaining)
499
+ data = mm.read(chunk_size)
500
+ if not data:
501
+ break
502
+ outfile.write(data)
503
+ remaining -= len(data)
504
+
505
+ def _extract_with_seek(self, infile: BinaryIO, outfile: BinaryIO, partition: PartitionInfo):
506
+ # Extract using classic file operations
507
+ file_size = os.path.getsize(self.input_file)
508
+
509
+ for extent in partition.extents:
510
+ # Validate extent bounds
511
+ if extent.offset >= file_size:
512
+ continue
513
+
514
+ infile.seek(extent.offset)
515
+ remaining = min(extent.size, file_size - extent.offset)
516
+
517
+ while remaining > 0:
518
+ chunk_size = min(BUFFER_SIZE, remaining)
519
+ data = infile.read(chunk_size)
520
+ if not data:
521
+ break
522
+ outfile.write(data)
523
+ remaining -= len(data)
524
+
525
+ def extract_all(self, partitions: List[PartitionInfo], show_progress: bool = True) -> None:
526
+ # Extract all partitions in parallel
527
+
528
+ if show_progress:
529
+ print(f"Extracting {len(partitions)} partitions using {self.max_workers} threads...")
530
+
531
+ # Use thread pool for parallel extraction
532
+ with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
533
+ # Submit all extraction tasks
534
+ future_to_partition = {
535
+ executor.submit(self.extract_partition, partition): partition
536
+ for partition in partitions
537
+ }
538
+
539
+ # Process completed tasks
540
+ completed = 0
541
+ total_time = 0
542
+
543
+ for future in as_completed(future_to_partition):
544
+ partition = future_to_partition[future]
545
+
546
+ try:
547
+ name, elapsed = future.result()
548
+ completed += 1
549
+ total_time += elapsed
550
+
551
+ if show_progress:
552
+ size_mb = partition.total_size / (1024 * 1024)
553
+ speed_mb = size_mb / elapsed if elapsed > 0 else 0
554
+ print(f"[{completed}/{len(partitions)}] {name}: "
555
+ f"{size_mb:.1f}MB in {elapsed:.2f}s ({speed_mb:.1f}MB/s)")
556
+
557
+ except Exception as e:
558
+ print(f"Error extracting {partition.name}: {e}")
559
+
560
+ if show_progress and len(partitions) > 1:
561
+ avg_time = total_time / len(partitions)
562
+ print(f"\nCompleted! Average extraction time: {avg_time:.2f}s per partition")
563
+ print(f"Total extraction time: {total_time:.2f}s")
564
+
565
+
566
+ def main():
567
+ ascii = r"""
568
+
569
+ /\ /\ _ __ ___ _ _ _ __ ___ _ __
570
+ / / \ \ '_ \/ __| | | | '_ \ / _ \ '__|
571
+ \ \_/ / | | \__ \ |_| | |_) | __/ |
572
+ \___/|_| |_|___/\__,_| .__/ \___|_|
573
+ |_|
574
+ Fastest super.img dumper ever
575
+
576
+ """
577
+
578
+ parser = argparse.ArgumentParser(
579
+ description=ascii,
580
+ usage="%(prog)s super_image [output_dir] [-h] [-p PARTITIONS [PARTITIONS ...]] [-j JOBS] [-q] [--list] [--temp-dir TEMP_DIR] [--version] [--unsparse]",
581
+ formatter_class=argparse.RawDescriptionHelpFormatter
582
+ )
583
+
584
+ parser.add_argument(
585
+ '--version',
586
+ action='version',
587
+ version=f'%(prog)s V{VERSION}'
588
+ )
589
+
590
+ parser.add_argument(
591
+ 'super_image',
592
+ help='Path to super.img file'
593
+ )
594
+
595
+ parser.add_argument(
596
+ 'output_dir',
597
+ nargs='?',
598
+ default='extracted_partitions',
599
+ help='Output directory for extracted partitions (default: extracted_partitions)'
600
+ )
601
+
602
+ parser.add_argument(
603
+ '-p', '--partitions',
604
+ nargs='+',
605
+ help='Specific partition names to extract (vendor, product etc.) (default: extract all)'
606
+ )
607
+
608
+ parser.add_argument(
609
+ '-j', '--jobs',
610
+ type=int,
611
+ default=4,
612
+ help='Number of parallel extraction threads (default: 4)'
613
+ )
614
+
615
+ parser.add_argument(
616
+ '-q', '--quiet',
617
+ action='store_true',
618
+ help='Suppress progress output'
619
+ )
620
+
621
+ parser.add_argument(
622
+ '--list',
623
+ action='store_true',
624
+ help='List available partitions and exit'
625
+ )
626
+
627
+ parser.add_argument(
628
+ '--temp-dir',
629
+ help='Directory for temporary unsparse file (default: system temp directory)'
630
+ )
631
+
632
+ parser.add_argument(
633
+ '--unsparse',
634
+ action='store_true',
635
+ help='Unsparse image and save to output directory'
636
+ )
637
+
638
+ args = parser.parse_args()
639
+
640
+ # Validate input file
641
+ input_path = Path(args.super_image)
642
+ if not input_path.exists():
643
+ print(f"Error: {args.super_image} not found")
644
+ sys.exit(1)
645
+
646
+ if input_path.stat().st_size == 0:
647
+ print(f"Error: {args.super_image} is empty")
648
+ sys.exit(1)
649
+
650
+ unsparse_file = None # Track unsparse file for cleanup
651
+
652
+ try:
653
+ input_file = args.super_image
654
+ unsparse_created_by_flag = False # Track if unsparse file was created by --unsparse flag
655
+
656
+ # Check if input is sparse and convert if needed
657
+ with open(input_file, 'rb') as f:
658
+ magic_data = f.read(4)
659
+ if len(magic_data) < 4:
660
+ raise FastDumperError("File too small to determine format")
661
+ magic = struct.unpack('<I', magic_data)[0]
662
+
663
+ if magic == SPARSE_HEADER_MAGIC:
664
+ if not args.quiet:
665
+ print("Sparse image detected, converting...")
666
+
667
+ # If --unsparse flag is used, save to output directory and use for extraction
668
+ if args.unsparse:
669
+ output_path = Path(args.output_dir)
670
+ output_path.mkdir(parents=True, exist_ok=True)
671
+ unsparse_output = output_path / f"{Path(args.super_image).stem}.unsparse.img"
672
+
673
+ converter = FastSparseConverter(input_file, str(output_path))
674
+ converted_file = converter.convert()
675
+
676
+ # Move/rename to desired location if needed
677
+ if converted_file != str(unsparse_output):
678
+ import shutil
679
+ shutil.move(converted_file, unsparse_output)
680
+
681
+ # Use the unsparsed file for further processing
682
+ input_file = str(unsparse_output)
683
+ unsparse_file = str(unsparse_output)
684
+ unsparse_created_by_flag = True
685
+
686
+ if not args.quiet:
687
+ print(f"Unsparsed image saved to: {unsparse_output}")
688
+ else:
689
+ # Normal temp conversion for extraction
690
+ converter = FastSparseConverter(input_file, args.temp_dir)
691
+ unsparse_file = converter.convert()
692
+ input_file = unsparse_file
693
+
694
+ if not args.quiet:
695
+ print(f"Converted to temporary file: {input_file}")
696
+ elif args.unsparse:
697
+ # Image is already unsparsed, just copy it to output directory
698
+ output_path = Path(args.output_dir)
699
+ output_path.mkdir(parents=True, exist_ok=True)
700
+ unsparse_output = output_path / f"{Path(args.super_image).stem}.unsparse.img"
701
+
702
+ import shutil
703
+ shutil.copy2(args.super_image, unsparse_output)
704
+
705
+ # Use the copied file for further processing
706
+ input_file = str(unsparse_output)
707
+ unsparse_file = str(unsparse_output)
708
+ unsparse_created_by_flag = True
709
+
710
+ if not args.quiet:
711
+ print(f"Image copied to: {unsparse_output} (was already unsparsed)")
712
+
713
+ # If --unsparse is used without any extraction arguments, exit after unsparsing
714
+ if args.unsparse and not args.partitions and not args.list:
715
+ if not args.quiet:
716
+ print("Unsparsing complete! Use --partitions to also extract partitions.")
717
+ return
718
+
719
+ # Parse metadata
720
+ if not args.quiet:
721
+ print("Parsing metadata...")
722
+
723
+ parser_obj = FastMetadataParser(input_file)
724
+ partitions = parser_obj.parse_partitions(args.partitions)
725
+
726
+ if not partitions:
727
+ if args.partitions:
728
+ print(f"No matching partitions found for: {', '.join(args.partitions)}")
729
+ else:
730
+ print("No partitions found in image")
731
+ sys.exit(1)
732
+
733
+ # List partitions if requested
734
+ if args.list:
735
+ print(f"\nFound {len(partitions)} partitions:")
736
+ total_size = 0
737
+ for partition in partitions:
738
+ size_mb = partition.total_size / (1024 * 1024)
739
+ total_size += partition.total_size
740
+ print(f" {partition.name}: {size_mb:.1f}MB ({len(partition.extents)} extents)")
741
+
742
+ total_mb = total_size / (1024 * 1024)
743
+ print(f"\nTotal size: {total_mb:.1f}MB")
744
+ return
745
+
746
+ # Extract partitions
747
+ extractor = FastExtractor(input_file, args.output_dir, args.jobs)
748
+ extractor.extract_all(partitions, not args.quiet)
749
+
750
+ if not args.quiet:
751
+ print(f"\nExtraction complete! Files saved to: {args.output_dir}")
752
+ if args.unsparse:
753
+ print(f"Unsparsed image saved to: {input_file}")
754
+
755
+ except FastDumperError as e:
756
+ print(f"Error: {e}")
757
+ sys.exit(1)
758
+ except KeyboardInterrupt:
759
+ print("\nOperation cancelled by user")
760
+ sys.exit(1)
761
+ except Exception as e:
762
+ print(f"Unexpected error: {e}")
763
+ sys.exit(1)
764
+ finally:
765
+ # Clean up temporary unsparse file only if it wasn't created by --unsparse flag
766
+ if unsparse_file and os.path.exists(unsparse_file) and not unsparse_created_by_flag:
767
+ try:
768
+ os.remove(unsparse_file)
769
+ if not args.quiet:
770
+ print(f"Temporary sparse image cleaned successfully.")
771
+ except Exception as e:
772
+ print(f"Warning: Could not remove temporary file {unsparse_file}: {e}")
773
+
774
+ if __name__ == "__main__":
775
+ main()