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,585 @@
|
|
1
|
+
# SPDX-License-Identifier: EUPL-1.2
|
2
|
+
|
3
|
+
require 'digest'
|
4
|
+
require 'digest/sha1'
|
5
|
+
require 'zlib'
|
6
|
+
require 'fileutils'
|
7
|
+
|
8
|
+
require_relative 'error'
|
9
|
+
require_relative 'rom/path'
|
10
|
+
|
11
|
+
module Distillery
|
12
|
+
|
13
|
+
# ROM representation. It will typically have a name (entry) and hold
|
14
|
+
# information about it's content (size and checksums). If physical
|
15
|
+
# content is present it is referenced by it's path
|
16
|
+
#
|
17
|
+
class ROM
|
18
|
+
class HeaderLookupError < Error
|
19
|
+
end
|
20
|
+
|
21
|
+
# @!visibility private
|
22
|
+
HEADERS = [
|
23
|
+
# Nintendo : Family Computer Disk System
|
24
|
+
{ :name => 'Family Computer Disk System',
|
25
|
+
:ext => 'fds',
|
26
|
+
:rules => [ [ 0, "FDS" ] ],
|
27
|
+
:offset => 16,
|
28
|
+
},
|
29
|
+
# Nintendo : NES
|
30
|
+
{ :name => 'NES',
|
31
|
+
:ext => 'nes',
|
32
|
+
:rules => [ [ 0, "NES" ] ],
|
33
|
+
:offset => 16,
|
34
|
+
},
|
35
|
+
# Atari : Lynx
|
36
|
+
{ :name => 'Atary Lynx',
|
37
|
+
:ext => 'lnx',
|
38
|
+
:rules => [ [ 0, "LYNX" ] ],
|
39
|
+
:offset => 40,
|
40
|
+
},
|
41
|
+
# Atari : 7800
|
42
|
+
{ :name => 'Atari 7800',
|
43
|
+
:ext => 'a78',
|
44
|
+
:rules => [ [ 1, "ATARI7800" ],
|
45
|
+
[ 60, "\x00\x00\x00\x00ACTUAL CART DATA STARTS HERE" ] ],
|
46
|
+
:offset => 80,
|
47
|
+
},
|
48
|
+
]
|
49
|
+
|
50
|
+
# @!visibility private
|
51
|
+
CHECKSUMS_DEF = {
|
52
|
+
:sha256 => [ 256, 'e3b0c44298fc1c149afbf4c8996fb924' \
|
53
|
+
'27ae41e4649b934ca495991b7852b855' ],
|
54
|
+
:sha1 => [ 160, 'da39a3ee5e6b4b0d3255bfef95601890afd80709' ],
|
55
|
+
:md5 => [ 128, 'd41d8cd98f00b204e9800998ecf8427e' ],
|
56
|
+
:crc32 => [ 32, '00000000' ],
|
57
|
+
}.freeze
|
58
|
+
|
59
|
+
# List of supported weak checksums sorted by strength order
|
60
|
+
# (a subset of {CHECKSUMS})
|
61
|
+
CHECKSUMS_WEAK = [ :crc32 ].freeze
|
62
|
+
|
63
|
+
# List of supported strong checksums sorted by strength order
|
64
|
+
# (a subset of {CHECKSUMS})
|
65
|
+
|
66
|
+
CHECKSUMS_STRONG = [ :sha256, :sha1, :md5 ].freeze
|
67
|
+
|
68
|
+
# List of all supported checksums sorted by strength order
|
69
|
+
CHECKSUMS = (CHECKSUMS_STRONG + CHECKSUMS_WEAK).freeze
|
70
|
+
|
71
|
+
# List of all DAT supported checksums sorted by strengh order
|
72
|
+
CHECKSUMS_DAT = [ :sha1, :md5, :crc32 ].freeze
|
73
|
+
|
74
|
+
# Checksum used when saving to file-system
|
75
|
+
FS_CHECKSUM = :sha1
|
76
|
+
|
77
|
+
# Get information about ROM file (size, checksum, header, ...)
|
78
|
+
#
|
79
|
+
# @param io [#read] input object responding to read
|
80
|
+
# @param bufsize [Integer] buffer size in kB
|
81
|
+
# @param headers [Array,nil,false] header definition list
|
82
|
+
#
|
83
|
+
# @return [Hash{Symbol=>Object}] ROM information
|
84
|
+
#
|
85
|
+
def self.info(io, bufsize: 32, headers: nil)
|
86
|
+
# Sanity check
|
87
|
+
if bufsize <= 0
|
88
|
+
raise ArgumentError, "bufsize argument must be > 0"
|
89
|
+
end
|
90
|
+
|
91
|
+
# Apply default
|
92
|
+
headers ||= HEADERS
|
93
|
+
|
94
|
+
# Adjust bufsize (from kB to B)
|
95
|
+
bufsize <<= 10
|
96
|
+
|
97
|
+
# Initialize info
|
98
|
+
offset = 0
|
99
|
+
size = 0
|
100
|
+
sha256 = Digest::SHA256.new
|
101
|
+
sha1 = Digest::SHA1.new
|
102
|
+
md5 = Digest::MD5.new
|
103
|
+
crc32 = 0
|
104
|
+
|
105
|
+
# Process whole data
|
106
|
+
if x = io.read(bufsize)
|
107
|
+
if headers != false
|
108
|
+
begin
|
109
|
+
if offset = self.headered?(x, headers: headers)
|
110
|
+
x = x[offset..-1]
|
111
|
+
end
|
112
|
+
rescue HeaderLookupError
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
loop do
|
117
|
+
size += x.length
|
118
|
+
sha256 << x
|
119
|
+
sha1 << x
|
120
|
+
md5 << x
|
121
|
+
crc32 = Zlib::crc32(x, crc32)
|
122
|
+
break unless x = io.read(bufsize)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Return info
|
127
|
+
{ :offset => offset,
|
128
|
+
:size => size,
|
129
|
+
:sha256 => sha256.digest,
|
130
|
+
:sha1 => sha1.digest,
|
131
|
+
:md5 => md5.digest,
|
132
|
+
:crc32 => crc32,
|
133
|
+
}.compact
|
134
|
+
end
|
135
|
+
|
136
|
+
# Check if an header is detected
|
137
|
+
#
|
138
|
+
# @param data [String] data sample for header detection
|
139
|
+
# @param ext [String,nil] extension name as hint
|
140
|
+
# @param headers [Array] header definition list
|
141
|
+
#
|
142
|
+
# @raise [HeaderLookupError] sample is too short
|
143
|
+
#
|
144
|
+
# @return [Integer,nil] ROM offset
|
145
|
+
#
|
146
|
+
def self.headered?(data, ext: nil, headers: HEADERS)
|
147
|
+
# Normalize
|
148
|
+
ext = ext[1..-1] if ext && (ext[0] == ?.)
|
149
|
+
|
150
|
+
size = data.size
|
151
|
+
hdr = headers.find {| rules:, ** |
|
152
|
+
rules.all? {|offset, string|
|
153
|
+
if (offset + string.size) > size
|
154
|
+
raise HeaderLookupError
|
155
|
+
end
|
156
|
+
data[offset, string.size] == string
|
157
|
+
}
|
158
|
+
}
|
159
|
+
|
160
|
+
hdr&.[](:offset)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Copy file, possibly using link if requested.
|
164
|
+
#
|
165
|
+
# @param from [String] file to copy
|
166
|
+
# @param to [String] file destination
|
167
|
+
# @param length [Integer,nil] data length to be copied
|
168
|
+
# @param offset [Integer] data offset
|
169
|
+
# @param force [Boolean] remove previous file if necessary
|
170
|
+
# @param link [:hard, :sym, nil] use link instead of copy if possible
|
171
|
+
#
|
172
|
+
# @return [Boolean] status of the operation
|
173
|
+
#
|
174
|
+
def self.filecopy(from, to, length = nil, offset = 0,
|
175
|
+
force: false, link: :hard)
|
176
|
+
# Ensure sub-directories are created
|
177
|
+
FileUtils.mkpath(File.dirname(to))
|
178
|
+
|
179
|
+
# If whole file is to be copied try optimisation
|
180
|
+
if length.nil? && offset.zero?
|
181
|
+
# If we are on the same filesystem, we can use hardlink
|
182
|
+
f_stat = File.stat(from)
|
183
|
+
f_dev = [ f_stat.dev_major, f_stat.dev_minor ]
|
184
|
+
t_stat = File.stat(File.dirname(to))
|
185
|
+
t_dev = [ t_stat.dev_major, t_stat.dev_minor ]
|
186
|
+
if f_dev == t_dev
|
187
|
+
# If file already exists we will need to unlink it before
|
188
|
+
# but we will try to create hardlink before to not remove
|
189
|
+
# it unnecessarily if hardlinks are not supported
|
190
|
+
begin
|
191
|
+
File.link(from, to)
|
192
|
+
return true
|
193
|
+
rescue Errno::EEXIST
|
194
|
+
raise if !force
|
195
|
+
# File exist and we need to unlink it
|
196
|
+
# if unlink or link fails, something is wrong
|
197
|
+
begin
|
198
|
+
File.unlink(to)
|
199
|
+
File.link(from, to)
|
200
|
+
return true
|
201
|
+
rescue Errno::ENOENT
|
202
|
+
end
|
203
|
+
rescue Errno::EOPNOTSUPP
|
204
|
+
# If link are not supported fallback to copy
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# Copy file
|
210
|
+
op = force ? File::TRUNC : File::EXCL
|
211
|
+
File.open(from, File::RDONLY) {|i|
|
212
|
+
i.seek(offset)
|
213
|
+
File.open(to, File::CREAT|File::WRONLY|op) {|o|
|
214
|
+
IO.copy_stream(i, o, length)
|
215
|
+
}
|
216
|
+
}
|
217
|
+
return true
|
218
|
+
|
219
|
+
rescue Errno::EEXIST
|
220
|
+
return false
|
221
|
+
end
|
222
|
+
|
223
|
+
|
224
|
+
# Create ROM object from file definition.
|
225
|
+
#
|
226
|
+
# If `file` is an absolute path or `root` is not specified,
|
227
|
+
# ROM will be created with basename/dirname of entry.
|
228
|
+
#
|
229
|
+
# @param file [String] path or relative path to file
|
230
|
+
# @param root [String] anchor for the relative entry path
|
231
|
+
# @param headers [Array,nil,false] header definition list
|
232
|
+
#
|
233
|
+
# @return [ROM] based on `file` content
|
234
|
+
#
|
235
|
+
def self.from_file(file, root=nil, headers: nil)
|
236
|
+
basedir, entry = if root.nil? then File.split(file)
|
237
|
+
elsif file.start_with?('/') then File.split(file)
|
238
|
+
else [ root, file ]
|
239
|
+
end
|
240
|
+
file = File.join(basedir, entry)
|
241
|
+
|
242
|
+
rominfo = File.open(file) {|io| ROM.info(io, headers: headers) }
|
243
|
+
self.new(ROM::Path::File.new(entry, basedir), **rominfo)
|
244
|
+
end
|
245
|
+
|
246
|
+
|
247
|
+
# Create ROM representation.
|
248
|
+
#
|
249
|
+
# @param path [ROM::Path] rom path
|
250
|
+
# @param size [Integer] size rom size
|
251
|
+
# @param offset [Integer,nil] rom start (if headered)
|
252
|
+
# @option cksums [String,Integer] :sha1 rom checksum using sha1
|
253
|
+
# @option cksums [String,Integer] :md5 rom checksum using md5
|
254
|
+
# @option cksums [String,Integer] :crc32 rom checksum using crc32
|
255
|
+
#
|
256
|
+
def initialize(path, logger: nil, offset: nil, size: nil, **cksums)
|
257
|
+
# Sanity check
|
258
|
+
if path.nil?
|
259
|
+
raise ArgumentError, "ROM path is required"
|
260
|
+
end
|
261
|
+
|
262
|
+
unsupported_cksums = cksums.keys - CHECKSUMS
|
263
|
+
if ! unsupported_cksums.empty?
|
264
|
+
raise ArgumentError,
|
265
|
+
"unsupported checksums <#{unsupported_cksums.join(',')}>"
|
266
|
+
end
|
267
|
+
|
268
|
+
# Ensure checksum for nul-size ROM
|
269
|
+
if size == 0
|
270
|
+
cksums = Hash[CHECKSUMS_DEF.map {|k, (_, z)| [k, z] } ]
|
271
|
+
end
|
272
|
+
|
273
|
+
# Initialize
|
274
|
+
@offset = offset
|
275
|
+
@path = path
|
276
|
+
@size = size
|
277
|
+
@cksum = Hash[CHECKSUMS_DEF.map {|k, (s, _)|
|
278
|
+
[k, case val = cksums[k]
|
279
|
+
# No checksum
|
280
|
+
when '', '-', nil
|
281
|
+
# Checksum as hexstring or binary string
|
282
|
+
when String
|
283
|
+
case val.size
|
284
|
+
when s/4 then [val].pack('H*')
|
285
|
+
when s/8 then val
|
286
|
+
else raise ArgumentError,
|
287
|
+
"wrong size #{val.size} for hash string #{k}"
|
288
|
+
end
|
289
|
+
# Checksum as integer
|
290
|
+
when Integer
|
291
|
+
raise ArgumentError if (val < 0) || (val > 2**s)
|
292
|
+
["%0#{s/4}x" % val].pack('H*')
|
293
|
+
# Oops
|
294
|
+
else raise ArgumentError, "unsupported hash value type"
|
295
|
+
end
|
296
|
+
]
|
297
|
+
}].compact
|
298
|
+
|
299
|
+
# Warns
|
300
|
+
warns = []
|
301
|
+
# warns << 'nul size' if @size == 0
|
302
|
+
warns << 'no checksum' if @cksum.empty?
|
303
|
+
if !warns.empty?
|
304
|
+
warn "ROM <#{self.to_s}> has #{warns.join(', ')}"
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
|
309
|
+
# Compare ROMs using their checksums.
|
310
|
+
#
|
311
|
+
# @param o [ROM] other rom
|
312
|
+
# @param weak [Boolean] use weak checksum if necessary
|
313
|
+
#
|
314
|
+
# @return [Boolean] if they are the same or not
|
315
|
+
# @return [nil] if it wasn't decidable due to missing checksum
|
316
|
+
#
|
317
|
+
def same?(o, weak: true)
|
318
|
+
return true if self.equal?(o)
|
319
|
+
decidable = false
|
320
|
+
(weak ? CHECKSUMS : CHECKSUMS_STRONG).each {|type|
|
321
|
+
s_cksum = self.cksum(type)
|
322
|
+
o_cksum = o.cksum(type)
|
323
|
+
|
324
|
+
if s_cksum.nil? || o_cksum.nil? then next
|
325
|
+
elsif s_cksum != o_cksum then return false
|
326
|
+
else decidable = true
|
327
|
+
end
|
328
|
+
}
|
329
|
+
decidable ? true : nil
|
330
|
+
end
|
331
|
+
|
332
|
+
# Check if ROM hold content
|
333
|
+
#
|
334
|
+
# @return [Boolean]
|
335
|
+
#
|
336
|
+
def has_content?
|
337
|
+
! @path.storage.nil?
|
338
|
+
end
|
339
|
+
|
340
|
+
# String representation.
|
341
|
+
#
|
342
|
+
# @param prefered [:name, :entry, :checksum]
|
343
|
+
#
|
344
|
+
# @return [String]
|
345
|
+
#
|
346
|
+
def to_s(prefered = :name)
|
347
|
+
case prefered
|
348
|
+
when :checksum
|
349
|
+
if key = CHECKSUMS.find {|k| @cksum.include?(k) }
|
350
|
+
then cksum(key, :hex)
|
351
|
+
else self.name
|
352
|
+
end
|
353
|
+
when :name
|
354
|
+
self.name
|
355
|
+
when :entry
|
356
|
+
self.entry
|
357
|
+
else
|
358
|
+
self.name
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
|
363
|
+
# Does this ROM have an header?
|
364
|
+
#
|
365
|
+
# @return [Boolean]
|
366
|
+
#
|
367
|
+
def headered?
|
368
|
+
!@offset.nil? && (@offset > 0)
|
369
|
+
end
|
370
|
+
|
371
|
+
|
372
|
+
# Get ROM header
|
373
|
+
#
|
374
|
+
# @return [String]
|
375
|
+
#
|
376
|
+
def header
|
377
|
+
return nil if !headered?
|
378
|
+
@path.reader {|io| io.read(@offset) }
|
379
|
+
end
|
380
|
+
|
381
|
+
|
382
|
+
# Get the ROM specific checksum
|
383
|
+
#
|
384
|
+
# @param type checksum type must be one defined in CHECKSUMS
|
385
|
+
# @param fmt [:bin,:hex] checksum formating
|
386
|
+
#
|
387
|
+
# @return [String] checksum value (either binary string
|
388
|
+
# or as an hexadecimal string)
|
389
|
+
#
|
390
|
+
# @raise [ArgumentError] if `type` is not one defined in {CHECKSUMS}
|
391
|
+
# or `fmt` is not :bin or :hex
|
392
|
+
#
|
393
|
+
def cksum(type, fmt=:bin)
|
394
|
+
raise ArgumentError unless CHECKSUMS.include?(type)
|
395
|
+
|
396
|
+
if ckobj = @cksum[type]
|
397
|
+
case fmt
|
398
|
+
when :bin then ckobj
|
399
|
+
when :hex then ckobj.unpack1('H*')
|
400
|
+
else raise ArgumentError
|
401
|
+
end
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
|
406
|
+
# Get the ROM checksums
|
407
|
+
#
|
408
|
+
# @param fmt [:bin,:hex] checksum formating
|
409
|
+
#
|
410
|
+
# @return [Hash{Symbol=>String}] checksum
|
411
|
+
#
|
412
|
+
# @raise [ArgumentError] if `type` is not one defined in {CHECKSUMS}
|
413
|
+
# or `fmt` is not :bin or :hex
|
414
|
+
#
|
415
|
+
def cksums(fmt=:bin)
|
416
|
+
case fmt
|
417
|
+
when :bin then @cksum
|
418
|
+
when :hex then @cksum.transform_values {|v| v.unpack1('H*') }
|
419
|
+
else raise ArgumentError
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
|
424
|
+
# Checksum to be used for naming on filesystem
|
425
|
+
#
|
426
|
+
# @return [String] checksum hexstring
|
427
|
+
#
|
428
|
+
def fshash
|
429
|
+
cksum(FS_CHECKSUM, :hex)
|
430
|
+
end
|
431
|
+
|
432
|
+
|
433
|
+
# Is size information missing?
|
434
|
+
# @return [Boolean]
|
435
|
+
def missing_size?
|
436
|
+
@size.nil?
|
437
|
+
end
|
438
|
+
|
439
|
+
|
440
|
+
# Get ROM size in bytes.
|
441
|
+
#
|
442
|
+
# @return [Integer] ROM size in bytes
|
443
|
+
# @return [nil] ROM has no size
|
444
|
+
#
|
445
|
+
def size
|
446
|
+
@size
|
447
|
+
end
|
448
|
+
|
449
|
+
|
450
|
+
# Get ROM sha1 as hexadecimal string (if defined)
|
451
|
+
#
|
452
|
+
# @return [String,nil] hexadecimal checksum value
|
453
|
+
#
|
454
|
+
def sha1
|
455
|
+
cksum(:sha1, :hex)
|
456
|
+
end
|
457
|
+
|
458
|
+
|
459
|
+
# Get ROM md5 as hexadecimal string (if defined)
|
460
|
+
#
|
461
|
+
# @return [String,nil] hexadecimal checksum value
|
462
|
+
#
|
463
|
+
def md5
|
464
|
+
cksum(:md5, :hex)
|
465
|
+
end
|
466
|
+
|
467
|
+
|
468
|
+
# Get ROM crc32 as hexadcimal string (if defined)
|
469
|
+
#
|
470
|
+
# @return [String,nil] hexadecimal checksum value
|
471
|
+
#
|
472
|
+
def crc32
|
473
|
+
cksum(:crc32, :hex)
|
474
|
+
end
|
475
|
+
|
476
|
+
|
477
|
+
# Are some checksums missing?
|
478
|
+
#
|
479
|
+
# @param checksums [Array<Symbol>] list of checksums to consider
|
480
|
+
#
|
481
|
+
# @return [Boolean]
|
482
|
+
#
|
483
|
+
def missing_checksums?(checksums = CHECKSUMS_DAT)
|
484
|
+
@cksum.keys != checksums
|
485
|
+
end
|
486
|
+
|
487
|
+
|
488
|
+
# Get ROM name.
|
489
|
+
#
|
490
|
+
# @return [String]
|
491
|
+
#
|
492
|
+
def name
|
493
|
+
@path.basename
|
494
|
+
end
|
495
|
+
|
496
|
+
|
497
|
+
# Get ROM path.
|
498
|
+
#
|
499
|
+
# @return [String]
|
500
|
+
#
|
501
|
+
def path
|
502
|
+
@path
|
503
|
+
end
|
504
|
+
|
505
|
+
|
506
|
+
# ROM reader
|
507
|
+
#
|
508
|
+
# @yieldparam [#read] io stream for reading
|
509
|
+
#
|
510
|
+
# @return block value
|
511
|
+
#
|
512
|
+
def reader(&block)
|
513
|
+
@path.reader(&block)
|
514
|
+
end
|
515
|
+
|
516
|
+
|
517
|
+
# Copy ROM content to the filesystem, possibly using link if requested.
|
518
|
+
#
|
519
|
+
# @param to [String] file destination
|
520
|
+
# @param length [Integer,nil] data length to be copied
|
521
|
+
# @param part [:all,:header,:rom] which part of the rom file to copy
|
522
|
+
# @param link [:hard, :sym, nil] use link instead of copy if possible
|
523
|
+
#
|
524
|
+
# @return [Boolean] status of the operation
|
525
|
+
#
|
526
|
+
def copy(to, part: :all, force: false, link: :hard)
|
527
|
+
# Sanity check
|
528
|
+
unless [ :all, :rom, :header ].include?(part)
|
529
|
+
raise ArgumenetError, "unsupported part (#{part})"
|
530
|
+
end
|
531
|
+
|
532
|
+
# Copy
|
533
|
+
length, offset = case part
|
534
|
+
when :all
|
535
|
+
[ nil, 0 ]
|
536
|
+
when :rom
|
537
|
+
[ nil, @offset || 0 ]
|
538
|
+
when :header
|
539
|
+
return false if !self.headered?
|
540
|
+
[ @offset, 0 ]
|
541
|
+
end
|
542
|
+
|
543
|
+
@path.copy(to, length, offset, force: force, link: link)
|
544
|
+
end
|
545
|
+
|
546
|
+
|
547
|
+
# Delete physical content.
|
548
|
+
#
|
549
|
+
# @return [Boolean]
|
550
|
+
#
|
551
|
+
def delete!
|
552
|
+
if @path.delete!
|
553
|
+
@path == ROM::Path::Virtual.new(@path.entry)
|
554
|
+
end
|
555
|
+
end
|
556
|
+
|
557
|
+
|
558
|
+
# Rename ROM and physical content.
|
559
|
+
#
|
560
|
+
# @note Renaming could lead to silent removing if same ROM is on its way
|
561
|
+
#
|
562
|
+
# @param path [String] new ROM path
|
563
|
+
# @param force [Boolean] remove previous file if necessary
|
564
|
+
#
|
565
|
+
# @return [Boolean] status of the operation
|
566
|
+
#
|
567
|
+
# @yield Rename operation (optional)
|
568
|
+
# @yieldparam old [String] old entry name
|
569
|
+
# @yieldparam new [String] new entry name
|
570
|
+
#
|
571
|
+
def rename(path, force: false)
|
572
|
+
# Deal with renaming
|
573
|
+
ok = @path.rename(path, force: force)
|
574
|
+
|
575
|
+
if ok
|
576
|
+
@entry = entry
|
577
|
+
yield(old_entry, entry) if block_given?
|
578
|
+
end
|
579
|
+
|
580
|
+
ok
|
581
|
+
end
|
582
|
+
end
|
583
|
+
|
584
|
+
end
|
585
|
+
|