Created
January 28, 2023 12:22
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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