Class: Distillery::ROM

Inherits:
Object
  • Object
show all
Defined in:
lib/distillery/rom.rb,
lib/distillery/rom/path.rb,
lib/distillery/rom/path/file.rb,
lib/distillery/rom/path/archive.rb,
lib/distillery/rom/path/virtual.rb

Overview

ROM representation. It will typically have a name (entry) and hold information about it’s content (size and checksums). If physical content is present it is referenced by it’s path

Defined Under Namespace

Classes: HeaderLookupError, Path

Constant Summary collapse

CHECKSUMS_WEAK =

List of supported weak checksums sorted by strength order (a subset of CHECKSUMS)

[ :crc32 ].freeze
CHECKSUMS_STRONG =

List of supported strong checksums sorted by strength order (a subset of CHECKSUMS)

[ :sha256, :sha1, :md5 ].freeze
CHECKSUMS =

List of all supported checksums sorted by strength order

(CHECKSUMS_STRONG + CHECKSUMS_WEAK).freeze
CHECKSUMS_DAT =

List of all DAT supported checksums sorted by strengh order

[ :sha1, :md5, :crc32 ].freeze
FS_CHECKSUM =

Checksum used when saving to file-system

:sha1

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path, logger: nil, offset: nil, size: nil, **cksums) ⇒ ROM

Create ROM representation.

Parameters:

  • path (ROM::Path)

    rom path

  • size (Integer) (defaults to: nil)

    size rom size

  • offset (Integer, nil) (defaults to: nil)

    rom start (if headered)

  • cksums (Hash)

    a customizable set of options

Options Hash (**cksums):

  • :sha1 (String, Integer)

    rom checksum using sha1

  • :md5 (String, Integer)

    rom checksum using md5

  • :crc32 (String, Integer)

    rom checksum using crc32



256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/distillery/rom.rb', line 256

def initialize(path, logger: nil, offset: nil, size: nil, **cksums)
    # Sanity check
    if path.nil?
        raise ArgumentError, "ROM path is required"
    end

    unsupported_cksums = cksums.keys - CHECKSUMS
    if ! unsupported_cksums.empty?
        raise ArgumentError,
              "unsupported checksums <#{unsupported_cksums.join(',')}>"
    end

    # Ensure checksum for nul-size ROM
    if size == 0
        cksums = Hash[CHECKSUMS_DEF.map {|k, (_, z)| [k, z] } ]
    end

    # Initialize
    @offset = offset
    @path   = path
    @size   = size
    @cksum  = Hash[CHECKSUMS_DEF.map {|k, (s, _)|
        [k, case val = cksums[k]
            # No checksum
            when '', '-', nil
            # Checksum as hexstring or binary string
            when String
                case val.size
                when s/4 then [val].pack('H*')
                when s/8 then val
                else raise ArgumentError,
                           "wrong size #{val.size} for hash string #{k}"
                end
            # Checksum as integer
            when Integer
                raise ArgumentError if (val < 0) || (val > 2**s)
                ["%0#{s/4}x" % val].pack('H*')
            # Oops
            else raise ArgumentError, "unsupported hash value type"
            end
        ]
    }].compact

    # Warns
    warns = []
#       warns << 'nul size'    if @size == 0
    warns << 'no checksum' if @cksum.empty?
    if !warns.empty?
        warn "ROM <#{self.to_s}> has #{warns.join(', ')}"
    end
end

Class Method Details

.filecopy(from, to, length = nil, offset = 0, force: false, link: :hard) ⇒ Boolean

Copy file, possibly using link if requested.

Parameters:

  • from (String)

    file to copy

  • to (String)

    file destination

  • length (Integer, nil) (defaults to: nil)

    data length to be copied

  • offset (Integer) (defaults to: 0)

    data offset

  • force (Boolean) (defaults to: false)

    remove previous file if necessary

  • link (:hard, :sym, nil) (defaults to: :hard)

    use link instead of copy if possible

Returns:

  • (Boolean)

    status of the operation



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/distillery/rom.rb', line 174

def self.filecopy(from, to, length = nil, offset = 0,
                  force: false, link: :hard)
    # Ensure sub-directories are created
    FileUtils.mkpath(File.dirname(to))

    # If whole file is to be copied try optimisation
    if length.nil? && offset.zero?
        # If we are on the same filesystem, we can use hardlink
        f_stat = File.stat(from)
        f_dev  = [ f_stat.dev_major, f_stat.dev_minor ]
        t_stat = File.stat(File.dirname(to))
        t_dev  = [ t_stat.dev_major, t_stat.dev_minor ]
        if f_dev == t_dev
            # If file already exists we will need to unlink it before
            # but we will try to create hardlink before to not remove
            # it unnecessarily if hardlinks are not supported
            begin
                File.link(from, to)
                return true
            rescue Errno::EEXIST
                raise if !force
                # File exist and we need to unlink it
                # if unlink or link fails, something is wrong
                begin
                    File.unlink(to)
                    File.link(from, to)
                return true
                rescue Errno::ENOENT
                end
            rescue Errno::EOPNOTSUPP
                # If link are not supported fallback to copy
            end
        end
    end
    
    # Copy file
    op = force ? File::TRUNC : File::EXCL
    File.open(from, File::RDONLY) {|i|
        i.seek(offset)
        File.open(to, File::CREAT|File::WRONLY|op) {|o|
            IO.copy_stream(i, o, length)
        }
    }
    return true
   
rescue Errno::EEXIST
    return false
end

.from_file(file, root = nil, headers: nil) ⇒ ROM

Create ROM object from file definition.

If ‘file` is an absolute path or `root` is not specified, ROM will be created with basename/dirname of entry.

Parameters:

  • file (String)

    path or relative path to file

  • root (String) (defaults to: nil)

    anchor for the relative entry path

  • headers (Array, nil, false) (defaults to: nil)

    header definition list

Returns:

  • (ROM)

    based on ‘file` content



235
236
237
238
239
240
241
242
243
244
# File 'lib/distillery/rom.rb', line 235

def self.from_file(file, root=nil, headers: nil)
    basedir, entry = if    root.nil?             then File.split(file)
                     elsif file.start_with?('/') then File.split(file)
                     else                             [ root, file ]
                     end
    file           = File.join(basedir, entry)

    rominfo = File.open(file) {|io| ROM.info(io, headers: headers) }
    self.new(ROM::Path::File.new(entry, basedir), **rominfo)
end

.headered?(data, ext: nil, headers: HEADERS) ⇒ Integer?

Check if an header is detected

Parameters:

  • data (String)

    data sample for header detection

  • ext (String, nil) (defaults to: nil)

    extension name as hint

  • headers (Array) (defaults to: HEADERS)

    header definition list

Returns:

  • (Integer, nil)

    ROM offset

Raises:



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/distillery/rom.rb', line 146

def self.headered?(data, ext: nil, headers: HEADERS)
    # Normalize
    ext  = ext[1..-1] if ext && (ext[0] == ?.)

    size = data.size
    hdr  = headers.find {| rules:, ** |
        rules.all? {|offset, string|
            if (offset + string.size) > size
                raise HeaderLookupError
            end
            data[offset, string.size] == string
        }
    }
    
    hdr&.[](:offset)
end

.info(io, bufsize: 32, headers: nil) ⇒ Hash{Symbol=>Object}

Get information about ROM file (size, checksum, header, …)

Parameters:

  • io (#read)

    input object responding to read

  • bufsize (Integer) (defaults to: 32)

    buffer size in kB

  • headers (Array, nil, false) (defaults to: nil)

    header definition list

Returns:

  • (Hash{Symbol=>Object})

    ROM information



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/distillery/rom.rb', line 85

def self.info(io, bufsize: 32, headers: nil)
    # Sanity check
    if bufsize <= 0
        raise ArgumentError, "bufsize argument must be > 0"
    end

    # Apply default
    headers ||= HEADERS
    
    # Adjust bufsize (from kB to B)
    bufsize <<= 10
    
    # Initialize info
    offset = 0
    size   = 0
    sha256 = Digest::SHA256.new
    sha1   = Digest::SHA1.new
    md5    = Digest::MD5.new
    crc32  = 0

    # Process whole data
    if x = io.read(bufsize)
        if headers != false
            begin
                if offset = self.headered?(x, headers: headers)
                    x = x[offset..-1]
                end
            rescue HeaderLookupError
            end
        end

        loop do
            size  += x.length
            sha256 << x
            sha1   << x
            md5    << x
            crc32  = Zlib::crc32(x, crc32)
            break unless x = io.read(bufsize)
        end
    end
    
    # Return info
    { :offset  => offset,
      :size    => size,
      :sha256  => sha256.digest,
      :sha1    => sha1.digest,
      :md5     => md5.digest,
      :crc32   => crc32,
    }.compact
end

Instance Method Details

#cksum(type, fmt = :bin) ⇒ String

Get the ROM specific checksum

Parameters:

  • type

    checksum type must be one defined in CHECKSUMS

  • fmt (:bin, :hex) (defaults to: :bin)

    checksum formating

Returns:

  • (String)

    checksum value (either binary string or as an hexadecimal string)

Raises:

  • (ArgumentError)

    if ‘type` is not one defined in CHECKSUMS or `fmt` is not :bin or :hex



393
394
395
396
397
398
399
400
401
402
403
# File 'lib/distillery/rom.rb', line 393

def cksum(type, fmt=:bin)
    raise ArgumentError unless CHECKSUMS.include?(type)

    if ckobj = @cksum[type]
        case fmt
        when :bin then ckobj
        when :hex then ckobj.unpack1('H*')
        else raise ArgumentError
        end
    end
end

#cksums(fmt = :bin) ⇒ Hash{Symbol=>String}

Get the ROM checksums

Parameters:

  • fmt (:bin, :hex) (defaults to: :bin)

    checksum formating

Returns:

  • (Hash{Symbol=>String})

    checksum

Raises:

  • (ArgumentError)

    if ‘type` is not one defined in CHECKSUMS or `fmt` is not :bin or :hex



415
416
417
418
419
420
421
# File 'lib/distillery/rom.rb', line 415

def cksums(fmt=:bin)
    case fmt
    when :bin then @cksum
    when :hex then @cksum.transform_values {|v| v.unpack1('H*') }
    else raise ArgumentError
    end
end

#copy(to, part: :all, force: false, link: :hard) ⇒ Boolean

Copy ROM content to the filesystem, possibly using link if requested.

Parameters:

  • to (String)

    file destination

  • length (Integer, nil)

    data length to be copied

  • part (:all, :header, :rom) (defaults to: :all)

    which part of the rom file to copy

  • link (:hard, :sym, nil) (defaults to: :hard)

    use link instead of copy if possible

Returns:

  • (Boolean)

    status of the operation



526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
# File 'lib/distillery/rom.rb', line 526

def copy(to, part: :all, force: false, link: :hard)
    # Sanity check
    unless [ :all, :rom, :header ].include?(part)
        raise ArgumenetError, "unsupported part (#{part})"
    end

    # Copy
    length, offset = case part
                     when :all
                         [ nil, 0 ]
                     when :rom
                         [ nil, @offset || 0 ]
                     when :header
                         return false if !self.headered?
                         [ @offset, 0 ]
                     end
    
    @path.copy(to, length, offset, force: force, link: link)
end

#crc32String?

Get ROM crc32 as hexadcimal string (if defined)

Returns:

  • (String, nil)

    hexadecimal checksum value



472
473
474
# File 'lib/distillery/rom.rb', line 472

def crc32
    cksum(:crc32, :hex)
end

#delete!Boolean

Delete physical content.

Returns:

  • (Boolean)


551
552
553
554
555
# File 'lib/distillery/rom.rb', line 551

def delete!
    if @path.delete!
        @path == ROM::Path::Virtual.new(@path.entry)
    end
end

#fshashString

Checksum to be used for naming on filesystem

Returns:

  • (String)

    checksum hexstring



428
429
430
# File 'lib/distillery/rom.rb', line 428

def fshash
    cksum(FS_CHECKSUM, :hex)
end

#has_content?Boolean

Check if ROM hold content

Returns:

  • (Boolean)


336
337
338
# File 'lib/distillery/rom.rb', line 336

def has_content?
    ! @path.storage.nil?
end

#headerString

Get ROM header

Returns:

  • (String)


376
377
378
379
# File 'lib/distillery/rom.rb', line 376

def header
    return nil if !headered?
    @path.reader {|io| io.read(@offset) }
end

#headered?Boolean

Does this ROM have an header?

Returns:

  • (Boolean)


367
368
369
# File 'lib/distillery/rom.rb', line 367

def headered?
    !@offset.nil? && (@offset > 0)
end

#md5String?

Get ROM md5 as hexadecimal string (if defined)

Returns:

  • (String, nil)

    hexadecimal checksum value



463
464
465
# File 'lib/distillery/rom.rb', line 463

def md5
    cksum(:md5, :hex)
end

#missing_checksums?(checksums = CHECKSUMS_DAT) ⇒ Boolean

Are some checksums missing?

Parameters:

  • checksums (Array<Symbol>) (defaults to: CHECKSUMS_DAT)

    list of checksums to consider

Returns:

  • (Boolean)


483
484
485
# File 'lib/distillery/rom.rb', line 483

def missing_checksums?(checksums = CHECKSUMS_DAT)
    @cksum.keys != checksums
end

#missing_size?Boolean

Is size information missing?

Returns:

  • (Boolean)


435
436
437
# File 'lib/distillery/rom.rb', line 435

def missing_size?
    @size.nil?
end

#nameString

Get ROM name.

Returns:

  • (String)


492
493
494
# File 'lib/distillery/rom.rb', line 492

def name
    @path.basename
end

#pathString

Get ROM path.

Returns:

  • (String)


501
502
503
# File 'lib/distillery/rom.rb', line 501

def path
    @path
end

#reader {|io| ... } ⇒ Object

ROM reader

Yield Parameters:

  • io (#read)

    stream for reading

Returns:

  • block value



512
513
514
# File 'lib/distillery/rom.rb', line 512

def reader(&block)
    @path.reader(&block)
end

#rename(path, force: false) {|old, new| ... } ⇒ Boolean

Note:

Renaming could lead to silent removing if same ROM is on its way

Rename ROM and physical content.

Parameters:

  • path (String)

    new ROM path

  • force (Boolean) (defaults to: false)

    remove previous file if necessary

Yields:

  • Rename operation (optional)

Yield Parameters:

  • old (String)

    old entry name

  • new (String)

    new entry name

Returns:

  • (Boolean)

    status of the operation



571
572
573
574
575
576
577
578
579
580
581
# File 'lib/distillery/rom.rb', line 571

def rename(path, force: false)
    # Deal with renaming
    ok = @path.rename(path, force: force)
    
    if ok
        @entry = entry
        yield(old_entry, entry) if block_given?
    end

    ok
end

#same?(o, weak: true) ⇒ Boolean?

Compare ROMs using their checksums.

Parameters:

  • o (ROM)

    other rom

  • weak (Boolean) (defaults to: true)

    use weak checksum if necessary

Returns:

  • (Boolean)

    if they are the same or not

  • (nil)

    if it wasn’t decidable due to missing checksum



317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/distillery/rom.rb', line 317

def same?(o, weak: true)
    return true if self.equal?(o)
    decidable = false
    (weak ? CHECKSUMS : CHECKSUMS_STRONG).each {|type|
        s_cksum = self.cksum(type)
        o_cksum =    o.cksum(type)

        if s_cksum.nil? || o_cksum.nil? then next
        elsif s_cksum != o_cksum        then return false
        else                                 decidable = true
        end
    }
    decidable ? true : nil
end

#sha1String?

Get ROM sha1 as hexadecimal string (if defined)

Returns:

  • (String, nil)

    hexadecimal checksum value



454
455
456
# File 'lib/distillery/rom.rb', line 454

def sha1
    cksum(:sha1, :hex)
end

#sizeInteger?

Get ROM size in bytes.

Returns:

  • (Integer)

    ROM size in bytes

  • (nil)

    ROM has no size



445
446
447
# File 'lib/distillery/rom.rb', line 445

def size
    @size
end

#to_s(prefered = :name) ⇒ String

String representation.

Parameters:

  • prefered (:name, :entry, :checksum) (defaults to: :name)

Returns:

  • (String)


346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'lib/distillery/rom.rb', line 346

def to_s(prefered = :name)
    case prefered
    when :checksum
        if key = CHECKSUMS.find {|k| @cksum.include?(k) }
        then cksum(key, :hex)
        else self.name
        end
    when :name
        self.name
    when :entry
        self.entry
    else
        self.name
    end
end