ffi_libarchive 1.1.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.
- checksums.yaml +7 -0
- data/LICENSE +201 -0
- data/README.md +87 -0
- data/lib/ffi_libarchive.rb +8 -0
- data/lib/ffi_libarchive/api.rb +282 -0
- data/lib/ffi_libarchive/archive.rb +182 -0
- data/lib/ffi_libarchive/entry.rb +575 -0
- data/lib/ffi_libarchive/reader.rb +227 -0
- data/lib/ffi_libarchive/utils.rb +119 -0
- data/lib/ffi_libarchive/writer.rb +178 -0
- metadata +130 -0
@@ -0,0 +1,227 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Archive
|
4
|
+
class Reader < BaseArchive
|
5
|
+
private_class_method :new
|
6
|
+
|
7
|
+
# @param [String] file_name
|
8
|
+
# @return [Reader]
|
9
|
+
# @yieldparam [Reader]
|
10
|
+
def self.open_filename(file_name, command = nil)
|
11
|
+
if block_given?
|
12
|
+
reader = open_filename file_name, command
|
13
|
+
begin
|
14
|
+
yield reader
|
15
|
+
ensure
|
16
|
+
reader.close if reader.respond_to?(:close)
|
17
|
+
end
|
18
|
+
else
|
19
|
+
new file_name: file_name, command: command
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param [String] string
|
24
|
+
# @return [Reader]
|
25
|
+
# @yieldparam [Reader]
|
26
|
+
def self.open_memory(string, command = nil)
|
27
|
+
if block_given?
|
28
|
+
reader = open_memory string, command
|
29
|
+
begin
|
30
|
+
yield reader
|
31
|
+
ensure
|
32
|
+
reader.close if reader.respond_to?(:close)
|
33
|
+
end
|
34
|
+
else
|
35
|
+
new memory: string, command: command
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# @param [#call] stream
|
40
|
+
# @return [Reader]
|
41
|
+
# @yieldparam [Reader]
|
42
|
+
def self.open_stream(stream, command = nil)
|
43
|
+
if block_given?
|
44
|
+
reader = open_stream stream, command
|
45
|
+
begin
|
46
|
+
yield reader
|
47
|
+
ensure
|
48
|
+
reader.close if reader.respond_to?(:close)
|
49
|
+
end
|
50
|
+
else
|
51
|
+
new reader: stream, command: command
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# @param [Hash] params
|
56
|
+
# @option params [Object] :command
|
57
|
+
# @option params [String] :file_name
|
58
|
+
# @option params [String] :memory
|
59
|
+
# @option params [#call] :reader
|
60
|
+
def initialize(params = {})
|
61
|
+
super C.method(:archive_read_new), C.method(:archive_read_free)
|
62
|
+
|
63
|
+
begin
|
64
|
+
init_compression params[:command]
|
65
|
+
init_format
|
66
|
+
|
67
|
+
if params[:file_name]
|
68
|
+
init_for_filename params[:file_name]
|
69
|
+
elsif params[:memory]
|
70
|
+
init_for_memory params[:memory]
|
71
|
+
elsif params[:reader]
|
72
|
+
init_for_stream params[:reader]
|
73
|
+
end
|
74
|
+
rescue StandardError
|
75
|
+
close
|
76
|
+
raise
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# @param [Entry] entry
|
81
|
+
# @param [Integer] flags see ::EXTRACT_*
|
82
|
+
def extract(entry, flags = 0)
|
83
|
+
raise ArgumentError, 'Expected Archive::Entry as first argument' unless entry.is_a? Entry
|
84
|
+
raise ArgumentError, 'Expected Integer as second argument' unless flags.is_a? Integer
|
85
|
+
|
86
|
+
flags |= EXTRACT_FFLAGS
|
87
|
+
raise Error, self if C.archive_read_extract(archive, entry.entry, flags) != C::OK
|
88
|
+
end
|
89
|
+
|
90
|
+
# Retrieve the byte offset in UNCOMPRESSED data where last-read header started.
|
91
|
+
# @return [Integer]
|
92
|
+
def header_position
|
93
|
+
C.archive_read_header_position archive
|
94
|
+
end
|
95
|
+
|
96
|
+
# @return [Entry]
|
97
|
+
def next_header
|
98
|
+
entry_ptr = FFI::MemoryPointer.new(:pointer)
|
99
|
+
|
100
|
+
case C.archive_read_next_header(archive, entry_ptr)
|
101
|
+
when C::OK
|
102
|
+
Entry.from_pointer entry_ptr.get_pointer(0)
|
103
|
+
when C::EOF
|
104
|
+
nil
|
105
|
+
else
|
106
|
+
raise Error, self
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# @yieldparam [Entry]
|
111
|
+
def each_entry
|
112
|
+
while (entry = next_header)
|
113
|
+
yield entry
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# @yieldparam [Entry] entry
|
118
|
+
# @yieldparam [String] data
|
119
|
+
def each_entry_with_data(size = C::DATA_BUFFER_SIZE)
|
120
|
+
while (entry = next_header)
|
121
|
+
yield entry, read_data(size)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# @yieldparam [Entry] entry
|
126
|
+
def each_entry_skip_data
|
127
|
+
while (entry = next_header)
|
128
|
+
begin
|
129
|
+
yield entry
|
130
|
+
ensure
|
131
|
+
C.archive_read_data_skip archive
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# @return [String, Integer]
|
137
|
+
# @yieldparam [String] chunk
|
138
|
+
def read_data(size = C::DATA_BUFFER_SIZE)
|
139
|
+
raise ArgumentError, "Buffer size must be > 0 (was: #{size})" if !size.is_a?(Integer) || size <= 0
|
140
|
+
|
141
|
+
data = nil
|
142
|
+
buffer = FFI::MemoryPointer.new(:char, size)
|
143
|
+
len = 0
|
144
|
+
|
145
|
+
while (n = C.archive_read_data(archive, buffer, size)) != 0
|
146
|
+
# TODO: C::FATAL, C::WARN, C::RETRY
|
147
|
+
raise Error, self if n < 0
|
148
|
+
|
149
|
+
chunk = buffer.get_bytes(0, n)
|
150
|
+
if block_given?
|
151
|
+
yield chunk
|
152
|
+
elsif data
|
153
|
+
data << chunk
|
154
|
+
else
|
155
|
+
data = chunk.dup
|
156
|
+
end
|
157
|
+
|
158
|
+
len += n
|
159
|
+
end
|
160
|
+
|
161
|
+
data || len
|
162
|
+
end
|
163
|
+
|
164
|
+
# @param [String] file_name
|
165
|
+
def save_data(file_name)
|
166
|
+
File.open(file_name, 'wb') do |f|
|
167
|
+
raise Error, self if C.archive_read_data_into_fd(archive, f.fileno) != C::OK
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
protected
|
172
|
+
|
173
|
+
def init_compression(command)
|
174
|
+
if command && !(cmd = command.to_s).empty?
|
175
|
+
raise Error, self if C.archive_read_support_compression_program(archive, cmd) != C::OK
|
176
|
+
elsif C.respond_to?(:archive_read_support_filter_all)
|
177
|
+
raise Error, self if C.archive_read_support_filter_all(archive) != C::OK
|
178
|
+
elsif C.archive_read_support_compression_all(archive) != C::OK
|
179
|
+
raise Error, self
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def init_format
|
184
|
+
raise Error, self if C.archive_read_support_format_all(archive) != C::OK
|
185
|
+
end
|
186
|
+
|
187
|
+
BLOCK_SIZE = 1024
|
188
|
+
|
189
|
+
def init_for_filename(file_name)
|
190
|
+
raise Error, self if C.archive_read_open_filename(archive, file_name, BLOCK_SIZE) != C::OK
|
191
|
+
end
|
192
|
+
|
193
|
+
def init_for_memory(string)
|
194
|
+
buffer = Utils.get_memory_ptr(string)
|
195
|
+
raise Error, self if C.archive_read_open_memory(archive, buffer, string.bytesize) != C::OK
|
196
|
+
end
|
197
|
+
|
198
|
+
def init_for_stream(reader)
|
199
|
+
read_callback = proc do |_ar, _client_data, buffer|
|
200
|
+
# @type [String]
|
201
|
+
data = reader.call
|
202
|
+
|
203
|
+
if data.is_a?(String)
|
204
|
+
buffer.put_pointer 0, Utils.get_memory_ptr(data)
|
205
|
+
data.bytesize
|
206
|
+
else
|
207
|
+
0
|
208
|
+
end
|
209
|
+
end
|
210
|
+
raise Error, self if C.archive_read_set_read_callback(archive, read_callback) != C::OK
|
211
|
+
|
212
|
+
if reader.respond_to?(:skip)
|
213
|
+
skip_callback = proc { |_ar, _client_data, request| reader.skip(request) }
|
214
|
+
raise Error, self if C.archive_read_set_skip_callback(archive, skip_callback) != C::OK
|
215
|
+
end
|
216
|
+
|
217
|
+
if reader.respond_to?(:seek)
|
218
|
+
seek_callback = proc { |_ar, _client_data, offset, whence| reader.seek(offset, whence) }
|
219
|
+
raise Error, self if C.archive_read_set_seek_callback(archive, seek_callback) != C::OK
|
220
|
+
end
|
221
|
+
|
222
|
+
# Required or open1 will segfault, even though the callback data is not used.
|
223
|
+
raise Error, self if C.archive_read_set_callback_data(archive, FFI::Pointer::NULL) != C::OK
|
224
|
+
raise Error, self if C.archive_read_open1(archive) != C::OK
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ffi'
|
4
|
+
|
5
|
+
module Archive
|
6
|
+
module Utils
|
7
|
+
module LibC
|
8
|
+
extend FFI::Library
|
9
|
+
ffi_lib FFI::Library::LIBC
|
10
|
+
|
11
|
+
# @!method mbstowcs(dest, src, max)
|
12
|
+
# Convert multibyte string to wide-character string
|
13
|
+
# @param [FFI::Pointer] dest of :wchar_t*
|
14
|
+
# @param [String] src
|
15
|
+
# @param [Integer] max Maximum number of :wchar_t characters to write to dest
|
16
|
+
# @return [Integer] Number of wide characters written to dest (excluding null-terminated), or -1 if error
|
17
|
+
attach_function :mbstowcs, [:pointer, :string, :size_t], :size_t
|
18
|
+
|
19
|
+
# @!method chdir(path)
|
20
|
+
# Changes the current working directory
|
21
|
+
# @param [String] path
|
22
|
+
# @return [Integer] 0 if successful. Otherwise, -1
|
23
|
+
begin
|
24
|
+
attach_function :chdir, [:string], :int
|
25
|
+
rescue FFI::NotFoundError
|
26
|
+
attach_function :chdir, :_chdir, [:string], :int
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
WCHAR_ENCODINGS = {
|
31
|
+
'UTF-16LE' => "!\x00@\x00\x00\x00",
|
32
|
+
'UTF-16BE' => "\x00!\x00@\x00\x00",
|
33
|
+
'UTF-32LE' => "!\x00\x00\x00@\x00",
|
34
|
+
'UTF-32BE' => "\x00\x00\x00!\x00\x00"
|
35
|
+
}.freeze
|
36
|
+
|
37
|
+
class << self
|
38
|
+
# @return [String]
|
39
|
+
def wchar_encoding
|
40
|
+
@wchar_encoding ||=
|
41
|
+
begin
|
42
|
+
ptr = FFI::MemoryPointer.new :char, 12
|
43
|
+
rc = LibC.mbstowcs ptr, '!@', 3
|
44
|
+
|
45
|
+
str = ptr.get_bytes 0, 6
|
46
|
+
enc = WCHAR_ENCODINGS.key(str)
|
47
|
+
raise "Unsupported wide-character: #{rc} - #{str.inspect}" unless enc
|
48
|
+
|
49
|
+
enc
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# @param [FFI::Pointer] ptr
|
54
|
+
# @return [String]
|
55
|
+
def read_wide_string(ptr)
|
56
|
+
return nil if !ptr.respond_to?(:null?) || ptr.null?
|
57
|
+
|
58
|
+
if wchar_encoding.include?('32')
|
59
|
+
wchar_sz = 4
|
60
|
+
wchar_t = :int32
|
61
|
+
else
|
62
|
+
wchar_sz = 2
|
63
|
+
wchar_t = :int16
|
64
|
+
end
|
65
|
+
|
66
|
+
# detect string length in bytes
|
67
|
+
sz = ptr.size
|
68
|
+
sz -= wchar_sz if sz
|
69
|
+
|
70
|
+
len = 0
|
71
|
+
len += wchar_sz while (!sz || len < sz) && ptr.send("get_#{wchar_t}", len) != 0
|
72
|
+
|
73
|
+
ptr.get_bytes(0, len).force_encoding(wchar_encoding)
|
74
|
+
end
|
75
|
+
|
76
|
+
# @param [String] str
|
77
|
+
# @return [String]
|
78
|
+
def to_wide_string(str)
|
79
|
+
str ? str.encode(wchar_encoding) : str
|
80
|
+
end
|
81
|
+
|
82
|
+
# @param [String] dir
|
83
|
+
# @return [Integer]
|
84
|
+
def change_cwd(dir)
|
85
|
+
if block_given?
|
86
|
+
# @type [String]
|
87
|
+
cwd = Dir.getwd
|
88
|
+
change_cwd dir
|
89
|
+
|
90
|
+
begin
|
91
|
+
yield
|
92
|
+
ensure
|
93
|
+
change_cwd cwd
|
94
|
+
end
|
95
|
+
else
|
96
|
+
rc = Dir.chdir dir
|
97
|
+
rc != 0 ? rc : LibC.chdir(dir)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# @param [String] string MANDATORY
|
102
|
+
# @return [FFI::Pointer]
|
103
|
+
# @yieldparam [FFI::Pointer]
|
104
|
+
def get_memory_ptr(string)
|
105
|
+
len = string.bytesize
|
106
|
+
if block_given?
|
107
|
+
FFI::MemoryPointer.new(:char, len, false) do |ptr|
|
108
|
+
ptr.put_bytes(0, string, 0, len)
|
109
|
+
yield ptr
|
110
|
+
end
|
111
|
+
else
|
112
|
+
ptr = FFI::MemoryPointer.new(:char, len, false)
|
113
|
+
ptr.put_bytes(0, string, 0, len)
|
114
|
+
ptr
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Archive
|
4
|
+
class Writer < BaseArchive
|
5
|
+
private_class_method :new
|
6
|
+
|
7
|
+
# @param [String] file_name
|
8
|
+
# @return [Writer]
|
9
|
+
# @yieldparam [Writer]
|
10
|
+
def self.open_filename(file_name, compression, format)
|
11
|
+
if block_given?
|
12
|
+
writer = open_filename file_name, compression, format
|
13
|
+
|
14
|
+
begin
|
15
|
+
yield writer
|
16
|
+
ensure
|
17
|
+
writer.close if writer.respond_to?(:close)
|
18
|
+
end
|
19
|
+
else
|
20
|
+
new file_name: file_name, compression: compression, format: format
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# @param [String] string
|
25
|
+
# @return [Writer]
|
26
|
+
# @yieldparam [Writer]
|
27
|
+
def self.open_memory(string, compression, format)
|
28
|
+
if block_given?
|
29
|
+
writer = open_memory string, compression, format
|
30
|
+
|
31
|
+
begin
|
32
|
+
yield writer
|
33
|
+
ensure
|
34
|
+
writer.close if writer.respond_to?(:close)
|
35
|
+
end
|
36
|
+
else
|
37
|
+
new memory: string, compression: compression, format: format
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# @param [Hash] params
|
42
|
+
# @option params [Object] :compression
|
43
|
+
# @option params [Object] :format
|
44
|
+
# @option params [String] :file_name
|
45
|
+
# @option params [String] :memory
|
46
|
+
def initialize(params = {})
|
47
|
+
super C.method(:archive_write_new), C.method(:archive_write_free)
|
48
|
+
|
49
|
+
begin
|
50
|
+
init_compression params[:compression]
|
51
|
+
init_format params[:format]
|
52
|
+
|
53
|
+
if params[:file_name]
|
54
|
+
init_for_filename params[:file_name]
|
55
|
+
elsif params[:memory]
|
56
|
+
init_for_memory params[:memory]
|
57
|
+
end
|
58
|
+
rescue StandardError
|
59
|
+
close
|
60
|
+
raise
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# @return [Entry]
|
65
|
+
# @yieldparam [Entry]
|
66
|
+
def new_entry
|
67
|
+
entry = Entry.new
|
68
|
+
|
69
|
+
if block_given?
|
70
|
+
begin
|
71
|
+
yield entry
|
72
|
+
ensure
|
73
|
+
entry.close
|
74
|
+
end
|
75
|
+
else
|
76
|
+
entry
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# @raise [ArgumentError] If no block given
|
81
|
+
# @return [NilClass]
|
82
|
+
# @yieldparam [Entry]
|
83
|
+
# @yieldreturn [String]
|
84
|
+
def add_entry
|
85
|
+
raise ArgumentError, 'No block given' unless block_given?
|
86
|
+
|
87
|
+
entry = Entry.new
|
88
|
+
begin
|
89
|
+
data = yield entry
|
90
|
+
|
91
|
+
if data
|
92
|
+
entry.size = data.bytesize
|
93
|
+
|
94
|
+
write_header entry
|
95
|
+
write_data data
|
96
|
+
else
|
97
|
+
write_header entry
|
98
|
+
end
|
99
|
+
|
100
|
+
nil
|
101
|
+
ensure
|
102
|
+
entry.close
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# @param [Array<String>] args
|
107
|
+
# @return [Integer]
|
108
|
+
# @raise [ArgumentError]
|
109
|
+
# @yieldreturn [String]
|
110
|
+
def write_data(*args)
|
111
|
+
if block_given?
|
112
|
+
raise ArgumentError, 'Not support arguments when block given' unless args.empty?
|
113
|
+
|
114
|
+
len = 0
|
115
|
+
loop do
|
116
|
+
str = yield len
|
117
|
+
n = str.is_a?(String) ? C.archive_write_data(archive, Utils.get_memory_ptr(str), str.bytesize) : 0
|
118
|
+
|
119
|
+
raise Error, self if n < 0
|
120
|
+
break if n.zero?
|
121
|
+
|
122
|
+
len += n
|
123
|
+
end
|
124
|
+
|
125
|
+
len
|
126
|
+
else
|
127
|
+
str = args[0]
|
128
|
+
raise ArgumentError, 'Invalid String argument' unless str.is_a?(String)
|
129
|
+
|
130
|
+
n = C.archive_write_data(archive, Utils.get_memory_ptr(str), str.bytesize)
|
131
|
+
raise Error, self if n < 0
|
132
|
+
|
133
|
+
n
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# @param [Entry] entry
|
138
|
+
def write_header(entry)
|
139
|
+
raise Error, self if C.archive_write_header(archive, entry.entry) != C::OK
|
140
|
+
end
|
141
|
+
|
142
|
+
protected
|
143
|
+
|
144
|
+
def init_compression(compression)
|
145
|
+
raise ArgumentError, 'Missing :compression argument' if !compression || compression.to_s.empty?
|
146
|
+
|
147
|
+
unless compression.is_a?(Integer) || compression.is_a?(String)
|
148
|
+
prefix = C.respond_to?(:archive_write_add_filter) ? 'FILTER' : 'COMPRESSION'
|
149
|
+
compression = Archive.const_get("#{prefix}_#{compression}".upcase)
|
150
|
+
end
|
151
|
+
|
152
|
+
raise Error, self if C.archive_write_set_compression(archive, compression) != C::OK
|
153
|
+
end
|
154
|
+
|
155
|
+
def init_format(format)
|
156
|
+
raise ArgumentError, 'Missing :format argument' if !format || format.to_s.empty?
|
157
|
+
|
158
|
+
format = Archive.const_get("FORMAT_#{format}".upcase) unless format.is_a?(Integer)
|
159
|
+
raise Error, self if C.archive_write_set_format(archive, format) != C::OK
|
160
|
+
end
|
161
|
+
|
162
|
+
def init_for_filename(file_name)
|
163
|
+
raise Error, self if C.archive_write_open_filename(archive, file_name) != C::OK
|
164
|
+
end
|
165
|
+
|
166
|
+
def init_for_memory(memory)
|
167
|
+
C.archive_write_set_bytes_in_last_block(archive, 1) if C.archive_write_get_bytes_in_last_block(archive) < 0
|
168
|
+
|
169
|
+
write_callback = proc do |_ar, _client_data, buffer, length|
|
170
|
+
memory << buffer.get_bytes(0, length)
|
171
|
+
length
|
172
|
+
end
|
173
|
+
|
174
|
+
null_ptr = FFI::Pointer::NULL
|
175
|
+
raise Error, self if C.archive_write_open(archive, null_ptr, null_ptr, write_callback, null_ptr) != C::OK
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|