rom-distillery 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +6 -0
- data/LICENSE +287 -0
- data/README.md +24 -0
- data/bin/rhum +6 -0
- data/distillery.gemspec +38 -0
- data/lib/distillery.rb +10 -0
- data/lib/distillery/archiver.rb +372 -0
- data/lib/distillery/archiver/archive.rb +102 -0
- data/lib/distillery/archiver/external.rb +182 -0
- data/lib/distillery/archiver/external.yaml +31 -0
- data/lib/distillery/archiver/libarchive.rb +105 -0
- data/lib/distillery/archiver/zip.rb +88 -0
- data/lib/distillery/cli.rb +234 -0
- data/lib/distillery/cli/check.rb +100 -0
- data/lib/distillery/cli/clean.rb +60 -0
- data/lib/distillery/cli/header.rb +61 -0
- data/lib/distillery/cli/index.rb +65 -0
- data/lib/distillery/cli/overlap.rb +39 -0
- data/lib/distillery/cli/rebuild.rb +47 -0
- data/lib/distillery/cli/rename.rb +34 -0
- data/lib/distillery/cli/repack.rb +113 -0
- data/lib/distillery/cli/validate.rb +171 -0
- data/lib/distillery/datfile.rb +180 -0
- data/lib/distillery/error.rb +13 -0
- data/lib/distillery/game.rb +70 -0
- data/lib/distillery/game/release.rb +40 -0
- data/lib/distillery/refinements.rb +41 -0
- data/lib/distillery/rom-archive.rb +266 -0
- data/lib/distillery/rom.rb +585 -0
- data/lib/distillery/rom/path.rb +110 -0
- data/lib/distillery/rom/path/archive.rb +103 -0
- data/lib/distillery/rom/path/file.rb +100 -0
- data/lib/distillery/rom/path/virtual.rb +70 -0
- data/lib/distillery/storage.rb +170 -0
- data/lib/distillery/vault.rb +433 -0
- data/lib/distillery/version.rb +7 -0
- metadata +192 -0
@@ -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
|
+
|