#!/usr/bin/perl
#
# Simple tool for generating a USB desciptor table or ROM/RAM
#

use integer;
use strict;
use File::Spec;
use Encode;

require 'langid.ph';

# Some forward declarations
my($mode, $infile, $outfile) = @ARGV;
my $module_name;
my $err = 0;

# Descriptor types. This follows the document in which they were specified;
# some USB documents specify in hex and others in decimal...
our %DT = (
    'device' => 1,
    'configuration' => 2, 'config' => 2, 'conf' => 2,
    'string' => 3, 'str' => '3',
    'interface' => 4, 'if' => '4',
    'endpoint' => 5, 'ep' => 5,
    'device_qualifier' => 6, 'dq' => 6,
    'other_speed_configuration' => 7, 'otherspeed' => 7, 'osc' => 7,
    'interface_power' => 8, 'ifpwr' => 8,
    'otg' => 9,
    'debug' => 10,
    'interface_association' => 11, 'iad' => 11,

    'bos' => 15, 'binary_object_storage' => 15,
    'device_capability' => 16, 'devcap' => 16,

    'cs_interface' => 0x24, 'cs_if' => 0x24,
    'cs_endpoint' => 0x25, 'cs_ep' => 0x25,

    'superspeed_usb_endpoint_companion' => 48, 'usb_ep_companion' => 48,
    'superspeedplus_isochronous_endpoint_companion' => 49, 'iso_ep_compation' => 49,
    );

# Descriptor subtypes, where applicable
our %DS = (
    # Under CS_INTERFACE
    0x24 => {
	'header' => 0x00,
	    'call' => 0x01, 'call_management' => 0x01,
	    'abstract_control' => 0x02, 'acm' => 0x02,
	    'direct_line' => 0x03,
	    'ringer' => 0x04,
	    'line_state' => 0x05,
	    'union' => 0x06,
	    'country' => 0x07,
	    'op_mode' => 0x08,
	    'terminal' => 0x09,
	    'net_channel' => 0x0a,
	    'protocol_unit' => 0x0b,
	    'extension_unit' => 0x0c,
	    'multi_channel' => 0x0d,
	    'capi' => 0x0e,
	    'ethernet' => 0x0f,
	    'atm' => 0x10,
	    'whcm' => 0x11, 'wireless_handset' => 0x11,
	    'mobile_line' => 0x12, 'mobile_direct_line' => 0x12,
	    'mdlm_detail' => 0x13, 'mdlm' => 0x13,
	    'device_management' => 0x14, 'device' => 0x14, 'mgmt' => 0x14,
	    'command_set' => 0x16,
	    'command_set_detail' => 0x17,
	    'telephone_control' => 0x18, 'tcm' => 0x18, 'phone' => 0x18,
	    'obex_service_identifier' => 0x19, 'obex' => 0x19
    }
    );

#
# Class, subclass, and protocol codes. Map a string to a number, then
# use that number as an index to descend, if that entry exists.  0 is
# the default; for some classes the subclass code is unused and so is
# set to 0; no string to look up.
#
# Numbers below the class level are massively incomplete, feel free to add.
#

# Applies to all levels
my %class_all_levels = (
    undef => 0x00, 'undef' => 0x00, '-' => 0x00,
    'none' => 0x00, 'default' => 0x00,
    'vendor_specific' => 0xff, 'vendor' => 0xff,
    );

my $cdc_pstn_protocols = {
    'v250' => 0x01, 'at' => 0x01, 'v25ter' => 0x01,
	'pcca101' => 0x02,
	'pcca101o' => 0x03, 'pcca' => 0x03,
	'gsm707' => 0x04, 'gsm' => 0x04,
	'3gpp2707' => 0x05, '3gpp' => 0x05, '3g' => 0x05,
	'ca00170' => 0x06, 'tia_cdma' => 0x06, 'cdma' => 0x06
};

my %class_codes = (
    'multi' => 0x00,		# Real class code in interface descriptors
    'audio' => 0x01,
    'cdc' => 0x02, 'communications' => 0x02,
    0x02 => {
	'dlcm' => 0x01, 'direct_line_control' => 0x01,
	    'acm' => 0x02, 'abstract_control' => 0x02,
	    0x02 => $cdc_pstn_protocols,
	    'tcm' => 0x03, 'telephone_control' => 0x03,
	    0x03 => $cdc_pstn_protocols,
	    'mccm' => 0x04, 'multi_channel_control' => 0x04,
	    0x04 => $cdc_pstn_protocols,
	    'ccm' => 0x05, 'capi' => 0x05, 'capi_control' => 0x05,
	    0x05 => $cdc_pstn_protocols,
	    'ecm' => 0x06, 'ethernet_control' => 0x06,
	    0x06 => $cdc_pstn_protocols,
	    'atm' => 0x07, 'ancm' => 0x07, 'atm_networking_control' => 0x07,
	    'whcm' => 0x08, 'wireless_handset_control' => 0x08,
	    0x08 => $cdc_pstn_protocols,
	    'device_management' => 0x09, 'mgmt' => 0x09, 'device' => 0x09,
	    'mdlm' => 0x0a, 'mobile_direct_line' => 0x0a,
	    'obex' => 0x0b,
	    'eem' => 0x0c, 'ethernet' => 0x0c, 'ethernet_emulation' => 0x0c,
	    0x0c => {
		'ethernet' => 0x07, 'eem' => 0x07
	},
	    'ncm' => 0x0d, 'net' => 0x0d, 'network_control' => 0x0d,
    },

    'hid' => 0x03,

    'physical' => 0x05,
    'imaging' => 0x06, 'photo' => 0x06, 'idc' => 0x06,
    'printer' => 0x07,
    'mass_storage' => 0x08, 'storage' => 0x08, 'disk' => 0x08,
    'hub' => 0x09,
    'cdc_data' => 0x0a, 'data' => 0x0a,
    0x0a => {
	0 => {
	    'ntb' => 0x01, 'network_transfer_block' => 0x01,

		'isdn_bri' => 0x30, 'isdn' => 0x30,
		'hdlc' => 0x31,
		'transparent' => 0x32,

		'q921_management' => 0x50, 'q921m' => 0x50,
		'q921_datalink' => 0x51, 'q921' => 0x51,
		'q921_tei_mux' => 0x52, 'q921tm' => 0x52,

		'v42bis' => 0x90,
		'euro_isdn' => 0x91, 'q931' => 0x91,
		'v120' => 0x92, 'isdn_v24' => 0x92,
		'capi' => 0x93,

		'host' => 0xfd,
		'external' => 0xfe,
		'vendor' => 0xff
	},
    },
    'smart_card' => 0x0b, 'smartcard' => 0x0b, 'scdc' => 0x0b,

    'content_security' => 0x0d, 'drm' => 0x0d, 'csdc' => 0x0d,
    'video' => 0x0e, 'vdc' => 0x0e,
    'personal_healthcare' => 0x0f, 'healthcare' => 0x0f, 'health' => 0x0f, 'phdc' => 0x0f,
    'audio_video' => 0x10, 'av' => 0x10, 'avdc' => 0x10,
    'billboard' => 0x11, 'bdc' => 0x11,
    'usb_c_bridge' => 0x12, 'usbc' => 0x12, 'usbcbdc' => 0x12,

    'diagnostic' => 0xdc,
    'wireless_controller' => 0xe0, 'wireless' => 0xe0,

    'miscellaneous' => 0xef, 'misc' => 0xef,

    'application_specific' => 0xfe, 'app_specific' => 0xfe, 'app' => 0xfe,
    );

my %packfmt = ( 1 => 'C', 2 => 'v', 4 => 'V', 8 => 'Q<' );

my $utf16le = find_encoding('utf16le');

sub atom($@) {
    my($bytes,$b,$adj) = @_;
    my @o = ();

    $adj = toint($adj);
    
    my $t;
    while (($t = ref $b) eq 'REF') {
	$b = $$b;
    }
    if ($t eq 'SCALAR') {
	# To be resolved later
	push(@o, {'bytes' => $bytes, 'num' => $b, 'adj' => $adj });
    } elsif ($t eq '') {
	push(@o, pack($packfmt{$bytes}, $b+$adj));
    } else {
	push(@o, {'bytes' => $bytes, 'data' => $b});
    }
    return [@o];
}

sub byte(@) {
    return atom(1,@_);
}
sub word(@) {
    return atom(2,@_);
}
sub dword(@) {
    return atom(4,@_);
}
sub qword(@) {
    return atom(8,@_);
}

# Generate endpoint identifiers
sub ep_i($) {
    my($n) = @_;
    return byte($n,0x80);
}
sub ep_o($) {
    my($n) = @_;
    return byte($n,0x00);
}


#
# General handy stuff...
#

# Decode an integer in C notation
sub toint($) {
    my($i) = @_;
    return ($i =~ /^0/) ? oct $i : ($i =~ /^[1-9]/) ? $i+0 : undef;
}

# For sorting
sub numeric {
    return $a <=> $b;
}

# Generate class code triplets
sub usb_class($;$$) {
    my @cl = @_;
    my $lvl = \%class_codes;
    my $cd = '';

    for (my $i = 0; $i < 3; $i++) {
	my $cs = shift(@cl);
	my $cc = defined($cs) ? toint($cs) : 0;
	if (!defined($cc)) {
	    $cs = lc($cs);
	    $cs =~ s/\P{Alnum}+/_/g;

	    $cc = $lvl->{$cs} if (defined($lvl));
	    $cc = $class_all_levels{$cs} unless (defined($cc));
	    if (!defined($cc)) {
		print STDERR "$0: unknown class code ", join('.', @_), "\n";
		$err = 1;
		$cc = 0;
	    }
	}

	$cd .= pack('C', $cc);
	$lvl = $lvl->{$cc};
    }
    return $cd;
}

sub datalen(@) {
    my $l = 0;

    foreach my $e (@_) {
	my $b = $e;
	my $t;
	while (($t = ref $b) eq 'REF') {
	    $b = $$b;
	}
	if ($t eq 'HASH') {
	    $l += $b->{'bytes'};
	} elsif ($t eq 'ARRAY') {
	    $l += datalen(@$b);
	} elsif ($t eq 'SCALAR') {
	    $l += length($$b);
	} elsif ($t eq '') {
	    $l += length($b);
	} else {
	    die;
	}
    }

    return $l;
}

sub makedata(@) {
    my $o = '';

    foreach my $e (@_) {
	my $b = $e;
	my $t;
	while (($t = ref $b) eq 'REF') {
	    $b = $$b;
	}
	if ($t eq 'HASH') {
	    unless (defined($b->{'raw'})) {
		if (defined(my $n = $b->{'num'})) {
		    my $tt;
		    my $adj = $b->{'adj'};
		    while (($tt = ref $n) eq 'REF') {
			$n = $$n;
		    }
		    if ($tt eq 'SCALAR') {
			$b->{'raw'} = pack($packfmt{$b->{'bytes'}}, $$n+$adj);
		    } else {
			$b->{'raw'} = makedata($n);
		    }
		} elsif (defined($b->{'data'})) {
		    $b->{'raw'}  = makedata($b->{'data'});
		} else {
		    die;
		}
	    }
	    $b->{'offs'}  = length($o);
	    $b->{'bytes'} = length($b->{'raw'});
	    $o .= $b->{'raw'};
	} elsif ($t eq 'ARRAY') {
	    $o .= makedata(@$b);
	} elsif ($t eq 'SCALAR') {
	    $o .= makedata($$b);
	} elsif ($t eq '') {
	    $o .= $b;
	} else {
	    die;
	}
    }

    return $o;
}

# USB descriptor set
my $u_self   = { 'children' => \(my $children = 0), clist => [] };
sub usb_dset(&) {
    my($contents) = @_;
    my $parent = $u_self;
    my $children = 0;
    my $index = ${$u_self->{'children'}}++;

    my $ds = { 'type' => 'dset',
		   'parent' => $parent,
		   'data' => undef,
		   'bytes' => undef,
		   'raw' => undef,
		   'children' => \$children,
		   'index' => \$index,
		   'offs' => undef,
		   'clist' => [] };
    $u_self = $ds;
    push(@{$parent->{'clist'}}, $ds);
    my @data = $contents->($ds, $parent);
    $ds->{'data'} = \@data;
    $ds->{'bytes'} = datalen(@data);
    $u_self = $parent;
    return $ds;
}
sub usb_totallen(;$) {
    my($r) = @_;
    $r = $u_self unless(defined($r));

    return \$r->{'bytes'};
}
sub usb_index(;$) {
    my($r) = @_;
    $r = $u_self unless(defined($r));

    return $r->{'index'};
}
sub usb_peers(;$) {
    my($r) = @_;
    $r = $u_self unless(defined($r));

    return $r->{'parent'}{'children'};
}
sub usb_children(;$) {
    my($r) = @_;
    $r = $u_self unless(defined($r));

    return $r->{'children'};
}

# USB descriptor
sub usb_desc($@) {
    my($typestr, @data) = @_;

    my($type,$subtype) = split(/\./, $typestr, 2);

    my $tn;
    my $sn;
    my $hdr;
    my $dlen = datalen(@data);

    $tn = toint($type);
    $tn = $DT{lc($type)} unless (defined($tn));
    die "$0: unknown descriptor type: $typestr\n" unless (defined($tn));

    if (defined($subtype)) {
	$sn = toint($subtype);
	$sn = $DS{$tn}->{$subtype} unless (defined($sn));
	die "$0: unknown descriptor type: $typestr\n" unless (defined($sn));

	$dlen += 3;
	$hdr = pack('CCC', $dlen, $tn, $sn);
    } else {
	$dlen += 2;
	$hdr = pack('CC', $dlen, $tn);
    }

    return { 'type' => 'descriptor',
		 'data' => [$hdr, @data],
		 'bytes' => $dlen };
}

# Device top level
my $device_dset;
sub usb_device(&) {
    my($contents) = @_;
    $device_dset = usb_dset(\&$contents);
}

# Additional USB data
my $additional_dset;
sub usb_additional_data(&) {
    my($contents) = @_;
    $additional_dset = usb_dset(\&$contents);
}

my @langlist;
my %lang;
my @lang_mask = (0xffff, 0x03ff, 0); # Masks for language codes
my $stringdata;
my %stringoffs;			# Pointer into stringdata

# Reserved string descriptor numbers
my $strdesc_lang   = 0;		# 0 = reserved for language descriptors
my $strdesc_serial = 1;		# 1 = reserved for serial number (see below)
my $strdesc_msft   = 0xee;

my %special_strings = ($strdesc_lang => undef,
		       $strdesc_serial => undef,
		       $strdesc_msft   => undef);

# The index of string descriptors are sets of descriptors which
# match for ALL languages so they can be given the same index.
my %strdesci = ();
my @strdescs = ();

# Register a string into the string table and return a descriptor index byte.
# Input should be a hash.
sub usb_string(%) {
    my(%strh) = @_;

    my $descval = '';
    my %txts;
    my $found = 0;

    if (!%strh) {
	%strh = ( '' => '' );	# Null string descriptor
    }
    
    foreach my $l (keys(%strh)) {
	my $str = $strh{$l};
	my $co = langid($l);
	next unless (defined($co));

	foreach my $m (@lang_mask) {
	    my $coi = $co & $m;
	    next unless (defined($lang{$coi}));
	    next if (defined($txts{$coi}));
	    $txts{$coi} = $str;
	    $found++;
	}
    }

    return pack('C', 0) unless ($found);

    foreach my $co (@langlist) {
	my $txt;
	foreach my $m (@lang_mask) {
	    last if (defined($txt = $txts{$co & $m}));
	}

	my $utf16str = $utf16le->encode($txt, Encode::FB_WARN);
	unless (defined($stringoffs{$utf16str})) {
	    $stringoffs{$utf16str} = length($stringdata);
	    $stringdata .= pack('CC', length($utf16str)+2, $DT{string});
	    $stringdata .= $utf16str;
	}

	$descval .= pack('v', $stringoffs{$utf16str});
    }

    my $descindex = $strdesci{$descval};
    unless (defined($descindex)) {
	$descindex = scalar(@strdescs) - 1;
	while (exists($special_strings{++$descindex})) {
	    push(@strdescs, undef); # Skip reserved index
	}
	$strdesci{$descval} = $descindex;
	push(@strdescs, $descval);
    }

    return pack('C', $descindex);
}

#
# Special string descriptors. Currently supports the serial number,
# but could also be used for the M$ special 0xEE string descriptor
# with a bit more infrastructure. Noteworthy is that the serial number
# descriptor is quite likely to need to be patched in the field or
# at runtime. Thus, these special string descriptors come at the
# very beginning of the data, in index order.
#
# Language codes are not supported for special strings.
#
# Usage: usb_special_string(index,'default',length)
#
sub usb_special_string($@) {
    my($ix,$dft,$len) = @_;

    if (defined($dft) || $len || !defined($special_strings{$ix})) {
	$dft = $utf16le->encode($dft, Encode::FB_WARN);
	my $dchar = length($dft) >> 1;
	if (!$len) {
	    $len = $dchar;
	} elsif ($len < $dchar) {
	    $dft = substr($dft, 0, $len << 1);
	} elsif ($len > $dchar) {
	    # Pad with spaces
	    $dft .= $utf16le->encode(' ' x ($len - $dchar));
	}
	$special_strings{$ix} = pack('CC', ($len << 1)+2, $DT{string}).$dft;
    }

    return pack('C', $ix);
}

sub usb_serial($;$) {
    return usb_special_string($strdesc_serial, @_);
}

#
# Register the possible languages
#
sub usb_languages(@) {
    my @langs = @_;

    # There are two differences between keys(%lang) and @langlist: the
    # former lists all possible language codes for which a lookup is
    # possible in no specific order, whereas @langlist is exactly the
    # items wanted in order in string descriptor zero.
    #
    # In other words, %lang is a reverse mapping of langlist, *plus*
    # permissible aliases.
    %lang = ();
    @langlist = ();
    foreach my $l (@langs) {
	my $co = langid($l);
	if (defined($co) && !defined($lang{$co})) {
	    my $nlang = scalar(@langlist);
	    push(@langlist, $co);

	    # Defaults for partial languages?
	    foreach my $m (@lang_mask) {
		if (!defined($lang{$co & $m})) {
		    $lang{$co & $m} = $nlang;
		}
	    }
	}
    }

    if (!scalar(@langlist)) {
	$stringdata = '';
    } else {
	$stringdata = pack("CCv*", scalar(@langlist)*2 + 2,
			   $DT{string}, @langlist);
    }
}

my $descriptor_data;
my @descriptor_ptrs;
my $additional_offs;
my $additional_len;

sub generate_data()
{
    my $data = '';
    my @ptrs = ();

    # Special string descriptors first
    foreach my $ssn (sort numeric keys(%special_strings)) {
	my $ss = $special_strings{$ssn};

	next unless defined($ss);

	push(@ptrs, {'type' => $DT{string}, 'dindex' => $ssn,
			 'offs' => length($data),
			 'len' => length($ss)});
	$data .= $ss;
    }

    # Device and configuration descriptors
    my $dev_offs = length($data);
    $data .= makedata($device_dset);

    foreach my $dc (@{$device_dset->{data}}) {
	my $offs = $dev_offs + $dc->{offs};
	# Length and type of the *first* descriptor in a set
	my($flen,$ftype) = unpack('CC', substr($data, $offs, 2));
	my $dindex = $dc->{index};
	$dindex = $$dindex while (ref $dindex eq 'SCALAR');
	push(@ptrs, {'type' => $ftype, 'dindex' => $dindex,
			 'offs' => $offs, 'len' => $dc->{bytes}});
    }

    my $string_offs = length($data);

    # 'Regular' string descriptors
    push(@ptrs, {'type' => $DT{string}, 'dindex' => 0,
		     'offs' => $string_offs,
		     'len' => unpack('C', substr($stringdata, 0, 1))});

    for (my $i = 1; $i < scalar(@strdescs); $i++) {
	my @sofs = unpack('v*', $strdescs[$i]);
	for (my $j = 0; $j < scalar(@sofs); $j++) {
	    my $co = $langlist[$j];
	    my $of = $sofs[$j];
	    my $m  = $lang_mask[0];

	    # Widen the mask as much as possible
	    foreach my $mx (@lang_mask) {
		last unless ($lang{$co & $mx} == $lang{$co & $m});
		$m = $mx;
	    }

	    push(@ptrs, {'type' => $DT{string}, 'dindex' => $i,
			     'windex' => $co & $m,
			     'wimask' => $m,
			     'offs' => $string_offs + $of,
			     'len' => unpack('C', substr($stringdata,$of,1))});
	}
    }
    $data .= $stringdata;

    $additional_offs = length($data);
    if (defined($additional_dset)) {
	$data .= makedata($additional_dset);
    }
    $additional_len  = length($data) - $additional_offs;
    
    $descriptor_data = $data;
    @descriptor_ptrs = @ptrs;

    return $descriptor_data;
}

# Output a number and a mask as a Verilog case with ? as needed
sub v_case($$$) {
    my($b,$v,$m) = @_;

    my $o = sprintf("%d'b", $b);

    for (my $i = $b-1; $i >= 0; $i--) {
	my $ix = 1 << $i;
	$o .= ($m & $ix) ? (($v & $ix) ? '1' : '0') : '?';
	$o .= '_' if ($i > 0 && ($i & 7) == 0);
    }

    return $o;
}

# Output a Verilog module
sub output_verilog($) {
    my($out) = @_;

    my $bytes = length($descriptor_data);

    my $abits = 1;
    while ((1 << $abits) < $bytes) {
	$abits++;
    }

    my $amax = $abits-1;
    my $bmax = (1 << $abits)-1;

    my $afmt = sprintf("%d'h%%0%dx", $abits, ($abits+3) >> 2);
    my $abad = sprintf("%d'h", $abits) . ('x' x (($abits+3) >> 2));
    my $bfmt = "8'h%02x";
    my $bbad = "8'hxx";

    print $out <<"EOF";
/*
 * Call it a ROM even through it can be optionally written to.
 * Trust the tools to figure out if we don't need part of the whole thing.
 */
module ${module_name}_rom (
	input clk,

	input [$amax:0] usb_addr,
	output [7:0] usb_rdata,

	input cpu_clk,
	input [$amax:0] cpu_addr,
	output [7:0] cpu_rdata,
	input [7:0] cpu_wdata,
	input cpu_wren
);

	reg [7:0] rom [0:$bmax];

	initial begin
EOF

    my $addr = 0;
    foreach my $b (unpack("C*", $descriptor_data)) {
	printf $out "\t\trom[$afmt] = $bfmt;\n", $addr++, $b;
    }
    while ($addr < $bytes) {
	printf $out "\t\trom[$afmt] = $bbad;\n", $addr++;
    }

    print $out <<"EOF";
	end

	always \@(posedge clk) begin
		usb_rdata <= rom[usb_addr];
	end

	always \@(posedge cpu_clk) begin
		cpu_rdata <= rom[cpu_addr];
		if (cpu_wren)
			rom[cpu_addr] <= cpu_wdata;
	end
endmodule

module ${module_name}_index (
	input [7:0]  dtype,
	input [7:0]  dindex,
	input [15:0] windex,
	input 	     additional,

	output reg [$amax:0] addr,
	output reg [$amax:0] len
);

	always \@(\*)
       	if (additional)
EOF
    printf $out "\t\t{addr,len} = {$afmt,$afmt};\n", $additional_offs, $additional_len;
    print $out <<"EOF";
	else priority casez ({windex,dindex,dtype})
EOF

    my @cases;

    foreach my $d ({ 'len' => 0 }, @descriptor_ptrs) {
	my $id   = 0;
	my $mask = 0;

	if (defined($d->{type})) {
	    $id   |= $d->{type};
	    $mask |= 0xff;
	}

	if (defined($d->{dindex})) {
	    $id |= $d->{dindex} << 8;
	    $mask |= 0xff << 8;
	}

	if (defined($d->{windex})) {
	    $id |= $d->{windex} << 16;
	    $mask |= $d->{wimask} << 16;
	}

	my $cs = "\t\t".v_case(32,$id,$mask).': {addr,len} = ';
	if ($d->{len}) {
	    $cs .= sprintf("{$afmt,$afmt};\n", $d->{offs},$d->{len});
	} else {
	    $cs .= sprintf("{$abad,$afmt};\n", $d->{len});
	}
	push(@cases, $cs);
    }

    # This relies on ? sorting after digits
    print $out sort @cases;


    print $out <<'EOF';
	endcase
endmodule
EOF
}

if (scalar(@ARGV) < 3) {
    die "Usage: $0 {bin|v} input output\n";
}

my @ofs = File::Spec->splitpath($outfile);
$module_name = $ofs[2];
$module_name =~ s/\..*$//;
$module_name =~ s/\P{Alnum}+/_/g;

unless (defined(do File::Spec->rel2abs($infile))) {
    die "$0: $infile: ".(($@ ne '') ? $@ : $!)."\n";
}

generate_data();

exit ($err) if ($err);

open(my $out, '>', $outfile)
    or die "$0: $outfile: $!\n";

if ($mode eq 'bin') {
    print $out $descriptor_data;
} elsif ($mode eq 'v') {
    output_verilog($out);
} else {
    die "$0: $mode: unknown operations mode\n";
}
close($out);

if ($err) {
    remove($outfile);
}
exit($err);