#include <string.h>
#include <stdio.h>
#include <errno.h>
+#include <gpxe/uaccess.h>
+#include <gpxe/uuid.h>
#include <realmode.h>
#include <pnpbios.h>
#include <smbios.h>
*
*/
-/** Signature for an SMBIOS structure */
+/** Signature for SMBIOS entry point */
#define SMBIOS_SIGNATURE \
( ( '_' << 0 ) + ( 'S' << 8 ) + ( 'M' << 16 ) + ( '_' << 24 ) )
-/** SMBIOS entry point */
+/**
+ * SMBIOS entry point
+ *
+ * This is the single table which describes the list of SMBIOS
+ * structures. It is located by scanning through the BIOS segment.
+ */
struct smbios_entry {
/** Signature
*
uint8_t bcd_revision;
} __attribute__ (( packed ));
-/** An SMBIOS structure */
+/**
+ * SMBIOS entry point descriptor
+ *
+ * This contains the information from the SMBIOS entry point that we
+ * care about.
+ */
struct smbios {
- /** Type */
- uint8_t type;
- /** Length */
- uint8_t length;
- /** Handle */
- uint16_t handle;
-} __attribute__ (( packed ));
+ /** Start of SMBIOS structures */
+ userptr_t address;
+ /** Length of SMBIOS structures */
+ size_t length;
+ /** Number of SMBIOS structures */
+ unsigned int count;
+};
-struct smbios_system_information {
- struct smbios header;
- uint8_t manufacturer;
- uint8_t product;
- uint8_t version;
- uint8_t serial;
-} __attribute__ (( packed ));
+/**
+ * SMBIOS strings descriptor
+ *
+ * This is returned as part of the search for an SMBIOS structure, and
+ * contains the information needed for extracting the strings within
+ * the "unformatted" portion of the structure.
+ */
+struct smbios_strings {
+ /** Start of strings data */
+ userptr_t data;
+ /** Length of strings data */
+ size_t length;
+};
/**
* Find SMBIOS
*
- * @v emtry SMBIOS entry point to fill in
- * @ret rc Return status code
+ * @ret smbios SMBIOS entry point descriptor, or NULL if not found
*/
-static int find_smbios_entry ( struct smbios_entry *entry ) {
+static struct smbios * find_smbios ( void ) {
+ static struct smbios smbios = {
+ .address = UNULL,
+ };
union {
struct smbios_entry entry;
uint8_t bytes[256]; /* 256 is maximum length possible */
unsigned int offset;
size_t len;
unsigned int i;
- uint8_t sum = 0;
+ uint8_t sum;
+
+ /* Return cached result if available */
+ if ( smbios.address != UNULL )
+ return &smbios;
/* Try to find SMBIOS */
for ( offset = 0 ; offset < 0x10000 ; offset += 0x10 ) {
/* Read whole header and verify checksum */
len = u.entry.length;
copy_from_real ( &u.bytes, BIOS_SEG, offset, len );
- for ( i = 0 ; i < len ; i++ ) {
+ for ( i = 0 , sum = 0 ; i < len ; i++ ) {
sum += u.bytes[i];
}
if ( sum != 0 ) {
/* Fill result structure */
DBG ( "Found SMBIOS entry point at %04x:%04x\n",
BIOS_SEG, offset );
- memcpy ( entry, &u.entry, sizeof ( *entry ) );
- return 0;
+ smbios.address = phys_to_user ( u.entry.smbios_address );
+ smbios.length = u.entry.smbios_length;
+ smbios.count = u.entry.smbios_count;
+ return &smbios;
}
DBG ( "No SMBIOS found\n" );
- return -ENOENT;
+ return NULL;
+}
+
+/**
+ * Find SMBIOS strings terminator
+ *
+ * @v smbios SMBIOS entry point descriptor
+ * @v offset Offset to start of strings
+ * @ret offset Offset to strings terminator, or 0 if not found
+ */
+static size_t find_strings_terminator ( struct smbios *smbios,
+ size_t offset ) {
+ size_t max_offset = ( smbios->length - 2 );
+ uint16_t nulnul;
+
+ for ( ; offset <= max_offset ; offset++ ) {
+ copy_from_user ( &nulnul, smbios->address, offset, 2 );
+ if ( nulnul == 0 )
+ return ( offset + 1 );
+ }
+ return 0;
}
/**
* Find specific structure type within SMBIOS
*
- * @v entry SMBIOS entry point
- * @v type Structure type
- * @v data SMBIOS structure buffer to fill in
+ * @v type Structure type to search for
+ * @v structure Buffer to fill in with structure
+ * @v length Length of buffer
+ * @v strings Strings descriptor to fill in, or NULL
* @ret rc Return status code
- *
- * The buffer must be at least @c entry->max bytes in size.
*/
-static int find_smbios ( struct smbios_entry *entry, unsigned int type,
- void *data ) {
- struct smbios *smbios = data;
- userptr_t smbios_address = phys_to_user ( entry->smbios_address );
+int find_smbios_structure ( unsigned int type, void *structure,
+ size_t length, struct smbios_strings *strings ) {
+ struct smbios *smbios;
+ struct smbios_header header;
+ struct smbios_strings temp_strings;
unsigned int count = 0;
size_t offset = 0;
- size_t frag_len;
- void *end;
-
- while ( ( offset < entry->smbios_length ) &&
- ( count < entry->smbios_count ) ) {
- /* Read next SMBIOS structure */
- frag_len = ( entry->smbios_length - offset );
- if ( frag_len > entry->max )
- frag_len = entry->max;
- copy_from_user ( data, smbios_address, offset, frag_len );
-
- /* Sanity protection; ensure the last two bytes of the
- * buffer are 0x00,0x00, just so that a terminator
- * exists somewhere. Also ensure that this lies
- * outside the formatted area.
- */
- *( ( uint16_t * ) ( data + entry->max - 2 ) ) = 0;
- if ( smbios->length > ( entry->max - 2 ) ) {
- DBG ( "Invalid SMBIOS structure length %zd\n",
- smbios->length );
+ size_t strings_offset;
+ size_t terminator_offset;
+
+ /* Locate SMBIOS entry point */
+ if ( ! ( smbios = find_smbios() ) )
+ return -ENOENT;
+
+ /* Ensure that we have a usable strings descriptor buffer */
+ if ( ! strings )
+ strings = &temp_strings;
+
+ /* Scan through list of structures */
+ while ( ( ( offset + sizeof ( header ) ) < smbios->length ) &&
+ ( count < smbios->count ) ) {
+
+ /* Read next SMBIOS structure header */
+ copy_from_user ( &header, smbios->address, offset,
+ sizeof ( header ) );
+
+ /* Determine start and extent of strings block */
+ strings_offset = ( offset + header.length );
+ if ( strings_offset > smbios->length ) {
+ DBG ( "SMBIOS structure at offset %zx with length "
+ "%zx extends beyond SMBIOS\n", offset,
+ header.length );
+ return -ENOENT;
+ }
+ terminator_offset =
+ find_strings_terminator ( smbios, strings_offset );
+ if ( ! terminator_offset ) {
+ DBG ( "SMBIOS structure at offset %zx has "
+ "unterminated strings section\n", offset );
return -ENOENT;
}
+ strings->data = userptr_add ( smbios->address,
+ strings_offset );
+ strings->length = ( terminator_offset - strings_offset );
- DBG ( "Found SMBIOS structure type %d at offset %zx\n",
- smbios->type, offset );
+ DBG ( "SMBIOS structure at offset %zx has type %d, "
+ "length %zx, strings length %zx\n",
+ offset, header.type, header.length, strings->length );
/* If this is the structure we want, return */
- if ( smbios->type == type )
+ if ( header.type == type ) {
+ if ( length > header.length )
+ length = header.length;
+ copy_from_user ( structure, smbios->address,
+ offset, length );
return 0;
-
- /* Find end of record. This will always exist, thanks
- * to our sanity check above.
- */
- for ( end = ( data + smbios->length ) ;
- end < ( data + entry->max ) ; end++ ) {
- if ( *( ( uint16_t * ) end ) == 0 ) {
- end += 2;
- break;
- }
}
- offset += ( end - data );
+ /* Move to next SMBIOS structure */
+ offset = ( terminator_offset + 1 );
count++;
}
/**
* Find indexed string within SMBIOS structure
*
- * @v data SMBIOS structure
+ * @v strings SMBIOS strings descriptor
* @v index String index
- * @ret string String, or NULL
+ * @v buffer Buffer for string
+ * @v length Length of string buffer
+ * @ret rc Return status code
*/
-static const char * find_smbios_string ( void *data, unsigned int index ) {
- struct smbios *smbios = data;
- const char *string;
- size_t len;
+int find_smbios_string ( struct smbios_strings *strings, unsigned int index,
+ char *buffer, size_t length ) {
+ size_t offset = 0;
+ size_t string_len;
+ /* Zero buffer. This ensures that a valid NUL terminator is
+ * always present (unless length==0).
+ */
+ memset ( buffer, 0, length );
+
+ /* String numbers start at 1 (0 is used to indicate "no string") */
if ( ! index )
- return NULL;
-
- string = ( data + smbios->length );
- while ( --index ) {
- /* Move to next string */
- len = strlen ( string );
- if ( len == 0 ) {
- /* Reached premature end of string table */
- DBG ( "SMBIOS string index %d not found\n", index );
- return NULL;
+ return 0;
+
+ while ( offset < strings->length ) {
+ /* Get string length. This is known safe, since the
+ * smbios_strings struct is constructed so as to
+ * always end on a string boundary.
+ */
+ string_len = strlen_user ( strings->data, offset );
+ if ( --index == 0 ) {
+ /* Copy string, truncating as necessary. */
+ if ( string_len >= length )
+ string_len = ( length - 1 );
+ copy_from_user ( buffer, strings->data,
+ offset, string_len );
+ return 0;
}
- string += ( len + 1 );
+ offset += ( string_len + 1 );
}
- return string;
+
+ DBG ( "SMBIOS string index %d not found\n", index );
+ return -ENOENT;
}
/**
- * Find SMBIOS serial number
+ * Get UUID from SMBIOS
*
- * @v data Buffer to fill
- * @v len Length of buffer
+ * @v uuid UUID to fill in
+ * @ret rc Return status code
*/
-int find_smbios_serial ( void *data, size_t len ) {
- struct smbios_entry entry;
- const char *string;
+int smbios_get_uuid ( union uuid *uuid ) {
+ struct smbios_system_information sysinfo;
int rc;
- if ( ( rc = find_smbios_entry ( &entry ) ) != 0 )
+ if ( ( rc = find_smbios_structure ( SMBIOS_TYPE_SYSTEM_INFORMATION,
+ &sysinfo, sizeof ( sysinfo ),
+ NULL ) ) != 0 )
return rc;
- char buffer[entry.max];
- if ( ( rc = find_smbios ( &entry, 1, buffer ) ) != 0 )
- return rc;
-
- struct smbios_system_information *sysinfo = ( void * ) buffer;
- string = find_smbios_string ( buffer, sysinfo->serial );
- if ( ! string )
- return -ENOENT;
+ memcpy ( uuid, sysinfo.uuid, sizeof ( *uuid ) );
+ DBG ( "SMBIOS found UUID %s\n", uuid_ntoa ( uuid ) );
- DBG ( "Found serial number \"%s\"\n", string );
- snprintf ( data, len, "%s", string );
return 0;
}
--- /dev/null
+#ifndef _I386_UUID_H
+#define _I386_UUID_H
+
+#include <smbios.h>
+
+static inline int get_uuid ( union uuid *uuid ) {
+ return smbios_get_uuid ( uuid );
+}
+
+#endif /* _I386_UUID_H */
* System Management BIOS
*/
-extern int find_smbios_serial ( void *data, size_t len );
+#include <stdint.h>
+
+/** An SMBIOS structure header */
+struct smbios_header {
+ /** Type */
+ uint8_t type;
+ /** Length */
+ uint8_t length;
+ /** Handle */
+ uint16_t handle;
+} __attribute__ (( packed ));
+
+/** SMBIOS system information structure */
+struct smbios_system_information {
+ /** SMBIOS structure header */
+ struct smbios_header header;
+ /** Manufacturer string */
+ uint8_t manufacturer;
+ /** Product string */
+ uint8_t product;
+ /** Version string */
+ uint8_t version;
+ /** Serial number string */
+ uint8_t serial;
+ /** UUID */
+ uint8_t uuid[16];
+ /** Wake-up type */
+ uint8_t wakeup;
+} __attribute__ (( packed ));
+
+/** SMBIOS system information structure type */
+#define SMBIOS_TYPE_SYSTEM_INFORMATION 1
+
+struct smbios_strings;
+extern int find_smbios_structure ( unsigned int type,
+ void *structure, size_t length,
+ struct smbios_strings *strings );
+extern int find_smbios_string ( struct smbios_strings *strings,
+ unsigned int index,
+ char *buffer, size_t length );
+extern int smbios_get_uuid ( union uuid *uuid );
#endif /* _SMBIOS_H */
.asciz "http://etherboot.org"
.size mfgstr, . - mfgstr
prodstr:
- .asciz "Etherboot"
+ .asciz "gPXE"
.size prodstr, . - prodstr
undiheader:
.size init_vector, . - init_vector
ispnp_message:
- .asciz "Etherboot detected PnP BIOS\r\n"
+ .asciz "gPXE detected PnP BIOS\r\n"
.size ispnp_message, . - ispnp_message
notpnp_message:
- .asciz "Etherboot detected non-PnP BIOS\r\n"
+ .asciz "gPXE detected non-PnP BIOS\r\n"
.size notpnp_message, . - notpnp_message
/* Boot execution vector
.previous
exec_message:
- .asciz "Etherboot starting boot\r\n"
+ .asciz "gPXE starting boot\r\n"
.size exec_message, . - exec_message
/* UNDI loader
--- /dev/null
+/*
+ * Copyright (C) 2007 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+
+#include <stdint.h>
+#include <stdio.h>
+#include <byteswap.h>
+#include <gpxe/uuid.h>
+
+/** @file
+ *
+ * Universally unique IDs
+ *
+ */
+
+/**
+ * Convert UUID to printable string
+ *
+ * @v uuid UUID
+ * @ret string UUID in canonical form
+ */
+char * uuid_ntoa ( union uuid *uuid ) {
+ static char buf[37]; /* "00000000-0000-0000-0000-000000000000" */
+
+ sprintf ( buf, "%08lx-%04x-%04x-%04x-%02x%02x%02x%02x%02x%02x",
+ le32_to_cpu ( uuid->canonical.a ),
+ le16_to_cpu ( uuid->canonical.b ),
+ le16_to_cpu ( uuid->canonical.c ),
+ be16_to_cpu ( uuid->canonical.d ),
+ uuid->canonical.e[0], uuid->canonical.e[1],
+ uuid->canonical.e[2], uuid->canonical.e[3],
+ uuid->canonical.e[4], uuid->canonical.e[5] );
+ return buf;
+}
*/
#define DHCP_BOOTFILE_NAME 67
+/** Client system architecture */
+#define DHCP_CLIENT_ARCHITECTURE 93
+
+/** Client network device interface */
+#define DHCP_CLIENT_NDI 94
+
+/** UUID client identifier */
+#define DHCP_CLIENT_UUID 97
+
/** Etherboot-specific encapsulated options
*
* This encapsulated options field is used to contain all options
ref_put ( &options->refcnt );
}
+extern struct list_head dhcp_option_blocks;
+
extern unsigned long dhcp_num_option ( struct dhcp_option *option );
extern void dhcp_ipv4_option ( struct dhcp_option *option,
struct in_addr *inp );
--- /dev/null
+#ifndef _GPXE_UUID_H
+#define _GPXE_UUID_H
+
+/** @file
+ *
+ * Universally unique IDs
+ */
+
+#include <stdint.h>
+
+union uuid;
+#include <bits/uuid.h>
+
+/** A universally unique ID */
+union uuid {
+ /** Canonical form (00000000-0000-0000-0000-000000000000) */
+ struct {
+ /** 8 hex digits, little-endian */
+ uint32_t a;
+ /** 2 hex digits, little-endian */
+ uint16_t b;
+ /** 2 hex digits, little-endian */
+ uint16_t c;
+ /** 2 hex digits, big-endian */
+ uint16_t d;
+ /** 12 hex digits, big-endian */
+ uint8_t e[6];
+ } canonical;
+ uint8_t raw[16];
+};
+
+extern char * uuid_ntoa ( union uuid *uuid );
+
+#endif /* _GPXE_UUID_H */
*/
/** List of registered DHCP option blocks */
-static LIST_HEAD ( option_blocks );
+LIST_HEAD ( dhcp_option_blocks );
/** Registered DHCP option applicators */
static struct dhcp_option_applicator dhcp_option_applicators[0]
if ( options ) {
return find_dhcp_option_with_encap ( options, tag, NULL );
} else {
- list_for_each_entry ( options, &option_blocks, list ) {
+ list_for_each_entry ( options, &dhcp_option_blocks, list ) {
if ( ( option = find_dhcp_option ( options, tag ) ) )
return option;
}
options, options->priority );
/* Insert after any existing blocks which have a higher priority */
- list_for_each_entry ( existing, &option_blocks, list ) {
+ list_for_each_entry ( existing, &dhcp_option_blocks, list ) {
if ( options->priority > existing->priority )
break;
}
#include <gpxe/retry.h>
#include <gpxe/tcpip.h>
#include <gpxe/ip.h>
+#include <gpxe/uuid.h>
#include <gpxe/dhcp.h>
/** @file
static uint8_t dhcp_request_options_data[] = {
DHCP_MAX_MESSAGE_SIZE, DHCP_WORD ( ETH_MAX_MTU ),
DHCP_VENDOR_CLASS_ID,
- DHCP_STRING ( 'E', 't', 'h', 'e', 'r', 'b', 'o', 'o', 't' ),
+ DHCP_STRING ( 'P', 'X', 'E', 'C', 'l', 'i', 'e', 'n', 't', ':',
+ 'A', 'r', 'c', 'h', ':', '0', '0', '0', '0', '0', ':',
+ 'U', 'N', 'D', 'I', ':', '0', '0', '2', '0', '0', '1' ),
+ DHCP_CLIENT_ARCHITECTURE, DHCP_WORD ( 0 ),
+ DHCP_CLIENT_NDI, DHCP_OPTION ( 1 /* UNDI */ , 2, 1 /* v2.1 */ ),
DHCP_PARAMETER_REQUEST_LIST,
- DHCP_OPTION ( DHCP_SUBNET_MASK, DHCP_ROUTERS, DHCP_DNS_SERVERS, DHCP_LOG_SERVERS,
- DHCP_HOST_NAME, DHCP_DOMAIN_NAME, DHCP_ROOT_PATH,
- DHCP_VENDOR_ENCAP, DHCP_TFTP_SERVER_NAME,
- DHCP_BOOTFILE_NAME, DHCP_EB_ENCAP,
- DHCP_ISCSI_INITIATOR_IQN ),
+ DHCP_OPTION ( DHCP_SUBNET_MASK, DHCP_ROUTERS, DHCP_DNS_SERVERS,
+ DHCP_LOG_SERVERS, DHCP_HOST_NAME, DHCP_DOMAIN_NAME,
+ DHCP_ROOT_PATH, DHCP_VENDOR_ENCAP, DHCP_VENDOR_CLASS_ID,
+ DHCP_TFTP_SERVER_NAME, DHCP_BOOTFILE_NAME,
+ DHCP_EB_ENCAP, DHCP_ISCSI_INITIATOR_IQN ),
DHCP_END
};
uint8_t ll_addr[MAX_LL_ADDR_LEN];
} __attribute__ (( packed ));
+/** DHCP client UUID */
+struct dhcp_client_uuid {
+ /** Identifier type */
+ uint8_t type;
+ /** UUID */
+ union uuid uuid;
+} __attribute__ (( packed ));
+
+#define DHCP_CLIENT_UUID_TYPE 0
+
/**
* Create DHCP request
*
struct device_description *desc = &netdev->dev->desc;
struct dhcp_netdev_desc dhcp_desc;
struct dhcp_client_id client_id;
+ struct dhcp_client_uuid client_uuid;
size_t dhcp_features_len;
size_t ll_addr_len;
int rc;
return rc;
}
+ /* Add client UUID, if we have one. Required for PXE. */
+ client_uuid.type = DHCP_CLIENT_UUID_TYPE;
+ if ( ( rc = get_uuid ( &client_uuid.uuid ) ) == 0 ) {
+ if ( ( rc = set_dhcp_packet_option ( dhcppkt,
+ DHCP_CLIENT_UUID, &client_uuid,
+ sizeof ( client_uuid ) ) ) != 0 ) {
+ DBG ( "DHCP could not set client UUID: %s\n",
+ strerror ( rc ) );
+ return rc;
+ }
+ }
+
return 0;
}
struct in_addr gateway = { INADDR_NONE };
int rc;
- /* Clear any existing routing table entry */
- del_ipv4_address ( netdev );
-
/* Retrieve IP address configuration */
find_dhcp_ipv4_option ( options, DHCP_EB_YIADDR, &address );
find_dhcp_ipv4_option ( options, DHCP_SUBNET_MASK, &netmask );
find_dhcp_ipv4_option ( options, DHCP_ROUTERS, &gateway );
+ /* Do nothing unless we have at least an IP address to use */
+ if ( ! address.s_addr )
+ return 0;
+
+ /* Clear any existing routing table entry */
+ del_ipv4_address ( netdev );
+
/* Set up new IP address configuration */
if ( ( rc = add_ipv4_address ( netdev, address, netmask,
gateway ) ) != 0 ) {
*
*/
-static struct dhcp_option_block *dhcp_options = NULL;
-
static int dhcp_success ( struct net_device *netdev,
struct dhcp_option_block *options ) {
- dhcp_options = dhcpopt_get ( options );
+ DBGC ( options, "DHCP client registering options %p\n", options );
register_dhcp_options ( options );
return dhcp_configure_netdev ( netdev, options );
}
int dhcp ( struct net_device *netdev ) {
+ struct dhcp_option_block *options;
+ struct dhcp_option_block *tmp;
int rc;
/* Check we can open the interface first */
if ( ( rc = ifopen ( netdev ) ) != 0 )
return rc;
- /* Unregister any previously acquired options */
- if ( dhcp_options ) {
- unregister_dhcp_options ( dhcp_options );
- dhcpopt_put ( dhcp_options );
- dhcp_options = NULL;
+ /* Unregister any option blocks acquired via DHCP */
+ list_for_each_entry_safe ( options, tmp, &dhcp_option_blocks, list ) {
+ /* Skip static option blocks (e.g. from NVS) */
+ if ( find_dhcp_option ( options, DHCP_MESSAGE_TYPE ) ) {
+ DBGC ( options, "DHCP client unregistering options "
+ "%p\n", options );
+ unregister_dhcp_options ( options );
+ }
}
/* Perform DHCP */
print "DRIVER_$image = $driver_name\n";
print "ROM_TYPE_$image = $type\n";
print "ROM_DESCRIPTION_$image = \"$desc\"\n";
- print "PCI_VENDOR_$image = $vendor\n" if $vendor;
- print "PCI_DEVICE_$image = $device\n" if $device;
+ print "PCI_VENDOR_$image = 0x$vendor\n" if $vendor;
+ print "PCI_DEVICE_$image = 0x$device\n" if $device;
print "ROMS += $image\n";
print "ROMS_$driver_name += $image\n";
}
next unless /(PCI|ISA)_ROM\s*\(/;
if ( /^\s*PCI_ROM\s*\(
- \s*(0x[0-9A-Fa-f]{4})\s*, # PCI vendor
- \s*(0x[0-9A-Fa-f]{4})\s*, # PCI device
+ \s*0x([0-9A-Fa-f]{4})\s*, # PCI vendor
+ \s*0x([0-9A-Fa-f]{4})\s*, # PCI device
\s*\"([^\"]*)\"\s*, # Image
\s*\"([^\"]*)\"\s* # Description
\)/x ) {
( my $vendor, my $device, my $image, my $desc ) = ( lc $1, lc $2, $3, $4 );
rom ( "pci", $image, $desc, $vendor, $device );
+ rom ( "pci", lc "pci_${vendor}_${device}", $desc, $vendor, $device );
} elsif ( /^\s*ISA_ROM\s*\(
\s*\"([^\"]*)\"\s*, # Image
\s*\"([^\"]*)\"\s* # Description