craftbook-nbt 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []