@controlplane/cli
Version:
Control Plane Corporation CLI
139 lines • 5.12 kB
JavaScript
;
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