fontist/excavate

View on GitHub
lib/excavate/extractors/cpio/cpio_old_format.rb

Summary

Maintainability
B
5 hrs
Test Coverage
# code is obtained from https://github.com/thumblemonks/cpio/blob/bad40c293280bb3c1678251c66f0f1f6fb1cc03e/cpio.rb
# rubocop:disable all

require 'stringio'

module CPIO
  class ArchiveFormatError < IOError; end

  class ArchiveHeader
    Magic = '070707'
    Fields = [[6,  :magic   ],
              [6,  :dev     ],
              [6,  :inode   ],
              [6,  :mode    ],
              [6,  :uid     ],
              [6,  :gid     ],
              [6,  :numlinks],
              [6,  :rdev    ],
              [11, :mtime   ],
              [6,  :namesize],
              [11, :filesize]]

    FieldDefaults = {:magic    => Integer(Magic),
                     :dev      => 0777777,
                     :inode    => 0,
                     :mode     => 0100444,
                     :uid      => 0,
                     :gid      => 0,
                     :numlinks => 1,
                     :rdev     => 0,
                     :mtime    => lambda { Time.now.to_i }}

    FieldMaxValues = Fields.inject({}) do |map,(width,name)|
      map[name] = Integer("0#{'7' * width}")
      map
    end

    HeaderSize = Fields.inject(0) do |sum,(size,name)|
      sum + size
    end

    HeaderUnpackFormat = Fields.collect do |size,name|
      "a%s" % size
    end.join('')

    Fields.each do |(size,name)|
      define_method(name) { @attrs[name.to_sym] }
    end

    class << self
      private :new
    end

    def initialize(attrs)
      @attrs = attrs
      check_attrs
    end

    def self.from(io)
      data = io.read(HeaderSize)
      verify_size(data)
      verify_magic(data)
      new(unpack_data(data))
    end

    def self.with_defaults(opts)
      name = opts[:name]
      defaults = FieldDefaults.merge(:mode => opts[:mode], :filesize => opts[:filesize], :namesize => name.size + 1)
      new(defaults)
    end

    def to_data
      Fields.collect do |(width,name)|
        raise ArchiveFormatError, "Expected header to have key #{name}" unless @attrs.has_key?(name)
        val = @attrs[name].respond_to?(:to_proc) ? @attrs[name].call : @attrs[name]
        raise ArchiveFormatError, "Header value for #{name} exceeds max length of #{FieldMaxValues[name]}" if val > FieldMaxValues[name]
        sprintf("%0*o", Fields.rassoc(name).first, val)
      end.join('')
    end

  private

    def check_attrs
      [:mode, :namesize, :filesize].each do |attr|
        raise ArgumentError, "#{attr.inspect} must be given" if !@attrs.has_key?(attr)
      end
    end

    def self.verify_size(data)
      unless data.size == HeaderSize
        raise ArchiveFormatError, "Header is not long enough to be a valid CPIO archive with ASCII headers."
      end
    end

    def self.verify_magic(data)
      unless data[0..Magic.size - 1] == Magic
        raise ArchiveFormatError, "Archive does not seem to be a valid CPIO archive with ASCII headers."
      end
    end

    def self.unpack_data(data)
      contents = {}
      data.unpack(HeaderUnpackFormat).zip(Fields) do |(chunk,(size,name))|
        contents[name] = Integer("0#{chunk}")
      end
      contents
    end

  end

  class ArchiveEntry
    TrailerMagic = "TRAILER!!!"
    S_IFMT  = 0170000   # bitmask for the file type bitfields
    S_IFREG = 0100000   # regular file
    S_IFDIR = 0040000   # directory

    ExecutableMask = (0100 | # Owner executable
                      0010 | # Group executable
                      0001)  # Other executable

    attr_reader :filename, :data

    class << self
      private :new
    end

    def self.from(io)
      header = ArchiveHeader.from(io)
      filename = read_filename(header, io)
      data = read_data(header, io)
      new(header, filename, data)
    end

    def self.new_directory(opts)
      mode = S_IFDIR | opts[:mode]
      header = ArchiveHeader.with_defaults(:mode => mode, :name => opts[:name], :filesize => 0)
      new(header, opts[:name], '')
    end

    def self.new_file(opts)
      mode = S_IFREG | opts[:mode]
      header = ArchiveHeader.with_defaults(:mode => mode, :name => opts[:name], :filesize => opts[:io].size)
      opts[:io].rewind
      new(header, opts[:name], opts[:io].read)
    end

    def self.new_trailer
      header = ArchiveHeader.with_defaults(:mode => S_IFREG, :name => TrailerMagic, :filesize => 0)
      new(header, TrailerMagic, '')
    end

    def initialize(header, filename, data)
      @header = header
      @filename = filename
      @data = data
    end

    def trailer?
      @filename == TrailerMagic && @data.size == 0
    end

    def directory?
      mode & S_IFMT == S_IFDIR
    end

    def file?
      mode & S_IFMT == S_IFREG
    end

    def executable?
      (mode & ExecutableMask) != 0
    end

    def mode
      @mode ||= sprintf('%o', @header.mode).to_s.oct
    end

    def to_data
      sprintf("%s%s\000%s", @header.to_data, filename, data)
    end

  private

    def self.read_filename(header, io)
      fname = io.read(header.namesize)
      if fname.size != header.namesize
        raise ArchiveFormatError, "Archive header seems to innacurately contain length of filename"
      end
      fname.chomp("\000")
    end

    def self.read_data(header, io)
      data = io.read(header.filesize)
      if data.size != header.filesize
        raise ArchiveFormatError, "Archive header seems to inaccurately contain length of the entry"
      end
      data
    end

  end

  class ArchiveWriter
    class ArchiveFinalizedError < RuntimeError; end

    def initialize(io)
      @io = io
      @open = false
    end

    def open?
      @open
    end

    def open
      raise ArchiveFinalizedError, "This archive has already been finalized" if @finalized
      @open = true
      yield(self)
    ensure
      close
      finalize
    end

    def mkdir(name, mode = 0555)
      entry = ArchiveEntry.new_directory(:name => name, :mode => mode)
      @io.write(entry.to_data)
    end

    def add_file(name, mode = 0444)
      file = StringIO.new
      yield(file)
      entry = ArchiveEntry.new_file(:name => name, :mode => mode, :io => file)
      @io.write(entry.to_data)
    end

  private

    def add_entry(opts)
    end

    def write_trailer
      entry = ArchiveEntry.new_trailer
      @io.write(entry.to_data)
    end

    def finalize
      write_trailer
      @finalized = true
    end

    def check_open
      raise "#{self.class.name} not open for writing" unless open?
    end

    def close
      @open = false
    end

  end # ArchiveWriter

  class ArchiveReader

    def initialize(io)
      @io = io
    end

    def each_entry
      @io.rewind
      while (entry = ArchiveEntry.from(@io)) && !entry.trailer?
        yield(entry)
      end
    end

  end # ArchiveReader

end   # CPIO

if $PROGRAM_NAME == __FILE__
require 'stringio'
require 'test/unit'
require 'digest/sha1'

class CPIOArchiveReaderTest < Test::Unit::TestCase
  CPIOFixture = StringIO.new(DATA.read)
  # These are SHA1 hashes
  ExpectedFixtureHashes = { 'cpio_test/test_executable'    => '97bd38305a81f2d89b5f3aa44500ec964b87cf8a',
                            'cpio_test/test_dir/test_file' => 'e7f1aa55a7f83dc99c9978b91072d01a3f5c812e' }

  def test_given_a_archive_with_a_bad_magic_number_should_raise
    assert_raises(CPIO::ArchiveFormatError) do
      CPIO::ArchiveReader.new(StringIO.new('foo')).each_entry { }
    end
  end

  def test_given_a_archive_with_a_valid_magic_number_should_not_raise
    archive = CPIO::ArchiveReader.new(CPIOFixture)
    assert_nil archive.each_entry { }
  end

  def test_given_a_valid_archive_should_have_the_expected_number_of_entries
    archive = CPIO::ArchiveReader.new(CPIOFixture)
    entries = 4
    archive.each_entry { |ent| entries -= 1 }
    assert_equal 0, entries, "Expected #{entries} in the archive."
  end

  def test_given_a_valid_archive_should_have_the_expected_entry_filenames
    expected = %w[cpio_test cpio_test/test_dir cpio_test/test_dir/test_file cpio_test/test_executable]
    archive = CPIO::ArchiveReader.new(CPIOFixture)
    archive.each_entry { |ent| expected.delete(ent.filename) }
    assert_equal 0, expected.size, "The expected array should be empty but we still have: #{expected.inspect}"
  end

  def test_given_a_valid_archive_should_have_the_expected_number_of_directories
    expected = 2
    archive = CPIO::ArchiveReader.new(CPIOFixture)
    archive.each_entry { |ent| expected -= 1 if ent.directory? }
    assert_equal 0, expected
  end

  def test_given_a_valid_archive_should_have_the_expected_number_of_regular_files
    expected = 1
    archive = CPIO::ArchiveReader.new(CPIOFixture)
    archive.each_entry { |ent| expected -= 1 if ent.file? && !ent.executable? }
    assert_equal 0, expected
  end

  def test_given_a_valid_archive_should_have_the_expected_number_of_executable_files
    expected = 1
    archive = CPIO::ArchiveReader.new(CPIOFixture)
    archive.each_entry { |ent| expected -= 1 if ent.file? && ent.executable? }
    assert_equal 0, expected
  end

  def test_given_a_valid_archive_should_have_correct_file_contents
    expected = ExpectedFixtureHashes.size
    archive = CPIO::ArchiveReader.new(CPIOFixture)
    archive.each_entry do |ent|
      if (sha1_hash = ExpectedFixtureHashes[ent.filename]) && Digest::SHA1.hexdigest(ent.data) == sha1_hash
        expected -= 1
      end
    end
    assert_equal 0, expected, "Expected all files in the archive to hash correctly."
  end

end

class CPIOArchiveWriterTest < Test::Unit::TestCase

  def test_making_directories_should_work
    expected = 2
    io = StringIO.new
    archiver = CPIO::ArchiveWriter.new(io)
    archiver.open do |arch|
      arch.mkdir "foo"
      arch.mkdir "bar"
    end
    CPIO::ArchiveReader.new(io).each_entry { |ent| expected -= 1 if ent.directory? }
    assert_equal 0, expected
  end

  def test_making_files_should_work
    expected = 2
    io = StringIO.new
    archiver = CPIO::ArchiveWriter.new(io)
    archiver.open do |arch|
      arch.add_file("foo") { |sio| sio.write("foobar") }
      arch.add_file("barfoo") { |sio| sio.write("barfoo") }
    end
    CPIO::ArchiveReader.new(io).each_entry { |ent| expected -= 1 if ent.file? }
    assert_equal 0, expected
  end

  def test_making_files_and_directories_should_work
    expected = 4
    io = StringIO.new
    archiver = CPIO::ArchiveWriter.new(io)
    archiver.open do |arch|
      arch.mkdir "blah"
      arch.add_file("foo") { |sio| sio.write("foobar") }
      arch.add_file("barfoo") { |sio| sio.write("barfoo") }
      arch.add_file("barfoobaz", 0111) { |sio| sio.write("wee") }
    end
    CPIO::ArchiveReader.new(io).each_entry { |ent| expected -= 1 }
    assert_equal 0, expected
  end

  def test_adding_empty_files_should_work
    expected = 1
    io = StringIO.new
    archiver = CPIO::ArchiveWriter.new(io)
    archiver.open do |arch|
      arch.add_file("barfoo", 0111) { |sio| }
    end
    CPIO::ArchiveReader.new(io).each_entry { |ent| expected -= 1 if ent.file? }
    assert_equal 0, expected
  end

  def test_adding_a_file_with_an_excessively_long_name_should_raise
    archiver = CPIO::ArchiveWriter.new(StringIO.new)
    assert_raise(CPIO::ArchiveFormatError) do
      archiver.open do |arch|
        name = "fffff" * (CPIO::ArchiveHeader::FieldMaxValues[:namesize])
        arch.add_file(name) { |sio| }
      end
    end
  end

  def test_adding_a_non_executable_file_should_preserve_said_mode
    io = StringIO.new
    archiver = CPIO::ArchiveWriter.new(io)
    archiver.open do |arch|
      arch.add_file("barfoo", 0444) { |sio| }
    end
    CPIO::ArchiveReader.new(io).each_entry do |ent|
      assert !ent.executable? && ent.file?
    end
  end

  def test_adding_an_executable_file_should_preserve_said_mode
    io = StringIO.new
    archiver = CPIO::ArchiveWriter.new(io)
    archiver.open do |arch|
      arch.add_file("barfoo", 0500) { |sio| }
    end
    CPIO::ArchiveReader.new(io).each_entry do |ent|
      assert ent.executable? && ent.file?
    end
  end
end

end

__END__
0707077777770465470407550007650000240000040000001130242405100001200000000000cpio_test0707077777770465520407550007650000240000030000001130242404300002300000000000cpio_test/test_dir0707077777770465531006440007650000240000010000001130242637200003500000000016cpio_test/test_dir/test_filefoobarbazbeep
0707077777770465541007550007650000240000010000001130242636000003200000000012cpio_test/test_executablefoobarbaz
0707070000000000000000000000000000000000010000000000000000000001300000000000TRAILER!!!