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,110 @@
|
|
1
|
+
# SPDX-License-Identifier: EUPL-1.2
|
2
|
+
|
3
|
+
module Distillery
|
4
|
+
class ROM
|
5
|
+
|
6
|
+
# @abstract Abstract class used for ROM path
|
7
|
+
class Path
|
8
|
+
|
9
|
+
# Path value as string.
|
10
|
+
#
|
11
|
+
# @return [String]
|
12
|
+
#
|
13
|
+
def to_s
|
14
|
+
raise NotImplementedError
|
15
|
+
end
|
16
|
+
|
17
|
+
# File directly accessible on the file system
|
18
|
+
#
|
19
|
+
# @return [String]
|
20
|
+
#
|
21
|
+
def file
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
# File or directory that is considered the storage space for entries
|
27
|
+
#
|
28
|
+
# @return [String]
|
29
|
+
#
|
30
|
+
def storage
|
31
|
+
raise NotImplementedError
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
# Entry
|
36
|
+
#
|
37
|
+
# @return [String]
|
38
|
+
#
|
39
|
+
def entry
|
40
|
+
raise NotImplementedError
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
# Get path basename
|
45
|
+
#
|
46
|
+
# @return [String]
|
47
|
+
#
|
48
|
+
def basename
|
49
|
+
raise NotImplementedError
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
# ROM reader
|
54
|
+
# @note Can be costly, prefer existing #copy if possible
|
55
|
+
#
|
56
|
+
# @yieldparam [#read] io stream for reading
|
57
|
+
#
|
58
|
+
# @return block value
|
59
|
+
#
|
60
|
+
def reader(&block)
|
61
|
+
raise NotImplementedError
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
# Copy ROM content to the filesystem, possibly using link if requested.
|
66
|
+
#
|
67
|
+
# @param to [String] file destination
|
68
|
+
# @param length [Integer,nil] data length to be copied
|
69
|
+
# @param offset [Integer] data offset
|
70
|
+
# @param force [Boolean] remove previous file if necessary
|
71
|
+
# @param link [:hard, :sym, nil] use link instead of copy if possible
|
72
|
+
#
|
73
|
+
# @return [Boolean] status of the operation
|
74
|
+
#
|
75
|
+
def copy(to, length = nil, offset = 0, force: false, link: :hard)
|
76
|
+
raise NotImplementedError
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
# Rename ROM and physical content.
|
81
|
+
#
|
82
|
+
# @note Renaming could lead to silent removing if same ROM is on its way
|
83
|
+
#
|
84
|
+
# @param path [String] new ROM path
|
85
|
+
# @param force [Boolean] remove previous file if necessary
|
86
|
+
#
|
87
|
+
# @return [Boolean] status of the operation
|
88
|
+
#
|
89
|
+
def rename(path, force: false)
|
90
|
+
raise NotImplementedError
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
# Delete physical content.
|
95
|
+
#
|
96
|
+
# @return [Boolean]
|
97
|
+
#
|
98
|
+
def delete!
|
99
|
+
raise NotImplementedError
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
|
108
|
+
require_relative 'path/virtual'
|
109
|
+
require_relative 'path/archive'
|
110
|
+
require_relative 'path/file'
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# SPDX-License-Identifier: EUPL-1.2
|
2
|
+
|
3
|
+
module Distillery
|
4
|
+
class ROM
|
5
|
+
class Path
|
6
|
+
|
7
|
+
# Path from archive, binding archive and entry together.
|
8
|
+
class Archive < Path
|
9
|
+
|
10
|
+
# @!visibility private
|
11
|
+
@@separator = '#'
|
12
|
+
|
13
|
+
|
14
|
+
# Set the separator used to distinguish archive file from entry
|
15
|
+
#
|
16
|
+
# @param sep [String] separator
|
17
|
+
#
|
18
|
+
def self.separator=(sep)
|
19
|
+
@@separator = sep.dup.freeze
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
# Get the separator used to distinguish archive file from entry
|
24
|
+
#
|
25
|
+
# @return [String,Array]
|
26
|
+
#
|
27
|
+
def self.separator
|
28
|
+
@@separator
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
# Create a an Archive Path instance
|
33
|
+
#
|
34
|
+
# @param archive [ROMArchive] archive instance
|
35
|
+
# @param entry [String] archive entry
|
36
|
+
#
|
37
|
+
def initialize(archive, entry)
|
38
|
+
@archive = archive
|
39
|
+
@entry = entry
|
40
|
+
end
|
41
|
+
|
42
|
+
# (see ROM::Path#to_s)
|
43
|
+
def to_s(separator = nil)
|
44
|
+
separator ||= @@separator
|
45
|
+
"#{self.file}#{separator[0]}#{self.entry}#{separator[1]}"
|
46
|
+
end
|
47
|
+
|
48
|
+
# (see ROM::Path#file)
|
49
|
+
def file
|
50
|
+
@archive.file
|
51
|
+
end
|
52
|
+
|
53
|
+
# (see ROM::Path#storage)
|
54
|
+
def storage
|
55
|
+
self.file
|
56
|
+
end
|
57
|
+
|
58
|
+
# (see ROM::Path#entry)
|
59
|
+
def entry
|
60
|
+
@entry
|
61
|
+
end
|
62
|
+
|
63
|
+
# (see ROM::Path#basename)
|
64
|
+
def basename
|
65
|
+
::File.basename(self.entry)
|
66
|
+
end
|
67
|
+
|
68
|
+
# (see ROM::Path#grouping)
|
69
|
+
def grouping
|
70
|
+
[ self.storage, self.entry, @archive.size ]
|
71
|
+
end
|
72
|
+
|
73
|
+
# (see ROM::Path#reader)
|
74
|
+
def reader(&block)
|
75
|
+
@archive.reader(@entry, &block)
|
76
|
+
end
|
77
|
+
|
78
|
+
# (see ROM::Path#copy)
|
79
|
+
def copy(to, length = nil, offset = 0, force: false, link: :hard)
|
80
|
+
# XXX: improve like String
|
81
|
+
@archive.extract(@entry, to, length, offset, force: force)
|
82
|
+
end
|
83
|
+
|
84
|
+
# (see ROM::Path#rename)
|
85
|
+
def rename(path, force: false)
|
86
|
+
# XXX: improve like String
|
87
|
+
@archive.rename(@entry, path, force: force)
|
88
|
+
end
|
89
|
+
|
90
|
+
# (see ROM::Path#delete!)
|
91
|
+
def delete!
|
92
|
+
@archive.delete!(@entry)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Returns the value of attribute archive
|
96
|
+
# @return [ROMArchive]
|
97
|
+
attr_reader :archive
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# SPDX-License-Identifier: EUPL-1.2
|
2
|
+
|
3
|
+
module Distillery
|
4
|
+
class ROM
|
5
|
+
class Path
|
6
|
+
|
7
|
+
# Path from a file
|
8
|
+
class File < Path
|
9
|
+
# Returns a new instance of File.
|
10
|
+
#
|
11
|
+
# @param entry [String] path to file in basedir
|
12
|
+
# @param basedir [String, nil] base directory
|
13
|
+
#
|
14
|
+
def initialize(entry, basedir=nil)
|
15
|
+
if entry.start_with?('/')
|
16
|
+
raise ArgumentError, "entry must be relative to basedir"
|
17
|
+
end
|
18
|
+
|
19
|
+
@entry = entry
|
20
|
+
@basedir = basedir || '.'
|
21
|
+
end
|
22
|
+
|
23
|
+
# (see ROM::Path#to_s)
|
24
|
+
def to_s
|
25
|
+
self.file
|
26
|
+
end
|
27
|
+
|
28
|
+
# (see ROM::Path#file)
|
29
|
+
def file
|
30
|
+
if @basedir == '.'
|
31
|
+
then @entry
|
32
|
+
else ::File.join(@basedir, @entry)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# (see ROM::Path#storage)
|
37
|
+
def storage
|
38
|
+
@basedir
|
39
|
+
end
|
40
|
+
|
41
|
+
# (see ROM::Path#entry)
|
42
|
+
def entry
|
43
|
+
@entry
|
44
|
+
end
|
45
|
+
|
46
|
+
# (see ROM::Path#basename)
|
47
|
+
def basename
|
48
|
+
::File.basename(@entry)
|
49
|
+
end
|
50
|
+
|
51
|
+
# (see ROM::Path#reader)
|
52
|
+
def reader(&block)
|
53
|
+
::File.open(self.file, ::File::RDONLY, binmode: true, &block)
|
54
|
+
end
|
55
|
+
|
56
|
+
# (see ROM#copy)
|
57
|
+
def copy(to, length = nil, offset = 0, force: false, link: :hard)
|
58
|
+
(!force && length.nil? && offset.zero? &&
|
59
|
+
::File.exists?(to) && self.same?(ROM.from_file(to))) ||
|
60
|
+
ROM.filecopy(self.file, to, length, offset,
|
61
|
+
force: force, link: link)
|
62
|
+
end
|
63
|
+
|
64
|
+
# (see ROM#rename)
|
65
|
+
def rename(path, force: false)
|
66
|
+
case path
|
67
|
+
when String
|
68
|
+
else raise ArgumentError, "unsupport path type (#{path.class})"
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
file = if path.start_with?('/')
|
73
|
+
then path
|
74
|
+
else ::File.join(@basedir, path)
|
75
|
+
end
|
76
|
+
|
77
|
+
if !::File.exists?(file)
|
78
|
+
::File.rename(self.file, file) == 0
|
79
|
+
elsif self.same?(ROM.from_file(file))
|
80
|
+
::File.unlink(self.file) == 1
|
81
|
+
elsif force
|
82
|
+
::File.rename(self.file, file) == 0
|
83
|
+
else
|
84
|
+
false
|
85
|
+
end
|
86
|
+
rescue SystemCallError
|
87
|
+
false
|
88
|
+
end
|
89
|
+
|
90
|
+
# (see ROM#delete!)
|
91
|
+
def delete!
|
92
|
+
::File.unlink(self.file) == 1
|
93
|
+
rescue SystemCallError
|
94
|
+
false
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# SPDX-License-Identifier: EUPL-1.2
|
2
|
+
|
3
|
+
module Distillery
|
4
|
+
class ROM
|
5
|
+
class Path
|
6
|
+
|
7
|
+
# Path without physical implementation.
|
8
|
+
# Used for ROM defined in DAT file
|
9
|
+
class Virtual < Path
|
10
|
+
# @param entry [String]
|
11
|
+
def initialize(entry)
|
12
|
+
if ! entry.kind_of?(String)
|
13
|
+
raise ArgumentError
|
14
|
+
end
|
15
|
+
@entry = entry
|
16
|
+
end
|
17
|
+
|
18
|
+
# (see ROM::Path#to_s)
|
19
|
+
def to_s
|
20
|
+
@entry
|
21
|
+
end
|
22
|
+
|
23
|
+
# (see ROM::Path#file)
|
24
|
+
def file
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
|
28
|
+
# (see ROM::Path#storage)
|
29
|
+
def storage
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
|
33
|
+
# (see ROM::Path#entry)
|
34
|
+
def entry
|
35
|
+
@entry
|
36
|
+
end
|
37
|
+
|
38
|
+
# (see ROM::Path#basename)
|
39
|
+
def basename
|
40
|
+
::File.basename(@entry)
|
41
|
+
end
|
42
|
+
|
43
|
+
# (see ROM::Path#reader)
|
44
|
+
def reader(&block)
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
|
48
|
+
# (see ROM::Path#copy)
|
49
|
+
def copy(to, length = nil, offset = 0, force: false, link: :hard)
|
50
|
+
false
|
51
|
+
end
|
52
|
+
|
53
|
+
# (see ROM::Path#rename)
|
54
|
+
def rename(path, force: false)
|
55
|
+
case path
|
56
|
+
when String then @entry = path
|
57
|
+
else raise ArgumentError, "unsupport path type (#{path.class})"
|
58
|
+
end
|
59
|
+
true
|
60
|
+
end
|
61
|
+
|
62
|
+
# (see ROM::Path#delete!)
|
63
|
+
def delete!
|
64
|
+
true
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
# SPDX-License-Identifier: EUPL-1.2
|
2
|
+
|
3
|
+
require_relative 'vault'
|
4
|
+
|
5
|
+
module Distillery
|
6
|
+
|
7
|
+
class Storage
|
8
|
+
include Enumerable
|
9
|
+
|
10
|
+
# Hidden ROMs directory
|
11
|
+
ROMS_DIR = '.roms'
|
12
|
+
|
13
|
+
# Hidden games directory
|
14
|
+
GAMES_DIR = '.games'
|
15
|
+
|
16
|
+
def initialize(vault)
|
17
|
+
@roms = vault
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
def headered
|
22
|
+
@roms.headered
|
23
|
+
end
|
24
|
+
|
25
|
+
def each
|
26
|
+
block_given? ? @roms.each {|r| yield(r) }
|
27
|
+
: @roms.each
|
28
|
+
end
|
29
|
+
|
30
|
+
def index(type = nil, separator = nil)
|
31
|
+
type ||= ROM::FS_CHECKSUM
|
32
|
+
each.map {|rom|
|
33
|
+
hash = rom.cksum(type, :hex)
|
34
|
+
file = case path = rom.path
|
35
|
+
when ROM::Path::Archive then path.to_s(separator)
|
36
|
+
else path.to_s
|
37
|
+
end
|
38
|
+
[ hash, file ]
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
def build_roms_directory(dest, pristine: false, force: false, delete: false)
|
43
|
+
block = if delete
|
44
|
+
proc {|rom| rom.delete! }
|
45
|
+
end
|
46
|
+
@roms.save(dest,
|
47
|
+
part: :rom, subdir: true, pristine: pristine, force: force,
|
48
|
+
&block)
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
def build_games(dat, vault, &block)
|
53
|
+
dat.games.each {|game|
|
54
|
+
puts "Building: #{game}"
|
55
|
+
|
56
|
+
game.roms.each {|rom|
|
57
|
+
# Find in the matching ROM in the storage vault
|
58
|
+
# Note that in the vault:
|
59
|
+
# - the same ROM can be present multiple time
|
60
|
+
# - all checksums are defined
|
61
|
+
match = Array(vault.match(rom)).uniq {|r| r.cksum(FS_CHECKSUM) }
|
62
|
+
|
63
|
+
# Sanity check
|
64
|
+
if match.size > 1
|
65
|
+
# Due to weak ROM definition in DAT file
|
66
|
+
puts "- multiple matching ROMs for #{rom} (IGNORING)"
|
67
|
+
next
|
68
|
+
elsif match.size == 0
|
69
|
+
# Sadly we don't have this ROM
|
70
|
+
puts "- no mathing ROM for #{rom} (IGNORING)"
|
71
|
+
next
|
72
|
+
end
|
73
|
+
|
74
|
+
# Get vault ROM
|
75
|
+
vrom = match.first
|
76
|
+
|
77
|
+
# Call block
|
78
|
+
block.call(game.name, vrom, rom.path.entry)
|
79
|
+
}
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
def build_games_directories(dir, dat, vault, pristine: false, force: false)
|
85
|
+
# Directory
|
86
|
+
Dir.unlink(dir) if pristine # Create clean env if requested
|
87
|
+
Dir.mkdir(dir) unless Dir.exist?(dir) # Ensure directory exists
|
88
|
+
|
89
|
+
# Build game directories
|
90
|
+
build_games(dat, vault) {|game, rom, dst|
|
91
|
+
rom.copy(File.join(dir, game, dst))
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
def build_games_archives(dir, dat, vault, type = '7z', pristine: false)
|
97
|
+
# Normalize to lower case
|
98
|
+
type = type.downcase
|
99
|
+
|
100
|
+
# Directory
|
101
|
+
Dir.unlink(dir) if pristine # Create clean env if requested
|
102
|
+
Dir.mkdir(dir) unless Dir.exist?(dir) # Ensure directory exists
|
103
|
+
|
104
|
+
# Ensure we support this type of archive
|
105
|
+
if ! ROMArchive::EXTENSIONS.include?(type)
|
106
|
+
raise ArgumentError, "unsupported type (#{type})"
|
107
|
+
end
|
108
|
+
|
109
|
+
# Build game archives
|
110
|
+
build_games(dat, vault) {|game, rom, dst|
|
111
|
+
file = File.join(dir, "#{game}.#{type}")
|
112
|
+
Distillery::Archiver.for(file).writer(dst) {|o|
|
113
|
+
rom.reader {|i|
|
114
|
+
while data = i.read(32 * 1024)
|
115
|
+
o.write(data)
|
116
|
+
end
|
117
|
+
}
|
118
|
+
}
|
119
|
+
}
|
120
|
+
end
|
121
|
+
|
122
|
+
|
123
|
+
|
124
|
+
def rename(dat)
|
125
|
+
@roms.each {|rom|
|
126
|
+
# Skip if ROM is not present in DAT ?
|
127
|
+
if (m = dat.roms.match(rom)).nil?
|
128
|
+
puts "No DAT rom matching <#{rom}>"
|
129
|
+
next
|
130
|
+
end
|
131
|
+
|
132
|
+
# Find new rom name
|
133
|
+
name = if m.size == 1
|
134
|
+
# Easy, take the DAT rom name if different
|
135
|
+
next if m.first.name == rom.name
|
136
|
+
m.first.name
|
137
|
+
else
|
138
|
+
# Find name in the DAT, that is not currently present
|
139
|
+
# in our vault for this rom.
|
140
|
+
match_name = m.map {|r| r.name }
|
141
|
+
roms_name = @roms.match(rom).map {|r| r.name}
|
142
|
+
lst_name = match_name - roms_name
|
143
|
+
|
144
|
+
# Check if all DAT names are present in our vault,
|
145
|
+
# but perhaps we have an extra name to be removed
|
146
|
+
if lst_name.empty?
|
147
|
+
if (roms_name - match_name).include?(rom.name)
|
148
|
+
rom.delete!
|
149
|
+
end
|
150
|
+
next
|
151
|
+
end
|
152
|
+
|
153
|
+
# Use the first name
|
154
|
+
lst_name.first
|
155
|
+
end
|
156
|
+
|
157
|
+
# Apply new name. (Will be a no-op if same name)
|
158
|
+
rom.rename(name) {|old_name, new_name|
|
159
|
+
puts " < #{old_name}"
|
160
|
+
puts " > #{new_name}"
|
161
|
+
}
|
162
|
+
}
|
163
|
+
end
|
164
|
+
|
165
|
+
|
166
|
+
attr_reader :roms
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
170
|
+
|