UNPKG

@controlplane/cli

Version:

Control Plane Corporation CLI

199 lines 7.93 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.realDmgOps = void 0; exports.extractMembersFromDmg = extractMembersFromDmg; const fs = require("fs"); const os = require("os"); const path = require("path"); const child_process_1 = require("child_process"); // ANCHOR - Constants /** Time budget for a single hdiutil invocation, so a wedged mount cannot hang the CLI. */ const HDIUTIL_TIMEOUT_MS = 2 * 60 * 1000; /** The macOS tool that mounts and unmounts disk images. */ const HDIUTIL = 'hdiutil'; /** The prefix of the temporary directory holding the image and its mount point. */ const WORK_DIR_PREFIX = 'cpln-upgrade-'; /** The file name the downloaded image is staged under. */ const IMAGE_FILE_NAME = 'release.dmg'; /** The directory the image is mounted at, inside the working directory. */ const MOUNT_DIR_NAME = 'mnt'; // SECTION - Functions /** * Extracts executables from a macOS disk image by mounting it read-only, copying * the requested members out, and unmounting it again. * * The image is mounted at a private mount point rather than at its volume name, so * an identically named volume that is already mounted can never be read by mistake. * * @param {Buffer} archive - The raw .dmg bytes. * @param {Set<string>} wanted - The member base names to collect. * @param {DmgOps} ops - The filesystem and process operations to use. * @returns {Map<string, Buffer>} The collected members (only those found). * @throws {Error} When the image cannot be mounted or cannot be unmounted afterwards. */ function extractMembersFromDmg(archive, wanted, ops) { const workDir = ops.makeTempDir(WORK_DIR_PREFIX); const imagePath = path.join(workDir, IMAGE_FILE_NAME); const mountPoint = path.join(workDir, MOUNT_DIR_NAME); try { // hdiutil reads the image from a path, so the downloaded bytes are staged on disk ops.writeFile(imagePath, archive); ops.makeDir(mountPoint); attachImage(imagePath, mountPoint, ops); let found; try { found = readMountedMembers(mountPoint, wanted, ops); } finally { // The image must be detached before the working directory can be removed detachImage(mountPoint, ops); } return found; } finally { tryRemoveDir(workDir, ops); } } /** * Mounts a disk image read-only at a private mount point. * * @param {string} imagePath - The staged .dmg file. * @param {string} mountPoint - The directory to mount the image at. * @param {DmgOps} ops - The operations to use. * @returns {void} * @throws {Error} When hdiutil cannot be run or the image cannot be mounted. */ function attachImage(imagePath, mountPoint, ops) { // The image's own checksum is redundant: these bytes were already verified against SHA256SUMS const args = ['attach', '-nobrowse', '-readonly', '-noverify', '-mountpoint', mountPoint, imagePath]; runHdiutil(args, 'mount the downloaded disk image', ops); } /** * Runs hdiutil, turning any failure into an error naming the attempted action. * * @param {string[]} args - The hdiutil arguments. * @param {string} action - The action being attempted, used in the error message. * @param {DmgOps} ops - The operations to use. * @returns {void} * @throws {Error} When hdiutil cannot be run or exits non-zero. */ function runHdiutil(args, action, ops) { const result = ops.run(HDIUTIL, args); if (result.error) { throw new Error(`Unable to ${action}: could not run ${HDIUTIL}: ${result.error.message}`); } if (result.status !== 0) { // A null status means the process was killed rather than exiting on its own const outcome = result.status === null ? 'did not complete' : `exited with status ${result.status}`; const detail = result.stderr.trim(); throw new Error(`Unable to ${action}: ${HDIUTIL} ${outcome}${detail === '' ? '' : ` (${detail})`}.`); } } /** * Reads the requested members from the root of a mounted volume. * * @param {string} mountPoint - The directory the image is mounted at. * @param {Set<string>} wanted - The member base names to collect. * @param {DmgOps} ops - The operations to use. * @returns {Map<string, Buffer>} The collected members. */ function readMountedMembers(mountPoint, wanted, ops) { const found = new Map(); for (const name of wanted) { // The image stores these members at the volume root, so only the base name matters const base = path.basename(name); const filePath = path.join(mountPoint, base); // Only a regular file carries executable bytes: a directory or a symlink of the // same name must never satisfy a member if (ops.isFile(filePath)) { found.set(base, readMember(filePath, base, ops)); } } return found; } /** * Reads one member off the mounted volume, naming it when the read fails. * * @param {string} filePath - The member's path on the mounted volume. * @param {string} name - The member's base name, used in the error message. * @param {DmgOps} ops - The operations to use. * @returns {Buffer} The member's contents. * @throws {Error} When the member cannot be read. */ function readMember(filePath, name, ops) { try { return ops.readFile(filePath); } catch (error) { // A failure mid-read carries no path of its own, so it has to be named here throw new Error(`Unable to read '${name}' from the mounted disk image at ${filePath}: ${describeError(error)}`); } } /** * Unmounts a disk image, reporting how to release it by hand if that fails. * * @param {string} mountPoint - The directory the image is mounted at. * @param {DmgOps} ops - The operations to use. * @returns {void} * @throws {Error} When the image cannot be unmounted. */ function detachImage(mountPoint, ops) { try { runHdiutil(['detach', '-force', mountPoint], 'unmount the downloaded disk image', ops); } catch (error) { throw new Error(`${describeError(error)} The image is still mounted; run '${HDIUTIL} detach -force ${mountPoint}' to release it.`); } } /** * Renders an unknown thrown value as a message safe to embed in another error. * * @param {unknown} error - The caught value. * @returns {string} The error's message, or a placeholder for a non-Error value. */ function describeError(error) { return error instanceof Error ? error.message : 'Unknown error'; } /** * Removes a directory tree, ignoring any error. * * @param {string} directory - The directory to remove. * @param {DmgOps} ops - The operations to use. * @returns {void} */ function tryRemoveDir(directory, ops) { try { ops.removeDir(directory); } catch (_a) { // Ignore — cleanup is best-effort } } /** * The default operations, backed by the real fs module and hdiutil. */ exports.realDmgOps = { isFile: (filePath) => { try { // lstat, not stat: a symlink must be rejected rather than followed return fs.lstatSync(filePath).isFile(); } catch (_a) { return false; } }, makeTempDir: (prefix) => fs.mkdtempSync(path.join(os.tmpdir(), prefix)), makeDir: (directory) => { fs.mkdirSync(directory, { recursive: true }); }, readFile: (filePath) => fs.readFileSync(filePath), writeFile: (filePath, data) => fs.writeFileSync(filePath, data), removeDir: (directory) => fs.rmSync(directory, { recursive: true, force: true }), run: (program, args) => { var _a; const result = (0, child_process_1.spawnSync)(program, args, { encoding: 'utf-8', timeout: HDIUTIL_TIMEOUT_MS }); return { status: result.status, stderr: (_a = result.stderr) !== null && _a !== void 0 ? _a : '', error: result.error }; }, }; // !SECTION //# sourceMappingURL=dmg-archive.js.map