xcx0902 commited on
Commit
fa5280c
·
verified ·
1 Parent(s): c6b1cd4

Upload macrecovery.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. 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())