Class: MPQ::Archive

Inherits:
Object
  • Object
show all
Defined in:
lib/mpq.rb

Direct Known Subclasses

SC2ReplayFile

Instance Method Summary collapse

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