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,433 @@
1
+ # SPDX-License-Identifier: EUPL-1.2
2
+
3
+ require 'set'
4
+ require 'pathname'
5
+ require 'fileutils'
6
+ require 'find'
7
+
8
+ require_relative 'rom'
9
+ require_relative 'rom-archive'
10
+
11
+ module Distillery
12
+
13
+ class Vault
14
+ include Enumerable
15
+
16
+ # @!visibility private
17
+ GLOB_PATTERN_REGEX = /(?<!\\)[?*}{\[\]]/
18
+
19
+ # List of ROM checksums
20
+ CHECKSUMS = ROM::CHECKSUMS
21
+
22
+ # List of archives extensions
23
+ ARCHIVES = ROMArchive::EXTENSIONS
24
+
25
+ # List of files to be ignored
26
+ IGNORE_FILES = Set[ '.dat', '.missing', '.baddump', '.extra' ]
27
+
28
+ # List of directories to be ignored
29
+ IGNORE_DIRS = Set[ '.roms', '.games', '.trash' ]
30
+
31
+ # Directory pruning
32
+ DIR_PRUNING = Set[ '.dat' ]
33
+
34
+
35
+ # Potential ROM from directory.
36
+ # @note file in {IGNORE_FILES}, directory in {IGNORE_DIRS},
37
+ # directories holding a {DIR_PRUNING} file or starting with a
38
+ # dot are ignored
39
+ #
40
+ # @param dir [String] path to directory
41
+ # @param depth [Integer,nil] exploration depth
42
+ #
43
+ # @yieldparam file [String] file being processed
44
+ # @yieldparam dir: [String] directory relative to
45
+ #
46
+ def self.from_dir(dir, depth: nil)
47
+ Find.find(dir) do |path|
48
+ basename = File.basename(path)
49
+ subpath = Pathname(path).relative_path_from(dir).to_s
50
+ if FileTest.directory?(path)
51
+ next if path == dir
52
+ Find.prune if IGNORE_DIRS.include?(basename)
53
+ Find.prune if basename.start_with?('.')
54
+ Find.prune if !depth.nil? &&
55
+ subpath.split(File::Separator).size > depth
56
+ Find.prune if DIR_PRUNING.any? {|f| File.exists?(f) }
57
+ elsif FileTest.file?(path)
58
+ next if IGNORE_FILES.include?(basename)
59
+ yield(subpath, dir: dir) if block_given?
60
+ end
61
+ end
62
+ end
63
+
64
+
65
+ # Potential ROM from glob
66
+ # @note file in {IGNORE_FILES}, directory in {IGNORE_DIRS},
67
+ # directories holding a {DIR_PRUNING} file or starting with a
68
+ # dot are ignored
69
+ #
70
+ # @param glob [String] ruby glob
71
+ # @param basedir [:guess,nil] basedir to use when interpreting glob
72
+ # matching
73
+ #
74
+ # @yieldparam file [String] file being processed
75
+ # @yieldparam dir: [String] directory relative to
76
+ #
77
+ def self.from_glob(glob, basedir: :guess)
78
+ if basedir == :guess
79
+ gentry = glob.split(File::SEPARATOR)
80
+ idx = gentry.find_index{|entry| entry =~ GLOB_PATTERN_REGEX }
81
+ gentry = gentry[0,idx]
82
+ basedir = if gentry.empty? then nil
83
+ elsif gentry.first.empty? then '/'
84
+ else File.join(gentry)
85
+ end
86
+ end
87
+
88
+ # Build file list (reject ignored files and dirs)
89
+ lst = Dir[glob].reject {|path|
90
+ (! FileTest.file?(path)) ||
91
+ IGNORE_FILES.include?(File.basename(path)) ||
92
+ path.split(File::SEPARATOR)[0..-1].any? {|dir|
93
+ IGNORE_DIRS.include?(dir) || dir.start_with?('.')
94
+ }
95
+ }
96
+ # Build cut list based on directory prunning
97
+ cutlst = lst.map {|f| File.dirname(f) }.uniq.select {|f|
98
+ DIR_PRUNING.any? {|p| FileTest.exist?(File.join(f,p)) }
99
+ }
100
+ # Apply cut list
101
+ lst.reject! {|path|
102
+ cutlst.any? {|cut| path.start_with?("#{cut}#{File::SEPARATOR}") }
103
+ }
104
+
105
+ # Iterate on list
106
+ lst.each do |path|
107
+ subpath = if basedir.nil?
108
+ then path
109
+ else Pathname(path).relative_path_from(basedir).to_s
110
+ end
111
+ yield(subpath, dir: basedir) if block_given?
112
+ end
113
+ end
114
+
115
+
116
+ def initialize(roms = [])
117
+ @cksum = Hash[CHECKSUMS.map {|k| [ k, {} ] }]
118
+ @roms = []
119
+
120
+ Array(roms).each {|rom| add_rom(rom) }
121
+ end
122
+
123
+ # @return [Boolean]
124
+ def empty?
125
+ @roms.empty?
126
+ end
127
+
128
+ # @return [Integer]
129
+ def size
130
+ @roms.size
131
+ end
132
+
133
+ # Iterate over each ROM
134
+ #
135
+ # @yieldparam rom [ROM]
136
+ #
137
+ # @return [self,Enumerator]
138
+ #
139
+ def each
140
+ block_given? ? @roms.each {|r| yield(r) }
141
+ : @roms.each
142
+ end
143
+
144
+ # Construct a new ROM vault as the intersection
145
+ #
146
+ # @param o [Vault] ROM vault to intersect with self
147
+ #
148
+ # @return [Vault]
149
+ def &(o)
150
+ Vault::new(@roms.select {|rom| o.match(rom) })
151
+ end
152
+
153
+
154
+ # Constuct a new ROM vault as the difference
155
+ #
156
+ # @param o [Vault] ROM vault to substract to self
157
+ #
158
+ # @return [Vault]
159
+ def -(o)
160
+ Vault::new(@roms.reject {|rom| o.match(rom) })
161
+ end
162
+
163
+
164
+ # Add ROM
165
+ #
166
+ # @param [ROM] *roms ROM to add
167
+ #
168
+ # @return self
169
+ #
170
+ def <<(rom)
171
+ add_rom(rom)
172
+ end
173
+
174
+
175
+ # Add ROM
176
+ #
177
+ # @param [ROM] rom ROM to add
178
+ #
179
+ # @return self
180
+ #
181
+ def add_rom(rom)
182
+ # Sanity check
183
+ unless ROM === rom
184
+ raise ArgumentError, "not a ROM"
185
+ end
186
+
187
+ # Add it to the list
188
+ @roms << rom
189
+
190
+ # Keep track of checksums
191
+ @cksum.each {|type, hlist|
192
+ hlist.merge!(rom.cksum(type) => rom) {|key, old, new|
193
+ if Array(old).any? {|r| r.path == new.path}
194
+ then old
195
+ else Array(old) + [ new ]
196
+ end
197
+ }
198
+ }
199
+
200
+ # Chainable
201
+ self
202
+ end
203
+
204
+
205
+ # Add ROM from file
206
+ #
207
+ # @param file [String] path to files relative to basedir
208
+ # @param basedir [String,nil] base directory
209
+ # @param archives [#include?] archives tester
210
+ #
211
+ # @return [self]
212
+ #
213
+ def add_from_file(file, basedir = nil, archives: ARCHIVES)
214
+ filepath = File.join(*[ basedir, file ].compact)
215
+ romlist = if ROMArchive.archive?(filepath, archives: archives)
216
+ then ROMArchive.from_file(filepath).to_a
217
+ else ROM.from_file(file, basedir)
218
+ end
219
+
220
+ Array(romlist).each {|rom| add_rom(rom) }
221
+ end
222
+
223
+
224
+ # Add ROM from directory.
225
+ # @note file in {IGNORE_FILES}, directory in {IGNORE_DIRS},
226
+ # directories holding a {DIR_PRUNING} file or starting with a
227
+ # dot are ignored
228
+ #
229
+ # @param dir [String] path to directory
230
+ # @param depth [Integer,nil] exploration depth
231
+ # @param archives [#include?] archives tester
232
+ #
233
+ # @yieldparam file [String] file being processed
234
+ # @yieldparam dir [String] directory relative to
235
+ #
236
+ # @return [self]
237
+ #
238
+ def add_from_dir(dir, depth: nil, archives: ARCHIVES)
239
+ Vault.from_dir(dir, depth: depth) do | file, dir: |
240
+ yield(file, dir: dir) if block_given?
241
+ add_from_file(file, dir, archives: archives)
242
+ end
243
+ self
244
+ end
245
+
246
+
247
+ # Add ROM from glob
248
+ # @note file in {IGNORE_FILES}, directory in {IGNORE_DIRS},
249
+ # directories holding a {DIR_PRUNING} file or starting with a
250
+ # dot are ignored
251
+ #
252
+ # @param glob [String] ruby glob
253
+ # @param basedir [:guess,nil] basedir to use when interpreting glob
254
+ # matching
255
+ # @param archives [#include?] archives tester
256
+ #
257
+ # @yieldparam file [String] file being processed
258
+ # @yieldparam dir [String] directory relative to
259
+ #
260
+ # @return [self]
261
+ #
262
+ def add_from_glob(glob, basedir: :guess, archives: ARCHIVES)
263
+ Vault.from_dir(glob, basedir: basedir) do | file, dir: |
264
+ yield(file, dir: dir) if block_given?
265
+ add_from_file(file, dir, archives: archives)
266
+ end
267
+ self
268
+ end
269
+
270
+
271
+ # List of ROM with loosely defined (ie: with some missing checksum)
272
+ #
273
+ # @return [Array<ROM>,nil]
274
+ #
275
+ def with_partial_checksum
276
+ @roms.select {|rom| rom.missing_checksums? }
277
+ end
278
+
279
+
280
+ # Check if we have some headered ROM.
281
+ #
282
+ # @return [Integer] only some ROMs are headered
283
+ # @return [true] all ROMs are headered
284
+ # @return [false] no headered ROM
285
+ #
286
+ def headered
287
+ size = @roms.select {|rom| rom.headered? }.size
288
+
289
+ if size == 0 then false
290
+ elsif size == @roms.size then true
291
+ else size
292
+ end
293
+ end
294
+
295
+ # Return list of matching ROMs.
296
+ #
297
+ # @param query [Hash{Symbol=>String}] Hash of checksums to match with
298
+ #
299
+ # @return [Array<ROM>] list of matching ROMs
300
+ # @return [nil] if no match
301
+ #
302
+ def cksummatch(query)
303
+ CHECKSUMS.each {|type|
304
+ if (q = query[type]) && (r = @cksum[type][q])
305
+ return Array(r)
306
+ end
307
+ }
308
+ return nil
309
+ end
310
+
311
+ # Return list of matching ROMs.
312
+ #
313
+ # @param rom [ROM] ROM to match with
314
+ #
315
+ # @return [Array<ROM>] list of matching ROMs
316
+ # @return [nil] if no match
317
+ #
318
+ def rommatch(rom)
319
+ self.cksummatch(rom.cksums)
320
+ end
321
+
322
+
323
+ # Return list of matching ROMs.
324
+ #
325
+ # @param query [Hash{Symbol=>String},ROM] Hash of checksums or ROM
326
+ # to match with
327
+ #
328
+ # @yieldparam rom [ROM] ROM that has been saved
329
+ #
330
+ # @return [Array<ROM>] list of matching ROMs
331
+ # @return [nil] if no match
332
+ #
333
+ def match(query)
334
+ case query
335
+ when Hash then self.cksummatch(query)
336
+ when ROM then self.rommatch(query)
337
+ else raise ArgumentError
338
+ end
339
+ end
340
+
341
+
342
+ # Save ROM to filesystem
343
+ #
344
+ # @param dir [String] directory used for saving
345
+ # @param part [:all,:header,:rom] wich part of the ROM file to save
346
+ # @param subdir [Boolean,Integer,Proc] use subdirectory
347
+ # @param pristine [Boolean] should existing directory be removed
348
+ # @param force [Boolean] remove previous file if necessary
349
+ #
350
+ # @yieldparam rom [ROM] ROM saved
351
+ #
352
+ # @return [self]
353
+ #
354
+ def save(dir, part: :all, subdir: false, pristine: false, force: false,
355
+ &block)
356
+ # Directory
357
+ FileUtils.remove_dir(dir) if pristine # Create clean env
358
+ Dir.mkdir(dir) unless Dir.exist?(dir) # Ensure directory
359
+
360
+ # Fill directory.
361
+ # -> We have the physical ROMs, so we have all the checksums
362
+ # except if the file is an header without rom content
363
+ @roms.select {|rom| rom.has_content? && !rom.fshash.nil? }
364
+ .each {|rom|
365
+ hash = rom.fshash
366
+ destdir = dir
367
+ dirpart = case subdir
368
+ when nil, false then nil
369
+ when true then hash[0..3]
370
+ when Integer then hash[0..subdir]
371
+ when Proc then subdir.call(rom)
372
+ else raise ArgumentError, "unsupported subdir type"
373
+ end
374
+
375
+ if dirpart
376
+ # Update destination directory
377
+ destdir = File.join(destdir, *dirpart)
378
+ # Ensure destination directory exists
379
+ FileUtils.mkdir_p(destdir)
380
+ end
381
+
382
+ # Destination file
383
+ dest = File.join(destdir, hash)
384
+
385
+ # If the file exist, it is the right file, as it is
386
+ # named from it's hash (ie: content)
387
+ if force || !File.exists?(dest)
388
+ rom.copy(dest, part: part, force: force)
389
+ end
390
+
391
+ block.call(rom) if block
392
+ }
393
+ self
394
+ end
395
+
396
+
397
+ # Dumping of ROM vault entries
398
+ #
399
+ # @param compact [Boolean]
400
+ #
401
+ # @return [self]
402
+ #
403
+ # @yieldparam group [String]
404
+ # @yieldparam entries [Array<String>]
405
+ #
406
+ def dump(compact: false, &block)
407
+ self.each.inject({}) {|grp, rom|
408
+ grp.merge(rom.path.storage => [rom]) {|key, old, new| old + new }
409
+ }.each {|storage, roms|
410
+ size = if ROM::Path::Archive === roms.first.path
411
+ roms.first.path.archive.size
412
+ end
413
+
414
+ if storage.nil?
415
+ roms.each {|rom| block.call(rom.path.entry, nil) }
416
+ else
417
+ if compact && (size == roms.size)
418
+ then block.call(storage)
419
+ else block.call(storage, roms.map {|r| r.path.entry })
420
+ end
421
+ end
422
+ }
423
+ self
424
+ end
425
+
426
+ protected
427
+
428
+ def roms
429
+ @roms
430
+ end
431
+ end
432
+
433
+ end
@@ -0,0 +1,7 @@
1
+ # SPDX-License-Identifier: EUPL-1.2
2
+
3
+ module Distillery
4
+
5
+ VERSION = "0.1"
6
+
7
+ end
metadata ADDED
@@ -0,0 +1,192 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rom-distillery
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Stephane D'Alu
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-05-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: nokogiri
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubyzip
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: tty-screen
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: tty-logger
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: tty-spinner
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: tty-progressbar
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: yard
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rake
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: Help organise emulation ROM using DAT file
126
+ email:
127
+ - sdalu@sdalu.com
128
+ executables:
129
+ - rhum
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - Gemfile
134
+ - LICENSE
135
+ - README.md
136
+ - bin/rhum
137
+ - distillery.gemspec
138
+ - lib/distillery.rb
139
+ - lib/distillery/archiver.rb
140
+ - lib/distillery/archiver/archive.rb
141
+ - lib/distillery/archiver/external.rb
142
+ - lib/distillery/archiver/external.yaml
143
+ - lib/distillery/archiver/libarchive.rb
144
+ - lib/distillery/archiver/zip.rb
145
+ - lib/distillery/cli.rb
146
+ - lib/distillery/cli/check.rb
147
+ - lib/distillery/cli/clean.rb
148
+ - lib/distillery/cli/header.rb
149
+ - lib/distillery/cli/index.rb
150
+ - lib/distillery/cli/overlap.rb
151
+ - lib/distillery/cli/rebuild.rb
152
+ - lib/distillery/cli/rename.rb
153
+ - lib/distillery/cli/repack.rb
154
+ - lib/distillery/cli/validate.rb
155
+ - lib/distillery/datfile.rb
156
+ - lib/distillery/error.rb
157
+ - lib/distillery/game.rb
158
+ - lib/distillery/game/release.rb
159
+ - lib/distillery/refinements.rb
160
+ - lib/distillery/rom-archive.rb
161
+ - lib/distillery/rom.rb
162
+ - lib/distillery/rom/path.rb
163
+ - lib/distillery/rom/path/archive.rb
164
+ - lib/distillery/rom/path/file.rb
165
+ - lib/distillery/rom/path/virtual.rb
166
+ - lib/distillery/storage.rb
167
+ - lib/distillery/vault.rb
168
+ - lib/distillery/version.rb
169
+ homepage: http://github.com/sdalu/distillery
170
+ licenses:
171
+ - EUPL-1.2
172
+ metadata: {}
173
+ post_install_message:
174
+ rdoc_options: []
175
+ require_paths:
176
+ - lib
177
+ required_ruby_version: !ruby/object:Gem::Requirement
178
+ requirements:
179
+ - - ">="
180
+ - !ruby/object:Gem::Version
181
+ version: '2.5'
182
+ required_rubygems_version: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ version: '0'
187
+ requirements: []
188
+ rubygems_version: 3.0.6
189
+ signing_key:
190
+ specification_version: 4
191
+ summary: ROM manager
192
+ test_files: []