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