#!/usr/bin/env python3 import argparse import os import os.path as path import re from datetime import datetime from machfs import Volume, Folder, File import macresources ######################################################################## 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 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='800k', type=imgsize, action='store', help='volume size (default: size of OUTPUT)') 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() ######################################################################## vol = Volume() vol.name = args.name vol.crdate = vol.mddate = vol.bkdate = args.date ######################################################################## def includefilter(n): if n.startswith('.'): return False if n.endswith('.rdump'): return True if n.endswith('.idump'): return True return True def swapsep(n): return n.replace(':', path.sep) def mkbasename(n): base, ext = path.splitext(n) if ext in ('.rdump', '.idump'): return base else: return n if args.dir is not None: tmptree = {args.dir: vol} for dirpath, dirnames, filenames in os.walk(args.dir): dirnames[:] = [swapsep(x) for x in dirnames if includefilter(x)] filenames[:] = [swapsep(x) for x in filenames if includefilter(x)] for dn in dirnames: newdir = Folder() newdir.crdate = newdir.mddate = newdir.bkdate = args.date tmptree[dirpath][dn] = newdir tmptree[path.join(dirpath, dn)] = newdir for fn in filenames: basename = mkbasename(fn) fullbase = path.join(dirpath, basename) fullpath = path.join(dirpath, fn) try: thefile = tmptree[fullbase] except KeyError: thefile = File() thefile.real_t = 0 # for the MPW hack thefile.crdate = thefile.mddate = thefile.bkdate = args.date thefile.contributors = [] tmptree[fullbase] = thefile if fn.endswith('.idump'): with open(fullpath, 'rb') as f: thefile.type = f.read(4) thefile.creator = f.read(4) elif fn.endswith('rdump'): rez = open(fullpath, 'rb').read() resources = macresources.parse_rez_code(rez) resfork = macresources.make_file(resources, align=4) thefile.rsrc = resfork else: thefile.data = open(fullpath, 'rb').read() thefile.contributors.append(fullpath) if args.mpw_dates: thefile.real_t = max(thefile.real_t, path.getmtime(fullpath)) tmptree[dirpath][basename] = thefile for pathtpl, obj in vol.iter_paths(): try: if obj.type == b'TEXT': obj.data = obj.data.decode('utf8').replace('\r\n', '\r').replace('\n', '\r').encode('mac_roman') except AttributeError: pass ######################################################################## if args.mpw_dates: all_real_times = set() for pathtpl, obj in vol.iter_paths(): try: all_real_times.add(obj.real_t) except AttributeError: pass ts2idx = {ts: idx for (idx, ts) in enumerate(sorted(set(all_real_times)))} for pathtpl, obj in vol.iter_paths(): try: real_t = obj.real_t except AttributeError: pass else: fake_t = obj.crdate + 60 * ts2idx[real_t] obj.crdate = obj.mddate = obj.bkdate = fake_t ######################################################################## image = vol.write(args.size, startapp=args.app) with open(args.dest[0], 'wb') as f: f.write(image)