import { unzipSync } from "fflate"; import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from "fs"; import { join, resolve, sep } from "path"; import { extractSignatureBlock } from "../node/sign.js"; import { getLogger } from "../shared/log.js"; export async function unpackExtension({ mcpbPath, outputDir, silent, }) { const logger = getLogger({ silent }); const resolvedMcpbPath = resolve(mcpbPath); if (!existsSync(resolvedMcpbPath)) { logger.error(`ERROR: MCPB file not found: ${mcpbPath}`); return false; } const finalOutputDir = outputDir ? resolve(outputDir) : process.cwd(); if (!existsSync(finalOutputDir)) { mkdirSync(finalOutputDir, { recursive: true }); } try { const fileContent = readFileSync(resolvedMcpbPath); const { originalContent } = extractSignatureBlock(fileContent); // Parse file attributes from ZIP central directory const fileAttributes = new Map(); const isUnix = process.platform !== "win32"; if (isUnix) { // Parse ZIP central directory to extract file attributes const zipBuffer = originalContent; // Find end of central directory record let eocdOffset = -1; for (let i = zipBuffer.length - 22; i >= 0; i--) { if (zipBuffer.readUInt32LE(i) === 0x06054b50) { eocdOffset = i; break; } } if (eocdOffset !== -1) { const centralDirOffset = zipBuffer.readUInt32LE(eocdOffset + 16); const centralDirEntries = zipBuffer.readUInt16LE(eocdOffset + 8); let offset = centralDirOffset; for (let i = 0; i < centralDirEntries; i++) { if (zipBuffer.readUInt32LE(offset) === 0x02014b50) { const externalAttrs = zipBuffer.readUInt32LE(offset + 38); const filenameLength = zipBuffer.readUInt16LE(offset + 28); const filename = zipBuffer.toString("utf8", offset + 46, offset + 46 + filenameLength); // Extract Unix permissions from external attributes (upper 16 bits) const mode = (externalAttrs >> 16) & 0o777; if (mode > 0) { fileAttributes.set(filename, mode); } const extraFieldLength = zipBuffer.readUInt16LE(offset + 30); const commentLength = zipBuffer.readUInt16LE(offset + 32); offset += 46 + filenameLength + extraFieldLength + commentLength; } else { break; } } } } const decompressed = unzipSync(originalContent); for (const relativePath in decompressed) { if (Object.prototype.hasOwnProperty.call(decompressed, relativePath)) { const data = decompressed[relativePath]; const fullPath = join(finalOutputDir, relativePath); // Prevent zip slip attacks by validating the resolved path const normalizedPath = resolve(fullPath); const normalizedOutputDir = resolve(finalOutputDir); if (!normalizedPath.startsWith(normalizedOutputDir + sep) && normalizedPath !== normalizedOutputDir) { throw new Error(`Path traversal attempt detected: ${relativePath}`); } const dir = join(fullPath, ".."); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } writeFileSync(fullPath, data); // Restore Unix file permissions if available if (isUnix && fileAttributes.has(relativePath)) { try { const mode = fileAttributes.get(relativePath); if (mode !== undefined) { chmodSync(fullPath, mode); } } catch (error) { // Silently ignore permission errors } } } } logger.log(`Extension unpacked successfully to ${finalOutputDir}`); return true; } catch (error) { if (error instanceof Error) { logger.error(`ERROR: Failed to unpack extension: ${error.message}`); } else { logger.error("ERROR: An unknown error occurred during unpacking."); } return false; } }