nbtfile 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/.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