Skip to content

Commit 41c6023

Browse files
committed
Special-case dragging in of .zip files created by modern macOS
They contain resource forks and Finder metadata, thus are a good way of doing a full-fidelity import. Fixes #255
1 parent 495a7bf commit 41c6023

3 files changed

Lines changed: 150 additions & 3 deletions

File tree

‎src/emulator/emulator-finder.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,19 @@ function getDebugFinderFlags(flags: number): string[] {
8787
}
8888
return result;
8989
}
90+
91+
/**
92+
* Given a serialized FInfo struct, checks to see if the location data is
93+
* present. If it's not, it clears the "has been inited" flag so that the Finder
94+
* assigns a position to the item when it loads it.
95+
*/
96+
export function fixArchiveFinderInfo(buffer: ArrayBuffer) {
97+
const fInfoView = new DataView(buffer);
98+
const x = fInfoView.getInt16(FInfoFields.fdLocation);
99+
const y = fInfoView.getInt16(FInfoFields.fdLocation + 2);
100+
if (x === 0 && y === 0) {
101+
let flags = fInfoView.getInt16(FInfoFields.fdFlags);
102+
flags &= ~FinderFlags.kHasBeenInited;
103+
fInfoView.setInt16(FInfoFields.fdFlags, flags);
104+
}
105+
}

‎src/emulator/emulator-ui-extractor.ts‎

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,18 @@ import {
66
type EmulatorWorkerDirectorExtractionEntry,
77
} from "./emulator-common";
88
import * as varz from "../varz";
9+
import {fixArchiveFinderInfo} from "./emulator-finder";
910

10-
export async function uploadsFromDirectoryExtractionFile(
11+
export async function uploadsFromFile(
12+
file: File
13+
): Promise<EmulatorFileUpload[] | undefined> {
14+
return (
15+
(await uploadsFromDirectoryExtractionFile(file)) ??
16+
(await uploadsFromMacOSArchive(file))
17+
);
18+
}
19+
20+
async function uploadsFromDirectoryExtractionFile(
1121
file: File
1222
): Promise<EmulatorFileUpload[] | undefined> {
1323
if (!file.name.endsWith(".zip")) {
@@ -41,6 +51,127 @@ export async function uploadsFromDirectoryExtractionFile(
4151
});
4252
}
4353

54+
/**
55+
* Special-cases .zips created by modern macOS, which persist extra information
56+
* (resource forks and Finder info metadata) in a parallel __MACOSX directory
57+
* structure using AppleDouble-encoded files.
58+
*/
59+
async function uploadsFromMacOSArchive(
60+
file: File
61+
): Promise<EmulatorFileUpload[] | undefined> {
62+
if (!file.name.endsWith(".zip")) {
63+
return undefined;
64+
}
65+
const zip = await JSZip.loadAsync(file);
66+
const appleDoubleFiles = Object.values(zip.files).filter(
67+
f => !f.dir && f.name.startsWith("__MACOSX")
68+
);
69+
// Not a macOS-created archive.
70+
if (!appleDoubleFiles.length) {
71+
return undefined;
72+
}
73+
const parentName = file.name.slice(0, -4);
74+
// Directories are implicitly created
75+
const files = Object.values(zip.files).filter(
76+
f => !f.dir && !f.name.startsWith("__MACOSX")
77+
);
78+
const prefix = files.every(f => f.name.startsWith(parentName))
79+
? ""
80+
: parentName + "/";
81+
82+
const fileBlobs = await Promise.all(files.map(f => f.async("blob")));
83+
const uploads = files.map((f, i) => {
84+
const blob = fileBlobs[i];
85+
const url = URL.createObjectURL(blob);
86+
return {
87+
name: prefix + f.name,
88+
url,
89+
size: blob.size,
90+
};
91+
});
92+
93+
const appleDoubleBuffers = await Promise.all(
94+
appleDoubleFiles.map(f => f.async("arraybuffer"))
95+
);
96+
for (let i = 0; i < appleDoubleFiles.length; i++) {
97+
const file = appleDoubleFiles[i];
98+
const buffer = appleDoubleBuffers[i];
99+
100+
const path = file.name.split("/");
101+
path.shift(); // Drop the __MACOSX directory
102+
let name = path.pop()!;
103+
if (name.startsWith("._")) {
104+
name = name.substring(2);
105+
}
106+
const {resourceFork, metadata} = parseAppleDouble(buffer, name);
107+
if (resourceFork) {
108+
const url = URL.createObjectURL(resourceFork);
109+
uploads.push({
110+
name: prefix + [...path, ".rsrc", name].join("/"),
111+
url,
112+
size: resourceFork.size,
113+
});
114+
}
115+
if (metadata) {
116+
const url = URL.createObjectURL(metadata);
117+
uploads.push({
118+
name: prefix + [...path, ".finf", name].join("/"),
119+
url,
120+
size: metadata.size,
121+
});
122+
}
123+
}
124+
return uploads;
125+
}
126+
127+
function parseAppleDouble(
128+
buffer: ArrayBuffer,
129+
name: string
130+
): {
131+
resourceFork?: Blob;
132+
metadata?: Blob;
133+
} {
134+
const data = new DataView(buffer);
135+
const magic = data.getUint32(0);
136+
if (magic !== 0x00051607) {
137+
throw new Error(`Invalid AppleDouble magic ${magic.toString(16)}`);
138+
}
139+
const version = data.getUint32(4);
140+
if (version !== 0x00020000) {
141+
throw new Error(`Invalid AppleDouble version ${version.toString(16)}`);
142+
}
143+
144+
const entryCount = data.getUint16(24);
145+
let cursor = 26;
146+
let resourceFork;
147+
let metadata;
148+
for (let i = 0; i < entryCount; i++) {
149+
const entryID = data.getUint32(cursor);
150+
const offset = data.getUint32(cursor + 4);
151+
const length = data.getUint32(cursor + 8);
152+
switch (entryID) {
153+
case 2:
154+
resourceFork = new Blob([
155+
buffer.slice(offset, offset + length),
156+
]);
157+
break;
158+
case 9: {
159+
const metadataBuffer = buffer.slice(offset, offset + length);
160+
fixArchiveFinderInfo(metadataBuffer);
161+
metadata = new Blob([metadataBuffer]);
162+
break;
163+
}
164+
default:
165+
console.log(
166+
`Ignoring unexpected entry ID ${entryID} found in ${name}`
167+
);
168+
}
169+
cursor += 12;
170+
}
171+
172+
return {resourceFork, metadata};
173+
}
174+
44175
export async function handleDirectoryExtraction(
45176
extraction: EmulatorWorkerDirectorExtraction
46177
) {

‎src/emulator/emulator-ui.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import {
4141
} from "./emulator-ui-files";
4242
import {
4343
handleDirectoryExtraction,
44-
uploadsFromDirectoryExtractionFile,
44+
uploadsFromFile,
4545
} from "./emulator-ui-extractor";
4646
import {
4747
JS_CODE_TO_ADB_KEYCODE,
@@ -436,7 +436,7 @@ export class Emulator {
436436
const remainingNames: string[] = [];
437437
for (let i = 0; i < files.length; i++) {
438438
const file = files[i];
439-
const uploads = await uploadsFromDirectoryExtractionFile(file);
439+
const uploads = await uploadsFromFile(file);
440440
if (uploads) {
441441
this.#files.uploadFiles(uploads);
442442
continue;

0 commit comments

Comments
 (0)