Class: MPQ::Archive
- Inherits:
-
Object
- Object
- MPQ::Archive
- Defined in:
- lib/mpq.rb
Direct Known Subclasses
Instance Method Summary collapse
-
#initialize(io) ⇒ Archive
constructor
In general, MPQ archives start with either the MPQ header, or they start with a user header which points to the MPQ header.
-
#read_file(filename) ⇒ Object
To read a file from the MPQ archive, we need to locate its blocks.
-
#read_table(table) ⇒ Object
Both the hash and block tables’ contents are hashed (in the same way), so we need to decrypt them in order to read their contents.
Constructor Details
#initialize(io) ⇒ Archive
In general, MPQ archives start with either the MPQ header, or they start with a user header which points to the MPQ header. StarCraft 2 replays always have a user header, so we don’t even bother to check here.
The MPQ header points to two very helpful parts of the MPQ archive: the hash table, which tells us where the contents of files are found, and the block table, which holds said contents of files. That’s all we need to read up front.
28 29 30 31 32 33 34 35 |
# File 'lib/mpq.rb', line 28 def initialize io @io = io @user_header = UserHeader.read @io @io.seek @user_header.archive_header_offset @archive_header = ArchiveHeader.read @io @hash_table = read_table :hash @block_table = read_table :block end |
Instance Method Details
#read_file(filename) ⇒ Object
To read a file from the MPQ archive, we need to locate its blocks.
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
# File 'lib/mpq.rb', line 53 def read_file filename # The first block location is stored in the hash table. hash_a = Hashing::hash_for :hash_a, filename hash_b = Hashing::hash_for :hash_b, filename hash_entry = @hash_table.find do |h| [h.hash_a, h.hash_b] == [hash_a, hash_b] end unless hash_entry return nil end block_entry = @block_table[hash_entry.block_index] unless block_entry.file? return nil end @io.seek @user_header.archive_header_offset + block_entry.block_offset file_data = @io.read block_entry.archived_size # Blocks can be encrypted. Decryption isn't currently implemented as none # of the blocks in a StarCraft 2 replay are encrypted. if block_entry.encrypted? return nil end # Files can consist of one or many blocks. In either case, each block # (or *sector*) is read and individually decompressed if needed, then # stitched together for the final result. if block_entry.single_unit? if block_entry.compressed? if file_data.bytes.next == 16 file_data = Bzip2.uncompress file_data[1, file_data.length] end end return file_data end sector_size = 512 << @archive_header.sector_size_shift sectors = block_entry.size / sector_size + 1 if block_entry.has_checksums sectors += 1 end positions = file_data[0, 4 * (sectors + 1)].unpack "V#{sectors + 1}" sectors = [] positions.each_with_index do |pos, i| break if i + 1 == positions.length sector = file_data[pos, positions[i + 1] - pos] if block_entry.compressed? if block_entry.size > block_entry.archived_size if sector.bytes.next == 16 sector = Bzip2.uncompress sector end end end sectors << sector end sectors.join '' end |
#read_table(table) ⇒ Object
Both the hash and block tables’ contents are hashed (in the same way), so we need to decrypt them in order to read their contents.
39 40 41 42 43 44 45 46 47 48 49 50 |
# File 'lib/mpq.rb', line 39 def read_table table table_offset = @archive_header.send "#{table}_table_offset" @io.seek @user_header.archive_header_offset + table_offset table_entries = @archive_header.send "#{table}_table_entries" data = @io.read table_entries * 16 key = Hashing::hash_for :table, "(#{table} table)" data = Hashing::decrypt data, key klass = table == :hash ? HashTableEntry : BlockTableEntry (0...table_entries).map do |i| klass.read(data[i * 16, 16]) end end |