Files
dualfroz d32a327c5f Stop rewriting an alias file's own metadata
Writing a volume rebuilds every alias file, because the `alis` record
names its target by CNID and CNIDs are assigned afresh on each write.
It rebuilt more than the record, though: the file's type and creator
were overwritten with ones guessed from the target, the rest of the
resource fork was thrown away, and the data fork was cleared.

Round-tripping the Apple Menu's Control Panels alias out of a System 7
image therefore turned `fact` into `fdrp`, dropped its `vers` resources,
and renamed the `alis` from "Control Panels alias" to "Control Panels".
Aliases to applications lost their `adrp` for whatever the target's type
happened to be -- `APPC`, `cdev`.

Only the `alis` resource is replaced now, in place and under its own
name; type and creator are guessed only for an alias built here, which
has neither. Reading and rewriting the 29 infinite-mac images machfs
can parse touches 73 alias files: type and creator used to change on 10
of them and now change on none, and 28 resource forks used to lose more
than the `alis` record and now lose nothing else. (Their data forks were
already empty, so clearing those had cost nothing in practice.)

For mihaip/infinite-mac#84
2026-09-13 15:20:49 +02:00

228 lines
6.4 KiB
Python

from machfs import *
from machfs import bitmanip, btree
from machfs.main import FinderFlags
from macresources import Resource, make_file, parse_file
import os
import struct
import time
def test_upperlower():
h = Volume()
h['alpha'] = File()
assert h['alpha'] is h['ALPHA']
assert list(h.keys()) == ['alpha']
def test_roundtrip():
h = Volume()
f = File()
h['single file'] = f
f.data = f.rsrc = b'1234' * 4096
copies = [h.write(800*1024)]
for i in range(2):
h2 = Volume()
h2.read(copies[-1])
copies.append(h2.write(800*1024))
assert copies[0] == copies[1]
assert copies[1] == copies[2]
assert f.data in copies[-1]
def test_alias_keeps_what_belongs_to_the_file():
h = Volume()
h.name = 'AliasTest'
target = File()
target.type, target.creator = b'APPC', b'wnkl'
target.data = b'target data'
h['target'] = target
# Neither 'fact' nor the 'vers' resource can be derived from the target.
vers = Resource(b'vers', 2, data=b'\x01\x00\x80\x00\x00\x00\x041.0')
stale = Resource(b'alis', 0, name='alias resource name', data=bytes(150))
alias = File()
alias.flags |= FinderFlags.kIsAlias
alias.type, alias.creator = b'fact', b'MACS'
alias.rsrc = make_file([stale, vers])
alias.aliastarget = target
h['the alias'] = alias
copy = Volume()
copy.read(h.write(10*1024*1024, desktopdb=False, bootable=False))
got = copy['the alias']
assert (got.type, got.creator) == (b'fact', b'MACS')
resources = {(r.type, r.id): r for r in parse_file(got.rsrc)}
assert resources[b'vers', 2].data == vers.data
assert resources[b'alis', 0].name == 'alias resource name'
# The record itself holds this volume's CNIDs, so it is rebuilt.
assert resources[b'alis', 0].data != stale.data
def test_open_folder():
h = Volume()
h.name = 'OpenFolderTest'
h['folder'] = Folder()
for open_folder, expected_cnid in [
(None, 0),
(h, 2),
(h['folder'], 16),
]:
h.open_folder = open_folder
image = h.write(
10 * 1024 * 1024,
desktopdb=False,
bootable=False,
)
mdb = struct.unpack_from(
'>2sLLHHHHHLLHLH28pLHLLLHLL32sHHHL12sL12s', image, 1024
)
assert struct.unpack('>8L', mdb[22])[2] == expected_cnid
copy = Volume()
copy.read(image)
if open_folder is None:
assert copy.open_folder is None
elif open_folder is h:
assert copy.open_folder is copy
else:
assert copy.open_folder is copy['folder']
h['folder']['app'] = File()
h.open_folder = None
image = h.write(
10 * 1024 * 1024,
desktopdb=False,
bootable=False,
startapp=('folder', 'app'),
)
mdb = struct.unpack_from(
'>2sLLHHHHHLLHLH28pLHLLLHLL32sHHHL12sL12s', image, 1024
)
finder_info = struct.unpack('>8L', mdb[22])
assert finder_info[1] == 16
assert finder_info[2] == 0
def test_file_catalog_reserved_fields():
h = Volume()
h.name = 'CatalogFieldsTest'
f = File()
f.data = b'data fork'
f.rsrc = b'resource fork'
h['file'] = f
image = h.write(
10 * 1024 * 1024,
desktopdb=False,
bootable=False,
)
mdb = struct.unpack_from(
'>2sLLHHHHHLLHLH28pLHLLLHLL32sHHHL12sL12s', image, 1024
)
allocation_block_size = mdb[8]
allocation_block_start = mdb[10]
catalog_size = mdb[-2]
catalog_extents = btree.unpack_extent_record(mdb[-1])
catalog = b''.join(
image[
512 * allocation_block_start + first_block * allocation_block_size:
512 * allocation_block_start + (first_block + block_count) * allocation_block_size
]
for first_block, block_count in catalog_extents
)[:catalog_size]
file_record = None
for record in btree.dump_btree(catalog):
key_length = record[0]
value = record[bitmanip.pad_up(1 + key_length, 2):]
if value[0] == 2:
file_record = struct.unpack(
'>BxBB16sLHLLHLLLLL16sH12s12sL', value
)
break
assert file_record is not None
assert file_record[1] & 0x02 == 0 # kHFSThreadExistsMask
assert file_record[5] == 0 # dataStartBlock (reserved)
assert file_record[8] == 0 # rsrcStartBlock (reserved)
assert file_record[18] == 0 # reserved
assert btree.unpack_extent_record(file_record[16])
assert btree.unpack_extent_record(file_record[17])
def test_map_node_heights():
# Force enough leaf nodes to require more bitmap space than the header
# node provides, and enough total nodes to require multiple map nodes.
records = [(i.to_bytes(4, 'big'), bytes(470)) for i in range(6000)]
tree = btree.make_btree(records, bthKeyLen=37, blksize=512)
first_map_node, _, _, _, _ = btree._unpack_btree_node(tree, 0)
assert first_map_node != 0
map_node_count = 0
map_node = first_map_node
while map_node:
map_node, _, node_type, node_height, _ = btree._unpack_btree_node(
tree, 512 * map_node
)
assert node_type == 2
assert node_height == 0
map_node_count += 1
assert map_node_count > 1
def test_macos_mount():
h = Volume()
h.name = 'ElmoTest'
hf = File()
hf.data = b'12345' * 10
for i in reversed(range(100)):
last = 'testfile-%03d' % i
h[last] = hf
ser = h.write(10*1024*1024)
open('/tmp/SMALL.dmg','wb').write(ser)
os.system('hdiutil attach /tmp/SMALL.dmg')
n = 10
while 1:
n += 1
assert n < 200
time.sleep(0.1)
try:
os.stat('/Volumes/ElmoTest/testfile-000')
except:
pass
else:
break
recovered = open('/Volumes/ElmoTest/' + last,'rb').read()
os.system('umount /Volumes/ElmoTest')
assert recovered == hf.data
h2 = Volume()
h2.read(ser)
assert h2['testfile-000'].data == hf.data
# def test_extents_overflow():
# h = Volume()
# h.read(open('SourceForEmulator.dmg','rb').read())
# assert h['aa'].data == b'a' * 278528
def test_many_sizes():
sizes = [800*1024, 1024*1024]
while sizes[-1] < 4*1024*1024*1024:
sizes.append(sizes[-1] * 2)
# okay, we have heaps of sizes
v = Volume()
v.name = 'ImportantTestVol'
for s in sizes:
ser = v.write(s-512)
open('/tmp/SMALL-%X.dmg'%s, 'wb').write(ser)