Skip to content

Instantly share code, notes, and snippets.

@m0001a
Created February 25, 2024 10:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save m0001a/50f7daec316049ea9a90a96abdceed93 to your computer and use it in GitHub Desktop.
Save m0001a/50f7daec316049ea9a90a96abdceed93 to your computer and use it in GitHub Desktop.
import argparse
from pathlib import Path
from dataclasses import dataclass
def from_beint(b):
return int.from_bytes(b, 'big')
def from_word(b):
assert len(b) == 2
return from_beint(b)
def from_long_word(b):
assert len(b) == 4
return from_beint(b)
def to_beint(i, width):
return i.to_bytes(width, 'big')
def to_word(i):
assert -(1 << 16) <= i <= ((1 << 16) - 1)
return to_beint(i, 2)
def to_long_word(i):
assert (-(1 << 31) <= i <= ((1 << 31) - 1) or i < (1 << 32))
return to_beint(i, 4)
@dataclass
class VolumeInformation:
drSigWord: int
drCrDate: int
drLsBkUp: int
drAtrb: int
drNmFls: int
drDirSt: int
drBlLen: int
drNmAlBlks: int
drAlBlkSiz: int
drClpSiz: int
drAlBlSt: int
drNxtFNum: int
drFreeBks: int
drVN: int
name: bytes
@classmethod
def from_variable_bytes(cls, data):
drSigWord = from_word(data[0:2])
drCrDate = from_long_word(data[2:6])
drLsBkUp = from_long_word(data[6:10])
drAtrb = from_word(data[10:12])
drNmFls = from_word(data[12:14])
drDirSt = from_word(data[14:16])
drBlLen = from_word(data[16:18])
drNmAlBlks = from_word(data[18:20])
drAlBlkSiz = from_long_word(data[20:24])
drClpSiz = from_long_word(data[24:28])
drAlBlSt = from_word(data[28:30])
drNxtFNum = from_long_word(data[30:34])
drFreeBks = from_word(data[34:36])
drVN = data[36]
name = data[37:37+drVN]
return cls(
drSigWord=drSigWord,
drCrDate=drCrDate,
drLsBkUp=drLsBkUp,
drAtrb=drAtrb,
drNmFls=drNmFls,
drDirSt=drDirSt,
drBlLen=drBlLen,
drNmAlBlks=drNmAlBlks,
drAlBlkSiz=drAlBlkSiz,
drClpSiz=drClpSiz,
drAlBlSt=drAlBlSt,
drNxtFNum=drNxtFNum,
drFreeBks=drFreeBks,
drVN=drVN,
name=name,
), data[37+drVN:]
class Blocked:
BLOCK_SIZE = 512
def __init__(self, contents):
self.contents = contents
def _offset_slice(self, s):
start = None if s.start is None else s.start * self.BLOCK_SIZE
stop = None if s.stop is None else s.stop * self.BLOCK_SIZE
step = None if s.step is None else s.step * self.BLOCK_SIZE
return slice(start, stop, step)
def __getitem__(self, key):
if isinstance(key, slice):
return self.contents[self._offset_slice(key)]
if isinstance(key, int):
return self.contents[self._offset_slice(slice(key, key+1, None))]
raise ValueError("unknown key type: {key!r}")
def __setitem__(self, key, value):
try:
value_iter = iter(value)
except TypeError:
raise ValueError("value must be iterable, not {value!r}")
contents = bytearray(self.contents)
if isinstance(key, slice):
contents[self._offset_slice(key)] = value_iter
elif isinstance(key, int):
contents[self._offset_slice(slice(key, key+1, None))] = value_iter
else:
raise ValueError("unknown key type: {key!r}")
self.contents = bytes(contents)
@dataclass
class FileFlags:
# https://web.archive.org/web/20070927021320/http://mactech.com/articles/mactech/Vol.01/01.05/Disks/
used: bool
locked: bool
copy_protected: bool
original: int
@classmethod
def from_byte(cls, byte):
used = bool(byte & (1 << 7))
copy_protected = bool(byte & (1 << 6))
locked = bool(byte & 1)
return cls(used=used,
locked=locked,
copy_protected=copy_protected,
original=byte)
def to_byte(self):
byte = self.original
for flag, bit in [(self.used, 7),
(self.copy_protected, 6),
(self.locked, 0)]:
if flag:
byte |= (1 << bit)
else:
byte &= ~(1 << bit)
return byte
@dataclass
class FinderFlags:
# https://vintageapple.org/macprogramming/pdf/Macintosh_Revealed_2nd_Edition_Volume_One_1987.pdf
locked: bool
invisible: bool
bundle: bool
system: bool
bozo: bool
busy: bool
changed: bool
inited: bool
private: int
original: int
@classmethod
def from_bytes(cls, data):
flags = from_word(data)
private = flags & 0xFF
locked = bool(flags & (1 << 15))
invisible = bool(flags & (1 << 14))
bundle = bool(flags & (1 << 13))
system = bool(flags & (1 << 12))
bozo = bool(flags & (1 << 11))
busy = bool(flags & (1 << 10))
changed = bool(flags & (1 << 9))
inited = bool(flags & (1 << 8))
return cls(
locked=locked,
invisible=invisible,
bundle=bundle,
system=system,
bozo=bozo,
busy=busy,
changed=changed,
inited=inited,
private=private,
original=flags,
)
def to_bytes(self):
flags = (0 << 8) | self.private
for flag, bit in [(self.locked, 15),
(self.invisible, 14),
(self.bundle, 13),
(self.system, 12),
(self.bozo, 11),
(self.busy, 10),
(self.changed, 9),
(self.inited, 8)]:
if flag:
flags |= (1 << bit)
else:
flags &= ~(1 << bit)
return to_word(flags)
@dataclass
class UserWords:
file_type: bytes
file_creator: bytes
finder_flags: int
word_2: int
word_3: int
word_4: int
@classmethod
def from_bytes(cls, data):
file_type = data[:4]
file_creator = data[4:8]
finder_flags = FinderFlags.from_bytes(data[8:10])
word_2 = from_word(data[10:12])
word_3 = from_word(data[12:14])
word_4 = from_word(data[14:16])
return cls(file_type=file_type,
file_creator=file_creator,
finder_flags=finder_flags,
word_2=word_2,
word_3=word_3,
word_4=word_4)
def to_bytes(self):
data = bytearray()
data.extend(self.file_type)
data.extend(self.file_creator)
data.extend(self.finder_flags.to_bytes())
data.extend(to_word(self.word_2))
data.extend(to_word(self.word_3))
data.extend(to_word(self.word_4))
return bytes(data)
@dataclass
class FileDirectoryEntry:
flFlags: FileFlags
flTyp: int
flUsrWrds: int
flFlNum: bytes
flStBlk: int
flLgLen: int
flPyLen: int
fRStBlk: int
flRLgLen: int
flRPyLen: int
flCrDat: int
flMdDat: int
flNam: bytes
name: bytes
padding_length: int
@classmethod
def from_variable_bytes(cls, data):
flFlags = FileFlags.from_byte(data[0])
flTyp = data[1]
flUsrWrds = UserWords.from_bytes(data[2:18])
flFlNum = from_long_word(data[18:22])
flStBlk = from_word(data[22:24])
flLgLen = from_long_word(data[24:28])
flPyLen = from_long_word(data[28:32])
fRStBlk = from_word(data[32:34])
flRLgLen = from_long_word(data[34:38])
flRPyLen = from_long_word(data[38:42])
flCrDat = from_long_word(data[42:46])
flMdDat = from_long_word(data[46:50])
flNam = data[50]
name = data[51:51+flNam]
padding_and_next_data = data[51+flNam:]
# padding consists of a contiguous sequence of zeroes
next_data = padding_and_next_data.lstrip(b'\x00')
padding_length = len(padding_and_next_data) - len(next_data)
return cls(flFlags=flFlags,
flTyp=flTyp,
flUsrWrds=flUsrWrds,
flFlNum=flFlNum,
flStBlk=flStBlk,
flLgLen=flLgLen,
flPyLen=flPyLen,
fRStBlk=fRStBlk,
flRLgLen=flRLgLen,
flRPyLen=flRPyLen,
flCrDat=flCrDat,
flMdDat=flMdDat,
flNam=flNam,
name=name,
padding_length=padding_length), next_data
def to_bytes(self):
data = bytearray()
data.append(self.flFlags.to_byte())
data.append(self.flTyp)
data.extend(self.flUsrWrds.to_bytes())
data.extend(to_long_word(self.flFlNum))
data.extend(to_word(self.flStBlk))
data.extend(to_long_word(self.flLgLen))
data.extend(to_long_word(self.flPyLen))
data.extend(to_word(self.fRStBlk))
data.extend(to_long_word(self.flRLgLen))
data.extend(to_long_word(self.flRPyLen))
data.extend(to_long_word(self.flCrDat))
data.extend(to_long_word(self.flMdDat))
data.append(self.flNam)
data.extend(self.name)
data.extend(bytes(self.padding_length))
return bytes(data)
def remove_copy_protection(input_path, output_path):
blocks = Blocked(input_path.read_bytes())
vi, _ = VolumeInformation.from_variable_bytes(blocks[2])
parsed_entries = {}
original_entries = entries = blocks[vi.drDirSt:vi.drDirSt + vi.drBlLen]
while entries:
fe, entries = FileDirectoryEntry.from_variable_bytes(entries)
if not fe.flFlags.used:
break
name = fe.name.decode('mac-roman')
print(name)
print(f" copy protected? {fe.flFlags.copy_protected}")
print(f" {fe.flUsrWrds}")
parsed_entries[name] = fe
assert b''.join(
e.to_bytes()
for e in parsed_entries.values()
) == original_entries
for e in parsed_entries.values():
e.flFlags.copy_protected = False
blocks[vi.drDirSt:vi.drDirSt + vi.drBlLen] = b''.join(
e.to_bytes()
for e in parsed_entries.values()
)
output_path.write_bytes(blocks.contents)
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description='remove copy protection bits from'
' all files on an MFS disk image'
)
parser.add_argument('input_path',
help='the MFS image with copy protected files',
type=Path)
parser.add_argument('output_path',
help='where to write the patched MFS image',
type=Path)
args = parser.parse_args()
remove_copy_protection(args.input_path, args.output_path)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment