#!/usr/bin/env python3 import argparse from datetime import datetime from machfs import Volume import os ######################################################################## 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 elif x.endswith('T'): factor = 1024*1024*1024*1024 else: factor = 1 x += 'b' return int(x[:-1]) * factor def hfspathtpl(s): return tuple(c for c in s.split(':') if c) 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') args.add_argument('-a', '--app', default=None, type=hfspathtpl, help='Path:To:Startup:App') args.add_argument('-s', '--size', default=None, type=imgsize, action='store', help='volume size (default: sized for OUTPUT, or 800k)') 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() ######################################################################## 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 vol = Volume() vol.name = args.name if args.dir: vol.read_folder(args.dir, date=args.date, mpw_dates=args.mpw_dates) try: f = open(args.dest[0], 'rb+') existed = True except FileNotFoundError: f = open(args.dest[0], 'wb+') existed = False if args.size is not None: offset = 0 size = args.size trunc = True nameoffset = None elif not existed: # make a tiny floppy offset = 0 size = 800 * 1024 trunc = True nameoffset = None elif f.read(2) == b'ER': # is partitioned disk blksize = int.from_bytes(f.read(2), byteorder='big') f.seek(blksize + 4) entrycnt = int.from_bytes(f.read(4), byteorder='big') for i in range(entrycnt): entryoffset = blksize * (i + 1) f.seek(entryoffset + 48) if f.read(10) == b'Apple_HFS\x00': f.seek(entryoffset + 8) offset = blksize * int.from_bytes(f.read(4), byteorder='big') size = blksize * int.from_bytes(f.read(4), byteorder='big') trunc = False nameoffset = entryoffset + 16 break else: raise ValueError("No HFS partition in this map") else: # is raw filesystem offset = 0 size = hack_file_size(f) trunc = False left, gap, right = vol.write(size, startapp=args.app, sparse=True) f.seek(offset) f.write(left) f.seek(gap, 1) problem = len(left) + gap - f.tell() if problem > 0: f.write(bytes(problem)) f.write(right) if nameoffset is not None: f.seek(nameoffset) f.write(args.name.encode('mac_roman').ljust(32, b'\x00')) if trunc: f.truncate()