psptool 3.0__tar.gz → 3.2__tar.gz
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.
- psptool-3.2/.github/workflows/gha-metrics.yml +33 -0
- psptool-3.2/.github/workflows/publish.yml +51 -0
- {psptool-3.0 → psptool-3.2}/PKG-INFO +5 -16
- {psptool-3.0 → psptool-3.2}/psptool/__init__.py +2 -2
- {psptool-3.0 → psptool-3.2}/psptool/__main__.py +101 -40
- {psptool-3.0 → psptool-3.2}/psptool/blob.py +5 -1
- {psptool-3.0 → psptool-3.2}/psptool/directory.py +18 -16
- psptool-3.2/psptool/entry.py +180 -0
- {psptool-3.0 → psptool-3.2}/psptool/fet.py +16 -3
- {psptool-3.0 → psptool-3.2}/psptool/file.py +162 -42
- psptool-3.2/psptool/microcode_file.py +60 -0
- {psptool-3.0 → psptool-3.2}/psptool/psptool.py +30 -3
- psptool-3.2/pyproject.toml +21 -0
- psptool-3.2/tests/gha_metrics.py +80 -0
- {psptool-3.0 → psptool-3.2}/tests/metrics.txt +70 -70
- psptool-3.0/psptool/entry.py +0 -96
- psptool-3.0/psptool.egg-info/PKG-INFO +0 -883
- psptool-3.0/psptool.egg-info/SOURCES.txt +0 -38
- psptool-3.0/psptool.egg-info/dependency_links.txt +0 -1
- psptool-3.0/psptool.egg-info/entry_points.txt +0 -2
- psptool-3.0/psptool.egg-info/requires.txt +0 -3
- psptool-3.0/psptool.egg-info/top_level.txt +0 -1
- psptool-3.0/setup.cfg +0 -27
- psptool-3.0/setup.py +0 -13
- {psptool-3.0 → psptool-3.2}/.github/workflows/python-tests.yml +0 -0
- {psptool-3.0 → psptool-3.2}/.gitignore +0 -0
- {psptool-3.0 → psptool-3.2}/.gitmodules +0 -0
- {psptool-3.0 → psptool-3.2}/LICENSE +0 -0
- {psptool-3.0 → psptool-3.2}/README.md +0 -0
- {psptool-3.0 → psptool-3.2}/psptool/cert_tree.py +0 -0
- {psptool-3.0 → psptool-3.2}/psptool/crypto.py +0 -0
- {psptool-3.0 → psptool-3.2}/psptool/errors.py +0 -0
- {psptool-3.0 → psptool-3.2}/psptool/firmware.py +0 -0
- {psptool-3.0 → psptool-3.2}/psptool/header_file.py +0 -0
- {psptool-3.0 → psptool-3.2}/psptool/key_store_file.py +0 -0
- {psptool-3.0 → psptool-3.2}/psptool/pubkey_file.py +0 -0
- {psptool-3.0 → psptool-3.2}/psptool/rom.py +0 -0
- {psptool-3.0 → psptool-3.2}/psptool/utils.py +0 -0
- {psptool-3.0 → psptool-3.2}/tests/__init__.py +0 -0
- {psptool-3.0 → psptool-3.2}/tests/create_metrics.sh +0 -0
- {psptool-3.0 → psptool-3.2}/tests/integration/__init__.py +0 -0
- {psptool-3.0 → psptool-3.2}/tests/integration/test_psptrace.py +0 -0
- {psptool-3.0 → psptool-3.2}/tests/integration/test_rom_files.py +0 -0
- {psptool-3.0 → psptool-3.2}/tests/unit/__init__.py +0 -0
- {psptool-3.0 → psptool-3.2}/tests/unit/test_cli.py +0 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Post metrics comment
|
|
2
|
+
|
|
3
|
+
on: [pull_request_target]
|
|
4
|
+
|
|
5
|
+
jobs:
|
|
6
|
+
test:
|
|
7
|
+
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
strategy:
|
|
10
|
+
matrix:
|
|
11
|
+
python-version: ["3.9"]
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v3
|
|
15
|
+
with:
|
|
16
|
+
submodules: recursive
|
|
17
|
+
ssh-key: ${{ secrets.PSPTOOL_FIXTURES_NEW_PRIVATE_KEY }}
|
|
18
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
19
|
+
uses: actions/setup-python@v3
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python-version }}
|
|
22
|
+
- name: Install dependencies
|
|
23
|
+
run: |
|
|
24
|
+
python -m pip install --upgrade pip
|
|
25
|
+
pip install .
|
|
26
|
+
- name: Run metrics and post comment
|
|
27
|
+
run: |
|
|
28
|
+
python tests/gha_metrics.py > metrics.md
|
|
29
|
+
gh pr comment $PRNUM --body-file metrics.md
|
|
30
|
+
env:
|
|
31
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
32
|
+
GH_REPO: ${{ github.repository }}
|
|
33
|
+
PRNUM: ${{ github.event.pull_request.number }}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: Publish Python distribution to PyPI and TestPyPI
|
|
2
|
+
|
|
3
|
+
on: push
|
|
4
|
+
|
|
5
|
+
jobs:
|
|
6
|
+
build:
|
|
7
|
+
name: Build distribution
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
with:
|
|
13
|
+
persist-credentials: false
|
|
14
|
+
- name: Set up Python
|
|
15
|
+
uses: actions/setup-python@v5
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.11"
|
|
18
|
+
- name: Install pypa/build
|
|
19
|
+
run: >-
|
|
20
|
+
python3 -m
|
|
21
|
+
pip install
|
|
22
|
+
build
|
|
23
|
+
--user
|
|
24
|
+
- name: Build a binary wheel and a source tarball
|
|
25
|
+
run: python3 -m build
|
|
26
|
+
- name: Store the distribution packages
|
|
27
|
+
uses: actions/upload-artifact@v4
|
|
28
|
+
with:
|
|
29
|
+
name: python-package-distributions
|
|
30
|
+
path: dist/
|
|
31
|
+
|
|
32
|
+
publish-to-pypi:
|
|
33
|
+
name: >-
|
|
34
|
+
Publish Python distribution to PyPI
|
|
35
|
+
if: startsWith(github.ref, 'refs/tags') # only publish on tag push
|
|
36
|
+
needs:
|
|
37
|
+
- build
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
environment:
|
|
40
|
+
name: pypi
|
|
41
|
+
url: https://pypi.org/p/psptool/
|
|
42
|
+
permissions:
|
|
43
|
+
id-token: write # important for trusted publishing
|
|
44
|
+
steps:
|
|
45
|
+
- name: Download all the dists
|
|
46
|
+
uses: actions/download-artifact@v4
|
|
47
|
+
with:
|
|
48
|
+
name: python-package-distributions
|
|
49
|
+
path: dist/
|
|
50
|
+
- name: Publish distribution to PyPI
|
|
51
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -1,24 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: psptool
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2
|
|
4
4
|
Summary: PSPTool is a tool for dealing with AMD binary blobs
|
|
5
|
-
|
|
6
|
-
Author: Christian Werling
|
|
7
|
-
Author-email: cwerling@posteo.de
|
|
8
|
-
Classifier: Development Status :: 4 - Beta
|
|
9
|
-
Classifier: Intended Audience :: Science/Research
|
|
10
|
-
Classifier: Topic :: Security
|
|
11
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
-
Requires-Python: >=3.8
|
|
14
|
-
Description-Content-Type: text/markdown
|
|
5
|
+
Author-email: Christian Werling <cwerling@posteo.de>
|
|
15
6
|
License-File: LICENSE
|
|
16
|
-
Requires-
|
|
7
|
+
Requires-Python: >=3.8
|
|
17
8
|
Requires-Dist: cryptography>=38.0.0
|
|
18
9
|
Requires-Dist: prettytable
|
|
19
|
-
|
|
20
|
-
Dynamic: description-content-type
|
|
21
|
-
Dynamic: license-file
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
22
11
|
|
|
23
12
|
# PSPTool
|
|
24
13
|
|
|
@@ -880,4 +869,4 @@ b'\x01\x00\x00\x00\x1b\xb9\x87\xc3YIF\x06\xb1t\x94V\x01\xc9\xea[\x1b\xb9\x87\xc3
|
|
|
880
869
|
> psp.blob.roms[0].directories[0].files[1].move_buffer(0x60000, 0x1000)
|
|
881
870
|
> psp.blob.roms[0].set_bytes(0x60000, 0x1000, my_stuff)
|
|
882
871
|
> psp.to_file('my_modified_bios.bin')
|
|
883
|
-
```
|
|
872
|
+
```
|
|
@@ -15,6 +15,6 @@
|
|
|
15
15
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
16
|
|
|
17
17
|
from .psptool import PSPTool
|
|
18
|
+
from importlib.metadata import version
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
__version__ = pkg_resources.get_distribution("psptool").version
|
|
20
|
+
__version__ = version("psptool")
|
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
|
|
17
17
|
import sys
|
|
18
18
|
import os
|
|
19
|
-
import
|
|
19
|
+
import re
|
|
20
20
|
|
|
21
21
|
from .psptool import PSPTool
|
|
22
|
+
from . import __version__
|
|
22
23
|
from .utils import ObligingArgumentParser, PrintHelper
|
|
23
24
|
from .header_file import HeaderFile
|
|
24
25
|
from .pubkey_file import PubkeyFile
|
|
@@ -27,6 +28,36 @@ from .crypto import PrivateKeyDict
|
|
|
27
28
|
from argparse import RawTextHelpFormatter, SUPPRESS
|
|
28
29
|
|
|
29
30
|
|
|
31
|
+
def find_files_by_type_regex(psp, pattern, rom_index=0):
|
|
32
|
+
"""Find files whose type matches the given regex pattern.
|
|
33
|
+
|
|
34
|
+
Returns a tuple of (matching_files, are_identical) where:
|
|
35
|
+
- matching_files: list of matching files
|
|
36
|
+
- are_identical: boolean indicating if all matching files have identical content
|
|
37
|
+
"""
|
|
38
|
+
matching_files = []
|
|
39
|
+
regex = re.compile(pattern, re.IGNORECASE)
|
|
40
|
+
|
|
41
|
+
# Search through all directories in the specified ROM
|
|
42
|
+
for directory in psp.blob.roms[rom_index].directories:
|
|
43
|
+
for file in directory.files:
|
|
44
|
+
if file is not None:
|
|
45
|
+
file_type = file.get_readable_type()
|
|
46
|
+
if regex.search(file_type):
|
|
47
|
+
matching_files.append(file)
|
|
48
|
+
|
|
49
|
+
# Check if all matching files are identical
|
|
50
|
+
are_identical = True
|
|
51
|
+
if len(matching_files) > 1:
|
|
52
|
+
first_bytes = matching_files[0].get_bytes()
|
|
53
|
+
for file in matching_files[1:]:
|
|
54
|
+
if file.get_bytes() != first_bytes:
|
|
55
|
+
are_identical = False
|
|
56
|
+
break
|
|
57
|
+
|
|
58
|
+
return matching_files, are_identical
|
|
59
|
+
|
|
60
|
+
|
|
30
61
|
def main():
|
|
31
62
|
# CLI stuff to create a PSPTool object and interact with it
|
|
32
63
|
parser = ObligingArgumentParser(description='Display, extract, and manipulate AMD PSP firmware inside BIOS ROMs.\n',
|
|
@@ -50,6 +81,7 @@ def main():
|
|
|
50
81
|
parser.add_argument('-m', '--metrics', help=SUPPRESS, action='store_true')
|
|
51
82
|
parser.add_argument('-p', '--privkeystub', help=SUPPRESS)
|
|
52
83
|
parser.add_argument('-a', '--privkeypass', help=SUPPRESS)
|
|
84
|
+
parser.add_argument('-T', '--type-regex', help=SUPPRESS)
|
|
53
85
|
|
|
54
86
|
action = parser.add_mutually_exclusive_group(required=False)
|
|
55
87
|
|
|
@@ -67,11 +99,15 @@ def main():
|
|
|
67
99
|
|
|
68
100
|
action.add_argument('-X', '--extract-file', help='\n'.join([
|
|
69
101
|
'Extract one or more PSP firmware files.',
|
|
70
|
-
'[-d idx [-e idx]] [-n] [-u] [-c] [-k] [-o outfile]',
|
|
102
|
+
'[-d idx [-e idx]] [-n] [-u] [-c] [-k] [-o outfile] [-T regex]',
|
|
71
103
|
'',
|
|
72
|
-
'-r idx: specifies rom_index (default: 0)',
|
|
104
|
+
'-r idx: specifies rom_index (default: ROM 0 only)',
|
|
73
105
|
'-d idx: specifies directory_index (default: all directories)',
|
|
74
106
|
'-e idx: specifies file_index (default: all files)',
|
|
107
|
+
'-T regex: extract file(s) whose type matches the regex pattern',
|
|
108
|
+
' (e.g., "SMUSCS" matches "FW_PSP_SMUSCS~0x5f")',
|
|
109
|
+
' If multiple identical files match, extracts one with warning',
|
|
110
|
+
' If multiple different files match, exits with error',
|
|
75
111
|
'-n: skip duplicate files and extract unique files only',
|
|
76
112
|
'-u: uncompress compressed files',
|
|
77
113
|
'-c: try to decrypt files',
|
|
@@ -97,7 +133,7 @@ def main():
|
|
|
97
133
|
ph = PrintHelper(args.verbose)
|
|
98
134
|
|
|
99
135
|
if args.version:
|
|
100
|
-
print(
|
|
136
|
+
print(__version__)
|
|
101
137
|
sys.exit(0)
|
|
102
138
|
elif not args.file:
|
|
103
139
|
parser.print_help(sys.stderr)
|
|
@@ -107,9 +143,29 @@ def main():
|
|
|
107
143
|
output = None
|
|
108
144
|
|
|
109
145
|
if args.extract_file:
|
|
110
|
-
|
|
146
|
+
file = None
|
|
147
|
+
|
|
148
|
+
# Option 1) A single file should be extracted and we get it from a regex-based type search
|
|
149
|
+
if args.type_regex:
|
|
150
|
+
matching_files, are_identical = find_files_by_type_regex(psp, args.type_regex, args.rom_index)
|
|
151
|
+
|
|
152
|
+
if not matching_files:
|
|
153
|
+
ph.print_error_and_exit(f'No files found matching type regex: {args.type_regex}')
|
|
154
|
+
elif len(matching_files) > 1:
|
|
155
|
+
if are_identical:
|
|
156
|
+
ph.print_warning(f'Found {len(matching_files)} files matching "{args.type_regex}" but they are identical. Extracting one of them.')
|
|
157
|
+
file = matching_files[0]
|
|
158
|
+
else:
|
|
159
|
+
ph.print_error_and_exit(f'Found {len(matching_files)} files matching "{args.type_regex}" but they have different contents. Not extracting any.')
|
|
160
|
+
else:
|
|
161
|
+
file = matching_files[0]
|
|
162
|
+
|
|
163
|
+
elif args.directory_index is not None and args.file_index is not None:
|
|
164
|
+
# Option 2) A single file should be extracted and we get it from (rom_index, directory_index, file_index)
|
|
111
165
|
file = psp.blob.roms[args.rom_index].directories[args.directory_index].files[args.file_index]
|
|
112
166
|
|
|
167
|
+
if file:
|
|
168
|
+
# For Options 1 and 2, do additional extraction magic if requested
|
|
113
169
|
if args.decompress:
|
|
114
170
|
if not file.compressed:
|
|
115
171
|
ph.print_error_and_exit(f'File is not compressed {file.get_readable_type()}')
|
|
@@ -123,36 +179,17 @@ def main():
|
|
|
123
179
|
else:
|
|
124
180
|
output = file.get_bytes()
|
|
125
181
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if args.decompress and type(file) is HeaderFile:
|
|
138
|
-
out_bytes = file.get_signed_bytes()
|
|
139
|
-
elif args.decrypt and type(file) is HeaderFile:
|
|
140
|
-
out_bytes = file.get_decrypted()
|
|
141
|
-
elif args.pem_key and type(file) is PubkeyFile:
|
|
142
|
-
out_bytes = file.get_pem_encoded()
|
|
143
|
-
else:
|
|
144
|
-
out_bytes = file.get_bytes()
|
|
145
|
-
|
|
146
|
-
outpath = outdir + '/d%.2d_e%.2d_%s' % (dir_index, file_index, file.get_readable_type())
|
|
147
|
-
if type(file) is HeaderFile:
|
|
148
|
-
outpath += f'_{file.get_readable_version()}'
|
|
149
|
-
|
|
150
|
-
os.makedirs(os.path.dirname(outpath), exist_ok=True)
|
|
151
|
-
with open(outpath, 'wb') as f:
|
|
152
|
-
f.write(out_bytes)
|
|
153
|
-
ph.print_info(f"Extracted all files to {outdir}")
|
|
154
|
-
else: # no_duplicates is True
|
|
155
|
-
for file in psp.blob.roms[args.rom_index].unique_files:
|
|
182
|
+
# Option 3) Multiple files should be extracted
|
|
183
|
+
if args.file_index is None:
|
|
184
|
+
if args.directory_index is not None: # Option 3a) All files from a given directory_index
|
|
185
|
+
directories = [psp.blob.roms[args.rom_index].directories[args.directory_index]]
|
|
186
|
+
else: # Option 3b) All files from all directories from the given ROM (default: 0)
|
|
187
|
+
directories = psp.blob.roms[args.rom_index].directories
|
|
188
|
+
|
|
189
|
+
if args.no_duplicates is False:
|
|
190
|
+
outdir = args.outfile or f'./{psp.filename}_extracted'
|
|
191
|
+
for dir_index, directory in enumerate(directories):
|
|
192
|
+
for file_index, file in enumerate(directory.files):
|
|
156
193
|
if args.decompress and type(file) is HeaderFile:
|
|
157
194
|
out_bytes = file.get_signed_bytes()
|
|
158
195
|
elif args.decrypt and type(file) is HeaderFile:
|
|
@@ -162,17 +199,41 @@ def main():
|
|
|
162
199
|
else:
|
|
163
200
|
out_bytes = file.get_bytes()
|
|
164
201
|
|
|
165
|
-
|
|
166
|
-
outpath = outdir + '/%s' % (file.get_readable_type())
|
|
202
|
+
outpath = outdir + '/d%.2d_e%.2d_%s' % (dir_index, file_index, file.get_readable_type())
|
|
167
203
|
|
|
168
|
-
|
|
204
|
+
# We may have same file types but different subprograms, instances
|
|
205
|
+
# Account for it in the filenames
|
|
206
|
+
if file.entry.subprogram != 0 or file.entry.instance != 0:
|
|
207
|
+
outpath += f'_SUB_{hex(file.entry.subprogram)}_INS_{hex(file.entry.instance)}'
|
|
208
|
+
if type(file) is HeaderFile:
|
|
169
209
|
outpath += f'_{file.get_readable_version()}'
|
|
170
210
|
|
|
171
211
|
os.makedirs(os.path.dirname(outpath), exist_ok=True)
|
|
172
212
|
with open(outpath, 'wb') as f:
|
|
173
213
|
f.write(out_bytes)
|
|
174
|
-
else:
|
|
175
|
-
|
|
214
|
+
else: # no_duplicates is True
|
|
215
|
+
for file in psp.blob.unique_files():
|
|
216
|
+
if args.decompress and type(file) is HeaderFile:
|
|
217
|
+
out_bytes = file.get_signed_bytes()
|
|
218
|
+
elif args.decrypt and type(file) is HeaderFile:
|
|
219
|
+
out_bytes = file.get_decrypted()
|
|
220
|
+
elif args.pem_key and type(file) is PubkeyFile:
|
|
221
|
+
out_bytes = file.get_pem_encoded()
|
|
222
|
+
else:
|
|
223
|
+
out_bytes = file.get_bytes()
|
|
224
|
+
|
|
225
|
+
outdir = args.outfile or f'./{psp.filename}_unique_extracted'
|
|
226
|
+
outpath = outdir + '/%s' % (file.get_readable_type())
|
|
227
|
+
|
|
228
|
+
if issubclass(type(file), HeaderFile):
|
|
229
|
+
outpath += f'_{file.get_readable_version()}'
|
|
230
|
+
|
|
231
|
+
os.makedirs(os.path.dirname(outpath), exist_ok=True)
|
|
232
|
+
with open(outpath, 'wb') as f:
|
|
233
|
+
f.write(out_bytes)
|
|
234
|
+
ph.print_info(f"Extracted all files to {outdir}")
|
|
235
|
+
else:
|
|
236
|
+
parser.print_help(sys.stderr)
|
|
176
237
|
|
|
177
238
|
elif args.replace_file:
|
|
178
239
|
if args.directory_index is not None and args.file_index is not None and args.outfile is not None:
|
|
@@ -67,7 +67,11 @@ class Blob(NestedBuffer):
|
|
|
67
67
|
continue
|
|
68
68
|
try:
|
|
69
69
|
rom_offset = fet_location - fet_offset # e.g. 0x20800 - 0x20000 = 0x0800
|
|
70
|
-
|
|
70
|
+
# W/A for images with single ROMs that have entries crossing 16MB boundary
|
|
71
|
+
if rom_page == 0:
|
|
72
|
+
potential_rom = Rom(self, rom_size, rom_offset, fet_offset, psptool)
|
|
73
|
+
else:
|
|
74
|
+
potential_rom = Rom(self, min(rom_size, self._MAX_PAGE_SIZE), rom_offset, fet_offset, psptool)
|
|
71
75
|
self.roms.append(potential_rom)
|
|
72
76
|
fet_parsed = True
|
|
73
77
|
break # found correct fet_offset!
|
|
@@ -31,10 +31,6 @@ class Directory(NestedBuffer):
|
|
|
31
31
|
ENTRY_SIZE = DirectoryEntry.ENTRY_SIZE
|
|
32
32
|
FILE_CLASS = File
|
|
33
33
|
|
|
34
|
-
# all directories by offset in rom
|
|
35
|
-
# todo: what if we have multi-ROM? then two ROMs share this singleton!
|
|
36
|
-
directories_by_offset = {}
|
|
37
|
-
|
|
38
34
|
class ParseError(Exception):
|
|
39
35
|
pass
|
|
40
36
|
|
|
@@ -45,14 +41,15 @@ class Directory(NestedBuffer):
|
|
|
45
41
|
@classmethod
|
|
46
42
|
def create_directories_if_not_exist(cls, offset, fet, zen_generation=None) -> List['Directory']:
|
|
47
43
|
# Recursively return or create and return found directories
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
|
|
45
|
+
if offset in fet.psptool.directories_by_offset:
|
|
46
|
+
return [fet.psptool.directories_by_offset[offset]]
|
|
50
47
|
else:
|
|
51
48
|
# 1. Create the immediate directory in front of us
|
|
52
49
|
created_directories = []
|
|
53
50
|
try:
|
|
54
51
|
directory = cls.from_offset(fet, offset, zen_generation)
|
|
55
|
-
|
|
52
|
+
fet.psptool.directories_by_offset[offset] = directory
|
|
56
53
|
created_directories.append(directory)
|
|
57
54
|
except Directory.ParseError as e:
|
|
58
55
|
# Handle empty entries gracefully (like master branch)
|
|
@@ -70,8 +67,8 @@ class Directory(NestedBuffer):
|
|
|
70
67
|
|
|
71
68
|
# 3. Recursively add tertiary directories (double references introduced in Zen 4), if applicable
|
|
72
69
|
for tertiary_directory_offset in directory.tertiary_directory_offsets:
|
|
73
|
-
directory_body = fet.rom.get_bytes(tertiary_directory_offset
|
|
74
|
-
actual_tertiary_offset = int.from_bytes(directory_body[:
|
|
70
|
+
directory_body = fet.rom.get_bytes(tertiary_directory_offset, 32)
|
|
71
|
+
actual_tertiary_offset = int.from_bytes(directory_body[16:20], 'little')
|
|
75
72
|
# Resolve one more indirection
|
|
76
73
|
tertiary_directories = cls.create_directories_if_not_exist(actual_tertiary_offset, fet, zen_generation)
|
|
77
74
|
created_directories += tertiary_directories
|
|
@@ -83,7 +80,7 @@ class Directory(NestedBuffer):
|
|
|
83
80
|
rom_offset &= fet.rom.addr_mask
|
|
84
81
|
magic = fet.rom.get_bytes(rom_offset, 4)
|
|
85
82
|
|
|
86
|
-
if magic == b'\xff\xff\xff\xff':
|
|
83
|
+
if magic == b'\xff\xff\xff\xff' or magic == b'\x00\x00\x00\x00':
|
|
87
84
|
fet.psptool.ph.print_warning(f"Empty FET entry at ROM address 0x{rom_offset:x}")
|
|
88
85
|
raise Directory.ParseError("Empty entry")
|
|
89
86
|
if magic in cls.DIRECTORY_MAGICS:
|
|
@@ -106,7 +103,7 @@ class Directory(NestedBuffer):
|
|
|
106
103
|
self._count = int.from_bytes(self.rom[self.buffer_offset + 8: self.buffer_offset + 12], 'little')
|
|
107
104
|
self.magic = self.rom.get_bytes(self.buffer_offset, 4)
|
|
108
105
|
assert self.magic in self.DIRECTORY_MAGICS
|
|
109
|
-
self.
|
|
106
|
+
self.additional_info = self.rom.get_bytes(self.buffer_offset + 12, 4)
|
|
110
107
|
self.header = NestedBuffer(self, self.HEADER_SIZE)
|
|
111
108
|
self.body = NestedBuffer(self, self.ENTRY_SIZE * self.count, buffer_offset=self.HEADER_SIZE)
|
|
112
109
|
self.buffer_size = len(self.header) + len(self.body)
|
|
@@ -157,13 +154,18 @@ class Directory(NestedBuffer):
|
|
|
157
154
|
self.header[8:12] = struct.pack('<I', self.count)
|
|
158
155
|
self.update_checksum()
|
|
159
156
|
|
|
157
|
+
# 00b: x86 Physical address
|
|
158
|
+
# 01b: Offset from start of the BIOS (flash offset)
|
|
159
|
+
# 10b: Offset from start of directory header
|
|
160
|
+
# 11b: Offset from start of partition
|
|
160
161
|
@property
|
|
161
162
|
def address_mode(self):
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
163
|
+
info = struct.unpack('<L', self.additional_info)[0]
|
|
164
|
+
version = (info >> 31) & 1
|
|
165
|
+
if version == 1:
|
|
166
|
+
return (info >> 24) & 3
|
|
167
|
+
else:
|
|
168
|
+
return (info >> 29) & 3
|
|
167
169
|
|
|
168
170
|
def verify_checksum(self):
|
|
169
171
|
data = self[0x8:]
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# PSPTool - Display, extract and manipulate PSP firmware inside UEFI images
|
|
2
|
+
# Copyright (C) 2021 Christian Werling, Robert Buhren, Hans Niklas Jacob
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
import struct
|
|
18
|
+
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from directory import Directory
|
|
22
|
+
|
|
23
|
+
from .utils import NestedBuffer
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DirectoryEntry(NestedBuffer):
|
|
27
|
+
ENTRY_SIZE = 4 * 4
|
|
28
|
+
|
|
29
|
+
def __init__(self, parent_directory: 'Directory', entry_offset):
|
|
30
|
+
super().__init__(parent_directory.body, self.ENTRY_SIZE, entry_offset)
|
|
31
|
+
self.parent_directory = parent_directory
|
|
32
|
+
self.entry_offset = entry_offset
|
|
33
|
+
|
|
34
|
+
def __repr__(self):
|
|
35
|
+
return f'{self.__class__.__name__}({self.type=:#x}, {self.flags=:#x}, {self.size=:#x}, ' \
|
|
36
|
+
f'{self.offset=:#x}, {self.entry_offset=:#x}, {self.rsv0=:#x})'
|
|
37
|
+
|
|
38
|
+
def file_offset(self):
|
|
39
|
+
# Special case soft fuse chain, no offset or size
|
|
40
|
+
if self.type == 0xb:
|
|
41
|
+
dir_start = self.parent_directory.buffer_offset + self.parent_directory.HEADER_SIZE
|
|
42
|
+
return dir_start + self.entry_offset
|
|
43
|
+
|
|
44
|
+
addr_mode = self.parent_directory.address_mode
|
|
45
|
+
# If directory address mode is 2 or 3 (relative to dir or slot), the
|
|
46
|
+
# entry address mode must be taken into account, otherwise ignored.
|
|
47
|
+
if addr_mode == 2 or addr_mode == 3:
|
|
48
|
+
addr_mode = self.address_mode
|
|
49
|
+
|
|
50
|
+
if addr_mode == 0:
|
|
51
|
+
# x86 physical address, should be in range 0xff000000 - 0xffffffff
|
|
52
|
+
# But some images use flash offset in x86 physical address mode.
|
|
53
|
+
# If ROM is bigger than 16MB and the entry address is in the
|
|
54
|
+
# 0xff000000 - 0xffffffff range, we have to override the mask for
|
|
55
|
+
# physical address.
|
|
56
|
+
rom_size = self.parent_directory.rom.addr_mask + 1
|
|
57
|
+
if self.offset > 0xFF000000 and rom_size > 16 * 1024 * 1024:
|
|
58
|
+
return self.offset & 0x00FFFFFF
|
|
59
|
+
else:
|
|
60
|
+
return self.offset & self.parent_directory.rom.addr_mask
|
|
61
|
+
elif addr_mode == 1:
|
|
62
|
+
# Flash offset from start of BIOS, most common on modern systems
|
|
63
|
+
return self.offset
|
|
64
|
+
elif addr_mode == 2:
|
|
65
|
+
# Flash offset from start of directory header
|
|
66
|
+
return self.parent_directory.buffer_offset + self.offset
|
|
67
|
+
elif addr_mode == 3:
|
|
68
|
+
# Flash offset from start of the slot
|
|
69
|
+
# TODO: How to calculate the offset from slot? Is this correct?
|
|
70
|
+
return self.parent_directory.buffer_offset + self.offset
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def type(self):
|
|
74
|
+
return struct.unpack('<B', self[0:1])[0]
|
|
75
|
+
|
|
76
|
+
@type.setter
|
|
77
|
+
def type(self, value):
|
|
78
|
+
self.set_bytes(0, 1, struct.pack('<B', value))
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def subprogram(self):
|
|
82
|
+
return struct.unpack('<B', self[1:2])[0]
|
|
83
|
+
|
|
84
|
+
@subprogram.setter
|
|
85
|
+
def subprogram(self, value):
|
|
86
|
+
self.set_bytes(1, 1, struct.pack('<B', value))
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def flags(self):
|
|
90
|
+
return struct.unpack('<H', self[2:4])[0]
|
|
91
|
+
|
|
92
|
+
@flags.setter
|
|
93
|
+
def flags(self, value):
|
|
94
|
+
self.set_bytes(2, 2, struct.pack('<H', value))
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def instance(self):
|
|
98
|
+
return (self.flags >> 3) & 0xF
|
|
99
|
+
|
|
100
|
+
@instance.setter
|
|
101
|
+
def instance(self, value):
|
|
102
|
+
self.flags = self.flags | ((value & 0xF) << 3)
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def size(self):
|
|
106
|
+
return struct.unpack('<I', self[4:8])[0]
|
|
107
|
+
|
|
108
|
+
@size.setter
|
|
109
|
+
def size(self, value):
|
|
110
|
+
self.set_bytes(4, 4, struct.pack('<I', value))
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def offset(self):
|
|
114
|
+
return struct.unpack('<I', self[8:12])[0]
|
|
115
|
+
|
|
116
|
+
@offset.setter
|
|
117
|
+
def offset(self, value):
|
|
118
|
+
self.set_bytes(8, 4, struct.pack('<I', value))
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def rsv0(self):
|
|
122
|
+
return struct.unpack('<I', self[12:16])[0]
|
|
123
|
+
|
|
124
|
+
@rsv0.setter
|
|
125
|
+
def rsv0(self, value):
|
|
126
|
+
self.set_bytes(12, 4, struct.pack('<I', value))
|
|
127
|
+
|
|
128
|
+
# 00b: x86 Physical address
|
|
129
|
+
# 01b: Offset from start of the BIOS (flash offset)
|
|
130
|
+
# 10b: Offset from start of directory header
|
|
131
|
+
# 11b: Offset from start of partition
|
|
132
|
+
@property
|
|
133
|
+
def address_mode(self):
|
|
134
|
+
return (self.rsv0 >> 30) & 3
|
|
135
|
+
|
|
136
|
+
class BiosDirectoryEntry(DirectoryEntry):
|
|
137
|
+
ENTRY_SIZE = 4 * 6
|
|
138
|
+
|
|
139
|
+
def __repr__(self):
|
|
140
|
+
return super().__repr__()[:-1] + f', {self.destination=:#x})'
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def destination(self):
|
|
144
|
+
return struct.unpack('<Q', self[16:24])[0]
|
|
145
|
+
|
|
146
|
+
@destination.setter
|
|
147
|
+
def destination(self, value):
|
|
148
|
+
self.set_bytes(16, 8, struct.pack('<Q', value))
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def region_type(self):
|
|
152
|
+
return struct.unpack('<B', self[1:2])[0]
|
|
153
|
+
|
|
154
|
+
@region_type.setter
|
|
155
|
+
def region_type(self, value):
|
|
156
|
+
self.set_bytes(1, 1, struct.pack('<B', value))
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def flags(self):
|
|
160
|
+
return struct.unpack('<H', self[2:4])[0]
|
|
161
|
+
|
|
162
|
+
@flags.setter
|
|
163
|
+
def flags(self, value):
|
|
164
|
+
self.set_bytes(2, 2, struct.pack('<H', value))
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def subprogram(self):
|
|
168
|
+
return (self.flags >> 8) & 0x7
|
|
169
|
+
|
|
170
|
+
@subprogram.setter
|
|
171
|
+
def subprogram(self, value):
|
|
172
|
+
self.flags = self.flags | ((value & 0x7) << 8)
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def instance(self):
|
|
176
|
+
return (self.flags >> 4) & 0xF
|
|
177
|
+
|
|
178
|
+
@instance.setter
|
|
179
|
+
def instance(self, value):
|
|
180
|
+
self.flags = self.flags | ((value & 0xF) << 4)
|
|
@@ -69,10 +69,16 @@ class Fet(NestedBuffer):
|
|
|
69
69
|
# TODO: Why is 0xFFFFFFFe a possible value here?
|
|
70
70
|
if rom_addr in [0x0, 0xFFFFFFFF, 0xFFFFFFFe]:
|
|
71
71
|
continue
|
|
72
|
-
|
|
72
|
+
# if ROM is bigger than 16MB, we have to override the mask for
|
|
73
|
+
# physical address
|
|
74
|
+
if rom_addr > 0xFF000000 and self.rom.addr_mask + 1 > 16 * 1024 * 1024:
|
|
75
|
+
rom_addr &= 0x00FFFFFF
|
|
76
|
+
else:
|
|
77
|
+
rom_addr &= self.rom.addr_mask
|
|
78
|
+
|
|
73
79
|
try:
|
|
74
80
|
dir_magic = self.rom[rom_addr:rom_addr + 4]
|
|
75
|
-
except:
|
|
81
|
+
except AssertionError as e:
|
|
76
82
|
self.psptool.ph.print_warning(f"FET entry 0x{rom_addr:x} not found or invalid, skipping ...")
|
|
77
83
|
continue
|
|
78
84
|
if dir_magic == b'2PSP' or dir_magic == b'2BHD':
|
|
@@ -105,7 +111,14 @@ class Fet(NestedBuffer):
|
|
|
105
111
|
entry_addr = int.from_bytes(entry, 'little')
|
|
106
112
|
if entry_addr in [0, 0xFFFFFFFF]:
|
|
107
113
|
continue
|
|
108
|
-
|
|
114
|
+
# if ROM is bigger than 16MB, we have to override the mask for
|
|
115
|
+
# physical address
|
|
116
|
+
rom_size = self.rom.addr_mask + 1
|
|
117
|
+
if entry_addr > 0xFF000000 and rom_size > 16 * 1024 * 1024:
|
|
118
|
+
entry_addr &= 0x00FFFFFF
|
|
119
|
+
else:
|
|
120
|
+
entry_addr &= self.rom.addr_mask
|
|
121
|
+
|
|
109
122
|
# entry_addr += self.blob_offset
|
|
110
123
|
zen_generation_id = combo_dir[i*16+5:i*16+8]
|
|
111
124
|
zen_generation = 'unknown'
|