ruby-lzws 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,66 @@
1
+ # Ruby bindings for lzws library.
2
+ # Copyright (c) 2019 AUTHORS, MIT License.
3
+
4
+ require_relative "error"
5
+ require_relative "validation"
6
+
7
+ module LZWS
8
+ module Option
9
+ # Default options will be compatible with UNIX compress.
10
+
11
+ LOWEST_MAX_CODE_BIT_LENGTH = 9
12
+ BIGGEST_MAX_CODE_BIT_LENGTH = 16
13
+
14
+ DECOMPRESSOR_DEFAULTS = {
15
+ :buffer_length => 0,
16
+ :without_magic_header => false,
17
+ :msb => false,
18
+ :unaligned_bit_groups => false,
19
+ :quiet => false
20
+ }
21
+ .freeze
22
+
23
+ COMPRESSOR_DEFAULTS = DECOMPRESSOR_DEFAULTS.merge(
24
+ :max_code_bit_length => BIGGEST_MAX_CODE_BIT_LENGTH,
25
+ :block_mode => true
26
+ )
27
+ .freeze
28
+
29
+ def self.get_compressor_options(options)
30
+ Validation.validate_hash options
31
+
32
+ options = COMPRESSOR_DEFAULTS.merge options
33
+
34
+ Validation.validate_not_negative_integer options[:buffer_length]
35
+
36
+ max_code_bit_length = options[:max_code_bit_length]
37
+ Validation.validate_positive_integer max_code_bit_length
38
+
39
+ raise ValidateError, "invalid max code bit length" if
40
+ max_code_bit_length < LOWEST_MAX_CODE_BIT_LENGTH ||
41
+ max_code_bit_length > BIGGEST_MAX_CODE_BIT_LENGTH
42
+
43
+ Validation.validate_bool options[:without_magic_header]
44
+ Validation.validate_bool options[:block_mode]
45
+ Validation.validate_bool options[:msb]
46
+ Validation.validate_bool options[:unaligned_bit_groups]
47
+ Validation.validate_bool options[:quiet]
48
+
49
+ options
50
+ end
51
+
52
+ def self.get_decompressor_options(options)
53
+ Validation.validate_hash options
54
+
55
+ options = DECOMPRESSOR_DEFAULTS.merge options
56
+
57
+ Validation.validate_not_negative_integer options[:buffer_length]
58
+ Validation.validate_bool options[:without_magic_header]
59
+ Validation.validate_bool options[:msb]
60
+ Validation.validate_bool options[:unaligned_bit_groups]
61
+ Validation.validate_bool options[:quiet]
62
+
63
+ options
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,153 @@
1
+ # Ruby bindings for lzws library.
2
+ # Copyright (c) 2019 AUTHORS, MIT License.
3
+
4
+ require_relative "delegates"
5
+ require_relative "stat"
6
+ require_relative "../error"
7
+ require_relative "../validation"
8
+
9
+ module LZWS
10
+ module Stream
11
+ class Abstract
12
+ # LZWS native stream is not seekable by design.
13
+ # Related methods like "seek" and "pos=" can't be implemented.
14
+
15
+ # It is not possible to maintain correspondance between bytes consumed from source and bytes written to destination by design.
16
+ # We will consume all source bytes and maintain buffer with remaining destination data.
17
+
18
+ include Delegates
19
+
20
+ attr_reader :io
21
+ attr_reader :stat
22
+ attr_reader :external_encoding
23
+ attr_reader :internal_encoding
24
+ attr_reader :pos
25
+ alias tell pos
26
+
27
+ def initialize(io, external_encoding: nil, internal_encoding: nil, transcode_options: {})
28
+ @raw_stream = create_raw_stream
29
+
30
+ Validation.validate_io io
31
+ @io = io
32
+
33
+ @stat = Stat.new @io.stat
34
+
35
+ set_encoding external_encoding, internal_encoding, transcode_options
36
+ reset_buffer
37
+ reset_io_advise
38
+
39
+ @pos = 0
40
+ end
41
+
42
+ # -- buffer --
43
+
44
+ protected def reset_buffer
45
+ @buffer = ::String.new :encoding => ::Encoding::BINARY
46
+ end
47
+
48
+ # -- advise --
49
+
50
+ protected def reset_io_advise
51
+ # Both compressor and decompressor need sequential io access.
52
+ @io.advise :sequential
53
+ rescue ::Errno::ESPIPE # rubocop:disable Lint/HandleExceptions
54
+ # ok
55
+ end
56
+
57
+ def advise
58
+ # Noop
59
+ nil
60
+ end
61
+
62
+ # -- encoding --
63
+
64
+ def set_encoding(*args)
65
+ external_encoding, internal_encoding, transcode_options = process_set_encoding_arguments(*args)
66
+
67
+ set_target_encoding :@external_encoding, external_encoding
68
+ set_target_encoding :@internal_encoding, internal_encoding
69
+ @transcode_options = transcode_options
70
+
71
+ self
72
+ end
73
+
74
+ protected def process_set_encoding_arguments(*args)
75
+ external_encoding = args[0]
76
+
77
+ unless external_encoding.nil? || external_encoding.is_a?(::Encoding)
78
+ Validation.validate_string external_encoding
79
+
80
+ # First argument can be "external_encoding:internal_encoding".
81
+ match = %r{(.+?):(.+)}.match external_encoding
82
+
83
+ unless match.nil?
84
+ external_encoding = match[0]
85
+ internal_encoding = match[1]
86
+
87
+ transcode_options = args[1]
88
+ Validation.validate_hash transcode_options unless transcode_options.nil?
89
+
90
+ return [external_encoding, internal_encoding, transcode_options]
91
+ end
92
+ end
93
+
94
+ internal_encoding = args[1]
95
+ Validation.validate_string internal_encoding \
96
+ unless internal_encoding.nil? || internal_encoding.is_a?(::Encoding)
97
+
98
+ transcode_options = args[2]
99
+ Validation.validate_hash transcode_options unless transcode_options.nil?
100
+
101
+ [external_encoding, internal_encoding, transcode_options]
102
+ end
103
+
104
+ protected def set_target_encoding(name, value)
105
+ unless value.nil? || value.is_a?(::Encoding)
106
+ begin
107
+ value = ::Encoding.find value
108
+ rescue ::ArgumentError
109
+ raise ValidateError, "invalid #{name} encoding"
110
+ end
111
+ end
112
+
113
+ instance_variable_set name, value
114
+ end
115
+
116
+ protected def target_encoding
117
+ return @internal_encoding unless @internal_encoding.nil?
118
+
119
+ return @external_encoding unless @external_encoding.nil?
120
+
121
+ ::Encoding::BINARY
122
+ end
123
+
124
+ # -- etc --
125
+
126
+ def rewind
127
+ @raw_stream = create_raw_stream
128
+
129
+ @io.rewind
130
+ reset_buffer
131
+ reset_io_advise
132
+
133
+ @pos = 0
134
+
135
+ 0
136
+ end
137
+
138
+ def close
139
+ @io.close
140
+
141
+ nil
142
+ end
143
+
144
+ def closed?
145
+ @raw_stream.closed? && @io.closed?
146
+ end
147
+
148
+ def to_io
149
+ self
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,36 @@
1
+ # Ruby bindings for lzws library.
2
+ # Copyright (c) 2019 AUTHORS, MIT License.
3
+
4
+ require "forwardable"
5
+
6
+ module LZWS
7
+ module Stream
8
+ module Delegates
9
+ DELEGATES = %i[
10
+ autoclose=
11
+ autoclose?
12
+ binmode
13
+ binmode?
14
+ close_on_exec=
15
+ close_on_exec?
16
+ fcntl
17
+ fdatasync
18
+ fileno
19
+ fsync
20
+ ioctl
21
+ isatty
22
+ pid
23
+ sync
24
+ sync=
25
+ to_i
26
+ tty?
27
+ ]
28
+ .freeze
29
+
30
+ def self.included(klass)
31
+ klass.extend ::Forwardable
32
+ klass.def_delegators :@io, *DELEGATES
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,46 @@
1
+ # Ruby bindings for lzws library.
2
+ # Copyright (c) 2019 AUTHORS, MIT License.
3
+
4
+ require "lzws_ext"
5
+
6
+ require_relative "../../error"
7
+
8
+ module LZWS
9
+ module Stream
10
+ module Raw
11
+ class Abstract
12
+ def initialize(native_stream)
13
+ @native_stream = native_stream
14
+ @is_closed = false
15
+ end
16
+
17
+ def close(&writer)
18
+ return nil if @is_closed
19
+
20
+ flush(&writer)
21
+
22
+ @native_stream.close
23
+ @is_closed = true
24
+
25
+ nil
26
+ end
27
+
28
+ def closed?
29
+ @is_closed
30
+ end
31
+
32
+ protected def flush_destination_buffer(&writer)
33
+ result_bytesize = write_result(&writer)
34
+ raise NotEnoughDestinationError, "not enough destination" if result_bytesize == 0
35
+ end
36
+
37
+ protected def write_result(&_writer)
38
+ result = @native_stream.read_result
39
+ yield result
40
+
41
+ result.bytesize
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,100 @@
1
+ # Ruby bindings for lzws library.
2
+ # Copyright (c) 2019 AUTHORS, MIT License.
3
+
4
+ require "lzws_ext"
5
+
6
+ require_relative "abstract"
7
+ require_relative "../../error"
8
+ require_relative "../../option"
9
+ require_relative "../../validation"
10
+
11
+ module LZWS
12
+ module Stream
13
+ module Raw
14
+ class Compressor < Abstract
15
+ def initialize(options = {})
16
+ options = Option.get_compressor_options options
17
+ native_stream = NativeCompressor.new options
18
+
19
+ super native_stream
20
+
21
+ @need_to_write_magic_header = !options[:without_magic_header]
22
+ end
23
+
24
+ def write(source, &writer)
25
+ do_not_use_after_close
26
+
27
+ Validation.validate_string source
28
+ Validation.validate_proc writer
29
+
30
+ if @need_to_write_magic_header
31
+ write_magic_header(&writer)
32
+ @need_to_write_magic_header = false
33
+ end
34
+
35
+ total_bytes_written = 0
36
+
37
+ loop do
38
+ bytes_written, need_more_destination = @native_stream.write source
39
+ total_bytes_written += bytes_written
40
+
41
+ if need_more_destination
42
+ source = source.byteslice bytes_written, source.bytesize - bytes_written
43
+ flush_destination_buffer(&writer)
44
+ next
45
+ end
46
+
47
+ unless bytes_written == source.bytesize
48
+ # Compressor write should eat all provided "source" without remainder.
49
+ raise UnexpectedError, "unexpected error"
50
+ end
51
+
52
+ break
53
+ end
54
+
55
+ total_bytes_written
56
+ end
57
+
58
+ protected def write_magic_header(&writer)
59
+ loop do
60
+ need_more_destination = @native_stream.write_magic_header
61
+
62
+ if need_more_destination
63
+ flush_destination_buffer(&writer)
64
+ next
65
+ end
66
+
67
+ break
68
+ end
69
+
70
+ nil
71
+ end
72
+
73
+ def flush(&writer)
74
+ do_not_use_after_close
75
+
76
+ Validation.validate_proc writer
77
+
78
+ loop do
79
+ need_more_destination = @native_stream.flush
80
+
81
+ if need_more_destination
82
+ flush_destination_buffer(&writer)
83
+ next
84
+ end
85
+
86
+ break
87
+ end
88
+
89
+ write_result(&writer)
90
+
91
+ nil
92
+ end
93
+
94
+ protected def do_not_use_after_close
95
+ raise UsedAfterCloseError, "compressor used after close" if @is_closed
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,78 @@
1
+ # Ruby bindings for lzws library.
2
+ # Copyright (c) 2019 AUTHORS, MIT License.
3
+
4
+ require "lzws_ext"
5
+
6
+ require_relative "abstract"
7
+ require_relative "../../error"
8
+ require_relative "../../option"
9
+ require_relative "../../validation"
10
+
11
+ module LZWS
12
+ module Stream
13
+ module Raw
14
+ class Decompressor < Abstract
15
+ def initialize(options = {})
16
+ options = Option.get_decompressor_options options
17
+ native_stream = NativeDecompressor.new options
18
+
19
+ super native_stream
20
+
21
+ @need_to_read_magic_header = !options[:without_magic_header]
22
+ end
23
+
24
+ def read(source, &writer)
25
+ do_not_use_after_close
26
+
27
+ Validation.validate_string source
28
+ Validation.validate_proc writer
29
+
30
+ total_bytes_read = 0
31
+
32
+ if @need_to_read_magic_header
33
+ bytes_read = @native_stream.read_magic_header source
34
+ if bytes_read == 0
35
+ # Decompressor is not able to read full magic header.
36
+ return 0
37
+ end
38
+
39
+ total_bytes_read += bytes_read
40
+ source = source.byteslice bytes_read, source.bytesize - bytes_read
41
+
42
+ @need_to_read_magic_header = false
43
+ end
44
+
45
+ loop do
46
+ bytes_read, need_more_destination = @native_stream.read source
47
+ total_bytes_read += bytes_read
48
+
49
+ if need_more_destination
50
+ source = source.byteslice bytes_read, source.bytesize - bytes_read
51
+ flush_destination_buffer(&writer)
52
+ next
53
+ end
54
+
55
+ break
56
+ end
57
+
58
+ # Please remember that "total_bytes_read" can not be equal to "source.bytesize".
59
+ total_bytes_read
60
+ end
61
+
62
+ def flush(&writer)
63
+ do_not_use_after_close
64
+
65
+ Validation.validate_proc writer
66
+
67
+ write_result(&writer)
68
+
69
+ nil
70
+ end
71
+
72
+ protected def do_not_use_after_close
73
+ raise UsedAfterCloseError, "decompressor used after close" if @is_closed
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,163 @@
1
+ # Ruby bindings for lzws library.
2
+ # Copyright (c) 2019 AUTHORS, MIT License.
3
+
4
+ require_relative "abstract"
5
+ require_relative "raw/decompressor"
6
+ require_relative "reader_helpers"
7
+ require_relative "../validation"
8
+
9
+ module LZWS
10
+ module Stream
11
+ class Reader < Abstract
12
+ include ReaderHelpers
13
+
14
+ DEFAULT_IO_PORTION_BYTESIZE = 1 << 15 # 32 KB
15
+
16
+ attr_accessor :lineno
17
+
18
+ def initialize(source_io, options = {}, *args)
19
+ @options = options
20
+
21
+ super source_io, *args
22
+
23
+ io_portion_bytesize = @options[:io_portion_bytesize]
24
+ @options.delete :io_portion_bytesize
25
+
26
+ Validation.validate_positive_integer io_portion_bytesize unless io_portion_bytesize.nil?
27
+ @io_portion_bytesize = io_portion_bytesize || DEFAULT_IO_PORTION_BYTESIZE
28
+
29
+ reset_io_remainder
30
+
31
+ @lineno = 0
32
+ end
33
+
34
+ protected def create_raw_stream
35
+ Raw::Decompressor.new @options
36
+ end
37
+
38
+ protected def reset_io_remainder
39
+ @io_remainder = ::String.new :encoding => ::Encoding::BINARY
40
+ end
41
+
42
+ # -- synchronous --
43
+
44
+ def read(bytes_to_read = nil, out_buffer = nil)
45
+ Validation.validate_not_negative_integer bytes_to_read unless bytes_to_read.nil?
46
+ Validation.validate_string out_buffer unless out_buffer.nil?
47
+
48
+ return ::String.new :encoding => ::Encoding::BINARY if bytes_to_read == 0
49
+
50
+ unless bytes_to_read.nil?
51
+ return nil if eof?
52
+
53
+ read_more_from_buffer until @buffer.bytesize >= bytes_to_read || @io.eof?
54
+
55
+ return read_bytes_from_buffer bytes_to_read, out_buffer
56
+ end
57
+
58
+ read_more_from_buffer until @io.eof?
59
+
60
+ result = @buffer
61
+ reset_buffer
62
+ @pos += result.bytesize
63
+
64
+ result.force_encoding @external_encoding unless @external_encoding.nil?
65
+ result = transcode_to_internal result
66
+ result = out_buffer.replace result unless out_buffer.nil?
67
+
68
+ result
69
+ end
70
+
71
+ protected def read_more_from_buffer
72
+ io_data = @io.read @io_portion_bytesize
73
+ append_io_data_to_buffer io_data
74
+ end
75
+
76
+ def readpartial(bytes_to_read = nil, out_buffer = nil)
77
+ raise ::EOFError if eof?
78
+
79
+ readpartial_from_buffer until @buffer.bytesize >= bytes_to_read || @io.eof?
80
+
81
+ read_bytes_from_buffer bytes_to_read, out_buffer
82
+ end
83
+
84
+ protected def readpartial_from_buffer
85
+ io_data = @io.readpartial @io_portion_bytesize
86
+ append_io_data_to_buffer io_data
87
+ end
88
+
89
+ def rewind
90
+ raw_wrapper :close
91
+
92
+ reset_io_remainder
93
+
94
+ super
95
+ end
96
+
97
+ def close
98
+ raw_wrapper :close
99
+
100
+ super
101
+ end
102
+
103
+ # -- asynchronous --
104
+
105
+ def read_nonblock(bytes_to_read, out_buffer = nil, *options)
106
+ raise ::EOFError if eof?
107
+
108
+ read_more_from_buffer_nonblock(*options) until @buffer.bytesize >= bytes_to_read || @io.eof?
109
+
110
+ read_bytes_from_buffer bytes_to_read, out_buffer
111
+ end
112
+
113
+ protected def read_more_from_buffer_nonblock(*options)
114
+ io_data = @io.read_nonblock @io_portion_bytesize, *options
115
+ append_io_data_to_buffer io_data
116
+ end
117
+
118
+ # -- common --
119
+
120
+ def eof?
121
+ @io.eof? && @buffer.bytesize == 0
122
+ end
123
+
124
+ protected def read_bytes_from_buffer(bytes_to_read, out_buffer)
125
+ bytes_read = [@buffer.bytesize, bytes_to_read].min
126
+
127
+ # Result uses buffer binary encoding.
128
+ result = @buffer.byteslice 0, bytes_read
129
+ @buffer = @buffer.byteslice bytes_read, @buffer.bytesize - bytes_read
130
+ @pos += bytes_read
131
+
132
+ result = out_buffer.replace result unless out_buffer.nil?
133
+ result
134
+ end
135
+
136
+ protected def append_io_data_to_buffer(io_data)
137
+ io_portion = @io_remainder + io_data
138
+ bytes_read = raw_wrapper :read, io_portion
139
+ @io_remainder = io_portion.byteslice bytes_read, io_portion.bytesize - bytes_read
140
+
141
+ # We should just ignore case when "io.eof?" appears but "io_remainder" is not empty.
142
+ # Ancient compress implementations can write bytes from not initialized buffer parts to output.
143
+ raw_wrapper :flush if @io.eof?
144
+ end
145
+
146
+ protected def transcode_to_internal(data)
147
+ data = data.encode @internal_encoding, @transcode_options unless @internal_encoding.nil?
148
+ data
149
+ end
150
+
151
+ # We should be able to return data back to buffer.
152
+ # We won't use any transcode options because transcoded data should be backward compatible.
153
+ protected def transcode_to_external(data)
154
+ data = data.encode @external_encoding unless @external_encoding.nil?
155
+ data
156
+ end
157
+
158
+ protected def raw_wrapper(method_name, *args)
159
+ @raw_stream.send(method_name, *args) { |portion| @buffer << portion }
160
+ end
161
+ end
162
+ end
163
+ end