@@ -6,8 +6,18 @@ import {
66 type EmulatorWorkerDirectorExtractionEntry ,
77} from "./emulator-common" ;
88import * 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+
44175export async function handleDirectoryExtraction (
45176 extraction : EmulatorWorkerDirectorExtraction
46177) {
0 commit comments