2018-11-03 12:29:47 +00:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
from datetime import datetime
|
2018-11-06 00:38:59 +00:00
|
|
|
from machfs import Volume
|
2018-12-10 08:10:06 +00:00
|
|
|
import os
|
2018-11-03 12:29:47 +00:00
|
|
|
|
|
|
|
########################################################################
|
|
|
|
|
|
|
|
def hfsdat(x):
|
|
|
|
if x.lower() == 'now':
|
|
|
|
x = datetime.now().isoformat()
|
|
|
|
|
|
|
|
if len(x) == 8 and all(c in '0123456789ABCDEF' for c in x.upper()):
|
|
|
|
try:
|
|
|
|
return int(x, base=16)
|
|
|
|
except ValueError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
epoch = '19040101000000' # ISO8601 with the non-numerics stripped
|
|
|
|
|
|
|
|
# strip non-numerics and pad out using the epoch (cheeky)
|
|
|
|
stripped = ''.join(c for c in x if c in '0123456789')
|
|
|
|
stripped = stripped[:len(epoch)] + epoch[len(stripped):]
|
|
|
|
|
|
|
|
tformat = '%Y%m%d%H%M%S'
|
|
|
|
|
|
|
|
delta = datetime.strptime(stripped, tformat) - datetime.strptime(epoch, tformat)
|
|
|
|
delta = int(delta.total_seconds())
|
|
|
|
|
|
|
|
if not 0 <= delta <= 0xFFFFFFFF:
|
|
|
|
print('Warning: moving %r into the legacy MacOS date range (1904-2040)' % x)
|
|
|
|
|
|
|
|
delta = min(delta, 0xFFFFFFFF)
|
|
|
|
delta = max(delta, 0)
|
|
|
|
|
|
|
|
return delta
|
|
|
|
|
|
|
|
def imgsize(x):
|
|
|
|
x = x.upper()
|
|
|
|
x = x.replace('B', '').replace('I', '')
|
|
|
|
if x.endswith('K'):
|
|
|
|
factor = 1024
|
|
|
|
elif x.endswith('M'):
|
|
|
|
factor = 1024*1024
|
|
|
|
elif x.endswith('G'):
|
|
|
|
factor = 1024*1024*1024
|
2018-12-10 08:10:43 +00:00
|
|
|
elif x.endswith('T'):
|
|
|
|
factor = 1024*1024*1024*1024
|
2018-11-03 12:29:47 +00:00
|
|
|
else:
|
|
|
|
factor = 1
|
|
|
|
x += 'b'
|
|
|
|
return int(x[:-1]) * factor
|
|
|
|
|
2018-11-05 15:46:58 +00:00
|
|
|
def hfspathtpl(s):
|
|
|
|
return tuple(c for c in s.split(':') if c)
|
|
|
|
|
2018-11-03 12:29:47 +00:00
|
|
|
args = argparse.ArgumentParser()
|
|
|
|
|
|
|
|
args.add_argument('dest', metavar='OUTPUT', nargs=1, help='Destination file')
|
|
|
|
args.add_argument('-n', '--name', default='untitled', action='store', help='volume name (default: untitled)')
|
|
|
|
args.add_argument('-i', '--dir', action='store', help='folder to copy into the image')
|
2018-11-05 15:46:58 +00:00
|
|
|
args.add_argument('-a', '--app', default=None, type=hfspathtpl, help='Path:To:Startup:App')
|
2018-12-10 08:10:06 +00:00
|
|
|
args.add_argument('-s', '--size', default=None, type=imgsize, action='store', help='volume size (default: sized for OUTPUT, or 800k)')
|
2018-11-03 12:29:47 +00:00
|
|
|
args.add_argument('-d', '--date', default='1994', type=hfsdat, action='store', help='creation & mod date (ISO-8601 or "now")')
|
|
|
|
args.add_argument('--mpw-dates', action='store_true', help='''
|
|
|
|
preserve the modification order of files by setting on-disk dates
|
|
|
|
that differ by 1-minute increments, so that MPW Make can decide
|
|
|
|
which files to rebuild
|
|
|
|
''')
|
|
|
|
|
|
|
|
args = args.parse_args()
|
|
|
|
|
|
|
|
########################################################################
|
|
|
|
|
2018-12-10 08:10:06 +00:00
|
|
|
integral_sizes = [800*1024, 1024*1024]
|
|
|
|
while integral_sizes[-1] < 2 * 1024**4: # absolute max = 2TB
|
|
|
|
integral_sizes.append(integral_sizes[-1] * 2)
|
|
|
|
integral_sizes = [x // 512 for x in integral_sizes]
|
|
|
|
|
|
|
|
def is_at_least(f, size):
|
|
|
|
try:
|
|
|
|
f.seek(size - 512)
|
|
|
|
if len(f.read(512)) == 512:
|
|
|
|
return True
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
return False
|
|
|
|
|
|
|
|
def hack_file_size(f):
|
|
|
|
size = f.seek(0, 2) # seek to the end of the file
|
|
|
|
if size: # this should work most of the time, but...
|
|
|
|
size -= size % 512
|
|
|
|
return size
|
|
|
|
|
|
|
|
f.seek(0)
|
|
|
|
if len(f.read(1)) == 0: return 0
|
|
|
|
|
|
|
|
# exponentially increase the size...
|
|
|
|
left_ge = 0
|
|
|
|
for trysize in integral_sizes:
|
|
|
|
if is_at_least(f, trysize * 512):
|
|
|
|
left_ge = trysize
|
|
|
|
else:
|
|
|
|
right_lt = trysize
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
return integral_sizes[-1] # reached max size
|
|
|
|
if left_ge == 0: return 0 # never got anywhere
|
|
|
|
|
|
|
|
# then refine with binary search
|
|
|
|
while left_ge + 1 < right_lt:
|
|
|
|
midpoint = left_ge + (right_lt - left_ge)//2
|
|
|
|
if is_at_least(f, midpoint * 512):
|
|
|
|
left_ge = midpoint
|
|
|
|
else:
|
|
|
|
right_lt = midpoint
|
|
|
|
|
|
|
|
return left_ge * 512
|
|
|
|
|
2018-11-03 12:29:47 +00:00
|
|
|
vol = Volume()
|
|
|
|
vol.name = args.name
|
|
|
|
|
2018-11-06 00:38:59 +00:00
|
|
|
if args.dir: vol.read_folder(args.dir, date=args.date, mpw_dates=args.mpw_dates)
|
2018-11-03 12:29:47 +00:00
|
|
|
|
2018-12-10 08:10:06 +00:00
|
|
|
with open(args.dest[0], 'ab+') as f:
|
|
|
|
if args.size is None: args.size = hack_file_size(f)
|
|
|
|
if args.size == 0: args.size = 800 * 1024
|
|
|
|
|
|
|
|
with open(args.dest[0], 'rb+') as f:
|
|
|
|
left, gap, right = vol.write(args.size, startapp=args.app, sparse=True)
|
|
|
|
|
|
|
|
f.write(left)
|
|
|
|
f.seek(gap, 1)
|
|
|
|
problem = len(left) + gap - f.tell()
|
|
|
|
if problem > 0:
|
|
|
|
f.write(bytes(problem))
|
|
|
|
f.write(right)
|
|
|
|
f.truncate()
|