#!/usr/bin/perl
#
#   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, see
#   <http://www.gnu.org/licenses/>.
#
#
# 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)
#   July 2020 /  Henry Cox: henry.cox@mediatek.com
#                Refactor to use common lcovutil package.
#                Add filters to suppress certain line and branch coverpoints
#   Sept 2020 /  Henry Cox:  modify to use common lcov package for coverage
#                data representation.

use strict;
use warnings;
use File::Basename qw(basename dirname fileparse);
use File::Spec::Functions qw /abs2rel catdir file_name_is_absolute splitdir
                              splitpath catpath catfile/;
use File::Temp;
use File::Copy qw(copy move);
use File::Path;
use Cwd qw/abs_path getcwd/;
use Time::HiRes;    # for profiling
use Capture::Tiny;
use Module::Load;
use Storable;

use lib "/usr/lib/lcov";
use lcovutil qw (define_errors parse_ignore_errors
                 $tool_name $tool_dir $lcov_version $lcov_url
                 ignorable_error ignorable_warning is_ignored
                 set_info_callback info init_verbose_flag $verbose
                 debug $debug
                 $br_coverage $func_coverage
                 system_no_output $devnull $dirseparator
                 die_handler warn_handler
                 parse_cov_filters summarize_cov_filters
                 $EXCL_START $EXCL_STOP $EXCL_BR_START $EXCL_BR_STOP
                 $EXCL_EXCEPTION_BR_START $EXCL_EXCEPTION_BR_STOP
                 $EXCL_LINE $EXCL_BR_LINE $EXCL_EXCEPTION_LINE

                 @exclude_file_patterns @include_file_patterns %excluded_files
                 warn_file_patterns
                 @extractVersionScript

                 %geninfoErrs $ERROR_GCOV $ERROR_SOURCE $ERROR_GRAPH $ERROR_PACKAGE
                 $ERROR_EMPTY $ERROR_PARALLEL $ERROR_UNSUPPORTED
                 report_parallel_error
                 is_external @internal_dirs
                 parseOptions
                 $maxParallelism init_parallel_params $maxMemory
                );

if ($^O eq "msys") {
    require File::Spec::Win32;
}

# Constants
our @gcov_tool;

our $GCOV_VERSION_8_0_0 = 0x80000;
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
lcovutil::define_errors(\%lcovutil::geninfoErrs);

# 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 $UNNAMED_BLOCK = -1;

our $trace_data;

# 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 my_info(@);
set_info_callback(\&my_info);
sub process_intermediate($$$$);
sub map_llvm_version($);
sub version_to_str($);
sub get_gcov_version();
sub apply_exclusion_data($$$);
sub process_graphfile($$);
sub filter_fn_name($$);
sub graph_error($$);
sub graph_read(*$;$$);
sub graph_skip(*$;$);
sub uniq(@);
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 int_handler();
sub compat_name($);
sub parse_compat_modes($);
sub is_compat($);
sub is_compat_auto($);
sub which($);

# Global variables
our $gcov_version;
our $gcov_version_string;
our $graph_file_extension;
our $data_file_extension;
our @data_directory;
# root of directory where .gcno files can be found -directory name,
# if .gcda and .gcno not in same place
our @build_directory;
our $test_name = "";
our $help;
our $output_filename;
our $single_file;          # Write result into single merged file or not
our $files_created = 0;    # Number of output files created
our $base_directory;
our $version;
our $follow;
our $opt_compat_libtool;
our $opt_no_compat_libtool;
our $rc_adjust_src_path;    # Regexp specifying parts to remove from source path
our $adjust_testname;
our @ignore_errors;         # List of errors to ignore (parameter)
our @opt_filter;            # list of coverpoints to ignore (parameter)
our $initial;
our $no_recursion = 0;
our $maxdepth;
our $no_markers           = 0;
our $opt_derive_func_data = 0;
our $opt_external;
our $gcov_caps;
our @opt_config_file;
our $opt_gcov_all_blocks          = 1;
our $opt_adjust_unexecuted_blocks = 0;
our $opt_compat;
our %compat_value;
our $gcno_split_crc;
our $rc_auto_base    = 1;
our $rc_intermediate = "auto";
our $intermediate;

our $cwd = getcwd();
chomp($cwd);

lcovutil::save_cmd_line(\@ARGV, "/usr/bin");

#
# Code entry point
#

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

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

# retrieve settings from RC file - use these if not overridden on command line

my %geninfo_rc_opts = (
          "geninfo_gcov_tool"           => \@gcov_tool,
          "geninfo_adjust_testname"     => \$adjust_testname,
          "geninfo_checksum"            => \$lcovutil::verify_checksum,
          "geninfo_compat_libtool"      => \$opt_compat_libtool,
          "geninfo_external"            => \$opt_external,
          "geninfo_gcov_all_blocks"     => \$opt_gcov_all_blocks,
          "geninfo_unexecuted_blocks"   => \$opt_adjust_unexecuted_blocks,
          "geninfo_compat"              => \$opt_compat,
          "geninfo_adjust_src_path"     => \$rc_adjust_src_path,
          "geninfo_auto_base"           => \$rc_auto_base,
          "geninfo_intermediate"        => \$rc_intermediate,
          "geninfo_no_exception_branch" => \$lcovutil::exclude_exception_branch,
);

my %geninfo_opts = ("test-name|t=s"       => \$test_name,
                    "output-filename|o=s" => \$output_filename,
                    "base-directory|b=s"  => \$base_directory,
                    "build-directory|b=s" => \@build_directory,
                    "follow|f"            => \$follow,
                    "compat-libtool"      => \$opt_compat_libtool,
                    "no-compat-libtool"   => \$opt_no_compat_libtool,
                    "gcov-tool=s"         => \@gcov_tool,
                    "initial|i"           => \$initial,
                    "no-recursion"        => \$no_recursion,
                    "no-markers"          => \$no_markers,
                    "derive-func-data"    => \$opt_derive_func_data,
                    "external|e"          => \$opt_external,
                    "no-external"         => \$lcovutil::opt_no_external,
                    "compat=s"            => \$opt_compat,);

# Parse command line options
if (!lcovutil::parseOptions(\%geninfo_rc_opts, \%geninfo_opts)) {
    print(STDERR "Use $tool_name --help to get usage information\n");
    exit(1);
}

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

        chomp($msg);
        $msg =~ s/at \(eval.*$//;
        warn("WARNING: invalid pattern in geninfo_adjust_src_path: $msg\n");

    } else {
        if (!defined($replace)) {
            # If no replacement is specified, simply remove pattern
            $replace = "";
        }
        my $p = 's#$pattern#$replace#g';
        push(@lcovutil::file_subst_patterns,
             [lcovutil::transform_pattern($p), $p, 0]);
    }
}

if (defined($lcovutil::tempdirname)) {
    $lcovutil::tmp_dir = $lcovutil::tempdirname;
    File::Path::make_path($lcovutil::tmp_dir) or
        die("unable to mkdir $lcovutil::tmp_dir: $!")
        unless (-d $lcovutil::tmp_dir);
}

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

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

my $start = Time::HiRes::gettimeofday();

@data_directory = @ARGV;

debug("$lcov_version\n");

if (0 == scalar(@gcov_tool)) {
    # not specified - use gcov by default - expected to be in user's path
    push(@gcov_tool, 'gcov');
} else {
    my $tool = $gcov_tool[0];
    my (undef, $dir, $file) = splitpath($tool);

    if ($dir eq "") {
        $tool = which($tool);
    } elsif (!file_name_is_absolute($tool)) {
        $tool = abs_path($tool);
    }
    if (!-x $tool) {
        die("Error: cannot access gcov tool '$gcov_tool[0]'");
    }
    $gcov_tool[0] = $tool;
    if (scalar(@gcov_tool) > 1) {
        foreach my $e (@gcov_tool) {
            $e = "'$e'" if ($e =~ /\s/);
        }
    }
}
if (scalar(@lcovutil::extractVersionScript) > 1) {
    foreach my $e (@lcovutil::extractVersionScript) {
        $e = "'$e'" if ($e =~ /\s/);
    }
}

# Check gcov tool
if (system_no_output(3, @gcov_tool, "--help") == -1) {
    die("ERROR: failed execution of gcov_tool \"" .
        join(' ', @gcov_tool) . " --help\": $!");
}

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

# Determine intermediate mode
if ($rc_intermediate eq "0") {
    $intermediate = 0;
} elsif ($rc_intermediate eq "1") {
    $intermediate = 1;
} elsif (lc($rc_intermediate) eq "auto") {
    # Use intermediate format if supported by gcov and not conflicting with
    # exception branch exclusion
    $intermediate = (($gcov_caps->{'intermediate-format'} &&
                          !$lcovutil::exclude_exception_branch) ||
                         $gcov_caps->{'json-format'}) ? 1 : 0;
} else {
    die("ERROR: invalid value for geninfo_intermediate: " .
        "'$rc_intermediate'\n");
}

if ($gcov_version >= (9 << 16) &&
    !$intermediate) {
    lcovutil::ignorable_error($ERROR_UNSUPPORTED,
        "geninfo does not support text format for gcov/9 or higher (your version appears to be '$gcov_version_string').\n  Please remove config file entry 'geninfo_intermdediate = 0'."
    );
    $intermediate = 1;
}

if ($intermediate) {
    info("Using intermediate gcov format\n");
    if ($opt_derive_func_data) {
        warn("WARNING: --derive-func-data is not compatible with " .
             "intermediate format - ignoring\n");
        $opt_derive_func_data = 0;
    }
    if ($lcovutil::exclude_exception_branch && !$gcov_caps->{'json-format'}) {
        die("ERROR: excluding exception branches is not compatible with " .
            "text intermediate format\n");
    }
}

if ($lcovutil::exclude_exception_branch &&
    ($gcov_version < $GCOV_VERSION_3_3_0)) {
    die("ERROR: excluding exception branches is not compatible with " .
        "gcov versions older than 3.3\n");
}

# Determine gcov options
push(@gcov_tool, "-b")
    if ($gcov_caps->{'branch-probabilities'} &&
        ($lcovutil::br_coverage ||
         $lcovutil::func_coverage ||
         $opt_adjust_unexecuted_blocks));
push(@gcov_tool, "-c")
    if ($gcov_caps->{'branch-counts'} &&
        $lcovutil::br_coverage);
push(@gcov_tool, "-a")
    if ($gcov_caps->{'all-blocks'} &&
        $opt_gcov_all_blocks   &&
        $lcovutil::br_coverage &&
        !$intermediate);
if ($gcov_caps->{'hash-filenames'}) {
    push(@gcov_tool, "-x");
} else {
    push(@gcov_tool, "-p") if ($gcov_caps->{'preserve-paths'});
}
push(@gcov_tool, '-i') if $intermediate;

# Determine compatibility modes
parse_compat_modes($opt_compat);

push(@opt_filter, "region", "branch_region") unless $no_markers;
parse_cov_filters(@opt_filter);

# 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);
    push(@ReadCurrentSource::source_directories, $base_directory);
}

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

# Determine checksum mode - normalize to boolean
$lcovutil::verify_checksum =
    defined($lcovutil::verify_checksum) && $lcovutil::verify_checksum;

# 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 {
    my @dirs;
    foreach my $pattern (@data_directory) {
        my @glob  = glob($pattern);
        my $count = 0;
        foreach (@glob) {

            stat($_);
            if (!-r _) {
                ignorable_error($ERROR_GCOV, "cannot read $_!");
            } else {
                push(@dirs, $_);
                $count++;
            }
        }
        if (0 == $count) {
            ignorable_error($ERROR_GCOV, "$pattern does not match anything.");
        }
    }
    @data_directory = @dirs;
}

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
$single_file = defined($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 (!file_name_is_absolute($output_filename)) {
        $output_filename = catfile($cwd, $output_filename);
    }
}

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

# Function is_external() requires all internal_dirs to end with a slash
foreach my $dir (@lcovutil::internal_dirs) {
    $dir =~ s#/*$#/#;
}

if ($initial && $lcovutil::br_coverage && !$intermediate) {
    warn("Note: --initial does not generate branch coverage data\n");
}

# where to write parallel child data
my $tempFileDir =
    defined($lcovutil::tempdirname) ? $lcovutil::tempdirname :
    File::Temp->newdir("geninfo_datXXXX",
                       DIR     => $lcovutil::tmp_dir,
                       CLEANUP => !defined($lcovutil::preserve_intermediates));

lcovutil::info("Writing temporary data to $tempFileDir\n");
# Do something
my $processedFiles = 0;
my $exit_code      = 0;
foreach my $entry (@data_directory) {
    my $now = Time::HiRes::gettimeofday();
    eval { $processedFiles += gen_info($entry); };
    my $then = Time::HiRes::gettimeofday();
    $lcovutil::profileData{gen_info}{$entry} = $then - $now;
    if ($@) {
        $exit_code = 1;
        print(STDERR $@);
        last;
    }
}

if (0 == $exit_code) {
    my $now = Time::HiRes::gettimeofday();
    # have to check the loaded input data for exclusion markers because the
    #  data was generated directly from the gcov files - did not go through
    #  TraceFile::load which explicitly checks
    if ($single_file && defined($trace_data)) {
        $trace_data->applyFilters();
        $trace_data->write_info_file($output_filename,
                                     $lcovutil::verify_checksum);
        $files_created++;
    }
    if ($files_created == 0) {
        warn("WARNING: no data generated\n");
    }
    my $then = Time::HiRes::gettimeofday();
    $lcovutil::profileData{emit} = $then - $now;

    lcovutil::info(1, "Processed $processedFiles GCDA files\n");
    info("Finished .info-file creation\n");
    summarize_cov_filters();

    # print warnings
    lcovutil::warn_file_patterns();
}
my $end = Time::HiRes::gettimeofday();
$lcovutil::profileData{total} = $end - $start;

lcovutil::save_profile(
     (defined($output_filename) && '-' ne $output_filename) ? $output_filename :
         "geninfo");

exit($exit_code);

#
# 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 tracefile for each compiler coverage data file
found (.gcda/.gcno). Note that you may specify more than one directory, all of
which are then processed sequentially.

COMMON OPTIONS
  -h, --help                        Print this help, then exit
      --version                     Print version number, then exit
  -v, --verbose                     Increase verbosity level
  -q, --quiet                       Decrease verbosity level (e.g. to turn off
                                    progress messages)
      --debug                       Increase debug verbosity level
      --config-file FILENAME        Specify configuration file location
      --rc SETTING=VALUE            Override configuration file setting
      --ignore-errors ERRORS        Continue after ERRORS (see man page for full
                                    list of errors and their meaning)
      --keep-going                  Do not stop if an error occurs
      --tempdir DIRNAME             Write temporary and intermediate data here
      --preserve                    Keep intermediate files for debugging

OPTIONS
  -i, --initial                     Capture initial zero coverage data
  -t, --test-name NAME              Use test case 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
      --build-directory DIR         Search DIR for .gcno files (if not found
                                    with .gcda)
      --(no-)function-coverage      Enable (disable) function coverage
      --(no)-branch-coverage        Enable (disable) branch coverage
      --(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)
      --keep-going                  Do not stop if error occurs.  Try to
                                    produce a result
      --filter TYPE                 Apply FILTERS to input data (see man page
                                    for full list of filters and their effects)
      --demangle-cpp [PARAM]        Demangle C++ function names
      --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
      --compat MODE=on|off|auto     Set compat MODE (libtool, hammer, split_crc)
      --include PATTERN             Include files matching PATTERN
      --exclude PATTERN             Exclude files matching PATTERN
      --substitute REGEXP           Change source file names according to REGEXP
      --erase-functions REGEXP      Exclude data for functions matching REGEXP
      --omit-lines REGEXP           Ignore data in lines matching REGEXP
      --forget-test-names           Merge data for all tests names
      --version-script SCRIPTNAME   Call script to find revison control version
                                    ID of source file
  -j, --parallel [N]                Use parallel processing with at most N jobs
      --memory MB                   Use at most MB memory in parallel processing
      --profile [FILENAME]          Write performance statistics to FILENAME
                                    (default: OUTPUT_FILENAME.json)

For more information see the geninfo man page.
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 _process_one_file($$$$)
{
    my ($searchdir, $gcda_file, $gcno_file, $pid) = @_;
    # "file" will be .gcno if "$initial" else will be $gcda

    my $name = defined($gcda_file) ? $gcda_file : $gcno_file;
    info("Processing $name%s\n", defined($pid) ? " in child $pid" : "");
    my $now = Time::HiRes::gettimeofday();

    # Process file
    my $trace;
    # multiple gcda files may refer to the same source - so generate the
    #  same 'source.gcda' output file - so they each need a different directory
    #  This is necessary to preserve intermediates, and if we are running
    #  in parallel; we don't want to overwrite and don't want multiple children to
    #  conflict.
    my $tmp = File::Temp->newdir(
                          "geninfo_XXXXX",
                          DIR     => $tempFileDir,
                          CLEANUP => !defined($lcovutil::preserve_intermediates)
    ) if ($intermediate || !defined($initial));

    if ($intermediate) {
        $trace = process_intermediate($searchdir, $gcda_file,
                                      $gcno_file, $tmp->dirname);
    } elsif ($initial) {
        # just read the gcno file and set all the counters to zero
        $trace = process_graphfile($searchdir, $gcno_file);
    } else {
        $trace =
            process_dafile($searchdir, $gcda_file, $gcno_file, $tmp->dirname);
    }
    my $then = Time::HiRes::gettimeofday();
    $lcovutil::profileData{parse}{$searchdir}{$name} = $then - $now;

    if (!$single_file && defined($trace)) {
        # Create one .info file per input file
        $trace->applyFilters();
        $trace->write_info_file(solve_relative_path($cwd, $name . ".info"),
                                $lcovutil::verify_checksum);
        $files_created++;
    }

    return $trace;
}

sub _merge_one_child($$$$$)
{
    my ($child, $children, $type, $tempFileExt, $childstatus) = @_;
    my $start = Time::HiRes::gettimeofday();

    my $tmp = '' . $tempFileDir;
    debug(
        "_merge_one_child: $child $type (parent $$) status $childstatus from $tmp\n"
    );
    my ($directory, $filename, $from) = @{$children->{$child}};
    my $dumped   = "$tmp/dumper_$child";
    my $childLog = "$tmp/geninfo_$child.log";
    my $childErr = "$tmp/geninfo_$child.err";

    foreach my $f ($childLog, $childErr) {
        if (open(RESTORE, "<", $f)) {
            # slurp into a string and eval..
            my $str = do { local $/; <RESTORE> };    # slurp whole thing
            close(RESTORE);
            unlink $f;
            $f = $str;
        } else {
            report_parallel_error('geninfo', "unable to open $f: $!");
        }
    }
    print(STDOUT $childLog)
        if ($childstatus != 0 ||
            $lcovutil::verbose > 1);
    print(STDERR $childErr);
    if (0 == $childstatus && -f $dumped) {
        my $data = Storable::retrieve($dumped);
        if (defined($data)) {
            my ($childInfo, $excluded, $subst_patterns, $profile, $num_files) =
                @$data;
            # HGC:  need to pass back substitutions, etc.
            lcovutil::merge_child_pattern_counts($excluded, $subst_patterns);
            lcovutil::merge_child_profile($profile);

            $files_created += $num_files;

            my $now = Time::HiRes::gettimeofday();
            $lcovutil::profileData{undump}{$directory}{$filename} =
                $now - $start;
            if (defined($childInfo)) {
                if (defined($trace_data)) {
                    $trace_data->append_tracefile($childInfo);
                    my $final = Time::HiRes::gettimeofday();
                    $lcovutil::profileData{append}{$directory}{$filename} =
                        $final - $now;
                } else {
                    $trace_data = $childInfo;
                }
            }
        } else {
            report_parallel_error('geninfo', "unable to deserialize $dumped");
        }
    } elsif ($childstatus > 0) {
        report_parallel_error('geninfo',
            "child $child returned non-zero code $childstatus: ignoring data in $filename"
        );
    }
    foreach my $f ($dumped) {
        unlink $f
            if -f $f;
    }
    my $to = Time::HiRes::gettimeofday();
    $lcovutil::profileData{$type}{$directory}{$filename} = $to - $from;
    $lcovutil::profileData{merge}{$directory}{$filename} = $to - $start;
}

sub gen_info($)
{
    my $directory = $_[0];
    my @file_list;
    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");

        my $now = Time::HiRes::gettimeofday();
        @file_list =
            `find "$directory" $maxdepth $follow -name \\*$ext -type f -o -name \\*$ext -type l 2>$lcovutil::devnull`;
        die("Error return code from 'find \"$directory\" ...': $!")
            if ($?);
        my $then = Time::HiRes::gettimeofday();
        $lcovutil::profileData{find}{$directory} = $then - $now;
        chomp(@file_list);
        if (!@file_list) {
            lcovutil::ignorable_error($ERROR_EMPTY,
                                      "no $ext files found in $directory");
            return 0;
        }
        info("Found %d %s files in %s\n", $#file_list + 1, $type, $directory);
        # keep track of directory where we found the file
        @file_list = map({ [$_, $directory]; } @file_list);
    } else {
        die("no such file or directory '$directory'")
            unless (-e $directory);
        # use the directory where we find the file as the base dir
        @file_list = ([$directory, dirname($directory)]);
    }

    # Process all files in list
    my $currentParallel = 0;
    my %children;
    my $tempFileExt = '';
    $tempFileExt = ".gz"
        if (defined $output_filename) && $output_filename =~ /\.gz$/;
    my $total          = scalar(@file_list);
    my $processedFiles = 0;
    my $start          = Time::HiRes::gettimeofday();
    DATA_FILE: foreach my $fileData (@file_list) {
        #  $filename will be .gcda or .gcno.
        #  expect gcno only if --initial
        my ($filename, $searchdir) = @$fileData;
        lcovutil::info(2, "process $filename\n");
        --$total;
        # find the corresponding gcno file...
        my ($name, $d, $e) = fileparse($filename, qr/\.[^.]*/);
        die("unexpected extension '$e': should be $ext" .
            ($initial ? " for initial capture" : ""))
            unless ($e eq $ext);
        my ($gcda_file, $gcno_file);
        if ($initial) {
            $gcno_file = $filename;
        } else {
            $gcda_file = $filename;
            $gcno_file = File::Spec->catfile($d, $name . ".gcno");
            GCNO: foreach ($gcno_file) {
                last if (-f $gcno_file || -l $gcno_file);

                my $alt = lcovutil::subst_file_name($gcno_file);
                if ($alt ne $gcno_file) {
                    $gcno_file = $alt;
                    lcovutil::info(1,
                        "looking for GCNO corresponding to '$gcda_file' at '$gcno_file'\n"
                    );
                    last GCNO if (-f $gcno_file || -l $gcno_file);
                }

                foreach my $build_directory (@build_directory) {
                    # handle case that gcda and gcno are in different directories
                    #  - say, where is the gcda that we found, then see if gcno is
                    #    in the same directory. If not, then look in a gcno path and
                    #    link both gcda and gcno into this tempdir, run gcov, then
                    #    unlink
                    # from the directory where the gcda file is found:
                    #   strip off the directory where we start the search
                    #   then strip off the GCOV_PREFIX (if there is one)
                    #   then append the remaining path the the GCDA file, to the
                    #   'GCNO_PATH' that we were provided.
                    #     - if there is a file there:  use it.
                    $d =~ s#^$searchdir##g;
                    $gcno_file =
                        File::Spec->catfile($build_directory, $d,
                                            $name . ".gcno");

                    lcovutil::info(1,
                        "looking for GCNO corresponding to '$gcda_file' at '$gcno_file'\n"
                    );
                    last GCNO if (-f $gcno_file || -l $gcno_file);

                    $alt = lcovutil::subst_file_name($gcno_file);
                    if ($alt ne $gcno_file) {
                        $gcno_file = $alt;
                        lcovutil::info(1,
                            "looking for GCNO corresponding to '$gcda_file' at '$gcno_file'\n"
                        );
                        last GCNO if (-f $gcno_file || -l $gcno_file);
                    }
                }    # foreach build_directory

                # skip the .gcda file if there is no .gcno

                lcovutil::ignorable_error($ERROR_GCOV,
                    "skipping .gcda file $gcda_file because corresponding .gcno file '$gcno_file' is missing (see the '--build-directory' entry in 'man geninfo' for suggestions)"
                );
                next DATA_FILE;
            }    # foreach
        }

        ++$processedFiles;
        if (1 < $lcovutil::maxParallelism &&
            scalar(@file_list) > 1) {

            my $currentSize = 0;
            if (0 != $lcovutil::maxMemory) {
                $currentSize = lcovutil::current_process_size();
            }
            while ($currentParallel >= $lcovutil::maxParallelism ||
                   ($currentParallel > 1 &&
                    (($currentParallel + 1) * $currentSize) >
                    $lcovutil::maxMemory)
            ) {
                lcovutil::info(1,
                    "memory constraint ($currentParallel + 1) * $currentSize > $lcovutil::maxMemory violated: waiting.  "
                        . ($total + 1)
                        . " remaining\n")
                    if ((($currentParallel + 1) * $currentSize) >
                        $lcovutil::maxMemory);
                my $child       = wait();
                my $childstatus = $?;
                _merge_one_child($child, \%children, $type, $tempFileExt,
                                 $childstatus);
                --$currentParallel;
            }
            if (0 == ($processedFiles % 200)) {
                my $now     = Time::HiRes::gettimeofday();
                my $elapsed = $now - $start;
                my $rate    = $processedFiles / $elapsed;
                lcovutil::info(
                    "files remaining: $total"
                        .
                        sprintf(
                           " (%0.2f files/s - predict %0.2f minutes remaining)",
                           $rate, ($total) / ($rate * 60)) .
                        "\n");
            }

            my $now      = Time::HiRes::gettimeofday();
            my $pid      = fork();
            my $filename = defined($gcda_file) ? $gcda_file : $gcno_file;
            if (!defined($pid)) {
                # fork failed
                lcovutil::ignorable_error($lcovutil::ERROR_PARALLEL,
                     "fork() syscall failed while trying to process $filename");
                --$processedFiles;
                push(@file_list, $fileData);
                sleep(10);
                next DATA_FILE;
            }
            if (0 == $pid) {
                # I'm the child...
                #   set my output file to temp location so my dump won't
                #   collide with another child - then merge at the end...
                # would be better if the various gcov data readers would
                #   build a datastructure that we could dump - rather than
                #   printing a .info file that we have to parse....but so
                #   be it.
                my $childStart = Time::HiRes::gettimeofday();
                # clear profile - want only my contribution
                %lcovutil::profileData = ();
                my $tmp = '' . $tempFileDir;
                $output_filename = "$tmp/geninfo_$$.info" . $tempFileExt;
                my $stdout_file = "$tmp/geninfo_$$.log";
                my $stderr_file = "$tmp/geninfo_$$.err";
                # using 'capture' here so that we can both capture/redirect geninfo
                #   messages from a child process during parallel execution AND
                #   redirect stdout/stderr from gcov calls.
                # It does not work directly open/reopne the STDOUT and STDERR
                #   descriptors due to interactions between the child and parent
                #   processes (see the Capture::Tiny doc for some details)
                my $childInfo;
                my ($stdout, $stderr, $code) = Capture::Tiny::capture {
                    $trace_data = undef;
                    $childInfo =
                        _process_one_file($searchdir, $gcda_file, $gcno_file,
                                          $$);
                };
                # print stdout and stderr ...
                foreach my $d (['log', $stdout], ['err', $stderr]) {
                    my $f = InOutFile->out("$tmp/geninfo_$$." . $d->[0]);
                    my $h = $f->hdl();
                    print($h $d->[1]);
                }
                my $then  = Time::HiRes::gettimeofday();
                my $dumpf = "$tmp/dumper_$$";
                foreach my $f ($output_filename) {
                    unlink $f if -f $f && !$lcovutil::preserve_intermediates;
                }
                my @excluded = keys %lcovutil::excluded_files;
                my $done     = Time::HiRes::gettimeofday();

                # keep separate timestamp for when this child block was entered
                # vs when fork() was called - lest this job waited in queue for
                # a while
                $lcovutil::profileData{process}{$searchdir}{$filename} =
                    $done - $now;
                $lcovutil::profileData{child}{$searchdir}{$filename} =
                    $done - $childStart;
                # dump parsed data - then read back and merge
                Storable::store([$single_file ? $childInfo : undef,
                                 \@excluded,
                                 \@lcovutil::file_subst_patterns,
                                 \%lcovutil::profileData,
                                 $files_created,
                                ],
                                $dumpf) if defined($childInfo);
                exit(0);
            } else {
                # I'm the parent
                $children{$pid} = [$searchdir, $filename, $now];
                ++$currentParallel;
            }
        } else {
            # not parallel..
            my $now = Time::HiRes::gettimeofday();
            my $fileData =
                _process_one_file($searchdir, $gcda_file, $gcno_file, undef);
            my $then = Time::HiRes::gettimeofday();
            $lcovutil::profileData{process}{$searchdir}{$filename} =
                $then - $now;
            if ($single_file && defined($fileData)) {
                if (defined($trace_data)) {
                    $trace_data->append_tracefile($fileData);
                    my $end = Time::HiRes::gettimeofday();
                    $lcovutil::profileData{merge}{$searchdir}{$filename} =
                        $end - $then;
                } else {
                    $trace_data = $fileData;
                }
            }
            my $end = Time::HiRes::gettimeofday();
            $lcovutil::profileData{$type}{$searchdir}{$filename} = $end - $now;
        }
    }    # end foreach

    while ($currentParallel != 0) {
        my $child       = wait();
        my $childstatus = $?;
        --$currentParallel;
        _merge_one_child($child, \%children, $type, $tempFileExt, $childstatus);
    }
    # Report whether files were excluded.
    if (%lcovutil::excluded_files) {
        my $count = scalar keys %lcovutil::excluded_files;

        info("Excluded data for %d file%s due to include/exclude options\n",
             $count, 1 == $count ? '' : 's');
    }
    return $processedFiles;
}

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

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

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

        $fn_count{$fn} = $count;
    }

    # Convert line coverage data to function data
    foreach my $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
        my $count = 0;
        foreach my $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;
    }

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

    return @gcov_functions;
}

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

sub process_dafile($$$$)
{
    my ($dirname, $gcda_file, $gcno_file, $tempdir) = @_;
    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 $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 $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 $base_dir;           # Base directory for current file

    # Get path to data file in absolute and normalized form (begins with /,
    # contains no more ../ or ./)
    $da_filename = solve_relative_path($cwd, $gcda_file);
    my $gcno_filename = solve_relative_path($cwd, $gcno_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$//;
    }

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

    # Construct name of graph file
    $bb_basename = File::Basename::basename($gcno_file);
    $bb_filename = solve_relative_path($cwd, $gcno_file);

    # 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");
        chdir($cwd) or die("can't cd back to $cwd: $!");
        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_source($base_dir,
                                          [keys(%{$instr}), keys(%{$graph})]);
    }

    adjust_source_filenames($instr, $base_dir);
    adjust_source_filenames($graph, $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);
    my $da_arg = File::Spec->catfile($base_dir, $da_filename);
    # Is the data file in a different directory? (this happens e.g. with
    # the gcov-kernel patch).
    if ($object_dir ne $da_dir) {
        # Use links in tempdir
        $da_arg = File::Basename::basename($da_filename);
        my $gcda = File::Spec->catfile($tempdir, $da_arg);
        symlink($da_filename, $gcda) or
            die("ERROR: cannot create link $gcda: $!\n");
        my $gcno = File::Spec->catfile($tempdir,
                                       File::Basename::basename($bb_filename));
        symlink($bb_filename, $gcno) or
            die("ERROR: cannot create link $gcno: $!\n");
        $object_dir = '.';
    }

    chdir($tempdir) or die("can't cd to $tempdir: $!");
    # Execute gcov command and suppress standard output
    #  also redirect stderr to /dev/null if 'quiet'
    # HGC: what we really want to do is to redirect stdout/stderr
    #  unless verbose - but echo them for non-zero exit status.
    my $now = Time::HiRes::gettimeofday();
    debug("call gcov: " . join(' ', @gcov_tool) . " $da_arg -o $object_dir\n");
    lcovutil::info(2,
                   "process $da_arg (for $base_dir/$da_filename in $tempdir\n");
    my ($out, $err, $code) =
        system_no_output(1 + 2 + 4, @gcov_tool, $da_arg, "-o", $object_dir);
    my $then = Time::HiRes::gettimeofday();
    $lcovutil::profileData{exec}{$dirname}{$gcda_file} = $then - $now;

    if (0 != $code) {
        check_gcov_fail($err, $da_filename);
    }
    print_gcov_warnings('stdout', $out, 0, {})
        if ('' ne $out &&
            (0 != $code ||
             $lcovutil::verbose > 1));
    print_gcov_warnings('stderr', $err, 0, {})
        if ('' ne $err &&
            (0 != $code ||
             $lcovutil::verbose));

    # Change back to initial directory
    debug(2, "chdir back to $cwd\n");
    chdir($cwd) or die("can't cd back to $cwd: $!");
    # Collect data from resulting .gcov files and create .info file
    # this version of gcov wrote the files to "." - but we want to
    # save them in tempdir
    my @gcov_list;
    foreach my $f (glob("$tempdir/*.gcov $tempdir/.*.gcov")) {
        # Skip gcov file for gcc built-in code
        push(@gcov_list, $f) unless ($f eq "<built-in>.gcov");
    }

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

    if ($code) {
        ignorable_error($ERROR_GCOV, "GCOV failed for $da_filename!");
        return;
    }

    my $traceFile = TraceFile->new();
    my $warnedUnexecuted;
    # Traverse the list of generated .gcov files and combine them into a
    # single .info file
    foreach $gcov_file (@gcov_list) {
        GCOV_FILE_LOOP: {
            my $i;
            my $num;
            next unless -f $gcov_file;    # skp if we didn't copy it over

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

            #  apply more patterns here
            $source = lcovutil::subst_file_name($source);

            @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) unless $lcovutil::preserve_intermediates;
                next;
            }

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

            # Skip empty files
            if (!@gcov_content) {
                warn("WARNING: skipping empty file " . $gcov_file . "\n");
                unlink($gcov_file) unless $lcovutil::preserve_intermediates;
                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);
            }
            $source_filename = lcovutil::subst_file_name($source_filename);
            if (TraceFile::skipCurrentFile($source_filename)) {
                $lcovutil::excluded_files{$source_filename} = ();
                unlink($gcov_file) unless $lcovutil::preserve_intermediates;
                next GCOV_FILE_LOOP;
            }

            # Skip external files if requested
            if (is_external($source_filename)) {
                info("  ignoring data for external file $source_filename\n");
                unlink($gcov_file) unless $lcovutil::preserve_intermediates;
                next;
            }

            my $fileData    = $traceFile->data($source_filename);
            my $functionMap = $fileData->testfnc($test_name);
            my $branchMap   = $fileData->testbr($test_name);
            my $lineMap     = $fileData->test($test_name);

            if (@lcovutil::extractVersionScript &&
                -f $source_filename) {
                my $version = lcovutil::extractFileVersion($source_filename);
                $fileData->version($version)
                    if (defined($version) && $version ne "");
            }

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

            # Hold function-related information
            my %functionData;
            if (defined($graph->{$source_filename})) {
                my $fn_data = $graph->{$source_filename};

                while (my ($fn, $ln_data) = each(%$fn_data)) {
                    next if ($fn eq "");
                    my $line = $ln_data->[0];

                    # Normalize function name - need to demangle here because
                    # the graph data came from reading the gcno file and isn't
                    # demangled already
                    $fn = filter_fn_name($fn,
                                         defined($lcovutil::demangle_cpp_cmd));
                    $functionData{$fn} =
                        $functionMap->define_function($fn, $source_filename,
                                                      $line);
                }
            }

            while (@gcov_functions) {
                my $count = shift(@gcov_functions);
                my $fn    = shift(@gcov_functions);

                # don't need to demangle here because this came from reading
                #  the gcov result - and we demangled that.
                $fn = filter_fn_name($fn, 0);
                next unless exists($functionData{$fn});
                $functionData{$fn}->addAlias($fn, $count);
            }

            # Coverage information for each instrumented branch:
            foreach my $line ($branchData->keylist()) {

                my $branchEntry = $branchData->value($line);

                # gcov extraction block numbers can be strange - so
                #  just renumber them.
                my $blockRenumber = 0;
                foreach my $blockId ($branchEntry->blocks()) {
                    my $blockData = $branchEntry->getBlock($blockId);
                    foreach my $br (@$blockData) {
                        $branchMap->append($line, $blockRenumber,
                                           $br, $source_filename);
                    }
                    ++$blockRenumber;
                }
            }    # end for each branch

            # Reset line counters
            $line_number = 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]) {
                    my $hit = $gcov_content[1];
                    # do we have branch data on this line?
                    # if no branch data and this line was marked as
                    # 'unexecuted' - then set its hit count to zero
                    if ('ARRAY' eq ref($hit)) {
                        die("unexpected 'unexec' count") unless $hit->[1] == 1;
                        $hit = $hit->[0];
                        if ($hit != 0 &&
                            !defined($branchMap->value($line_number))) {
                            lcovutil::debug(
                                "$source_filename:$line_number: unexecuted block on non-branch line with count=$hit\n"
                            );
                            if ($opt_adjust_unexecuted_blocks) {
                                $hit = 0;
                            } else {
                                lcovutil::ignorable_warning($ERROR_GCOV,
                                    "$source_filename:$line_number: unexecuted block on non-branch line with non-zero hit count.  Use \"geninfo --rc geninfo_unexecuted_blocks=1 to set count to zero."
                                ) unless $warnedUnexecuted;
                                $warnedUnexecuted = 1;
                            }
                        }
                    }
                    $lineMap->append($line_number, $hit);
                }
                # Remove already processed data from array
                splice(@gcov_content, 0, 3);
            }
            # now go through lines, functions, branches - append to test_name data
            $fileData->sum()->merge($lineMap);
            $fileData->sumbr()->merge($branchMap);
            $fileData->func()->merge($functionMap);

            # Remove .gcov file after processing
            unlink($gcov_file) unless $lcovutil::preserve_intermediates;
        }
    }

    return $traceFile;
}

#
# 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 .
    while ($result =~ s/\/\.\//\//g) {
    }
    $result =~ s/\/\.$/\//g;

    # Remove trailing /
    $result =~ s/\/$//g if ($result ne $lcovutil::dirseparator);

    # Solve ..
    while ($result =~ s/\/[^\/]+\/\.\.\//\//) {
    }
    # change "X/dirname/../Y" into "X/Y"
    while ($result =~ s#\/[^\/.][^\/]+\/\.\.\/#\/#) {
    }
    # change "dirname/../Y" into "./Y" (i.e,, at head of path)
    $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 ($vol, $dir, $name) = File::Spec->splitpath($_[0]);

    my @file_components = split('\.', $name);
    my $extension       = pop(@file_components);

    return ($vol . $dir, 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])) {
        ignorable_error($ERROR_GCOV, "cannot read $_[0]: $!");
        return (undef, undef);
    }

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

#
# read_gcov_file(gcov_filename, gcda_filename, source_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 BranchData instance - see lcovutil.pm
#
# 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, $da_filename, $source_filename) = @_;
    my @result     = ();
    my $branchData = BranchData->new();
    my @functions  = ();
    my $number;
    my $exclude_flag              = 0;
    my $exclude_line              = 0;
    my $exclude_br_flag           = 0;
    my $exclude_exception_br_flag = 0;
    my $exclude_branch            = 0;
    my $exclude_exception_branch  = 0;
    my $last_block                = $UNNAMED_BLOCK;
    my $last_line                 = 0;
    my $branchId;
    my $unexec;

    my $f     = InOutFile->in($filename, $lcovutil::demangle_cpp_cmd);
    my $input = $f->hdl();

    my $currentBlock;
    die("your GCC version is ANCIENT.  Not supported by this version of lcov")
        unless $gcov_version >= $GCOV_VERSION_3_3_0;

    # Expect gcov format as used in gcc >= 3.3
    debug("reading $filename\n");
    # line content "/*EOF*/" occurs when gcov knows the
    # number of lines in the file but can't find the source code -
    # most of the time, that is deliberate because we run gcov in a
    # temp directory in order to avoid conflicts from parallel execution
    # we do a bit of error checking that the content is not inconsistent
    # - e.g., if we find the file due to fully qualified paths, and find
    # a non-EOF line following an EOF line.
    my $foundEOF = 0;
    my ($warnedBranch, $warnedFunction, $warnedLine);
    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
            $branchId   = 0;
            $last_line  = $2;
            $last_block = $3;
        } elsif (/^branch\s+(\d+)\s+taken\s+(\d+)(?:\s+\(([^)]*)\))?/) {
            next
                if (!$lcovutil::br_coverage ||
                    $exclude_line           ||
                    $exclude_branch         ||
                    (($exclude_exception_branch ||
                      $lcovutil::exclude_exception_branch) &&
                     defined($3) &&
                     ($3 eq "throw")));
            # $1 is block ID, $2 is hit count
            my $count = $2;
            if ($count < 0) {
                #negative...
                lcovutil::ignorable_error($lcovutil::ERROR_NEGATIVE,
                    "Unexpected negative count '$count' for branch $1 of $source_filename:$last_line at $filename:$.\n\tPerhaps you need to compile with '-fprofile-update=atomic"
                ) unless $warnedBranch;
                $warnedBranch = 1;    # only once
                $count        = 0;
            }
            my $br = BranchBlock->new($branchId, $count, undef,
                                      defined($3) && $3 eq 'throw');
            $branchData->append($last_line, $last_block, $br, $filename);
            ++$branchId;
        } elsif (/^branch\s+(\d+)\s+never\s+executed/) {
            next
                if (!$lcovutil::br_coverage ||
                    $exclude_line ||
                    $exclude_branch);
            # this branch not taken
            my $br = BranchBlock->new($branchId, '-');
            $branchData->append($last_line, $last_block, $br, $filename);
            ++$branchId;
        } elsif (/^function\s+(.+)\s+called\s+(\d+)\s+/) {
            next if (!$lcovutil::func_coverage || $exclude_line);
            my $name  = $1;
            my $count = $2;
            if ($count < 0) {
                #negative...
                lcovutil::ignorable_error($lcovutil::ERROR_NEGATIVE,
                    "Unexpected negative count '$count' for function $name of $source_filename:$last_line at $filename:$.\n\tPerhaps you need to compile with '-fprofile-update=atomic"
                ) unless $warnedFunction;
                $warnedFunction = 1;
                $count          = 0;
            }
            push(@functions, $count, $name);
        } elsif (/^call/) {
            # Function call return data
        } elsif (/^\s*([^:]+):\s*([^:]+):(.*)$/) {
            my ($count, $line, $code) = ($1, $2, $3);
            # Skip instance-specific counts
            next if ($line <= (scalar(@result) / 3));
            # skip fake line inserted by gcov
            if ($code eq '/*EOF*/') {
                $foundEOF = 1;
            } elsif ($foundEOF) {
                # data looks inconsistent...we started finding some EOF entries
                # and now we found a following entry which claims not to be EOF
                lcovutil::ignorable_error($ERROR_GCOV,
                    "non-EOF for $source_filename:$line at $filename:$. while processing $da_filename: '$code'"
                );
            }
            $branchId   = 0;                # if $last_line != $line;
            $last_line  = $line;
            $last_block = $UNNAMED_BLOCK;
            # Check for exclusion markers
            if (!$no_markers) {
                # universal markers..
                if (/$lcovutil::EXCL_STOP/) {
                    $exclude_flag = 0;
                } elsif (/$lcovutil::EXCL_START/) {
                    $exclude_flag = 1;
                }
                if (/$lcovutil::EXCL_LINE/ || $exclude_flag) {
                    $exclude_line = 1;
                } else {
                    $exclude_line = 0;
                }

                # branch exclusion markers..
                if (/$lcovutil::EXCL_BR_STOP/) {
                    $exclude_br_flag = 0;
                } elsif (/$lcovutil::EXCL_BR_START/) {
                    $exclude_br_flag = 1;
                }
                if (/$lcovutil::EXCL_BR_LINE/ || $exclude_br_flag) {
                    $exclude_branch = 1;
                } else {
                    $exclude_branch = 0;
                }

                # Check for exception branch exclude markers
                if (/$lcovutil::EXCL_EXCEPTION_BR_STOP/) {
                    $exclude_exception_br_flag = 0;
                } elsif (/$lcovutil::EXCL_EXCEPTION_BR_START/) {
                    $exclude_exception_br_flag = 1;
                }
                if (/$lcovutil::EXCL_EXCEPTION_LINE/ ||
                    $exclude_exception_br_flag) {
                    $exclude_exception_branch = 1;
                } else {
                    $exclude_exception_branch = 0;
                }
            }

            # Strip unexecuted basic block marker
            if ($count =~ /^([^*]+)\*$/) {
                # need to do something about lines which have non-zero count
                #  but unexecuted block.  If there are no branches associated
                #  with this line, then we should mark the line as not hit.
                # Otherwise, result is misleading because we can see
                #  (for example) a non-zero hit could for the the 'if' clause
                #  of an untaken branch.
                $unexec = 1;
                $count  = $1;
            } else {
                $unexec = 0;
            }

            # <exec count>:<line number>:<source code>
            if ($line eq "0") {
                # Extra data
            } elsif ($count eq "-") {
                # Uninstrumented line
                push(@result, 0, 0, $code);
            } else {
                if ($exclude_line) {
                    push(@result, 0, 0);
                } else {
                    # Check for zero count
                    if ($count =~ /^[#=]/) {
                        $count = 0;
                    } elsif ($count < 0) {
                        #negative...
                        lcovutil::ignorable_error($lcovutil::ERROR_NEGATIVE,
                            "Unexpected negative count '$count' for $source_filename:$line at $filename:$.\n\tPerhaps you need to compile with '-fprofile-update=atomic"
                        ) unless $warnedLine;
                        $warnedLine = 1;
                        $count      = 0;
                    }
                    if ($unexec) {
                        push(@result, 1, [$count, $unexec]);
                    } else {
                        push(@result, 1, $count);
                    }
                }
                push(@result, $code);
            }
        }
    }

    if ($exclude_flag || $exclude_br_flag || $exclude_exception_br_flag) {
        warn("WARNING: unterminated exclusion section in $filename\n");
    }
    return (\@result, $branchData, \@functions);
}

#
# read_intermediate_text(gcov_filename, data)
#
# Read gcov intermediate text format in GCOV_FILENAME and add the resulting
# data to DATA in the following format:
#
# data:      source_filename -> file_data
# file_data: concatenated lines of intermediate text data
#

sub read_intermediate_text($$)
{
    my ($gcov_filename, $data) = @_;
    my $filename;

    my $f = InOutFile->in($gcov_filename, $lcovutil::demangle_cpp_cmd);
    my $h = $f->hdl();
    while (my $line = <$h>) {
        if ($line =~ /^file:(.*)$/) {
            $filename = $1;
            $filename =~ s/[\r\n]$//g;
            # adjust filename...
            $filename = lcovutil::subst_file_name($filename);
        } elsif (defined($filename)) {
            $data->{$filename} .= $line;
        }
    }
}

#
# read_intermediate_json(gcov_filename, data, basedir_ref)
#
# Read gcov intermediate JSON format in GCOV_FILENAME and add the resulting
# data to DATA in the following format:
#
# data:      source_filename -> file_data
# file_data: GCOV JSON data for file
#
# Also store the value for current_working_directory to BASEDIR_REF.
#

sub read_intermediate_json($$$)
{
    my ($gcov_filename, $data, $basedir_ref) = @_;
    my $text;
    my $json;

    {
        # intermediate JSON contains the demangled name
        my $f     = InOutFile->in($gcov_filename);
        my $h     = $f->hdl();
        my @lines = <$h>;
        $text = join("\n", @lines);
    }

    $json = JsonSupport::decode($text);    # imported from lcovutil.pm
    if (!defined($json) ||
        !exists($json->{"files"}) ||
        ref($json->{"files"} ne "ARRAY")) {
        die("ERROR: Unrecognized JSON output format in $gcov_filename\n");
    }

    $$basedir_ref = $json->{"current_working_directory"};

    # Workaround for bug in MSYS GCC 9.x that encodes \ as \n in gcov JSON
    # output
    if ($^O eq "msys" && $$basedir_ref =~ /\n/) {
        $$basedir_ref =~ s#\n#/#g;
    }

    for my $file (@{$json->{"files"}}) {
        # decode_json() is decoding UTF-8 strings from the JSON file into
        # Perl's internal encoding, but filenames on the filesystem are
        # usually UTF-8 encoded, so the filename strings need to be
        # converted back to UTF-8 so that they actually match the name
        # on the filesystem.
        utf8::encode($file->{"file"});

        my $filename = $file->{"file"};
        # substitute filename here
        $filename = lcovutil::subst_file_name($filename);

        $data->{$filename} = $file;
    }
}

#
# intermediate_text_to_info(data)
#
# Write DATA in info format to file descriptor FD.
#
# data:      filename -> file_data:
# file_data: concatenated lines of intermediate text data
#
# Note: To simplify processing, gcov data is not combined here, that is counts
#       that appear multiple times for the same lines/branches are not added.
#       This is done by lcov/genhtml when reading the data files.
#

sub intermediate_text_to_info($)
{
    my $data       = shift;
    my $branch_num = 0;

    return if (!%{$data});

    my $traceFile = TraceFile->new();

    my ($warnedLine, $warnedBranch, $warnedFunction);    # once only
    for my $filename (keys(%{$data})) {

        lcovutil::info(1, "emit data for $filename\n");

        # there is no meaningful parse location for this data
        my $fileData    = $traceFile->data($filename);
        my $functionMap = $fileData->testfnc($test_name);
        my $branchMap   = $fileData->testbr($test_name);
        my $lineMap     = $fileData->test($test_name);

        if (@lcovutil::extractVersionScript &&
            -f $filename) {
            my $version = lcovutil::extractFileVersion($filename);
            $fileData->version($version)
                if (defined($version) && $version ne "");
        }
        for my $line (split(/\n/, $data->{$filename})) {
            if ($line =~ /^lcount:(\d+),(\d+),?/) {
                # lcount:<line>,<count>
                # lcount:<line>,<count>,<has_unexecuted_blocks>
                my $lineNo = $1;
                my $hit    = $2;
                if ($hit < 0) {
                    #negative...
                    lcovutil::ignorable_error($lcovutil::ERROR_NEGATIVE,
                        "Unexpected negative count '$hit' for $filename:$lineNo.\n\tPerhaps you need to compile with '-fprofile-update=atomic"
                    ) unless $warnedLine;
                    $warnedLine = 1;
                    $hit        = 0;
                }
                $lineMap->append($lineNo, $hit);

                # Intermediate text format does not provide
                # branch numbers, and the same branch may appear
                # multiple times on the same line (e.g. in
                # template instances). Synthesize a branch
                # number based on the assumptions:
                # a) the order of branches is fixed across
                #    instances
                # b) an instance starts with an lcount line
                $branch_num = 0;
            } elsif ($line =~ /^function:((\d+)(,(\d+))?),(\d+),(.+)$/) {
                next unless $lcovutil::func_coverage;

                # function:<line>,<endline,>?<count>,<name>
                my ($lineNo, $endline, $hit, $name) = ($2, $4, $5, $6);
                if ($hit < 0) {
                    #negative...
                    lcovutil::ignorable_error($lcovutil::ERROR_NEGATIVE,
                        "Unexpected negative count '$hit' for function $name at $filename:$lineNo.\n\tPerhaps you need to compile with '-fprofile-update=atomic"
                    ) unless $warnedFunction;
                    $warnedFunction = 1;
                    $hit            = 0;
                }
                my $func =
                    $functionMap->define_function($name, $filename, $lineNo,
                                                  $endline);
                $func->addAlias($name, $hit);
            } elsif ($line =~ /^branch:(\d+),(taken|nottaken|notexec)/) {
                my $lineNo = $1;
                next
                    unless $lcovutil::br_coverage;
                my $c;
                # branch:<line>,taken|nottaken|notexec
                if ($2 eq "taken") {
                    $c = 1;
                } elsif ($2 eq "nottaken") {
                    $c = 0;
                } else {
                    $c = "-";
                }
                my $br = BranchBlock->new($branch_num, $c);
                # "block" is always zero for intermedaite text
                $branchMap->append($lineNo, 0, $br, $filename);
                ++$branch_num;
            }
        }
        # now go through lines, functions, branches - append to test_name data
        $fileData->sum()->merge($lineMap);
        $fileData->sumbr()->merge($branchMap);
        $fileData->func()->merge($functionMap);
    }
    return $traceFile;
}

#
# intermediate_json_to_info(data)
#
# Write DATA in info format to file descriptor FD.
#
# data:      filename -> file_data:
# file_data: GCOV JSON data for file
#
# Note: To simplify processing, gcov data is not combined here, that is counts
#       that appear multiple times for the same lines/branches are not added.
#       This is done by lcov/genhtml when reading the data files.
#

sub intermediate_json_to_info($)
{
    my $data       = shift;
    my $branch_num = 0;

    return if (!%{$data});

    my $traceFile = TraceFile->new();
    lcovutil::debug(1,
          "called intermediate_json_to_info " . join(' ', keys(%$data)) . "\n");
    my ($warnedLine, $warnedBranch, $warnedFunction, $warnedUnexecuted)
        ;    # once only
    while (my ($filename, $file_data) = each(%$data)) {
        # there is no meaningful parse location for this data
        my $fileData    = $traceFile->data($filename);
        my $functionMap = $fileData->testfnc($test_name);
        my $branchMap   = $fileData->testbr($test_name);
        my $lineMap     = $fileData->test($test_name);

        if (@lcovutil::extractVersionScript &&
            -f $filename) {
            my $version = lcovutil::extractFileVersion($filename);
            $fileData->version($version)
                if (defined($version) && $version ne "");
        }

        # Function data
        if ($lcovutil::func_coverage) {
            for my $d (@{$file_data->{"functions"}}) {
                my $start_line = $d->{"start_line"};
                my $end_line   = $d->{"end_line"}
                    if exists($d->{end_line});
                my $count = $d->{"execution_count"};
                my $name  = $lcovutil::demangle_cpp_cmd ? $d->{demangled_name} :
                    $d->{"name"};

                if ($count < 0) {
                    #negative...
                    lcovutil::ignorable_error($lcovutil::ERROR_NEGATIVE,
                        "Unexpected negative count '$count' for function $name at $filename:$start_line.\n\tPerhaps you need to compile with '-fprofile-update=atomic"
                    ) unless $warnedFunction;
                    $warnedFunction = 1;
                    $count          = 0;
                }
                my $func =
                    $functionMap->define_function($name, $filename,
                                                  $start_line, $end_line);
                $func->addAlias($name, $count);
            }
        }
        for my $d (@{$file_data->{"lines"}}) {
            my $line  = $d->{"line_number"};
            my $count = $d->{"count"};

            my $branches = $d->{"branches"};
            my $unexec   = $d->{"unexecuted_block"};

            next
                if (!defined($line) || !defined($count));

            if ($count < 0) {
                #negative...
                lcovutil::ignorable_error($lcovutil::ERROR_NEGATIVE,
                    "Unexpected negative count '$count' for $filename:$line.\n\tPerhaps you need to compile with '-fprofile-update=atomic"
                ) unless $warnedLine;
                $warnedLine = 1;
                $count      = 0;
            }
            if (0 == scalar(@$branches) && $unexec && $count != 0) {
                lcovutil::debug(
                    "$filename:$line: unexecuted block on non-branch line with count=$count\n"
                );
                if ($opt_adjust_unexecuted_blocks) {
                    $count = 0;
                } else {
                    lcovutil::ignorable_warning($ERROR_GCOV,
                        "$filename:$line: unexecuted block on non-branch line with non-zero hit count.  Use \"geninfo --rc geninfo_unexecuted_blocks=1 to set count to zero."
                    ) unless $warnedUnexecuted;
                    $warnedUnexecuted = 1;
                }
            }

            # just add the line - worry about filtering later
            $lineMap->append($line, $count);

            # Branch data
            if ($lcovutil::br_coverage) {

                # there may be compiler-generated branch data on
                #  the closing brace of the function...
                $branch_num = 0;
                $unexec     = (defined($unexec) && $unexec && $count == 0);
                my %branchRenumber;

                for my $b (@$branches) {
                    my $brcount      = $b->{"count"};
                    my $is_exception = $b->{"throw"};
                    # need to keep track of whether compiler thinks this
                    #  branch is an exception - so we can skip it later.

                    if (
                        !( $is_exception && $lcovutil::exclude_exception_branch)
                    ) {
                        if (!defined($brcount) || $unexec) {
                            $brcount = "-";
                        } else {
                            if ($brcount < 0) {
                                #negative...
                                lcovutil::ignorable_error(
                                    $lcovutil::ERROR_NEGATIVE,
                                    "Unexpected negative count '$brcount' for branch $branch_num of $filename:$line.\n\tPerhaps you need to compile with '-fprofile-update=atomic"
                                ) unless $warnedBranch;
                                $warnedBranch = 1;
                                $brcount      = 0;
                            }
                        }
                        my $entry =
                            BranchBlock->new($branch_num, $brcount, undef,
                                  defined($is_exception) && $is_exception != 0);
                        if (exists($branchRenumber{$branch_num})) {
                            $branchRenumber{$branch_num}->merge($entry);
                        } else {
                            $branchRenumber{$branch_num} = $entry;
                        }
                        ++$branch_num;
                    }
                }
                my $id = 0;
                foreach my $b (sort { $a <=> $b } keys(%branchRenumber)) {
                    my $br = $branchRenumber{$b};
                    my $newBr =
                        BranchBlock->new($id, $br->data(), undef,
                                         $br->is_exception());
                    # "block" is always zero for intermediate JSON data
                    $branchMap->append($line, 0, $newBr, $filename);
                    ++$id;
                }
            }
        }
        # now go through lines, functions, branches - append to test_name data
        $fileData->sum()->merge($lineMap);
        $fileData->sumbr()->merge($branchMap);
        $fileData->func()->merge($functionMap);
    }
    return $traceFile;
}

sub which($)
{
    my ($filename) = @_;

    return $filename if (file_name_is_absolute($filename));
    foreach my $dir (File::Spec->path()) {
        my $p = catfile($dir, $filename);
        return $p if (-x $p);
    }
    return $filename;
}

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

    if ($msg =~ /version\s+'([^']+)',\s+prefer\s+'([^']+)'/) {
        my $have = $1;
        my $want = $2;
        foreach my $f (\$have, \$want) {
            if ($$f =~ /^(.)0(.)\*$/) {
                # version ID numbering in the gcda/gcno file is not entirely
                # clear to me - but it appears to be "major.0.minor" - where
                # major is integral for versions older than gcc/10, and hex +1
                # for versions after gcc/10.
                my $major =
                    (ord($1) >= ord('A')) ? (ord($1) - ord('A') + 9) : $1;
                $$f = sprintf("%d.%d", $major, $2);
            }
        }
        my $path = which($gcov_tool[0]);
        lcovutil::ignorable_error($lcovutil::ERROR_VERSION,
            "Incompatible GCC/GCOV version found while processing $filename:\n\tYour test was built with '$have'.\n\tYou are trying to capture with gcov tool '$path' which is version '$want'."
        );
        return 1;
    }
    return 0;
}

#
# print_gcov_warnings(type, stderr_file, is_graph, map)
#
# Print GCOV warnings in file STDERR_FILE to STDERR. If IS_GRAPH is non-zero,
# suppress warnings about missing as these are expected. Replace keys found
# in MAP with their values.
#

sub print_gcov_warnings($$$$)
{
    my ($type, $data, $is_graph, $map) = @_;

    my $leader = "$type:\n  ";
    foreach my $line (split('\n', $data)) {
        next if ($is_graph && $line =~ /cannot open data file/);

        for my $key (keys(%{$map})) {
            $line =~ s/\Q$key\E/$map->{$key}/g;
        }

        print(STDERR $leader, $line, "\n");
        $leader = '  ';
    }
}

#
# process_intermediate(directory, file, gcno_file, tempdir)
#
# Create output for a single file (either a data file or a graph file) using
# gcov's intermediate option.
#

sub process_intermediate($$$$)
{
    my ($searchdir, $file, $gcno_file, $tempdir) = @_;
    my ($fdir, $fbase, $fext);
    my $data_file;
    my $errmsg;
    my %data;
    my $fd;
    my $base;
    my ($out, $err, $rc);
    my $json_basedir;
    my $json_format;

    my $filename = defined($file) ? $file : $gcno_file;
    $file = solve_relative_path($cwd, $filename);
    ($fdir, $fbase, $fext) = split_filename($file);

    my $is_graph = (".$fext" eq $graph_file_extension);

    if ($is_graph) {
        # Process graph file - copy to temp directory to prevent
        # accidental processing of associated data file
        $data_file =
            File::Spec->catfile($tempdir, "$fbase$graph_file_extension");
        if (!copy($file, $data_file)) {
            $errmsg = "ERROR: Could not copy file $file: $!";
            goto err;
        }
    } else {
        # if .gcda and .gcno files are in the same directory, then simply
        #   processs in place - otherwise, link the .gcda and .gcno files
        #   into tempdir and run from here
        if (dirname($filename) eq dirname($gcno_file)) {
            # Process data file in place
            $data_file = $file;
        } else {
            $data_file = basename($file);
            my $gcda = File::Spec->catfile($tempdir, $data_file);
            debug("create links to process $filename in $tempdir\n");
            symlink($file, $gcda);
            if ($?) {
                $errmsg = "unable to create link $data_file: $!";
                goto err;
            }
            # and the gcno file
            my $g    = solve_relative_path($cwd, $gcno_file);
            my $gcno = File::Spec->catfile($tempdir, basename($gcno_file));
            symlink($g, $gcno);
            if ($?) {
                $errmsg = "unable to create link $gcno: $!";
                goto err;
            }
        }
    }

    # Change directory
    debug(2, "chdir to tempdir $tempdir\n");
    if (!chdir($tempdir)) {
        $errmsg = "Could not change to directory $tempdir: $!";
        goto err;
    }

    # Run gcov on data file
    debug("gcov: " . join(' ', @gcov_tool) . " $data_file\n");
    my $now = Time::HiRes::gettimeofday();
    ($out, $err, $rc) = system_no_output(1 + 2 + 4, @gcov_tool, $data_file);
    my $then = Time::HiRes::gettimeofday();
    $lcovutil::profileData{exec}{$searchdir}{$filename} = $then - $now;
    print_gcov_warnings('stdout', $out, $is_graph, {$data_file => $file,})
        if ('' ne $out &&
            (0 != $rc ||
             $lcovutil::verbose > 1));
    unlink($out);
    debug(2, "chdir back to '$cwd'\n");
    chdir($cwd) or die("can't cd back to $cwd: $!");
    print_gcov_warnings('stderr', $err, $is_graph, {$data_file => $file,})
        if ('' ne $err &&
            (0 != $rc ||
             $lcovutil::verbose));

    if (0 != $rc) {
        if (check_gcov_fail($err, $file)) {
            return;
        }
    }
    if ($rc) {
        $errmsg = "GCOV failed for $file";
        goto err;
    }

    if ($is_graph) {
        # Remove graph file copy
        unlink($data_file) unless $lcovutil::preserve_intermediates;
    }

    # Parse resulting file(s)
    # 'meson' build system likes to use "." as leading character in generated
    # files.  Seems an unfortunate decision.
    for my $gcov_filename (
        glob(
            "$tempdir/*.gcov $tempdir/.*.gcov $tempdir/*.gcov.json.gz $tempdir/.*gcov.json.gz"
        )
    ) {
        if ($gcov_filename =~ /\.gcov\.json/) {
            read_intermediate_json($gcov_filename, \%data, \$json_basedir);
            $json_format = 1;
        } else {
            read_intermediate_text($gcov_filename, \%data);
        }
        if ($lcovutil::preserve_intermediates) {
            File::Copy::move($gcov_filename, $fdir) or
                die("ERROR: cannot rename $gcov_filename: $!");
        } else {
            unlink($gcov_filename);
        }
    }

    if (!%data) {
        ignorable_warning($ERROR_GCOV,
                          "WARNING: GCOV did not produce any data for $file");
        return;
    }

    # Determine base directory
    if (defined($base_directory)) {
        $base = $base_directory;
    } elsif (defined($json_basedir)) {
        $base = $json_basedir;
    } else {
        $base = $fdir;

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

        # Try to find base directory automatically if requested by user
        if ($rc_auto_base) {
            $base = find_base_from_source($base, [keys(%data)]);
        }
    }

    # Apply base file name to relative source files
    adjust_source_filenames(\%data, $base);

    # Remove excluded source files
    filter_source_files(\%data);

    # Generate output
    my $trace =
        $json_format ? intermediate_json_to_info(\%data) :
        intermediate_text_to_info(\%data);

    return $trace;

    err:
    ignorable_error($ERROR_GCOV, "$errmsg!");
}

# Map LLVM versions to the version of GCC gcov which they emulate.

sub map_llvm_version($)
{
    my ($ver) = @_;

    return 0x040200 if ($ver >= 0x030400);

    warn("WARNING: This version of LLVM's gcov is unknown.  " .
         "Assuming it emulates GCC gcov version 4.2.\n");

    return 0x040200;
}

# Return a readable version of encoded gcov version.

sub version_to_str($)
{
    my ($ver) = @_;
    my ($a, $b, $c);

    $a = $ver >> 16 & 0xff;
    $b = $ver >> 8 & 0xff;
    $c = $ver & 0xff;

    return "$a.$b.$c";
}

#
# 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;
    my ($a, $b, $c) = (4, 2, 0);    # Fallback version

    # Examples for gcov version output:
    #
    # gcov (GCC) 4.4.7 20120313 (Red Hat 4.4.7-3)
    #
    # gcov (crosstool-NG 1.18.0) 4.7.2
    #
    # LLVM (http://llvm.org/):
    #   LLVM version 3.4svn
    #
    # Apple LLVM version 8.0.0 (clang-800.0.38)
    #       Optimized build.
    #       Default target: x86_64-apple-darwin16.0.0
    #       Host CPU: haswell

    if ($lcovutil::verbose) {
        my $which = which($gcov_tool[0]);
        lcovutil::info("gcov is '$which'\n");
    }

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

    # Remove all bracketed information
    $version_string =~ s/\([^\)]*\)//g;

    if ($version_string =~ /(\d+)\.(\d+)(\.(\d+))?/) {
        ($a, $b, $c) = ($1, $2, $4);
        $c              = 0 if (!defined($c));
        $version_string = (split('\n', $version_string))[0];
        chomp($version_string);
    } else {
        warn("WARNING: cannot determine gcov version - assuming $a.$b.$c\n");
    }
    $result = $a << 16 | $b << 8 | $c;

    if ($version_string =~ /LLVM/) {
        $result = map_llvm_version($result);
        info("Found LLVM gcov version $a.$b.$c, which emulates gcov " .
             "version " . version_to_str($result) . "\n");
    } else {
        info("Found gcov version: " . version_to_str($result) . "\n");
    }

    return ($result, $version_string);
}

#
# info(printf_parameter)
#
# Use printf to write PRINTF_PARAMETER to stdout only when not --quiet
#

sub my_info(@)
{
    # 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);
}

sub process_graphfile($$)
{
    my ($dirname, $file) = @_;
    my $graph_filename = $file;
    my $graph_dir;
    my $graph_basename;
    my $source_dir;
    my $base_dir;
    my $graph;
    my $instr;

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

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

    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_source($base_dir,
                                          [keys(%{$instr}), keys(%{$graph})]);
    }

    adjust_source_filenames($instr, $base_dir);
    adjust_source_filenames($graph, $base_dir);

    my $traceFile = TraceFile->new();
    foreach my $filename (sort(keys(%{$instr}))) {
        my $funcdata = $graph->{$filename};
        my $line;
        my $linedata;

        # Skip external files if requested
        if (is_external($filename)) {
            info("  ignoring data for external file $filename\n");
            next;
        }
        # there is no meaningful parse location for this data
        my $fileData    = $traceFile->data($filename);
        my $functionMap = $fileData->testfnc($test_name);
        my $lineMap     = $fileData->test($test_name);

        if (@lcovutil::extractVersionScript &&
            -f $filename) {
            my $version = lcovutil::extractFileVersion($filename);
            $fileData->version($version)
                if (defined($version) && $version ne "");
        }
        if (defined($funcdata) && $lcovutil::func_coverage) {
            my @functions =
                sort({ $funcdata->{$a}->[0] <=> $funcdata->{$b}->[0] }
                     keys(%{$funcdata}));
            # Gather list of instrumented lines and functions
            foreach my $func (@functions) {
                $linedata = $funcdata->{$func};
                my $lineNo = $linedata->[0];
                my $fnName =
                    filter_fn_name($func, defined($lcovutil::demangle_cpp_cmd));
                my $func =
                    $functionMap->define_function($fnName, $filename, $lineNo);
                $func->addAlias($fnName, 0);
            }
        }
        # Print zero line coverage data
        foreach $line (@{$instr->{$filename}}) {
            $lineMap->append($line, 0);
        }
        # now go through lines, functions, branches - append to test_name data
        $fileData->sum()->merge($lineMap);
        $fileData->func()->merge($functionMap);
    }
    return $traceFile;
}

sub filter_fn_name($$)
{
    my ($fn, $demangle) = @_;
    my $f;
    if ($demangle) {
        $f = `$lcovutil::demangle_cpp_cmd $fn`;
        chomp($f);
        die("unable to demangle '$fn': $!") if ($?);
    } else {
        $f = $fn;
    }
    # Remove characters used internally as function name delimiters
    $f =~ s/[,=]/_/g;

    return $f;
}

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

    ignorable_error($ERROR_GRAPH, "$filename: $msg - skipping.");
    return;
}

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

    lcovutil::debug(2, $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 &&
        $debug >= 2) {
        my $op    = $peek ? "peek" : "read";
        my $ascii = "";
        my $hex   = "";
        my $i;

        my $msg = "$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 .= ".";
            }
        }
        lcovutil::debug(2, "$msg$hex |$ascii|\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;
}

#
# uniq(list)
#
# Return list without duplicate entries.
#

sub uniq(@)
{
    my (@list) = @_;
    my @new_list;
    my %known;

    foreach my $item (@list) {
        next if ($known{$item});
        $known{$item} = 1;
        push(@new_list, $item);
    }

    return @new_list;
}

#
# 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_source(base_dir, source_files)
#
# Try to determine the base directory of the object file built from
# SOURCE_FILES. The base directory is the base for all relative filenames in
# the gcov data. 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_source($$)
{
    my ($base_dir, $source_files) = @_;
    my $old_base;
    my $best_miss;
    my $best_base;
    my %rel_files;

    # Determine list of relative paths
    foreach my $filename (@$source_files) {
        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_source_filenames(hash, base_dir)
#
# Transform all keys of HASH to absolute form and apply requested
# transformations.
#

sub adjust_source_filenames($$$)
{
    my ($hash, $base_dir) = @_;

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

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

        # Apply adjustment
        $filename = lcovutil::subst_file_name($filename);

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

#
# filter_source_files(hash)
#
# Remove unwanted source file data from HASH.
#

sub filter_source_files($)
{
    my ($hash) = @_;

    foreach my $filename (keys(%{$hash})) {
        # Skip external files if requested
        if (is_external($filename) ||
            TraceFile::skipCurrentFile($filename)) {
            # Remove file data
            delete($hash->{$filename});
            $lcovutil::excluded_files{$filename} = 1;
        }
    }
}

#
# 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} = [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, fileorder_first)
#
# Convert data from bb to the graph format and list of instrumented lines.
#
# If FILEORDER_FIRST is set, use fileorder data to determine a functions
# base source file.
#
# 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, $fileorder_first) = @_;
    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}) &&
            !$fileorder_first) {
            # 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 = "";

    lcovutil::debug(2, "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
            lcovutil::debug(2, "filename");
            $filename = read_bb_string(*HANDLE, $minus_one);
            goto incomplete if (!defined($filename));
        } elsif ($value == $minus_two) {
            # Function name
            lcovutil::debug(2, "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, 0);
    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;

    lcovutil::debug(2, "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;

    lcovutil::debug(2, "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
            lcovutil::debug(2, "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) {
            lcovutil::debug(2, "function record");
            # Read function name
            lcovutil::debug(2, "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, 0);

    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;

    lcovutil::debug(2, "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;

    lcovutil::debug(2, "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
            lcovutil::debug(2, "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, version)
#
# 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, $version) = @_;
    my $strlen;
    my $overlong_string;

    return 1 if ($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, version)
#
# Read a gcno format function record from handle and add the relevant data
# to graph. Return (filename, function, artificial) on success, undef on error.
#

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

    lcovutil::debug(2, "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, $version);
        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
    lcovutil::debug(2, "function name");
    $function = read_gcno_string($handle, $big_endian);
    return undef if (!defined($function));
    if ($version >= $GCOV_VERSION_8_0_0) {
        $artificial = read_gcno_value($handle, $big_endian,
                                      "compiler-generated entity flag");
        return undef if (!defined($artificial));
    }
    # Read filename
    lcovutil::debug(2, "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));
    # Skip column and ending line number
    if ($version >= $GCOV_VERSION_8_0_0) {
        graph_skip($handle, 4, "column number")      or return undef;
        graph_skip($handle, 4, "ending line number") or return undef;
    }
    # Add to list
    push(@{$bb->{$function}->{$filename}}, $lineno);
    graph_add_order($fileorder, $function, $filename);

    return ($filename, $function, $artificial);
}

#
# map_gcno_version
#
# Map version number as found in .gcno files to the format used in geninfo.
#

sub map_gcno_version($)
{
    my ($version) = @_;
    my ($a, $b, $c);
    my ($major, $minor);

    $a = $version >> 24;
    $b = $version >> 16 & 0xff;
    $c = $version >> 8 & 0xff;

    if ($a < ord('A')) {
        $major = $a - ord('0');
        $minor = ($b - ord('0')) * 10 + $c - ord('0');
    } else {
        $major = ($a - ord('A')) * 10 + $b - ord('0');
        $minor = $c - ord('0');
    }

    return $major << 16 | $minor << 8;
}

sub remove_fn_from_hash($$)
{
    my ($hash, $fns) = @_;

    foreach my $fn (@$fns) {
        delete($hash->{$fn});
    }
}

#
# 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;
    my $filelength;
    my $version;
    my $artificial;
    my @artificial_fns;
    local *HANDLE;

    open(HANDLE, "<", $gcno_filename) or goto open_error;
    $filelength = (stat(HANDLE))[7];
    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;
    }
    # Read version
    $version = read_gcno_value(*HANDLE, $big_endian, "compiler version");
    $version = map_gcno_version($version);
    debug(sprintf("found version 0x%08x\n", $version));
    # Skip stamp
    graph_skip(*HANDLE, 4, "file timestamp") or goto incomplete;
    if ($version >= $GCOV_VERSION_8_0_0) {
        graph_skip(*HANDLE, 4, "support unexecuted blocks flag") 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;
        # Catch garbage at the end of a gcno file
        if ($next_pos > $filelength) {
            debug("Overlong record: file_length=$filelength " .
                  "rec_length=$length\n");
            warn("WARNING: $gcno_filename: Overlong record at end of file!\n");
            last;
        }
        # Process record
        if ($tag == $tag_function) {
            ($filename, $function, $artificial) =
                read_gcno_function_record(*HANDLE, $bb, $fileorder, $big_endian,
                                          $length, $version);
            goto incomplete if (!defined($function));
            push(@artificial_fns, $function) if ($artificial);
        } 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);

    # Remove artificial functions from result data
    remove_fn_from_hash($bb, \@artificial_fns);
    remove_fn_from_hash($fileorder, \@artificial_fns);

    ($instr, $graph) = graph_from_bb($bb, $fileorder, $gcno_filename, 1);
    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;
}

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

sub get_gcov_capabilities()
{
    my $help = join(' ', @gcov_tool) . ' --help';
    $help = `$help`;
    die("Error return code from '\"$gcov_tool[0]\" --help': $!")
        if ($?);
    my %capabilities;
    my %short_option_translations = ('a' => 'all-blocks',
                                     'b' => 'branch-probabilities',
                                     'c' => 'branch-counts',
                                     'f' => 'function-summaries',
                                     'h' => 'help',
                                     'i' => 'intermediate-format',
                                     'l' => 'long-file-names',
                                     'n' => 'no-output',
                                     'o' => 'object-directory',
                                     'p' => 'preserve-paths',
                                     'u' => 'unconditional-branches',
                                     'v' => 'version',
                                     'x' => 'hash-filenames',);

    foreach (split(/\n/, $help)) {
        my $capability;
        if (/--(\S+)/) {
            $capability = $1;
        } else {
            # If the line provides a short option, translate it.
            next if (!/^\s*-(\S)\s/);
            $capability = $short_option_translations{$1};
            next if not defined($capability);
        }
        next if ($capability eq 'help');
        next if ($capability eq 'version');
        next if ($capability eq 'object-directory');

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

    return \%capabilities;
}

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