chromium/third_party/lcov/bin/geninfo

#!/usr/bin/perl -w
#
#   Copyright (c) International Business Machines  Corp., 2002,2012
#
#   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 (at
#   your option) 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
#
# geninfo
#
#   This script generates .info files from data files as created by code
#   instrumented with gcc's built-in profiling mechanism. Call it with
#   --help and refer to the geninfo man page to get information on usage
#   and available options.
#
#
# Authors:
#   2002-08-23 created by Peter Oberparleiter <[email protected]>
#                         IBM Lab Boeblingen
#        based on code by Manoj Iyer <[email protected]> and
#                         Megan Bock <[email protected]>
#                         IBM Austin
#   2002-09-05 / Peter Oberparleiter: implemented option that allows file list
#   2003-04-16 / Peter Oberparleiter: modified read_gcov so that it can also
#                parse the new gcov format which is to be introduced in gcc 3.3
#   2003-04-30 / Peter Oberparleiter: made info write to STDERR, not STDOUT
#   2003-07-03 / Peter Oberparleiter: added line checksum support, added
#                --no-checksum
#   2003-09-18 / Nigel Hinds: capture branch coverage data from GCOV
#   2003-12-11 / Laurent Deniel: added --follow option
#                workaround gcov (<= 3.2.x) bug with empty .da files
#   2004-01-03 / Laurent Deniel: Ignore empty .bb files
#   2004-02-16 / Andreas Krebbel: Added support for .gcno/.gcda files and
#                gcov versioning
#   2004-08-09 / Peter Oberparleiter: added configuration file support
#   2008-07-14 / Tom Zoerner: added --function-coverage command line option
#   2008-08-13 / Peter Oberparleiter: modified function coverage
#                implementation (now enabled per default)
#

use strict;
use File::Basename; 
use File::Spec::Functions qw /abs2rel catdir file_name_is_absolute splitdir
			      splitpath catpath/;
use Getopt::Long;
use Digest::MD5 qw(md5_base64);
if( $^O eq "msys" )
{
	require File::Spec::Win32;
}

# Constants
our $lcov_version	= 'LCOV version 1.10';
our $lcov_url		= "http://ltp.sourceforge.net/coverage/lcov.php";
our $gcov_tool		= "gcov";
our $tool_name		= basename($0);

our $GCOV_VERSION_4_7_0	= 0x40700;
our $GCOV_VERSION_3_4_0	= 0x30400;
our $GCOV_VERSION_3_3_0	= 0x30300;
our $GCNO_FUNCTION_TAG	= 0x01000000;
our $GCNO_LINES_TAG	= 0x01450000;
our $GCNO_FILE_MAGIC	= 0x67636e6f;
our $BBG_FILE_MAGIC	= 0x67626267;

# Error classes which users may specify to ignore during processing
our $ERROR_GCOV		= 0;
our $ERROR_SOURCE	= 1;
our $ERROR_GRAPH	= 2;
our %ERROR_ID = (
	"gcov" => $ERROR_GCOV,
	"source" => $ERROR_SOURCE,
	"graph" => $ERROR_GRAPH,
);

our $EXCL_START = "LCOV_EXCL_START";
our $EXCL_STOP = "LCOV_EXCL_STOP";
our $EXCL_LINE = "LCOV_EXCL_LINE";

# Compatibility mode values
our $COMPAT_VALUE_OFF	= 0;
our $COMPAT_VALUE_ON	= 1;
our $COMPAT_VALUE_AUTO	= 2;

# Compatibility mode value names
our %COMPAT_NAME_TO_VALUE = (
	"off"	=> $COMPAT_VALUE_OFF,
	"on"	=> $COMPAT_VALUE_ON,
	"auto"	=> $COMPAT_VALUE_AUTO,
);

# Compatiblity modes
our $COMPAT_MODE_LIBTOOL	= 1 << 0;
our $COMPAT_MODE_HAMMER		= 1 << 1;
our $COMPAT_MODE_SPLIT_CRC	= 1 << 2;

# Compatibility mode names
our %COMPAT_NAME_TO_MODE = (
	"libtool"	=> $COMPAT_MODE_LIBTOOL,
	"hammer"	=> $COMPAT_MODE_HAMMER,
	"split_crc"	=> $COMPAT_MODE_SPLIT_CRC,
	"android_4_4_0"	=> $COMPAT_MODE_SPLIT_CRC,
);

# Map modes to names
our %COMPAT_MODE_TO_NAME = (
	$COMPAT_MODE_LIBTOOL	=> "libtool",
	$COMPAT_MODE_HAMMER	=> "hammer",
	$COMPAT_MODE_SPLIT_CRC	=> "split_crc",
);

# Compatibility mode default values
our %COMPAT_MODE_DEFAULTS = (
	$COMPAT_MODE_LIBTOOL	=> $COMPAT_VALUE_ON,
	$COMPAT_MODE_HAMMER	=> $COMPAT_VALUE_AUTO,
	$COMPAT_MODE_SPLIT_CRC	=> $COMPAT_VALUE_AUTO,
);

# Compatibility mode auto-detection routines
sub compat_hammer_autodetect();
our %COMPAT_MODE_AUTO = (
	$COMPAT_MODE_HAMMER	=> \&compat_hammer_autodetect,
	$COMPAT_MODE_SPLIT_CRC	=> 1,	# will be done later
);

our $BR_LINE		= 0;
our $BR_BLOCK		= 1;
our $BR_BRANCH		= 2;
our $BR_TAKEN		= 3;
our $BR_VEC_ENTRIES	= 4;
our $BR_VEC_WIDTH	= 32;

our $UNNAMED_BLOCK	= 9999;

# Prototypes
sub print_usage(*);
sub gen_info($);
sub process_dafile($$);
sub match_filename($@);
sub solve_ambiguous_match($$$);
sub split_filename($);
sub solve_relative_path($$);
sub read_gcov_header($);
sub read_gcov_file($);
sub info(@);
sub get_gcov_version();
sub system_no_output($@);
sub read_config($);
sub apply_config($);
sub get_exclusion_data($);
sub apply_exclusion_data($$);
sub process_graphfile($$);
sub filter_fn_name($);
sub warn_handler($);
sub die_handler($);
sub graph_error($$);
sub graph_expect($);
sub graph_read(*$;$$);
sub graph_skip(*$;$);
sub sort_uniq(@);
sub sort_uniq_lex(@);
sub graph_cleanup($);
sub graph_find_base($);
sub graph_from_bb($$$);
sub graph_add_order($$$);
sub read_bb_word(*;$);
sub read_bb_value(*;$);
sub read_bb_string(*$);
sub read_bb($);
sub read_bbg_word(*;$);
sub read_bbg_value(*;$);
sub read_bbg_string(*);
sub read_bbg_lines_record(*$$$$$);
sub read_bbg($);
sub read_gcno_word(*;$$);
sub read_gcno_value(*$;$$);
sub read_gcno_string(*$);
sub read_gcno_lines_record(*$$$$$$);
sub determine_gcno_split_crc($$$);
sub read_gcno_function_record(*$$$$);
sub read_gcno($);
sub get_gcov_capabilities();
sub get_overall_line($$$$);
sub print_overall_rate($$$$$$$$$);
sub br_gvec_len($);
sub br_gvec_get($$);
sub debug($);
sub int_handler();
sub parse_ignore_errors(@);
sub is_external($);
sub compat_name($);
sub parse_compat_modes($);
sub is_compat($);
sub is_compat_auto($);


# Global variables
our $gcov_version;
our $gcov_version_string;
our $graph_file_extension;
our $data_file_extension;
our @data_directory;
our $test_name = "";
our $quiet;
our $help;
our $output_filename;
our $base_directory;
our $version;
our $follow;
our $checksum;
our $no_checksum;
our $opt_compat_libtool;
our $opt_no_compat_libtool;
our $rc_adjust_src_path;# Regexp specifying parts to remove from source path
our $adjust_src_pattern;
our $adjust_src_replace;
our $adjust_testname;
our $config;		# Configuration file contents
our @ignore_errors;	# List of errors to ignore (parameter)
our @ignore;		# List of errors to ignore (array)
our $initial;
our $no_recursion = 0;
our $maxdepth;
our $no_markers = 0;
our $opt_derive_func_data = 0;
our $opt_external = 1;
our $opt_no_external;
our $debug = 0;
our $gcov_caps;
our @gcov_options;
our @internal_dirs;
our $opt_config_file;
our $opt_gcov_all_blocks = 1;
our $opt_compat;
our %opt_rc;
our %compat_value;
our $gcno_split_crc;
our $func_coverage = 1;
our $br_coverage = 0;
our $rc_auto_base = 1;

our $cwd = `pwd`;
chomp($cwd);


#
# Code entry point
#

# Register handler routine to be called when interrupted
$SIG{"INT"} = \&int_handler;
$SIG{__WARN__} = \&warn_handler;
$SIG{__DIE__} = \&die_handler;

# Prettify version string
$lcov_version =~ s/\$\s*Revision\s*:?\s*(\S+)\s*\$/$1/;

# Set LANG so that gcov output will be in a unified format
$ENV{"LANG"} = "C";

# Check command line for a configuration file name
Getopt::Long::Configure("pass_through", "no_auto_abbrev");
GetOptions("config-file=s" => \$opt_config_file,
	   "rc=s%" => \%opt_rc);
Getopt::Long::Configure("default");

# Read configuration file if available
if (defined($opt_config_file)) {
	$config = read_config($opt_config_file);
} elsif (defined($ENV{"HOME"}) && (-r $ENV{"HOME"}."/.lcovrc"))
{
	$config = read_config($ENV{"HOME"}."/.lcovrc");
}
elsif (-r "/etc/lcovrc")
{
	$config = read_config("/etc/lcovrc");
}

if ($config || %opt_rc)
{
	# Copy configuration file and --rc values to variables
	apply_config({
		"geninfo_gcov_tool"		=> \$gcov_tool,
		"geninfo_adjust_testname"	=> \$adjust_testname,
		"geninfo_checksum"		=> \$checksum,
		"geninfo_no_checksum"		=> \$no_checksum, # deprecated
		"geninfo_compat_libtool"	=> \$opt_compat_libtool,
		"geninfo_external"		=> \$opt_external,
		"geninfo_gcov_all_blocks"	=> \$opt_gcov_all_blocks,
		"geninfo_compat"		=> \$opt_compat,
		"geninfo_adjust_src_path"	=> \$rc_adjust_src_path,
		"geninfo_auto_base"		=> \$rc_auto_base,
		"lcov_function_coverage"	=> \$func_coverage,
		"lcov_branch_coverage"		=> \$br_coverage,
	});

	# Merge options
	if (defined($no_checksum))
	{
		$checksum = ($no_checksum ? 0 : 1);
		$no_checksum = undef;
	}

	# Check regexp
	if (defined($rc_adjust_src_path)) {
		my ($pattern, $replace) = split(/\s*=>\s*/,
						$rc_adjust_src_path);
		local $SIG{__DIE__};
		eval '$adjust_src_pattern = qr>'.$pattern.'>;';
		if (!defined($adjust_src_pattern)) {
			my $msg = $@;

			chomp($msg);
			$msg =~ s/at \(eval.*$//;
			warn("WARNING: invalid pattern in ".
			     "geninfo_adjust_src_path: $msg\n");
		} elsif (!defined($replace)) {
			# If no replacement is specified, simply remove pattern
			$adjust_src_replace = "";
		} else {
			$adjust_src_replace = $replace;
		}
	}
}

# Parse command line options
if (!GetOptions("test-name|t=s" => \$test_name,
		"output-filename|o=s" => \$output_filename,
		"checksum" => \$checksum,
		"no-checksum" => \$no_checksum,
		"base-directory|b=s" => \$base_directory,
		"version|v" =>\$version,
		"quiet|q" => \$quiet,
		"help|h|?" => \$help,
		"follow|f" => \$follow,
		"compat-libtool" => \$opt_compat_libtool,
		"no-compat-libtool" => \$opt_no_compat_libtool,
		"gcov-tool=s" => \$gcov_tool,
		"ignore-errors=s" => \@ignore_errors,
		"initial|i" => \$initial,
		"no-recursion" => \$no_recursion,
		"no-markers" => \$no_markers,
		"derive-func-data" => \$opt_derive_func_data,
		"debug" => \$debug,
		"external" => \$opt_external,
		"no-external" => \$opt_no_external,
		"compat=s" => \$opt_compat,
		"config-file=s" => \$opt_config_file,
		"rc=s%" => \%opt_rc,
		))
{
	print(STDERR "Use $tool_name --help to get usage information\n");
	exit(1);
}
else
{
	# Merge options
	if (defined($no_checksum))
	{
		$checksum = ($no_checksum ? 0 : 1);
		$no_checksum = undef;
	}

	if (defined($opt_no_compat_libtool))
	{
		$opt_compat_libtool = ($opt_no_compat_libtool ? 0 : 1);
		$opt_no_compat_libtool = undef;
	}

	if (defined($opt_no_external)) {
		$opt_external = 0;
		$opt_no_external = undef;
	}
}

@data_directory = @ARGV;

# Check for help option
if ($help)
{
	print_usage(*STDOUT);
	exit(0);
}

# Check for version option
if ($version)
{
	print("$tool_name: $lcov_version\n");
	exit(0);
}

# Check gcov tool
if (system_no_output(3, $gcov_tool, "--help") == -1)
{
	die("ERROR: need tool $gcov_tool!\n");
}

($gcov_version, $gcov_version_string) = get_gcov_version();

# Determine gcov options
$gcov_caps = get_gcov_capabilities();
push(@gcov_options, "-b") if ($gcov_caps->{'branch-probabilities'} &&
			      ($br_coverage || $func_coverage));
push(@gcov_options, "-c") if ($gcov_caps->{'branch-counts'} &&
			      $br_coverage);
push(@gcov_options, "-a") if ($gcov_caps->{'all-blocks'} &&
			      $opt_gcov_all_blocks && $br_coverage);
push(@gcov_options, "-p") if ($gcov_caps->{'preserve-paths'});

# Determine compatibility modes
parse_compat_modes($opt_compat);

# Determine which errors the user wants us to ignore
parse_ignore_errors(@ignore_errors);

# Make sure test names only contain valid characters
if ($test_name =~ s/\W/_/g)
{
	warn("WARNING: invalid characters removed from testname!\n");
}

# Adjust test name to include uname output if requested
if ($adjust_testname)
{
	$test_name .= "__".`uname -a`;
	$test_name =~ s/\W/_/g;
}

# Make sure base_directory contains an absolute path specification
if ($base_directory)
{
	$base_directory = solve_relative_path($cwd, $base_directory);
}

# Check for follow option
if ($follow)
{
	$follow = "-follow"
}
else
{
	$follow = "";
}

# Determine checksum mode
if (defined($checksum))
{
	# Normalize to boolean
	$checksum = ($checksum ? 1 : 0);
}
else
{
	# Default is off
	$checksum = 0;
}

# Determine max depth for recursion
if ($no_recursion)
{
	$maxdepth = "-maxdepth 1";
}
else
{
	$maxdepth = "";
}

# Check for directory name
if (!@data_directory)
{
	die("No directory specified\n".
	    "Use $tool_name --help to get usage information\n");
}
else
{
	foreach (@data_directory)
	{
		stat($_);
		if (!-r _)
		{
			die("ERROR: cannot read $_!\n");
		}
	}
}

if ($gcov_version < $GCOV_VERSION_3_4_0)
{
	if (is_compat($COMPAT_MODE_HAMMER))
	{
		$data_file_extension = ".da";
		$graph_file_extension = ".bbg";
	}
	else
	{
		$data_file_extension = ".da";
		$graph_file_extension = ".bb";
	}
}
else
{
	$data_file_extension = ".gcda";
	$graph_file_extension = ".gcno";
}	

# Check output filename
if (defined($output_filename) && ($output_filename ne "-"))
{
	# Initially create output filename, data is appended
	# for each data file processed
	local *DUMMY_HANDLE;
	open(DUMMY_HANDLE, ">", $output_filename)
		or die("ERROR: cannot create $output_filename!\n");
	close(DUMMY_HANDLE);

	# Make $output_filename an absolute path because we're going
	# to change directories while processing files
	if (!($output_filename =~ /^\/(.*)$/))
	{
		$output_filename = $cwd."/".$output_filename;
	}
}

# Build list of directories to identify external files
foreach my $entry(@data_directory, $base_directory) {
	next if (!defined($entry));
	push(@internal_dirs, solve_relative_path($cwd, $entry));
}

# Do something
foreach my $entry (@data_directory) {
	gen_info($entry);
}

if ($initial && $br_coverage) {
	warn("Note: --initial does not generate branch coverage ".
	     "data\n");
}
info("Finished .info-file creation\n");

exit(0);



#
# print_usage(handle)
#
# Print usage information.
#

sub print_usage(*)
{
	local *HANDLE = $_[0];

	print(HANDLE <<END_OF_USAGE);
Usage: $tool_name [OPTIONS] DIRECTORY

Traverse DIRECTORY and create a .info file for each data file found. Note
that you may specify more than one directory, all of which are then processed
sequentially.

  -h, --help                        Print this help, then exit
  -v, --version                     Print version number, then exit
  -q, --quiet                       Do not print progress messages
  -i, --initial                     Capture initial zero coverage data
  -t, --test-name NAME              Use test case name NAME for resulting data
  -o, --output-filename OUTFILE     Write data only to OUTFILE
  -f, --follow                      Follow links when searching .da/.gcda files
  -b, --base-directory DIR          Use DIR as base directory for relative paths
      --(no-)checksum               Enable (disable) line checksumming
      --(no-)compat-libtool         Enable (disable) libtool compatibility mode
      --gcov-tool TOOL              Specify gcov tool location
      --ignore-errors ERROR         Continue after ERROR (gcov, source, graph)
      --no-recursion                Exclude subdirectories from processing
      --no-markers                  Ignore exclusion markers in source code
      --derive-func-data            Generate function data from line data
      --(no-)external               Include (ignore) data for external files
      --config-file FILENAME        Specify configuration file location
      --rc SETTING=VALUE            Override configuration file setting
      --compat MODE=on|off|auto     Set compat MODE (libtool, hammer, split_crc)

For more information see: $lcov_url
END_OF_USAGE
	;
}

#
# get_common_prefix(min_dir, filenames)
#
# Return the longest path prefix shared by all filenames. MIN_DIR specifies
# the minimum number of directories that a filename may have after removing
# the prefix.
#

sub get_common_prefix($@)
{
	my ($min_dir, @files) = @_;
	my $file;
	my @prefix;
	my $i;

	foreach $file (@files) {
		my ($v, $d, $f) = splitpath($file);
		my @comp = splitdir($d);

		if (!@prefix) {
			@prefix = @comp;
			next;
		}
		for ($i = 0; $i < scalar(@comp) && $i < scalar(@prefix); $i++) {
			if ($comp[$i] ne $prefix[$i] ||
			    ((scalar(@comp) - ($i + 1)) <= $min_dir)) {
				delete(@prefix[$i..scalar(@prefix)]);
				last;
			}
		}
	}

	return catdir(@prefix);
}

#
# gen_info(directory)
#
# Traverse DIRECTORY and create a .info file for each data file found.
# The .info file contains TEST_NAME in the following format:
#
#   TN:<test name>
#
# For each source file name referenced in the data file, there is a section
# containing source code and coverage data:
#
#   SF:<absolute path to the source file>
#   FN:<line number of function start>,<function name> for each function
#   DA:<line number>,<execution count> for each instrumented line
#   LH:<number of lines with an execution count> greater than 0
#   LF:<number of instrumented lines>
#
# Sections are separated by:
#
#   end_of_record
#
# In addition to the main source code file there are sections for each
# #included file containing executable code. Note that the absolute path
# of a source file is generated by interpreting the contents of the respective
# graph file. Relative filenames are prefixed with the directory in which the
# graph file is found. Note also that symbolic links to the graph file will be
# resolved so that the actual file path is used instead of the path to a link.
# This approach is necessary for the mechanism to work with the /proc/gcov
# files.
#
# Die on error.
#

sub gen_info($)
{
	my $directory = $_[0];
	my @file_list;
	my $file;
	my $prefix;
	my $type;
	my $ext;

	if ($initial) {
		$type = "graph";
		$ext = $graph_file_extension;
	} else {
		$type = "data";
		$ext = $data_file_extension;
	}

	if (-d $directory)
	{
		info("Scanning $directory for $ext files ...\n");

		@file_list = `find "$directory" $maxdepth $follow -name \\*$ext -type f 2>/dev/null`;
		chomp(@file_list);
		@file_list or
			die("ERROR: no $ext files found in $directory!\n");
		$prefix = get_common_prefix(1, @file_list);
		info("Found %d %s files in %s\n", $#file_list+1, $type,
		     $directory);
	}
	else
	{
		@file_list = ($directory);
		$prefix = "";
	}

	# Process all files in list
	foreach $file (@file_list) {
		# Process file
		if ($initial) {
			process_graphfile($file, $prefix);
		} else {
			process_dafile($file, $prefix);
		}
	}
}


#
# derive_data(contentdata, funcdata, bbdata)
#
# Calculate function coverage data by combining line coverage data and the
# list of lines belonging to a function.
#
# contentdata: [ instr1, count1, source1, instr2, count2, source2, ... ]
# instr<n>: Instrumentation flag for line n
# count<n>: Execution count for line n
# source<n>: Source code for line n
#
# funcdata: [ count1, func1, count2, func2, ... ]
# count<n>: Execution count for function number n
# func<n>: Function name for function number n
#
# bbdata: function_name -> [ line1, line2, ... ]
# line<n>: Line number belonging to the corresponding function
#

sub derive_data($$$)
{
	my ($contentdata, $funcdata, $bbdata) = @_;
	my @gcov_content = @{$contentdata};
	my @gcov_functions = @{$funcdata};
	my %fn_count;
	my %ln_fn;
	my $line;
	my $maxline;
	my %fn_name;
	my $fn;
	my $count;

	if (!defined($bbdata)) {
		return @gcov_functions;
	}

	# First add existing function data
	while (@gcov_functions) {
		$count = shift(@gcov_functions);
		$fn = shift(@gcov_functions);

		$fn_count{$fn} = $count;
	}

	# Convert line coverage data to function data
	foreach $fn (keys(%{$bbdata})) {
		my $line_data = $bbdata->{$fn};
		my $line;
		my $fninstr = 0;

		if ($fn eq "") {
			next;
		}
		# Find the lowest line count for this function
		$count = 0;
		foreach $line (@$line_data) {
			my $linstr = $gcov_content[ ( $line - 1 ) * 3 + 0 ];
			my $lcount = $gcov_content[ ( $line - 1 ) * 3 + 1 ];

			next if (!$linstr);
			$fninstr = 1;
			if (($lcount > 0) &&
			    (($count == 0) || ($lcount < $count))) {
				$count = $lcount;
			}
		}
		next if (!$fninstr);
		$fn_count{$fn} = $count;
	}


	# Check if we got data for all functions
	foreach $fn (keys(%fn_name)) {
		if ($fn eq "") {
			next;
		}
		if (defined($fn_count{$fn})) {
			next;
		}
		warn("WARNING: no derived data found for function $fn\n");
	}

	# Convert hash to list in @gcov_functions format
	foreach $fn (sort(keys(%fn_count))) {
		push(@gcov_functions, $fn_count{$fn}, $fn);
	}

	return @gcov_functions;
}

#
# get_filenames(directory, pattern)
#
# Return a list of filenames found in directory which match the specified
# pattern.
#
# Die on error.
#

sub get_filenames($$)
{
	my ($dirname, $pattern) = @_;
	my @result;
	my $directory;
	local *DIR;

	opendir(DIR, $dirname) or
		die("ERROR: cannot read directory $dirname\n");
	while ($directory = readdir(DIR)) {
		push(@result, $directory) if ($directory =~ /$pattern/);
	}
	closedir(DIR);

	return @result;
}

#
# process_dafile(da_filename, dir)
#
# Create a .info file for a single data file.
#
# Die on error.
#

sub process_dafile($$)
{
	my ($file, $dir) = @_;
	my $da_filename;	# Name of data file to process
	my $da_dir;		# Directory of data file
	my $source_dir;		# Directory of source file
	my $da_basename;	# data filename without ".da/.gcda" extension
	my $bb_filename;	# Name of respective graph file
	my $bb_basename;	# Basename of the original graph file
	my $graph;		# Contents of graph file
	my $instr;		# Contents of graph file part 2
	my $gcov_error;		# Error code of gcov tool
	my $object_dir;		# Directory containing all object files
	my $source_filename;	# Name of a source code file
	my $gcov_file;		# Name of a .gcov file
	my @gcov_content;	# Content of a .gcov file
	my $gcov_branches;	# Branch content of a .gcov file
	my @gcov_functions;	# Function calls of a .gcov file
	my @gcov_list;		# List of generated .gcov files
	my $line_number;	# Line number count
	my $lines_hit;		# Number of instrumented lines hit
	my $lines_found;	# Number of instrumented lines found
	my $funcs_hit;		# Number of instrumented functions hit
	my $funcs_found;	# Number of instrumented functions found
	my $br_hit;
	my $br_found;
	my $source;		# gcov source header information
	my $object;		# gcov object header information
	my @matches;		# List of absolute paths matching filename
	my @unprocessed;	# List of unprocessed source code files
	my $base_dir;		# Base directory for current file
	my @tmp_links;		# Temporary links to be cleaned up
	my @result;
	my $index;
	my $da_renamed;		# If data file is to be renamed
	local *INFO_HANDLE;

	info("Processing %s\n", abs2rel($file, $dir));
	# Get path to data file in absolute and normalized form (begins with /,
	# contains no more ../ or ./)
	$da_filename = solve_relative_path($cwd, $file);

	# Get directory and basename of data file
	($da_dir, $da_basename) = split_filename($da_filename);

	$source_dir = $da_dir;
	if (is_compat($COMPAT_MODE_LIBTOOL)) {
		# Avoid files from .libs dirs 	 
		$source_dir =~ s/\.libs$//;
	}

	if (-z $da_filename)
	{
		$da_renamed = 1;
	}
	else
	{
		$da_renamed = 0;
	}

	# Construct base_dir for current file
	if ($base_directory)
	{
		$base_dir = $base_directory;
	}
	else
	{
		$base_dir = $source_dir;
	}

	# Check for writable $base_dir (gcov will try to write files there)
	stat($base_dir);
	if (!-w _)
	{
		die("ERROR: cannot write to directory $base_dir!\n");
	}

	# Construct name of graph file
	$bb_basename = $da_basename.$graph_file_extension;
	$bb_filename = "$da_dir/$bb_basename";

	# Find out the real location of graph file in case we're just looking at
	# a link
	while (readlink($bb_filename))
	{
		my $last_dir = dirname($bb_filename);

		$bb_filename = readlink($bb_filename);
		$bb_filename = solve_relative_path($last_dir, $bb_filename);
	}

	# Ignore empty graph file (e.g. source file with no statement)
	if (-z $bb_filename)
	{
		warn("WARNING: empty $bb_filename (skipped)\n");
		return;
	}

	# Read contents of graph file into hash. We need it later to find out
	# the absolute path to each .gcov file created as well as for
	# information about functions and their source code positions.
	if ($gcov_version < $GCOV_VERSION_3_4_0)
	{
		if (is_compat($COMPAT_MODE_HAMMER))
		{
			($instr, $graph) = read_bbg($bb_filename);
		}
		else
		{
			($instr, $graph) = read_bb($bb_filename);
		}
	} 
	else
	{
		($instr, $graph) = read_gcno($bb_filename);
	} 

	# Try to find base directory automatically if requested by user
	if ($rc_auto_base) {
		$base_dir = find_base_from_graph($base_dir, $instr, $graph);
	}

	($instr, $graph) = adjust_graph_filenames($base_dir, $instr, $graph);

	# Set $object_dir to real location of object files. This may differ
	# from $da_dir if the graph file is just a link to the "real" object
	# file location.
	$object_dir = dirname($bb_filename);

	# Is the data file in a different directory? (this happens e.g. with
	# the gcov-kernel patch)
	if ($object_dir ne $da_dir)
	{
		# Need to create link to data file in $object_dir
		system("ln", "-s", $da_filename, 
		       "$object_dir/$da_basename$data_file_extension")
			and die ("ERROR: cannot create link $object_dir/".
				 "$da_basename$data_file_extension!\n");
		push(@tmp_links,
		     "$object_dir/$da_basename$data_file_extension");
		# Need to create link to graph file if basename of link
		# and file are different (CONFIG_MODVERSION compat)
		if ((basename($bb_filename) ne $bb_basename) &&
		    (! -e "$object_dir/$bb_basename")) {
			symlink($bb_filename, "$object_dir/$bb_basename") or
				warn("WARNING: cannot create link ".
				     "$object_dir/$bb_basename\n");
			push(@tmp_links, "$object_dir/$bb_basename");
		}
	}

	# Change to directory containing data files and apply GCOV
	debug("chdir($base_dir)\n");
        chdir($base_dir);

	if ($da_renamed)
	{
		# Need to rename empty data file to workaround
	        # gcov <= 3.2.x bug (Abort)
		system_no_output(3, "mv", "$da_filename", "$da_filename.ori")
			and die ("ERROR: cannot rename $da_filename\n");
	}

	# Execute gcov command and suppress standard output
	$gcov_error = system_no_output(1, $gcov_tool, $da_filename,
				       "-o", $object_dir, @gcov_options);

	if ($da_renamed)
	{
		system_no_output(3, "mv", "$da_filename.ori", "$da_filename")
			and die ("ERROR: cannot rename $da_filename.ori");
	}

	# Clean up temporary links
	foreach (@tmp_links) {
		unlink($_);
	}

	if ($gcov_error)
	{
		if ($ignore[$ERROR_GCOV])
		{
			warn("WARNING: GCOV failed for $da_filename!\n");
			return;
		}
		die("ERROR: GCOV failed for $da_filename!\n");
	}

	# Collect data from resulting .gcov files and create .info file
	@gcov_list = get_filenames('.', '\.gcov$');

	# Check for files
	if (!@gcov_list)
	{
		warn("WARNING: gcov did not create any files for ".
		     "$da_filename!\n");
	}

	# Check whether we're writing to a single file
	if ($output_filename)
	{
		if ($output_filename eq "-")
		{
			*INFO_HANDLE = *STDOUT;
		}
		else
		{
			# Append to output file
			open(INFO_HANDLE, ">>", $output_filename)
				or die("ERROR: cannot write to ".
				       "$output_filename!\n");
		}
	}
	else
	{
		# Open .info file for output
		open(INFO_HANDLE, ">", "$da_filename.info")
			or die("ERROR: cannot create $da_filename.info!\n");
	}

	# Write test name
	printf(INFO_HANDLE "TN:%s\n", $test_name);

	# Traverse the list of generated .gcov files and combine them into a
	# single .info file
	@unprocessed = keys(%{$instr});
	foreach $gcov_file (sort(@gcov_list))
	{
		my $i;
		my $num;

		# Skip gcov file for gcc built-in code
		next if ($gcov_file eq "<built-in>.gcov");

		($source, $object) = read_gcov_header($gcov_file);

		if (!defined($source)) {
			# Derive source file name from gcov file name if
			# header format could not be parsed
			$source = $gcov_file;
			$source =~ s/\.gcov$//;
		}

		$source = solve_relative_path($base_dir, $source);

		if (defined($adjust_src_pattern)) {
			# Apply transformation as specified by user
			$source =~ s/$adjust_src_pattern/$adjust_src_replace/g;
		}

		# gcov will happily create output even if there's no source code
		# available - this interferes with checksum creation so we need
		# to pull the emergency brake here.
		if (! -r $source && $checksum)
		{
			if ($ignore[$ERROR_SOURCE])
			{
				warn("WARNING: could not read source file ".
				     "$source\n");
				next;
			}
			die("ERROR: could not read source file $source\n");
		}

		@matches = match_filename($source, keys(%{$instr}));

		# Skip files that are not mentioned in the graph file
		if (!@matches)
		{
			warn("WARNING: cannot find an entry for ".$gcov_file.
			     " in $graph_file_extension file, skipping ".
			     "file!\n");
			unlink($gcov_file);
			next;
		}

		# Read in contents of gcov file
		@result = read_gcov_file($gcov_file);
		if (!defined($result[0])) {
			warn("WARNING: skipping unreadable file ".
			     $gcov_file."\n");
			unlink($gcov_file);
			next;
		}
		@gcov_content = @{$result[0]};
		$gcov_branches = $result[1];
		@gcov_functions = @{$result[2]};

		# Skip empty files
		if (!@gcov_content)
		{
			warn("WARNING: skipping empty file ".$gcov_file."\n");
			unlink($gcov_file);
			next;
		}

		if (scalar(@matches) == 1)
		{
			# Just one match
			$source_filename = $matches[0];
		}
		else
		{
			# Try to solve the ambiguity
			$source_filename = solve_ambiguous_match($gcov_file,
						\@matches, \@gcov_content);
		}

		# Remove processed file from list
		for ($index = scalar(@unprocessed) - 1; $index >= 0; $index--)
		{
			if ($unprocessed[$index] eq $source_filename)
			{
				splice(@unprocessed, $index, 1);
				last;
			}
		}

		# Skip external files if requested
		if (!$opt_external) {
			if (is_external($source_filename)) {
				info("  ignoring data for external file ".
				     "$source_filename\n");
				unlink($gcov_file);
				next;
			}
		}

		# Write absolute path of source file
		printf(INFO_HANDLE "SF:%s\n", $source_filename);

		# If requested, derive function coverage data from
		# line coverage data of the first line of a function
		if ($opt_derive_func_data) {
			@gcov_functions =
				derive_data(\@gcov_content, \@gcov_functions,
					    $graph->{$source_filename});
		}

		# Write function-related information
		if (defined($graph->{$source_filename}))
		{
			my $fn_data = $graph->{$source_filename};
			my $fn;

			foreach $fn (sort
				{$fn_data->{$a}->[0] <=> $fn_data->{$b}->[0]}
				keys(%{$fn_data})) {
				my $ln_data = $fn_data->{$fn};
				my $line = $ln_data->[0];

				# Skip empty function
				if ($fn eq "") {
					next;
				}
				# Remove excluded functions
				if (!$no_markers) {
					my $gfn;
					my $found = 0;

					foreach $gfn (@gcov_functions) {
						if ($gfn eq $fn) {
							$found = 1;
							last;
						}
					}
					if (!$found) {
						next;
					}
				}

				# Normalize function name
				$fn = filter_fn_name($fn);

				print(INFO_HANDLE "FN:$line,$fn\n");
			}
		}

		#--
		#-- FNDA: <call-count>, <function-name>
		#-- FNF: overall count of functions
		#-- FNH: overall count of functions with non-zero call count
		#--
		$funcs_found = 0;
		$funcs_hit = 0;
		while (@gcov_functions)
		{
			my $count = shift(@gcov_functions);
			my $fn = shift(@gcov_functions);

			$fn = filter_fn_name($fn);
			printf(INFO_HANDLE "FNDA:$count,$fn\n");
			$funcs_found++;
			$funcs_hit++ if ($count > 0);
		}
		if ($funcs_found > 0) {
			printf(INFO_HANDLE "FNF:%s\n", $funcs_found);
			printf(INFO_HANDLE "FNH:%s\n", $funcs_hit);
		}

		# Write coverage information for each instrumented branch:
		#
		#   BRDA:<line number>,<block number>,<branch number>,<taken>
		#
		# where 'taken' is the number of times the branch was taken
		# or '-' if the block to which the branch belongs was never
		# executed
		$br_found = 0;
		$br_hit = 0;
		$num = br_gvec_len($gcov_branches);
		for ($i = 0; $i < $num; $i++) {
			my ($line, $block, $branch, $taken) =
				br_gvec_get($gcov_branches, $i);

			print(INFO_HANDLE "BRDA:$line,$block,$branch,$taken\n");
			$br_found++;
			$br_hit++ if ($taken ne '-' && $taken > 0);
		}
		if ($br_found > 0) {
			printf(INFO_HANDLE "BRF:%s\n", $br_found);
			printf(INFO_HANDLE "BRH:%s\n", $br_hit);
		}

		# Reset line counters
		$line_number = 0;
		$lines_found = 0;
		$lines_hit = 0;

		# Write coverage information for each instrumented line
		# Note: @gcov_content contains a list of (flag, count, source)
		# tuple for each source code line
		while (@gcov_content)
		{
			$line_number++;

			# Check for instrumented line
			if ($gcov_content[0])
			{
				$lines_found++;
				printf(INFO_HANDLE "DA:".$line_number.",".
				       $gcov_content[1].($checksum ?
				       ",". md5_base64($gcov_content[2]) : "").
				       "\n");

				# Increase $lines_hit in case of an execution
				# count>0
				if ($gcov_content[1] > 0) { $lines_hit++; }
			}

			# Remove already processed data from array
			splice(@gcov_content,0,3);
		}

		# Write line statistics and section separator
		printf(INFO_HANDLE "LF:%s\n", $lines_found);
		printf(INFO_HANDLE "LH:%s\n", $lines_hit);
		print(INFO_HANDLE "end_of_record\n");

		# Remove .gcov file after processing
		unlink($gcov_file);
	}

	# Check for files which show up in the graph file but were never
	# processed
	if (@unprocessed && @gcov_list)
	{
		foreach (@unprocessed)
		{
			warn("WARNING: no data found for $_\n");
		}
	}

	if (!($output_filename && ($output_filename eq "-")))
	{
		close(INFO_HANDLE);
	}

	# Change back to initial directory
	chdir($cwd);
}


#
# solve_relative_path(path, dir)
#
# Solve relative path components of DIR which, if not absolute, resides in PATH.
#

sub solve_relative_path($$)
{
	my $path = $_[0];
	my $dir = $_[1];
	my $volume;
	my $directories;
	my $filename;
	my @dirs;			# holds path elements
	my $result;

	# Convert from Windows path to msys path
	if( $^O eq "msys" )
	{
		# search for a windows drive letter at the beginning
		($volume, $directories, $filename) = File::Spec::Win32->splitpath( $dir );
		if( $volume ne '' )
		{
			my $uppercase_volume;
			# transform c/d\../e/f\g to Windows style c\d\..\e\f\g
			$dir = File::Spec::Win32->canonpath( $dir );
			# use Win32 module to retrieve path components
			# $uppercase_volume is not used any further
			( $uppercase_volume, $directories, $filename ) = File::Spec::Win32->splitpath( $dir );
			@dirs = File::Spec::Win32->splitdir( $directories );
			
			# prepend volume, since in msys C: is always mounted to /c
			$volume =~ s|^([a-zA-Z]+):|/\L$1\E|;
			unshift( @dirs, $volume );
			
			# transform to Unix style '/' path
			$directories = File::Spec->catdir( @dirs );
			$dir = File::Spec->catpath( '', $directories, $filename );
		} else {
			# eliminate '\' path separators
			$dir = File::Spec->canonpath( $dir );
		}
	}

	$result = $dir;
	# Prepend path if not absolute
	if ($dir =~ /^[^\/]/)
	{
		$result = "$path/$result";
	}

	# Remove //
	$result =~ s/\/\//\//g;

	# Remove .
	$result =~ s/\/\.\//\//g;
	$result =~ s/\/\.$/\//g;

	# Remove trailing /
	$result =~ s/\/$//g;

	# Solve ..
	while ($result =~ s/\/[^\/]+\/\.\.\//\//)
	{
	}

	# Remove preceding ..
	$result =~ s/^\/\.\.\//\//g;

	return $result;
}


#
# match_filename(gcov_filename, list)
#
# Return a list of those entries of LIST which match the relative filename
# GCOV_FILENAME.
#

sub match_filename($@)
{
	my ($filename, @list) = @_;
	my ($vol, $dir, $file) = splitpath($filename);
	my @comp = splitdir($dir);
	my $comps = scalar(@comp);
	my $entry;
	my @result;

entry:
	foreach $entry (@list) {
		my ($evol, $edir, $efile) = splitpath($entry);
		my @ecomp;
		my $ecomps;
		my $i;

		# Filename component must match
		if ($efile ne $file) {
			next;
		}
		# Check directory components last to first for match
		@ecomp = splitdir($edir);
		$ecomps = scalar(@ecomp);
		if ($ecomps < $comps) {
			next;
		}
		for ($i = 0; $i < $comps; $i++) {
			if ($comp[$comps - $i - 1] ne
			    $ecomp[$ecomps - $i - 1]) {
				next entry;
			}
		}
		push(@result, $entry),
	}

	return @result;
}

#
# solve_ambiguous_match(rel_filename, matches_ref, gcov_content_ref)
#
# Try to solve ambiguous matches of mapping (gcov file) -> (source code) file
# by comparing source code provided in the GCOV file with that of the files
# in MATCHES. REL_FILENAME identifies the relative filename of the gcov
# file.
# 
# Return the one real match or die if there is none.
#

sub solve_ambiguous_match($$$)
{
	my $rel_name = $_[0];
	my $matches = $_[1];
	my $content = $_[2];
	my $filename;
	my $index;
	my $no_match;
	local *SOURCE;

	# Check the list of matches
	foreach $filename (@$matches)
	{

		# Compare file contents
		open(SOURCE, "<", $filename)
			or die("ERROR: cannot read $filename!\n");

		$no_match = 0;
		for ($index = 2; <SOURCE>; $index += 3)
		{
			chomp;

			# Also remove CR from line-end
			s/\015$//;

			if ($_ ne @$content[$index])
			{
				$no_match = 1;
				last;
			}
		}

		close(SOURCE);

		if (!$no_match)
		{
			info("Solved source file ambiguity for $rel_name\n");
			return $filename;
		}
	}

	die("ERROR: could not match gcov data for $rel_name!\n");
}


#
# split_filename(filename)
#
# Return (path, filename, extension) for a given FILENAME.
#

sub split_filename($)
{
	my @path_components = split('/', $_[0]);
	my @file_components = split('\.', pop(@path_components));
	my $extension = pop(@file_components);

	return (join("/",@path_components), join(".",@file_components),
		$extension);
}


#
# read_gcov_header(gcov_filename)
#
# Parse file GCOV_FILENAME and return a list containing the following
# information:
#
#   (source, object)
#
# where:
#
# source: complete relative path of the source code file (gcc >= 3.3 only)
# object: name of associated graph file
#
# Die on error.
#

sub read_gcov_header($)
{
	my $source;
	my $object;
	local *INPUT;

	if (!open(INPUT, "<", $_[0]))
	{
		if ($ignore_errors[$ERROR_GCOV])
		{
			warn("WARNING: cannot read $_[0]!\n");
			return (undef,undef);
		}
		die("ERROR: cannot read $_[0]!\n");
	}

	while (<INPUT>)
	{
		chomp($_);

		# Also remove CR from line-end
		s/\015$//;

		if (/^\s+-:\s+0:Source:(.*)$/)
		{
			# Source: header entry
			$source = $1;
		}
		elsif (/^\s+-:\s+0:Object:(.*)$/)
		{
			# Object: header entry
			$object = $1;
		}
		else
		{
			last;
		}
	}

	close(INPUT);

	return ($source, $object);
}


#
# br_gvec_len(vector)
#
# Return the number of entries in the branch coverage vector.
#

sub br_gvec_len($)
{
	my ($vec) = @_;

	return 0 if (!defined($vec));
	return (length($vec) * 8 / $BR_VEC_WIDTH) / $BR_VEC_ENTRIES;
}


#
# br_gvec_get(vector, number)
#
# Return an entry from the branch coverage vector.
#

sub br_gvec_get($$)
{
	my ($vec, $num) = @_;
	my $line;
	my $block;
	my $branch;
	my $taken;
	my $offset = $num * $BR_VEC_ENTRIES;

	# Retrieve data from vector
	$line	= vec($vec, $offset + $BR_LINE, $BR_VEC_WIDTH);
	$block	= vec($vec, $offset + $BR_BLOCK, $BR_VEC_WIDTH);
	$branch = vec($vec, $offset + $BR_BRANCH, $BR_VEC_WIDTH);
	$taken	= vec($vec, $offset + $BR_TAKEN, $BR_VEC_WIDTH);

	# Decode taken value from an integer
	if ($taken == 0) {
		$taken = "-";
	} else {
		$taken--;
	}

	return ($line, $block, $branch, $taken);
}


#
# br_gvec_push(vector, line, block, branch, taken)
#
# Add an entry to the branch coverage vector.
#

sub br_gvec_push($$$$$)
{
	my ($vec, $line, $block, $branch, $taken) = @_;
	my $offset;

	$vec = "" if (!defined($vec));
	$offset = br_gvec_len($vec) * $BR_VEC_ENTRIES;

	# Encode taken value into an integer
	if ($taken eq "-") {
		$taken = 0;
	} else {
		$taken++;
	}

	# Add to vector
	vec($vec, $offset + $BR_LINE, $BR_VEC_WIDTH) = $line;
	vec($vec, $offset + $BR_BLOCK, $BR_VEC_WIDTH) = $block;
	vec($vec, $offset + $BR_BRANCH, $BR_VEC_WIDTH) = $branch;
	vec($vec, $offset + $BR_TAKEN, $BR_VEC_WIDTH) = $taken;

	return $vec;
}


#
# read_gcov_file(gcov_filename)
#
# Parse file GCOV_FILENAME (.gcov file format) and return the list:
# (reference to gcov_content, reference to gcov_branch, reference to gcov_func)
#
# gcov_content is a list of 3 elements
# (flag, count, source) for each source code line:
#
# $result[($line_number-1)*3+0] = instrumentation flag for line $line_number
# $result[($line_number-1)*3+1] = execution count for line $line_number
# $result[($line_number-1)*3+2] = source code text for line $line_number
#
# gcov_branch is a vector of 4 4-byte long elements for each branch:
# line number, block number, branch number, count + 1 or 0
#
# gcov_func is a list of 2 elements
# (number of calls, function name) for each function
#
# Die on error.
#

sub read_gcov_file($)
{
	my $filename = $_[0];
	my @result = ();
	my $branches = "";
	my @functions = ();
	my $number;
	my $exclude_flag = 0;
	my $exclude_line = 0;
	my $last_block = $UNNAMED_BLOCK;
	my $last_line = 0;
	local *INPUT;

	if (!open(INPUT, "<", $filename)) {
		if ($ignore_errors[$ERROR_GCOV])
		{
			warn("WARNING: cannot read $filename!\n");
			return (undef, undef, undef);
		}
		die("ERROR: cannot read $filename!\n");
	}

	if ($gcov_version < $GCOV_VERSION_3_3_0)
	{
		# Expect gcov format as used in gcc < 3.3
		while (<INPUT>)
		{
			chomp($_);

			# Also remove CR from line-end
			s/\015$//;

			if (/^branch\s+(\d+)\s+taken\s+=\s+(\d+)/) {
				next if (!$br_coverage);
				next if ($exclude_line);
				$branches = br_gvec_push($branches, $last_line,
						$last_block, $1, $2);
			} elsif (/^branch\s+(\d+)\s+never\s+executed/) {
				next if (!$br_coverage);
				next if ($exclude_line);
				$branches = br_gvec_push($branches, $last_line,
						$last_block, $1, '-');
			}
			elsif (/^call/ || /^function/)
			{
				# Function call return data
			}
			else
			{
				$last_line++;
				# Check for exclusion markers
				if (!$no_markers) {
					if (/$EXCL_STOP/) {
						$exclude_flag = 0;
					} elsif (/$EXCL_START/) {
						$exclude_flag = 1;
					}
					if (/$EXCL_LINE/ || $exclude_flag) {
						$exclude_line = 1;
					} else {
						$exclude_line = 0;
					}
				}
				# Source code execution data
				if (/^\t\t(.*)$/)
				{
					# Uninstrumented line
					push(@result, 0);
					push(@result, 0);
					push(@result, $1);
					next;
				}
				$number = (split(" ",substr($_, 0, 16)))[0];

				# Check for zero count which is indicated
				# by ######
				if ($number eq "######") { $number = 0;	}

				if ($exclude_line) {
					# Register uninstrumented line instead
					push(@result, 0);
					push(@result, 0);
				} else {
					push(@result, 1);
					push(@result, $number);
				}
				push(@result, substr($_, 16));
			}
		}
	}
	else
	{
		# Expect gcov format as used in gcc >= 3.3
		while (<INPUT>)
		{
			chomp($_);

			# Also remove CR from line-end
			s/\015$//;

			if (/^\s*(\d+|\$+):\s*(\d+)-block\s+(\d+)\s*$/) {
				# Block information - used to group related
				# branches
				$last_line = $2;
				$last_block = $3;
			} elsif (/^branch\s+(\d+)\s+taken\s+(\d+)/) {
				next if (!$br_coverage);
				next if ($exclude_line);
				$branches = br_gvec_push($branches, $last_line,
						$last_block, $1, $2);
			} elsif (/^branch\s+(\d+)\s+never\s+executed/) {
				next if (!$br_coverage);
				next if ($exclude_line);
				$branches = br_gvec_push($branches, $last_line,
						$last_block, $1, '-');
			}
			elsif (/^function\s+(.+)\s+called\s+(\d+)\s+/)
			{
				next if (!$func_coverage);
				if ($exclude_line) {
					next;
				}
				push(@functions, $2, $1);
			}
			elsif (/^call/)
			{
				# Function call return data
			}
			elsif (/^\s*([^:]+):\s*([^:]+):(.*)$/)
			{
				my ($count, $line, $code) = ($1, $2, $3);

				$last_line = $line;
				$last_block = $UNNAMED_BLOCK;
				# Check for exclusion markers
				if (!$no_markers) {
					if (/$EXCL_STOP/) {
						$exclude_flag = 0;
					} elsif (/$EXCL_START/) {
						$exclude_flag = 1;
					}
					if (/$EXCL_LINE/ || $exclude_flag) {
						$exclude_line = 1;
					} else {
						$exclude_line = 0;
					}
				}
				# <exec count>:<line number>:<source code>
				if ($line eq "0")
				{
					# Extra data
				}
				elsif ($count eq "-")
				{
					# Uninstrumented line
					push(@result, 0);
					push(@result, 0);
					push(@result, $code);
				}
				else
				{
					if ($exclude_line) {
						push(@result, 0);
						push(@result, 0);
					} else {
						# Check for zero count
						if ($count eq "#####") {
							$count = 0;
						}
						push(@result, 1);
						push(@result, $count);
					}
					push(@result, $code);
				}
			}
		}
	}

	close(INPUT);
	if ($exclude_flag) {
		warn("WARNING: unterminated exclusion section in $filename\n");
	}
	return(\@result, $branches, \@functions);
}


#
# Get the GCOV tool version. Return an integer number which represents the
# GCOV version. Version numbers can be compared using standard integer
# operations.
#

sub get_gcov_version()
{
	local *HANDLE;
	my $version_string;
	my $result;

	open(GCOV_PIPE, "-|", "$gcov_tool -v")
		or die("ERROR: cannot retrieve gcov version!\n");
	$version_string = <GCOV_PIPE>;
	close(GCOV_PIPE);

	$result = 0;
	if ($version_string =~ /(\d+)\.(\d+)(\.(\d+))?/)
	{
		if (defined($4))
		{
			info("Found gcov version: $1.$2.$4\n");
			$result = $1 << 16 | $2 << 8 | $4;
		}
		else
		{
			info("Found gcov version: $1.$2\n");
			$result = $1 << 16 | $2 << 8;
		}
	}
	return ($result, $version_string);
}


#
# info(printf_parameter)
#
# Use printf to write PRINTF_PARAMETER to stdout only when the $quiet flag
# is not set.
#

sub info(@)
{
	if (!$quiet)
	{
		# Print info string
		if (defined($output_filename) && ($output_filename eq "-"))
		{
			# Don't interfere with the .info output to STDOUT
			printf(STDERR @_);
		}
		else
		{
			printf(@_);
		}
	}
}


#
# int_handler()
#
# Called when the script was interrupted by an INT signal (e.g. CTRl-C)
#

sub int_handler()
{
	if ($cwd) { chdir($cwd); }
	info("Aborted.\n");
	exit(1);
}


#
# system_no_output(mode, parameters)
#
# Call an external program using PARAMETERS while suppressing depending on
# the value of MODE:
#
#   MODE & 1: suppress STDOUT
#   MODE & 2: suppress STDERR
#
# Return 0 on success, non-zero otherwise.
#

sub system_no_output($@)
{
	my $mode = shift;
	my $result;
	local *OLD_STDERR;
	local *OLD_STDOUT;

	# Save old stdout and stderr handles
	($mode & 1) && open(OLD_STDOUT, ">>&", "STDOUT");
	($mode & 2) && open(OLD_STDERR, ">>&", "STDERR");

	# Redirect to /dev/null
	($mode & 1) && open(STDOUT, ">", "/dev/null");
	($mode & 2) && open(STDERR, ">", "/dev/null");
 
	debug("system(".join(' ', @_).")\n");
	system(@_);
	$result = $?;

	# Close redirected handles
	($mode & 1) && close(STDOUT);
	($mode & 2) && close(STDERR);

	# Restore old handles
	($mode & 1) && open(STDOUT, ">>&", "OLD_STDOUT");
	($mode & 2) && open(STDERR, ">>&", "OLD_STDERR");
 
	return $result;
}


#
# read_config(filename)
#
# Read configuration file FILENAME and return a reference to a hash containing
# all valid key=value pairs found.
#

sub read_config($)
{
	my $filename = $_[0];
	my %result;
	my $key;
	my $value;
	local *HANDLE;

	if (!open(HANDLE, "<", $filename))
	{
		warn("WARNING: cannot read configuration file $filename\n");
		return undef;
	}
	while (<HANDLE>)
	{
		chomp;
		# Skip comments
		s/#.*//;
		# Remove leading blanks
		s/^\s+//;
		# Remove trailing blanks
		s/\s+$//;
		next unless length;
		($key, $value) = split(/\s*=\s*/, $_, 2);
		if (defined($key) && defined($value))
		{
			$result{$key} = $value;
		}
		else
		{
			warn("WARNING: malformed statement in line $. ".
			     "of configuration file $filename\n");
		}
	}
	close(HANDLE);
	return \%result;
}


#
# apply_config(REF)
#
# REF is a reference to a hash containing the following mapping:
#
#   key_string => var_ref
#
# where KEY_STRING is a keyword and VAR_REF is a reference to an associated
# variable. If the global configuration hashes CONFIG or OPT_RC contain a value
# for keyword KEY_STRING, VAR_REF will be assigned the value for that keyword. 
#

sub apply_config($)
{
	my $ref = $_[0];

	foreach (keys(%{$ref}))
	{
		if (defined($opt_rc{$_})) {
			${$ref->{$_}} = $opt_rc{$_};
		} elsif (defined($config->{$_})) {
			${$ref->{$_}} = $config->{$_};
		}
	}
}


#
# get_exclusion_data(filename)
#
# Scan specified source code file for exclusion markers and return
#   linenumber -> 1
# for all lines which should be excluded.
#

sub get_exclusion_data($)
{
	my ($filename) = @_;
	my %list;
	my $flag = 0;
	local *HANDLE;

	if (!open(HANDLE, "<", $filename)) {
		warn("WARNING: could not open $filename\n");
		return undef;
	}
	while (<HANDLE>) {
		if (/$EXCL_STOP/) {
			$flag = 0;
		} elsif (/$EXCL_START/) {
			$flag = 1;
		}
		if (/$EXCL_LINE/ || $flag) {
			$list{$.} = 1;
		}
	}
	close(HANDLE);

	if ($flag) {
		warn("WARNING: unterminated exclusion section in $filename\n");
	}

	return \%list;
}


#
# apply_exclusion_data(instr, graph)
#
# Remove lines from instr and graph data structures which are marked
# for exclusion in the source code file.
#
# Return adjusted (instr, graph).
#
# graph         : file name -> function data
# function data : function name -> line data
# line data     : [ line1, line2, ... ]
#
# instr     : filename -> line data
# line data : [ line1, line2, ... ]
#

sub apply_exclusion_data($$)
{
	my ($instr, $graph) = @_;
	my $filename;
	my %excl_data;
	my $excl_read_failed = 0;

	# Collect exclusion marker data
	foreach $filename (sort_uniq_lex(keys(%{$graph}), keys(%{$instr}))) {
		my $excl = get_exclusion_data($filename);

		# Skip and note if file could not be read
		if (!defined($excl)) {
			$excl_read_failed = 1;
			next;
		}

		# Add to collection if there are markers
		$excl_data{$filename} = $excl if (keys(%{$excl}) > 0);
	}

	# Warn if not all source files could be read
	if ($excl_read_failed) {
		warn("WARNING: some exclusion markers may be ignored\n");
	}

	# Skip if no markers were found
	return ($instr, $graph) if (keys(%excl_data) == 0);

	# Apply exclusion marker data to graph
	foreach $filename (keys(%excl_data)) {
		my $function_data = $graph->{$filename};
		my $excl = $excl_data{$filename};
		my $function;

		next if (!defined($function_data));

		foreach $function (keys(%{$function_data})) {
			my $line_data = $function_data->{$function};
			my $line;
			my @new_data;

			# To be consistent with exclusion parser in non-initial
			# case we need to remove a function if the first line
			# was excluded
			if ($excl->{$line_data->[0]}) {
				delete($function_data->{$function});
				next;
			}
			# Copy only lines which are not excluded
			foreach $line (@{$line_data}) {
				push(@new_data, $line) if (!$excl->{$line});
			}

			# Store modified list
			if (scalar(@new_data) > 0) {
				$function_data->{$function} = \@new_data;
			} else {
				# All of this function was excluded
				delete($function_data->{$function});
			}
		}

		# Check if all functions of this file were excluded
		if (keys(%{$function_data}) == 0) {
			delete($graph->{$filename});
		}
	}

	# Apply exclusion marker data to instr
	foreach $filename (keys(%excl_data)) {
		my $line_data = $instr->{$filename};
		my $excl = $excl_data{$filename};
		my $line;
		my @new_data;

		next if (!defined($line_data));

		# Copy only lines which are not excluded
		foreach $line (@{$line_data}) {
			push(@new_data, $line) if (!$excl->{$line});
		}

		# Store modified list
		$instr->{$filename} = \@new_data;
	}

	return ($instr, $graph);
}


sub process_graphfile($$)
{
	my ($file, $dir) = @_;
	my $graph_filename = $file;
	my $graph_dir;
	my $graph_basename;
	my $source_dir;
	my $base_dir;
	my $graph;
	my $instr;
	my $filename;
	local *INFO_HANDLE;

	info("Processing %s\n", abs2rel($file, $dir));

	# Get path to data file in absolute and normalized form (begins with /,
	# contains no more ../ or ./)
	$graph_filename = solve_relative_path($cwd, $graph_filename);

	# Get directory and basename of data file
	($graph_dir, $graph_basename) = split_filename($graph_filename);

	$source_dir = $graph_dir;
	if (is_compat($COMPAT_MODE_LIBTOOL)) {
		# Avoid files from .libs dirs 	 
		$source_dir =~ s/\.libs$//;
	}

	# Construct base_dir for current file
	if ($base_directory)
	{
		$base_dir = $base_directory;
	}
	else
	{
		$base_dir = $source_dir;
	}

	if ($gcov_version < $GCOV_VERSION_3_4_0)
	{
		if (is_compat($COMPAT_MODE_HAMMER))
		{
			($instr, $graph) = read_bbg($graph_filename);
		}
		else
		{
			($instr, $graph) = read_bb($graph_filename);
		}
	} 
	else
	{
		($instr, $graph) = read_gcno($graph_filename);
	}

	# Try to find base directory automatically if requested by user
	if ($rc_auto_base) {
		$base_dir = find_base_from_graph($base_dir, $instr, $graph);
	}

	($instr, $graph) = adjust_graph_filenames($base_dir, $instr, $graph);

	if (!$no_markers) {
		# Apply exclusion marker data to graph file data
		($instr, $graph) = apply_exclusion_data($instr, $graph);
	}

	# Check whether we're writing to a single file
	if ($output_filename)
	{
		if ($output_filename eq "-")
		{
			*INFO_HANDLE = *STDOUT;
		}
		else
		{
			# Append to output file
			open(INFO_HANDLE, ">>", $output_filename)
				or die("ERROR: cannot write to ".
				       "$output_filename!\n");
		}
	}
	else
	{
		# Open .info file for output
		open(INFO_HANDLE, ">", "$graph_filename.info")
			or die("ERROR: cannot create $graph_filename.info!\n");
	}

	# Write test name
	printf(INFO_HANDLE "TN:%s\n", $test_name);
	foreach $filename (sort(keys(%{$instr})))
	{
		my $funcdata = $graph->{$filename};
		my $line;
		my $linedata;

		print(INFO_HANDLE "SF:$filename\n");

		if (defined($funcdata) && $func_coverage) {
			my @functions = sort {$funcdata->{$a}->[0] <=>
					      $funcdata->{$b}->[0]}
					     keys(%{$funcdata});
			my $func;

			# Gather list of instrumented lines and functions
			foreach $func (@functions) {
				$linedata = $funcdata->{$func};

				# Print function name and starting line
				print(INFO_HANDLE "FN:".$linedata->[0].
				      ",".filter_fn_name($func)."\n");
			}
			# Print zero function coverage data
			foreach $func (@functions) {
				print(INFO_HANDLE "FNDA:0,".
				      filter_fn_name($func)."\n");
			}
			# Print function summary
			print(INFO_HANDLE "FNF:".scalar(@functions)."\n");
			print(INFO_HANDLE "FNH:0\n");
		}
		# Print zero line coverage data
		foreach $line (@{$instr->{$filename}}) {
			print(INFO_HANDLE "DA:$line,0\n");
		}
		# Print line summary
		print(INFO_HANDLE "LF:".scalar(@{$instr->{$filename}})."\n");
		print(INFO_HANDLE "LH:0\n");

		print(INFO_HANDLE "end_of_record\n");
	}
	if (!($output_filename && ($output_filename eq "-")))
	{
		close(INFO_HANDLE);
	}
}

sub filter_fn_name($)
{
	my ($fn) = @_;

	# Remove characters used internally as function name delimiters
	$fn =~ s/[,=]/_/g;

	return $fn;
}

sub warn_handler($)
{
	my ($msg) = @_;

	warn("$tool_name: $msg");
}

sub die_handler($)
{
	my ($msg) = @_;

	die("$tool_name: $msg");
}


#
# graph_error(filename, message)
#
# Print message about error in graph file. If ignore_graph_error is set, return.
# Otherwise abort.
#

sub graph_error($$)
{
	my ($filename, $msg) = @_;

	if ($ignore[$ERROR_GRAPH]) {
		warn("WARNING: $filename: $msg - skipping\n");
		return;
	}
	die("ERROR: $filename: $msg\n");
}

#
# graph_expect(description)
#
# If debug is set to a non-zero value, print the specified description of what
# is expected to be read next from the graph file.
#

sub graph_expect($)
{
	my ($msg) = @_;

	if (!$debug || !defined($msg)) {
		return;
	}

	print(STDERR "DEBUG: expecting $msg\n");
}

#
# graph_read(handle, bytes[, description, peek])
#
# Read and return the specified number of bytes from handle. Return undef
# if the number of bytes could not be read. If PEEK is non-zero, reset
# file position after read.
#

sub graph_read(*$;$$)
{
	my ($handle, $length, $desc, $peek) = @_;
	my $data;
	my $result;
	my $pos;

	graph_expect($desc);
	if ($peek) {
		$pos = tell($handle);
		if ($pos == -1) {
			warn("Could not get current file position: $!\n");
			return undef;
		}
	}
	$result = read($handle, $data, $length);
	if ($debug) {
		my $op = $peek ? "peek" : "read";
		my $ascii = "";
		my $hex = "";
		my $i;

		print(STDERR "DEBUG: $op($length)=$result: ");
		for ($i = 0; $i < length($data); $i++) {
			my $c = substr($data, $i, 1);;
			my $n = ord($c);

			$hex .= sprintf("%02x ", $n);
			if ($n >= 32 && $n <= 127) {
				$ascii .= $c;
			} else {
				$ascii .= ".";
			}
		}
		print(STDERR "$hex |$ascii|");
		print(STDERR "\n");
	}
	if ($peek) {
		if (!seek($handle, $pos, 0)) {
			warn("Could not set file position: $!\n");
			return undef;
		}
	}
	if ($result != $length) {
		return undef;
	}
	return $data;
}

#
# graph_skip(handle, bytes[, description])
#
# Read and discard the specified number of bytes from handle. Return non-zero
# if bytes could be read, zero otherwise.
#

sub graph_skip(*$;$)
{
	my ($handle, $length, $desc) = @_;

	if (defined(graph_read($handle, $length, $desc))) {
		return 1;
	}
	return 0;
}

#
# sort_uniq(list)
#
# Return list in numerically ascending order and without duplicate entries.
#

sub sort_uniq(@)
{
	my (@list) = @_;
	my %hash;

	foreach (@list) {
		$hash{$_} = 1;
	}
	return sort { $a <=> $b } keys(%hash);
}

#
# sort_uniq_lex(list)
#
# Return list in lexically ascending order and without duplicate entries.
#

sub sort_uniq_lex(@)
{
	my (@list) = @_;
	my %hash;

	foreach (@list) {
		$hash{$_} = 1;
	}
	return sort keys(%hash);
}

#
# parent_dir(dir)
#
# Return parent directory for DIR. DIR must not contain relative path
# components.
#

sub parent_dir($)
{
	my ($dir) = @_;
	my ($v, $d, $f) = splitpath($dir, 1);
	my @dirs = splitdir($d);

	pop(@dirs);

	return catpath($v, catdir(@dirs), $f);
}

#
# find_base_from_graph(base_dir, instr, graph)
#
# Try to determine the base directory of the graph file specified by INSTR
# and GRAPH. The base directory is the base for all relative filenames in
# the graph file. It is defined by the current working directory at time
# of compiling the source file.
#
# This function implements a heuristic which relies on the following
# assumptions:
# - all files used for compilation are still present at their location
# - the base directory is either BASE_DIR or one of its parent directories
# - files by the same name are not present in multiple parent directories
#

sub find_base_from_graph($$$)
{
	my ($base_dir, $instr, $graph) = @_;
	my $old_base;
	my $best_miss;
	my $best_base;
	my %rel_files;

	# Determine list of relative paths
	foreach my $filename (keys(%{$instr}), keys(%{$graph})) {
		next if (file_name_is_absolute($filename));

		$rel_files{$filename} = 1;
	}

	# Early exit if there are no relative paths
	return $base_dir if (!%rel_files);

	do {
		my $miss = 0;

		foreach my $filename (keys(%rel_files)) {
			if (!-e solve_relative_path($base_dir, $filename)) {
				$miss++;
			}
		}

		debug("base_dir=$base_dir miss=$miss\n");

		# Exit if we find an exact match with no misses
		return $base_dir if ($miss == 0);

		# No exact match, aim for the one with the least source file
		# misses
		if (!defined($best_base) || $miss < $best_miss) {
			$best_base = $base_dir;
			$best_miss = $miss;
		}

		# Repeat until there's no more parent directory
		$old_base = $base_dir;
		$base_dir = parent_dir($base_dir);
	} while ($old_base ne $base_dir);

	return $best_base;
}

#
# adjust_graph_filenames(base_dir, instr, graph)
#
# Make relative paths in INSTR and GRAPH absolute and apply
# geninfo_adjust_src_path setting to graph file data.
#

sub adjust_graph_filenames($$$)
{
	my ($base_dir, $instr, $graph) = @_;

	foreach my $filename (keys(%{$instr})) {
		my $old_filename = $filename;

		# Convert to absolute canonical form
		$filename = solve_relative_path($base_dir, $filename);

		# Apply adjustment
		if (defined($adjust_src_pattern)) {
			$filename =~ s/$adjust_src_pattern/$adjust_src_replace/g;
		}

		if ($filename ne $old_filename) {
			$instr->{$filename} = delete($instr->{$old_filename});
		}
	}

	foreach my $filename (keys(%{$graph})) {
		my $old_filename = $filename;

		# Make absolute
		# Convert to absolute canonical form
		$filename = solve_relative_path($base_dir, $filename);

		# Apply adjustment
		if (defined($adjust_src_pattern)) {
			$filename =~ s/$adjust_src_pattern/$adjust_src_replace/g;
		}

		if ($filename ne $old_filename) {
			$graph->{$filename} = delete($graph->{$old_filename});
		}
	}

	return ($instr, $graph);
}

#
# graph_cleanup(graph)
#
# Remove entries for functions with no lines. Remove duplicate line numbers.
# Sort list of line numbers numerically ascending.
#

sub graph_cleanup($)
{
	my ($graph) = @_;
	my $filename;

	foreach $filename (keys(%{$graph})) {
		my $per_file = $graph->{$filename};
		my $function;

		foreach $function (keys(%{$per_file})) {
			my $lines = $per_file->{$function};

			if (scalar(@$lines) == 0) {
				# Remove empty function
				delete($per_file->{$function});
				next;
			}
			# Normalize list
			$per_file->{$function} = [ sort_uniq(@$lines) ];
		}
		if (scalar(keys(%{$per_file})) == 0) {
			# Remove empty file
			delete($graph->{$filename});
		}
	}
}

#
# graph_find_base(bb)
#
# Try to identify the filename which is the base source file for the
# specified bb data.
#

sub graph_find_base($)
{
	my ($bb) = @_;
	my %file_count;
	my $basefile;
	my $file;
	my $func;
	my $filedata;
	my $count;
	my $num;

	# Identify base name for this bb data.
	foreach $func (keys(%{$bb})) {
		$filedata = $bb->{$func};

		foreach $file (keys(%{$filedata})) {
			$count = $file_count{$file};

			# Count file occurrence
			$file_count{$file} = defined($count) ? $count + 1 : 1;
		}
	}
	$count = 0;
	$num = 0;
	foreach $file (keys(%file_count)) {
		if ($file_count{$file} > $count) {
			# The file that contains code for the most functions
			# is likely the base file
			$count = $file_count{$file};
			$num = 1;
			$basefile = $file;
		} elsif ($file_count{$file} == $count) {
			# If more than one file could be the basefile, we
			# don't have a basefile
			$basefile = undef;
		}
	}

	return $basefile;
}

#
# graph_from_bb(bb, fileorder, bb_filename)
#
# Convert data from bb to the graph format and list of instrumented lines.
# Returns (instr, graph).
#
# bb         : function name -> file data
#            : undef -> file order
# file data  : filename -> line data
# line data  : [ line1, line2, ... ]
#
# file order : function name -> [ filename1, filename2, ... ]
#
# graph         : file name -> function data
# function data : function name -> line data
# line data     : [ line1, line2, ... ]
#
# instr     : filename -> line data
# line data : [ line1, line2, ... ]
#

sub graph_from_bb($$$)
{
	my ($bb, $fileorder, $bb_filename) = @_;
	my $graph = {};
	my $instr = {};
	my $basefile;
	my $file;
	my $func;
	my $filedata;
	my $linedata;
	my $order;

	$basefile = graph_find_base($bb);
	# Create graph structure
	foreach $func (keys(%{$bb})) {
		$filedata = $bb->{$func};
		$order = $fileorder->{$func};

		# Account for lines in functions
		if (defined($basefile) && defined($filedata->{$basefile})) {
			# If the basefile contributes to this function,
			# account this function to the basefile.
			$graph->{$basefile}->{$func} = $filedata->{$basefile};
		} else {
			# If the basefile does not contribute to this function,
			# account this function to the first file contributing
			# lines.
			$graph->{$order->[0]}->{$func} =
				$filedata->{$order->[0]};
		}

		foreach $file (keys(%{$filedata})) {
			# Account for instrumented lines
			$linedata = $filedata->{$file};
			push(@{$instr->{$file}}, @$linedata);
		}
	}
	# Clean up array of instrumented lines
	foreach $file (keys(%{$instr})) {
		$instr->{$file} = [ sort_uniq(@{$instr->{$file}}) ];
	}

	return ($instr, $graph);
}

#
# graph_add_order(fileorder, function, filename)
#
# Add an entry for filename to the fileorder data set for function.
#

sub graph_add_order($$$)
{
	my ($fileorder, $function, $filename) = @_;
	my $item;
	my $list;

	$list = $fileorder->{$function};
	foreach $item (@$list) {
		if ($item eq $filename) {
			return;
		}
	}
	push(@$list, $filename);
	$fileorder->{$function} = $list;
}

#
# read_bb_word(handle[, description])
#
# Read and return a word in .bb format from handle.
#

sub read_bb_word(*;$)
{
	my ($handle, $desc) = @_;

	return graph_read($handle, 4, $desc);
}

#
# read_bb_value(handle[, description])
#
# Read a word in .bb format from handle and return the word and its integer
# value.
#

sub read_bb_value(*;$)
{
	my ($handle, $desc) = @_;
	my $word;

	$word = read_bb_word($handle, $desc);
	return undef if (!defined($word));

	return ($word, unpack("V", $word));
}

#
# read_bb_string(handle, delimiter)
#
# Read and return a string in .bb format from handle up to the specified
# delimiter value.
#

sub read_bb_string(*$)
{
	my ($handle, $delimiter) = @_;
	my $word;
	my $value;
	my $string = "";

	graph_expect("string");
	do {
		($word, $value) = read_bb_value($handle, "string or delimiter");
		return undef if (!defined($value));
		if ($value != $delimiter) {
			$string .= $word;
		}
	} while ($value != $delimiter);
	$string =~ s/\0//g;

	return $string;
}

#
# read_bb(filename)
#
# Read the contents of the specified .bb file and return (instr, graph), where:
#
#   instr     : filename -> line data
#   line data : [ line1, line2, ... ]
#
#   graph     :     filename -> file_data
#   file_data : function name -> line_data
#   line_data : [ line1, line2, ... ]
#
# See the gcov info pages of gcc 2.95 for a description of the .bb file format.
#

sub read_bb($)
{
	my ($bb_filename) = @_;
	my $minus_one = 0x80000001;
	my $minus_two = 0x80000002;
	my $value;
	my $filename;
	my $function;
	my $bb = {};
	my $fileorder = {};
	my $instr;
	my $graph;
	local *HANDLE;

	open(HANDLE, "<", $bb_filename) or goto open_error;
	binmode(HANDLE);
	while (!eof(HANDLE)) {
		$value = read_bb_value(*HANDLE, "data word");
		goto incomplete if (!defined($value));
		if ($value == $minus_one) {
			# Source file name
			graph_expect("filename");
			$filename = read_bb_string(*HANDLE, $minus_one);
			goto incomplete if (!defined($filename));
		} elsif ($value == $minus_two) {
			# Function name
			graph_expect("function name");
			$function = read_bb_string(*HANDLE, $minus_two);
			goto incomplete if (!defined($function));
		} elsif ($value > 0) {
			# Line number
			if (!defined($filename) || !defined($function)) {
				warn("WARNING: unassigned line number ".
				     "$value\n");
				next;
			}
			push(@{$bb->{$function}->{$filename}}, $value);
			graph_add_order($fileorder, $function, $filename);
		}
	}
	close(HANDLE);
	($instr, $graph) = graph_from_bb($bb, $fileorder, $bb_filename);
	graph_cleanup($graph);

	return ($instr, $graph);

open_error:
	graph_error($bb_filename, "could not open file");
	return undef;
incomplete:
	graph_error($bb_filename, "reached unexpected end of file");
	return undef;
}

#
# read_bbg_word(handle[, description])
#
# Read and return a word in .bbg format.
#

sub read_bbg_word(*;$)
{
	my ($handle, $desc) = @_;

	return graph_read($handle, 4, $desc);
}

#
# read_bbg_value(handle[, description])
#
# Read a word in .bbg format from handle and return its integer value.
#

sub read_bbg_value(*;$)
{
	my ($handle, $desc) = @_;
	my $word;

	$word = read_bbg_word($handle, $desc);
	return undef if (!defined($word));

	return unpack("N", $word);
}

#
# read_bbg_string(handle)
#
# Read and return a string in .bbg format.
#

sub read_bbg_string(*)
{
	my ($handle, $desc) = @_;
	my $length;
	my $string;

	graph_expect("string");
	# Read string length
	$length = read_bbg_value($handle, "string length");
	return undef if (!defined($length));
	if ($length == 0) {
		return "";
	}
	# Read string
	$string = graph_read($handle, $length, "string");
	return undef if (!defined($string));
	# Skip padding
	graph_skip($handle, 4 - $length % 4, "string padding") or return undef;

	return $string;
}

#
# read_bbg_lines_record(handle, bbg_filename, bb, fileorder, filename,
#                       function)
#
# Read a bbg format lines record from handle and add the relevant data to
# bb and fileorder. Return filename on success, undef on error.
#

sub read_bbg_lines_record(*$$$$$)
{
	my ($handle, $bbg_filename, $bb, $fileorder, $filename, $function) = @_;
	my $string;
	my $lineno;

	graph_expect("lines record");
	# Skip basic block index
	graph_skip($handle, 4, "basic block index") or return undef;
	while (1) {
		# Read line number
		$lineno = read_bbg_value($handle, "line number");
		return undef if (!defined($lineno));
		if ($lineno == 0) {
			# Got a marker for a new filename
			graph_expect("filename");
			$string = read_bbg_string($handle);
			return undef if (!defined($string));
			# Check for end of record
			if ($string eq "") {
				return $filename;
			}
			$filename = $string;
			if (!exists($bb->{$function}->{$filename})) {
				$bb->{$function}->{$filename} = [];
			}
			next;
		}
		# Got an actual line number
		if (!defined($filename)) {
			warn("WARNING: unassigned line number in ".
			     "$bbg_filename\n");
			next;
		}
		push(@{$bb->{$function}->{$filename}}, $lineno);
		graph_add_order($fileorder, $function, $filename);
	}
}

#
# read_bbg(filename)
#
# Read the contents of the specified .bbg file and return the following mapping:
#   graph:     filename -> file_data
#   file_data: function name -> line_data
#   line_data: [ line1, line2, ... ]
#
# See the gcov-io.h file in the SLES 9 gcc 3.3.3 source code for a description
# of the .bbg format.
#

sub read_bbg($)
{
	my ($bbg_filename) = @_;
	my $file_magic = 0x67626267;
	my $tag_function = 0x01000000;
	my $tag_lines = 0x01450000;
	my $word;
	my $tag;
	my $length;
	my $function;
	my $filename;
	my $bb = {};
	my $fileorder = {};
	my $instr;
	my $graph;
	local *HANDLE;

	open(HANDLE, "<", $bbg_filename) or goto open_error;
	binmode(HANDLE);
	# Read magic
	$word = read_bbg_value(*HANDLE, "file magic");
	goto incomplete if (!defined($word));
	# Check magic
	if ($word != $file_magic) {
		goto magic_error;
	}
	# Skip version
	graph_skip(*HANDLE, 4, "version") or goto incomplete;
	while (!eof(HANDLE)) {
		# Read record tag
		$tag = read_bbg_value(*HANDLE, "record tag");
		goto incomplete if (!defined($tag));
		# Read record length
		$length = read_bbg_value(*HANDLE, "record length");
		goto incomplete if (!defined($tag));
		if ($tag == $tag_function) {
			graph_expect("function record");
			# Read function name
			graph_expect("function name");
			$function = read_bbg_string(*HANDLE);
			goto incomplete if (!defined($function));
			$filename = undef;
			# Skip function checksum
			graph_skip(*HANDLE, 4, "function checksum")
				or goto incomplete;
		} elsif ($tag == $tag_lines) {
			# Read lines record
			$filename = read_bbg_lines_record(HANDLE, $bbg_filename,
					  $bb, $fileorder, $filename,
					  $function);
			goto incomplete if (!defined($filename));
		} else {
			# Skip record contents
			graph_skip(*HANDLE, $length, "unhandled record")
				or goto incomplete;
		}
	}
	close(HANDLE);
	($instr, $graph) = graph_from_bb($bb, $fileorder, $bbg_filename);
	graph_cleanup($graph);

	return ($instr, $graph);

open_error:
	graph_error($bbg_filename, "could not open file");
	return undef;
incomplete:
	graph_error($bbg_filename, "reached unexpected end of file");
	return undef;
magic_error:
	graph_error($bbg_filename, "found unrecognized bbg file magic");
	return undef;
}

#
# read_gcno_word(handle[, description, peek])
#
# Read and return a word in .gcno format.
#

sub read_gcno_word(*;$$)
{
	my ($handle, $desc, $peek) = @_;

	return graph_read($handle, 4, $desc, $peek);
}

#
# read_gcno_value(handle, big_endian[, description, peek])
#
# Read a word in .gcno format from handle and return its integer value
# according to the specified endianness. If PEEK is non-zero, reset file
# position after read.
#

sub read_gcno_value(*$;$$)
{
	my ($handle, $big_endian, $desc, $peek) = @_;
	my $word;
	my $pos;

	$word = read_gcno_word($handle, $desc, $peek);
	return undef if (!defined($word));
	if ($big_endian) {
		return unpack("N", $word);
	} else {
		return unpack("V", $word);
	}
}

#
# read_gcno_string(handle, big_endian)
#
# Read and return a string in .gcno format.
#

sub read_gcno_string(*$)
{
	my ($handle, $big_endian) = @_;
	my $length;
	my $string;

	graph_expect("string");
	# Read string length
	$length = read_gcno_value($handle, $big_endian, "string length");
	return undef if (!defined($length));
	if ($length == 0) {
		return "";
	}
	$length *= 4;
	# Read string
	$string = graph_read($handle, $length, "string and padding");
	return undef if (!defined($string));
	$string =~ s/\0//g;

	return $string;
}

#
# read_gcno_lines_record(handle, gcno_filename, bb, fileorder, filename,
#                        function, big_endian)
#
# Read a gcno format lines record from handle and add the relevant data to
# bb and fileorder. Return filename on success, undef on error.
#

sub read_gcno_lines_record(*$$$$$$)
{
	my ($handle, $gcno_filename, $bb, $fileorder, $filename, $function,
	    $big_endian) = @_;
	my $string;
	my $lineno;

	graph_expect("lines record");
	# Skip basic block index
	graph_skip($handle, 4, "basic block index") or return undef;
	while (1) {
		# Read line number
		$lineno = read_gcno_value($handle, $big_endian, "line number");
		return undef if (!defined($lineno));
		if ($lineno == 0) {
			# Got a marker for a new filename
			graph_expect("filename");
			$string = read_gcno_string($handle, $big_endian);
			return undef if (!defined($string));
			# Check for end of record
			if ($string eq "") {
				return $filename;
			}
			$filename = $string;
			if (!exists($bb->{$function}->{$filename})) {
				$bb->{$function}->{$filename} = [];
			}
			next;
		}
		# Got an actual line number
		if (!defined($filename)) {
			warn("WARNING: unassigned line number in ".
			     "$gcno_filename\n");
			next;
		}
		# Add to list
		push(@{$bb->{$function}->{$filename}}, $lineno);
		graph_add_order($fileorder, $function, $filename);
	}
}

#
# determine_gcno_split_crc(handle, big_endian, rec_length)
#
# Determine if HANDLE refers to a .gcno file with a split checksum function
# record format. Return non-zero in case of split checksum format, zero
# otherwise, undef in case of read error.
#

sub determine_gcno_split_crc($$$)
{
	my ($handle, $big_endian, $rec_length) = @_;
	my $strlen;
	my $overlong_string;

	return 1 if ($gcov_version >= $GCOV_VERSION_4_7_0);
	return 1 if (is_compat($COMPAT_MODE_SPLIT_CRC));

	# Heuristic:
	# Decide format based on contents of next word in record:
	# - pre-gcc 4.7
	#   This is the function name length / 4 which should be
	#   less than the remaining record length
	# - gcc 4.7
	#   This is a checksum, likely with high-order bits set,
	#   resulting in a large number
	$strlen = read_gcno_value($handle, $big_endian, undef, 1);
	return undef if (!defined($strlen));
	$overlong_string = 1 if ($strlen * 4 >= $rec_length - 12);

	if ($overlong_string) {
		if (is_compat_auto($COMPAT_MODE_SPLIT_CRC)) {
			info("Auto-detected compatibility mode for split ".
			     "checksum .gcno file format\n");

			return 1;
		} else {
			# Sanity check
			warn("Found overlong string in function record: ".
			     "try '--compat split_crc'\n");
		}
	}

	return 0;
}

#
# read_gcno_function_record(handle, graph, big_endian, rec_length)
#
# Read a gcno format function record from handle and add the relevant data
# to graph. Return (filename, function) on success, undef on error. 
#

sub read_gcno_function_record(*$$$$)
{
	my ($handle, $bb, $fileorder, $big_endian, $rec_length) = @_;
	my $filename;
	my $function;
	my $lineno;
	my $lines;

	graph_expect("function record");
	# Skip ident and checksum
	graph_skip($handle, 8, "function ident and checksum") or return undef;
	# Determine if this is a function record with split checksums
	if (!defined($gcno_split_crc)) {
		$gcno_split_crc = determine_gcno_split_crc($handle, $big_endian,
							   $rec_length);
		return undef if (!defined($gcno_split_crc));
	}
	# Skip cfg checksum word in case of split checksums
	graph_skip($handle, 4, "function cfg checksum") if ($gcno_split_crc);
	# Read function name
	graph_expect("function name");
	$function = read_gcno_string($handle, $big_endian);
	return undef if (!defined($function));
	# Read filename
	graph_expect("filename");
	$filename = read_gcno_string($handle, $big_endian);
	return undef if (!defined($filename));
	# Read first line number
	$lineno = read_gcno_value($handle, $big_endian, "initial line number");
	return undef if (!defined($lineno));
	# Add to list
	push(@{$bb->{$function}->{$filename}}, $lineno);
	graph_add_order($fileorder, $function, $filename);

	return ($filename, $function);
}

#
# read_gcno(filename)
#
# Read the contents of the specified .gcno file and return the following
# mapping:
#   graph:    filename -> file_data
#   file_data: function name -> line_data
#   line_data: [ line1, line2, ... ]
#
# See the gcov-io.h file in the gcc 3.3 source code for a description of
# the .gcno format.
#

sub read_gcno($)
{
	my ($gcno_filename) = @_;
	my $file_magic = 0x67636e6f;
	my $tag_function = 0x01000000;
	my $tag_lines = 0x01450000;
	my $big_endian;
	my $word;
	my $tag;
	my $length;
	my $filename;
	my $function;
	my $bb = {};
	my $fileorder = {};
	my $instr;
	my $graph;
	local *HANDLE;

	open(HANDLE, "<", $gcno_filename) or goto open_error;
	binmode(HANDLE);
	# Read magic
	$word = read_gcno_word(*HANDLE, "file magic");
	goto incomplete if (!defined($word));
	# Determine file endianness
	if (unpack("N", $word) == $file_magic) {
		$big_endian = 1;
	} elsif (unpack("V", $word) == $file_magic) {
		$big_endian = 0;
	} else {
		goto magic_error;
	}
	# Skip version and stamp
	graph_skip(*HANDLE, 8, "version and stamp") or goto incomplete;
	while (!eof(HANDLE)) {
		my $next_pos;
		my $curr_pos;

		# Read record tag
		$tag = read_gcno_value(*HANDLE, $big_endian, "record tag");
		goto incomplete if (!defined($tag));
		# Read record length
		$length = read_gcno_value(*HANDLE, $big_endian,
					  "record length");
		goto incomplete if (!defined($length));
		# Convert length to bytes
		$length *= 4;
		# Calculate start of next record
		$next_pos = tell(HANDLE);
		goto tell_error if ($next_pos == -1);
		$next_pos += $length;
		# Process record
		if ($tag == $tag_function) {
			($filename, $function) = read_gcno_function_record(
				*HANDLE, $bb, $fileorder, $big_endian,
				$length);
			goto incomplete if (!defined($function));
		} elsif ($tag == $tag_lines) {
			# Read lines record
			$filename = read_gcno_lines_record(*HANDLE,
					$gcno_filename, $bb, $fileorder,
					$filename, $function,
					$big_endian);
			goto incomplete if (!defined($filename));
		} else {
			# Skip record contents
			graph_skip(*HANDLE, $length, "unhandled record")
				or goto incomplete;
		}
		# Ensure that we are at the start of the next record
		$curr_pos = tell(HANDLE);
		goto tell_error if ($curr_pos == -1);
		next if ($curr_pos == $next_pos);
		goto record_error if ($curr_pos > $next_pos);
		graph_skip(*HANDLE, $next_pos - $curr_pos,
			   "unhandled record content")
			or goto incomplete;
	}
	close(HANDLE);
	($instr, $graph) = graph_from_bb($bb, $fileorder, $gcno_filename);
	graph_cleanup($graph);

	return ($instr, $graph);

open_error:
	graph_error($gcno_filename, "could not open file");
	return undef;
incomplete:
	graph_error($gcno_filename, "reached unexpected end of file");
	return undef;
magic_error:
	graph_error($gcno_filename, "found unrecognized gcno file magic");
	return undef;
tell_error:
	graph_error($gcno_filename, "could not determine file position");
	return undef;
record_error:
	graph_error($gcno_filename, "found unrecognized record format");
	return undef;
}

sub debug($)
{
	my ($msg) = @_;

	return if (!$debug);
	print(STDERR "DEBUG: $msg");
}

#
# get_gcov_capabilities
#
# Determine the list of available gcov options.
#

sub get_gcov_capabilities()
{
	my $help = `$gcov_tool --help`;
	my %capabilities;

	foreach (split(/\n/, $help)) {
		next if (!/--(\S+)/);
		next if ($1 eq 'help');
		next if ($1 eq 'version');
		next if ($1 eq 'object-directory');

		$capabilities{$1} = 1;
		debug("gcov has capability '$1'\n");
	}

	return \%capabilities;
}

#
# parse_ignore_errors(@ignore_errors)
#
# Parse user input about which errors to ignore.
#

sub parse_ignore_errors(@)
{
	my (@ignore_errors) = @_;
	my @items;
	my $item;

	return if (!@ignore_errors);

	foreach $item (@ignore_errors) {
		$item =~ s/\s//g;
		if ($item =~ /,/) {
			# Split and add comma-separated parameters
			push(@items, split(/,/, $item));
		} else {
			# Add single parameter
			push(@items, $item);
		}
	}
	foreach $item (@items) {
		my $item_id = $ERROR_ID{lc($item)};

		if (!defined($item_id)) {
			die("ERROR: unknown argument for --ignore-errors: ".
			    "$item\n");
		}
		$ignore[$item_id] = 1;
	}
}

#
# is_external(filename)
#
# Determine if a file is located outside of the specified data directories.
#

sub is_external($)
{
	my ($filename) = @_;
	my $dir;

	foreach $dir (@internal_dirs) {
		return 0 if ($filename =~ /^\Q$dir\/\E/);
	}
	return 1;
}

#
# compat_name(mode)
#
# Return the name of compatibility mode MODE.
#

sub compat_name($)
{
	my ($mode) = @_;
	my $name = $COMPAT_MODE_TO_NAME{$mode};

	return $name if (defined($name));

	return "<unknown>";
}

#
# parse_compat_modes(opt)
#
# Determine compatibility mode settings.
#

sub parse_compat_modes($)
{
	my ($opt) = @_;
	my @opt_list;
	my %specified;

	# Initialize with defaults
	%compat_value = %COMPAT_MODE_DEFAULTS;

	# Add old style specifications
	if (defined($opt_compat_libtool)) {
		$compat_value{$COMPAT_MODE_LIBTOOL} =
			$opt_compat_libtool ? $COMPAT_VALUE_ON
					    : $COMPAT_VALUE_OFF;
	}

	# Parse settings
	if (defined($opt)) {
		@opt_list = split(/\s*,\s*/, $opt);
	}
	foreach my $directive (@opt_list) {
		my ($mode, $value);

		# Either
		#   mode=off|on|auto or
		#   mode (implies on)
		if ($directive !~ /^(\w+)=(\w+)$/ &&
		    $directive !~ /^(\w+)$/) {
			die("ERROR: Unknown compatibility mode specification: ".
			    "$directive!\n");
		}
		# Determine mode
		$mode = $COMPAT_NAME_TO_MODE{lc($1)};
		if (!defined($mode)) {
			die("ERROR: Unknown compatibility mode '$1'!\n");
		}
		$specified{$mode} = 1;
		# Determine value
		if (defined($2)) {
			$value = $COMPAT_NAME_TO_VALUE{lc($2)};
			if (!defined($value)) {
				die("ERROR: Unknown compatibility mode ".
				    "value '$2'!\n");
			}
		} else {
			$value = $COMPAT_VALUE_ON;
		}
		$compat_value{$mode} = $value;
	}
	# Perform auto-detection
	foreach my $mode (sort(keys(%compat_value))) {
		my $value = $compat_value{$mode};
		my $is_autodetect = "";
		my $name = compat_name($mode);

		if ($value == $COMPAT_VALUE_AUTO) {
			my $autodetect = $COMPAT_MODE_AUTO{$mode};

			if (!defined($autodetect)) {
				die("ERROR: No auto-detection for ".
				    "mode '$name' available!\n");
			}

			if (ref($autodetect) eq "CODE") {
				$value = &$autodetect();
				$compat_value{$mode} = $value;
				$is_autodetect = " (auto-detected)";
			}
		}

		if ($specified{$mode}) {
			if ($value == $COMPAT_VALUE_ON) {
				info("Enabling compatibility mode ".
				     "'$name'$is_autodetect\n");
			} elsif ($value == $COMPAT_VALUE_OFF) {
				info("Disabling compatibility mode ".
				     "'$name'$is_autodetect\n");
			} else {
				info("Using delayed auto-detection for ".
				     "compatibility mode ".
				     "'$name'\n");
			}
		}
	}
}

sub compat_hammer_autodetect()
{
        if ($gcov_version_string =~ /suse/i && $gcov_version == 0x30303 ||
            $gcov_version_string =~ /mandrake/i && $gcov_version == 0x30302)
	{
		info("Auto-detected compatibility mode for GCC 3.3 (hammer)\n");
		return $COMPAT_VALUE_ON;
	}
	return $COMPAT_VALUE_OFF;
}

#
# is_compat(mode)
#
# Return non-zero if compatibility mode MODE is enabled.
#

sub is_compat($)
{
	my ($mode) = @_;

	return 1 if ($compat_value{$mode} == $COMPAT_VALUE_ON);
	return 0;
}

#
# is_compat_auto(mode)
#
# Return non-zero if compatibility mode MODE is set to auto-detect.
#

sub is_compat_auto($)
{
	my ($mode) = @_;

	return 1 if ($compat_value{$mode} == $COMPAT_VALUE_AUTO);
	return 0;
}