mdx-m3-viewer
Version:
A browser WebGL model viewer. Mainly focused on models of the games Warcraft 3 and Starcraft 2.
401 lines • 16.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const pako_1 = require("pako");
const utf8_1 = require("../../common/utf8");
const constants_1 = require("./constants");
const explode_1 = require("./explode");
const isarchive_1 = require("./isarchive");
/**
* A MPQ file.
*/
class MpqFile {
constructor(archive, hash, block, rawBuffer, buffer) {
this.rawBuffer = null;
this.buffer = null;
const headerOffset = archive.headerOffset;
this.archive = archive;
this.c = archive.c;
this.name = `File${`${hash.blockIndex}`.padStart(8, '0')}`;
this.nameResolved = false;
this.hash = hash;
this.block = block;
if (rawBuffer) {
this.rawBuffer = rawBuffer.slice(headerOffset + block.offset, headerOffset + block.offset + block.compressedSize);
}
if (buffer) {
this.buffer = buffer;
}
}
/**
* Gets this file's data as a Uint8Array.
*
* An exception will be thrown if the file needs to be decoded, and decoding fails.
*/
bytes() {
// Decode if needed
if (this.buffer === null) {
this.decode();
}
// If decoding failed, an exception would have been thrown, so buffer is known to exist at this point.
return this.buffer;
}
/**
* Gets this file's data as an ArrayBuffer.
*
* An exception will be thrown if the file needs to be decoded, and decoding fails.
*/
arrayBuffer() {
return this.bytes().buffer;
}
/**
* Gets this file's data as a UTF8 string.
*
* An exception will be thrown if the file needs to be decoded, and decoding fails.
*/
text() {
return (0, utf8_1.decodeUtf8)(this.bytes());
}
/**
* Changes the buffer of this file.
*
* Does nothing if the archive is in readonly mode.
*/
set(buffer) {
if (this.archive.readonly) {
return false;
}
const hash = this.hash;
const block = this.block;
// Reset the hash.
hash.locale = 0;
hash.platform = 0;
// Reset the block.
block.compressedSize = 0;
block.normalSize = buffer.byteLength;
block.flags = 0;
this.buffer = buffer;
this.rawBuffer = null;
return true;
}
/**
* Deletes this file.
*
* Using the file after it was deleted will result in undefined behavior.
*
* Does nothing if the archive is in readonly mode.
*/
delete() {
if (this.archive.readonly) {
return false;
}
const archive = this.archive;
const hash = this.hash;
const blockIndex = hash.blockIndex;
hash.delete();
for (const hash of archive.hashTable.entries) {
if (hash.blockIndex < constants_1.HASH_ENTRY_DELETED && hash.blockIndex > blockIndex) {
hash.blockIndex -= 1;
}
}
archive.blockTable.entries.splice(blockIndex, 1);
archive.files.splice(blockIndex, 1);
return true;
}
/**
* Renames this file.
*
* Note that this sets the current file's hash's status to being deleted, rather than removing it.
* This is due to the way the search algorithm works.
*
* Does nothing if the archive is in readonly mode.
*/
rename(newName) {
if (this.archive.readonly) {
return false;
}
const hash = this.hash;
const locale = hash.locale;
const platform = hash.platform;
const blockIndex = hash.blockIndex;
// First delete the current hash.
// This will allow its entry to be reused in case it's the only empty/deleted entry in the hashtable.
hash.delete();
const newHash = this.archive.hashTable.add(newName, blockIndex);
newHash.locale = locale;
newHash.platform = platform;
this.name = newName;
this.nameResolved = true;
this.hash = newHash;
return true;
}
/**
* Decode this file.
*/
decode() {
if (!this.rawBuffer) {
throw new Error(`File ${this.name}: Nothing to decode`);
}
const archive = this.archive;
const block = this.block;
const c = archive.c;
const encryptionKey = c.computeFileKey(this.name, block);
const data = this.rawBuffer;
const flags = block.flags;
// One buffer of raw data.
// I don't know why having no flags means it's a chunk of memory rather than sectors.
// After all, there is no flag to say there are indeed sectors.
if (flags === constants_1.FILE_EXISTS) {
this.buffer = data.slice(0, block.normalSize);
}
else if (flags & constants_1.FILE_SINGLE_UNIT) {
// One buffer of possibly encrypted and/or compressed data.
// Read the sector
let sector;
// If this block is encrypted, decrypt the sector.
if (flags & constants_1.FILE_ENCRYPTED) {
sector = c.decryptBlock(data.slice(0, block.compressedSize), encryptionKey);
}
else {
sector = data.subarray(0, block.compressedSize);
}
// If this block is compressed, decompress the sector.
// Otherwise, copy the sector as-is.
if (flags & constants_1.FILE_COMPRESSED) {
sector = this.decompressSector(sector, block.normalSize);
}
else {
sector = sector.slice();
}
this.buffer = sector;
}
else {
// One or more sectors of possibly encrypted and/or compressed data.
const sectorCount = Math.ceil(block.normalSize / archive.sectorSize);
// Alocate a buffer for the uncompressed block size
const buffer = new Uint8Array(block.normalSize);
// Get the sector offsets
let sectorOffsets = new Uint32Array(data.buffer, 0, sectorCount + 1);
// If this file is encrypted, copy the sector offsets and decrypt them.
if (flags & constants_1.FILE_ENCRYPTED) {
sectorOffsets = c.decryptBlock(sectorOffsets.slice(), encryptionKey - 1);
}
let start = sectorOffsets[0];
let end = sectorOffsets[1];
let offset = 0;
for (let i = 0; i < sectorCount; i++) {
let sector;
// If this file is encrypted, copy the sector and decrypt it.
// Otherwise a view can be used directly.
if (flags & constants_1.FILE_ENCRYPTED) {
sector = c.decryptBlock(data.slice(start, end), encryptionKey + i);
}
else {
sector = data.subarray(start, end);
}
// Decompress the sector
if (flags & constants_1.FILE_COMPRESSED) {
let uncompressedSize = archive.sectorSize;
// If this is the last sector, its uncompressed size might not be the size of a sector.
if (block.normalSize - offset < uncompressedSize) {
uncompressedSize = block.normalSize - offset;
}
sector = this.decompressSector(sector, uncompressedSize);
}
// Some sectors have this flags instead of the compression flag + algorithm byte.
if (flags & constants_1.FILE_IMPLODE) {
sector = (0, explode_1.default)(sector);
}
// Add the sector bytes to the buffer
buffer.set(sector, offset);
offset += sector.byteLength;
// Prepare for the next sector
if (i < sectorCount) {
start = end;
end = sectorOffsets[i + 2];
}
}
this.buffer = buffer;
}
// If the archive is in read-only mode, the raw buffer isn't needed anymore, so free the memory.
if (archive.readonly) {
this.rawBuffer = null;
}
}
decompressSector(bytes, decompressedSize) {
// If the size of the data is the same as its decompressed size, it's not compressed.
if (bytes.byteLength === decompressedSize) {
return bytes;
}
else {
const compressionMask = bytes[0];
if (compressionMask & constants_1.COMPRESSION_BZIP2) {
throw new Error(`File ${this.name}: compression type 'bzip2' not supported`);
}
if (compressionMask & constants_1.COMPRESSION_IMPLODE) {
try {
bytes = (0, explode_1.default)(bytes.subarray(1));
}
catch (e) {
throw new Error(`File ${this.name}: failed to decompress with 'explode': ${e}`);
}
}
if (compressionMask & constants_1.COMPRESSION_DEFLATE) {
try {
bytes = (0, pako_1.inflate)(bytes.subarray(1));
}
catch (e) {
throw new Error(`File ${this.name}: failed to decompress with 'zlib': ${e}`);
}
}
if (compressionMask & constants_1.COMPRESSION_HUFFMAN) {
// try {
// bytes = decodeHuffman(bytes.subarray(1));
// } catch (e) {
// throw new Error(`File ${this.name}: failed to decompress with 'huffman': ${e}`);
// }
throw new Error(`File ${this.name}: compression type 'huffman' not supported`);
}
if (compressionMask & constants_1.COMPRESSION_ADPCM_STEREO) {
throw new Error(`File ${this.name}: compression type 'adpcm stereo' not supported`);
}
if (compressionMask & constants_1.COMPRESSION_ADPCM_MONO) {
throw new Error(`File ${this.name}: compression type 'adpcm mono' not supported`);
}
return bytes;
}
}
/**
* Encode this file.
* Archives (maps or generic MPQs) are stored uncompressed in one chunk.
* Other files are always stored in sectors, except when a file is smaller than a sector.
* Sectors themselves are always compressed, except when the result is smaller than the uncompressed data.
*/
encode() {
if (this.buffer !== null && this.rawBuffer === null) {
const data = this.buffer;
if ((0, isarchive_1.isArchive)(data)) {
this.rawBuffer = this.buffer;
this.block.compressedSize = this.buffer.byteLength;
this.block.flags = constants_1.FILE_EXISTS;
}
else {
const sectorSize = this.archive.sectorSize;
const sectorCount = Math.ceil(data.byteLength / sectorSize);
const offsets = new Uint32Array(sectorCount + 1);
let offset = offsets.byteLength;
const sectors = [];
const compression = [];
// First offset is right after the offsets list.
offsets[0] = offset;
for (let i = 0; i < sectorCount; i++) {
const sectorOffset = i * sectorSize;
let sector = data.subarray(sectorOffset, sectorOffset + sectorSize);
let size = sector.byteLength;
const compressed = (0, pako_1.deflate)(sector);
let isCompressed = false;
// If the compressed size of the sector is smaller than the uncompressed, use the compressed data.
// +1 because of the compression mask byte.
if (compressed.byteLength + 1 < size) {
sector = compressed;
size = compressed.byteLength + 1;
isCompressed = true;
}
offset += size;
offsets[i + 1] = offset;
sectors[i] = sector;
compression[i] = isCompressed;
}
// Only use the compressed data if it's actually smaller than the uncompressed data.
if (offset < data.byteLength) {
const rawBuffer = new Uint8Array(offset);
// Write the offsets list.
rawBuffer.set(new Uint8Array(offsets.buffer));
offset = offsets.byteLength;
for (let i = 0; i < sectorCount; i++) {
// If this sector is compressed, set it to zlib.
if (compression[i]) {
rawBuffer[offset] = 2;
offset += 1;
}
// Write the sector.
const sector = sectors[i];
rawBuffer.set(sector, offset);
offset += sector.byteLength;
}
this.rawBuffer = rawBuffer;
this.block.compressedSize = rawBuffer.byteLength;
this.block.flags = (constants_1.FILE_EXISTS | constants_1.FILE_COMPRESSED) >>> 0;
}
else {
this.rawBuffer = this.buffer;
this.block.compressedSize = this.buffer.byteLength;
this.block.flags = constants_1.FILE_EXISTS;
}
}
}
}
/**
* Decrypt this file and encrypt it back, with a new offset in the archive.
* This is used for files that use FILE_OFFSET_ADJUSTED_KEY, which are encrypted with a key that depends on their offset.
*/
reEncrypt(offset) {
if (!this.rawBuffer) {
return false;
}
const archive = this.archive;
const block = this.block;
const c = archive.c;
const bytes = this.rawBuffer;
const flags = block.flags;
const encryptionKey = c.computeFileKey(this.name, block);
block.offset = offset;
const newEncryptionKey = c.computeFileKey(this.name, block);
if (flags & constants_1.FILE_SINGLE_UNIT) {
// Decrypt the chunk with the old key.
c.decryptBlock(bytes, encryptionKey);
// Encrypt the chunk with the new key.
c.encryptBlock(bytes, newEncryptionKey);
}
else {
const sectorCount = Math.ceil(block.normalSize / archive.sectorSize);
// Get the sector offsets
const sectorOffsets = new Uint32Array(bytes.buffer, 0, sectorCount + 1);
// Decrypt the sector offsets with the old key.
c.decryptBlock(sectorOffsets, encryptionKey - 1);
let start = sectorOffsets[0];
let end = sectorOffsets[1];
for (let i = 0; i < sectorCount; i++) {
const sector = bytes.subarray(start, end);
// Decrypt the chunk with the old key.
c.decryptBlock(sector, encryptionKey + i);
// Encrypt the chunk with the new key.
c.encryptBlock(sector, newEncryptionKey + i);
// Prepare for the next sector
if (i < sectorCount) {
start = end;
end = sectorOffsets[i + 2];
}
}
// Encrypt the sector offsets with the new key.
c.encryptBlock(sectorOffsets, newEncryptionKey - 1);
}
return true;
}
/**
* The offset of the file has been recalculated.
* If the offset is different, and this file uses FILE_OFFSET_ADJUSTED_KEY encryption, it must be re-encrypted with the new offset.
*/
offsetChanged(offset) {
const block = this.block;
if (block.offset !== offset && block.flags & constants_1.FILE_OFFSET_ADJUSTED_KEY) {
if (this.nameResolved) {
return this.reEncrypt(offset);
}
return false;
}
block.offset = offset;
return true;
}
}
exports.default = MpqFile;
//# sourceMappingURL=file.js.map