nbtfile 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 MenTaLguY
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,140 @@
1
+ = nbtfile
2
+
3
+ NBTFile is a low-level library for reading and writing
4
+ NBT-format files, as used by the popular game Minecraft.
5
+
6
+ == Official Specification
7
+
8
+ The official (if somewhat confusing) specification for
9
+ the NBT file format may be found at
10
+ http://www.minecraft.net/docs/NBT.txt
11
+
12
+ == Data Model
13
+
14
+ The NBT data model has ten different data types:
15
+
16
+ - 8-bit signed integers (Tag_Byte)
17
+ - 16-bit signed integers (Tag_Short)
18
+ - 32-bit signed integers (Tag_Int)
19
+ - 64-bit signed integers (Tag_Long)
20
+ - 32-bit floating-point numbers (Tag_Float)
21
+ - 64-bit floating-point numbers (Tag_Double)
22
+ - UTF-8 strings (Tag_String)
23
+ - raw byte strings (Tag_Byte_Array)
24
+ - homogenous lists (Tag_List)
25
+ - compound structures (Tag_Compound)
26
+
27
+ Compound structures (Tag_Compound) are unordered
28
+ collections where each item ("tag") has a name associated
29
+ with it. Compound structures are heterogenous; the
30
+ elements of a compound structure may be any mixture of
31
+ types.
32
+
33
+ Lists (Tag_List) are ordered collections of unnamed items.
34
+ Lists are homogenous; every element of a particular list
35
+ must have the same type. Note that all lists have the
36
+ same type (Tag_List) regardless of the type of their
37
+ elements.
38
+
39
+ Items of an eleventh "type" (Tag_End) serve to terminate
40
+ compound structures and lists (of any type). The wording
41
+ of the official specification could permit lists of
42
+ Tag_End, but this is not supported in practice.
43
+
44
+ The top level of an NBT file must be a single-element
45
+ compound structure.
46
+
47
+ == Syntax
48
+
49
+ NBT files are gzip-compressed; the structure of the
50
+ uncompressed data can be described by the following
51
+ ABNF (see RFC 5234).
52
+
53
+ nbt-data = tag-compound
54
+
55
+ tag-compound = TAG-COMPOUND name compound-body
56
+
57
+ compound-body = *tag TAG-END
58
+
59
+ tag = TAG-BYTE name byte /
60
+ TAG-SHORT name short /
61
+ TAG-INT name int /
62
+ TAG-LONG name long /
63
+ TAG-FLOAT name float /
64
+ TAG-DOUBLE name double /
65
+ TAG-STRING name string /
66
+ TAG-BYTE-ARRAY name byte-array /
67
+ TAG-LIST name list /
68
+ tag-compound
69
+
70
+ name = string
71
+
72
+ list = TAG-BYTE list-length *byte /
73
+ TAG-SHORT list-length *short /
74
+ TAG-INT list-length *int /
75
+ TAG-LONG list-length *long /
76
+ TAG-FLOAT list-length *float /
77
+ TAG-DOUBLE list-length *double /
78
+ TAG-STRING list-length *string /
79
+ TAG-BYTE-ARRAY list-length *byte-array /
80
+ TAG-LIST list-length *list /
81
+ TAG-COMPOUND list-length *compound-body
82
+
83
+ list-length = int
84
+
85
+ ; see RFC 3629 for definition of UTF8-octets
86
+ string = string-length UTF8-octets
87
+
88
+ string-length = short
89
+
90
+ byte-array = byte-array-length *byte
91
+
92
+ byte-array-length = int
93
+
94
+ byte = OCTET ; 8-bit signed integer
95
+
96
+ short = 2OCTET ; 16-bit signed integer, big-endian
97
+
98
+ int = 4OCTET ; 32-bit signed integer, big-endian
99
+
100
+ long = 8OCTET ; 64-bit signed integer, big-endian
101
+
102
+ float = 4OCTET ; 32-bit float, big-endian, IEEE 754-2008
103
+
104
+ double = 8OCTET ; 64-bit float, big-endian, IEEE 754-2008
105
+
106
+ TAG-END = %x00
107
+ TAG-BYTE = %x01
108
+ TAG-SHORT = %x02
109
+ TAG-INT = %x03
110
+ TAG-LONG = %x04
111
+ TAG-FLOAT = %x05
112
+ TAG-DOUBLE = %x06
113
+ TAG-BYTE-ARRAY = %x07
114
+ TAG-STRING = %x08
115
+ TAG-LIST = %x09
116
+ TAG-COMPOUND = %x0a
117
+
118
+ == Interface
119
+
120
+ == Low-level API
121
+
122
+ === NBTFile.tokenize
123
+
124
+ == High-level API
125
+
126
+ === NBTFile.load
127
+
128
+ == Note on Patches/Pull Requests
129
+
130
+ * Fork the project.
131
+ * Make your feature addition or bug fix.
132
+ * Add tests for it. This is important so I don't break it in a
133
+ future version unintentionally.
134
+ * Commit, do not mess with rakefile, version, or history.
135
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
136
+ * Send me a pull request. Bonus points for topic branches.
137
+
138
+ == Copyright
139
+
140
+ Copyright (c) 2010 MenTaLguY. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,45 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "nbtfile"
8
+ gem.summary = %Q{nbtfile provides a low-level API for reading and writing files using Minecraft's NBT serialization format}
9
+ gem.description = %Q{Library for reading and writing NBT files (as used by Minecraft).}
10
+ gem.email = "mental@rydia.net"
11
+ gem.homepage = "http://github.com/mental/nbtfile"
12
+ gem.authors = ["MenTaLguY"]
13
+ gem.add_development_dependency "rspec", ">= 1.2.9"
14
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
15
+ end
16
+ Jeweler::GemcutterTasks.new
17
+ rescue LoadError
18
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
19
+ end
20
+
21
+ require 'spec/rake/spectask'
22
+ Spec::Rake::SpecTask.new(:spec) do |spec|
23
+ spec.libs << 'lib' << 'spec'
24
+ spec.spec_files = FileList['spec/**/*_spec.rb']
25
+ end
26
+
27
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
28
+ spec.libs << 'lib' << 'spec'
29
+ spec.pattern = 'spec/**/*_spec.rb'
30
+ spec.rcov = true
31
+ end
32
+
33
+ task :spec => :check_dependencies
34
+
35
+ task :default => :spec
36
+
37
+ require 'rake/rdoctask'
38
+ Rake::RDocTask.new do |rdoc|
39
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
40
+
41
+ rdoc.rdoc_dir = 'rdoc'
42
+ rdoc.title = "nbtfile #{version}"
43
+ rdoc.rdoc_files.include('README*')
44
+ rdoc.rdoc_files.include('lib/**/*.rb')
45
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
data/lib/nbtfile.rb ADDED
@@ -0,0 +1,517 @@
1
+ # nbtfile
2
+ #
3
+ # Copyright (c) 2010 MenTaLguY
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the
7
+ # "Software"), to deal in the Software without restriction, including
8
+ # without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ # permit persons to whom the Software is furnished to do so, subject to
11
+ # the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ require 'zlib'
25
+ require 'stringio'
26
+
27
+ class String
28
+ begin
29
+ alias_method :_nbtfile_getbyte, :getbyte
30
+ rescue NameError
31
+ alias_method :_nbtfile_getbyte, :[]
32
+ end
33
+
34
+ begin
35
+ alias_method :_nbtfile_force_encoding, :force_encoding
36
+ rescue NameError
37
+ def _nbtfile_force_encoding(encoding)
38
+ end
39
+ end
40
+ end
41
+
42
+ module NBTFile
43
+
44
+ TOKEN_CLASSES_BY_INDEX = []
45
+ TOKEN_INDICES_BY_CLASS = {}
46
+
47
+ BaseToken = Struct.new :name, :value
48
+
49
+ module Tokens
50
+ tag_names = %w(End Byte Short Int Long Float Double
51
+ Byte_Array String List Compound)
52
+ tag_names.each_with_index do |tag_name, index|
53
+ tag_name = "TAG_#{tag_name}"
54
+ token_class = Class.new(BaseToken)
55
+
56
+ const_set tag_name, token_class
57
+
58
+ TOKEN_CLASSES_BY_INDEX[index] = token_class
59
+ TOKEN_INDICES_BY_CLASS[token_class] = index
60
+ end
61
+ end
62
+
63
+ module CommonMethods
64
+ def sign_bit(n_bytes)
65
+ 1 << ((n_bytes << 3) - 1)
66
+ end
67
+ end
68
+
69
+ module ReadMethods
70
+ include Tokens
71
+ include CommonMethods
72
+
73
+ def read_raw(io, n_bytes)
74
+ data = io.read(n_bytes)
75
+ raise EOFError unless data and data.length == n_bytes
76
+ data
77
+ end
78
+
79
+ def read_integer(io, n_bytes)
80
+ raw_value = read_raw(io, n_bytes)
81
+ value = (0...n_bytes).reduce(0) do |accum, n|
82
+ (accum << 8) | raw_value._nbtfile_getbyte(n)
83
+ end
84
+ value -= ((value & sign_bit(n_bytes)) << 1)
85
+ value
86
+ end
87
+
88
+ def read_byte(io)
89
+ read_integer(io, 1)
90
+ end
91
+
92
+ def read_short(io)
93
+ read_integer(io, 2)
94
+ end
95
+
96
+ def read_int(io)
97
+ read_integer(io, 4)
98
+ end
99
+
100
+ def read_long(io)
101
+ read_integer(io, 8)
102
+ end
103
+
104
+ def read_float(io)
105
+ read_raw(io, 4).unpack("g").first
106
+ end
107
+
108
+ def read_double(io)
109
+ read_raw(io, 8).unpack("G").first
110
+ end
111
+
112
+ def read_string(io)
113
+ length = read_short(io)
114
+ string = read_raw(io, length)
115
+ string._nbtfile_force_encoding("UTF-8")
116
+ string
117
+ end
118
+
119
+ def read_byte_array(io)
120
+ length = read_int(io)
121
+ read_raw(io, length)
122
+ end
123
+
124
+ def read_list_header(io)
125
+ list_type = read_type(io)
126
+ list_length = read_int(io)
127
+ [list_type, list_length]
128
+ end
129
+
130
+ def read_type(io)
131
+ byte = read_byte(io)
132
+ begin
133
+ TOKEN_CLASSES_BY_INDEX.fetch(byte)
134
+ rescue IndexError
135
+ raise RuntimeError, "Unexpected tag ordinal #{byte}"
136
+ end
137
+ end
138
+
139
+ def read_value(io, type, name, state, cont)
140
+ next_state = state
141
+
142
+ case
143
+ when type == TAG_End
144
+ next_state = cont
145
+ value = nil
146
+ when type == TAG_Byte
147
+ value = read_byte(io)
148
+ when type == TAG_Short
149
+ value = read_short(io)
150
+ when type == TAG_Int
151
+ value = read_int(io)
152
+ when type == TAG_Long
153
+ value = read_long(io)
154
+ when type == TAG_Float
155
+ value = read_float(io)
156
+ when type == TAG_Double
157
+ value = read_double(io)
158
+ when type == TAG_Byte_Array
159
+ value = read_byte_array(io)
160
+ when type == TAG_String
161
+ value = read_string(io)
162
+ when type == TAG_List
163
+ list_type, list_length = read_list_header(io)
164
+ next_state = ListReaderState.new(state, list_type, list_length)
165
+ value = list_type
166
+ when type == TAG_Compound
167
+ next_state = CompoundReaderState.new(state)
168
+ end
169
+
170
+ [next_state, type[name, value]]
171
+ end
172
+ end
173
+
174
+ class TopReaderState
175
+ include ReadMethods
176
+ include Tokens
177
+
178
+ def get_token(io)
179
+ type = read_type(io)
180
+ raise RuntimeError, "expected TAG_Compound" unless type == TAG_Compound
181
+ name = read_string(io)
182
+ end_state = EndReaderState.new()
183
+ next_state = CompoundReaderState.new(end_state)
184
+ [next_state, type[name, nil]]
185
+ end
186
+ end
187
+
188
+ class CompoundReaderState
189
+ include ReadMethods
190
+ include Tokens
191
+
192
+ def initialize(cont)
193
+ @cont = cont
194
+ end
195
+
196
+ def get_token(io)
197
+ type = read_type(io)
198
+
199
+ if type != TAG_End
200
+ name = read_string(io)
201
+ else
202
+ name = ""
203
+ end
204
+
205
+ read_value(io, type, name, self, @cont)
206
+ end
207
+ end
208
+
209
+ class ListReaderState
210
+ include ReadMethods
211
+ include Tokens
212
+
213
+ def initialize(cont, type, length)
214
+ @cont = cont
215
+ @length = length
216
+ @offset = 0
217
+ @type = type
218
+ end
219
+
220
+ def get_token(io)
221
+ if @offset < @length
222
+ type = @type
223
+ else
224
+ type = TAG_End
225
+ end
226
+
227
+ index = @offset
228
+ @offset += 1
229
+
230
+ read_value(io, type, index, self, @cont)
231
+ end
232
+ end
233
+
234
+ class EndReaderState
235
+ def get_token(io)
236
+ [self, nil]
237
+ end
238
+ end
239
+
240
+ class Reader
241
+ def initialize(io)
242
+ @gz = Zlib::GzipReader.new(io)
243
+ @state = TopReaderState.new()
244
+ end
245
+
246
+ def each_token
247
+ while token = get_token()
248
+ yield token
249
+ end
250
+ end
251
+
252
+ def get_token
253
+ @state, token = @state.get_token(@gz)
254
+ token
255
+ end
256
+ end
257
+
258
+ module WriteMethods
259
+ include Tokens
260
+ include CommonMethods
261
+
262
+ def emit_integer(io, n_bytes, value)
263
+ value -= ((value & sign_bit(n_bytes)) << 1)
264
+ bytes = (1..n_bytes).map do |n|
265
+ byte = (value >> ((n_bytes - n) << 3) & 0xff)
266
+ end
267
+ io.write(bytes.pack("C*"))
268
+ end
269
+
270
+ def emit_byte(io, value)
271
+ emit_integer(io, 1, value)
272
+ end
273
+
274
+ def emit_short(io, value)
275
+ emit_integer(io, 2, value)
276
+ end
277
+
278
+ def emit_int(io, value)
279
+ emit_integer(io, 4, value)
280
+ end
281
+
282
+ def emit_long(io, value)
283
+ emit_integer(io, 8, value)
284
+ end
285
+
286
+ def emit_float(io, value)
287
+ io.write([value].pack("g"))
288
+ end
289
+
290
+ def emit_double(io, value)
291
+ io.write([value].pack("G"))
292
+ end
293
+
294
+ def emit_byte_array(io, value)
295
+ emit_int(io, value.length)
296
+ io.write(value)
297
+ end
298
+
299
+ def emit_string(io, value)
300
+ emit_short(io, value.length)
301
+ io.write(value)
302
+ end
303
+
304
+ def emit_type(io, type)
305
+ emit_byte(io, TOKEN_INDICES_BY_CLASS[type])
306
+ end
307
+
308
+ def emit_list_header(io, type, count)
309
+ emit_type(io, type)
310
+ emit_int(io, count)
311
+ end
312
+
313
+ def emit_value(io, type, value, capturing, state, cont)
314
+ next_state = state
315
+
316
+ case
317
+ when type == TAG_Byte
318
+ emit_byte(io, value)
319
+ when type == TAG_Short
320
+ emit_short(io, value)
321
+ when type == TAG_Int
322
+ emit_int(io, value)
323
+ when type == TAG_Long
324
+ emit_long(io, value)
325
+ when type == TAG_Float
326
+ emit_float(io, value)
327
+ when type == TAG_Double
328
+ emit_double(io, value)
329
+ when type == TAG_Byte_Array
330
+ emit_byte_array(io, value)
331
+ when type == TAG_String
332
+ emit_string(io, value)
333
+ when type == TAG_Float
334
+ emit_float(io, value)
335
+ when type == TAG_Double
336
+ emit_double(io, value)
337
+ when type == TAG_List
338
+ next_state = ListWriterState.new(state, value, capturing)
339
+ when type == TAG_Compound
340
+ next_state = CompoundWriterState.new(state, capturing)
341
+ when type == TAG_End
342
+ next_state = cont
343
+ else
344
+ raise RuntimeError, "Unexpected token #{type}"
345
+ end
346
+
347
+ next_state
348
+ end
349
+ end
350
+
351
+ class TopWriterState
352
+ include WriteMethods
353
+ include Tokens
354
+
355
+ def emit_token(io, token)
356
+ case token
357
+ when TAG_Compound
358
+ emit_type(io, token.class)
359
+ emit_string(io, token.name)
360
+ end_state = EndWriterState.new()
361
+ next_state = CompoundWriterState.new(end_state, nil)
362
+ next_state
363
+ end
364
+ end
365
+ end
366
+
367
+ class CompoundWriterState
368
+ include WriteMethods
369
+ include Tokens
370
+
371
+ def initialize(cont, capturing)
372
+ @cont = cont
373
+ @capturing = capturing
374
+ end
375
+
376
+ def emit_token(io, token)
377
+ out = @capturing || io
378
+
379
+ type = token.class
380
+
381
+ emit_type(out, type)
382
+ emit_string(out, token.name) unless type == TAG_End
383
+
384
+ emit_value(out, type, token.value, @capturing, self, @cont)
385
+ end
386
+
387
+ def emit_item(io, value)
388
+ raise RuntimeError, "not in a list"
389
+ end
390
+ end
391
+
392
+ class ListWriterState
393
+ include WriteMethods
394
+ include Tokens
395
+
396
+ def initialize(cont, type, capturing)
397
+ @cont = cont
398
+ @type = type
399
+ @count = 0
400
+ @value = StringIO.new()
401
+ @capturing = capturing
402
+ end
403
+
404
+ def emit_token(io, token)
405
+ type = token.class
406
+
407
+ if type == TAG_End
408
+ out = @capturing || io
409
+ emit_list_header(out, @type, @count)
410
+ out.write(@value.string)
411
+ elsif type != @type
412
+ raise RuntimeError, "unexpected token #{token.class}, expected #{@type}"
413
+ end
414
+
415
+ _emit_item(io, type, token.value)
416
+ end
417
+
418
+ def emit_item(io, value)
419
+ _emit_item(io, @type, value)
420
+ end
421
+
422
+ def _emit_item(io, type, value)
423
+ @count += 1
424
+ emit_value(@value, type, value, @value, self, @cont)
425
+ end
426
+ end
427
+
428
+ class EndWriterState
429
+ def emit_token(io, token)
430
+ raise RuntimeError, "unexpected token #{token.class} after end"
431
+ end
432
+
433
+ def emit_item(io, value)
434
+ raise RuntimeError, "not in a list"
435
+ end
436
+ end
437
+
438
+ class Writer
439
+ include WriteMethods
440
+
441
+ def initialize(stream)
442
+ @gz = Zlib::GzipWriter.new(stream)
443
+ @state = TopWriterState.new()
444
+ end
445
+
446
+ def emit_token(token)
447
+ @state = @state.emit_token(@gz, token)
448
+ end
449
+
450
+ def emit_compound(name)
451
+ emit_token(TAG_Compound[name, nil])
452
+ begin
453
+ yield
454
+ ensure
455
+ emit_token(TAG_End[nil, nil])
456
+ end
457
+ end
458
+
459
+ def emit_list(name, type)
460
+ emit_token(TAG_List[name, type])
461
+ begin
462
+ yield
463
+ ensure
464
+ emit_token(TAG_End[nil, nil])
465
+ end
466
+ end
467
+
468
+ def emit_item(value)
469
+ @state = @state.emit_item(@gz, value)
470
+ end
471
+
472
+ def finish
473
+ @gz.close
474
+ end
475
+ end
476
+
477
+ def self.tokenize(io)
478
+ case io
479
+ when String
480
+ io = StringIO.new(io, "rb")
481
+ end
482
+ reader = Reader.new(io)
483
+
484
+ reader.each_token do |token|
485
+ yield token
486
+ end
487
+ end
488
+
489
+ def self.load(io)
490
+ root = {}
491
+ stack = [root]
492
+
493
+ self.tokenize(io) do |token|
494
+ case token
495
+ when Tokens::TAG_Compound
496
+ value = {}
497
+ when Tokens::TAG_List
498
+ value = []
499
+ when Tokens::TAG_End
500
+ stack.pop
501
+ next
502
+ else
503
+ value = token.value
504
+ end
505
+
506
+ stack.last[token.name] = value
507
+
508
+ case token
509
+ when Tokens::TAG_Compound, Tokens::TAG_List
510
+ stack.push value
511
+ end
512
+ end
513
+
514
+ root
515
+ end
516
+
517
+ end
Binary file
Binary file
data/samples/test.nbt ADDED
Binary file
@@ -0,0 +1,279 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+ require 'enumerator'
3
+ require 'nbtfile'
4
+ require 'stringio'
5
+ require 'zlib'
6
+
7
+ shared_examples_for "readers and writers" do
8
+ Tokens = NBTFile::Tokens unless defined? Tokens
9
+
10
+ def self.a_reader_or_writer(desc, serialized, tokens)
11
+ it desc do
12
+ check_reader_or_writer(serialized, tokens)
13
+ end
14
+ end
15
+
16
+ a_reader_or_writer "should handle basic documents",
17
+ "\x0a\x00\x03foo" \
18
+ "\x00",
19
+ [Tokens::TAG_Compound["foo", nil],
20
+ Tokens::TAG_End["", nil]]
21
+
22
+ a_reader_or_writer "should treat integers as signed",
23
+ "\x0a\x00\x03foo" \
24
+ "\x03\x00\x03bar\xff\xff\xff\xfe" \
25
+ "\x00",
26
+ [Tokens::TAG_Compound["foo", nil],
27
+ Tokens::TAG_Int["bar", -2],
28
+ Tokens::TAG_End["", nil]]
29
+
30
+ a_reader_or_writer "should handle integer fields",
31
+ "\x0a\x00\x03foo" \
32
+ "\x03\x00\x03bar\x01\x02\x03\x04" \
33
+ "\x00",
34
+ [Tokens::TAG_Compound["foo", nil],
35
+ Tokens::TAG_Int["bar", 0x01020304],
36
+ Tokens::TAG_End["", nil]]
37
+
38
+ a_reader_or_writer "should handle short fields",
39
+ "\x0a\x00\x03foo" \
40
+ "\x02\x00\x03bar\x4e\x5a" \
41
+ "\x00",
42
+ [Tokens::TAG_Compound["foo", nil],
43
+ Tokens::TAG_Short["bar", 0x4e5a],
44
+ Tokens::TAG_End["", nil]]
45
+
46
+ a_reader_or_writer "should handle byte fields",
47
+ "\x0a\x00\x03foo" \
48
+ "\x01\x00\x03bar\x4e" \
49
+ "\x00",
50
+ [Tokens::TAG_Compound["foo", nil],
51
+ Tokens::TAG_Byte["bar", 0x4e],
52
+ Tokens::TAG_End["", nil]]
53
+
54
+ a_reader_or_writer "should handle string fields",
55
+ "\x0a\x00\x03foo" \
56
+ "\x08\x00\x03bar\x00\x04hoge" \
57
+ "\x00",
58
+ [Tokens::TAG_Compound["foo", nil],
59
+ Tokens::TAG_String["bar", "hoge"],
60
+ Tokens::TAG_End["", nil]]
61
+
62
+ a_reader_or_writer "should handle byte array fields",
63
+ "\x0a\x00\x03foo" \
64
+ "\x07\x00\x03bar\x00\x00\x00\x05\x01\x02\x03\x04\x05" \
65
+ "\x00",
66
+ [Tokens::TAG_Compound["foo", nil],
67
+ Tokens::TAG_Byte_Array["bar", "\x01\x02\x03\x04\x05"],
68
+ Tokens::TAG_End["", nil]]
69
+
70
+ a_reader_or_writer "should handle long fields",
71
+ "\x0a\x00\x03foo" \
72
+ "\x04\x00\x03bar\x01\x02\x03\x04\x05\x06\x07\x08" \
73
+ "\x00",
74
+ [Tokens::TAG_Compound["foo", nil],
75
+ Tokens::TAG_Long["bar", 0x0102030405060708],
76
+ Tokens::TAG_End["", nil]]
77
+
78
+ a_reader_or_writer "should handle float fields",
79
+ "\x0a\x00\x03foo" \
80
+ "\x05\x00\x03bar\x3f\xa0\x00\x00" \
81
+ "\x00",
82
+ [Tokens::TAG_Compound["foo", nil],
83
+ Tokens::TAG_Float["bar", "\x3f\xa0\x00\x00".unpack("g").first],
84
+ Tokens::TAG_End["", nil]]
85
+
86
+ a_reader_or_writer "should handle double fields",
87
+ "\x0a\x00\x03foo" \
88
+ "\x06\x00\x03bar\x3f\xf4\x00\x00\x00\x00\x00\x00" \
89
+ "\x00",
90
+ [Tokens::TAG_Compound["foo", nil],
91
+ Tokens::TAG_Double["bar", "\x3f\xf4\x00\x00\x00\x00\x00\x00".unpack("G").first],
92
+ Tokens::TAG_End["", nil]]
93
+
94
+ a_reader_or_writer "should handle nested compound fields",
95
+ "\x0a\x00\x03foo" \
96
+ "\x0a\x00\x03bar" \
97
+ "\x01\x00\x04hoge\x4e" \
98
+ "\x00" \
99
+ "\x00",
100
+ [Tokens::TAG_Compound["foo", nil],
101
+ Tokens::TAG_Compound["bar", nil],
102
+ Tokens::TAG_Byte["hoge", 0x4e],
103
+ Tokens::TAG_End["", nil],
104
+ Tokens::TAG_End["", nil]]
105
+
106
+ simple_list_types = [
107
+ ["bytes", Tokens::TAG_Byte, 0x01, lambda { |ns| ns.pack("C*") }],
108
+ ["shorts", Tokens::TAG_Short, 0x02, lambda { |ns| ns.pack("n*") }],
109
+ ["ints", Tokens::TAG_Int, 0x03, lambda { |ns| ns.pack("N*") }],
110
+ ["longs", Tokens::TAG_Long, 0x04, lambda { |ns| ns.map { |n| [n].pack("x4N") }.join("") }],
111
+ ["floats", Tokens::TAG_Float, 0x05, lambda { |ns| ns.pack("g*") }],
112
+ ["doubles", Tokens::TAG_Double, 0x06, lambda { |ns| ns.pack("G*") }]
113
+ ]
114
+
115
+ for label, type, token, pack in simple_list_types
116
+ values = [9, 5]
117
+ a_reader_or_writer "should handle lists of #{label}",
118
+ "\x0a\x00\x03foo" \
119
+ "\x09\x00\x03bar#{[token].pack("C")}\x00\x00\x00\x02" \
120
+ "#{pack.call(values)}" \
121
+ "\x00",
122
+ [Tokens::TAG_Compound["foo", nil],
123
+ Tokens::TAG_List["bar", type],
124
+ type[0, values[0]],
125
+ type[1, values[1]],
126
+ Tokens::TAG_End[2, nil],
127
+ Tokens::TAG_End["", nil]]
128
+ end
129
+
130
+ a_reader_or_writer "should handle nested lists",
131
+ "\x0a\x00\x03foo" \
132
+ "\x09\x00\x03bar\x09\x00\x00\x00\x01" \
133
+ "\x01\x00\x00\x00\x01" \
134
+ "\x4a" \
135
+ "\x00",
136
+ [Tokens::TAG_Compound["foo", nil],
137
+ Tokens::TAG_List["bar", Tokens::TAG_List],
138
+ Tokens::TAG_List[0, Tokens::TAG_Byte],
139
+ Tokens::TAG_Byte[0, 0x4a],
140
+ Tokens::TAG_End[1, nil],
141
+ Tokens::TAG_End[1, nil],
142
+ Tokens::TAG_End["", nil]]
143
+ end
144
+
145
+ describe "NBTFile::tokenize" do
146
+ include ZlibHelpers
147
+
148
+ it_should_behave_like "readers and writers"
149
+
150
+ def check_reader_or_writer(input, tokens)
151
+ io = make_zipped_stream(input)
152
+ actual_tokens = []
153
+ NBTFile.tokenize(io) do |token|
154
+ actual_tokens << token
155
+ end
156
+ actual_tokens.should == tokens
157
+ end
158
+ end
159
+
160
+ describe "NBTFile::load" do
161
+ include ZlibHelpers
162
+
163
+ def self.nbtfile_load(description, tokens, result)
164
+ it description do
165
+ io = StringIO.new
166
+ writer = NBTFile::Writer.new(io)
167
+ for token in tokens
168
+ writer.emit_token(token)
169
+ end
170
+ writer.finish
171
+ actual_result = NBTFile.load(StringIO.new(io.string))
172
+ actual_result.should == result
173
+ end
174
+ end
175
+
176
+ nbtfile_load "should generate a top-level hash",
177
+ [Tokens::TAG_Compound["foo", nil],
178
+ Tokens::TAG_Byte["a", 19],
179
+ Tokens::TAG_Byte["b", 23],
180
+ Tokens::TAG_End[nil, nil]],
181
+ {"foo" => {"a" => 19, "b" => 23}}
182
+
183
+ nbtfile_load "should map compound structures to hashes",
184
+ [Tokens::TAG_Compound["foo", nil],
185
+ Tokens::TAG_Compound["bar", nil],
186
+ Tokens::TAG_Byte["a", 123],
187
+ Tokens::TAG_Byte["b", 56],
188
+ Tokens::TAG_End[nil, nil],
189
+ Tokens::TAG_End[nil, nil]],
190
+ {"foo" => {"bar" => {"a" => 123, "b" => 56}}}
191
+
192
+ nbtfile_load "should map lists to arrays",
193
+ [Tokens::TAG_Compound["foo", nil],
194
+ Tokens::TAG_List["bar", Tokens::TAG_Byte],
195
+ Tokens::TAG_Byte[0, 32],
196
+ Tokens::TAG_Byte[1, 45],
197
+ Tokens::TAG_End[2, nil],
198
+ Tokens::TAG_End["", nil]],
199
+ {"foo" => {"bar" => [32, 45]}}
200
+ end
201
+
202
+ describe NBTFile::Reader do
203
+ include ZlibHelpers
204
+
205
+ it_should_behave_like "readers and writers"
206
+
207
+ def check_reader_or_writer(input, tokens)
208
+ io = make_zipped_stream(input)
209
+ reader = NBTFile::Reader.new(io)
210
+ actual_tokens = []
211
+ reader.each_token do |token|
212
+ actual_tokens << token
213
+ end
214
+ actual_tokens.should == tokens
215
+ end
216
+ end
217
+
218
+ describe NBTFile::Writer do
219
+ include ZlibHelpers
220
+
221
+ it_should_behave_like "readers and writers"
222
+
223
+ def check_reader_or_writer(output, tokens)
224
+ stream = StringIO.new()
225
+ writer = NBTFile::Writer.new(stream)
226
+ begin
227
+ for token in tokens
228
+ writer.emit_token(token)
229
+ end
230
+ ensure
231
+ writer.finish
232
+ end
233
+ actual_output = unzip_string(stream.string)
234
+ actual_output.should == output
235
+ end
236
+
237
+ it "should support shorthand for emitting lists" do
238
+ output = StringIO.new()
239
+ writer = NBTFile::Writer.new(output)
240
+ begin
241
+ writer.emit_token(Tokens::TAG_Compound["test", nil])
242
+ writer.emit_list("foo", Tokens::TAG_Byte) do
243
+ writer.emit_item(12)
244
+ writer.emit_item(43)
245
+ end
246
+ writer.emit_token(Tokens::TAG_End[nil, nil])
247
+ ensure
248
+ writer.finish
249
+ end
250
+
251
+ actual_output = unzip_string(output.string)
252
+ actual_output.should == "\x0a\x00\x04test" \
253
+ "\x09\x00\x03foo\x01\x00\x00\x00\x02" \
254
+ "\x0c\x2b" \
255
+ "\x00"
256
+ end
257
+
258
+ it "should support shorthand for emitting compound structures" do
259
+ output = StringIO.new()
260
+ writer = NBTFile::Writer.new(output)
261
+ begin
262
+ writer.emit_token(Tokens::TAG_Compound["test", nil])
263
+ writer.emit_compound("xyz") do
264
+ writer.emit_token(Tokens::TAG_Byte["foo", 0x08])
265
+ writer.emit_token(Tokens::TAG_Byte["bar", 0x02])
266
+ end
267
+ writer.emit_token(Tokens::TAG_End[nil, nil])
268
+ ensure
269
+ writer.finish
270
+ end
271
+ actual_output = unzip_string(output.string)
272
+ actual_output.should == "\x0a\x00\x04test" \
273
+ "\x0a\x00\x03xyz" \
274
+ "\x01\x00\x03foo\x08" \
275
+ "\x01\x00\x03bar\x02" \
276
+ "\x00" \
277
+ "\x00"
278
+ end
279
+ end
@@ -0,0 +1,33 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+ require 'nbtfile'
3
+ require 'stringio'
4
+ require 'zlib'
5
+
6
+
7
+ describe NBTFile do
8
+ include ZlibHelpers
9
+
10
+ sample_pattern = File.join(File.dirname(__FILE__), '..', 'samples', '*.nbt')
11
+
12
+ for file in Dir.glob(sample_pattern)
13
+ it "should roundtrip #{File.basename(file)}" do
14
+ input = StringIO.new(File.read(file))
15
+ output = StringIO.new()
16
+
17
+ reader = NBTFile::Reader.new(input)
18
+ writer = NBTFile::Writer.new(output)
19
+ begin
20
+ reader.each_token do |token|
21
+ writer.emit_token(token)
22
+ end
23
+ ensure
24
+ writer.finish
25
+ end
26
+
27
+ input_bytes = unzip_string(input.string)
28
+ output_bytes = unzip_string(output.string)
29
+
30
+ output_bytes.should == input_bytes
31
+ end
32
+ end
33
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,28 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'nbtfile'
4
+ require 'spec'
5
+ require 'spec/autorun'
6
+
7
+ Spec::Runner.configure do |config|
8
+
9
+ end
10
+
11
+ module ZlibHelpers
12
+
13
+ def make_zipped_stream(data)
14
+ gz = Zlib::GzipWriter.new(StringIO.new())
15
+ gz << data
16
+ string = gz.close.string
17
+ StringIO.new(string, "rb")
18
+ end
19
+
20
+ def unzip_string(string)
21
+ gz = Zlib::GzipReader.new(StringIO.new(string))
22
+ begin
23
+ gz.read
24
+ ensure
25
+ gz.close
26
+ end
27
+ end
28
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nbtfile
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - MenTaLguY
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-10-27 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rspec
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.2.9
24
+ version:
25
+ description: Library for reading and writing NBT files (as used by Minecraft).
26
+ email: mental@rydia.net
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - LICENSE
33
+ - README.rdoc
34
+ files:
35
+ - .document
36
+ - .gitignore
37
+ - LICENSE
38
+ - README.rdoc
39
+ - Rakefile
40
+ - VERSION
41
+ - lib/nbtfile.rb
42
+ - samples/bigtest.nbt
43
+ - samples/chunk0.nbt
44
+ - samples/test.nbt
45
+ - spec/nbtfile_spec.rb
46
+ - spec/roundtrip_spec.rb
47
+ - spec/spec.opts
48
+ - spec/spec_helper.rb
49
+ has_rdoc: true
50
+ homepage: http://github.com/mental/nbtfile
51
+ licenses: []
52
+
53
+ post_install_message:
54
+ rdoc_options:
55
+ - --charset=UTF-8
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: "0"
63
+ version:
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: "0"
69
+ version:
70
+ requirements: []
71
+
72
+ rubyforge_project:
73
+ rubygems_version: 1.3.5
74
+ signing_key:
75
+ specification_version: 3
76
+ summary: nbtfile provides a low-level API for reading and writing files using Minecraft's NBT serialization format
77
+ test_files:
78
+ - spec/nbtfile_spec.rb
79
+ - spec/roundtrip_spec.rb
80
+ - spec/spec_helper.rb