rom-distillery 0.1

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,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
+