import { deflate , inflate } from 'pako' ; import { decodeUtf8 } from '../../common/utf8' ; import MpqArchive from './archive' ; import MpqBlock from './block' ; import { COMPRESSION_ADPCM_MONO , COMPRESSION_ADPCM_STEREO , COMPRESSION_BZIP2 , COMPRESSION_DEFLATE , COMPRESSION_HUFFMAN , COMPRESSION_IMPLODE , FILE_COMPRESSED , FILE_ENCRYPTED , FILE_EXISTS , FILE_IMPLODE , FILE_OFFSET_ADJUSTED_KEY , FILE_SINGLE_UNIT , HASH_ENTRY_DELETED } from './constants' ; import MpqCrypto from './crypto' ; import explode from './explode' ; import MpqHash from './hash' ; import { isArchive } from './isarchive' ; /**
 * A MPQ file.
 */ export default class MpqFile { archive : MpqArchive ; c : MpqCrypto ; name : string ; nameResolved : boolean ; hash : MpqHash ; block : MpqBlock ; rawBuffer : Uint8Array | null = null ; buffer : Uint8Array | null = null ; constructor ( archive , hash , block , rawBuffer , buffer ) { 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 ( ) { if ( this . buffer === null ) { this . decode ( ) ; } return < Uint8Array > 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 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 ; hash . locale = 0 ; hash . platform = 0 ; 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 < 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 ; hash . delete ( ) ; const newHash = < MpqHash > 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 ; if ( flags === FILE_EXISTS ) { this . buffer = data . slice ( 0 , block . normalSize ) ; } else if ( flags & FILE_SINGLE_UNIT ) { let sector ; if ( flags & FILE_ENCRYPTED ) { sector = c . decryptBlock ( data . slice ( 0 , block . compressedSize ) , encryptionKey ) ; } else { sector = data . subarray ( 0 , block . compressedSize ) ; } if ( flags & FILE_COMPRESSED ) { sector = this . decompressSector ( sector , block . normalSize ) ; } else { sector = sector . slice ( ) ; } this . buffer = sector ; } else { const sectorCount = Math . ceil ( block . normalSize / archive . sectorSize ) ; const buffer = new Uint8Array ( block . normalSize ) ; let sectorOffsets = new Uint32Array ( data . buffer , 0 , sectorCount + 1 ) ; if ( flags & 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 ( flags & FILE_ENCRYPTED ) { sector = c . decryptBlock ( data . slice ( start , end ) , encryptionKey + i ) ; } else { sector = data . subarray ( start , end ) ; } if ( flags & FILE_COMPRESSED ) { let uncompressedSize = archive . sectorSize ; if ( block . normalSize - offset < uncompressedSize ) { uncompressedSize = block . normalSize - offset ; } sector = this . decompressSector ( sector , uncompressedSize ) ; } if ( flags & FILE_IMPLODE ) { sector = explode ( sector ) ; } buffer . set ( sector , offset ) ; offset += sector . byteLength ; if ( i < sectorCount ) { start = end ; end = sectorOffsets [ i + 2 ] ; } } this . buffer = buffer ; } if ( archive . readonly ) { this . rawBuffer = null ; } } decompressSector ( bytes , decompressedSize ) { if ( bytes . byteLength === decompressedSize ) { return bytes ; } else { const compressionMask = bytes [ 0 ] ; if ( compressionMask & COMPRESSION_BZIP2 ) { throw new Error ( `File ${ this . name }: compression type 'bzip2' not supported` ) ; } if ( compressionMask & COMPRESSION_IMPLODE ) { try { bytes = explode ( bytes . subarray ( 1 ) ) ; } catch ( e ) { throw new Error ( `File ${ this . name }: failed to decompress with 'explode': ${ e }` ) ; } } if ( compressionMask & COMPRESSION_DEFLATE ) { try { bytes = inflate ( bytes . subarray ( 1 ) ) ; } catch ( e ) { throw new Error ( `File ${ this . name }: failed to decompress with 'zlib': ${ e }` ) ; } } if ( compressionMask & COMPRESSION_HUFFMAN ) { throw new Error ( `File ${ this . name }: compression type 'huffman' not supported` ) ; } if ( compressionMask & COMPRESSION_ADPCM_STEREO ) { throw new Error ( `File ${ this . name }: compression type 'adpcm stereo' not supported` ) ; } if ( compressionMask & 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 ( isArchive ( data ) ) { this . rawBuffer = this . buffer ; this . block . compressedSize = this . buffer . byteLength ; this . block . flags = 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 = [ ] ; 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 = deflate ( sector ) ; let isCompressed = false ; 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 ; } if ( offset < data . byteLength ) { const rawBuffer = new Uint8Array ( offset ) ; rawBuffer . set ( new Uint8Array ( offsets . buffer ) ) ; offset = offsets . byteLength ; for ( let i = 0 ; i < sectorCount ; i ++ ) { if ( compression [ i ] ) { rawBuffer [ offset ] = 2 ; offset += 1 ; } const sector = sectors [ i ] ; rawBuffer . set ( sector , offset ) ; offset += sector . byteLength ; } this . rawBuffer = rawBuffer ; this . block . compressedSize = rawBuffer . byteLength ; this . block . flags = ( FILE_EXISTS | FILE_COMPRESSED ) >>> 0 ; } else { this . rawBuffer = this . buffer ; this . block . compressedSize = this . buffer . byteLength ; this . block . flags = 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 & FILE_SINGLE_UNIT ) { c . decryptBlock ( bytes , encryptionKey ) ; c . encryptBlock ( bytes , newEncryptionKey ) ; } else { const sectorCount = Math . ceil ( block . normalSize / archive . sectorSize ) ; const sectorOffsets = new Uint32Array ( bytes . buffer , 0 , sectorCount + 1 ) ; 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 ) ; c . decryptBlock ( sector , encryptionKey + i ) ; c . encryptBlock ( sector , newEncryptionKey + i ) ; if ( i < sectorCount ) { start = end ; end = sectorOffsets [ i + 2 ] ; } } 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 & FILE_OFFSET_ADJUSTED_KEY ) { if ( this . nameResolved ) { return this . reEncrypt ( offset ) ; } return false ; } block . offset = offset ; return true ; } }