From 0baa1c38ebe33ab8ae94266a7ff02fa6edee29eb Mon Sep 17 00:00:00 2001 From: Andrey Kolosov Date: Wed, 21 Aug 2019 19:20:36 +0300 Subject: [PATCH] save management --- package-lock.json | 37 +++- package.json | 5 +- src/App.js | 70 ++++++- src/App.scss | 22 +++ src/api/codec.js | 164 ++++++++++++++++ src/api/explode.js | 443 ++++++++++++++++++++++++++++++++++++++++++++ src/api/savefile.js | 229 +++++++++++++++++++++++ 7 files changed, 962 insertions(+), 8 deletions(-) create mode 100644 src/api/codec.js create mode 100644 src/api/explode.js create mode 100644 src/api/savefile.js diff --git a/package-lock.json b/package-lock.json index 05adfc6..a6e0c24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "diabloweb", - "version": "1.0.35", + "version": "1.0.36", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -926,6 +926,36 @@ "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-9.0.1.tgz", "integrity": "sha512-6It2EVfGskxZCQhuykrfnALg7oVeiI6KclWSmGDqB0AiInVrTGB9Jp9i4/Ad21u9Jde/voVQz6eFX/eSg/UsPA==" }, + "@fortawesome/fontawesome-common-types": { + "version": "0.2.21", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.21.tgz", + "integrity": "sha512-iJtcrU2BtF9Wyr0zm3tHEJy3HqA6sADExhCqCv3SKaJJKKp4ORJ40t4nyHvcWXSVFtd7r1gcdqcRsAfoREGTFA==" + }, + "@fortawesome/fontawesome-svg-core": { + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.21.tgz", + "integrity": "sha512-EhrgMZLJS0tTYZhUbodurZBqDgAFLDNdxJP/q5unrZJwiFo8Dd7xGvJdhAhY5WcX4khzkPQcbLTCMPHBtutD7Q==", + "requires": { + "@fortawesome/fontawesome-common-types": "^0.2.21" + } + }, + "@fortawesome/free-solid-svg-icons": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.10.1.tgz", + "integrity": "sha512-MKH+SCt0DnVoXdemxf6JEdTRtCPwYLMCWZcwgGccYU/ab6QcDtbAMn6Xm4Zub6YqQCcaiy0hU294YdHOldSBRA==", + "requires": { + "@fortawesome/fontawesome-common-types": "^0.2.21" + } + }, + "@fortawesome/react-fontawesome": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.4.tgz", + "integrity": "sha512-GwmxQ+TK7PEdfSwvxtGnMCqrfEm0/HbRHArbUudsYiy9KzVCwndxa2KMcfyTQ8El0vROrq8gOOff09RF1oQe8g==", + "requires": { + "humps": "^2.0.1", + "prop-types": "^15.5.10" + } + }, "@hapi/address": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.0.0.tgz", @@ -6270,6 +6300,11 @@ } } }, + "humps": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/humps/-/humps-2.0.1.tgz", + "integrity": "sha1-3QLqYIG9BWjcXQcxhEY5V7qe+ao=" + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/package.json b/package.json index d03877a..48310c1 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,12 @@ { "name": "diabloweb", - "version": "1.0.35", + "version": "1.0.36", "private": true, "dependencies": { "@babel/core": "7.4.3", + "@fortawesome/fontawesome-svg-core": "^1.2.21", + "@fortawesome/free-solid-svg-icons": "^5.10.1", + "@fortawesome/react-fontawesome": "^0.1.4", "@svgr/webpack": "4.1.0", "@typescript-eslint/eslint-plugin": "1.6.0", "@typescript-eslint/parser": "1.6.0", diff --git a/src/App.js b/src/App.js index 59f6dde..f01e1eb 100644 --- a/src/App.js +++ b/src/App.js @@ -2,6 +2,9 @@ import React from 'react'; import './App.scss'; import classNames from 'classnames'; import ReactGA from 'react-ga'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faTimes, faDownload } from '@fortawesome/free-solid-svg-icons'; +import getPlayerName from './api/savefile'; import { mapStackTrace } from 'sourcemapped-stacktrace'; @@ -127,6 +130,7 @@ class App extends React.Component { if (spawn && SpawnSizes.includes(spawn.byteLength)) { this.setState({has_spawn: true}); } + this.updateSaves(); }); } @@ -218,10 +222,29 @@ class App extends React.Component { setCurrentSave(name) { this.saveName = name; } - downloadSave = e => { - this.fs.then(fs => this.saveName && fs.download(this.saveName)); - e.stopPropagation(); - e.preventDefault(); + + updateSaves() { + this.fs.then(fs => { + const saves = []; + [...fs.files.keys()].filter(name => name.match(/\.sv$/i)).forEach(name => { + saves.push(name); + console.log(name, getPlayerName(fs.files.get(name).buffer)); + }); + this.setState({save_names: saves}); + }); + } + removeSave(name) { + if (window.confirm(`Are you sure you want to delete ${name}?`)) { + (async () => { + const fs = await this.fs; + await fs.delete(name.toLowerCase()); + fs.files.delete(name.toLowerCase()); + this.updateSaves(); + })(); + } + } + downloadSave(name) { + this.fs.then(fs => fs.download(name)); } drawBelt(idx, slot) { @@ -260,7 +283,12 @@ class App extends React.Component { start(file) { if (file && file.name.match(/\.sv$/i)) { - this.fs.then(fs => fs.upload(file)).then(console.log(`Updated ${file.name}`)); + this.fs.then(fs => fs.upload(file)).then(() => { + this.updateSaves(); + }); + return; + } + if (this.state.show_saves) { return; } if (file && !file.name.match(/\.mpq$/i)) { @@ -455,6 +483,13 @@ class App extends React.Component { } } + parseSave = e => { + const files = e.target.files; + if (files.length > 0) { + this.start(files[0]); + } + } + touchButton = null; touchCanvas = null; @@ -622,7 +657,29 @@ class App extends React.Component { } render() { - const {started, loading, error, progress, dropping, has_spawn} = this.state; + const {started, loading, error, progress, dropping, has_spawn, save_names, show_saves} = this.state; + if (show_saves && save_names) { + return ( +
+
+
+
    + {save_names.map(name =>
  • + {name} + this.downloadSave(name)}/> + this.removeSave(name)}/> +
  • )} +
+
+ + +
+ this.setState({show_saves: false})}>Back +
+
+
+ ); + } return (
@@ -678,6 +735,7 @@ class App extends React.Component { this.start()}>Play Shareware + {!!(save_names && save_names.length) && this.setState({show_saves: true})}>Manage Saves}
)}
diff --git a/src/App.scss b/src/App.scss index 94bd92a..af3e5ff 100644 --- a/src/App.scss +++ b/src/App.scss @@ -122,6 +122,28 @@ body, #root, .App { background-color: #111; } } + .saveList { + border: 1px solid #fff; + text-align: left; + li { + padding: 0 6px; + .btnRemove { + color: #f88; + float: right; + cursor: pointer; + margin: 0 4px; + } + .btnDownload { + color: #fff; + float: right; + cursor: pointer; + margin: 0 4px; + } + &:hover { + background-color: #444; + } + } + } } } diff --git a/src/api/codec.js b/src/api/codec.js new file mode 100644 index 0000000..2bf2aa6 --- /dev/null +++ b/src/api/codec.js @@ -0,0 +1,164 @@ +const W = new Uint32Array(80); + +const SHA1CircularShift = (shift, value) => ((value << shift) | (value >>> (32 - shift))); + +class SHA1 { + state = new Uint32Array(5); + count = 0; + + input(u8) { + const u32 = new Uint32Array(u8.buffer, u8.byteOffset, 16); + context.count += data.length * 32; + for (let i = 0; i < 16; ++i) { + W[i] = u32[i]; + } + for (let i = 16; i < 80; ++i) { + W[i] = W[i - 16] ^ W[i - 14] ^ W[i - 8] ^ W[i - 3]; + } + let A = this.state[0]; + let B = this.state[1]; + let C = this.state[2]; + let D = this.state[3]; + let E = this.state[4]; + + for (let i = 0; i < 20; i++) { + const temp = SHA1CircularShift(5, A) + ((B & C) | ((~B) & D)) + E + W[i] + 0x5A827999; + E = D; + D = C; + C = SHA1CircularShift(30, B); + B = A; + A = temp | 0; + } + + for (let i = 20; i < 40; i++) { + const temp = SHA1CircularShift(5, A) + (B ^ C ^ D) + E + W[i] + 0x6ED9EBA1; + E = D; + D = C; + C = SHA1CircularShift(30, B); + B = A; + A = temp | 0; + } + + for (let i = 40; i < 60; i++) { + const temp = SHA1CircularShift(5, A) + ((B & C) | (B & D) | (C & D)) + E + W[i] + 0x8F1BBCDC; + E = D; + D = C; + C = SHA1CircularShift(30, B); + B = A; + A = temp | 0; + } + + for (let i = 60; i < 80; i++) { + const temp = SHA1CircularShift(5, A) + (B ^ C ^ D) + E + W[i] + 0xCA62C1D6; + E = D; + D = C; + C = SHA1CircularShift(30, B); + B = A; + A = temp | 0; + } + + this.state[0] += A; + this.state[1] += B; + this.state[2] += C; + this.state[3] += D; + this.state[4] += E; + } + + constructor() { + this.state[0] = 0x67452301; + this.state[1] = 0xEFCDAB89; + this.state[2] = 0x98BADCFE; + this.state[3] = 0x10325476; + this.state[4] = 0xC3D2E1F0; + + this.result = new Uint8Array(this.state.buffer); + } +} + +class Random { + constructor(seed) { + this.seed = seed; + } + next() { + this.seed = (((this.seed * 3) << 16) + ((this.seed * 67) << 8) + (this.seed * 253) + 2531011) | 0; + return (this.seed >> 16) & 0x7FFF; + } +} + +struct CodecSignature { + DWORD checksum; + BYTE error; + BYTE last_chunk_size; + WORD unused; +}; + +function codec_init_key(password) { + const rand = new Random(0x7058); + const key = new Uint8Array(136); + for (let i = 0; i < 136; ++i) { + key[i] = rand.next(); + } + const pw = new Uint8Array(64); + for (let i = 0; i < 64; ++i) { + pw[i] = password.charCodeAt(i % password.length); + } + + const sha = new SHA1(); + sha.input(pw); + + for (let i = 0; i < 136; ++i) { + key[i] ^= sha.result[i % sha.result.length]; + } + + sha = new SHA1(); + sha.input(key.subarray(72)); + return sha; +} + +function codec_decode(data, password) { + const sha = codec_init_key(password); + if (data.length <= 8) { + return; + } + const size = data.length - 8; + if () + char buf[128]; + char dst[SHA1HashSize]; + int i; + CodecSignature *sig; + + codec_init_key(0, pszPassword); + if (size <= 8) + return 0; + size = size - 8; + if (size % 64 != 0) + return 0; + for (i = size; i != 0; pbSrcDst += 64, i -= 64) { + memcpy(buf, pbSrcDst, 64); + SHA1Result(0, dst); + for (int j = 0; j < 64; j++) { + buf[j] ^= dst[j % SHA1HashSize]; + } + SHA1Calculate(0, buf, NULL); + memset(dst, 0, sizeof(dst)); + memcpy(pbSrcDst, buf, 64); + } + + memset(buf, 0, sizeof(buf)); + sig = (CodecSignature *)pbSrcDst; + if (sig->error > 0) { + size = 0; + SHA1Clear(); + } else { + SHA1Result(0, dst); + if (sig->checksum != *(DWORD *)dst) { + memset(dst, 0, sizeof(dst)); + size = 0; + SHA1Clear(); + } else { + size += sig->last_chunk_size - 64; + SHA1Clear(); + } + } + return size; +} diff --git a/src/api/explode.js b/src/api/explode.js new file mode 100644 index 0000000..16e2480 --- /dev/null +++ b/src/api/explode.js @@ -0,0 +1,443 @@ +export const CMP_BINARY = 0; // Binary compression +export const CMP_ASCII = 1; // Ascii compression +export const CMP_NO_ERROR = 0; +export const CMP_INVALID_DICTSIZE = 1; +export const CMP_INVALID_MODE = 2; +export const CMP_BAD_DATA = 3; +export const CMP_ABORT = 4; +export const CMP_IMPLODE_DICT_SIZE1 = 1024; // Dictionary size of 1024 +export const CMP_IMPLODE_DICT_SIZE2 = 2048; // Dictionary size of 2048 +export const CMP_IMPLODE_DICT_SIZE3 = 4096; // Dictionary size of 4096 + +export const PKDCL_OK = 0; +export const PKDCL_STREAM_END = 1; // All data from the input stream is read +export const PKDCL_NEED_DICT = 2; // Need more data (dictionary) +export const PKDCL_CONTINUE = 10; // Internal flag, not returned to user +export const PKDCL_GET_INPUT = 11; // Internal flag, not returned to user + +const DistBits = new Uint8Array([ + 0x02, 0x04, 0x04, 0x05, 0x05, 0x05, 0x05, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, + 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08 +]); + +const DistCode = new Uint8Array([ + 0x03, 0x0D, 0x05, 0x19, 0x09, 0x11, 0x01, 0x3E, 0x1E, 0x2E, 0x0E, 0x36, 0x16, 0x26, 0x06, 0x3A, + 0x1A, 0x2A, 0x0A, 0x32, 0x12, 0x22, 0x42, 0x02, 0x7C, 0x3C, 0x5C, 0x1C, 0x6C, 0x2C, 0x4C, 0x0C, + 0x74, 0x34, 0x54, 0x14, 0x64, 0x24, 0x44, 0x04, 0x78, 0x38, 0x58, 0x18, 0x68, 0x28, 0x48, 0x08, + 0xF0, 0x70, 0xB0, 0x30, 0xD0, 0x50, 0x90, 0x10, 0xE0, 0x60, 0xA0, 0x20, 0xC0, 0x40, 0x80, 0x00 +]); + +const ExLenBits = new Uint8Array([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 +]); + +const LenBase = new Uint16Array([ + 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, + 0x0008, 0x000A, 0x000E, 0x0016, 0x0026, 0x0046, 0x0086, 0x0106 +]); + +const LenBits = new Uint8Array([ + 0x03, 0x02, 0x03, 0x03, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x05, 0x06, 0x06, 0x06, 0x07, 0x07 +]); + +const LenCode = new Uint8Array([ + 0x05, 0x03, 0x01, 0x06, 0x0A, 0x02, 0x0C, 0x14, 0x04, 0x18, 0x08, 0x30, 0x10, 0x20, 0x40, 0x00 +]); + +const ChBitsAsc = new Uint8Array([ + 0x0B, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x08, 0x07, 0x0C, 0x0C, 0x07, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0D, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x04, 0x0A, 0x08, 0x0C, 0x0A, 0x0C, 0x0A, 0x08, 0x07, 0x07, 0x08, 0x09, 0x07, 0x06, 0x07, 0x08, + 0x07, 0x06, 0x07, 0x07, 0x07, 0x07, 0x08, 0x07, 0x07, 0x08, 0x08, 0x0C, 0x0B, 0x07, 0x09, 0x0B, + 0x0C, 0x06, 0x07, 0x06, 0x06, 0x05, 0x07, 0x08, 0x08, 0x06, 0x0B, 0x09, 0x06, 0x07, 0x06, 0x06, + 0x07, 0x0B, 0x06, 0x06, 0x06, 0x07, 0x09, 0x08, 0x09, 0x09, 0x0B, 0x08, 0x0B, 0x09, 0x0C, 0x08, + 0x0C, 0x05, 0x06, 0x06, 0x06, 0x05, 0x06, 0x06, 0x06, 0x05, 0x0B, 0x07, 0x05, 0x06, 0x05, 0x05, + 0x06, 0x0A, 0x05, 0x05, 0x05, 0x05, 0x08, 0x07, 0x08, 0x08, 0x0A, 0x0B, 0x0B, 0x0C, 0x0C, 0x0C, + 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, + 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, + 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, + 0x0D, 0x0C, 0x0D, 0x0D, 0x0D, 0x0C, 0x0D, 0x0D, 0x0D, 0x0C, 0x0D, 0x0D, 0x0D, 0x0D, 0x0C, 0x0D, + 0x0D, 0x0D, 0x0C, 0x0C, 0x0C, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D +]); + +const ChCodeAsc = new Uint16Array([ + 0x0490, 0x0FE0, 0x07E0, 0x0BE0, 0x03E0, 0x0DE0, 0x05E0, 0x09E0, + 0x01E0, 0x00B8, 0x0062, 0x0EE0, 0x06E0, 0x0022, 0x0AE0, 0x02E0, + 0x0CE0, 0x04E0, 0x08E0, 0x00E0, 0x0F60, 0x0760, 0x0B60, 0x0360, + 0x0D60, 0x0560, 0x1240, 0x0960, 0x0160, 0x0E60, 0x0660, 0x0A60, + 0x000F, 0x0250, 0x0038, 0x0260, 0x0050, 0x0C60, 0x0390, 0x00D8, + 0x0042, 0x0002, 0x0058, 0x01B0, 0x007C, 0x0029, 0x003C, 0x0098, + 0x005C, 0x0009, 0x001C, 0x006C, 0x002C, 0x004C, 0x0018, 0x000C, + 0x0074, 0x00E8, 0x0068, 0x0460, 0x0090, 0x0034, 0x00B0, 0x0710, + 0x0860, 0x0031, 0x0054, 0x0011, 0x0021, 0x0017, 0x0014, 0x00A8, + 0x0028, 0x0001, 0x0310, 0x0130, 0x003E, 0x0064, 0x001E, 0x002E, + 0x0024, 0x0510, 0x000E, 0x0036, 0x0016, 0x0044, 0x0030, 0x00C8, + 0x01D0, 0x00D0, 0x0110, 0x0048, 0x0610, 0x0150, 0x0060, 0x0088, + 0x0FA0, 0x0007, 0x0026, 0x0006, 0x003A, 0x001B, 0x001A, 0x002A, + 0x000A, 0x000B, 0x0210, 0x0004, 0x0013, 0x0032, 0x0003, 0x001D, + 0x0012, 0x0190, 0x000D, 0x0015, 0x0005, 0x0019, 0x0008, 0x0078, + 0x00F0, 0x0070, 0x0290, 0x0410, 0x0010, 0x07A0, 0x0BA0, 0x03A0, + 0x0240, 0x1C40, 0x0C40, 0x1440, 0x0440, 0x1840, 0x0840, 0x1040, + 0x0040, 0x1F80, 0x0F80, 0x1780, 0x0780, 0x1B80, 0x0B80, 0x1380, + 0x0380, 0x1D80, 0x0D80, 0x1580, 0x0580, 0x1980, 0x0980, 0x1180, + 0x0180, 0x1E80, 0x0E80, 0x1680, 0x0680, 0x1A80, 0x0A80, 0x1280, + 0x0280, 0x1C80, 0x0C80, 0x1480, 0x0480, 0x1880, 0x0880, 0x1080, + 0x0080, 0x1F00, 0x0F00, 0x1700, 0x0700, 0x1B00, 0x0B00, 0x1300, + 0x0DA0, 0x05A0, 0x09A0, 0x01A0, 0x0EA0, 0x06A0, 0x0AA0, 0x02A0, + 0x0CA0, 0x04A0, 0x08A0, 0x00A0, 0x0F20, 0x0720, 0x0B20, 0x0320, + 0x0D20, 0x0520, 0x0920, 0x0120, 0x0E20, 0x0620, 0x0A20, 0x0220, + 0x0C20, 0x0420, 0x0820, 0x0020, 0x0FC0, 0x07C0, 0x0BC0, 0x03C0, + 0x0DC0, 0x05C0, 0x09C0, 0x01C0, 0x0EC0, 0x06C0, 0x0AC0, 0x02C0, + 0x0CC0, 0x04C0, 0x08C0, 0x00C0, 0x0F40, 0x0740, 0x0B40, 0x0340, + 0x0300, 0x0D40, 0x1D00, 0x0D00, 0x1500, 0x0540, 0x0500, 0x1900, + 0x0900, 0x0940, 0x1100, 0x0100, 0x1E00, 0x0E00, 0x0140, 0x1600, + 0x0600, 0x1A00, 0x0E40, 0x0640, 0x0A40, 0x0A00, 0x1200, 0x0200, + 0x1C00, 0x0C00, 0x1400, 0x0400, 0x1800, 0x0800, 0x1000, 0x0000 +]); + +/** + * @param {Uint8Array} positions [out] Table of positions + * @param {Uint8Array} start_indexes Table of start indexes + * @param {Uint8Array} length_bits Table of lengths. Each length is stored as number of bits + * @param {number} elements Number of elements in start_indexes and length_bits + */ +function GenDecodeTabs(positions, start_indexes, length_bits, elements) { + for (let i = 0; i < elements; i++) { + const length = 1 << length_bits[i]; // Get the length in bytes + for (let index = start_indexes[i]; index < 0x100; index += length) { + positions[index] = i; + } + } +} + +function GenAscTabs(pWork) { + let pChCodeAsc = 0xFF; + + for (let count = 0x00FF; pChCodeAsc >= 0; pChCodeAsc--, count--) { + let bits_asc = pWork.ChBitsAsc[count]; + let acc; + + if (bits_asc <= 8) { + const add = (1 << bits_asc); + acc = ChCodeAsc[pChCodeAsc]; + + do { + pWork.offs2C34[acc] = count; + acc += add; + } while(acc < 0x100); + } else if ((acc = (ChCodeAsc[pChCodeAsc] & 0xFF)) !== 0) { + pWork.offs2C34[acc] = 0xFF; + + if (ChCodeAsc[pChCodeAsc] & 0x3F) { + bits_asc -= 4; + pWork.ChBitsAsc[count] = bits_asc; + + const add = (1 << bits_asc); + acc = ChCodeAsc[pChCodeAsc] >> 4; + do { + pWork.offs2D34[acc] = count; + acc += add; + } while(acc < 0x100); + } else { + bits_asc -= 6; + pWork.ChBitsAsc[count] = bits_asc; + + const add = (1 << bits_asc); + acc = ChCodeAsc[pChCodeAsc] >> 6; + do { + pWork.offs2E34[acc] = count; + acc += add; + } while(acc < 0x80); + } + } else { + bits_asc -= 8; + pWork.ChBitsAsc[count] = bits_asc; + + const add = (1 << bits_asc); + acc = ChCodeAsc[pChCodeAsc] >> 8; + do { + pWork.offs2EB4[acc] = count; + acc += add; + } while(acc < 0x100); + } + } +} + +//----------------------------------------------------------------------------- +// Removes given number of bits in the bit buffer. New bits are reloaded from +// the input buffer, if needed. +// Returns: PKDCL_OK: Operation was successful +// PKDCL_STREAM_END: There are no more bits in the input buffer +function WasteBits(pWork, nBits) { + // If number of bits required is less than number of (bits in the buffer) ? + if (nBits <= pWork.extra_bits) { + pWork.extra_bits -= nBits; + pWork.bit_buff >>>= nBits; + return PKDCL_OK; + } + + // Load input buffer if necessary + pWork.bit_buff >>= pWork.extra_bits; + if (pWork.in_pos === pWork.in_bytes) { + if ((pWork.in_bytes = pWork.read_buf(pWork.in_buff)) === 0) { + return PKDCL_STREAM_END; + } + pWork.in_pos = 0; + } + + // Update bit buffer + pWork.bit_buff |= (pWork.in_buff[pWork.in_pos++] << 8); + pWork.bit_buff >>>= (nBits - pWork.extra_bits); + pWork.extra_bits = (pWork.extra_bits - nBits) + 8; + return PKDCL_OK; +} + +//----------------------------------------------------------------------------- +// Decodes next literal from the input (compressed) data. +// Returns : 0x000: One byte 0x00 +// 0x001: One byte 0x01 +// ... +// 0x0FF: One byte 0xFF +// 0x100: Repetition, length of 0x02 bytes +// 0x101: Repetition, length of 0x03 bytes +// ... +// 0x304: Repetition, length of 0x206 bytes +// 0x305: End of stream +// 0x306: Error +function DecodeLit(pWork) { + if(pWork.bit_buff & 1) { + // Remove one bit from the input data + if(WasteBits(pWork, 1)) { + return 0x306; + } + + // The next 8 bits hold the index to the length code table + let length_code = pWork.LengthCodes[pWork.bit_buff & 0xFF]; + + // Remove the apropriate number of bits + if(WasteBits(pWork, pWork.LenBits[length_code])) { + return 0x306; + } + + // Are there some extra bits for the obtained length code ? + const extra_length_bits = pWork.ExLenBits[length_code]; + if(extra_length_bits !== 0) { + const extra_length = pWork.bit_buff & ((1 << extra_length_bits) - 1); + if(WasteBits(pWork, extra_length_bits)) { + if((length_code + extra_length) != 0x10E) { + return 0x306; + } + } + length_code = pWork.LenBase[length_code] + extra_length; + } + + // In order to distinguish uncompressed byte from repetition length, + // we have to add 0x100 to the length. + return length_code + 0x100; + } + + // Remove one bit from the input data + if(WasteBits(pWork, 1)) { + return 0x306; + } + + // If the binary compression type, read 8 bits and return them as one byte. + if(pWork.ctype === CMP_BINARY) { + const uncompressed_byte = pWork.bit_buff & 0xFF; + + if(WasteBits(pWork, 8)) { + return 0x306; + } + return uncompressed_byte; + } + + // When ASCII compression ... + let value; + if (pWork.bit_buff & 0xFF) { + value = pWork.offs2C34[pWork.bit_buff & 0xFF]; + + if (value == 0xFF) { + if (pWork.bit_buff & 0x3F) { + if (WasteBits(pWork, 4)) { + return 0x306; + } + + value = pWork.offs2D34[pWork.bit_buff & 0xFF]; + } else { + if (WasteBits(pWork, 6)) { + return 0x306; + } + + value = pWork.offs2E34[pWork.bit_buff & 0x7F]; + } + } + } else { + if(WasteBits(pWork, 8)) { + return 0x306; + } + + value = pWork.offs2EB4[pWork.bit_buff & 0xFF]; + } + + return WasteBits(pWork, pWork.ChBitsAsc[value]) ? 0x306 : value; +} + +//----------------------------------------------------------------------------- +// Decodes the distance of the repetition, backwards relative to the +// current output buffer position +function DecodeDist(pWork, rep_length) { + // Next 2-8 bits in the input buffer is the distance position code + const dist_pos_code = pWork.DistPosCodes[pWork.bit_buff & 0xFF]; + const dist_pos_bits = pWork.DistBits[dist_pos_code]; + if (WasteBits(pWork, dist_pos_bits)) { + return 0; + } + + let distance; + if (rep_length === 2) { + // If the repetition is only 2 bytes length, + // then take 2 bits from the stream in order to get the distance + distance = (dist_pos_code << 2) | (pWork.bit_buff & 0x03); + if (WasteBits(pWork, 2)) { + return 0; + } + } else { + // If the repetition is more than 2 bytes length, + // then take "dsize_bits" bits in order to get the distance + distance = (dist_pos_code << pWork.dsize_bits) | (pWork.bit_buff & pWork.dsize_mask); + if (WasteBits(pWork, pWork.dsize_bits)) { + return 0; + } + } + return distance + 1; +} + +function Expand(pWork) { + let outputPos = 0x1000; // Initialize output buffer position + + // Decode the next literal from the input data. + // The returned literal can either be an uncompressed byte (next_literal < 0x100) + // or an encoded length of the repeating byte sequence that + // is to be copied to the current buffer position + let result, next_literal; + while ((result = next_literal = DecodeLit(pWork)) < 0x305) { + // If the literal is greater than 0x100, it holds length + // of repeating byte sequence + // literal of 0x100 means repeating sequence of 0x2 bytes + // literal of 0x101 means repeating sequence of 0x3 bytes + // ... + // literal of 0x305 means repeating sequence of 0x207 bytes + if(next_literal >= 0x100) { + // Get the length of the repeating sequence. + // Note that the repeating block may overlap the current output position, + // for example if there was a sequence of equal bytes + let rep_length = next_literal - 0xFE; + // Get backward distance to the repetition + const minus_dist = DecodeDist(pWork, rep_length); + if (minus_dist === 0) { + result = 0x306; + break; + } + + // Target and source pointer + let target = outputPos; + let source = target - minus_dist; + + // Update buffer output position + outputPos += rep_length; + + // Copy the repeating sequence + const out_buff = pWork.out_buff; + while (rep_length-- > 0) { + out_buff[target++] = out_buff[source++]; + } + } else { + pWork.out_buff[outputPos++] = next_literal; + } + + // Flush the output buffer, if number of extracted bytes has reached the end + if (outputPos >= 0x2000) { + // Copy decompressed data into user buffer + pWork.write_buf(pWork.out_buff.subarray(0x1000, 0x2000)); + + // Now copy the decompressed data to the first half of the buffer. + // This is needed because the decompression might reuse them as repetitions. + // Note that if the output buffer overflowed previously, the extra decompressed bytes + // are stored in "out_buff_overflow", and they will now be + // within decompressed part of the output buffer. + pWork.out_buff.copyWithin(0, 0x1000, outputPos); + outputPos -= 0x1000; + } + } + + // Flush any remaining decompressed bytes + pWork.write_buf(pWork.out_buff.subarray(0x1000, outputPos)); + return result; +} + +//----------------------------------------------------------------------------- +// Main exploding function. +export function explode(read_buf, write_buf) { + const buffer = new ArrayBuffer(0x3104); + const pWork = { + read_buf, + write_buf, + in_pos: 3, + extra_bits: 0, + in_buff: new Uint8Array(buffer, 0, 0x800), + DistPosCodes: new Uint8Array(buffer, 0x800, 0x100), + LengthCodes: new Uint8Array(buffer, 0x900, 0x100), + offs2C34: new Uint8Array(buffer, 0xA00, 0x100), + offs2D34: new Uint8Array(buffer, 0xB00, 0x100), + offs2E34: new Uint8Array(buffer, 0xC00, 0x80), + offs2EB4: new Uint8Array(buffer, 0xC80, 0x100), + ChBitsAsc: new Uint8Array(buffer, 0xD80, 0x100), + DistBits: new Uint8Array(buffer, 0xE80, 0x40), + LenBits: new Uint8Array(buffer, 0xEC0, 0x10), + ExLenBits: new Uint8Array(buffer, 0xED0, 0x10), + LenBase: new Uint16Array(buffer, 0xEE0, 0x10), + out_buff: new Uint8Array(buffer, 0xF00, 0x2204), + }; + pWork.in_bytes = read_buf(pWork.in_buff); + if (pWork.in_bytes <= 4) { + return CMP_BAD_DATA; + } + pWork.ctype = pWork.in_buff[0]; + pWork.dsize_bits = pWork.in_buff[1]; + pWork.bit_buff = pWork.in_buff[2]; + + // Test for the valid dictionary size + if(4 > pWork.dsize_bits || pWork.dsize_bits > 6) { + return CMP_INVALID_DICTSIZE; + } + + pWork.dsize_mask = 0xFFFF >> (0x10 - pWork.dsize_bits); // Shifted by 'sar' instruction + + if(pWork.ctype != CMP_BINARY) { + if(pWork.ctype != CMP_ASCII) { + return CMP_INVALID_MODE; + } + + pWork.ChBitsAsc.set(ChBitsAsc); + GenAscTabs(pWork); + } + + pWork.LenBits.set(LenBits); + GenDecodeTabs(pWork.LengthCodes, LenCode, pWork.LenBits, pWork.LenBits.length); + pWork.ExLenBits.set(ExLenBits); + pWork.LenBase.set(LenBase); + pWork.DistBits.set(DistBits); + GenDecodeTabs(pWork.DistPosCodes, DistCode, pWork.DistBits, pWork.DistBits.length); + if(Expand(pWork) !== 0x306) { + return CMP_NO_ERROR; + } + + return CMP_ABORT; +} + +export default explode; diff --git a/src/api/savefile.js b/src/api/savefile.js new file mode 100644 index 0000000..728b966 --- /dev/null +++ b/src/api/savefile.js @@ -0,0 +1,229 @@ +import { explode } from './explode'; + +function pkzip_decompress(data, out_size) { + if (data.length === out_size) { + return data; + } + + const output = new Uint8Array(out_size); + let in_pos = 0; + let out_pos = 0; + function read_buf(dst) { + const count = Math.min(data.length - in_pos, dst.length); + dst.set(data.subarray(in_pos, count)); + in_pos += count; + return count; + } + function write_buf(src) { + if (out_pos + src.length > out_size) { + throw Error('decompress buffer overflow'); + } + output.set(src, out_pos); + out_pos += src.length; + } + + if (explode(read_buf, write_buf) || out_pos !== out_size) { + return null; + } + + return output; +} + +const hashtable = (function() { + const hashtable = new Uint32Array(1280); + let seed = 0x00100001; + for (let i = 0; i < 256; i++) { + for (let j = i; j < 1280; j += 256) { + seed = (seed * 125 + 3) % 0x2AAAAB; + const a = (seed & 0xFFFF) << 16; + seed = (seed * 125 + 3) % 0x2AAAAB; + const b = (seed & 0xFFFF); + hashtable[j] = a | b; + } + } + return hashtable; +})(); +function decrypt(u32, key) { + let seed = 0xEEEEEEEE; + for (let i = 0; i < u32.length; ++i) { + seed += hashtable[0x400 + (key & 0xFF)]; + u32[i] ^= seed + key; + seed = (u32[i] + seed * 33 + 3) | 0; + key = ((~key << 0x15) + 0x11111111) | (key >>> 0x0B); + } +} +function decrypt8(u8, key) { + decrypt(new Uint32Array(u8.buffer, u8.byteOffset, u8.length >> 2), key); +} +function hash(name, type) { + let seed1 = 0x7FED7FED; + let seed2 = 0xEEEEEEEE; + for (let i = 0; i < name.length; ++i) { + let ch = name.charCodeAt(i); + if (ch >= 0x61 && ch <= 0x7A) { + ch -= 0x20; + } + if (ch === 0x2F) { + ch = 0x5C; + } + seed1 = hashtable[type * 256 + ch] ^ (seed1 + seed2); + seed2 = (ch + seed1 + seed2 * 33 + 3) | 0; + } + return seed1 >>> 0; +} + +function path_name(name) { + const pos = Math.max(name.lastIndexOf('/'), name.lastIndexOf('\\')); + return name.substring(pos + 1); +} + +const Flags = { + CompressPkWare: 0x00000100, + CompressMulti: 0x00000200, + Compressed: 0x0000FF00, + Encrypted: 0x00010000, + FixSeed: 0x00020000, + PatchFile: 0x00100000, + SingleUnit: 0x01000000, + DummyFile: 0x02000000, + SectorCrc: 0x04000000, + Exists: 0x80000000, +}; + +class MpqReader { + constructor(buffer) { + this.buffer = buffer; + this.u8 = new Uint8Array(buffer); + this.u32 = new Uint32Array(buffer); + + this.readHeader(); + } + + readHeader() { + const {u8, u32} = this; + if (u32[0] !== 0x1A51504D) { + throw Error('invalid MPQ header'); + } + const sizeId = u8[14] + (u8[15] << 8); + const hashOffset = u32[4]; + const blockOffset = u32[5]; + const hashCount = u32[6]; + const blockCount = u32[7]; + this.hashTable = this.readTable(hashOffset, hashCount, "(hash table)"); + this.blockTable = this.readTable(blockOffset, blockCount, "(block table)"); + this.blockSize = 1 << (9 + sizeId); + } + + readTable(offset, count, key) { + const buffer = new Uint32Array(this.buffer.slice(offset, offset + count * 16)); + decrypt(buffer, hash(key, 3)); + return buffer; + } + + fileIndex(name) { + const {hashTable} = this; + const length = hashTable.length >> 2; + const index = hash(name, 0) % length; + const keyA = hash(name, 1), keyB = hash(name, 2); + for (let i = index, count = 0; hashTable[i * 4 + 3] !== 0xFFFFFFFF && count < length; i = (i + 1) % length, ++count) { + if (hashTable[i * 4] === keyA && hashTable[i * 4 + 1] === keyB && hashTable[i * 4 + 3] !== 0xFFFFFFFE) { + return i; + } + } + } + + read(name) { + const index = this.fileIndex(name); + if (index == null) { + return; + } + const block = this.hashTable[index * 4 + 3]; + const filePos = this.blockTable[block * 4]; + let cmpSize = this.blockTable[block * 4 + 1]; + const fileSize = this.blockTable[block * 4 + 2]; + const flags = this.blockTable[block * 4 + 3]; + + if (flags & Flags.PatchFile) { + return; + } + if (!(flags & Flags.Compressed)) { + cmpSize = fileSize; + } + + let key = hash(path_name(name), 3); + if (flags & Flags.FixSeed) { + key = (key + filePos) ^ fileSize; + } + + if (flags & Flags.SingleUnit) { + const raw = new Uint8Array(this.buffer, filePos, cmpSize); + if (raw.length !== cmpSize) { + return; + } + if (flags & Flags.Encrypted) { + decrypt8(raw, key); + } + if (flags & Flags.CompressMulti) { + return; + } else if (flags & Flags.CompressPkWare) { + return pkzip_decompress(raw, fileSize); + } + return raw; + } else if (!(flags & Flags.Compressed)) { + const raw = Uint8Array(this.buffer, filePos, fileSize); + if (raw.length !== fileSize) { + return; + } + if (flags & Flags.Encrypted) { + for (let i = 0; i < fileSize; i += this.blockSize) { + decrypt8(raw.subarray(i, Math.min(fileSize, i + this.blockSize)), key + i / this.blockSize); + } + } + return raw; + } else { + const numBlocks = Math.floor((fileSize + this.blockSize - 1) / this.blockSize); + const tableSize = numBlocks + 1 + ((flags & Flags.SectorCrc) ? 1 : 0); + const blocks = new Uint32Array(this.buffer, filePos, tableSize); + if (blocks.length !== tableSize) { + return; + } + if (flags & Flags.Encrypted) { + decrypt(blocks, key - 1); + } + const output = new Uint8Array(fileSize); + for (let i = 0; i < numBlocks; ++i) { + const oPos = i * this.blockSize; + const cSize = blocks[i + 1] - blocks[i]; + const uSize = Math.min(this.blockSize, fileSize - oPos); + let tmp = new Uint8Array(this.buffer, filePos + blocks[i], cSize); + if (tmp.length !== cSize) { + return; + } + if (flags & Flags.Encrypted) { + decrypt8(tmp, key + i); + } + if (flags & Flags.CompressMulti) { + return; + } else if (flags & Flags.CompressPkWare) { + tmp = pkzip_decompress(tmp, uSize); + } + if (!tmp || tmp.length !== uSize) { + return; + } + output.set(tmp, oPos); + } + return output; + } + } +} + +export default function getPlayerName(data) { + debugger; + try { + const reader = new MpqReader(data); + const hero = reader.read("hero"); + return ''; + } catch (e) { + return null; + } +}