UNPKG

7.52 kBJavaScriptView Raw
1"use strict";
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6exports.Mounter = void 0;
7var _nodeChild_process = require("node:child_process");
8var _plistDom = require("@shockpkg/plist-dom");
9/**
10 * Mounter object.
11 */
12class Mounter {
13 /**
14 * The path to hdiutil.
15 */
16
17 /**
18 * Mounter constructor.
19 *
20 * @param options Options object.
21 */
22 constructor(options = null) {
23 this.hdiutil = (options ? options.hdiutil : null) || 'hdiutil';
24 }
25
26 /**
27 * Attach a disk image.
28 *
29 * @param file Path to disk image.
30 * @param options Options object.
31 * @returns Info object.
32 */
33 async attach(file, options = null) {
34 const devices = await this._runAttach(this._argsAttach(file, options));
35 const {
36 eject,
37 ejectSync
38 } = this._createEjects(devices);
39 return {
40 devices,
41 eject,
42 ejectSync
43 };
44 }
45
46 /**
47 * Attach a disk image.
48 *
49 * @param file Path to disk image.
50 * @param options Options object.
51 * @returns Info object.
52 */
53 attachSync(file, options = null) {
54 // eslint-disable-next-line no-sync
55 const devices = this._runAttachSync(this._argsAttach(file, options));
56 const {
57 eject,
58 ejectSync
59 } = this._createEjects(devices);
60 return {
61 devices,
62 eject,
63 ejectSync
64 };
65 }
66
67 /**
68 * Eject a disk image.
69 *
70 * @param file Path to device file or volume mount point.
71 * @param options Options object.
72 */
73 async eject(file, options = null) {
74 await this._runEject(this._argsEject(file, options));
75 }
76
77 /**
78 * Eject a disk image.
79 *
80 * @param file Path to device file or volume mount point.
81 * @param options Options object.
82 */
83 ejectSync(file, options = null) {
84 // eslint-disable-next-line no-sync
85 this._runEjectSync(this._argsEject(file, options));
86 }
87
88 /**
89 * Create args for attach.
90 *
91 * @param file Path to disk image.
92 * @param options Options object.
93 * @returns Argument list.
94 */
95 _argsAttach(file, options = null) {
96 const args = ['attach', '-plist'];
97 if (options) {
98 if (options.readonly) {
99 args.push('-readonly');
100 }
101 if (options.nobrowse) {
102 args.push('-nobrowse');
103 }
104 }
105 args.push(this._fileArg(file));
106 return args;
107 }
108
109 /**
110 * Create args for eject.
111 *
112 * @param file Path to device file or volume mount point.
113 * @param options Options object.
114 * @returns Argument list.
115 */
116 _argsEject(file, options = null) {
117 const args = ['eject'];
118 if (options && options.force) {
119 args.push('-force');
120 }
121 args.push(this._fileArg(file));
122 return args;
123 }
124
125 /**
126 * Run hdiutil attach command, returning the devices list on success.
127 *
128 * @param args CLI args.
129 * @returns Devices list.
130 */
131 async _runAttach(args) {
132 const stdouts = [];
133 const proc = (0, _nodeChild_process.spawn)(this.hdiutil, args);
134 proc.stdout.on('data', data => {
135 stdouts.push(data);
136 });
137 const code = await new Promise((resolve, reject) => {
138 proc.once('exit', resolve);
139 proc.once('error', reject);
140 });
141 if (code) {
142 throw new Error(`Attach failed: hdiutil exit code: ${code}`);
143 }
144 return this._parseDevices(Buffer.concat(stdouts).toString());
145 }
146
147 /**
148 * Run hdiutil attach command, returning the devices list on success.
149 *
150 * @param args CLI args.
151 * @returns Devices list.
152 */
153 _runAttachSync(args) {
154 const {
155 status,
156 error,
157 stdout
158 } = (0, _nodeChild_process.spawnSync)(this.hdiutil, args);
159 if (error) {
160 throw error;
161 }
162 if (status) {
163 throw new Error(`Attach failed: hdiutil exit code: ${status}`);
164 }
165 return this._parseDevices(stdout.toString());
166 }
167
168 /**
169 * Run hdiutil eject command.
170 *
171 * @param args CLI args.
172 */
173 async _runEject(args) {
174 const proc = (0, _nodeChild_process.spawn)(this.hdiutil, args);
175 const status = await new Promise((resolve, reject) => {
176 proc.once('exit', resolve);
177 proc.once('error', reject);
178 });
179 if (status) {
180 throw new Error(`Eject failed: hdiutil exit code: ${status}`);
181 }
182 }
183
184 /**
185 * Run hdiutil eject command.
186 *
187 * @param args CLI args.
188 */
189 _runEjectSync(args) {
190 const {
191 status,
192 error
193 } = (0, _nodeChild_process.spawnSync)(this.hdiutil, args);
194 if (error) {
195 throw error;
196 }
197 if (status) {
198 throw new Error(`Eject failed: hdiutil exit code: ${status}`);
199 }
200 }
201
202 /**
203 * Create file argument from file path.
204 *
205 * @param file File path.
206 * @returns A path for use as argument.
207 */
208 _fileArg(file) {
209 // Make sure it will not be recognized as option argument.
210 return file.startsWith('-') ? `./${file}` : file;
211 }
212
213 /**
214 * Parse devices plist into devices list.
215 *
216 * @param xml XML plist.
217 * @returns Devices list.
218 */
219 _parseDevices(xml) {
220 const plist = new _plistDom.Plist();
221 plist.fromXml(xml);
222 const systemEntities = plist.valueCastAs(_plistDom.ValueDict).getValue('system-entities').castAs(_plistDom.ValueArray);
223 const r = [];
224 for (const value of systemEntities.value) {
225 const dict = value.castAs(_plistDom.ValueDict);
226 const devEntry = dict.getValue('dev-entry').castAs(_plistDom.ValueString).value;
227 const potentiallyMountable = dict.getValue('potentially-mountable').castAs(_plistDom.ValueBoolean).value;
228 const contentHint = dict.get('content-hint');
229 const unmappedContentHint = dict.get('unmapped-content-hint');
230 const volumeKind = dict.get('volume-kind');
231 const mountPoint = dict.get('mount-point');
232 const device = {
233 devEntry,
234 potentiallyMountable
235 };
236 if (contentHint) {
237 device.contentHint = contentHint.castAs(_plistDom.ValueString).value;
238 }
239 if (unmappedContentHint) {
240 device.unmappedContentHint = unmappedContentHint.castAs(_plistDom.ValueString).value;
241 }
242 if (volumeKind) {
243 device.volumeKind = volumeKind.castAs(_plistDom.ValueString).value;
244 }
245 if (mountPoint) {
246 device.mountPoint = mountPoint.castAs(_plistDom.ValueString).value;
247 }
248 r.push(device);
249 }
250 return r;
251 }
252
253 /**
254 * Find the root device, null on empty list.
255 *
256 * @param devices Device list.
257 * @returns Root device or null if an empty list.
258 */
259 _findRootDevice(devices) {
260 let r = null;
261 for (const device of devices) {
262 if (r === null || r.devEntry.length > device.devEntry.length) {
263 r = device;
264 }
265 }
266 return r;
267 }
268
269 /**
270 * Create ejects callback from a list of devices.
271 *
272 * @param devices Device list.
273 * @returns Callback function.
274 */
275 _createEjects(devices) {
276 // Find the root device, to use to eject (none possible in theory).
277 let devEntry = this._findRootDevice(devices)?.devEntry;
278 return {
279 /**
280 * The eject callback function.
281 *
282 * @param options Eject options.
283 */
284 eject: async (options = null) => {
285 if (devEntry) {
286 await this.eject(devEntry, options);
287 devEntry = '';
288 }
289 },
290 /**
291 * The eject callback function.
292 *
293 * @param options Eject options.
294 */
295 ejectSync: (options = null) => {
296 if (devEntry) {
297 // eslint-disable-next-line no-sync
298 this.ejectSync(devEntry, options);
299 devEntry = '';
300 }
301 }
302 };
303 }
304}
305exports.Mounter = Mounter;
306//# sourceMappingURL=mounter.js.map
\No newline at end of file