Skip to content

Instantly share code, notes, and snippets.

@modeco80
Created January 28, 2023 12:22
Show Gist options
  • Save modeco80/2e436b4710ad86bba4c590068c159a40 to your computer and use it in GitHub Desktop.
Save modeco80/2e436b4710ad86bba4c590068c159a40 to your computer and use it in GitHub Desktop.
A extractor for QNX setup files, possibly useful for other InstallShield Java installers.
// A extractor for QNX setup files, possibly useful for other InstallShield Java installers.
// Tested with 6.3.0 setup JARs.
//
// (C) 2023 modeco80 <lily.modeco80@protonmail.ch>, under the MIT license.
//
// Usage: `node qnx-extract.js` in the setup tree (extract the setup .jar, then find the `index` file. That's where you should run it)
// Files will be written into a "extracted_output" directory.
//
// This does not need any additional node packages :)
const fs = require('node:fs');
const zlib = require('node:zlib');
const path = require('node:path');
const { Buffer } = require('node:buffer');
const SeekDir = {
BEG : 0,
CUR : 1,
END : 2
};
class BufferStream {
#bufferImpl = null;
#readPointer = 0;
constructor(buffer) {
this.#bufferImpl = buffer;
}
seek(where, whence) {
switch(whence) {
case SeekDir.BEG:
this.#readPointer = where;
break;
case SeekDir.CUR:
this.#readPointer += where;
break;
case SeekDir.END:
if(where > 0)
throw new Error("Cannot use SeekDir.END with where greater than 0");
this.#readPointer = this.#bufferImpl.length + whence;
break;
default:
throw new Error(`Unknown seek mode ${whence}`);
break;
}
return this.#readPointer;
}
tell() { return seek(0, SeekDir.CUR); }
// common impl function for read*()
#readImpl(func, size) {
let res = func.call(this.#bufferImpl, this.#readPointer);
this.#readPointer += size;
return res;
}
// this returns a sub-Buffer which does not deep-copy,
// but increments the read pointer.
subBuffer(len) {
let buffer = this.#bufferImpl.subarray(this.#readPointer, this.#readPointer + len);
this.#readPointer += len;
return new BufferStream(buffer);
}
readS8() { return this.#readImpl(Buffer.prototype.readInt8, 1); }
readU8() { return this.#readImpl(Buffer.prototype.readUInt8, 1); }
readS16LE() { return this.#readImpl(Buffer.prototype.readInt16LE, 2); }
readU16LE() { return this.#readImpl(Buffer.prototype.readUInt16LE, 2); }
readS16BE() { return this.#readImpl(Buffer.prototype.readInt16BE, 2); }
readU16BE() { return this.#readImpl(Buffer.prototype.readUInt16BE, 2); }
readS32LE() { return this.#readImpl(Buffer.prototype.readInt32LE, 4); }
readU32LE() { return this.#readImpl(Buffer.prototype.readUInt32LE, 4); }
readS32BE() { return this.#readImpl(Buffer.prototype.readInt32BE, 4); }
readU32BE() { return this.#readImpl(Buffer.prototype.readUInt32BE, 4); }
readS64LE() { return this.#readImpl(Buffer.prototype.readBigInt64LE, 8); }
readU64LE() { return this.#readImpl(Buffer.prototype.readBigUInt64LE, 8); }
readS64BE() { return this.#readImpl(Buffer.prototype.readBigInt64BE, 8); }
readU64BE() { return this.#readImpl(Buffer.prototype.readBigUInt64BE, 8); }
readJString() {
let len = this.readU16BE();
let str = "";
for(let i = 0; i < len; ++i)
str += String.fromCharCode(this.readU8());
return str;
}
readJByteArray() {
return this.subBuffer(this.readU32BE());
}
raw() {
return this.#bufferImpl;
}
}
function inflateDecompress(buffer) {
return new Promise((res, rej) => {
zlib.inflate(buffer.raw(), (err, decompressed) => {
if(err)
rej(err);
res(new BufferStream(decompressed));
})
});
}
function buf2hex(buffer) {
return [...new Uint8Array(buffer.raw())]
.map(x => x.toString(16).padStart(2, '0'))
.join('');
}
// THE FUN STUFF STARTS HERE!
const FileAttributeFlags = {
DIRECTORY : 0x1000 // this is genuinely the only one this extractor cares abotu
};
class FileAttributes {
flags = 0;
extendedData = null;
constructor(buffer) {
this.#fillOut(buffer);
}
#fillOut(buffer) {
class ExtendedData {
key = 0;
data = [];
constructor(buffer) {
this.#fillOut(buffer);
}
#fillOut(buffer) {
this.key = buffer.readU16BE();
this.data = buffer.readJByteArray();
}
};
this.flags = buffer.readU32BE();
let extendedLength = buffer.readU32BE();
if(extendedLength != 0) {
let extendedArr = [];
for(let i = 0; i < extendedLength; ++i)
arr.push(new ExtendedData(buffer));
}
}
}
class IndexPacket {
entryNumber = 0;
md5Digest = null;
name = "";
type = 0;
size = 0;
startMediaNumber = 0;
endMediaNumber = 0;
duplicateResource = 0;
hasAttribs = 0;
attribs = null;
lastModified = 0;
resourceType = 0;
source = "";
extra = [];
constructor(buffer) {
this.#fillOut(buffer);
}
#fillOut(buffer) {
this.entryNumber = buffer.readU32BE();
this.md5Digest = buf2hex(buffer.subBuffer(16));
this.name = buffer.readJString();
this.type = buffer.readU32BE();
this.size = buffer.readU64BE();
this.startMediaNumber = buffer.readU32BE();
this.endMediaNumber = buffer.readU32BE();
this.duplicateResource = buffer.readU8();
this.hasAttribs = buffer.readU8();
if(this.hasAttribs)
this.attribs = new FileAttributes(buffer);
this.lastModified = buffer.readU64BE();
this.resourceType = buffer.readU8();
this.source = buffer.readJString();
this.extra = buffer.readJByteArray();
}
}
class IndexFile {
files = []; // list of deserialized files
async fillOut(buffer) {
let packetChunks = [];
let compressedChunkData = []
// header of the index file
let packetSize = buffer.readU32BE();
let packetChunkCount = buffer.readU32BE();
// Read all the compressed packet chunks,
// decompress them, and then deserialize them
for(let i = 0; i < packetChunkCount; ++i) {
let chunkSize = buffer.readU32BE();
let decompressed = await inflateDecompress(buffer.subBuffer(chunkSize));
let chunkPacketSize = decompressed.readU32BE();
for(var j = 0; j < chunkPacketSize; ++j)
this.files.push(new IndexPacket(decompressed));
}
}
}
const OUTPUT_DIRECTORY = "extracted_output/";
(async () => {
var index = new IndexFile();
// Read, decompress & deserialize the index file.
await index.fillOut(new BufferStream(fs.readFileSync('./index')));
// After we deserialize the index file, this becomes a whole lot easier.
// We just have to decompress the md5 file (it's just straight Zlib) then write it out to disk.
for(var i = 0; i < index.files.length; ++i) {
if(!(index.files[i].attribs.flags & FileAttributeFlags.DIRECTORY)) {
let inPath = path.join(process.cwd(), 'md5/', index.files[i].md5Digest);
let outPath = path.join(process.cwd(), OUTPUT_DIRECTORY, index.files[i].name);
console.log(`Extracting ${index.files[i].name}`)
if(index.files[i].size == 0) {
fs.writeFileSync(outPath, "");
} else {
// this is a mouthful :(
fs.writeFileSync(outPath, (await inflateDecompress(new BufferStream(fs.readFileSync(inPath)))).raw(), { encoding: '' });
}
} else {
//console.log(`making directory "${index.files[i].name}"`);
let outPath = path.join(process.cwd(), OUTPUT_DIRECTORY, index.files[i].name);
fs.mkdirSync(outPath, { recursive: true });
}
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment