craftbook-nbt 1.0.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.
@@ -0,0 +1,51 @@
1
+
2
+ module CraftBook
3
+
4
+ module NBT
5
+ ##
6
+ # @abstract
7
+ # Abstract base class for tags that can contain a single primitive value.
8
+ class ValueTag < Tag
9
+
10
+ ##
11
+ # @!attribute [rw] value
12
+ # @return [Object] the value of the tag.
13
+
14
+ attr_reader :value
15
+
16
+ ##
17
+ # Creates a new instance of the {ValueTag} class.
18
+ #
19
+ # @param name [String,NilClass] The name of the tag, or `nil` when unnamed.
20
+ # @param value [Object] The value of the tag.
21
+ def initialize(type, name, value)
22
+ super(type, name)
23
+ self.value = value
24
+ end
25
+
26
+ def value=(value)
27
+ @value = value
28
+ end
29
+
30
+ ##
31
+ # @return [Hash{Symbol => Object}] the hash-representation of this object.
32
+ def to_h
33
+ { name: @name, type: @type, value: @value }
34
+ end
35
+
36
+ protected
37
+
38
+ def validate(value, min, max)
39
+ raise(TypeError, 'value cannot be nil') unless value
40
+ return value if value.between?(min, max)
41
+ raise(ArgumentError, sprintf("value must be between 0x%X and 0x%X inclusive", min, max))
42
+ end
43
+
44
+ protected
45
+
46
+ def parse_hash(hash)
47
+ self.value = hash[:value]
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CraftBook
4
+ module NBT
5
+
6
+ ##
7
+ # The version of the craftbook-nbt Gem.
8
+ VERSION = "1.0.0"
9
+ end
10
+ end
@@ -0,0 +1,298 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'stringio'
5
+ require 'zlib'
6
+
7
+ require_relative 'nbt/version'
8
+ require_relative 'nbt/tag'
9
+ require_relative 'nbt/value_tag'
10
+ require_relative 'nbt/enumerable_tag'
11
+ require_relative 'nbt/container_tag'
12
+ require_relative 'nbt/byte_tag'
13
+ require_relative 'nbt/short_tag'
14
+ require_relative 'nbt/int_tag'
15
+ require_relative 'nbt/long_tag'
16
+ require_relative 'nbt/float_tag'
17
+ require_relative 'nbt/double_tag'
18
+ require_relative 'nbt/string_tag'
19
+ require_relative 'nbt/byte_array_tag'
20
+ require_relative 'nbt/int_array_tag'
21
+ require_relative 'nbt/long_array_tag'
22
+ require_relative 'nbt/list_tag'
23
+ require_relative 'nbt/compound_tag'
24
+ require_relative 'nbt/tag_builder'
25
+ require_relative 'nbt/snbt/snbt'
26
+
27
+ ##
28
+ # Top-level namespace for the CraftBook API.
29
+ module CraftBook
30
+
31
+ ##
32
+ # Top-level namespace for the independent Named Binary Tag (NBT) module of the CraftBook API, providing classes and
33
+ # for reading and writing NBT tags used by the Java editions of Minecraft.
34
+ #
35
+ # @api NBT
36
+ # @author Eric "ForeverZer0" Freed
37
+ module NBT
38
+
39
+ ##
40
+ # Exception class used for errors relating to parsing and invalid formats.
41
+ class ParseError < StandardError
42
+ end
43
+
44
+ ##
45
+ # The encoding used for all strings.
46
+ ENCODING = Encoding::UTF_8
47
+
48
+ ##
49
+ # Serializes and writes the specified {Tag} to an IO-like object.
50
+ #
51
+ # @param io [IO,#write] An IO-like object that responds to `#write`
52
+ # @param tag [Tag] A {Tag} instance to write. If `io` represents a file stream, the specification expects this to
53
+ # be a {CompoundTag}.
54
+ #
55
+ # @return [Integer] The number of bytes written.
56
+ def self.write(io, tag)
57
+ unless io.is_a?(IO) || io.respond_to?(:write)
58
+ raise(ArgumentError, "object must be an IO instance or respond to #write")
59
+ end
60
+ write_tag(io, tag, false)
61
+ end
62
+
63
+ ##
64
+ # Serializes and writes the specified {Tag} to a file at the specified `path`. If file already exists at that
65
+ # location, it will be overwritten.
66
+ #
67
+ # @param path [String] The path to the file to write to.
68
+ # @param compound_tag [Tag] A {CompoundTag} instance to write.
69
+ # @param opts [Hash{Symbol => Symbol}] Options hash.
70
+ #
71
+ # @option opts [Symbol] :compression (:gzip) The type of compression to use when writing, if any.
72
+ # Valid values include:
73
+ # <ul>
74
+ # <li><code>:none</code> No compression</li>
75
+ # <li><code>:gzip</code> GZip compression</li>
76
+ # <li><code>:zlib</code> ZLib compression (DEFLATE with 2 byte header and post-fixed CRC checksum)</li>
77
+ # </ul>
78
+ # @option opts [Symbol] :level (:default) The level of compression to use, ignored when no compression is specified.
79
+ # Valid values include:
80
+ # <ul>
81
+ # <li><code>:default</code> The default compression employed by the specified algorithm.</li>
82
+ # <li><code>:none</code> No compression. Compressions formats will still include their additional meta-data.</li>
83
+ # <li><code>:optimal</code> Favor high compression-rate over speed.</li>
84
+ # <li><code>:fastest</code> Favor speed over compression-rate.</li>
85
+ # </ul>
86
+ #
87
+ # @return [Integer] The number of bytes written.
88
+ def self.write_file(path, compound_tag, **opts)
89
+
90
+ compression = opts[:compression] || :gzip
91
+ level = case opts[:level]
92
+ when nil then Zlib::DEFAULT_COMPRESSION
93
+ when :default then Zlib::DEFAULT_COMPRESSION
94
+ when :none then Zlib::NO_COMPRESSION
95
+ when :optimal then Zlib::BEST_COMPRESSION
96
+ when :fastest then Zlib::BEST_SPEED
97
+ else raise(ArgumentError, "invalid compression level specified: #{opts[:level]}")
98
+ end
99
+
100
+ written = 0
101
+ File.open(path, 'wb') do |io|
102
+
103
+ case compression
104
+ when :none then written = write(io, compound_tag)
105
+ when :gzip
106
+ gzip = Zlib::GzipWriter.new(io, level)
107
+ #noinspection RubyMismatchedParameterType
108
+ written = write(gzip, compound_tag)
109
+ gzip.finish
110
+ when :zlib
111
+ buffer = StringIO.new
112
+ #noinspection RubyMismatchedParameterType
113
+ write(buffer, compound_tag)
114
+ compressed = Zlib::Deflate.deflate(buffer.string, level)
115
+ written = io.write(compressed)
116
+ else
117
+ raise(ArgumentError, "invalid compression specified: #{compression}")
118
+ end
119
+ end
120
+
121
+ written
122
+ end
123
+
124
+ ##
125
+ # Deserializes a {Tag} instance from the specified IO-like object.
126
+ # @param io [IO,#read] A IO-like object that responds to `#read`.
127
+ #
128
+ # @return [Tag] The deserialized tag object.
129
+ def self.read(io)
130
+ unless io.is_a?(IO) || io.respond_to?(:read)
131
+ raise(ArgumentError, "object must be an IO instance or respond to #read")
132
+ end
133
+ type = io.readbyte
134
+ read_type(io, type, read_string(io))
135
+ end
136
+
137
+ ##
138
+ # Reads and deserializes a {Tag} from a file stored at the specified `path`.
139
+ # @param path [String] The path to a file to read from.
140
+ #
141
+ # @note Compression formats supported by the specification (GZip, ZLib) will be detected and handled automatically.
142
+ #
143
+ # @return [Tag] The deserialized tag object.
144
+ def self.read_file(path)
145
+
146
+ File.open(path, 'rb') do |io|
147
+ byte = io.readbyte
148
+ io.seek(0, IO::SEEK_SET)
149
+
150
+ stream = case byte
151
+ when 0x78 then StringIO.new(Zlib::Inflate.inflate(io.read))
152
+ when 0x1F then Zlib::GzipReader.new(io)
153
+ when 0x0A then io
154
+ else raise(ParseError, 'invalid NBT format')
155
+ end
156
+ read(stream)
157
+ end
158
+ end
159
+
160
+ private
161
+
162
+ def self.write_tag(io, tag, list_child = false)
163
+
164
+ written = 0
165
+ unless list_child
166
+ written += io.write([tag.type].pack('C'))
167
+ written += write_string(io, tag.name)
168
+ written
169
+ end
170
+
171
+ written += case tag.type
172
+ when Tag::TYPE_END then io.write("\0")
173
+ when Tag::TYPE_BYTE then io.write([tag.value].pack('c'))
174
+ when Tag::TYPE_SHORT then io.write([tag.value].pack('s>'))
175
+ when Tag::TYPE_INT then io.write([tag.value].pack('l>'))
176
+ when Tag::TYPE_LONG then io.write([tag.value].pack('q>'))
177
+ when Tag::TYPE_FLOAT then io.write([tag.value].pack('g'))
178
+ when Tag::TYPE_DOUBLE then io.write([tag.value].pack('G'))
179
+ when Tag::TYPE_BYTE_ARRAY
180
+ written += io.write([tag.size].pack('l>'))
181
+ io.write(tag.to_a.pack('c*'))
182
+ when Tag::TYPE_STRING then write_string(io, tag.value)
183
+ when Tag::TYPE_LIST
184
+ written += io.write([tag.child_type].pack('C'))
185
+ written += io.write([tag.size].pack('l>'))
186
+ tag.map { |child| write_tag(io, child, true) }.sum
187
+ when Tag::TYPE_COMPOUND
188
+ tag.each { |child| written += write_tag(io, child, false) }
189
+ io.write("\0")
190
+ when Tag::TYPE_INT_ARRAY
191
+ written += io.write([tag.size].pack('l>'))
192
+ io.write(tag.to_a.pack('l>*'))
193
+ when Tag::TYPE_LONG_ARRAY
194
+ written += io.write([tag.size].pack('l>'))
195
+ io.write(tag.to_a.pack('q>*'))
196
+ else
197
+ raise(RuntimeError, sprintf("invalid type specifier: 0x%X2", tag.type))
198
+ end
199
+
200
+ written
201
+ end
202
+
203
+ def self.write_string(io, str)
204
+ count = 0
205
+ if str
206
+ if str.encoding != ENCODING
207
+ str = str.encode(ENCODING)
208
+ warn("invalid UTF-8 characters in string") unless str.valid_encoding?
209
+ end
210
+ count += io.write([str.bytesize].pack('S>'))
211
+ count += io.write(str)
212
+ else
213
+ count += io.write([0].pack('S>'))
214
+ end
215
+ count
216
+ end
217
+
218
+ ##
219
+ # @param io [IO]
220
+ # @param type [Integer]
221
+ # @param name [String,NilClass]
222
+ def self.read_type(io, type, name)
223
+ case type
224
+ when Tag::TYPE_BYTE then read_value_tag(io, ByteTag, name, 'c', 1)
225
+ when Tag::TYPE_SHORT then read_value_tag(io, ShortTag, name, 's>', 2)
226
+ when Tag::TYPE_INT then read_value_tag(io, IntTag, name, 'l>', 4)
227
+ when Tag::TYPE_LONG then read_value_tag(io, LongTag, name, 'q>', 8)
228
+ when Tag::TYPE_FLOAT then read_value_tag(io, FloatTag, name, 'g', 4)
229
+ when Tag::TYPE_DOUBLE then read_value_tag(io, DoubleTag, name, 'G', 8)
230
+ when Tag::TYPE_BYTE_ARRAY then read_array_tag(io, ByteArrayTag, name, 'c*', 1)
231
+ when Tag::TYPE_STRING then StringTag.new(name, read_string(io))
232
+ when Tag::TYPE_LIST then read_list_tag(io, name)
233
+ when Tag::TYPE_COMPOUND then read_compound_tag(io, name)
234
+ when Tag::TYPE_INT_ARRAY then read_array_tag(io, IntArrayTag, name, 'l>*', 4)
235
+ when Tag::TYPE_LONG_ARRAY then read_array_tag(io, LongArrayTag, name, 'q>*', 8)
236
+ else raise(ParseError, 'invalid type specifier, likely due to incorrect stream position')
237
+ end
238
+ end
239
+
240
+ ##
241
+ # @param io [IO]
242
+ # @param klass [Class]
243
+ # @param name [String,NilClass]
244
+ # @param unpack [String]
245
+ # @param size [Integer]
246
+ def self.read_value_tag(io, klass, name, unpack, size)
247
+ #noinspection RubyNilAnalysis
248
+ value = io.read(size).unpack1(unpack)
249
+ #noinspection RubyArgCount
250
+ klass.new(name, value)
251
+ end
252
+
253
+ #
254
+ # @param io [IO]
255
+ # @param klass [Class]
256
+ # @param name [String,NilClass]
257
+ # @param unpack [String]
258
+ # @param size [Integer]
259
+ def self.read_array_tag(io, klass, name, unpack, size)
260
+ #noinspection RubyNilAnalysis
261
+ count = io.read(4).unpack1('l>')
262
+ #noinspection RubyNilAnalysis
263
+ values = io.read(count * size).unpack(unpack)
264
+ tag = klass.new(name)
265
+ tag.instance_variable_set(:@values, values)
266
+ tag
267
+ end
268
+
269
+ ##
270
+ # @return [String]
271
+ def self.read_string(io)
272
+ length = io.read(2).unpack1('S>')
273
+ #noinspection RubyResolve
274
+ length.zero? ? '' : io.read(length).force_encoding(ENCODING)
275
+ end
276
+
277
+ def self.read_list_tag(io, name)
278
+ child_type = io.readbyte
279
+ count = io.read(4).unpack1('l>')
280
+ list = ListTag.new(name, child_type)
281
+ values = (0...count).map { read_type(io, child_type, nil) }
282
+ list.instance_variable_set(:@values, values)
283
+ list
284
+ end
285
+
286
+ def self.read_compound_tag(io, name)
287
+ compound = CompoundTag.new(name)
288
+ loop do
289
+ type = io.readbyte
290
+ break if type == Tag::TYPE_END
291
+ child_name = read_string(io)
292
+ compound.push(read_type(io, type, child_name))
293
+ end
294
+ compound
295
+ end
296
+
297
+ end
298
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: craftbook-nbt
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - ForeverZer0
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-08-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '13.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '13.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rexical
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ description: A feature-rich and complete Ruby implementation of the Named Binary Tag
42
+ (NBT) format. While it is an integral part of the broader CraftBook API, it is an
43
+ independent module with no dependencies, and can be used for any purpose where reading/writing/converting
44
+ the NBT format is required.
45
+ email:
46
+ - efreed09@gmail.com
47
+ executables: []
48
+ extensions: []
49
+ extra_rdoc_files: []
50
+ files:
51
+ - ".gitignore"
52
+ - ".yardopts"
53
+ - CHANGELOG.md
54
+ - CODE_OF_CONDUCT.md
55
+ - Gemfile
56
+ - LICENSE.txt
57
+ - README.md
58
+ - Rakefile
59
+ - bin/console
60
+ - bin/setup
61
+ - craftbook-nbt.gemspec
62
+ - lib/craftbook/nbt.rb
63
+ - lib/craftbook/nbt/byte_array_tag.rb
64
+ - lib/craftbook/nbt/byte_tag.rb
65
+ - lib/craftbook/nbt/compound_tag.rb
66
+ - lib/craftbook/nbt/container_tag.rb
67
+ - lib/craftbook/nbt/double_tag.rb
68
+ - lib/craftbook/nbt/enumerable_tag.rb
69
+ - lib/craftbook/nbt/float_tag.rb
70
+ - lib/craftbook/nbt/int_array_tag.rb
71
+ - lib/craftbook/nbt/int_tag.rb
72
+ - lib/craftbook/nbt/list_tag.rb
73
+ - lib/craftbook/nbt/long_array_tag.rb
74
+ - lib/craftbook/nbt/long_tag.rb
75
+ - lib/craftbook/nbt/short_tag.rb
76
+ - lib/craftbook/nbt/snbt.rb
77
+ - lib/craftbook/nbt/snbt/lexer.rb
78
+ - lib/craftbook/nbt/snbt/snbt.rb
79
+ - lib/craftbook/nbt/snbt/snbt.rex
80
+ - lib/craftbook/nbt/string_tag.rb
81
+ - lib/craftbook/nbt/tag.rb
82
+ - lib/craftbook/nbt/tag_builder.rb
83
+ - lib/craftbook/nbt/value_tag.rb
84
+ - lib/craftbook/nbt/version.rb
85
+ homepage: https://github.com/ForeverZer0/craftbook-nbt
86
+ licenses:
87
+ - MIT
88
+ metadata:
89
+ homepage_uri: https://github.com/ForeverZer0/craftbook-nbt
90
+ source_code_uri: https://github.com/ForeverZer0/craftbook-nbt
91
+ changelog_uri: https://github.com/ForeverZer0/craftbook-nbt/CHANGELOG.md
92
+ post_install_message:
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: 2.4.0
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubygems_version: 3.2.21
108
+ signing_key:
109
+ specification_version: 4
110
+ summary: A feature-rich and complete Ruby implementation of the Named Binary Tag (NBT)
111
+ format and SNBT parser.
112
+ test_files: []