ruby-lzws 1.0.0

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