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 +14 -0
- data/Gemfile.lock +26 -0
- data/LICENSE.txt +13 -0
- data/README.md +6 -0
- data/Rakefile +43 -0
- data/VERSION +1 -0
- data/lib/mpq.rb +238 -0
- data/test/helper.rb +17 -0
- data/test/some.SC2Replay +0 -0
- data/test/test_mpq.rb +16 -0
- metadata +144 -0
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
|
data/Gemfile.lock
ADDED
@@ -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)
|
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
data/Rakefile
ADDED
@@ -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
|
data/lib/mpq.rb
ADDED
@@ -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
|
data/test/helper.rb
ADDED
@@ -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
|
data/test/some.SC2Replay
ADDED
Binary file
|
data/test/test_mpq.rb
ADDED
@@ -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
|