Skip to content

Commit a7f6937

Browse files
committed
Add Mac OS X variant of Infinite HD
Allows .dmgs as sources, we mount them using hdiutil and copy the files over, preserving Finder metadata as much as possible. Updates #357
1 parent b7186ef commit a7f6937

File tree

6 files changed

+144
-10
lines changed

6 files changed

+144
-10
lines changed

Library/Utilities/Iconographer X.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"src_url": "https://mscape.com/downloads/iconographerX.dmg",
3+
"src_folder": "Iconographer X 2.5",
4+
"needs_mac_os_x": true
5+
}

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
git+https://github.com/mihaip/machfs.git@0d2a3b200b2bd08f64039fdfed34c7f535479398#egg=machfs
22
beautifulsoup4 == 4.12.2
33
Pillow == 9.5.0
4+
xattr == 1.1.4

scripts/import-disks.py

Lines changed: 127 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,33 +24,38 @@
2424
import time
2525
import unicodedata
2626
import urls
27+
import xattr
2728

2829
CHUNK_SIZE = 256 * 1024
2930

3031
ImportFolders = typing.Tuple[
3132
typing.Dict[str, machfs.Folder], # Universal folders
3233
typing.Dict[str, machfs.Folder], # Folders that need System 7
34+
typing.Dict[str, machfs.Folder], # Folders that need Mac OS X
3335
]
3436

3537
def get_import_folders() -> ImportFolders:
3638
import_folders = {}
3739
import_folders7 = {}
40+
import_foldersX = {}
3841

39-
manifest_folders, manifest_folders7 = import_manifests()
42+
manifest_folders, manifest_folders7, manifest_foldersX = import_manifests()
4043
import_folders.update(manifest_folders)
4144
import_folders7.update(manifest_folders7)
45+
import_foldersX.update(manifest_foldersX)
4246

4347
zip_folders, zip_folder7 = import_zips()
4448
import_folders.update(zip_folders)
4549
import_folders7.update(zip_folder7)
4650

47-
return import_folders, import_folders7
51+
return import_folders, import_folders7, import_foldersX
4852

4953

5054
def import_manifests() -> ImportFolders:
5155
sys.stderr.write("Importing other images\n")
5256
import_folders = {}
5357
import_folders7 = {}
58+
import_foldersX = {}
5459
debug_filter = os.getenv("DEBUG_LIBRARY_FILTER")
5560

5661
for manifest_path in glob.iglob(os.path.join(paths.LIBRARY_DIR, "**",
@@ -76,15 +81,22 @@ def import_manifests() -> ImportFolders:
7681
"(build it with npm run build-xadmaster)\n")
7782
continue
7883
folder = import_archive(manifest_json)
84+
elif src_ext in [".dmg"]:
85+
if not os.path.exists(paths.HDIUTIL_PATH):
86+
sys.stderr.write(" Skipping .dmg import, hdiutil not found\n")
87+
continue
88+
folder = import_dmg(manifest_json)
7989
else:
8090
assert False, "Unexpected manifest URL extension: %s" % src_ext
8191

82-
if manifest_json.get("needs_system_7"):
92+
if manifest_json.get("needs_mac_os_x"):
93+
import_foldersX[folder_path] = folder
94+
elif manifest_json.get("needs_system_7"):
8395
import_folders7[folder_path] = folder
8496
else:
8597
import_folders[folder_path] = folder
8698

87-
return import_folders, import_folders7
99+
return import_folders, import_folders7, import_foldersX
88100

89101

90102
def import_disk_image(
@@ -319,6 +331,96 @@ def convert_date(date_str: str) -> int:
319331
file_or_folder.crdate = convert_date(entry["XADCreationDate"])
320332

321333

334+
def import_dmg(
335+
manifest_json: typing.Dict[str, typing.Any]) -> machfs.Folder:
336+
src_url = manifest_json["src_url"]
337+
archive_path = urls.read_url_to_path(src_url)
338+
root_folder = machfs.Folder()
339+
340+
def normalize(name: str) -> str:
341+
# Normalizes accented characters to their combined form, since only
342+
# those have an equivalent in the MacRoman encoding that HFS ends up
343+
# using.
344+
return unicodedata.normalize("NFC", name)
345+
346+
with tempfile.TemporaryDirectory() as tmp_dir_path:
347+
hdiutil_code = subprocess.call([
348+
paths.HDIUTIL_PATH, "attach", archive_path, "-mountpoint",
349+
tmp_dir_path
350+
],
351+
stdout=subprocess.DEVNULL)
352+
if hdiutil_code != 0:
353+
assert False, "Could not mount .dmg: %s (cached at %s):" % (
354+
src_url, archive_path)
355+
try:
356+
# TODO: allow selection of a child folder
357+
root_dir_path = tmp_dir_path
358+
359+
if "src_folder" in manifest_json:
360+
src_folder_name = manifest_json["src_folder"]
361+
root_dir_path = os.path.join(root_dir_path, src_folder_name)
362+
update_folder_from_xattr(root_folder, root_dir_path)
363+
clear_folder_window_position(root_folder)
364+
365+
for dir_path, dir_names, file_names in os.walk(root_dir_path):
366+
folder = root_folder
367+
dir_rel_path = os.path.relpath(dir_path, root_dir_path)
368+
if dir_rel_path != ".":
369+
folder_path_pieces = []
370+
for folder_name in dir_rel_path.split(os.path.sep):
371+
folder_path_pieces.append(folder_name)
372+
folder_name = normalize(folder_name)
373+
if folder_name not in folder:
374+
new_folder = folder[folder_name] = machfs.Folder()
375+
update_folder_from_xattr(new_folder, os.path.join(root_dir_path,
376+
*folder_path_pieces))
377+
folder = folder[folder_name]
378+
for file_name in file_names:
379+
file_path = os.path.join(dir_path, file_name)
380+
file = machfs.File()
381+
with open(file_path, "rb") as f:
382+
file.data = f.read()
383+
resource_fork_path = os.path.join(file_path, "..namedfork",
384+
"rsrc")
385+
if os.path.exists(resource_fork_path):
386+
with open(resource_fork_path, "rb") as f:
387+
file.rsrc = f.read()
388+
389+
update_file_from_xattr(file, file_path)
390+
391+
folder[normalize(file_name)] = file
392+
finally:
393+
hdiutil_code = subprocess.call([
394+
paths.HDIUTIL_PATH, "detach", tmp_dir_path])
395+
if hdiutil_code != 0:
396+
assert False, "Could not unmount .dmg: %s (cached at %s):" % (
397+
src_url, archive_path)
398+
399+
return root_folder
400+
401+
402+
def update_file_from_xattr(file: machfs.File, file_path: str) -> None:
403+
attr_name = "com.apple.FinderInfo"
404+
xattrs = xattr.listxattr(file_path)
405+
if attr_name not in xattrs:
406+
return
407+
finder_info = xattr.getxattr(file_path, attr_name)
408+
(file.type, file.creator, file.flags, file.y, file.x, _, file.fndrInfo) = struct.unpack(
409+
'>4s4sHhhH16s', finder_info)
410+
if file.x == 0 and file.y == 0:
411+
file.flags &= ~machfs.main.FinderFlags.kHasBeenInited
412+
413+
414+
def update_folder_from_xattr(folder: machfs.Folder, folder_path: str) -> None:
415+
attr_name = "com.apple.FinderInfo"
416+
xattrs = xattr.listxattr(folder_path)
417+
if attr_name not in xattrs:
418+
return
419+
# Get creator and type code
420+
finder_info = xattr.getxattr(folder_path, attr_name)
421+
folder.usrInfo = finder_info[0:16]
422+
folder.fndrInfo = finder_info[16:]
423+
322424
SYSTEM7_ZIP_PATHS = {
323425
"Games/Bungie/Marathon Infinity",
324426
"Graphics/Adobe Photoshop 3.0",
@@ -555,8 +657,8 @@ def build_system_image(
555657
return write_image_def(image_data, disk.name, dest_dir)
556658

557659

558-
def build_library_images(dest_dir: str) -> typing.Tuple[ImageDef, ImageDef]:
559-
import_folders, import_folders7 = get_import_folders()
660+
def build_library_images(dest_dir: str) -> typing.Tuple[ImageDef, ImageDef, ImageDef]:
661+
import_folders, import_folders7, import_foldersX = get_import_folders()
560662

561663
v = machfs.Volume()
562664
with open(os.path.join(paths.IMAGES_DIR, "Infinite HD.dsk"), "rb") as base:
@@ -593,7 +695,22 @@ def add_folders(folders: typing.Dict[str, machfs.Folder]) -> None:
593695
)
594696
image_def = write_image_def(image, "Infinite HD.dsk", dest_dir)
595697

596-
return image6_def, image_def
698+
# Not much point in including Classic software for the Mac OS X image, so
699+
# we start with a fresh volume.
700+
v = machfs.Volume()
701+
with open(os.path.join(paths.IMAGES_DIR, "Infinite HD.dsk"), "rb") as base:
702+
v.read(base.read())
703+
v.name = "Infinite HD"
704+
add_folders(import_foldersX)
705+
imageX = v.write(
706+
size=2000 * 1024 * 1024,
707+
align=512,
708+
desktopdb=False,
709+
bootable=False,
710+
)
711+
imageX_def = write_image_def(imageX, "Infinite HDX.dsk", dest_dir)
712+
713+
return image6_def, image_def, imageX_def
597714

598715

599716
def build_passthrough_image(base_name: str, dest_dir: str, compressed: bool = False) -> ImageDef:
@@ -738,12 +855,13 @@ def read_strings(name: str) -> str:
738855
continue
739856
images.append(build_system_image(disk, temp_dir))
740857
if not system_filter:
741-
infinite_hd6_image, infinite_hd_image = build_library_images(temp_dir)
858+
infinite_hd6_image, infinite_hd_image, infinite_hdX_image = build_library_images(temp_dir)
742859
images.append(infinite_hd6_image)
743860
images.append(infinite_hd_image)
861+
images.append(infinite_hdX_image)
744862
if not library_filter:
745863
build_desktop_db6([infinite_hd6_image])
746-
build_desktop_db([infinite_hd_image])
864+
build_desktop_db([infinite_hd_image, infinite_hdX_image])
747865

748866
images.append(
749867
build_passthrough_image("Infinite HD (MFS).dsk",

scripts/paths.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@
1313
XADMASTER_PATH = os.path.join(ROOT_DIR, "XADMaster-build", "Release")
1414
UNAR_PATH = os.path.join(XADMASTER_PATH, "unar")
1515
LSAR_PATH = os.path.join(XADMASTER_PATH, "lsar")
16+
HDIUTIL_PATH = "/usr/bin/hdiutil"

src/Mac.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
SAVED_HD,
2626
INFINITE_HD6,
2727
type DiskFile,
28+
INFINITE_HDX,
2829
} from "./disks";
2930
import {type MachineDefRAMSize, type MachineDef} from "./machines";
3031
import classNames from "classnames";
@@ -189,6 +190,8 @@ export default function Mac({
189190
infiniteHd = INFINITE_HD_MFS;
190191
} else if (disks[0]?.infiniteHdSubset === "system6") {
191192
infiniteHd = INFINITE_HD6;
193+
} else if (disks[0]?.infiniteHdSubset === "macosx") {
194+
infiniteHd = INFINITE_HDX;
192195
} else {
193196
infiniteHd = INFINITE_HD;
194197
}

src/disks.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export type SystemDiskDef = EmulatorDiskDef & {
5454
description: string;
5555
preferredMachine: MachineDef;
5656
appleTalkSupported?: boolean;
57-
infiniteHdSubset?: "mfs" | "system6";
57+
infiniteHdSubset?: "mfs" | "system6" | "macosx";
5858
delayAdditionalDiskMount?: boolean;
5959
appearance?: Appearance;
6060
isUnstable?: boolean;
@@ -958,6 +958,7 @@ const MAC_OS_X_10_2_8: SystemDiskDef = {
958958
],
959959
]),
960960
notable: true,
961+
infiniteHdSubset: "macosx",
961962
isUnstable: true,
962963
hasDeviceImageHeader: true,
963964
hiddenInBrowser: true,
@@ -1602,6 +1603,11 @@ export const INFINITE_HD6: EmulatorDiskDef = {
16021603
generatedSpec: () => import("./Data/Infinite HD6.dsk.json"),
16031604
};
16041605

1606+
export const INFINITE_HDX: EmulatorDiskDef = {
1607+
prefetchChunks: [0, 17, 7999],
1608+
generatedSpec: () => import("./Data/Infinite HDX.dsk.json"),
1609+
};
1610+
16051611
export const INFINITE_HD_MFS: EmulatorDiskDef = {
16061612
prefetchChunks: [0, 1, 2],
16071613
generatedSpec: () => import("./Data/Infinite HD (MFS).dsk.json"),

0 commit comments

Comments
 (0)