UNPKG

@kobold/core

Version:

Kobold core

193 lines (136 loc) 7.42 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault"); var _Object$defineProperty = require("@babel/runtime-corejs3/core-js/object/define-property"); _Object$defineProperty(exports, "__esModule", { value: true }); exports.Category = void 0; var _concat = _interopRequireDefault(require("@babel/runtime-corejs3/core-js/instance/concat")); var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js/promise")); var _map = _interopRequireDefault(require("@babel/runtime-corejs3/core-js/instance/map")); var _lastIndexOf = _interopRequireDefault(require("@babel/runtime-corejs3/core-js/instance/last-index-of")); var _indexOf = _interopRequireDefault(require("@babel/runtime-corejs3/core-js/instance/index-of")); var _padStart = _interopRequireDefault(require("@babel/runtime-corejs3/core-js/instance/pad-start")); var _slice = _interopRequireDefault(require("@babel/runtime-corejs3/core-js/instance/slice")); var _map2 = _interopRequireDefault(require("@babel/runtime-corejs3/core-js/map")); var _defineProperty2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/defineProperty")); var _fastGlob = _interopRequireDefault(require("fast-glob")); var _fs = _interopRequireDefault(require("fs")); var _path = _interopRequireDefault(require("path")); var _util = _interopRequireDefault(require("util")); var _zlib = _interopRequireDefault(require("zlib")); var _parser = require("./parser"); var _utilities = require("./utilities"); const async = { fs: { readFile: _util.default.promisify(_fs.default.readFile), open: _util.default.promisify(_fs.default.open), close: _util.default.promisify(_fs.default.close), read: _util.default.promisify(_fs.default.read) }, zlib: { inflateRaw: _util.default.promisify(_zlib.default.inflateRaw) } }; const asyncReadBuffer = (fd, length, offset) => async.fs.read(fd, Buffer.alloc(length), 0, length, offset); var IndexType; (function (IndexType) { IndexType[IndexType["INDEX"] = 0] = "INDEX"; IndexType[IndexType["INDEX2"] = 1] = "INDEX2"; })(IndexType || (IndexType = {})); // TODO: Most of this should be broken out into CategoryChunk handling class Category { constructor(opts) { (0, _defineProperty2.default)(this, "categoryId", void 0); (0, _defineProperty2.default)(this, "repositoryPath", void 0); (0, _defineProperty2.default)(this, "indexCache", new _map2.default()); (0, _defineProperty2.default)(this, "fixHashData", data => ({ isSynonym: (data & 0b1) === 0b1, dataFileId: (data & 0b1110) >>> 1, offset: (data & ~0xf) * 8 })); this.categoryId = opts.categoryId; this.repositoryPath = opts.repositoryPath; } get idPrefix() { var _context, _context2; return (0, _slice.default)(_context = (0, _padStart.default)(_context2 = this.categoryId.toString(16)).call(_context2, 2, '0')).call(_context, 0, 2); } async buildIndexes(type) { const extension = type === IndexType.INDEX ? 'index' : 'index2'; // Find index files const indexFiles = await (0, _fastGlob.default)(`${this.idPrefix}????.*.${extension}`, { cwd: this.repositoryPath, caseSensitiveMatch: false }); (0, _utilities.assert)(indexFiles.length === 1, 'TODO: Handle multiple chunks'); const indexFile = indexFiles[0]; const platform = indexFile.substring((0, _indexOf.default)(indexFile).call(indexFile, '.') + 1, (0, _lastIndexOf.default)(indexFile).call(indexFile, '.')); (0, _utilities.assert)(platform === 'win32', `TODO: Handle other platforms. Expected 'win32', got '${platform}'.`); const indexPath = _path.default.join(this.repositoryPath, indexFile); const indexBuffer = await async.fs.readFile(indexPath); const parser = type === IndexType.INDEX ? _parser.sqPackIndexParser : _parser.sqPackIndex2Parser; const parsed = parser.parse(indexBuffer); const indexes = new _map2.default(); for (const entry of parsed.indexes) { const key = typeof entry.hash === 'bigint' ? entry.hash : BigInt(entry.hash); indexes.set(key, this.fixHashData(entry.data)); } return indexes; } // TODO: Write my own parser so this shit isn't required // Square's doing a smart thing and storing a uint32 in 28 bits, as the last 4 are always 0 and can be used for the bitfield above. getIndexes(type) { let indexPromise = this.indexCache.get(type); if (indexPromise == null) { indexPromise = this.buildIndexes(type); this.indexCache.set(type, indexPromise); } return indexPromise; } async getFileEntry(pathInfo) { var _await$this$getIndexe; // TODO: Consider if both indexes should be preloaded. As-is, we're lazy loading both, // so a theoretical lookup that's only in index2 would have two sequential lookups, not parallel const entry = (_await$this$getIndexe = (await this.getIndexes(IndexType.INDEX)).get(pathInfo.indexHash)) !== null && _await$this$getIndexe !== void 0 ? _await$this$getIndexe : (await this.getIndexes(IndexType.INDEX2)).get(pathInfo.index2Hash); return entry; } async getFile(pathInfo) { const entry = await this.getFileEntry(pathInfo); (0, _utilities.assert)(entry != null, `${pathInfo.path} not found in indexes`); (0, _utilities.assert)(!entry.isSynonym, 'TODO: Handle synonym files' + pathInfo.path); // TODO: handle multiple platforms const fd = await async.fs.open(_path.default.join(this.repositoryPath, `${this.idPrefix}0000.win32.dat${entry.dataFileId}`), 'r'); const fileInfoSize = _parser.fileInfoParser.sizeOf(); const { buffer: fileInfoBuffer } = await asyncReadBuffer(fd, fileInfoSize, entry.offset); const fileInfo = _parser.fileInfoParser.parse(fileInfoBuffer); (0, _utilities.assert)(fileInfo.type === _parser.FileType.STANDARD, 'TODO: Better handling for file types'); const blockInfoSize = _parser.blockInfoParser.sizeOf(); const blockInfoGroupSize = blockInfoSize * fileInfo.blockCount; const { buffer: blockInfoBuffer } = await asyncReadBuffer(fd, blockInfoGroupSize, entry.offset + fileInfoSize); const blockInfos = []; for (let i = 0; i < fileInfo.blockCount; i++) { const begin = blockInfoSize * i; blockInfos.push(_parser.blockInfoParser.parse(blockInfoBuffer.subarray(begin, begin + blockInfoSize))); } const blockPromises = (0, _map.default)(blockInfos).call(blockInfos, blockInfo => this.readBlock(blockInfo, fd, entry.offset + fileInfo.size)); const blocks = await _promise.default.all(blockPromises); const fileBuffer = (0, _concat.default)(Buffer).call(Buffer, blocks, fileInfo.rawFileSize); // TODO: Cache FDs? // Close the FD - no await as timing of close is irrelevant async.fs.close(fd); return fileBuffer; } async readBlock(blockInfo, fd, baseOffset) { const { buffer: blockBuffer } = await asyncReadBuffer(fd, blockInfo.size, baseOffset + blockInfo.offset); const blockHeader = _parser.blockHeaderParser.parse(blockBuffer); const blockData = blockBuffer.subarray(blockHeader.size, blockHeader.size + blockHeader.compressedSize); // If it's compressed, handle it as such if (blockHeader.compressedSize !== 32000) { return async.zlib.inflateRaw(blockData); } return _promise.default.resolve(blockData); } } exports.Category = Category;