Skip to content

Commit 4403460

Browse files
committedFeb 26, 2022
dumpyara: Initial release
Change-Id: I869977019333cf23937e6d8bff1efd555d262fde
0 parents  commit 4403460

16 files changed

+1354
-0
lines changed
 

‎.gitignore

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Byte-compiled / optimized / DLL files
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
6+
# Distribution / packaging
7+
.Python
8+
build/
9+
develop-eggs/
10+
dist/
11+
downloads/
12+
eggs/
13+
.eggs/
14+
lib/
15+
lib64/
16+
parts/
17+
sdist/
18+
var/
19+
wheels/
20+
pip-wheel-metadata/
21+
share/python-wheels/
22+
*.egg-info/
23+
.installed.cfg
24+
*.egg
25+
MANIFEST
26+
27+
# PyInstaller
28+
# Usually these files are written by a python script from a template
29+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
30+
*.manifest
31+
*.spec
32+
33+
# Installer logs
34+
pip-log.txt
35+
pip-delete-this-directory.txt
36+
37+
# Unit test / coverage reports
38+
htmlcov/
39+
.tox/
40+
.nox/
41+
.coverage
42+
.coverage.*
43+
.cache
44+
nosetests.xml
45+
coverage.xml
46+
*.cover
47+
*.py,cover
48+
.hypothesis/
49+
.pytest_cache/
50+
51+
# Sphinx documentation
52+
docs/_build/
53+
54+
# PyBuilder
55+
target/
56+
57+
# pyenv
58+
.python-version
59+
60+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
61+
__pypackages__/
62+
63+
# Environments
64+
.env
65+
.venv
66+
env/
67+
venv/
68+
ENV/
69+
env.bak/
70+
venv.bak/
71+
roject
72+
73+
# mkdocs documentation
74+
/site
75+
76+
# mypy
77+
.mypy_cache/
78+
.dmypy.json
79+
dmypy.json
80+
81+
# Pyre type checker
82+
.pyre/
83+
84+
# editors
85+
.idea/
86+
.vscode/
87+
88+
# Don't ignore library folder
89+
!/dumpyara/lib/
90+
91+
# Ignore output folder
92+
working/

‎README.md

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Dumpyara
2+
3+
## Installation
4+
5+
```
6+
pip3 install dumpyara
7+
```
8+
The module is supported on Python 3.8 and above.
9+
10+
## How to use
11+
12+
```
13+
$ python3 -m dumpyara -h
14+
Dumpyara
15+
Version 1.0.0
16+
17+
usage: python3 -m dumpyara [-h] [-o OUTPUT] [-v] file
18+
19+
positional arguments:
20+
file path to a device OTA
21+
22+
optional arguments:
23+
-h, --help show this help message and exit
24+
-o OUTPUT, --output OUTPUT
25+
custom output folder
26+
-v, --verbose enable debugging logging
27+
```
28+
29+
## Supported formats
30+
31+
### Archives
32+
- All the ones supported by shutil's extract_archive
33+
34+
### Images
35+
- Raw images
36+
- Brotli compressed images
37+
- sdat images
38+
- Sparsed images
39+
40+
## Credits
41+
- lpunpack: unix3dgforce
42+
- sdat2img: xpirt

‎dumpyara/__init__.py

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#
2+
# Copyright (C) 2022 Dumpyara Project
3+
#
4+
# SPDX-License-Identifier: GPL-3.0
5+
#
6+
7+
import os
8+
from pathlib import Path
9+
10+
__version__ = "1.0.0"
11+
12+
module_path = Path(__file__).parent
13+
current_path = Path(os.getcwd())

‎dumpyara/__main__.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/usr/bin/python3
2+
#
3+
# Copyright (C) 2022 Dumpyara Project
4+
#
5+
# SPDX-License-Identifier: GPL-3.0
6+
#
7+
8+
from dumpyara.main import main
9+
10+
if __name__ == '__main__':
11+
main()

‎dumpyara/dumpyara.py

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#
2+
# Copyright (C) 2022 Dumpyara Project
3+
#
4+
# SPDX-License-Identifier: GPL-3.0
5+
#
6+
7+
from dumpyara.lib.liblogging import LOGD, LOGI
8+
from dumpyara.lib.liblpunpack import LpUnpack
9+
from dumpyara.utils.partitions import can_be_partition, extract_partition
10+
import fnmatch
11+
from os import walk
12+
from pathlib import Path
13+
from shutil import unpack_archive
14+
from tempfile import TemporaryDirectory
15+
16+
class Dumpyara:
17+
"""
18+
A class representing an Android dump
19+
"""
20+
def __init__(self, file: Path, output_path: Path) -> None:
21+
"""Initialize dumpyara class."""
22+
self.file = file
23+
self.output_path = output_path
24+
25+
# Create working folder dir
26+
self.output_path.mkdir(exist_ok=True, parents=True)
27+
28+
# Create a temporary directory where we will extract the images
29+
self.tempdir = TemporaryDirectory()
30+
self.tempdir_path = Path(self.tempdir.name)
31+
32+
# Output folder
33+
self.path = self.output_path / self.file.stem
34+
self.path.mkdir()
35+
36+
self.fileslist = []
37+
38+
LOGI("Extracting package...")
39+
unpack_archive(self.file, self.tempdir_path)
40+
self.update_tempdir_files_list()
41+
LOGD(f"All files in package: {', '.join(self.fileslist)}")
42+
43+
# Extract super first if it exists
44+
# It contains all the partitions that we are interested in
45+
super_match = fnmatch.filter(self.fileslist, "*super.img*")
46+
if super_match:
47+
LOGI("Super partition detected, first extracting it")
48+
LpUnpack(SUPER_IMAGE=self.tempdir_path / super_match[0], OUTPUT_DIR=self.tempdir_path).unpack()
49+
self.update_tempdir_files_list()
50+
51+
# Process all files
52+
for file in [self.tempdir_path / file for file in self.fileslist]:
53+
# Might have been deleted by previous step
54+
if not file.is_file():
55+
continue
56+
57+
if can_be_partition(file):
58+
extract_partition(file, self.path)
59+
else:
60+
LOGI(f"Skipping {file}")
61+
62+
# We don't need artifacts anymore
63+
self.tempdir.cleanup()
64+
65+
# Create all_files.txt
66+
LOGI("Creating all_files.txt")
67+
with open(self.path / "all_files.txt", "w") as f:
68+
f.write("\n".join(self.get_recursive_files_list(self.path, relative=True)))
69+
70+
@staticmethod
71+
def get_recursive_files_list(path: Path, relative=False):
72+
files_list = []
73+
74+
for currentpath, _, files in walk(path):
75+
for file in files:
76+
file_path = Path(currentpath) / file
77+
if relative:
78+
file_path = file_path.relative_to(path)
79+
files_list.append(str(file_path))
80+
81+
return files_list
82+
83+
def update_tempdir_files_list(self):
84+
self.fileslist.clear()
85+
self.fileslist.extend(self.get_recursive_files_list(self.tempdir_path))

‎dumpyara/lib/liblogging/__init__.py

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#
2+
# Copyright (C) 2022 Dumpyara Project
3+
#
4+
# SPDX-License-Identifier: GPL-3.0
5+
#
6+
7+
from logging import debug, info, warning, error, fatal
8+
9+
LOGD = debug
10+
LOGI = info
11+
LOGW = warning
12+
LOGE = error
13+
LOGF = fatal

‎dumpyara/lib/liblpunpack/__init__.py

+453
Large diffs are not rendered by default.

‎dumpyara/lib/libsdat2img/__init__.py

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
#====================================================
4+
# FILE: sdat2img.py
5+
# AUTHORS: xpirt - luxi78 - howellzhu
6+
# DATE: 2018-10-27 10:33:21 CEST
7+
#====================================================
8+
9+
from __future__ import print_function
10+
import sys, os, errno
11+
12+
def main(TRANSFER_LIST_FILE, NEW_DATA_FILE, OUTPUT_IMAGE_FILE):
13+
__version__ = '1.2'
14+
15+
if sys.hexversion < 0x02070000:
16+
print >> sys.stderr, "Python 2.7 or newer is required."
17+
try:
18+
input = raw_input
19+
except NameError: pass
20+
input('Press ENTER to exit...')
21+
sys.exit(1)
22+
else:
23+
print('sdat2img binary - version: {}\n'.format(__version__))
24+
25+
def rangeset(src):
26+
src_set = src.split(',')
27+
num_set = [int(item) for item in src_set]
28+
if len(num_set) != num_set[0]+1:
29+
print('Error on parsing following data to rangeset:\n{}'.format(src), file=sys.stderr)
30+
sys.exit(1)
31+
32+
return tuple ([ (num_set[i], num_set[i+1]) for i in range(1, len(num_set), 2) ])
33+
34+
def parse_transfer_list_file(path):
35+
trans_list = open(TRANSFER_LIST_FILE, 'r')
36+
37+
# First line in transfer list is the version number
38+
version = int(trans_list.readline())
39+
40+
# Second line in transfer list is the total number of blocks we expect to write
41+
new_blocks = int(trans_list.readline())
42+
43+
if version >= 2:
44+
# Third line is how many stash entries are needed simultaneously
45+
trans_list.readline()
46+
# Fourth line is the maximum number of blocks that will be stashed simultaneously
47+
trans_list.readline()
48+
49+
# Subsequent lines are all individual transfer commands
50+
commands = []
51+
for line in trans_list:
52+
line = line.split(' ')
53+
cmd = line[0]
54+
if cmd in ['erase', 'new', 'zero']:
55+
commands.append([cmd, rangeset(line[1])])
56+
else:
57+
# Skip lines starting with numbers, they are not commands anyway
58+
if not cmd[0].isdigit():
59+
print('Command "{}" is not valid.'.format(cmd), file=sys.stderr)
60+
trans_list.close()
61+
sys.exit(1)
62+
63+
trans_list.close()
64+
return version, new_blocks, commands
65+
66+
BLOCK_SIZE = 4096
67+
68+
version, new_blocks, commands = parse_transfer_list_file(TRANSFER_LIST_FILE)
69+
70+
if version == 1:
71+
print('Android Lollipop 5.0 detected!\n')
72+
elif version == 2:
73+
print('Android Lollipop 5.1 detected!\n')
74+
elif version == 3:
75+
print('Android Marshmallow 6.x detected!\n')
76+
elif version == 4:
77+
print('Android Nougat 7.x / Oreo 8.x detected!\n')
78+
else:
79+
print('Unknown Android version!\n')
80+
81+
# Don't clobber existing files to avoid accidental data loss
82+
try:
83+
output_img = open(OUTPUT_IMAGE_FILE, 'wb')
84+
except IOError as e:
85+
if e.errno == errno.EEXIST:
86+
print('Error: the output file "{}" already exists'.format(e.filename), file=sys.stderr)
87+
print('Remove it, rename it, or choose a different file name.', file=sys.stderr)
88+
sys.exit(e.errno)
89+
else:
90+
raise
91+
92+
new_data_file = open(NEW_DATA_FILE, 'rb')
93+
all_block_sets = [i for command in commands for i in command[1]]
94+
max_file_size = max(pair[1] for pair in all_block_sets)*BLOCK_SIZE
95+
96+
for command in commands:
97+
if command[0] == 'new':
98+
for block in command[1]:
99+
begin = block[0]
100+
end = block[1]
101+
block_count = end - begin
102+
print('Copying {} blocks into position {}...'.format(block_count, begin))
103+
104+
# Position output file
105+
output_img.seek(begin*BLOCK_SIZE)
106+
107+
# Copy one block at a time
108+
while(block_count > 0):
109+
output_img.write(new_data_file.read(BLOCK_SIZE))
110+
block_count -= 1
111+
else:
112+
print('Skipping command {}...'.format(command[0]))
113+
114+
# Make file larger if necessary
115+
if(output_img.tell() < max_file_size):
116+
output_img.truncate(max_file_size)
117+
118+
output_img.close()
119+
new_data_file.close()
120+
print('Done! Output image: {}'.format(os.path.realpath(output_img.name)))
121+
122+
if __name__ == '__main__':
123+
try:
124+
TRANSFER_LIST_FILE = str(sys.argv[1])
125+
NEW_DATA_FILE = str(sys.argv[2])
126+
except IndexError:
127+
print('\nUsage: sdat2img.py <transfer_list> <system_new_file> [system_img]\n')
128+
print(' <transfer_list>: transfer list file')
129+
print(' <system_new_file>: system new dat file')
130+
print(' [system_img]: output system image\n\n')
131+
print('Visit xda thread for more information.\n')
132+
try:
133+
input = raw_input
134+
except NameError: pass
135+
input('Press ENTER to exit...')
136+
sys.exit()
137+
138+
try:
139+
OUTPUT_IMAGE_FILE = str(sys.argv[3])
140+
except IndexError:
141+
OUTPUT_IMAGE_FILE = 'system.img'
142+
143+
main(TRANSFER_LIST_FILE, NEW_DATA_FILE, OUTPUT_IMAGE_FILE)

‎dumpyara/lib/libsevenz/__init__.py

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#
2+
# Copyright (C) 2022 Dumpyara Project
3+
#
4+
# SPDX-License-Identifier: GPL-3.0
5+
#
6+
7+
from subprocess import check_output
8+
9+
def sevenz(command: str):
10+
return check_output(f"7z {command}", shell=True)

‎dumpyara/main.py

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#
2+
# Copyright (C) 2022 Dumpyara Project
3+
#
4+
# SPDX-License-Identifier: GPL-3.0
5+
#
6+
7+
from argparse import ArgumentParser
8+
from dumpyara import __version__ as version, current_path
9+
from dumpyara.dumpyara import Dumpyara
10+
from dumpyara.utils.logging import setup_logging
11+
from pathlib import Path
12+
13+
def main():
14+
print(f"Dumpyara\n"
15+
f"Version {version}\n")
16+
17+
parser = ArgumentParser(prog='python3 -m dumpyara')
18+
19+
# Main arguments
20+
parser.add_argument("file", type=Path,
21+
help="path to a device OTA")
22+
parser.add_argument("-o", "--output", type=Path, default=current_path / "working",
23+
help="custom output folder")
24+
25+
# Optional arguments
26+
parser.add_argument("-v", "--verbose", action='store_true',
27+
help="enable debugging logging")
28+
29+
args = parser.parse_args()
30+
31+
setup_logging(args.verbose)
32+
33+
dumpyara = Dumpyara(args.file, args.output)
34+
35+
print(f"\nDone! You can find the dump in {str(dumpyara.path)}")

‎dumpyara/utils/build_prop.py

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#
2+
# Copyright (C) 2022 Dumpyara Project
3+
#
4+
# SPDX-License-Identifier: GPL-3.0
5+
#
6+
7+
from pathlib import Path
8+
from typing import Union
9+
10+
class BuildProp:
11+
"""
12+
A class representing a build prop.
13+
14+
This class basically mimics Android props system, with both getprop and setprop commands
15+
"""
16+
def __init__(self, file: Path):
17+
"""
18+
Create a dictionary containing all the key-value from a build prop.
19+
"""
20+
self.file = file.read_text()
21+
self.props = {}
22+
23+
for prop in self.file.splitlines():
24+
if prop.startswith("#"):
25+
continue
26+
try:
27+
prop_name, prop_value = prop.split("=", 1)
28+
except ValueError:
29+
continue
30+
else:
31+
self.set_prop(prop_name, prop_value)
32+
33+
def get_prop(self, prop: str) -> Union[str, None]:
34+
"""
35+
From a prop name, return the prop value.
36+
37+
Returns a string if it exists, else None
38+
"""
39+
try:
40+
return self.props[prop]
41+
except KeyError:
42+
return None
43+
44+
def set_prop(self, prop_name: str, prop_value: str):
45+
self.props[prop_name] = prop_value

‎dumpyara/utils/logging.py

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#
2+
# Copyright (C) 2022 Dumpyara Project
3+
#
4+
# SPDX-License-Identifier: GPL-3.0
5+
#
6+
7+
from logging import basicConfig, INFO, DEBUG
8+
9+
def setup_logging(verbose=False):
10+
if verbose:
11+
basicConfig(format='[%(filename)s:%(lineno)s %(levelname)s] %(funcName)s: %(message)s',
12+
level=DEBUG)
13+
else:
14+
basicConfig(format='[%(levelname)s] %(message)s', level=INFO)

‎dumpyara/utils/partitions.py

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#
2+
# Copyright (C) 2022 Dumpyara Project
3+
#
4+
# SPDX-License-Identifier: GPL-3.0
5+
#
6+
7+
from dumpyara.lib.liblogging import LOGE, LOGI
8+
from dumpyara.lib.libsevenz import sevenz
9+
from dumpyara.utils.raw_image import get_raw_image
10+
from pathlib import Path
11+
from shutil import copyfile
12+
from subprocess import CalledProcessError
13+
14+
# partition: is_raw
15+
PARTITIONS = {
16+
"boot": True,
17+
"dtbo": True,
18+
"cust": False,
19+
"factory": False,
20+
"india": True,
21+
"my_preload": True,
22+
"my_odm": True,
23+
"my_stock": True,
24+
"my_operator": True,
25+
"my_country": True,
26+
"my_product": True,
27+
"my_company": True,
28+
"my_engineering": True,
29+
"my_heytap": True,
30+
"odm": False,
31+
"oem": False,
32+
"oppo_product": False,
33+
"opproduct": True,
34+
"preload_common": False,
35+
"product": False,
36+
"recovery": True,
37+
"reserve": True,
38+
"system": False,
39+
"system_ext": False,
40+
"system_other": False,
41+
"systemex": False,
42+
"vendor": False,
43+
"xrom": False,
44+
"modem": True,
45+
"tz": True,
46+
}
47+
48+
# alternative name: generic name
49+
ALTERNATIVE_PARTITION_NAMES = {
50+
"boot-verified": "boot",
51+
"dtbo-verified": "dtbo",
52+
"NON-HLOS": "modem",
53+
}
54+
55+
def get_partition_name(file: Path):
56+
for partition in list(PARTITIONS) + list(ALTERNATIVE_PARTITION_NAMES):
57+
possible_names = [
58+
partition,
59+
f"{partition}.bin",
60+
f"{partition}.ext4",
61+
f"{partition}.image",
62+
f"{partition}.img",
63+
f"{partition}.mbn",
64+
f"{partition}.new.dat",
65+
f"{partition}.new.dat.br",
66+
f"{partition}.raw",
67+
f"{partition}.raw.img",
68+
]
69+
if file.name in possible_names:
70+
return partition
71+
72+
return None
73+
74+
def can_be_partition(file: Path):
75+
"""Check if the file can be a partition."""
76+
return get_partition_name(file) is not None
77+
78+
def extract_partition(file: Path, output_path: Path):
79+
"""
80+
Extract files from partition image.
81+
82+
If the partition is raw, the file will simply be copied to the output folder,
83+
else extracted using 7z (unsparsed if needed).
84+
"""
85+
partition_name = get_partition_name(file)
86+
if not partition_name:
87+
LOGI(f"Skipping {file.stem}")
88+
return
89+
90+
new_partition_name = ALTERNATIVE_PARTITION_NAMES.get(partition_name, partition_name)
91+
92+
if PARTITIONS[new_partition_name]:
93+
copyfile(file, output_path / f"{new_partition_name}.img", follow_symlinks=True)
94+
else:
95+
# Make sure we have a raw image
96+
raw_image = get_raw_image(partition_name, file.parent)
97+
98+
# TODO: EROFS
99+
try:
100+
sevenz(f'x {raw_image} -y -o"{output_path / new_partition_name}"/')
101+
except CalledProcessError as e:
102+
LOGE(f"Error extracting {file.stem}")
103+
LOGE(f"{e.output}")
104+
raise e

‎dumpyara/utils/raw_image.py

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#
2+
# Copyright (C) 2022 Dumpyara Project
3+
#
4+
# SPDX-License-Identifier: GPL-3.0
5+
#
6+
7+
import brotli
8+
from dumpyara.lib.liblogging import LOGI
9+
from dumpyara.lib.liblpunpack import SparseImage
10+
from dumpyara.lib.libsdat2img import main as sdat2img
11+
from pathlib import Path
12+
from shutil import move
13+
14+
def get_raw_image(partition: str, path: Path):
15+
"""
16+
Convert a partition image to a raw image.
17+
18+
This function handles brotli compression, sdat and sparse images.
19+
"""
20+
brotli_image = path / f"{partition}.new.dat.br"
21+
dat_image = path / f"{partition}.new.dat"
22+
transfer_list = path / f"{partition}.transfer.list"
23+
raw_image = path / f"{partition}.img"
24+
25+
if brotli_image.is_file():
26+
LOGI(f"Decompressing {brotli_image}")
27+
with brotli_image.open("rb") as f:
28+
with dat_image.open("wb") as g:
29+
g.write(brotli.decompress(f.read()))
30+
brotli_image.unlink()
31+
32+
if dat_image.is_file() and transfer_list.is_file():
33+
LOGI(f"Converting {dat_image} to {raw_image}")
34+
sdat2img(transfer_list, dat_image, raw_image)
35+
dat_image.unlink()
36+
transfer_list.unlink()
37+
38+
if raw_image.is_file():
39+
with raw_image.open("rb") as f:
40+
sparse_image = SparseImage(f)
41+
if sparse_image.check():
42+
LOGI(f"{raw_image.stem} is a sparse image, extracting...")
43+
unsparse_file = sparse_image.unsparse()
44+
move(unsparse_file, raw_image)
45+
46+
return raw_image
47+
48+
return None

‎poetry.lock

+227
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎pyproject.toml

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[tool.poetry]
2+
name = "dumpyara"
3+
version = "1.0.0"
4+
description = "Dumpyara"
5+
license = "GPL-3.0"
6+
readme = "README.md"
7+
repository = "https://github.com/SebaUbuntu/TWRP-device-tree-generator"
8+
authors = ["Sebastiano Barezzi <barezzisebastiano@gmail.com>"]
9+
10+
[tool.poetry.dependencies]
11+
python = "^3.8"
12+
Brotli = "^1.0.9"
13+
14+
[tool.poetry.dev-dependencies]
15+
pytest = "^6.2.1"
16+
17+
[build-system]
18+
requires = ["poetry>=0.12"]
19+
build-backend = "poetry.masonry.api"

0 commit comments

Comments
 (0)
Please sign in to comment.