rom-distillery 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,102 @@
1
+ # SPDX-License-Identifier: EUPL-1.2
2
+
3
+ module Distillery
4
+ class Archiver
5
+
6
+ # Allow archive file processing
7
+ #
8
+ # All the operations are forwarded to an {Archiver} instance
9
+ # which is able to process the selected archive file.
10
+ #
11
+ class Archive
12
+ include Enumerable
13
+
14
+ # Returns a new instance of Archive.
15
+ #
16
+ # @param file [String] file holding the archive
17
+ #
18
+ # @raise [ArchiverNotFound] an archiver able to process this file
19
+ # has not been found
20
+ #
21
+ def initialize(file)
22
+ @file = file
23
+ @archiver = Archiver.for_file(file)
24
+
25
+ if @archiver.nil?
26
+ raise ArchiverNotFound, "no archiver avalaible for this file"
27
+ end
28
+ end
29
+
30
+
31
+ # Iterate over each archive entry
32
+ #
33
+ # @yieldparam entry [String] entry name
34
+ # @yieldparam io [InputStream] input stream
35
+ #
36
+ # @return [self,Enumerator]
37
+ #
38
+ def each(&block)
39
+ @archiver.each(@file, &block)
40
+ self
41
+ end
42
+
43
+
44
+ # List of entries for the archive
45
+ #
46
+ # @return [Array<String>]
47
+ #
48
+ def entries
49
+ @archiver.entries(@file)
50
+ end
51
+
52
+
53
+ # Is the archive emtpy?
54
+ #
55
+ # @return [Boolean]
56
+ #
57
+ def empty?
58
+ @archiver.empty?(@file)
59
+ end
60
+
61
+
62
+ # Allow to perform read operation on an archive entry
63
+ #
64
+ # @param entry [String] entry name
65
+ #
66
+ # @yieldparam io [InputStream] input stream for reading
67
+ #
68
+ # @return block value
69
+ #
70
+ def reader(entry, &block)
71
+ @archiver.reader(@file, entry, &block)
72
+ end
73
+
74
+
75
+ # Allow to perform write operation on an archive entry
76
+ #
77
+ # @param entry [String] entry name
78
+ #
79
+ # @yieldparam io [OutputStream] output stream for writing
80
+ #
81
+ # @return block value
82
+ #
83
+ def writer(entry, &block)
84
+ @archiver.writer(@file, entry, &block)
85
+ end
86
+
87
+
88
+ # Delete the entry from the archive
89
+ #
90
+ # @param entry [String] entry name
91
+ #
92
+ # @return [Boolean] operation status
93
+ #
94
+ def delete!(entry)
95
+ @archiver.delete!(@file, entry)
96
+ end
97
+
98
+
99
+ end
100
+
101
+ end
102
+ end
@@ -0,0 +1,182 @@
1
+ # SPDX-License-Identifier: EUPL-1.2
2
+
3
+ require 'open3'
4
+ require 'yaml'
5
+
6
+ module Distillery
7
+ class Archiver
8
+
9
+ # Use external program to process archive
10
+ # Selection of external program is described in external.yaml
11
+ #
12
+ class External < Archiver
13
+ Archiver.add self
14
+
15
+ # Perform registration of the various archive format
16
+ # supported by this archiver provider
17
+ #
18
+ # @param list [Array<String>] list of format to register
19
+ # @param default_file [String] default configuration file
20
+ #
21
+ # @return [void]
22
+ #
23
+ def self.registering(list = [ '7z', 'zip' ],
24
+ default_file = File.join(__dir__, 'external.yaml'))
25
+ dflt_config = YAML.load_file(default_file)
26
+
27
+ if Array === list
28
+ list = Hash[list.map {|app| [ app, {} ] }]
29
+ end
30
+
31
+ list.each {|app, cfg|
32
+ dflt = dflt_config.dig(app) || {}
33
+ extensions = Array(cfg.dig('extension') || dflt.dig('extension'))
34
+ mimetypes = Array(cfg.dig('mimetype' ) || dflt.dig('mimetype' ))
35
+ list = {
36
+ :cmd => cfg .dig('list', 'cmd') || cfg .dig('cmd') ||
37
+ dflt.dig('list', 'cmd') || dflt.dig('cmd'),
38
+ :args => cfg .dig('list', 'args') ||
39
+ dflt.dig('list', 'args'),
40
+ :parser => if parser = (cfg .dig('list', 'parser') ||
41
+ dflt.dig('list', 'parser'))
42
+ Regexp.new('\A' + parser + '\Z')
43
+ end,
44
+ :validator => cfg .dig('list', 'validator') ||
45
+ dflt.dig('list', 'validator')
46
+ }
47
+ read = {
48
+ :cmd => cfg .dig('read', 'cmd') || cfg .dig('cmd') ||
49
+ dflt.dig('read', 'cmd') || dflt.dig('cmd'),
50
+ :args => cfg .dig('read', 'args') ||
51
+ dflt.dig('read', 'args'),
52
+ }
53
+ write = {
54
+ :cmd => cfg .dig('write', 'cmd') || cfg .dig('cmd') ||
55
+ dflt.dig('write', 'cmd') || dflt.dig('cmd'),
56
+ :args => cfg .dig('write', 'args') ||
57
+ dflt.dig('write', 'args'),
58
+ }
59
+ delete = {
60
+ :cmd => cfg .dig('delete', 'cmd') || cfg .dig('cmd') ||
61
+ dflt.dig('delete', 'cmd') || dflt.dig('cmd'),
62
+ :args => cfg .dig('delete', 'args') ||
63
+ dflt.dig('delete', 'args'),
64
+ }
65
+
66
+ if list[:cmd].nil? || read[:cmd].nil?
67
+ Archiver.logger&.warn {
68
+ "#{self}: command not defined for #{app} program (SKIP)"
69
+ }
70
+ next
71
+ end
72
+ if write[:cmd].nil?
73
+ Archiver.logger&.warn {
74
+ "#{self}: write mode not supported for #{app} program"
75
+ }
76
+ end
77
+
78
+ Archiver.register(External.new(list, read, write, delete,
79
+ extensions: extensions,
80
+ mimetypes: mimetypes))
81
+ }
82
+ end
83
+
84
+ def initialize(list, read, write=nil, delete=nil,
85
+ extensions:, mimetypes: nil)
86
+ @list = list
87
+ @read = read
88
+ @write = write
89
+ @delete = delete
90
+ @extensions = extensions
91
+ @mimetypes = mimetypes
92
+ end
93
+
94
+
95
+ # (see Archiver#extensions)
96
+ def extensions
97
+ @extensions
98
+ end
99
+
100
+
101
+ # (see Archiver#mimetypes)
102
+ def mimetypes
103
+ @mimetypes
104
+ end
105
+
106
+ # (see Archiver#each)
107
+ def each(file, &block)
108
+ return to_enum(:each, file) if block.nil?
109
+ entries(file).each {|entry|
110
+ reader(file, entry) {|io|
111
+ block.call(entry, io)
112
+ }
113
+ }
114
+ end
115
+
116
+ # (see Archiver#reader)
117
+ def reader(file, entry, &block)
118
+ subst = { '$(infile)' => file, '$(entry)' => entry }
119
+ cmd = @read[:cmd ]
120
+ args = @read[:args].map{|e| e.gsub(/\$\(\w+\)/, subst) }
121
+
122
+ Open3.popen2(cmd, *args) {|stdin, stdout|
123
+ stdin.close_write
124
+ block.call(InputStream.new(stdout))
125
+ }
126
+ end
127
+
128
+ # (see Archiver#writer)
129
+ def writer(file, entry, &block)
130
+ subst = { '$(infile)' => file, '$(entry)' => entry }
131
+ cmd = @write[:cmd ]
132
+ args = @write[:args].map{|e| e.gsub(/\$\(\w+\)/, subst) }
133
+
134
+ Open3.popen2(cmd, *args) {|stdin, stdout|
135
+ block.call(OutputStream.new(stdin))
136
+ }
137
+ end
138
+
139
+ # (see Archiver#delete!)
140
+ def delete!(file, entry)
141
+ subst = { '$(infile)' => file, '$(entry)' => entry }
142
+ cmd = @delete[:cmd ]
143
+ args = @delete[:args].map{|e| e.gsub(/\$\(\w+\)/, subst) }
144
+
145
+ Open3.popen2(cmd, *args) {|stdin, stdout|
146
+ stdin.close_write
147
+ }
148
+
149
+ true
150
+ end
151
+
152
+ # (see Archiver#entries)
153
+ def entries(file)
154
+ subst = { '$(infile)' => file }
155
+ cmd = @list[:cmd ]
156
+ args = @list[:args ].map{|e| e.gsub(/\$\(\w+\)/, subst) }
157
+ parser = @list[:parser ]
158
+ validator = @list[:validator]
159
+
160
+ stdout, stderr, status = Open3.capture3(cmd, *args)
161
+
162
+ if ! status.exitstatus.zero?
163
+ raise ExecError, "running external command failed (#{stderr})"
164
+ end
165
+
166
+ stdout.force_encoding('BINARY').lines(chomp: true).map {|l|
167
+ unless m = l.match(parser)
168
+ raise ProcessingError, "unable to parse entry (#{file}) (#{l})"
169
+ end
170
+
171
+ if validator && validator.find {|k,v| m[k] != v }
172
+ next
173
+ end
174
+
175
+ m[:entry]
176
+ }.compact
177
+ end
178
+ end
179
+
180
+ end
181
+ end
182
+
@@ -0,0 +1,31 @@
1
+ # SPDX-License-Identifier: EUPL-1.2
2
+
3
+ 7z:
4
+ extension: 7z
5
+ mimetype: application/x-7z-compressed
6
+ cmd: 7z
7
+ list:
8
+ args: [ l, -ba, $(infile) ]
9
+ parser: (?:\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})?\s+(?<type>.)....\s+\d+\s+(?:\d+\s+)?(?<entry>.+)
10
+ validator:
11
+ :type: '.'
12
+ read:
13
+ args: [ e, -so, $(infile), $(entry) ]
14
+ write:
15
+ args: [ a, $(infile), -m0=lzma, -mx=9, -si$(entry) ]
16
+ delete:
17
+ args: [ d, $(infile), $(entry) ]
18
+
19
+ zip:
20
+ extension: zip
21
+ mimetype: application/zip
22
+ list:
23
+ cmd: unzip
24
+ args: [ -Z1, $(infile) ]
25
+ parser: (?<entry>.+)
26
+ read:
27
+ cmd: unzip
28
+ args: [ -p, $(infile), $(entry) ]
29
+ delete:
30
+ cmd: zip
31
+ args: [ -d, $(infile), $(entry) ]
@@ -0,0 +1,105 @@
1
+ # SPDX-License-Identifier: EUPL-1.2
2
+
3
+ begin
4
+ require 'libarchive'
5
+ rescue LoadError
6
+ return
7
+ end
8
+
9
+
10
+ module Distillery
11
+ class Archiver
12
+
13
+ # Use binding to libarchive
14
+ #
15
+ class LibArchive < Archiver
16
+ Archiver.add self
17
+
18
+ MODE = { '7z' => { :extensions => '7z',
19
+ :mimetypes => 'application/x-7z-compressed' },
20
+ 'zip' => { :extensions => 'zip',
21
+ :mimetypes => 'application/zip' },
22
+ }
23
+
24
+
25
+ class InputStream < Archiver::InputStream
26
+ def initialize(ar)
27
+ @read_block = ar.to_enum(:read_data, 16*1024)
28
+ @buffer = StringIO.new
29
+ end
30
+
31
+ def read(length=nil)
32
+ return '' if length&.zero? # Zero length request
33
+ return nil if @buffer.nil? # End of stream
34
+
35
+ # Read data
36
+ data = @buffer.read(length) || ""
37
+ while data.size < length do
38
+ # We are short on data, that means buffer has been exhausted
39
+ # request new data block from the archive
40
+ block = @read_block.next
41
+ # Break if we already read all the archive data
42
+ if block.nil? || block.empty?
43
+ @buffer = nil
44
+ break
45
+ end
46
+ # Refill buffer from block
47
+ @buffer.string = block
48
+ # Continue reading
49
+ data.concat(@buffer.read(length - data.size))
50
+ end
51
+
52
+ data.empty? ? nil : data
53
+ end
54
+ end
55
+
56
+
57
+
58
+
59
+ def self.registering
60
+ MODE.each_key {|mode|
61
+ Archiver.register(LibArchive.new(mode))
62
+ }
63
+ end
64
+
65
+
66
+ def initialize(mode)
67
+ raise ArgumentError unless MODE.include?(mode)
68
+ @mode = mode
69
+ end
70
+
71
+
72
+ # List of supported extensions
73
+ #
74
+ def extensions
75
+ MODE[@mode][:extensions]
76
+ end
77
+
78
+
79
+ # List of supported mimetypes
80
+ #
81
+ def mimetypes
82
+ MODE[@mode][:mimetypes]
83
+ end
84
+
85
+
86
+ # (see Archiver#each)
87
+ def each(file, &block)
88
+ ::Archive.read_open_filename(file) {|ar|
89
+ while a_entry = ar.next_header
90
+ next unless a_entry.regular?
91
+ $stdout.puts a_entry.pathname
92
+ block.call(a_entry.pathname, InputStream.new(ar))
93
+ end
94
+ }
95
+ self
96
+ end
97
+
98
+ end
99
+
100
+
101
+ end
102
+ end
103
+
104
+
105
+
@@ -0,0 +1,88 @@
1
+ # SPDX-License-Identifier: EUPL-1.2
2
+
3
+ begin
4
+ require 'zip'
5
+ rescue LoadError
6
+ return
7
+ end
8
+
9
+
10
+ module Distillery
11
+ class Archiver
12
+
13
+ # Use of rubyzip as archiver
14
+ #
15
+ class Zip < Archiver
16
+ Archiver.add self
17
+
18
+
19
+
20
+ # Perform registration of the various archive format
21
+ # supported by this archiver provider
22
+ #
23
+ # @return [void]
24
+ #
25
+ def self.registering
26
+ Archiver.register(Zip.new)
27
+ end
28
+
29
+
30
+ def initialize
31
+ end
32
+
33
+ # (see Archiver#extensions)
34
+ def extensions
35
+ [ 'zip' ]
36
+ end
37
+
38
+
39
+ # (see Archiver#mimetypes)
40
+ def mimetypes
41
+ [ 'application/zip' ]
42
+ end
43
+
44
+
45
+ # (see Archiver#delete!)
46
+ def delete!(file, entry)
47
+ ::Zip::File.open(file) {|zip_file|
48
+ zip_file.remove(entry) ? true : false
49
+ }
50
+ rescue Errno::ENOENT
51
+ false
52
+ end
53
+
54
+ # (see Archiver#reader)
55
+ def reader(file, entry, &block)
56
+ ::Zip::File.open(file) {|zip_file|
57
+ zip_file.get_input_stream(entry) {|is|
58
+ block.call(InputStream.new(is))
59
+ }
60
+ }
61
+ end
62
+
63
+ # (see Archiver#writer)
64
+ def writer(file, entry, &block)
65
+ ::Zip::File.open(file, ::Zip::File::CREATE) {|zip_file|
66
+ zip_file.get_output_stream(entry) {|os|
67
+ block.call(OutputStream.new(os))
68
+ }
69
+ }
70
+ end
71
+
72
+ # (see Archiver#each)
73
+ def each(file, &block)
74
+ return to_enum(:each, file) if block.nil?
75
+ ::Zip::File.open(file) {|zip_file|
76
+ zip_file.each {|zip_entry|
77
+ next unless zip_entry.ftype == :file
78
+ block.call(zip_entry.name,
79
+ InputStream.new(zip_entry.get_input_stream))
80
+ }
81
+ }
82
+ end
83
+
84
+ end
85
+
86
+ end
87
+ end
88
+