asarmor
Version:
Protect asar archive files from extraction
141 lines (140 loc) • 5.51 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.close = exports.open = void 0;
const promises_1 = __importDefault(require("fs/promises"));
const fs_1 = __importDefault(require("fs"));
const _1 = require(".");
// eslint-disable-next-line @typescript-eslint/no-var-requires
const pickle = require('chromium-pickle-js');
const headerSizeOffset = 8;
async function readHeaderSize(handle) {
const sizeBuffer = Buffer.alloc(headerSizeOffset);
const { bytesRead } = await handle.read(sizeBuffer, 0, headerSizeOffset, null);
if (bytesRead !== headerSizeOffset)
throw new Error('Unable to read header size!');
const sizePickle = pickle.createFromBuffer(sizeBuffer);
return sizePickle.createIterator().readUInt32();
}
async function readArchive(filePath) {
const handle = await promises_1.default.open(filePath, 'r');
// Read header size
const headerSize = await readHeaderSize(handle);
// Read header
const headerBuffer = Buffer.alloc(headerSize);
const { bytesRead } = await handle.read(headerBuffer, 0, headerSize, null);
await handle.close();
if (bytesRead !== headerSize)
throw new Error('Unable to read header!');
const headerPickle = pickle.createFromBuffer(headerBuffer);
const header = JSON.parse(headerPickle.createIterator().readString());
// Returning archive object
return {
headerSize,
header,
};
}
/**
* Read and parse an asar archive.
*
* This can take a while depending on the size of the file.
*/
async function read(filePath) {
const { size: fileSize } = await promises_1.default.stat(filePath);
if (fileSize > 2147483648)
console.warn('Warning: archive is larger than 2GB. This might take a while.');
const archive = await readArchive(filePath);
return archive;
}
class Asarmor {
constructor(archivePath, archive) {
this.filePath = archivePath;
this.archive = archive;
}
/**
* Apply a patch to the asar archive.
*/
patch(patch) {
if (!patch) {
this.patch((0, _1.createBloatPatch)());
return this.archive;
}
if (patch.header)
this.archive.header.files = {
...patch.header.files,
...this.archive.header.files,
};
if (!patch.headerSize && patch.header) {
this.archive.headerSize = Buffer.from(JSON.stringify(this.archive.header)).length;
}
else if (patch.headerSize) {
this.archive.headerSize = patch.headerSize;
}
return this.archive;
}
/**
* Write modified asar archive to given absolute file path.
*/
async write(outputPath) {
// Convert header back to string
const headerPickle = pickle.createEmpty();
headerPickle.writeString(JSON.stringify(this.archive.header));
// Read new header size
const headerBuffer = headerPickle.toBuffer();
const sizePickle = pickle.createEmpty();
sizePickle.writeUInt32(headerBuffer.length);
const sizeBuffer = sizePickle.toBuffer();
// Write everything to output file :D
const tmp = outputPath + '.tmp'; // create temp file bcs we can't read & write the same file at the same time
const writeStream = fs_1.default.createWriteStream(tmp, { flags: 'w' });
writeStream.write(sizeBuffer);
writeStream.write(headerBuffer);
// write unmodified contents
const fd = await promises_1.default.open(this.filePath, 'r');
const originalHeaderSize = await readHeaderSize(fd);
await fd.close();
const readStream = fs_1.default.createReadStream(this.filePath, {
start: headerSizeOffset + originalHeaderSize,
});
readStream.pipe(writeStream);
readStream.on('close', () => readStream.unpipe());
return new Promise((resolve, reject) => {
writeStream.on('close', () => {
fs_1.default.renameSync(tmp, outputPath);
resolve(outputPath);
});
writeStream.on('error', reject);
});
}
async createBackup(options) {
const backupPath = (options === null || options === void 0 ? void 0 : options.backupPath) || this.filePath + '.bak';
if (!fs_1.default.existsSync(backupPath) || (options === null || options === void 0 ? void 0 : options.overwrite))
await promises_1.default.copyFile(this.filePath, backupPath);
}
async restoreBackup(options) {
const backupPath = (options === null || options === void 0 ? void 0 : options.backupPath) || this.filePath + '.bak';
if (fs_1.default.existsSync(backupPath)) {
await promises_1.default.copyFile(backupPath, this.filePath);
if (options === null || options === void 0 ? void 0 : options.remove)
await promises_1.default.unlink(backupPath);
}
}
}
exports.default = Asarmor;
/**
* Open and prepare asar archive file for modifications.
*/
async function open(asarFilePath) {
const archive = await read(asarFilePath);
return new Asarmor(asarFilePath, archive);
}
exports.open = open;
/**
* Alias of `asarmor.write()`.
*/
async function close(asarmor, outputfilePath) {
return asarmor.write(outputfilePath);
}
exports.close = close;