Created
February 25, 2024 10:17
-
-
Save m0001a/50f7daec316049ea9a90a96abdceed93 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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