package Option::ROM;

# Copyright (C) 2008 Michael Brown <mbrown@fensystems.co.uk>.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.

=head1 NAME

Option::ROM - Option ROM manipulation

=head1 SYNOPSIS

    use Option::ROM;

    # Load a ROM image
    my $rom = new Option::ROM;
    $rom->load ( "rtl8139.rom" );

    # Modify the PCI device ID
    $rom->pci_header->{device_id} = 0x1234;
    $rom->fix_checksum();

    # Write ROM image out to a new file
    $rom->save ( "rtl8139-modified.rom" );

=head1 DESCRIPTION

C<Option::ROM> provides a mechanism for manipulating Option ROM
images.

=head1 METHODS

=cut

##############################################################################
#
# Option::ROM::Fields
#
##############################################################################

package Option::ROM::Fields;

use strict;
use warnings;
use Carp;
use bytes;

sub TIEHASH {
  my $class = shift;
  my $self = shift;

  bless $self, $class;
  return $self;
}

sub FETCH {
  my $self = shift;
  my $key = shift;

  return undef unless $self->EXISTS ( $key );
  my $raw = substr ( ${$self->{data}},
		     ( $self->{offset} + $self->{fields}->{$key}->{offset} ),
		     $self->{fields}->{$key}->{length} );
  my $unpack = ( ref $self->{fields}->{$key}->{unpack} ?
		 $self->{fields}->{$key}->{unpack} :
		 sub { unpack ( $self->{fields}->{$key}->{pack}, shift ); } );
  return &$unpack ( $raw );
}

sub STORE {
  my $self = shift;
  my $key = shift;
  my $value = shift;

  croak "Nonexistent field \"$key\"" unless $self->EXISTS ( $key );
  my $pack = ( ref $self->{fields}->{$key}->{pack} ?
	       $self->{fields}->{$key}->{pack} :
	       sub { pack ( $self->{fields}->{$key}->{pack}, shift ); } );
  my $raw = &$pack ( $value );
  substr ( ${$self->{data}},
	   ( $self->{offset} + $self->{fields}->{$key}->{offset} ),
	   $self->{fields}->{$key}->{length} ) = $raw;
}

sub DELETE {
  my $self = shift;
  my $key = shift;

  $self->STORE ( $key, 0 );
}

sub CLEAR {
  my $self = shift;

  foreach my $key ( keys %{$self->{fields}} ) {
    $self->DELETE ( $key );
  }
}

sub EXISTS {
  my $self = shift;
  my $key = shift;

  return ( exists $self->{fields}->{$key} &&
	   ( ( $self->{fields}->{$key}->{offset} +
	       $self->{fields}->{$key}->{length} ) <= $self->{length} ) &&
	   ( ! defined $self->{fields}->{$key}->{check} ||
	     &{$self->{fields}->{$key}->{check}} ( $self, $key ) ) );
}

sub FIRSTKEY {
  my $self = shift;

  keys %{$self->{fields}};
  return each %{$self->{fields}};
}

sub NEXTKEY {
  my $self = shift;
  my $lastkey = shift;

  return each %{$self->{fields}};
}

sub SCALAR {
  my $self = shift;

  return 1;
}

sub UNTIE {
  my $self = shift;
}

sub DESTROY {
  my $self = shift;
}

sub checksum {
  my $self = shift;

  my $raw = substr ( ${$self->{data}}, $self->{offset}, $self->{length} );
  return unpack ( "%8C*", $raw );
}

##############################################################################
#
# Option::ROM
#
##############################################################################

package Option::ROM;

use strict;
use warnings;
use Carp;
use bytes;
use Exporter 'import';

use constant ROM_SIGNATURE => 0xaa55;
use constant PCI_SIGNATURE => 'PCIR';
use constant PCI_LAST_IMAGE => 0x80;
use constant PNP_SIGNATURE => '$PnP';
use constant UNDI_SIGNATURE => 'UNDI';
use constant IPXE_SIGNATURE => 'iPXE';
use constant EFI_SIGNATURE => 0x00000ef1;

our @EXPORT_OK = qw ( ROM_SIGNATURE PCI_SIGNATURE PCI_LAST_IMAGE
		      PNP_SIGNATURE UNDI_SIGNATURE IPXE_SIGNATURE EFI_SIGNATURE );
our %EXPORT_TAGS = ( all => [ @EXPORT_OK ] );

use constant JMP_SHORT => 0xeb;
use constant JMP_NEAR => 0xe9;
use constant CALL_NEAR => 0xe8;

sub pack_init {
  my $dest = shift;

  # Always create a near jump; it's simpler
  if ( $dest ) {
    return pack ( "CS", JMP_NEAR, ( $dest - 6 ) );
  } else {
    return pack ( "CS", 0, 0 );
  }
}

sub unpack_init {
  my $instr = shift;

  # Accept both short and near jumps
  my $jump = unpack ( "C", $instr );
  if ( $jump == JMP_SHORT ) {
    my $offset = unpack ( "xC", $instr );
    return ( $offset + 5 );
  } elsif ( $jump == JMP_NEAR ) {
    my $offset = unpack ( "xS", $instr );
    return ( $offset + 6 );
  } elsif ( $jump == CALL_NEAR ) {
    my $offset = unpack ( "xS", $instr );
    return ( $offset + 6 );
  } elsif ( $jump == 0 ) {
    return 0;
  } else {
    carp "Unrecognised jump instruction in init vector\n";
    return 0;
  }
}

sub check_pcat_rom {
  my $self = shift;
  my $key = shift;

  my $pci = $self->{rom}->pci_header ();

  return ! defined $pci || $pci->{code_type} == 0x00;
}

=pod

=item C<< new () >>

Construct a new C<Option::ROM> object.

=cut

sub new {
  my $class = shift;

  my $hash = {};
  tie %$hash, "Option::ROM::Fields", {
    rom => $hash, # ROM object itself
    data => undef,
    offset => 0x00,
    length => 0x20,
    file_offset => 0x0,
    fields => {
      signature =>	{ offset => 0x00, length => 0x02, pack => "S" },
      length =>		{ offset => 0x02, length => 0x01, pack => "C" },
      # "init" is part of a jump instruction
      init =>		{ offset => 0x03, length => 0x03,
			  pack => \&pack_init, unpack => \&unpack_init,
			  check => \&check_pcat_rom },
      checksum =>	{ offset => 0x06, length => 0x01, pack => "C",
			  check => \&check_pcat_rom },
      ipxe_header =>	{ offset => 0x10, length => 0x02, pack => "S",
			  check => \&check_pcat_rom },
      bofm_header =>	{ offset => 0x14, length => 0x02, pack => "S",
			  check => \&check_pcat_rom },
      undi_header =>	{ offset => 0x16, length => 0x02, pack => "S",
			  check => \&check_pcat_rom },
      pci_header =>	{ offset => 0x18, length => 0x02, pack => "S" },
      pnp_header =>	{ offset => 0x1a, length => 0x02, pack => "S",
			  check => \&check_pcat_rom },
    },
  };
  bless $hash, $class;
  return $hash;
}

=pod

=item C<< set ( $data [, $file_offset ] ) >>

Set option ROM contents, optionally sets original file offset.

=cut

sub set {
  my $hash = shift;
  my $self = tied(%$hash);
  my $data = shift;
  my $file_offset = shift // 0x0;

  # Store data
  $self->{data} = \$data;
  $self->{file_offset} = $file_offset;

  # Split out any data belonging to the next image
  delete $self->{next_image};
  my $pci_header = $hash->pci_header();
  if ( ( defined $pci_header ) &&
       ( ! ( $pci_header->{last_image} & PCI_LAST_IMAGE ) ) ) {
    my $length = ( $pci_header->{image_length} * 512 );
    my $remainder = substr ( $data, $length );
    $data = substr ( $data, 0, $length );
    $self->{next_image} = new Option::ROM;
    $self->{next_image}->set ( $remainder, $self->{file_offset} + $length );
  }
}

=pod

=item C<< get () >>

Get option ROM contents.

=cut

sub get {
  my $hash = shift;
  my $self = tied(%$hash);

  my $data = ${$self->{data}};
  $data .= $self->{next_image}->get() if $self->{next_image};
  return $data;
}

=pod

=item C<< load ( $filename ) >>

Load option ROM contents from the file C<$filename>.

=cut

sub load {
  my $hash = shift;
  my $self = tied(%$hash);
  my $filename = shift;

  $self->{filename} = $filename;

  open my $fh, "<$filename"
      or croak "Cannot open $filename for reading: $!";
  binmode $fh;
  read $fh, my $data, -s $fh;
  $hash->set ( $data );
  close $fh;
}

=pod

=item C<< save ( [ $filename ] ) >>

Write the ROM data back out to the file C<$filename>.  If C<$filename>
is omitted, the file used in the call to C<load()> will be used.

=cut

sub save {
  my $hash = shift;
  my $self = tied(%$hash);
  my $filename = shift;

  $filename ||= $self->{filename};

  open my $fh, ">$filename"
      or croak "Cannot open $filename for writing: $!";
  my $data = $hash->get();
  binmode $fh;
  print $fh $data;
  close $fh;
}

=pod

=item C<< length () >>

Length of option ROM data.  This is the length of the file, not the
length from the ROM header length field.

=cut

sub length {
  my $hash = shift;
  my $self = tied(%$hash);

  return length ${$self->{data}};
}

=pod

=item C<< pci_header () >>

Return a C<Option::ROM::PCI> object representing the ROM's PCI header,
if present.

=cut

sub pci_header {
  my $hash = shift;
  my $self = tied(%$hash);

  my $offset = $hash->{pci_header};
  return undef unless $offset;

  return Option::ROM::PCI->new ( $self, $offset );
}

=pod

=item C<< pnp_header () >>

Return a C<Option::ROM::PnP> object representing the ROM's PnP header,
if present.

=cut

sub pnp_header {
  my $hash = shift;
  my $self = tied(%$hash);

  my $offset = $hash->{pnp_header};
  return undef unless $offset;

  return Option::ROM::PnP->new ( $self, $offset );
}

=pod

=item C<< undi_header () >>

Return a C<Option::ROM::UNDI> object representing the ROM's UNDI header,
if present.

=cut

sub undi_header {
  my $hash = shift;
  my $self = tied(%$hash);

  my $offset = $hash->{undi_header};
  return undef unless $offset;

  return Option::ROM::UNDI->new ( $self, $offset );
}

=pod

=item C<< ipxe_header () >>

Return a C<Option::ROM::iPXE> object representing the ROM's iPXE
header, if present.

=cut

sub ipxe_header {
  my $hash = shift;
  my $self = tied(%$hash);

  my $offset = $hash->{ipxe_header};
  return undef unless $offset;

  return Option::ROM::iPXE->new ( $self, $offset );
}

=pod

=item C<< efi_header () >>

Return a C<Option::ROM::EFI> object representing the ROM's EFI header,
if present.

=cut

sub efi_header {
  my $hash = shift;
  my $self = tied(%$hash);

  my $pci = $hash->pci_header ();
  return undef unless defined $pci;

  return Option::ROM::EFI->new ( $self, $pci );
}

=pod

=item C<< next_image () >>

Return a C<Option::ROM> object representing the next image within the
ROM, if present.

=cut

sub next_image {
  my $hash = shift;
  my $self = tied(%$hash);

  return $self->{next_image};
}

=pod

=item C<< checksum () >>

Calculate the byte checksum of the ROM.

=cut

sub checksum {
  my $hash = shift;
  my $self = tied(%$hash);

  my $raw = substr ( ${$self->{data}}, 0, ( $hash->{length} * 512 ) );
  return unpack ( "%8C*", $raw );
}

=pod

=item C<< fix_checksum () >>

Fix the byte checksum of the ROM.

=cut

sub fix_checksum {
  my $hash = shift;
  my $self = tied(%$hash);

  return unless ( exists $hash->{checksum} );
  $hash->{checksum} = ( ( $hash->{checksum} - $hash->checksum() ) & 0xff );
}

=pod

=item C<< file_offset () >>

Get file offset of image.

=cut

sub file_offset {
  my $hash = shift;
  my $self = tied(%$hash);

  return $self->{file_offset};
}

##############################################################################
#
# Option::ROM::PCI
#
##############################################################################

package Option::ROM::PCI;

use strict;
use warnings;
use Carp;
use bytes;

sub new {
  my $class = shift;
  my $rom = shift;
  my $offset = shift;

  my $hash = {};
  tie %$hash, "Option::ROM::Fields", {
    rom => $rom,
    data => $rom->{data},
    offset => $offset,
    length => 0x0c,
    fields => {
      signature =>	{ offset => 0x00, length => 0x04, pack => "a4" },
      vendor_id =>	{ offset => 0x04, length => 0x02, pack => "S" },
      device_id =>	{ offset => 0x06, length => 0x02, pack => "S" },
      device_list =>	{ offset => 0x08, length => 0x02, pack => "S" },
      struct_length =>	{ offset => 0x0a, length => 0x02, pack => "S" },
      struct_revision =>{ offset => 0x0c, length => 0x01, pack => "C" },
      prog_intf => 	{ offset => 0x0d, length => 0x01, pack => "C" },
      sub_class => 	{ offset => 0x0e, length => 0x01, pack => "C" },
      base_class => 	{ offset => 0x0f, length => 0x01, pack => "C" },
      image_length =>	{ offset => 0x10, length => 0x02, pack => "S" },
      revision =>	{ offset => 0x12, length => 0x02, pack => "S" },
      code_type => 	{ offset => 0x14, length => 0x01, pack => "C" },
      last_image => 	{ offset => 0x15, length => 0x01, pack => "C" },
      runtime_length =>	{ offset => 0x16, length => 0x02, pack => "S" },
      conf_header =>	{ offset => 0x18, length => 0x02, pack => "S" },
      clp_entry =>	{ offset => 0x1a, length => 0x02, pack => "S" },
    },
  };
  bless $hash, $class;

  my $self = tied ( %$hash );
  my $length = $rom->{rom}->length ();

  return undef unless ( $offset + $self->{length} <= $length &&
			$hash->{signature} eq Option::ROM::PCI_SIGNATURE &&
			$offset + $hash->{struct_length} <= $length );

  # Retrieve true length of structure
  $self->{length} = $hash->{struct_length};

  return $hash;
}

sub device_list {
  my $hash = shift;
  my $self = tied(%$hash);

  my $device_list = $hash->{device_list};
  return undef unless $device_list;

  my @ids;
  my $offset = ( $self->{offset} + $device_list );
  while ( 1 ) {
    my $raw = substr ( ${$self->{data}}, $offset, 2 );
    my $id = unpack ( "S", $raw );
    last unless $id;
    push @ids, $id;
    $offset += 2;
  }

  return @ids;
}

##############################################################################
#
# Option::ROM::PnP
#
##############################################################################

package Option::ROM::PnP;

use strict;
use warnings;
use Carp;
use bytes;

sub new {
  my $class = shift;
  my $rom = shift;
  my $offset = shift;

  my $hash = {};
  tie %$hash, "Option::ROM::Fields", {
    rom => $rom,
    data => $rom->{data},
    offset => $offset,
    length => 0x06,
    fields => {
      signature =>	{ offset => 0x00, length => 0x04, pack => "a4" },
      struct_revision =>{ offset => 0x04, length => 0x01, pack => "C" },
      struct_length =>	{ offset => 0x05, length => 0x01, pack => "C" },
      checksum =>	{ offset => 0x09, length => 0x01, pack => "C" },
      manufacturer =>	{ offset => 0x0e, length => 0x02, pack => "S" },
      product =>	{ offset => 0x10, length => 0x02, pack => "S" },
      bcv =>		{ offset => 0x16, length => 0x02, pack => "S" },
      bdv =>		{ offset => 0x18, length => 0x02, pack => "S" },
      bev =>		{ offset => 0x1a, length => 0x02, pack => "S" },
    },
  };
  bless $hash, $class;

  my $self = tied ( %$hash );
  my $length = $rom->{rom}->length ();

  return undef unless ( $offset + $self->{length} <= $length &&
			$hash->{signature} eq Option::ROM::PNP_SIGNATURE &&
			$offset + $hash->{struct_length} * 16 <= $length );

  # Retrieve true length of structure
  $self->{length} = ( $hash->{struct_length} * 16 );

  return $hash;
}

sub checksum {
  my $hash = shift;
  my $self = tied(%$hash);

  return $self->checksum();
}

sub fix_checksum {
  my $hash = shift;
  my $self = tied(%$hash);

  $hash->{checksum} = ( ( $hash->{checksum} - $hash->checksum() ) & 0xff );
}

sub manufacturer {
  my $hash = shift;
  my $self = tied(%$hash);

  my $manufacturer = $hash->{manufacturer};
  return undef unless $manufacturer;

  my $raw = substr ( ${$self->{data}}, $manufacturer );
  return unpack ( "Z*", $raw );
}

sub product {
  my $hash = shift;
  my $self = tied(%$hash);

  my $product = $hash->{product};
  return undef unless $product;

  my $raw = substr ( ${$self->{data}}, $product );
  return unpack ( "Z*", $raw );
}

##############################################################################
#
# Option::ROM::UNDI
#
##############################################################################

package Option::ROM::UNDI;

use strict;
use warnings;
use Carp;
use bytes;

sub new {
  my $class = shift;
  my $rom = shift;
  my $offset = shift;

  my $hash = {};
  tie %$hash, "Option::ROM::Fields", {
    rom => $rom,
    data => $rom->{data},
    offset => $offset,
    length => 0x16,
    fields => {
      signature =>	{ offset => 0x00, length => 0x04, pack => "a4" },
      struct_length =>	{ offset => 0x04, length => 0x01, pack => "C" },
      checksum =>	{ offset => 0x05, length => 0x01, pack => "C" },
      struct_revision =>{ offset => 0x06, length => 0x01, pack => "C" },
      version_revision =>{ offset => 0x07, length => 0x01, pack => "C" },
      version_minor =>	{ offset => 0x08, length => 0x01, pack => "C" },
      version_major =>	{ offset => 0x09, length => 0x01, pack => "C" },
      loader_entry =>	{ offset => 0x0a, length => 0x02, pack => "S" },
      stack_size =>	{ offset => 0x0c, length => 0x02, pack => "S" },
      data_size =>	{ offset => 0x0e, length => 0x02, pack => "S" },
      code_size =>	{ offset => 0x10, length => 0x02, pack => "S" },
      bus_type =>	{ offset => 0x12, length => 0x04, pack => "a4" },
    },
  };
  bless $hash, $class;

  my $self = tied ( %$hash );
  my $length = $rom->{rom}->length ();

  return undef unless ( $offset + $self->{length} <= $length &&
			$hash->{signature} eq Option::ROM::UNDI_SIGNATURE &&
			$offset + $hash->{struct_length} <= $length );

  # Retrieve true length of structure
  $self->{length} = $hash->{struct_length};

  return $hash;
}

sub checksum {
  my $hash = shift;
  my $self = tied(%$hash);

  return $self->checksum();
}

sub fix_checksum {
  my $hash = shift;
  my $self = tied(%$hash);

  $hash->{checksum} = ( ( $hash->{checksum} - $hash->checksum() ) & 0xff );
}

##############################################################################
#
# Option::ROM::iPXE
#
##############################################################################

package Option::ROM::iPXE;

use strict;
use warnings;
use Carp;
use bytes;

sub new {
  my $class = shift;
  my $rom = shift;
  my $offset = shift;

  my $hash = {};
  tie %$hash, "Option::ROM::Fields", {
    rom => $rom,
    data => $rom->{data},
    offset => $offset,
    length => 0x06,
    fields => {
      signature =>	{ offset => 0x00, length => 0x04, pack => "a4" },
      struct_length =>	{ offset => 0x04, length => 0x01, pack => "C" },
      checksum =>	{ offset => 0x05, length => 0x01, pack => "C" },
      shrunk_length =>	{ offset => 0x06, length => 0x01, pack => "C" },
      build_id =>	{ offset => 0x08, length => 0x04, pack => "L" },
    },
  };
  bless $hash, $class;

  my $self = tied ( %$hash );
  my $length = $rom->{rom}->length ();

  return undef unless ( $offset + $self->{length} <= $length &&
			$hash->{signature} eq Option::ROM::IPXE_SIGNATURE &&
			$offset + $hash->{struct_length} <= $length );

  # Retrieve true length of structure
  $self->{length} = $hash->{struct_length};

  return $hash;
}

sub checksum {
  my $hash = shift;
  my $self = tied(%$hash);

  return $self->checksum();
}

sub fix_checksum {
  my $hash = shift;
  my $self = tied(%$hash);

  $hash->{checksum} = ( ( $hash->{checksum} - $hash->checksum() ) & 0xff );
}

##############################################################################
#
# Option::ROM::EFI
#
##############################################################################

package Option::ROM::EFI;

use strict;
use warnings;
use Carp;
use bytes;

sub new {
  my $class = shift;
  my $rom = shift;
  my $pci = shift;

  my $hash = {};
  tie %$hash, "Option::ROM::Fields", {
    rom => $rom,
    data => $rom->{data},
    offset => 0x00,
    length => 0x18,
    fields => {
      signature =>		{ offset => 0x00, length => 0x02, pack => "S" },
      init_size =>		{ offset => 0x02, length => 0x02, pack => "S" },
      efi_signature =>		{ offset => 0x04, length => 0x04, pack => "L" },
      efi_subsystem =>		{ offset => 0x08, length => 0x02, pack => "S" },
      efi_machine_type =>	{ offset => 0x0a, length => 0x02, pack => "S" },
      compression_type =>	{ offset => 0x0c, length => 0x02, pack => "S" },
      efi_image_offset =>	{ offset => 0x16, length => 0x02, pack => "S" },
    },
  };
  bless $hash, $class;

  my $self = tied ( %$hash );

  return undef unless ( $hash->{efi_signature} == Option::ROM::EFI_SIGNATURE &&
			$pci->{code_type} == 0x03 );

  return $hash;
}

1;
