class Distillery::ROM

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

Constants

CHECKSUMS

List of all supported checksums sorted by strength order

CHECKSUMS_DAT

List of all DAT supported checksums sorted by strengh order

CHECKSUMS_DEF

@!visibility private

CHECKSUMS_STRONG

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

CHECKSUMS_WEAK

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

FS_CHECKSUM

Checksum used when saving to file-system

HEADERS

@!visibility private

Public Class Methods

filecopy(from, to, length = nil, offset = 0, force: false, link: :hard) click to toggle source

Copy file, possibly using link if requested.

@param from [String] file to copy @param to [String] file destination @param length [Integer,nil] data length to be copied @param offset [Integer] data offset @param force [Boolean] remove previous file if necessary @param link [:hard, :sym, nil] use link instead of copy if possible

@return [Boolean] status of the operation

# 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) click to toggle source

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.

@param file [String] path or relative path to file @param root [String] anchor for the relative entry path @param headers [Array,nil,false] header definition list

@return [ROM] based on `file` content

# 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) click to toggle source

Check if an header is detected

@param data [String] data sample for header detection @param ext [String,nil] extension name as hint @param headers [Array] header definition list

@raise [HeaderLookupError] sample is too short

@return [Integer,nil] ROM offset

# 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) click to toggle source

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

@param io [#read] input object responding to read @param bufsize [Integer] buffer size in kB @param headers [Array,nil,false] header definition list

@return [Hash{Symbol=>Object}] ROM information

# 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
new(path, logger: nil, offset: nil, size: nil, **cksums) click to toggle source

Create ROM representation.

@param path [ROM::Path] rom path @param size [Integer] size rom size @param offset [Integer,nil] rom start (if headered) @option cksums [String,Integer] :sha1 rom checksum using sha1 @option cksums [String,Integer] :md5 rom checksum using md5 @option cksums [String,Integer] :crc32 rom checksum using crc32

# 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

Public Instance Methods

cksum(type, fmt=:bin) click to toggle source

Get the ROM specific checksum

@param type checksum type must be one defined in CHECKSUMS @param fmt [:bin,:hex] checksum formating

@return [String] checksum value (either binary string

or as an hexadecimal string)

@raise [ArgumentError] if `type` is not one defined in {CHECKSUMS}

or `fmt` is not :bin or :hex
# 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) click to toggle source

Get the ROM checksums

@param fmt [:bin,:hex] checksum formating

@return [Hash{Symbol=>String}] checksum

@raise [ArgumentError] if `type` is not one defined in {CHECKSUMS}

or `fmt` is not :bin or :hex
# 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) click to toggle source

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

@param to [String] file destination @param length [Integer,nil] data length to be copied @param part [:all,:header,:rom] which part of the rom file to copy @param link [:hard, :sym, nil] use link instead of copy if possible

@return [Boolean] status of the operation

# 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
crc32() click to toggle source

Get ROM crc32 as hexadcimal string (if defined)

@return [String,nil] hexadecimal checksum value

# File lib/distillery/rom.rb, line 472
def crc32
    cksum(:crc32, :hex)
end
delete!() click to toggle source

Delete physical content.

@return [Boolean]

# File lib/distillery/rom.rb, line 551
def delete!
    if @path.delete!
        @path == ROM::Path::Virtual.new(@path.entry)
    end
end
fshash() click to toggle source

Checksum to be used for naming on filesystem

@return [String] checksum hexstring

# File lib/distillery/rom.rb, line 428
def fshash
    cksum(FS_CHECKSUM, :hex)
end
has_content?() click to toggle source

Check if ROM hold content

@return [Boolean]

# File lib/distillery/rom.rb, line 336
def has_content?
    ! @path.storage.nil?
end
header() click to toggle source

Get ROM header

@return [String]

# File lib/distillery/rom.rb, line 376
def header
    return nil if !headered?
    @path.reader {|io| io.read(@offset) }
end
headered?() click to toggle source

Does this ROM have an header?

@return [Boolean]

# File lib/distillery/rom.rb, line 367
def headered?
    !@offset.nil? && (@offset > 0)
end
md5() click to toggle source

Get ROM md5 as hexadecimal string (if defined)

@return [String,nil] hexadecimal checksum value

# File lib/distillery/rom.rb, line 463
def md5
    cksum(:md5, :hex)
end
missing_checksums?(checksums = CHECKSUMS_DAT) click to toggle source

Are some checksums missing?

@param checksums [Array<Symbol>] list of checksums to consider

@return [Boolean]

# File lib/distillery/rom.rb, line 483
def missing_checksums?(checksums = CHECKSUMS_DAT)
    @cksum.keys != checksums
end
missing_size?() click to toggle source

Is size information missing? @return [Boolean]

# File lib/distillery/rom.rb, line 435
def missing_size?
    @size.nil?
end
name() click to toggle source

Get ROM name.

@return [String]

# File lib/distillery/rom.rb, line 492
def name
    @path.basename
end
path() click to toggle source

Get ROM path.

@return [String]

# File lib/distillery/rom.rb, line 501
def path
    @path
end
reader(&block) click to toggle source

ROM reader

@yieldparam [#read] io stream for reading

@return block value

# File lib/distillery/rom.rb, line 512
def reader(&block)
    @path.reader(&block)
end
rename(path, force: false) { |old_entry, entry| ... } click to toggle source

Rename ROM and physical content.

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

@param path [String] new ROM path @param force [Boolean] remove previous file if necessary

@return [Boolean] status of the operation

@yield Rename operation (optional) @yieldparam old [String] old entry name @yieldparam new [String] new entry name

# 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) click to toggle source

Compare ROMs using their checksums.

@param o [ROM] other rom @param weak [Boolean] use weak checksum if necessary

@return [Boolean] if they are the same or not @return [nil] if it wasn't decidable due to missing checksum

# 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
sha1() click to toggle source

Get ROM sha1 as hexadecimal string (if defined)

@return [String,nil] hexadecimal checksum value

# File lib/distillery/rom.rb, line 454
def sha1
    cksum(:sha1, :hex)
end
size() click to toggle source

Get ROM size in bytes.

@return [Integer] ROM size in bytes @return [nil] ROM has no size

# File lib/distillery/rom.rb, line 445
def size
    @size
end
to_s(prefered = :name) click to toggle source

String representation.

@param prefered [:name, :entry, :checksum]

@return [String]

# 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