rom-distillery 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+