vpk 0.0.1
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/.gitignore +18 -0
- data/Gemfile +5 -0
- data/README.md +5 -0
- data/Rakefile +2 -0
- data/lib/vpk.rb +324 -0
- data/lib/vpk/version.rb +3 -0
- data/vpk.gemspec +17 -0
- metadata +52 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
data/Rakefile
ADDED
data/lib/vpk.rb
ADDED
@@ -0,0 +1,324 @@
|
|
1
|
+
require "vpk/version"
|
2
|
+
require 'logger'
|
3
|
+
require 'zlib'
|
4
|
+
require 'find'
|
5
|
+
require 'stringio'
|
6
|
+
|
7
|
+
module VPK
|
8
|
+
class VPKHeader
|
9
|
+
attr_accessor :signature
|
10
|
+
attr_accessor :version
|
11
|
+
attr_accessor :directory_length
|
12
|
+
|
13
|
+
VPK_SIGNATURE = 0x55aa1234
|
14
|
+
VERSION = 1
|
15
|
+
end
|
16
|
+
|
17
|
+
class VPKDirectoryEntry
|
18
|
+
attr_accessor :extension
|
19
|
+
attr_accessor :path
|
20
|
+
attr_accessor :file
|
21
|
+
|
22
|
+
attr_accessor :crc
|
23
|
+
attr_accessor :preload_bytes
|
24
|
+
attr_accessor :archive_index
|
25
|
+
attr_accessor :entry_offset
|
26
|
+
attr_accessor :entry_length
|
27
|
+
attr_accessor :terminator
|
28
|
+
|
29
|
+
attr_accessor :payload
|
30
|
+
|
31
|
+
def self.load_from(path)
|
32
|
+
entry = VPKDirectoryEntry.new
|
33
|
+
entry.payload = File.read(path)
|
34
|
+
entry.full_path = path
|
35
|
+
entry
|
36
|
+
end
|
37
|
+
|
38
|
+
def full_path
|
39
|
+
if path == " "
|
40
|
+
"#{file}.#{extension}"
|
41
|
+
else
|
42
|
+
"#{path}/#{file}.#{extension}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def full_path=(path)
|
47
|
+
(path, file) = File.split(path)
|
48
|
+
ext = File.extname(file)
|
49
|
+
|
50
|
+
@path = path.gsub(/^\.\.?\//, "")
|
51
|
+
@extension = ext.gsub(/^./, "")
|
52
|
+
@file = File.basename(file, ext)
|
53
|
+
end
|
54
|
+
|
55
|
+
def read_payload(io, end_of_header = 0)
|
56
|
+
io.seek(end_of_header + @entry_offset)
|
57
|
+
@payload = io.read(@entry_length + @preload_bytes)
|
58
|
+
end
|
59
|
+
|
60
|
+
def valid?
|
61
|
+
@crc == Zlib.crc32(@payload)
|
62
|
+
end
|
63
|
+
|
64
|
+
def mkdir_p
|
65
|
+
FileUtils.mkdir_p(@path)
|
66
|
+
end
|
67
|
+
|
68
|
+
def write_to_file(bath_path = nil)
|
69
|
+
File.open(full_path, "wb") do |file|
|
70
|
+
file.write @payload
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class VPKFile
|
76
|
+
attr_accessor :header
|
77
|
+
attr_accessor :dir_entries
|
78
|
+
|
79
|
+
@@logger = Logger.new(nil)
|
80
|
+
def self.logger=(new_logger)
|
81
|
+
@@logger = new_logger
|
82
|
+
end
|
83
|
+
|
84
|
+
def initialize(path=nil)
|
85
|
+
return if path.nil?
|
86
|
+
|
87
|
+
@@logger.info "extract vpk file #{path.inspect}"
|
88
|
+
File.open(path, "rb") do |io|
|
89
|
+
@header = VPKHeader.new
|
90
|
+
@header.signature = io.read(4).unpack("V*").first
|
91
|
+
@header.version = io.read(4).unpack("V*").first
|
92
|
+
@header.directory_length = io.read(4).unpack("V*").first
|
93
|
+
@@logger.info "reading header: #{@header.inspect}"
|
94
|
+
end_of_header = io.pos
|
95
|
+
|
96
|
+
unless @header.signature == "0x55aa1234".hex
|
97
|
+
@@logger.error "signature is invalid: #{@header.signature.inspect}"
|
98
|
+
raise VPKFileFormatError
|
99
|
+
end
|
100
|
+
|
101
|
+
@@logger.info "signature is valid"
|
102
|
+
|
103
|
+
@dir_entries = read_directory(io)
|
104
|
+
|
105
|
+
@dir_entries.each do |entry|
|
106
|
+
@@logger.info "try to read #{entry.full_path} (#{entry.entry_length} bytes)"
|
107
|
+
entry.read_payload(io, end_of_header + @header.directory_length)
|
108
|
+
unless entry.valid?
|
109
|
+
@@logger.error "crc is invalid: #{entry.crc.inspect}"
|
110
|
+
raise VPKFileInvalidCRCError
|
111
|
+
end
|
112
|
+
@@logger.info "done"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def self.open(path, &block)
|
118
|
+
file = self.new(path)
|
119
|
+
block.call file
|
120
|
+
end
|
121
|
+
|
122
|
+
def self.entries(path, &block)
|
123
|
+
|
124
|
+
file = self.new(path)
|
125
|
+
file.dir_entries
|
126
|
+
end
|
127
|
+
|
128
|
+
def to_file_struct_tree
|
129
|
+
map = {}
|
130
|
+
@dir_entries.each{ |entry|
|
131
|
+
map[entry.extension] ||= {}
|
132
|
+
map[entry.extension][entry.path] ||= []
|
133
|
+
map[entry.extension][entry.path] << entry
|
134
|
+
}
|
135
|
+
map
|
136
|
+
end
|
137
|
+
|
138
|
+
def to_blob
|
139
|
+
@header.directory_length = calc_directory_length
|
140
|
+
@@logger.debug "calculated directory length: #{@header.directory_length}"
|
141
|
+
|
142
|
+
StringIO.open("") do |io|
|
143
|
+
io.write([@header.signature].pack("I*"))
|
144
|
+
io.write([@header.version].pack("I*"))
|
145
|
+
io.write([@header.directory_length].pack("I*"))
|
146
|
+
|
147
|
+
write_directory(io)
|
148
|
+
|
149
|
+
io.rewind
|
150
|
+
return io.read
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def extract_to(base_dir)
|
155
|
+
@dir_entries.each{ |entry|
|
156
|
+
unless entry.path == " "
|
157
|
+
path = File.join(base_dir, entry.path)
|
158
|
+
FileUtils.mkdir_p path
|
159
|
+
end
|
160
|
+
file_path = File.join(base_dir, entry.full_path)
|
161
|
+
File.write file_path, entry.payload
|
162
|
+
}
|
163
|
+
end
|
164
|
+
|
165
|
+
def self.archive(target_dir)
|
166
|
+
vpk = VPKFile.new
|
167
|
+
vpk.header = VPKHeader.new
|
168
|
+
vpk.header.signature = VPKHeader::VPK_SIGNATURE
|
169
|
+
vpk.header.version = VPKHeader::VERSION
|
170
|
+
vpk.header.directory_length = nil
|
171
|
+
vpk.dir_entries = []
|
172
|
+
|
173
|
+
Find.find(target_dir){ |f|
|
174
|
+
next if File.directory?(f)
|
175
|
+
entry = VPKDirectoryEntry.new
|
176
|
+
entry.full_path = f.gsub(/^#{target_dir}(\/)?/, "")
|
177
|
+
entry.payload = File.read(f)
|
178
|
+
vpk.dir_entries << entry
|
179
|
+
}
|
180
|
+
vpk
|
181
|
+
end
|
182
|
+
|
183
|
+
def write_to(path)
|
184
|
+
File.write path, to_blob
|
185
|
+
end
|
186
|
+
|
187
|
+
def to_s
|
188
|
+
"#<VPKFile: #{self.object_id} @header=#{@header.inspect} @files=#{self.dir_entries.size}>"
|
189
|
+
end
|
190
|
+
|
191
|
+
private
|
192
|
+
def calc_directory_length
|
193
|
+
len = 0
|
194
|
+
to_file_struct_tree.each{ |extension, path_map|
|
195
|
+
len += extension.size + 1
|
196
|
+
path_map.each{ |path, entries|
|
197
|
+
len += path.size + 1
|
198
|
+
entries.each{ |entry|
|
199
|
+
len += entry.file.size + 1
|
200
|
+
len += 18 #preload data分
|
201
|
+
}
|
202
|
+
len += 1
|
203
|
+
}
|
204
|
+
len += 1
|
205
|
+
}
|
206
|
+
len += 1
|
207
|
+
end
|
208
|
+
|
209
|
+
def read_string(io)
|
210
|
+
string = ""
|
211
|
+
while true
|
212
|
+
b = io.readbyte
|
213
|
+
if b == 0
|
214
|
+
return string
|
215
|
+
end
|
216
|
+
string += b.chr
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# write null terminate string
|
221
|
+
def write_string(io, string)
|
222
|
+
io.write string
|
223
|
+
io.write "\x00"
|
224
|
+
end
|
225
|
+
|
226
|
+
def read_directory(io)
|
227
|
+
dir_entries = []
|
228
|
+
while true
|
229
|
+
extension = read_string(io)
|
230
|
+
@@logger.debug "extension = #{extension.inspect}"
|
231
|
+
break if extension == ""
|
232
|
+
|
233
|
+
while true
|
234
|
+
path = read_string(io)
|
235
|
+
@@logger.debug "path = #{path.inspect}"
|
236
|
+
break if path == ""
|
237
|
+
|
238
|
+
while true
|
239
|
+
file = read_string(io)
|
240
|
+
@@logger.debug "file = #{file.inspect}"
|
241
|
+
break if file == ""
|
242
|
+
|
243
|
+
dir_entry = read_file_info_and_preload_data(io)
|
244
|
+
dir_entry.extension = extension
|
245
|
+
dir_entry.path = path
|
246
|
+
dir_entry.file = file
|
247
|
+
dir_entries << dir_entry
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
dir_entries
|
252
|
+
end
|
253
|
+
|
254
|
+
def write_directory(io)
|
255
|
+
end_of_header = io.pos
|
256
|
+
|
257
|
+
total_offset = 0
|
258
|
+
to_file_struct_tree.each{ |extension, path_map|
|
259
|
+
write_string(io, extension)
|
260
|
+
path_map.each{ |path, entries|
|
261
|
+
@@logger.debug "write path: #{path.inspect}"
|
262
|
+
write_string(io, path)
|
263
|
+
entries.each{ |entry|
|
264
|
+
write_string(io, entry.file)
|
265
|
+
|
266
|
+
entry.archive_index = 0x7fff
|
267
|
+
entry.terminator = 0xffff
|
268
|
+
entry.preload_bytes = 0
|
269
|
+
entry.entry_length = entry.payload.size
|
270
|
+
entry.entry_offset = total_offset
|
271
|
+
total_offset += entry.entry_length
|
272
|
+
write_file_info_and_preload(io, entry)
|
273
|
+
|
274
|
+
pos = io.pos
|
275
|
+
io.seek(end_of_header + @header.directory_length + entry.entry_offset)
|
276
|
+
io.write(entry.payload)
|
277
|
+
io.seek(pos)
|
278
|
+
}
|
279
|
+
io.write "\x00"
|
280
|
+
}
|
281
|
+
io.write "\x00"
|
282
|
+
}
|
283
|
+
io.write "\x00"
|
284
|
+
end
|
285
|
+
|
286
|
+
def read_file_info_and_preload_data(io)
|
287
|
+
entry = VPKDirectoryEntry.new
|
288
|
+
entry.crc = io.read(4).unpack("V*").first
|
289
|
+
entry.preload_bytes = io.read(2).unpack("v*").first
|
290
|
+
entry.archive_index = io.read(2).unpack("v*").first
|
291
|
+
entry.entry_offset = io.read(4).unpack("v*").first
|
292
|
+
entry.entry_length = io.read(4).unpack("v*").first
|
293
|
+
entry.terminator = io.read(2).unpack("v*").first
|
294
|
+
@@logger.debug "read entry_offset: #{entry.entry_offset}, entry_length: #{entry.entry_length}"
|
295
|
+
entry
|
296
|
+
end
|
297
|
+
|
298
|
+
def write_file_info_and_preload(io, entry)
|
299
|
+
entry.crc = Zlib.crc32(entry.payload)
|
300
|
+
io.write [entry.crc].pack("i*")
|
301
|
+
io.write [entry.preload_bytes].pack("S*")
|
302
|
+
io.write [entry.archive_index].pack("S*")
|
303
|
+
io.write [entry.entry_offset].pack("I*")
|
304
|
+
io.write [entry.entry_length].pack("I*")
|
305
|
+
io.write [entry.terminator].pack("S*")
|
306
|
+
@@logger.debug "write entry_offset: #{entry.entry_offset}, entry_length: #{entry.entry_length}"
|
307
|
+
entry
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
module VPKUtil
|
312
|
+
def self.extract(in_vpk_path, output_path)
|
313
|
+
VPKFile.new(in_vpk_path).extract_to(output_path)
|
314
|
+
end
|
315
|
+
|
316
|
+
def self.archive(dir_path, out_vpk_path)
|
317
|
+
VPKFile.archive(dir_path).write_to(out_vpk_path)
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
class VPKError < StandardError; end
|
322
|
+
class VPKFileFormatError < VPKError; end
|
323
|
+
class VPKFileInvalidCRCError < VPKError; end
|
324
|
+
end
|
data/lib/vpk/version.rb
ADDED
data/vpk.gemspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/vpk/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["kimoto"]
|
6
|
+
gem.email = ["sub+peerler@gmail.com"]
|
7
|
+
gem.description = %q{VPK File Format Parser (extract and archive)}
|
8
|
+
gem.summary = %q{VPK File Format Parser (extract and archive)}
|
9
|
+
gem.homepage = "http://github.com/kimoto/vpk"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "vpk"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = VPK::VERSION
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: vpk
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- kimoto
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-07-13 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: VPK File Format Parser (extract and archive)
|
15
|
+
email:
|
16
|
+
- sub+peerler@gmail.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- .gitignore
|
22
|
+
- Gemfile
|
23
|
+
- README.md
|
24
|
+
- Rakefile
|
25
|
+
- lib/vpk.rb
|
26
|
+
- lib/vpk/version.rb
|
27
|
+
- vpk.gemspec
|
28
|
+
homepage: http://github.com/kimoto/vpk
|
29
|
+
licenses: []
|
30
|
+
post_install_message:
|
31
|
+
rdoc_options: []
|
32
|
+
require_paths:
|
33
|
+
- lib
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
35
|
+
none: false
|
36
|
+
requirements:
|
37
|
+
- - ! '>='
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
requirements: []
|
47
|
+
rubyforge_project:
|
48
|
+
rubygems_version: 1.8.24
|
49
|
+
signing_key:
|
50
|
+
specification_version: 3
|
51
|
+
summary: VPK File Format Parser (extract and archive)
|
52
|
+
test_files: []
|