mpq 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+ gem "bindata", "~> 1.3.1"
6
+ gem "bzip2-ruby", "~> 0.2.7"
7
+
8
+ # Add dependencies to develop your gem here.
9
+ # Include everything needed to run rake, tests, features, etc.
10
+ group :development do
11
+ gem "bundler", "~> 1.0.0"
12
+ gem "jeweler", "~> 1.5.2"
13
+ gem "rocco", "~> 0.6"
14
+ end
@@ -0,0 +1,26 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ bindata (1.3.1)
5
+ bzip2-ruby (0.2.7)
6
+ git (1.2.5)
7
+ jeweler (1.5.2)
8
+ bundler (~> 1.0.0)
9
+ git (>= 1.2.5)
10
+ rake
11
+ mustache (0.99.3)
12
+ rake (0.8.7)
13
+ rdiscount (1.6.8)
14
+ rocco (0.6)
15
+ mustache
16
+ rdiscount
17
+
18
+ PLATFORMS
19
+ ruby
20
+
21
+ DEPENDENCIES
22
+ bindata (~> 1.3.1)
23
+ bundler (~> 1.0.0)
24
+ bzip2-ruby (~> 0.2.7)
25
+ jeweler (~> 1.5.2)
26
+ rocco (~> 0.6)
@@ -0,0 +1,13 @@
1
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
2
+ Version 2, December 2004
3
+
4
+ Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
5
+
6
+ Everyone is permitted to copy and distribute verbatim or modified
7
+ copies of this license document, and changing it is allowed as long
8
+ as the name is changed.
9
+
10
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
11
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
12
+
13
+ 0. You just DO WHAT THE FUCK YOU WANT TO.
@@ -0,0 +1,6 @@
1
+ mpq
2
+ ===
3
+
4
+ Read files and metadata from MPQ archives.
5
+
6
+ This code has no restrictions whatsoever. See `LICENSE.txt` for information.
@@ -0,0 +1,43 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'rake'
11
+
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |gem|
14
+ # gem is a Gem::Specification... see
15
+ # http://docs.rubygems.org/read/chapter/20 for more options
16
+ gem.name = "mpq"
17
+ gem.homepage = "http://github.com/nolanw/mpq"
18
+ gem.license = "WTFPL"
19
+ gem.summary = %Q{Read files and metadata from MPQ archives}
20
+ gem.description = %Q{Read files and metadata from MPQ archives}
21
+ gem.email = "nolan@nolanw.ca"
22
+ gem.authors = ["Nolan Waite"]
23
+ # Include your dependencies below. Runtime dependencies are required when
24
+ # using your gem, and development dependencies are only needed for
25
+ # development (ie running rake tasks, tests, etc)
26
+ gem.add_runtime_dependency 'bindata', '>= 1.3.1'
27
+ gem.add_development_dependency 'rocco', '>= 0.6'
28
+ end
29
+ Jeweler::RubygemsDotOrgTasks.new
30
+
31
+ require 'rake/testtask'
32
+ Rake::TestTask.new(:test) do |test|
33
+ test.libs << 'lib' << 'test'
34
+ test.pattern = 'test/**/test_*.rb'
35
+ test.verbose = true
36
+ end
37
+
38
+ desc "Prepare the mpq documentation"
39
+ task :docs do
40
+ system("cd lib && rocco -o ../docs *.rb")
41
+ end
42
+
43
+ task :default => :test
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,238 @@
1
+ # Read files and metadata from MPQ archives.
2
+ #
3
+ # We'll use `bindata` as a DSL for binary extraction, and since we only care
4
+ # about StarCraft 2 replays at the moment, the only decompression we need is
5
+ # `bzip2`.
6
+ require 'bindata'
7
+ require 'bzip2'
8
+
9
+ # A massive thanks to Justin Olbrantz (Quantam) and Jean-Francois Roy
10
+ # (BahamutZERO), whose [documentation of the MPQ
11
+ # format](http://wiki.devklog.net/index.php?title=The_MoPaQ_Archive_Format) was
12
+ # instrumental for this implementation.
13
+ #
14
+ # Thanks to Aku Kotkavuo (arkx) for [mpyq](https://github.com/arkx/mpyq), which
15
+ # clarified a bunch of the implementation details that I couldn't distill from
16
+ # the documentation mentioned above.
17
+ module MPQ
18
+ class Archive
19
+
20
+ # In general, MPQ archives start with either the MPQ header, or they start
21
+ # with a user header which points to the MPQ header. StarCraft 2 replays
22
+ # always have a user header, so we don't even bother to check here.
23
+ #
24
+ # The MPQ header points to two very helpful parts of the MPQ archive: the
25
+ # hash table, which tells us where the contents of files are found, and the
26
+ # block table, which holds said contents of files. That's all we need to
27
+ # read up front.
28
+ def initialize io
29
+ @io = io
30
+ @user_header = UserHeader.read @io
31
+ @io.seek @user_header.archive_header_offset
32
+ @archive_header = ArchiveHeader.read @io
33
+ @hash_table = read_table :hash
34
+ @block_table = read_table :block
35
+ end
36
+
37
+ # Both the hash and block tables' contents are hashed (in the same way), so
38
+ # we need to decrypt them in order to read their contents.
39
+ def read_table table
40
+ table_offset = @archive_header.send "#{table}_table_offset"
41
+ @io.seek @user_header.archive_header_offset + table_offset
42
+ table_entries = @archive_header.send "#{table}_table_entries"
43
+ data = @io.read table_entries * 16
44
+ key = Hashing::hash_for :table, "(#{table} table)"
45
+ data = Hashing::decrypt data, key
46
+ klass = table == :hash ? HashTableEntry : BlockTableEntry
47
+ (0...table_entries).map do |i|
48
+ klass.read(data[i * 16, 16])
49
+ end
50
+ end
51
+
52
+ # To read a file from the MPQ archive, we need to locate its blocks.
53
+ def read_file filename
54
+
55
+ # The first block location is stored in the hash table.
56
+ hash_a = Hashing::hash_for :hash_a, filename
57
+ hash_b = Hashing::hash_for :hash_b, filename
58
+ hash_entry = @hash_table.find do |h|
59
+ [h.hash_a, h.hash_b] == [hash_a, hash_b]
60
+ end
61
+ unless hash_entry
62
+ return nil
63
+ end
64
+ block_entry = @block_table[hash_entry.block_index]
65
+ unless block_entry.file?
66
+ return nil
67
+ end
68
+ @io.seek @user_header.archive_header_offset + block_entry.block_offset
69
+ file_data = @io.read block_entry.archived_size
70
+
71
+ # Blocks can be encrypted. Decryption isn't currently implemented as none
72
+ # of the blocks in a StarCraft 2 replay are encrypted.
73
+ if block_entry.encrypted?
74
+ return nil
75
+ end
76
+
77
+ # Files can consist of one or many blocks. In either case, each block
78
+ # (or *sector*) is read and individually decompressed if needed, then
79
+ # stitched together for the final result.
80
+ if block_entry.single_unit?
81
+ if block_entry.compressed?
82
+ if file_data.bytes.next == 16
83
+ file_data = Bzip2.uncompress file_data[1, file_data.length]
84
+ end
85
+ end
86
+ return file_data
87
+ end
88
+ sector_size = 512 << @archive_header.sector_size_shift
89
+ sectors = block_entry.size / sector_size + 1
90
+ if block_entry.has_checksums
91
+ sectors += 1
92
+ end
93
+ positions = file_data[0, 4 * (sectors + 1)].unpack "V#{sectors + 1}"
94
+ sectors = []
95
+ positions.each_with_index do |pos, i|
96
+ break if i + 1 == positions.length
97
+ sector = file_data[pos, positions[i + 1] - pos]
98
+ if block_entry.compressed?
99
+ if block_entry.size > block_entry.archived_size
100
+ if sector.bytes.next == 16
101
+ sector = Bzip2.uncompress sector
102
+ end
103
+ end
104
+ end
105
+ sectors << sector
106
+ end
107
+ sectors.join ''
108
+ end
109
+ end
110
+
111
+ # Various hashes are used throughout MPQ archives.
112
+ module Hashing
113
+
114
+ # The algorithm is unchanged across hash types, but the first step in the
115
+ # hashing differs depending on what we're hashing.
116
+ #
117
+ # Both this hashing and the decryption below make use of a precalculated
118
+ # table of values.
119
+ def self.hash_for hash_type, s
120
+ hash_type = [:table_offset, :hash_a, :hash_b, :table].index hash_type
121
+ seed1, seed2 = 0x7FED7FED, 0xEEEEEEEE
122
+ s.upcase.each_byte do |c|
123
+ value = @encryption_table[(hash_type << 8) + c]
124
+
125
+ # The seemingly pointless `AND`ing by 32 ones is because Ruby's numbers
126
+ # are arbitrary precision. Normally that's great, but right now that's
127
+ # actually unhelpful.
128
+ seed1 = (value ^ (seed1 + seed2)) & 0xFFFFFFFF
129
+ seed2 = (c + seed1 + seed2 + (seed2 << 5) + 3) & 0xFFFFFFFF
130
+ end
131
+ seed1
132
+ end
133
+
134
+ # Data in the hash and block tables can be decrypted using this algorithm.
135
+ def self.decrypt data, seed1
136
+ seed2 = 0xEEEEEEEE
137
+ data.unpack('V*').map do |value|
138
+
139
+ # Again, the `AND`s here forces 32-bit precision.
140
+ seed2 = (seed2 + @encryption_table[0x400 + (seed1 & 0xFF)]) & 0xFFFFFFFF
141
+ value = (value ^ (seed1 + seed2)) & 0xFFFFFFFF
142
+ seed1 = (((~seed1 << 0x15) + 0x11111111) | (seed1 >> 0x0B)) & 0xFFFFFFFF
143
+ seed2 = (value + seed2 + (seed2 << 5) + 3) & 0xFFFFFFFF
144
+ value
145
+ end.pack('V*')
146
+ end
147
+
148
+ # This table is used for the above hashing and decryption routines.
149
+ seed = 0x00100001
150
+ @encryption_table = {}
151
+ (0..255).each do |i|
152
+ index = i
153
+ (0..4).each do |j|
154
+ seed = (seed * 125 + 3) % 0x2AAAAB
155
+ tmp1 = (seed & 0xFFFF) << 0x10
156
+ seed = (seed * 125 + 3) % 0x2AAAAB
157
+ tmp2 = (seed & 0xFFFF)
158
+ @encryption_table[i + j * 0x100] = (tmp1 | tmp2)
159
+ end
160
+ end
161
+ end
162
+
163
+ # The layout of the user header, an optional part of MPQ archives. If it's
164
+ # present, it must be at the beginning of the archive.
165
+ class UserHeader < BinData::Record
166
+ endian :little
167
+
168
+ # This magic value is always the same: `MPQ\x1b`.
169
+ string :user_magic, :length => 4
170
+ uint32 :user_data_max_length
171
+ uint32 :archive_header_offset
172
+ uint32 :user_data_length
173
+ string :user_data, :length => :user_data_length
174
+ end
175
+
176
+ # All MPQ archives have an archive header. It's located at the start of the
177
+ # archive, unless a user header is there, in which case the user header
178
+ # points to the location of this archive header.
179
+ class ArchiveHeader < BinData::Record
180
+ endian :little
181
+
182
+ # This magic value is always the same: `MPQ\x1a`.
183
+ string :archive_magic, :length => 4
184
+ int32 :header_size
185
+ int32 :archive_size
186
+ int16 :format_version
187
+ int8 :sector_size_shift
188
+ int8
189
+ int32 :hash_table_offset
190
+ int32 :block_table_offset
191
+ int32 :hash_table_entries
192
+ int32 :block_table_entries
193
+ int64 :extended_block_table_offset
194
+ int16 :hash_table_offset_high
195
+ int16 :block_table_offset_high
196
+ end
197
+
198
+ # Each hash table entry follows this format. No idea what the spare byte is
199
+ # for.
200
+ class HashTableEntry < BinData::Record
201
+ endian :little
202
+
203
+ uint32 :hash_a
204
+ uint32 :hash_b
205
+ int16 :language
206
+ int8 :platform
207
+ int8
208
+ int32 :block_index
209
+ end
210
+
211
+ # Each block table follows this format. Although `BinData` can handle
212
+ # bitfields (`flags` in this case), I had problems setting them up, so I
213
+ # settled for methods instead.
214
+ class BlockTableEntry < BinData::Record
215
+ endian :little
216
+
217
+ int32 :block_offset
218
+ int32 :archived_size
219
+ int32 :file_size
220
+ uint32 :flags
221
+
222
+ def file?
223
+ (flags & 0x80000000) != 0
224
+ end
225
+
226
+ def compressed?
227
+ (flags & 0x00000200) != 0
228
+ end
229
+
230
+ def encrypted?
231
+ (flags & 0x00010000) != 0
232
+ end
233
+
234
+ def single_unit?
235
+ (flags & 0x01000000) != 0
236
+ end
237
+ end
238
+ end
@@ -0,0 +1,17 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'test/unit'
11
+
12
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
13
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
14
+ require 'mpq'
15
+
16
+ class Test::Unit::TestCase
17
+ end
Binary file
@@ -0,0 +1,16 @@
1
+ require 'helper'
2
+
3
+ class TestMPQ < Test::Unit::TestCase
4
+ def setup
5
+ @file = File.new File.join(File.dirname(__FILE__), "some.SC2Replay")
6
+ @archive = MPQ::Archive.new @file
7
+ end
8
+
9
+ def teardown
10
+ @file.close
11
+ end
12
+
13
+ def test_listfile
14
+ assert @archive.read_file("(listfile)")["replay.details"]
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,144 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mpq
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.1.0
6
+ platform: ruby
7
+ authors:
8
+ - Nolan Waite
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-05-01 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: bindata
17
+ requirement: &id001 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ~>
21
+ - !ruby/object:Gem::Version
22
+ version: 1.3.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *id001
26
+ - !ruby/object:Gem::Dependency
27
+ name: bzip2-ruby
28
+ requirement: &id002 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: 0.2.7
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: *id002
37
+ - !ruby/object:Gem::Dependency
38
+ name: bundler
39
+ requirement: &id003 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ~>
43
+ - !ruby/object:Gem::Version
44
+ version: 1.0.0
45
+ type: :development
46
+ prerelease: false
47
+ version_requirements: *id003
48
+ - !ruby/object:Gem::Dependency
49
+ name: jeweler
50
+ requirement: &id004 !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ~>
54
+ - !ruby/object:Gem::Version
55
+ version: 1.5.2
56
+ type: :development
57
+ prerelease: false
58
+ version_requirements: *id004
59
+ - !ruby/object:Gem::Dependency
60
+ name: rocco
61
+ requirement: &id005 !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ~>
65
+ - !ruby/object:Gem::Version
66
+ version: "0.6"
67
+ type: :development
68
+ prerelease: false
69
+ version_requirements: *id005
70
+ - !ruby/object:Gem::Dependency
71
+ name: bindata
72
+ requirement: &id006 !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: 1.3.1
78
+ type: :runtime
79
+ prerelease: false
80
+ version_requirements: *id006
81
+ - !ruby/object:Gem::Dependency
82
+ name: rocco
83
+ requirement: &id007 !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: "0.6"
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: *id007
92
+ description: Read files and metadata from MPQ archives
93
+ email: nolan@nolanw.ca
94
+ executables: []
95
+
96
+ extensions: []
97
+
98
+ extra_rdoc_files:
99
+ - LICENSE.txt
100
+ - README.md
101
+ files:
102
+ - Gemfile
103
+ - Gemfile.lock
104
+ - LICENSE.txt
105
+ - README.md
106
+ - Rakefile
107
+ - VERSION
108
+ - lib/mpq.rb
109
+ - test/helper.rb
110
+ - test/some.SC2Replay
111
+ - test/test_mpq.rb
112
+ homepage: http://github.com/nolanw/mpq
113
+ licenses:
114
+ - WTFPL
115
+ post_install_message:
116
+ rdoc_options: []
117
+
118
+ require_paths:
119
+ - lib
120
+ required_ruby_version: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ hash: -4446640813622028280
126
+ segments:
127
+ - 0
128
+ version: "0"
129
+ required_rubygems_version: !ruby/object:Gem::Requirement
130
+ none: false
131
+ requirements:
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ version: "0"
135
+ requirements: []
136
+
137
+ rubyforge_project:
138
+ rubygems_version: 1.7.2
139
+ signing_key:
140
+ specification_version: 3
141
+ summary: Read files and metadata from MPQ archives
142
+ test_files:
143
+ - test/helper.rb
144
+ - test/test_mpq.rb