burli 0.1.0-x86_64-darwin
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 +7 -0
- data/CHANGELOG.md +11 -0
- data/README.md +29 -0
- data/lib/burli/3.3/burli_native.bundle +0 -0
- data/lib/burli/3.4/burli_native.bundle +0 -0
- data/lib/burli/4.0/burli_native.bundle +0 -0
- data/lib/burli/reader.rb +116 -0
- data/lib/burli/version.rb +6 -0
- data/lib/burli/writer.rb +71 -0
- data/lib/burli.rb +137 -0
- metadata +58 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8d23cefd71fa6a78daf6c55fe79e0af31913ba575d43c0a7fcb48dd1d923a4be
|
|
4
|
+
data.tar.gz: ffdee64a068d7f1a99b9c7be23ab8532b3464fd43ce75951aef6e32e4f50e4ef
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6436b89f39b2eb9031de25daed26f187f50866ed3d1803b30c8e265dd22bddee4cb1b5aec12ce00e90fb14b6172179c593b710cfb07a8d87af0ed2e5007b2f0e
|
|
7
|
+
data.tar.gz: '093d315a2b3dbee4d033b228094c3249655a9bba42ce96dc72b27c744774f373fbff9e834219ff175cb37a2d2b865b939a18008e65d9fea32101ce4bcf81f224'
|
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
|
data/lib/burli/reader.rb
ADDED
|
@@ -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
|
data/lib/burli/writer.rb
ADDED
|
@@ -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-darwin
|
|
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.bundle
|
|
24
|
+
- lib/burli/3.4/burli_native.bundle
|
|
25
|
+
- lib/burli/4.0/burli_native.bundle
|
|
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: '0'
|
|
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: []
|