Upload macrecovery.py with huggingface_hub
Browse files- macrecovery.py +502 -0
macrecovery.py
ADDED
@@ -0,0 +1,502 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
|
3 |
+
"""
|
4 |
+
Gather recovery information for Macs.
|
5 |
+
|
6 |
+
Copyright (c) 2019, vit9696
|
7 |
+
"""
|
8 |
+
|
9 |
+
import argparse
|
10 |
+
import binascii
|
11 |
+
import hashlib
|
12 |
+
import json
|
13 |
+
import linecache
|
14 |
+
import os
|
15 |
+
import random
|
16 |
+
import struct
|
17 |
+
import sys
|
18 |
+
|
19 |
+
try:
|
20 |
+
from urllib.request import Request, HTTPError, urlopen
|
21 |
+
from urllib.parse import urlparse
|
22 |
+
except ImportError:
|
23 |
+
from urllib2 import Request, HTTPError, urlopen
|
24 |
+
from urlparse import urlparse
|
25 |
+
|
26 |
+
SELF_DIR = os.path.dirname(os.path.realpath(__file__))
|
27 |
+
|
28 |
+
RECENT_MAC = 'Mac-7BA5B2D9E42DDD94'
|
29 |
+
MLB_ZERO = '00000000000000000'
|
30 |
+
MLB_VALID = 'C02749200YGJ803AX'
|
31 |
+
MLB_PRODUCT = '00000000000J80300'
|
32 |
+
|
33 |
+
TYPE_SID = 16
|
34 |
+
TYPE_K = 64
|
35 |
+
TYPE_FG = 64
|
36 |
+
|
37 |
+
INFO_PRODUCT = 'AP'
|
38 |
+
INFO_IMAGE_LINK = 'AU'
|
39 |
+
INFO_IMAGE_HASH = 'AH'
|
40 |
+
INFO_IMAGE_SESS = 'AT'
|
41 |
+
INFO_SIGN_LINK = 'CU'
|
42 |
+
INFO_SIGN_HASH = 'CH'
|
43 |
+
INFO_SIGN_SESS = 'CT'
|
44 |
+
INFO_REQURED = [INFO_PRODUCT, INFO_IMAGE_LINK, INFO_IMAGE_HASH, INFO_IMAGE_SESS, INFO_SIGN_LINK, INFO_SIGN_HASH, INFO_SIGN_SESS]
|
45 |
+
|
46 |
+
|
47 |
+
def run_query(url, headers, post=None, raw=False):
|
48 |
+
if post is not None:
|
49 |
+
data = '\n'.join([entry + '=' + post[entry] for entry in post])
|
50 |
+
if sys.version_info[0] >= 3:
|
51 |
+
data = data.encode('utf-8')
|
52 |
+
else:
|
53 |
+
data = None
|
54 |
+
req = Request(url=url, headers=headers, data=data)
|
55 |
+
try:
|
56 |
+
response = urlopen(req)
|
57 |
+
if raw:
|
58 |
+
return response
|
59 |
+
return dict(response.info()), response.read()
|
60 |
+
except HTTPError as e:
|
61 |
+
print(f'ERROR: "{e}" when connecting to {url}')
|
62 |
+
sys.exit(1)
|
63 |
+
|
64 |
+
|
65 |
+
def generate_id(id_type, id_value=None):
|
66 |
+
valid_chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F']
|
67 |
+
return ''.join(random.choice(valid_chars) for i in range(id_type)) if not id_value else id_value
|
68 |
+
|
69 |
+
|
70 |
+
def product_mlb(mlb):
|
71 |
+
return '00000000000' + mlb[11] + mlb[12] + mlb[13] + mlb[14] + '00'
|
72 |
+
|
73 |
+
|
74 |
+
def mlb_from_eeee(eeee):
|
75 |
+
if len(eeee) != 4:
|
76 |
+
print('ERROR: Invalid EEEE code length!')
|
77 |
+
sys.exit(1)
|
78 |
+
|
79 |
+
return f'00000000000{eeee}00'
|
80 |
+
|
81 |
+
|
82 |
+
def int_from_unsigned_bytes(byte_list, byteorder):
|
83 |
+
if byteorder == 'little':
|
84 |
+
byte_list = byte_list[::-1]
|
85 |
+
encoded = binascii.hexlify(byte_list)
|
86 |
+
return int(encoded, 16)
|
87 |
+
|
88 |
+
|
89 |
+
# zhangyoufu https://gist.github.com/MCJack123/943eaca762730ca4b7ae460b731b68e7#gistcomment-3061078 2021-10-08
|
90 |
+
Apple_EFI_ROM_public_key_1 = 0xC3E748CAD9CD384329E10E25A91E43E1A762FF529ADE578C935BDDF9B13F2179D4855E6FC89E9E29CA12517D17DFA1EDCE0BEBF0EA7B461FFE61D94E2BDF72C196F89ACD3536B644064014DAE25A15DB6BB0852ECBD120916318D1CCDEA3C84C92ED743FC176D0BACA920D3FCF3158AFF731F88CE0623182A8ED67E650515F75745909F07D415F55FC15A35654D118C55A462D37A3ACDA08612F3F3F6571761EFCCBCC299AEE99B3A4FD6212CCFFF5EF37A2C334E871191F7E1C31960E010A54E86FA3F62E6D6905E1CD57732410A3EB0C6B4DEFDABE9F59BF1618758C751CD56CEF851D1C0EAA1C558E37AC108DA9089863D20E2E7E4BF475EC66FE6B3EFDCF
|
91 |
+
|
92 |
+
ChunkListHeader = struct.Struct('<4sIBBBxQQQ')
|
93 |
+
assert ChunkListHeader.size == 0x24
|
94 |
+
|
95 |
+
Chunk = struct.Struct('<I32s')
|
96 |
+
assert Chunk.size == 0x24
|
97 |
+
|
98 |
+
|
99 |
+
def verify_chunklist(cnkpath):
|
100 |
+
with open(cnkpath, 'rb') as f:
|
101 |
+
hash_ctx = hashlib.sha256()
|
102 |
+
data = f.read(ChunkListHeader.size)
|
103 |
+
hash_ctx.update(data)
|
104 |
+
magic, header_size, file_version, chunk_method, signature_method, chunk_count, chunk_offset, signature_offset = ChunkListHeader.unpack(data)
|
105 |
+
assert magic == b'CNKL'
|
106 |
+
assert header_size == ChunkListHeader.size
|
107 |
+
assert file_version == 1
|
108 |
+
assert chunk_method == 1
|
109 |
+
assert signature_method in [1, 2]
|
110 |
+
assert chunk_count > 0
|
111 |
+
assert chunk_offset == 0x24
|
112 |
+
assert signature_offset == chunk_offset + Chunk.size * chunk_count
|
113 |
+
for _ in range(chunk_count):
|
114 |
+
data = f.read(Chunk.size)
|
115 |
+
hash_ctx.update(data)
|
116 |
+
chunk_size, chunk_sha256 = Chunk.unpack(data)
|
117 |
+
yield chunk_size, chunk_sha256
|
118 |
+
digest = hash_ctx.digest()
|
119 |
+
if signature_method == 1:
|
120 |
+
data = f.read(256)
|
121 |
+
assert len(data) == 256
|
122 |
+
signature = int_from_unsigned_bytes(data, 'little')
|
123 |
+
plaintext = 0x1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff003031300d0609608648016503040201050004200000000000000000000000000000000000000000000000000000000000000000 | int_from_unsigned_bytes(digest, 'big')
|
124 |
+
assert pow(signature, 0x10001, Apple_EFI_ROM_public_key_1) == plaintext
|
125 |
+
elif signature_method == 2:
|
126 |
+
data = f.read(32)
|
127 |
+
assert data == digest
|
128 |
+
raise RuntimeError('Chunklist missing digital signature')
|
129 |
+
else:
|
130 |
+
raise NotImplementedError
|
131 |
+
assert f.read(1) == b''
|
132 |
+
|
133 |
+
|
134 |
+
def get_session(args):
|
135 |
+
headers = {
|
136 |
+
'Host': 'osrecovery.apple.com',
|
137 |
+
'Connection': 'close',
|
138 |
+
'User-Agent': 'InternetRecovery/1.0',
|
139 |
+
}
|
140 |
+
|
141 |
+
headers, _ = run_query('http://osrecovery.apple.com/', headers)
|
142 |
+
|
143 |
+
if args.verbose:
|
144 |
+
print('Session headers:')
|
145 |
+
for header in headers:
|
146 |
+
print(f'{header}: {headers[header]}')
|
147 |
+
|
148 |
+
for header in headers:
|
149 |
+
if header.lower() == 'set-cookie':
|
150 |
+
cookies = headers[header].split('; ')
|
151 |
+
for cookie in cookies:
|
152 |
+
return cookie if cookie.startswith('session=') else ...
|
153 |
+
|
154 |
+
raise RuntimeError('No session in headers ' + str(headers))
|
155 |
+
|
156 |
+
|
157 |
+
def get_image_info(session, bid, mlb=MLB_ZERO, diag=False, os_type='default', cid=None):
|
158 |
+
headers = {
|
159 |
+
'Host': 'osrecovery.apple.com',
|
160 |
+
'Connection': 'close',
|
161 |
+
'User-Agent': 'InternetRecovery/1.0',
|
162 |
+
'Cookie': session,
|
163 |
+
'Content-Type': 'text/plain',
|
164 |
+
}
|
165 |
+
|
166 |
+
post = {
|
167 |
+
'cid': generate_id(TYPE_SID, cid),
|
168 |
+
'sn': mlb,
|
169 |
+
'bid': bid,
|
170 |
+
'k': generate_id(TYPE_K),
|
171 |
+
'fg': generate_id(TYPE_FG)
|
172 |
+
}
|
173 |
+
|
174 |
+
if diag:
|
175 |
+
url = 'http://osrecovery.apple.com/InstallationPayload/Diagnostics'
|
176 |
+
else:
|
177 |
+
url = 'http://osrecovery.apple.com/InstallationPayload/RecoveryImage'
|
178 |
+
post['os'] = os_type
|
179 |
+
|
180 |
+
headers, output = run_query(url, headers, post)
|
181 |
+
|
182 |
+
output = output.decode('utf-8')
|
183 |
+
info = {}
|
184 |
+
for line in output.split('\n'):
|
185 |
+
try:
|
186 |
+
key, value = line.split(': ')
|
187 |
+
info[key] = value
|
188 |
+
except Exception:
|
189 |
+
continue
|
190 |
+
|
191 |
+
for k in INFO_REQURED:
|
192 |
+
if k not in info:
|
193 |
+
raise RuntimeError(f'Missing key {k}')
|
194 |
+
|
195 |
+
return info
|
196 |
+
|
197 |
+
|
198 |
+
def save_image(url, sess, filename='', directory=''):
|
199 |
+
purl = urlparse(url)
|
200 |
+
headers = {
|
201 |
+
'Host': purl.hostname,
|
202 |
+
'Connection': 'close',
|
203 |
+
'User-Agent': 'InternetRecovery/1.0',
|
204 |
+
'Cookie': '='.join(['AssetToken', sess])
|
205 |
+
}
|
206 |
+
|
207 |
+
if not os.path.exists(directory):
|
208 |
+
os.mkdir(directory)
|
209 |
+
|
210 |
+
if filename == '':
|
211 |
+
filename = os.path.basename(purl.path)
|
212 |
+
if filename.find('/') >= 0 or filename == '':
|
213 |
+
raise RuntimeError('Invalid save path ' + filename)
|
214 |
+
|
215 |
+
print(f'Saving {url} to {directory}/{filename}...')
|
216 |
+
|
217 |
+
with open(os.path.join(directory, filename), 'wb') as fh:
|
218 |
+
response = run_query(url, headers, raw=True)
|
219 |
+
size = 0
|
220 |
+
while True:
|
221 |
+
chunk = response.read(2**20)
|
222 |
+
if not chunk:
|
223 |
+
break
|
224 |
+
fh.write(chunk)
|
225 |
+
size += len(chunk)
|
226 |
+
print(f'\r{size / (2**20)} MBs downloaded...', end='')
|
227 |
+
sys.stdout.flush()
|
228 |
+
print('\rDownload complete!\t\t\t\t\t')
|
229 |
+
|
230 |
+
return os.path.join(directory, os.path.basename(filename))
|
231 |
+
|
232 |
+
|
233 |
+
def verify_image(dmgpath, cnkpath):
|
234 |
+
print('Verifying image with chunklist...')
|
235 |
+
|
236 |
+
with open(dmgpath, 'rb') as dmgf:
|
237 |
+
cnkcount = 0
|
238 |
+
for cnksize, cnkhash in verify_chunklist(cnkpath):
|
239 |
+
cnkcount += 1
|
240 |
+
print(f'\rChunk {cnkcount} ({cnksize} bytes)', end='')
|
241 |
+
sys.stdout.flush()
|
242 |
+
cnk = dmgf.read(cnksize)
|
243 |
+
if len(cnk) != cnksize:
|
244 |
+
raise RuntimeError(f'Invalid chunk {cnkcount} size: expected {cnksize}, read {len(cnk)}')
|
245 |
+
if hashlib.sha256(cnk).digest() != cnkhash:
|
246 |
+
raise RuntimeError(f'Invalid chunk {cnkcount}: hash mismatch')
|
247 |
+
if dmgf.read(1) != b'':
|
248 |
+
raise RuntimeError('Invalid image: larger than chunklist')
|
249 |
+
print('\rImage verification complete!\t\t\t\t\t')
|
250 |
+
|
251 |
+
|
252 |
+
def action_download(args):
|
253 |
+
"""
|
254 |
+
Reference information for queries:
|
255 |
+
|
256 |
+
Recovery latest:
|
257 |
+
cid=3076CE439155BA14
|
258 |
+
sn=...
|
259 |
+
bid=Mac-E43C1C25D4880AD6
|
260 |
+
k=4BE523BB136EB12B1758C70DB43BDD485EBCB6A457854245F9E9FF0587FB790C
|
261 |
+
os=latest
|
262 |
+
fg=B2E6AA07DB9088BE5BDB38DB2EA824FDDFB6C3AC5272203B32D89F9D8E3528DC
|
263 |
+
|
264 |
+
Recovery default:
|
265 |
+
cid=4A35CB95FF396EE7
|
266 |
+
sn=...
|
267 |
+
bid=Mac-E43C1C25D4880AD6
|
268 |
+
k=0A385E6FFC3DDD990A8A1F4EC8B98C92CA5E19C9FF1DD26508C54936D8523121
|
269 |
+
os=default
|
270 |
+
fg=B2E6AA07DB9088BE5BDB38DB2EA824FDDFB6C3AC5272203B32D89F9D8E3528DC
|
271 |
+
|
272 |
+
Diagnostics:
|
273 |
+
cid=050C59B51497CEC8
|
274 |
+
sn=...
|
275 |
+
bid=Mac-E43C1C25D4880AD6
|
276 |
+
k=37D42A8282FE04A12A7D946304F403E56A2155B9622B385F3EB959A2FBAB8C93
|
277 |
+
fg=B2E6AA07DB9088BE5BDB38DB2EA824FDDFB6C3AC5272203B32D89F9D8E3528DC
|
278 |
+
"""
|
279 |
+
|
280 |
+
session = get_session(args)
|
281 |
+
info = get_image_info(session, bid=args.board_id, mlb=args.mlb, diag=args.diagnostics, os_type=args.os_type)
|
282 |
+
if args.verbose:
|
283 |
+
print(info)
|
284 |
+
print(f'Downloading {info[INFO_PRODUCT]}...')
|
285 |
+
dmgname = '' if args.basename == '' else args.basename + '.dmg'
|
286 |
+
dmgpath = save_image(info[INFO_IMAGE_LINK], info[INFO_IMAGE_SESS], dmgname, args.outdir)
|
287 |
+
cnkname = '' if args.basename == '' else args.basename + '.chunklist'
|
288 |
+
cnkpath = save_image(info[INFO_SIGN_LINK], info[INFO_SIGN_SESS], cnkname, args.outdir)
|
289 |
+
try:
|
290 |
+
verify_image(dmgpath, cnkpath)
|
291 |
+
return 0
|
292 |
+
except Exception as err:
|
293 |
+
if isinstance(err, AssertionError) and str(err) == '':
|
294 |
+
try:
|
295 |
+
tb = sys.exc_info()[2]
|
296 |
+
while tb.tb_next:
|
297 |
+
tb = tb.tb_next
|
298 |
+
err = linecache.getline(tb.tb_frame.f_code.co_filename, tb.tb_lineno, tb.tb_frame.f_globals).strip()
|
299 |
+
except Exception:
|
300 |
+
err = "Invalid chunklist"
|
301 |
+
print(f'\rImage verification failed. ({err})')
|
302 |
+
return 1
|
303 |
+
|
304 |
+
|
305 |
+
def action_selfcheck(args):
|
306 |
+
"""
|
307 |
+
Sanity check server logic for recovery:
|
308 |
+
|
309 |
+
if not valid(bid):
|
310 |
+
return error()
|
311 |
+
ppp = get_ppp(sn)
|
312 |
+
if not valid(ppp):
|
313 |
+
return latest_recovery(bid = bid) # Returns newest for bid.
|
314 |
+
if valid(sn):
|
315 |
+
if os == 'default':
|
316 |
+
return default_recovery(sn = sn, ppp = ppp) # Returns oldest for sn.
|
317 |
+
else:
|
318 |
+
return latest_recovery(sn = sn, ppp = ppp) # Returns newest for sn.
|
319 |
+
return default_recovery(ppp = ppp) # Returns oldest.
|
320 |
+
"""
|
321 |
+
|
322 |
+
session = get_session(args)
|
323 |
+
valid_default = get_image_info(session, bid=RECENT_MAC, mlb=MLB_VALID, diag=False, os_type='default')
|
324 |
+
valid_latest = get_image_info(session, bid=RECENT_MAC, mlb=MLB_VALID, diag=False, os_type='latest')
|
325 |
+
product_default = get_image_info(session, bid=RECENT_MAC, mlb=MLB_PRODUCT, diag=False, os_type='default')
|
326 |
+
product_latest = get_image_info(session, bid=RECENT_MAC, mlb=MLB_PRODUCT, diag=False, os_type='latest')
|
327 |
+
generic_default = get_image_info(session, bid=RECENT_MAC, mlb=MLB_ZERO, diag=False, os_type='default')
|
328 |
+
generic_latest = get_image_info(session, bid=RECENT_MAC, mlb=MLB_ZERO, diag=False, os_type='latest')
|
329 |
+
|
330 |
+
if args.verbose:
|
331 |
+
print(valid_default)
|
332 |
+
print(valid_latest)
|
333 |
+
print(product_default)
|
334 |
+
print(product_latest)
|
335 |
+
print(generic_default)
|
336 |
+
print(generic_latest)
|
337 |
+
|
338 |
+
if valid_default[INFO_PRODUCT] == valid_latest[INFO_PRODUCT]:
|
339 |
+
# Valid MLB must give different default and latest if this is not a too new product.
|
340 |
+
print(f'ERROR: Cannot determine any previous product, got {valid_default[INFO_PRODUCT]}')
|
341 |
+
return 1
|
342 |
+
|
343 |
+
if product_default[INFO_PRODUCT] != product_latest[INFO_PRODUCT]:
|
344 |
+
# Product-only MLB must give the same value for default and latest.
|
345 |
+
print(f'ERROR: Latest and default do not match for product MLB, got {product_default[INFO_PRODUCT]} and {product_latest[INFO_PRODUCT]}')
|
346 |
+
return 1
|
347 |
+
|
348 |
+
if generic_default[INFO_PRODUCT] != generic_latest[INFO_PRODUCT]:
|
349 |
+
# Zero MLB always give the same value for default and latest.
|
350 |
+
print(f'ERROR: Generic MLB gives different product, got {generic_default[INFO_PRODUCT]} and {generic_latest[INFO_PRODUCT]}')
|
351 |
+
return 1
|
352 |
+
|
353 |
+
if valid_latest[INFO_PRODUCT] != generic_latest[INFO_PRODUCT]:
|
354 |
+
# Valid MLB must always equal generic MLB.
|
355 |
+
print(f'ERROR: Cannot determine unified latest product, got {valid_latest[INFO_PRODUCT]} and {generic_latest[INFO_PRODUCT]}')
|
356 |
+
return 1
|
357 |
+
|
358 |
+
if product_default[INFO_PRODUCT] != valid_default[INFO_PRODUCT]:
|
359 |
+
# Product-only MLB can give the same value with valid default MLB.
|
360 |
+
# This is not an error for all models, but for our chosen code it is.
|
361 |
+
print('ERROR: Valid and product MLB give mismatch, got {product_default[INFO_PRODUCT]} and {valid_default[INFO_PRODUCT]}')
|
362 |
+
return 1
|
363 |
+
|
364 |
+
print('SUCCESS: Found no discrepancies with MLB validation algorithm!')
|
365 |
+
return 0
|
366 |
+
|
367 |
+
|
368 |
+
def action_verify(args):
|
369 |
+
"""
|
370 |
+
Try to verify MLB serial number.
|
371 |
+
"""
|
372 |
+
session = get_session(args)
|
373 |
+
generic_latest = get_image_info(session, bid=RECENT_MAC, mlb=MLB_ZERO, diag=False, os_type='latest')
|
374 |
+
uvalid_default = get_image_info(session, bid=args.board_id, mlb=args.mlb, diag=False, os_type='default')
|
375 |
+
uvalid_latest = get_image_info(session, bid=args.board_id, mlb=args.mlb, diag=False, os_type='latest')
|
376 |
+
uproduct_default = get_image_info(session, bid=args.board_id, mlb=product_mlb(args.mlb), diag=False, os_type='default')
|
377 |
+
|
378 |
+
if args.verbose:
|
379 |
+
print(generic_latest)
|
380 |
+
print(uvalid_default)
|
381 |
+
print(uvalid_latest)
|
382 |
+
print(uproduct_default)
|
383 |
+
|
384 |
+
# Verify our MLB number.
|
385 |
+
if uvalid_default[INFO_PRODUCT] != uvalid_latest[INFO_PRODUCT]:
|
386 |
+
print(f'SUCCESS: {args.mlb} MLB looks valid and supported!' if uvalid_latest[INFO_PRODUCT] == generic_latest[INFO_PRODUCT] else f'SUCCESS: {args.mlb} MLB looks valid, but probably unsupported!')
|
387 |
+
return 0
|
388 |
+
|
389 |
+
print('UNKNOWN: Run selfcheck, check your board-id, or try again later!')
|
390 |
+
|
391 |
+
# Here we have matching default and latest products. This can only be true for very
|
392 |
+
# new models. These models get either latest or special builds.
|
393 |
+
if uvalid_default[INFO_PRODUCT] == generic_latest[INFO_PRODUCT]:
|
394 |
+
print(f'UNKNOWN: {args.mlb} MLB can be valid if very new!')
|
395 |
+
return 0
|
396 |
+
if uproduct_default[INFO_PRODUCT] != uvalid_default[INFO_PRODUCT]:
|
397 |
+
print(f'UNKNOWN: {args.mlb} MLB looks invalid, other models use product {uproduct_default[INFO_PRODUCT]} instead of {uvalid_default[INFO_PRODUCT]}!')
|
398 |
+
return 0
|
399 |
+
print(f'UNKNOWN: {args.mlb} MLB can be valid if very new and using special builds!')
|
400 |
+
return 0
|
401 |
+
|
402 |
+
|
403 |
+
def action_guess(args):
|
404 |
+
"""
|
405 |
+
Attempt to guess which model does this MLB belong.
|
406 |
+
"""
|
407 |
+
|
408 |
+
mlb = args.mlb
|
409 |
+
anon = mlb.startswith('000')
|
410 |
+
|
411 |
+
with open(args.board_db, 'r', encoding='utf-8') as fh:
|
412 |
+
db = json.load(fh)
|
413 |
+
|
414 |
+
supported = {}
|
415 |
+
|
416 |
+
session = get_session(args)
|
417 |
+
|
418 |
+
generic_latest = get_image_info(session, bid=RECENT_MAC, mlb=MLB_ZERO, diag=False, os_type='latest')
|
419 |
+
|
420 |
+
for model in db:
|
421 |
+
try:
|
422 |
+
if anon:
|
423 |
+
# For anonymous lookup check when given model does not match latest.
|
424 |
+
model_latest = get_image_info(session, bid=model, mlb=MLB_ZERO, diag=False, os_type='latest')
|
425 |
+
|
426 |
+
if model_latest[INFO_PRODUCT] != generic_latest[INFO_PRODUCT]:
|
427 |
+
if db[model] == 'current':
|
428 |
+
print(f'WARN: Skipped {model} due to using latest product {model_latest[INFO_PRODUCT]} instead of {generic_latest[INFO_PRODUCT]}')
|
429 |
+
continue
|
430 |
+
|
431 |
+
user_default = get_image_info(session, bid=model, mlb=mlb, diag=False, os_type='default')
|
432 |
+
|
433 |
+
if user_default[INFO_PRODUCT] != generic_latest[INFO_PRODUCT]:
|
434 |
+
supported[model] = [db[model], user_default[INFO_PRODUCT], generic_latest[INFO_PRODUCT]]
|
435 |
+
else:
|
436 |
+
# For normal lookup check when given model has mismatching normal and latest.
|
437 |
+
user_latest = get_image_info(session, bid=model, mlb=mlb, diag=False, os_type='latest')
|
438 |
+
|
439 |
+
user_default = get_image_info(session, bid=model, mlb=mlb, diag=False, os_type='default')
|
440 |
+
|
441 |
+
if user_latest[INFO_PRODUCT] != user_default[INFO_PRODUCT]:
|
442 |
+
supported[model] = [db[model], user_default[INFO_PRODUCT], user_latest[INFO_PRODUCT]]
|
443 |
+
|
444 |
+
except Exception as e:
|
445 |
+
print(f'WARN: Failed to check {model}, exception: {e}')
|
446 |
+
|
447 |
+
if len(supported) > 0:
|
448 |
+
print(f'SUCCESS: MLB {mlb} looks supported for:')
|
449 |
+
for model in supported.items():
|
450 |
+
print(f'- {model}, up to {supported[model][0]}, default: {supported[model][1]}, latest: {supported[model][2]}')
|
451 |
+
return 0
|
452 |
+
|
453 |
+
print(f'UNKNOWN: Failed to determine supported models for MLB {mlb}!')
|
454 |
+
return None
|
455 |
+
|
456 |
+
|
457 |
+
def main():
|
458 |
+
parser = argparse.ArgumentParser(description='Gather recovery information for Macs')
|
459 |
+
parser.add_argument('action', choices=['download', 'selfcheck', 'verify', 'guess'],
|
460 |
+
help='Action to perform: "download" - performs recovery downloading,'
|
461 |
+
' "selfcheck" checks whether MLB serial validation is possible, "verify" performs'
|
462 |
+
' MLB serial verification, "guess" tries to find suitable mac model for MLB.')
|
463 |
+
parser.add_argument('-o', '--outdir', type=str, default='com.apple.recovery.boot',
|
464 |
+
help='customise output directory for downloading, defaults to com.apple.recovery.boot')
|
465 |
+
parser.add_argument('-n', '--basename', type=str, default='',
|
466 |
+
help='customise base name for downloading, defaults to remote name')
|
467 |
+
parser.add_argument('-b', '--board-id', type=str, default=RECENT_MAC,
|
468 |
+
help=f'use specified board identifier for downloading, defaults to {RECENT_MAC}')
|
469 |
+
parser.add_argument('-m', '--mlb', type=str, default=MLB_ZERO,
|
470 |
+
help=f'use specified logic board serial for downloading, defaults to {MLB_ZERO}')
|
471 |
+
parser.add_argument('-e', '--code', type=str, default='',
|
472 |
+
help='generate product logic board serial with specified product EEEE code')
|
473 |
+
parser.add_argument('-os', '--os-type', type=str, default='default', choices=['default', 'latest'],
|
474 |
+
help=f'use specified os type, defaults to default {MLB_ZERO}')
|
475 |
+
parser.add_argument('-diag', '--diagnostics', action='store_true', help='download diagnostics image')
|
476 |
+
parser.add_argument('-v', '--verbose', action='store_true', help='print debug information')
|
477 |
+
parser.add_argument('-db', '--board-db', type=str, default=os.path.join(SELF_DIR, 'boards.json'),
|
478 |
+
help='use custom board list for checking, defaults to boards.json')
|
479 |
+
|
480 |
+
args = parser.parse_args()
|
481 |
+
|
482 |
+
if args.code != '':
|
483 |
+
args.mlb = mlb_from_eeee(args.code)
|
484 |
+
|
485 |
+
if len(args.mlb) != 17:
|
486 |
+
print('ERROR: Cannot use MLBs in non 17 character format!')
|
487 |
+
sys.exit(1)
|
488 |
+
|
489 |
+
if args.action == 'download':
|
490 |
+
return action_download(args)
|
491 |
+
if args.action == 'selfcheck':
|
492 |
+
return action_selfcheck(args)
|
493 |
+
if args.action == 'verify':
|
494 |
+
return action_verify(args)
|
495 |
+
if args.action == 'guess':
|
496 |
+
return action_guess(args)
|
497 |
+
|
498 |
+
assert False
|
499 |
+
|
500 |
+
|
501 |
+
if __name__ == '__main__':
|
502 |
+
sys.exit(main())
|