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