UNPKG

@controlplane/cli

Version:

Control Plane Corporation CLI

139 lines 5.12 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.parseSha256Sums = parseSha256Sums; exports.verifyChecksum = verifyChecksum; exports.extractMembers = extractMembers; const crypto = require("crypto"); const path = require("path"); const zlib = require("zlib"); const tar = require("tar-stream"); const AdmZip = require("adm-zip"); // SECTION - Functions /** * Parses a SHA256SUMS file into a map of file name to lowercase hex digest. * * Each line has the form `<hex> <filename>`; a leading `*` on the name (binary * mode marker) is stripped. * * @param {string} contents - The raw SHA256SUMS file contents. * @returns {Map<string, string>} A map from file name to its expected digest. */ function parseSha256Sums(contents) { const digests = new Map(); for (const rawLine of contents.split('\n')) { const line = rawLine.trim(); if (line === '') { continue; } // Split on the first run of whitespace separating the digest from the name const match = line.match(/^([0-9a-fA-F]{64})\s+\*?(.+)$/); if (!match) { continue; } const digest = match[1].toLowerCase(); const fileName = path.basename(match[2].trim()); digests.set(fileName, digest); } return digests; } /** * Verifies that a buffer's SHA-256 digest matches the expected value for a file. * * @param {Buffer} data - The downloaded archive bytes. * @param {string} fileName - The archive file name to look up in the checksums. * @param {Map<string, string>} sums - The parsed SHA256SUMS map. * @returns {void} * @throws {Error} When the checksum is missing or does not match. */ function verifyChecksum(data, fileName, sums) { const expected = sums.get(fileName); if (!expected) { throw new Error(`No checksum published for ${fileName}; refusing to install an unverified binary.`); } const actual = crypto.createHash('sha256').update(data).digest('hex'); if (actual !== expected) { throw new Error(`Checksum mismatch for ${fileName}: expected ${expected}, got ${actual}.`); } } /** * Extracts several members' bytes from a release archive, decompressing it once. * * Members are matched by base name only; archive-supplied paths never influence * where anything is written. * * @param {Buffer} archive - The raw archive bytes. * @param {BinaryArtifact} artifact - The artifact describing the archive format. * @param {string[]} names - The member base names to extract. * @returns {Promise<Map<string, Buffer>>} A map from member name to its contents (only those found). */ async function extractMembers(archive, artifact, names) { const wanted = new Set(names); if (artifact.format === 'zip') { return extractMembersFromZip(archive, wanted); } return extractMembersFromTgz(archive, wanted); } /** * Extracts the requested members from a zip archive. * * @param {Buffer} archive - The raw zip bytes. * @param {Set<string>} wanted - The member base names to collect. * @returns {Map<string, Buffer>} The collected members. */ function extractMembersFromZip(archive, wanted) { const zip = new AdmZip(archive); const found = new Map(); for (const entry of zip.getEntries()) { const name = path.basename(entry.entryName); // Only regular files can carry executable bytes if (!entry.isDirectory && wanted.has(name)) { found.set(name, entry.getData()); } } return found; } /** * Extracts the requested members from a gzip-compressed tar archive in one pass. * * @param {Buffer} archive - The raw .tgz bytes. * @param {Set<string>} wanted - The member base names to collect. * @returns {Promise<Map<string, Buffer>>} The collected members. */ function extractMembersFromTgz(archive, wanted) { return new Promise((resolve, reject) => { const extract = tar.extract(); const found = new Map(); extract.on('entry', (header, stream, next) => { // Archives store these members at the root, so only the base name matters. // Only regular files count: a directory or symlink of the same name has no bytes. const name = path.basename(header.name); const isWanted = wanted.has(name) && header.type === 'file'; const chunks = []; stream.on('data', (chunk) => { if (isWanted) { chunks.push(chunk); } }); stream.on('end', () => { if (isWanted) { found.set(name, Buffer.concat(chunks)); } next(); }); stream.resume(); }); extract.on('finish', () => { resolve(found); }); extract.on('error', reject); zlib.gunzip(archive, (error, unpacked) => { if (error) { reject(error); return; } extract.end(unpacked); }); }); } // !SECTION //# sourceMappingURL=binary-archive.js.map