import { bytesOf } from '../../common/bytesof' ; import { powerOfTwo } from '../../common/math' ; import { numberToUint32 } from '../../common/typecast' ; import MpqBlockTable from './blocktable' ; import { HASH_ENTRY_DELETED , HASH_ENTRY_EMPTY , MAGIC } from './constants' ; import MpqCrypto from './crypto' ; import MpqFile from './file' ; import MpqHashTable from './hashtable' ; import { searchHeader } from './isarchive' ; /**
 * MoPaQ archive (MPQ) version 0.
 */ export default class MpqArchive { headerOffset : number ; sectorSize : number ; c : MpqCrypto ; hashTable : MpqHashTable ; blockTable : MpqBlockTable ; files : MpqFile [ ] ; readonly = false ; constructor ( ) { this . headerOffset = 0 ; this . sectorSize = 4096 ; this . c = new MpqCrypto ( ) ; this . hashTable = new MpqHashTable ( this . c ) ; this . blockTable = new MpqBlockTable ( this . c ) ; this . files = [ ] ; } /**
     * Load an existing archive.
     *
     * Note that this clears the archive from whatever it had in it before.
     */ load ( buffer , readonly = false ) { const bytes = bytesOf ( buffer ) ; this . readonly = readonly ; const headerOffset = searchHeader ( bytes ) ; if ( headerOffset === - 1 ) { throw new Error ( 'No MPQ header' ) ; } const uint32array = new Uint32Array ( bytes . buffer , headerOffset , 8 ) ; const formatVersionSectorSize = uint32array [ 3 ] ; const hashPos = numberToUint32 ( uint32array [ 4 ] + headerOffset ) ; const blockPos = numberToUint32 ( uint32array [ 5 ] + headerOffset ) ; const hashSize = uint32array [ 6 ] ; let blockSize = uint32array [ 7 ] ; if ( blockSize > hashSize ) { blockSize = hashSize ; } this . headerOffset = headerOffset ; this . sectorSize = 512 * ( 1 << ( formatVersionSectorSize >>> 16 ) ) ; this . hashTable . load ( bytes . slice ( hashPos , hashPos + hashSize * 16 ) ) ; this . blockTable . load ( bytes . slice ( blockPos , blockPos + blockSize * 16 ) ) ; this . files . length = 0 ; for ( const hash of this . hashTable . entries ) { const blockIndex = hash . blockIndex ; if ( blockIndex < this . blockTable . entries . length ) { this . files [ blockIndex ] = new MpqFile ( this , hash , this . blockTable . entries [ blockIndex ] , bytes , null ) ; } } const listfile = this . get ( '(listfile)' ) ; if ( listfile ) { const list = listfile . text ( ) ; if ( list ) { for ( const name of list . split ( '\r\n' ) ) { this . get ( name ) ; } } } } /**
     * Save this archive.
     *
     * Returns null when...
     *
     *     1) The archive is in readonly mode.
     *     2) The offset of a file encrypted with FILE_OFFSET_ADJUSTED_KEY changed, and the file name is unknown.
     */ save ( ) { if ( this . readonly ) { return null ; } const headerSize = 32 ; this . delete ( '(attributes)' ) ; this . saveMemory ( ) ; this . set ( '(listfile)' , this . getFileNames ( ) . join ( '\r\n' ) ) ; let offset = headerSize ; for ( const file of this . files ) { if ( file ) { if ( ! file . offsetChanged ( offset ) ) { return null ; } file . encode ( ) ; offset += file . block . compressedSize ; } } const hashTable = this . hashTable ; const blockTable = this . blockTable ; const hashes = hashTable . entries . length ; const blocks = blockTable . entries . length ; const filesSize = offset - headerSize ; const archiveSize = headerSize + filesSize + hashes * 16 + blocks * 16 ; const hashPos = headerSize + filesSize ; const blockPos = hashPos + hashes * 16 ; const bytes = new Uint8Array ( archiveSize ) ; const uint32array = new Uint32Array ( bytes . buffer , 0 , 8 ) ; uint32array [ 0 ] = MAGIC ; uint32array [ 1 ] = headerSize ; uint32array [ 2 ] = archiveSize ; uint32array [ 3 ] = Math . log2 ( this . sectorSize / 512 ) << 16 ; uint32array [ 4 ] = hashPos ; uint32array [ 5 ] = blockPos ; uint32array [ 6 ] = hashes ; uint32array [ 7 ] = blocks ; offset = headerSize ; for ( const file of this . files ) { if ( file ) { if ( file . rawBuffer ) { bytes . set ( file . rawBuffer , offset ) ; } offset += file . block . compressedSize ; } } hashTable . save ( bytes . subarray ( offset , offset + hashTable . entries . length * 16 ) ) ; offset += hashTable . entries . length * 16 ; blockTable . save ( bytes . subarray ( offset , offset + blockTable . entries . length * 16 ) ) ; return bytes ; } /**
     * Some MPQs have empty memory chunks in them, left over from files that were deleted.
     * This function searches for such chunks, and removes them.
     *
     * Note that it is called automatically by save().
     *
     * Does nothing if the archive is in readonly mode.
     */ saveMemory ( ) { if ( this . readonly ) { return 0 ; } const blocks = this . blockTable . entries ; const hashes = this . hashTable . entries ; let i = blocks . length ; let saved = 0 ; while ( i -- ) { const block = blocks [ i ] ; if ( block . normalSize === 0 ) { this . removeBlock ( i ) ; saved += block . compressedSize ; } else { let used = false ; for ( const hash of hashes ) { if ( hash . blockIndex === i ) { used = true ; break ; } } if ( ! used ) { this . removeBlock ( i ) ; saved += block . compressedSize ; } } } return saved ; } removeBlock ( blockIndex ) { for ( const hash of this . hashTable . entries ) { if ( hash . blockIndex < HASH_ENTRY_DELETED && hash . blockIndex > blockIndex ) { hash . blockIndex -= 1 ; } } this . blockTable . entries . splice ( blockIndex , 1 ) ; } /**
     * Gets a list of the file names in the archive.
     *
     * Note that files loaded from an existing archive, without resolved names, will be named FileXXXXXXXX.
     */ getFileNames ( ) { const names = [ ] ; for ( const file of this . files ) { if ( file && file . name !== '' ) { names . push ( file . name ) ; } } return names ; } /**
     * Count the files with unresolved names.
     */ countUnresolved ( ) { let unresolved = 0 ; for ( const file of this . files ) { if ( ! file . nameResolved ) { unresolved ++ ; } } return unresolved ; } /**
     * Given an iterable of file names, attempt to resolve the archive files with them.
     */ applyListfile ( listfile ) { for ( const file of listfile ) { this . get ( file ) ; } } /**
     * Adds a file to this archive.
     * If the file already exists, its buffer will be set.
     *
     * Does nothing if the archive is in readonly mode.
     */ set ( name , buffer ) { if ( this . readonly ) { return false ; } const bytes = bytesOf ( buffer ) ; let file = this . get ( name ) ; if ( file ) { file . set ( bytes ) ; } else { const blockIndex = this . blockTable . entries . length ; const hash = this . hashTable . add ( name , blockIndex ) ; if ( ! hash ) { return false ; } const block = this . blockTable . add ( bytes ) ; file = new MpqFile ( this , hash , block , null , bytes ) ; file . name = name ; file . nameResolved = true ; this . files [ blockIndex ] = file ; } return true ; } /**
     * Gets a file from this archive.
     * If the file doesn't exist, null is returned.
     */ get ( name ) { const hash = this . hashTable . get ( name ) ; if ( hash ) { const blockIndex = hash . blockIndex ; if ( blockIndex < HASH_ENTRY_DELETED ) { const file = this . files [ blockIndex ] ; if ( file ) { file . name = name ; file . nameResolved = true ; return file ; } } } return null ; } /**
     * Checks if a file exists.
     *
     * Prefer to use get() if you are going to use get() afterwards anyway.
     */ has ( name ) { return ! ! this . get ( name ) ; } /**
     * Deletes a file from this archive.
     *
     * Does nothing if...
     *
     *     1) The archive is in readonly mode.
     *     2) The file does not exist.
     */ delete ( name ) { if ( this . readonly ) { return false ; } const file = this . get ( name ) ; if ( ! file ) { return false ; } file . delete ( ) ; return true ; } /**
     * Renames a file.
     *
     * Does nothing if...
     *
     *     1) The archive is in readonly mode.
     *     2) The file does not exist.
     *
     * 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.
     */ rename ( name , newName ) { if ( this . readonly ) { return false ; } const file = this . get ( name ) ; if ( ! file ) { return false ; } file . rename ( newName ) ; return true ; } /**
     * Resizes the hashtable to the nearest power of two equal to or bigger than the given size.
     *
     * Generally speaking, the bigger the hashtable is, the quicker insertions/searches are, at the cost of added memory.
     *
     * Does nothing if...
     *
     *     1) The archive is in readonly mode.
     *     2) The calculated size is smaller than the amount of files in the archive.
     *     3) Not all of the file names in the archive are resolved.
     */ resizeHashtable ( size ) { if ( this . readonly ) { return false ; } size = Math . max ( 4 , powerOfTwo ( size ) ) ; const files = this . files ; if ( files . length > size ) { return false ; } for ( const file of files ) { if ( ! file . nameResolved ) { return false ; } } const hashTable = this . hashTable ; const entries = hashTable . entries ; const oldEntries = entries . slice ( ) ; hashTable . clear ( ) ; hashTable . addEmpties ( size ) ; for ( const hash of oldEntries ) { if ( hash . blockIndex !== HASH_ENTRY_EMPTY ) { const file = files [ hash . blockIndex ] ; const insertionIndex = hashTable . getInsertionIndex ( file . name ) ; entries [ insertionIndex ] . copy ( hash ) ; } } return true ; } }