burli 0.1.0-x86_64-linux-musl

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 831df44e5e07a682f46a0bcdd05a9c93973aa1eb9f72ae117311cc48a248bd62
4
+ data.tar.gz: fe328038e234a18058950753246707c2214f95b2d39b00100298cf15d841046e
5
+ SHA512:
6
+ metadata.gz: 241173318f68c9bf4fd2207bc44871afb1b95d9084b7469054ab5b0bc8905244e18ac45cf3e3defd76fe3786b740bcfbaa71e22aab8517ca83bd52c63d543219
7
+ data.tar.gz: eaafeec370a7b911196ba9c0ae2a8e50f9a22ff0566ce114e407776d07d0d01ac6e8a86da2a51f52994a62366d76e5b05a7f51a8d5a8c71d0b3e08a865b1979c
data/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ## [0.1.0] - 2026-10-28
6
+
7
+ - Add the `burli` gem, with the API of the `brotli` gem: `Burli.deflate`,
8
+ `Burli.inflate`, `Burli::Compressor`, `Burli::Decompressor` with
9
+ `output_buffer_limit:`, `can_accept_more_data`, and `unused_data`,
10
+ `Burli::Writer`, `Burli::Reader`, and `Burli::Error`. Qualities 6 to 11 run
11
+ as 5. Raw dictionaries work for decompression.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # burli
2
+
3
+ Brotli compression and decompression for Ruby, built on
4
+ [burli](https://github.com/paddor/burli), a Brotli codec in safe Rust. The
5
+ API matches the `brotli` gem:
6
+
7
+ ```ruby
8
+ require "burli"
9
+
10
+ compressed = Burli.deflate(data)
11
+ Burli.inflate(compressed) == data # => true
12
+ ```
13
+
14
+ Code written for the `brotli` gem can switch with `Brotli = Burli`.
15
+ `Burli::Compressor`, `Burli::Decompressor` (with `output_buffer_limit:`),
16
+ `Burli::Writer`, and `Burli::Reader` work as in `brotli`. Calls release the
17
+ GVL while they encode or decode.
18
+
19
+ Differences from `brotli`:
20
+
21
+ - Qualities 6 to 11 run as 5, and the default quality is 5.
22
+ - `dictionary:` works for decompression only. Compression with it raises
23
+ `ArgumentError`.
24
+ - `Burli.inflate` raises on data after the stream. `Burli::Decompressor`
25
+ keeps such data in `#unused_data`, as in `brotli`.
26
+ - Error messages differ, and `Burli.version` returns the gem version.
27
+
28
+ Gems come precompiled for Linux (glibc and musl), macOS, and Windows. Other
29
+ platforms build from source with Rust 1.89 or later.
Binary file
Binary file
Binary file
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Burli
4
+ # Decompresses a stream read from an IO.
5
+ class Reader
6
+ # Compressed bytes read from the IO at a time, and the least decoded
7
+ # output each step asks for.
8
+ CHUNK_SIZE = 16 * 1024
9
+
10
+ # @param io [#read, #readpartial] where the compressed stream comes from
11
+ # @param options [Hash, nil] +dictionary+, as for {Burli.inflate}
12
+ def initialize(io, options = nil, **kwargs)
13
+ raise ArgumentError, "io should not be nil" if io.nil?
14
+
15
+ @io = io
16
+ @decompressor = Decompressor.new(Hash(options).merge(kwargs))
17
+ @buffer = "".b
18
+ @closed = false
19
+ end
20
+
21
+ # Reads +length+ decoded bytes, or all of them without +length+.
22
+ # @return [String, nil] nil at the end of the stream when +length+ is given
23
+ def read(length = nil, outbuf = nil)
24
+ ensure_open
25
+ if length.nil?
26
+ step(nil) until @decompressor.finished?
27
+ return output(outbuf, @buffer.bytesize)
28
+ end
29
+
30
+ length = Integer(length)
31
+ raise ArgumentError, "negative length #{length} given" if length.negative?
32
+ return output(outbuf, 0) if length.zero?
33
+
34
+ step(length - @buffer.bytesize) while @buffer.bytesize < length && !@decompressor.finished?
35
+ if @buffer.empty?
36
+ outbuf&.clear
37
+ return nil
38
+ end
39
+ output(outbuf, length)
40
+ end
41
+
42
+ # Reads at most +maxlen+ decoded bytes, waiting only until some arrive.
43
+ # @raise [EOFError] at the end of the stream
44
+ def readpartial(maxlen, outbuf = nil)
45
+ ensure_open
46
+ maxlen = Integer(maxlen)
47
+ raise ArgumentError, "max length must be positive" unless maxlen.positive?
48
+
49
+ step(maxlen) while @buffer.empty? && !@decompressor.finished?
50
+ if @buffer.empty?
51
+ outbuf&.clear
52
+ raise EOFError, "end of file reached"
53
+ end
54
+ output(outbuf, maxlen)
55
+ end
56
+
57
+ def eof?
58
+ ensure_open
59
+ step(1) while @buffer.empty? && !@decompressor.finished?
60
+ @buffer.empty?
61
+ end
62
+
63
+ # Closes the IO.
64
+ # @return [IO] the IO
65
+ def close
66
+ return @io if @closed
67
+
68
+ begin
69
+ @io.close if @io.respond_to?(:close)
70
+ ensure
71
+ @closed = true
72
+ @buffer.clear
73
+ end
74
+ @io
75
+ end
76
+
77
+ def closed?
78
+ @closed
79
+ end
80
+
81
+ private
82
+
83
+ def ensure_open
84
+ raise Error, "Reader is closed" if @closed
85
+ end
86
+
87
+ # Decodes into the buffer, at least +wanted+ bytes per step unless the
88
+ # input ends first. Reads input only when the decompressor takes it.
89
+ def step(wanted)
90
+ input = "".b
91
+ if @decompressor.can_accept_more_data
92
+ input = read_input
93
+ raise Error, "unexpected end of compressed stream" if input.nil? || input.empty?
94
+ end
95
+ limit = wanted && [wanted, CHUNK_SIZE].max
96
+ @buffer << @decompressor.process(input, output_buffer_limit: limit)
97
+ end
98
+
99
+ def read_input
100
+ return @io.read(CHUNK_SIZE) unless @io.respond_to?(:readpartial)
101
+
102
+ @io.readpartial(CHUNK_SIZE)
103
+ rescue EOFError
104
+ nil
105
+ end
106
+
107
+ # Takes up to +length+ bytes from the buffer.
108
+ def output(outbuf, length)
109
+ data = length >= @buffer.bytesize ? @buffer : @buffer.byteslice(0, length)
110
+ @buffer = length >= @buffer.bytesize ? "".b : @buffer.byteslice(length..)
111
+ return data unless outbuf
112
+
113
+ outbuf.replace(data)
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Burli
4
+ # The gem version.
5
+ VERSION = "0.1.0"
6
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Burli
4
+ # Compresses what is written to it into an IO.
5
+ class Writer
6
+ # @param io [#write] where the compressed stream goes
7
+ # @param options [Hash, nil] +mode+, +quality+, +lgwin+, and +lgblock+, as
8
+ # for {Burli.deflate}
9
+ def initialize(io, options = nil, **kwargs)
10
+ raise ArgumentError, "io should not be nil" if io.nil?
11
+
12
+ @io = io
13
+ @compressor = Compressor.new(Hash(options).merge(kwargs))
14
+ @closed = false
15
+ end
16
+
17
+ # Compresses +data+ and writes the output produced so far.
18
+ # @return [Integer] the bytes of +data+
19
+ def write(data)
20
+ ensure_open
21
+ data = String.try_convert(data) || raise(TypeError, "no implicit conversion of #{data.class} into String")
22
+ put(@compressor.process(data))
23
+ data.bytesize
24
+ end
25
+
26
+ # Writes output that decodes to all data so far, and flushes the IO.
27
+ # @return [self]
28
+ def flush
29
+ ensure_open
30
+ put(@compressor.flush)
31
+ @io.flush if @io.respond_to?(:flush)
32
+ self
33
+ end
34
+
35
+ # Ends the stream without closing the IO.
36
+ # @return [IO] the IO
37
+ def finish
38
+ ensure_open
39
+ put(@compressor.finish)
40
+ @io
41
+ end
42
+
43
+ # Ends the stream and closes the IO.
44
+ # @return [IO] the IO
45
+ def close
46
+ return @io if @closed
47
+
48
+ begin
49
+ finish
50
+ ensure
51
+ @closed = true
52
+ @io.close if @io.respond_to?(:close)
53
+ end
54
+ @io
55
+ end
56
+
57
+ def closed?
58
+ @closed
59
+ end
60
+
61
+ private
62
+
63
+ def ensure_open
64
+ raise Error, "Writer is closed" if @closed
65
+ end
66
+
67
+ def put(output)
68
+ @io.write(output) unless output.empty?
69
+ end
70
+ end
71
+ end
data/lib/burli.rb ADDED
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "burli/version"
4
+
5
+ # Precompiled gems hold one extension per Ruby minor version.
6
+ begin
7
+ require_relative "burli/#{RUBY_VERSION[/\A\d+\.\d+/]}/burli_native"
8
+ rescue LoadError
9
+ require_relative "burli/burli_native"
10
+ end
11
+
12
+ # Memory-safe Brotli compression and decompression, with the API of the
13
+ # brotli gem. Existing code can switch with:
14
+ #
15
+ # Brotli = Burli
16
+ #
17
+ # Qualities 6 to 11 run as 5.
18
+ module Burli
19
+ # Checks options, with the messages of the brotli gem, for the native
20
+ # methods.
21
+ module Options
22
+ MODES = {generic: 0, text: 1, font: 2}.freeze
23
+
24
+ module_function
25
+
26
+ def merge(options, kwargs)
27
+ Hash(options).merge(kwargs)
28
+ end
29
+
30
+ # @return [Array(Integer, Integer, Integer, Integer)] mode, quality,
31
+ # lgwin, and lgblock
32
+ def encoder(options)
33
+ unless options[:dictionary].nil?
34
+ raise ArgumentError, "burli does not support dictionaries for compression"
35
+ end
36
+
37
+ mode = options[:mode].nil? ? 0 : MODES.fetch(options[:mode]) { raise ArgumentError, "invalid mode" }
38
+ [
39
+ mode,
40
+ integer(options[:quality], 5, "invalid quality value. Should be 0 to 11.") { (0..11).cover?(_1) },
41
+ integer(options[:lgwin], 22, "invalid lgwin value. Should be 10 to 24.") { (10..24).cover?(_1) },
42
+ integer(options[:lgblock], 0, "invalid lgblock value. Should be 0 or 16 to 24.") do
43
+ _1.zero? || (16..24).cover?(_1)
44
+ end
45
+ ]
46
+ end
47
+
48
+ def integer(value, default, message)
49
+ return default if value.nil?
50
+ raise ArgumentError, message unless value.is_a?(Integer) && yield(value)
51
+
52
+ value
53
+ end
54
+
55
+ # @return [String, nil] raw dictionary bytes
56
+ def dictionary(options)
57
+ options[:dictionary]
58
+ end
59
+
60
+ # @return [Integer] the limit, or 0 for none
61
+ def output_buffer_limit(options)
62
+ limit = options[:output_buffer_limit]
63
+ return 0 if limit.nil?
64
+ raise TypeError, "output_buffer_limit must be an Integer" unless limit.is_a?(Integer)
65
+ raise ArgumentError, "output_buffer_limit must be positive" unless limit.positive?
66
+
67
+ limit
68
+ end
69
+ end
70
+ private_constant :Options
71
+
72
+ private_class_method :_native_deflate, :_native_inflate
73
+
74
+ class << self
75
+ # Compresses +string+.
76
+ # @param options [Hash, nil] +mode+ (+:generic+, +:text+, or +:font+),
77
+ # +quality+ (0 to 11), +lgwin+ (10 to 24), and +lgblock+ (0 or 16 to 24)
78
+ # @return [String] the Brotli stream
79
+ # @raise [ArgumentError] for invalid options
80
+ def deflate(string, options = nil, **kwargs)
81
+ raise ArgumentError, "input should not be nil" if string.nil?
82
+
83
+ _native_deflate(string, *Options.encoder(Options.merge(options, kwargs)))
84
+ end
85
+
86
+ # Decompresses a whole Brotli stream, from a String or an IO.
87
+ # @param options [Hash, nil] +dictionary+, a raw dictionary
88
+ # @return [String]
89
+ # @raise [Error] for malformed or truncated streams, and for data after
90
+ # the end of the stream
91
+ def inflate(string, options = nil, **kwargs)
92
+ string = string.read if string.respond_to?(:read)
93
+ _native_inflate(string, Options.dictionary(Options.merge(options, kwargs)))
94
+ end
95
+
96
+ # @return [String] the gem version
97
+ def version
98
+ VERSION
99
+ end
100
+ end
101
+
102
+ # Compresses a stream in pieces.
103
+ class Compressor
104
+ private_class_method :_native_new
105
+
106
+ # @param options [Hash, nil] as for {Burli.deflate}
107
+ def self.new(options = nil, **kwargs)
108
+ _native_new(*Options.encoder(Options.merge(options, kwargs)))
109
+ end
110
+ end
111
+
112
+ # Decompresses a stream in pieces.
113
+ class Decompressor
114
+ private_class_method :_native_new
115
+ private :_native_process
116
+
117
+ # @param options [Hash, nil] +dictionary+, a raw dictionary
118
+ def self.new(options = nil, **kwargs)
119
+ _native_new(Options.dictionary(Options.merge(options, kwargs)))
120
+ end
121
+
122
+ # Decompresses +input+, returning the output produced so far. With
123
+ # +output_buffer_limit+, returns at most that many bytes; then call with
124
+ # empty input until {#can_accept_more_data} is true before passing more.
125
+ # @raise [Error] for malformed streams, and for input while output is
126
+ # pending or after the end of the stream
127
+ def process(input, options = nil, **kwargs)
128
+ _native_process(input, Options.output_buffer_limit(Options.merge(options, kwargs)))
129
+ end
130
+
131
+ alias_method :is_finished, :finished?
132
+ alias_method :can_accept_more_data?, :can_accept_more_data
133
+ end
134
+ end
135
+
136
+ require_relative "burli/reader"
137
+ require_relative "burli/writer"
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: burli
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: x86_64-linux-musl
6
+ authors:
7
+ - Patrik Wenger
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-10-28 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - paddor@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - CHANGELOG.md
21
+ - README.md
22
+ - lib/burli.rb
23
+ - lib/burli/3.3/burli_native.so
24
+ - lib/burli/3.4/burli_native.so
25
+ - lib/burli/4.0/burli_native.so
26
+ - lib/burli/reader.rb
27
+ - lib/burli/version.rb
28
+ - lib/burli/writer.rb
29
+ homepage: https://github.com/paddor/burli/tree/main/bindings/ruby
30
+ licenses:
31
+ - MIT
32
+ metadata:
33
+ source_code_uri: https://github.com/paddor/burli/tree/main/bindings/ruby
34
+ changelog_uri: https://github.com/paddor/burli/tree/main/bindings/ruby/CHANGELOG.md
35
+ rubygems_mfa_required: 'true'
36
+ post_install_message:
37
+ rdoc_options: []
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: '3.3'
45
+ - - "<"
46
+ - !ruby/object:Gem::Version
47
+ version: 4.1.dev
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 3.3.22
53
+ requirements: []
54
+ rubygems_version: 3.5.23
55
+ signing_key:
56
+ specification_version: 4
57
+ summary: Memory-safe Brotli compression in Rust, with the API of the brotli gem
58
+ test_files: []