#!/usr/bin/perl -w
#
#   Copyright (c) International Business Machines  Corp., 2002,2007
#
#   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 <Peter.Oberparleiter@de.ibm.com>
#                         IBM Lab Boeblingen
#        based on code by Manoj Iyer <manjo@mail.utexas.edu> and
#                         Megan Bock <mbock@us.ibm.com>
#                         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 Getopt::Long;
use Digest::MD5 qw(md5_base64);


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

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;

our $COMPAT_HAMMER	= "hammer";

our $ERROR_GCOV		= 0;
our $ERROR_SOURCE	= 1;

# 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 get_dir($);
sub read_gcov_header($);
sub read_gcov_file($);
sub read_bb_file($$);
sub read_string(*$);
sub read_gcno_file($$);
sub read_gcno_string(*$);
sub read_hammer_bbg_file($$);
sub read_hammer_bbg_string(*$);
sub unpack_int32($$);
sub info(@);
sub get_gcov_version();
sub system_no_output($@);
sub read_config($);
sub apply_config($);
sub gen_initial_info($);
sub process_graphfile($);
sub warn_handler($);
sub die_handler($);

# Global variables
our $gcov_version;
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 $preserve_paths;
our $compat_libtool;
our $no_compat_libtool;
our $adjust_testname;
our $config;		# Configuration file contents
our $compatibility;	# Compatibility version flag - used to indicate
			# non-standard GCOV data format versions
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 $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;

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

if ($config)
{
	# Copy configuration file 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"	=> \$compat_libtool});

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

# Parse command line options
if (!GetOptions("test-name=s" => \$test_name,
		"output-filename=s" => \$output_filename,
		"checksum" => \$checksum,
		"no-checksum" => \$no_checksum,
		"base-directory=s" => \$base_directory,
		"version" =>\$version,
		"quiet" => \$quiet,
		"help|?" => \$help,
		"follow" => \$follow,
		"compat-libtool" => \$compat_libtool,
		"no-compat-libtool" => \$no_compat_libtool,
		"gcov-tool=s" => \$gcov_tool,
		"ignore-errors=s" => \@ignore_errors,
		"initial|i" => \$initial,
		"no-recursion" => \$no_recursion,
		))
{
	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($no_compat_libtool))
	{
		$compat_libtool = ($no_compat_libtool ? 0 : 1);
		$no_compat_libtool = 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);
}

# 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 libtool compatibility mode
if (defined($compat_libtool))
{
	$compat_libtool = ($compat_libtool? 1 : 0);
}
else
{
	# Default is on
	$compat_libtool = 1;
}

# 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 (@ignore_errors)
{
	my @expanded;
	my $error;

	# Expand comma-separated entries
	foreach (@ignore_errors) {
		if (/,/)
		{
			push(@expanded, split(",", $_));
		}
		else
		{
			push(@expanded, $_);
		}
	}

	foreach (@expanded)
	{
		/^gcov$/ && do { $ignore[$ERROR_GCOV] = 1; next; } ;
		/^source$/ && do { $ignore[$ERROR_SOURCE] = 1; next; };
		die("ERROR: unknown argument for --ignore-errors: $_\n");
	}
}

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

$gcov_version = get_gcov_version();

if ($gcov_version < $GCOV_VERSION_3_4_0)
{
	if (defined($compatibility) && $compatibility eq $COMPAT_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 for availability of --preserve-paths option of gcov
if (`$gcov_tool --help` =~ /--preserve-paths/)
{
	$preserve_paths = "--preserve-paths";
}

# 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;
	}
}

# Do something
if ($initial)
{
	foreach (@data_directory)
	{
		gen_initial_info($_);
	}
}
else
{
	foreach (@data_directory)
	{
		gen_info($_);
	}
}
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)
      --no-recursion                Exlude subdirectories from processing
      --function-coverage           Capture function call counts

For more information see: $lcov_url
END_OF_USAGE
	;
}


#
# 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;

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

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

	# Process all files in list
	foreach (@file_list) { process_dafile($_); }
}


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

sub process_dafile($)
{
	info("Processing %s\n", $_[0]);

	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_content;		# Contents of graph file
	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 $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 @result;
	my $index;
	my $da_renamed;		# If data file is to be renamed
	local *INFO_HANDLE;

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

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

	# avoid files from .libs dirs 	 
	if ($compat_libtool && $da_dir =~ m/(.*)\/\.libs$/) {
		$source_dir = $1;
	} else {
		$source_dir = $da_dir;
	}

	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_filename = $da_dir."/".$da_basename.$graph_file_extension;

	# 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 (defined($compatibility) && $compatibility eq $COMPAT_HAMMER)
		{
			%bb_content = read_hammer_bbg_file($bb_filename,
							   $base_dir);
		}
		else
		{
			%bb_content = read_bb_file($bb_filename, $base_dir);
		}
	} 
	else
	{
		%bb_content = read_gcno_file($bb_filename, $base_dir);
	} 

	# 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");
	}

	# Change to directory containing data files and apply GCOV
        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
	if ($preserve_paths)
	{
		$gcov_error = system_no_output(1, $gcov_tool, $da_filename,
					       "-o", $object_dir,
					       "--preserve-paths",
					       "-b");
	}
	else
	{
		$gcov_error = system_no_output(1, $gcov_tool, $da_filename,
					       "-o", $object_dir,
					       "-b");
	}

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

	# Clean up link
	if ($object_dir ne $da_dir)
	{
		unlink($object_dir."/".$da_basename.$data_file_extension);
	}

	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 = glob("*.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(%bb_content);
	foreach $gcov_file (@gcov_list)
	{
		($source, $object) = read_gcov_header($gcov_file);

		if (defined($source))
		{
			$source = solve_relative_path($base_dir, $source);
		}

		# 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 (defined($source) && ! -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(defined($source) ? $source :
					  $gcov_file, keys(%bb_content));

		# 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);
		@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;
			}
		}

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

		# Write function-related information
		if (defined($bb_content{$source_filename}))
		{
			foreach (split(",",$bb_content{$source_filename}))
			{
				my ($fn, $line) = split("=", $_);

				if ($fn eq "") {
					next;
				}

				# Normalize function name
				$fn =~ s/\W/_/g;

				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)
		{
			printf(INFO_HANDLE "FNDA:%s,%s\n",
				       $gcov_functions[0],
				       $gcov_functions[1]);
				$funcs_found++;
			$funcs_hit++ if $gcov_functions[0];
			splice(@gcov_functions,0,2);
		}
		if ($funcs_found > 0) {
			printf(INFO_HANDLE "FNF:%s\n", $funcs_found);
			printf(INFO_HANDLE "FNH:%s\n", $funcs_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);
		}

		#--
		#-- BA: <code-line>, <branch-coverage>
		#--
		#-- print one BA line for every branch of a
		#-- conditional.  <branch-coverage> values
		#-- are:
		#--     0 - not executed
		#--     1 - executed but not taken
		#--     2 - executed and taken
		#--
		while (@gcov_branches)
		{
			if ($gcov_branches[0])
			{
				printf(INFO_HANDLE "BA:%s,%s\n",
				       $gcov_branches[0],
				       $gcov_branches[1]);
			}
			splice(@gcov_branches,0,2);
		}

		# 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 $result;

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

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

	# Remove .
	$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 = shift;
	my @list = @_;
	my @result;

	$filename =~ s/^(.*).gcov$/$1/;

	if ($filename =~ /^\/(.*)$/)
	{
		$filename = "$1";
	}

	foreach (@list)
	{
		if (/\/\Q$filename\E(.*)$/ && $1 eq "")
		{
			@result = (@result, $_);
		}
	}
	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;

			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);
}


#
# get_dir(filename);
#
# Return the directory component of a given FILENAME.
#

sub get_dir($)
{
	my @components = split("/", $_[0]);
	pop(@components);

	return join("/", @components);
}


#
# 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($_);

		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);
}


#
# 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 list of 2 elements
# (linenumber, branch result) for each branch
#
# 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;
	local *INPUT;

	open(INPUT, $filename)
		or 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($_);

			if (/^\t\t(.*)$/)
			{
				# Uninstrumented line
				push(@result, 0);
				push(@result, 0);
				push(@result, $1);
			}
			elsif (/^branch/)
			{
				# Branch execution data
				push(@branches, scalar(@result) / 3);
				if (/^branch \d+ never executed$/)
				{
					push(@branches, 0);
				}
				elsif (/^branch \d+ taken = 0%/)
				{
					push(@branches, 1);
				}
				else
				{
					push(@branches, 2);
				}
			}
			elsif (/^call/ || /^function/)
			{
				# Function call return data
			}
			else
			{
				# Source code execution data
				$number = (split(" ",substr($_, 0, 16)))[0];

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

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

			if (/^branch\s+\d+\s+(\S+)\s+(\S+)/)
			{
				# Branch execution data
				push(@branches, scalar(@result) / 3);
				if ($1 eq "never")
				{
					push(@branches, 0);
				}
				elsif ($2 eq "0%")
				{
					push(@branches, 1);
				}
				else
				{
					push(@branches, 2);
				}
			}
			elsif (/^function\s+(\S+)\s+called\s+(\d+)/)
			{
				push(@functions, $2, $1);
			}
			elsif (/^call/)
			{
				# Function call return data
			}
			elsif (/^\s*([^:]+):\s*([^:]+):(.*)$/)
			{
				# <exec count>:<line number>:<source code>
				if ($2 eq "0")
				{
					# Extra data
				}
				elsif ($1 eq "-")
				{
					# Uninstrumented line
					push(@result, 0);
					push(@result, 0);
					push(@result, $3);
				}
				else
				{
					# Source code execution data
					$number = $1;

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

					push(@result, 1);
					push(@result, $number);
					push(@result, $3);
				}
			}
		}
	}

	close(INPUT);
	return(\@result, \@branches, \@functions);
}


#
# read_bb_file(bb_filename, base_dir)
#
# Read .bb file BB_FILENAME and return a hash containing the following
# mapping:
#
#   filename -> comma-separated list of pairs (function name=starting
#               line number) to indicate the starting line of a function or
#               =name to indicate an instrumented line
#
# for each entry in the .bb file. Filenames are absolute, i.e. relative
# filenames are prefixed with BASE_DIR.
#
# Die on error.
#

sub read_bb_file($$)
{
	my $bb_filename		= $_[0];
	my $base_dir = $_[1];
	my %result;
	my $filename;
	my $function_name;
	my $minus_one		= sprintf("%d", 0x80000001);
	my $minus_two		= sprintf("%d", 0x80000002);
	my $value;
	my $packed_word;
	local *INPUT;

	open(INPUT, $bb_filename)
		or die("ERROR: cannot read $bb_filename!\n");

	binmode(INPUT);

	# Read data in words of 4 bytes
	while (read(INPUT, $packed_word, 4) == 4)
	{
		# Decode integer in intel byteorder
		$value = unpack_int32($packed_word, 0);

		# Note: the .bb file format is documented in GCC info pages
		if ($value == $minus_one)
		{
			# Filename follows
			$filename = read_string(*INPUT, $minus_one)
				or die("ERROR: incomplete filename in ".
				       "$bb_filename!\n");

			# Make path absolute
			$filename = solve_relative_path($base_dir, $filename);

			# Insert into hash if not yet present.
			# This is necessary because functions declared as
			# "inline" are not listed as actual functions in
			# .bb files
			if (!$result{$filename})
			{
				$result{$filename}="";
			}
		}
		elsif ($value == $minus_two)
		{
			# Function name follows
			$function_name = read_string(*INPUT, $minus_two)
				 or die("ERROR: incomplete function ".
					"name in $bb_filename!\n");
			$function_name =~ s/\W/_/g;
		}
		elsif ($value > 0)
		{
			if (defined($filename))
			{
				$result{$filename} .=
					($result{$filename} ? "," : "").
					"=$value";
			}
			else
			{
				warn("WARNING: unassigned line".
				     " number in .bb file ".
				     "$bb_filename\n");
			}
			if ($function_name)
			{
				# Got a full entry filename, funcname, lineno
				# Add to resulting hash

				$result{$filename}.=
				  ($result{$filename} ? "," : "").
				  join("=",($function_name,$value));
				undef($function_name);
			}
		}
	}
	close(INPUT);

	if (!scalar(keys(%result)))
	{
		die("ERROR: no data found in $bb_filename!\n");
	}
	return %result;
}


#
# read_string(handle, delimiter);
#
# Read and return a string in 4-byte chunks from HANDLE until DELIMITER
# is found.
#
# Return empty string on error.
#

sub read_string(*$)
{
	my $HANDLE	= $_[0];
	my $delimiter	= $_[1];
	my $string	= "";
	my $packed_word;
	my $value;

	while (read($HANDLE,$packed_word,4) == 4)
	{
		$value = unpack_int32($packed_word, 0);

		if ($value == $delimiter)
		{
			# Remove trailing nil bytes
			$/="\0";
			while (chomp($string)) {};
			$/="\n";
			return($string);
		}

		$string = $string.$packed_word;
	}
	return("");
}


#
# read_gcno_file(bb_filename, base_dir)
#
# Read .gcno file BB_FILENAME and return a hash containing the following
# mapping:
#
#   filename -> comma-separated list of pairs (function name=starting
#               line number) to indicate the starting line of a function or
#               =name to indicate an instrumented line
#
# for each entry in the .gcno file. Filenames are absolute, i.e. relative
# filenames are prefixed with BASE_DIR.
#
# Die on error.
#

sub read_gcno_file($$)
{
	my $gcno_filename	= $_[0];
	my $base_dir = $_[1];
	my %result;
	my $filename;
	my $function_name;
	my $lineno;
	my $length;
	my $value;
	my $endianness;
	my $blocks;
	my $packed_word;
	my $string;
	local *INPUT;

	open(INPUT, $gcno_filename)
		or die("ERROR: cannot read $gcno_filename!\n");

	binmode(INPUT);
	
	read(INPUT, $packed_word, 4) == 4
		or die("ERROR: Invalid gcno file format\n");

	$value = unpack_int32($packed_word, 0);
	$endianness = !($value == $GCNO_FILE_MAGIC);

	unpack_int32($packed_word, $endianness) == $GCNO_FILE_MAGIC
		or die("ERROR: gcno file magic does not match\n");

	seek(INPUT, 8, 1);

	# Read data in words of 4 bytes
	while (read(INPUT, $packed_word, 4) == 4)
	{
		# Decode integer in intel byteorder
		$value = unpack_int32($packed_word, $endianness);

		if ($value == $GCNO_FUNCTION_TAG)
		{
			# skip length, ident and checksum
			seek(INPUT, 12, 1);
			(undef, $function_name) =
				read_gcno_string(*INPUT, $endianness);
			$function_name =~ s/\W/_/g;
			(undef, $filename) =
				read_gcno_string(*INPUT, $endianness);
			$filename = solve_relative_path($base_dir, $filename);

			read(INPUT, $packed_word, 4);
			$lineno = unpack_int32($packed_word, $endianness);

			$result{$filename}.=
			    ($result{$filename} ? "," : "").
				join("=",($function_name,$lineno));
		}
		elsif ($value == $GCNO_LINES_TAG)
		{
			# Check for names of files containing inlined code
			# included in this file
			read(INPUT, $packed_word, 4);
			$length = unpack_int32($packed_word, $endianness);
			if ($length > 0)
			{
				# Block number
				read(INPUT, $packed_word, 4);
				$length--;
			}
			while ($length > 0)
			{
				read(INPUT, $packed_word, 4);
				$lineno = unpack_int32($packed_word,
						       $endianness);
				$length--;
				if ($lineno != 0)
				{
					if (defined($filename))
					{
						$result{$filename} .=
							($result{$filename} ? "," : "").
							"=$lineno";
					}
					else
					{
						warn("WARNING: unassigned line".
						     " number in .gcno file ".
						     "$gcno_filename\n");
					}
					next;
				}
				last if ($length == 0);
				($blocks, $string) =
					read_gcno_string(*INPUT, $endianness);
				if (defined($string))
				{
					$filename = $string;
				}
				if ($blocks > 1)
				{
					$filename = solve_relative_path(
							$base_dir, $filename);
					if (!defined($result{$filename}))
					{
						$result{$filename} = "";
					}
				}
				$length -= $blocks;
			}
		}
		else
		{
			read(INPUT, $packed_word, 4);
			$length = unpack_int32($packed_word, $endianness);
			seek(INPUT, 4 * $length, 1);
		}
	}
	close(INPUT);

	if (!scalar(keys(%result)))
	{
		die("ERROR: no data found in $gcno_filename!\n");
	}
	return %result;
}


#
# read_gcno_string(handle, endianness);
#
# Read a string in 4-byte chunks from HANDLE.
#
# Return (number of 4-byte chunks read, string).
#

sub read_gcno_string(*$)
{
	my $handle		= $_[0];
	my $endianness		= $_[1];
	my $number_of_blocks	= 0;
	my $string		= "";
	my $packed_word;

	read($handle, $packed_word, 4) == 4
		or die("ERROR: reading string\n");

	$number_of_blocks = unpack_int32($packed_word, $endianness);

	if ($number_of_blocks == 0)
	{
		return (1, undef);
	}

	if (read($handle, $packed_word, 4 * $number_of_blocks) !=
	     4 * $number_of_blocks)
	{
		my $msg = "invalid string size ".(4 * $number_of_blocks)." in ".
			  "gcno file at position ".tell($handle)."\n";
		if ($ignore[$ERROR_SOURCE])
		{
			warn("WARNING: $msg");
			return (1, undef);
		}
		else
		{
			die("ERROR: $msg");
		}
	}

	$string = $string . $packed_word;

	# Remove trailing nil bytes
	$/="\0";
	while (chomp($string)) {};
	$/="\n";

	return(1 + $number_of_blocks, $string);
}


#
# read_hammer_bbg_file(bb_filename, base_dir)
#
# Read .bbg file BB_FILENAME and return a hash containing the following
# mapping:
#
#   filename -> comma-separated list of pairs (function name=starting
#               line number) to indicate the starting line of a function or
#               =name to indicate an instrumented line
#
# for each entry in the .bbg file. Filenames are absolute, i.e. relative
# filenames are prefixed with BASE_DIR.
#
# Die on error.
#

sub read_hammer_bbg_file($$)
{
	my $bbg_filename = $_[0];
	my $base_dir = $_[1];
	my %result;
	my $filename;
	my $function_name;
	my $first_line;
	my $lineno;
	my $length;
	my $value;
	my $endianness;
	my $blocks;
	my $packed_word;
	local *INPUT;

	open(INPUT, $bbg_filename)
		or die("ERROR: cannot read $bbg_filename!\n");

	binmode(INPUT);
	
	# Read magic
	read(INPUT, $packed_word, 4) == 4
		or die("ERROR: invalid bbg file format\n");

	$endianness = 1;

	unpack_int32($packed_word, $endianness) == $BBG_FILE_MAGIC
		or die("ERROR: bbg file magic does not match\n");

	# Skip version
	seek(INPUT, 4, 1);

	# Read data in words of 4 bytes
	while (read(INPUT, $packed_word, 4) == 4)
	{
		# Get record tag
		$value = unpack_int32($packed_word, $endianness);

		# Get record length
		read(INPUT, $packed_word, 4);
		$length = unpack_int32($packed_word, $endianness);

		if ($value == $GCNO_FUNCTION_TAG)
		{
			# Get function name
			($value, $function_name) =
				read_hammer_bbg_string(*INPUT, $endianness);
			$function_name =~ s/\W/_/g;
			$filename = undef;
			$first_line = undef;

			seek(INPUT, $length - $value * 4, 1);
		}
		elsif ($value == $GCNO_LINES_TAG)
		{
			# Get linenumber and filename
			# Skip block number
			seek(INPUT, 4, 1);
			$length -= 4;

			while ($length > 0)
			{
				read(INPUT, $packed_word, 4);
				$lineno = unpack_int32($packed_word,
						       $endianness);
				$length -= 4;
				if ($lineno != 0)
				{
					if (!defined($first_line))
					{
						$first_line = $lineno;
					}
					if (defined($filename))
					{
						$result{$filename} .=
							($result{$filename} ? "," : "").
							"=$lineno";
					}
					else
					{
						warn("WARNING: unassigned line".
						     " number in .bbg file ".
						     "$bbg_filename\n");
					}
					next;
				}
				($blocks, $value) =
					read_hammer_bbg_string(
						*INPUT, $endianness);
				# Add all filenames to result list
				if (defined($value))
				{
					$value = solve_relative_path(
							$base_dir, $value);
					if (!defined($result{$value}))
					{
						$result{$value} = undef;
					}
					if (!defined($filename))
					{
						$filename = $value;
					}
				}
				$length -= $blocks * 4;

				# Got a complete data set?
				if (defined($filename) &&
				    defined($first_line) &&
				    defined($function_name))
				{
					# Add it to our result hash
					if (defined($result{$filename}))
					{
						$result{$filename} .=
						",$function_name=$first_line";
					}
					else
					{
						$result{$filename} =
						"$function_name=$first_line";
					}
					$function_name = undef;
					$filename = undef;
					$first_line = undef;
				}
			}
		}
		else
		{
			# Skip other records
			seek(INPUT, $length, 1);
		}
	}
	close(INPUT);

	if (!scalar(keys(%result)))
	{
		die("ERROR: no data found in $bbg_filename!\n");
	}
	return %result;
}


#
# read_hammer_bbg_string(handle, endianness);
#
# Read a string in 4-byte chunks from HANDLE.
#
# Return (number of 4-byte chunks read, string).
#

sub read_hammer_bbg_string(*$)
{
	my $handle		= $_[0];
	my $endianness		= $_[1];
	my $length		= 0;
	my $string		= "";
	my $packed_word;
	my $pad;

	read($handle, $packed_word, 4) == 4
		or die("ERROR: reading string\n");

	$length = unpack_int32($packed_word, $endianness);
	$pad = 4 - $length % 4;

	if ($length == 0)
	{
		return (1, undef);
	}

	read($handle, $string, $length) ==
		$length or die("ERROR: reading string\n");
	seek($handle, $pad, 1);

	return(1 + ($length + $pad) / 4, $string);
}

#
# unpack_int32(word, endianness)
#
# Interpret 4-byte binary string WORD as signed 32 bit integer in
# endian encoding defined by ENDIANNESS (0=little, 1=big) and return its
# value.
#

sub unpack_int32($$)
{
	return sprintf("%d", unpack($_[1] ? "N" : "V",$_[0]));
}


#
# 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;
		}
	}
        if ($version_string =~ /suse/i && $result == 0x30303 ||
            $version_string =~ /mandrake/i && $result == 0x30302)
	{
		info("Using compatibility mode for GCC 3.3 (hammer)\n");
		$compatibility = $COMPAT_HAMMER;
	}
	return $result;
}


#
# 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");
 
	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 hash CONFIG contains 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($config->{$_}))
		{
			${$ref->{$_}} = $config->{$_};
		}
	}
}


sub gen_initial_info($)
{
	my $directory = $_[0];
	my @file_list;

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

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

	# Process all files in list
	foreach (@file_list) { process_graphfile($_); }
}

sub process_graphfile($)
{
	my $graph_filename = $_[0];
	my $graph_dir;
	my $graph_basename;
	my $source_dir;
	my $base_dir;
	my %graph_data;
	my $filename;
	local *INFO_HANDLE;

	info("Processing $_[0]\n");

	# 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);

	# avoid files from .libs dirs 	 
	if ($compat_libtool && $graph_dir =~ m/(.*)\/\.libs$/) {
		$source_dir = $1;
	} else {
		$source_dir = $graph_dir;
	}

	# 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 (defined($compatibility) && $compatibility eq $COMPAT_HAMMER)
		{
			%graph_data = read_hammer_bbg_file($graph_filename,
							   $base_dir);
		}
		else
		{
			%graph_data = read_bb_file($graph_filename, $base_dir);
		}
	} 
	else
	{
		%graph_data = read_gcno_file($graph_filename, $base_dir);
	}

	# 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 (keys(%graph_data))
	{
		my %lines;
		my $count = 0;
		my @functions;

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

		# Write function related data
		foreach (split(",",$graph_data{$filename}))
		{
			my ($fn, $line) = split("=", $_);

			if ($fn eq "")
			{
				$lines{$line} = "";
				next;
			}

			# Normalize function name
			$fn =~ s/\W/_/g;

			print(INFO_HANDLE "FN:$line,$fn\n");
			push(@functions, $fn);
		}
		foreach (@functions) {
			print(INFO_HANDLE "FNDA:$_,0\n");
		}
		print(INFO_HANDLE "FNF:".scalar(@functions)."\n");
		print(INFO_HANDLE "FNH:0\n");

		# Write line related data
		foreach (sort {$a <=> $b } keys(%lines))
		{
			print(INFO_HANDLE "DA:$_,0\n");
			$count++;
		}
		print(INFO_HANDLE "LH:0\n");
		print(INFO_HANDLE "LF:$count\n");
		print(INFO_HANDLE "end_of_record\n");
	}
	if (!($output_filename && ($output_filename eq "-")))
	{
		close(INFO_HANDLE);
	}
}

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

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

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

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