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