#!/usr/bin/env 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 realpath/;
use Time::HiRes;    # for profiling
use Capture::Tiny;
use FindBin;
use Storable;
use POSIX;

use lib "$FindBin::RealBin/../lib";
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

                 %excluded_files
                 warn_file_patterns
                 @extractVersionScript

                 $ERROR_GCOV $ERROR_GRAPH $ERROR_PACKAGE $ERROR_CHILD
                 $ERROR_EMPTY $ERROR_PARALLEL $ERROR_UNSUPPORTED $ERROR_PATH
                 $ERROR_INCONSISTENT_DATA $ERROR_UTILITY $ERROR_FORMAT
                 report_parallel_error check_parent_process
                 summarize_messages
                 is_external @internal_dirs
                 parseOptions
                 @comments
                 $maxParallelism init_parallel_params $maxMemory
                );

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

our @gcov_tool;

# Constants

our $GCOV_VERSION_8_0_0 = 0x80000;
our $GCOV_VERSION_4_7_0 = 0x40700;
our $GCOV_VERSION_4_2_0 = 0x40200;    # llvm/11

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

# Compatibility 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
our %COMPAT_MODE_AUTO = ($COMPAT_MODE_HAMMER    => 0,    # no gcc/3.3 support
                         $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 compute_internal_directories(@);
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 graph_cleanup($);
sub graph_find_base($);
sub graph_from_bb($$$$);
sub graph_add_order($$$);
sub read_gcno_word(*;$$);
sub read_gcno_value(*$;$$);
sub read_gcno_string(*$);
sub read_gcno_lines_record(*$$$$$$);
sub read_gcno_function_record(*$$$$$);
sub read_gcno($);
sub get_gcov_capabilities();
sub int_handler();
sub compat_name($);
sub parse_compat_modes($);
sub is_compat($);
sub which($);

# Global variables
our $gcov_version;
our $gcov_version_string;
our $graph_file_extension = '.gcno';
our $data_file_extension  = '.gcda';
our @data_directory;
our $buildDirSearchPath;
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 $opt_no_compat_libtool;
our $initial;
our $no_recursion = 0;
our $maxdepth;
our $no_markers           = 0;
our $opt_derive_func_data = 0;
our $gcov_caps;
our %compat_value;
our $intermediate;
our $intervalMonitor;             # class for progress reporting
our $totalChildCpuTime    = 0;
our $intervalChildCpuTime = 0;    # since last updata

# for performance tweaking/tuning
our $defaultChunkSize;
our $defaultInterval;
our %childRetryCounts;
our @large_files;

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

lcovutil::save_cmd_line(\@ARGV, "$FindBin::RealBin");

#
# 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_opts = ("test-name|t=s"       => \$test_name,
                    "output-filename|o=s" => \$output_filename,
                    "base-directory|b=s"  => \$base_directory,
                    "follow|f"            => \$lcovutil::opt_follow,
                    "compat-libtool"      => \$lcovutil::opt_compat_libtool,
                    "no-compat-libtool"   => \$opt_no_compat_libtool,
                    "gcov-tool=s"         => \@gcov_tool,
                    "initial|i"           => \$initial,
                    "all"                 => \$lcovutil::geninfo_captureAll,
                    "no-recursion"        => \$no_recursion,
                    "no-markers"          => \$no_markers,
                    "derive-func-data"    => \$opt_derive_func_data,
                    "external|e"          => \$lcovutil::opt_external,
                    "no-external"         => \$lcovutil::opt_no_external,
                    "compat=s"            => \$lcovutil::geninfo_opt_compat,
                    'large-file=s'        => \@large_files);

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

$buildDirSearchPath =
    SearchPath->new('build directory', @lcovutil::build_directory);
@gcov_tool = @lcovutil::rc_gcov_tool unless @gcov_tool;

eval {
    map { qr($_) } @large_files;
};
die("invalid 'large-file' regexp: $@")
    if ($@);

# Check regexp
if (defined($lcovutil::rc_adjust_src_path)) {
    my ($pattern, $replace) = split(/\s*=>\s*/, $lcovutil::rc_adjust_src_path);
    # If no replacement is specified, simply remove pattern
    $replace = '' unless defined($replace);
    my $p = "s#$pattern#$replace#g";
    $p .= 'i' if $lcovutil::case_insensitive;
    my $text = 'abc';
    my $str  = eval { '$test =~ ' . $p . ';' };
    if ($@) {
        lcovutil::ignorable_error($lcovutil::ERROR_FORMAT,
            "Invalid 'geninfo_adjust_src_path=$lcovutil::rc_adjust_src_path' syntax: '$@'"
        );
    } else {
        push(@lcovutil::file_subst_patterns, [$p, 0]);
    }
}

if ($lcovutil::geninfo_captureAll && $initial) {
    lcovutil::ignorable_error($lcovutil::ERROR_USAGE,
                              "'--all' ignored when '--initial' is used");
    $lcovutil::geninfo_captureAll = undef;
}

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)) {
    $lcovutil::opt_compat_libtool = ($opt_no_compat_libtool ? 0 : 1);
    $opt_no_compat_libtool        = undef;
}

if (defined($lcovutil::opt_external)) {
    $lcovutil::opt_no_external = !$lcovutil::opt_external;
    $lcovutil::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 {
    @gcov_tool =
        split($lcovutil::split_char, join($lcovutil::split_char, @gcov_tool));
    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("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("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 ($lcovutil::rc_intermediate eq "0") {
    $intermediate = 0;
} elsif ($lcovutil::rc_intermediate eq "1") {
    $intermediate = 1;
} elsif (lc($lcovutil::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("invalid value for geninfo_intermediate: " .
        "'$lcovutil::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) {
        lcovutil::ignorable_error($lcovutil::ERROR_USAGE,
               "--derive-func-data is not compatible with intermediate format");
        $opt_derive_func_data = 0;
    }
    if ($lcovutil::exclude_exception_branch && !$gcov_caps->{'json-format'}) {
        die("excluding exception branches is not compatible with " .
            "text intermediate format\n");
    }
}

# Determine gcov options
push(@gcov_tool, "-b")
    if ($gcov_caps->{'branch-probabilities'} &&
        ($lcovutil::br_coverage ||
         $lcovutil::func_coverage ||
         $lcovutil::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'} &&
        $lcovutil::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;

if ($lcovutil::mcdc_coverage) {
    if ($gcov_caps->{'conditions'}) {
        push(@gcov_tool, '--conditions');
    } else {
        $lcovutil::mcdc_coverage = 0;
        lcovutil::ignorable_error($lcovutil::ERROR_USAGE,
            "MC/DC coverage enabled but \"$gcov_tool[0]\" does not support the '--conditions' option."
        );
    }
}

# Determine compatibility modes
parse_compat_modes($lcovutil::geninfo_opt_compat);

if ($no_markers) {
    lcovutil::ignorable_error($lcovutil::ERROR_USAGE,
                   "use new '--filter' option or old '--no-markers' - not both")
        if (@lcovutil::opt_filter);
} elsif (!@lcovutil::opt_filter) {
    # don't apply the backward-compatible options if user specifies any filters
    lcovutil::info(1,
        "$lcovutil::tool_name:  applying '--filter region,branch_region' by default - see the '--no-markers' section in the man page for more information.\n"
    );
    push(@lcovutil::opt_filter, "region");
    push(@lcovutil::opt_filter, "branch_region") if $br_coverage;
} else {
    lcovutil::info(
        "Note: 'region' and 'branch_region' filters are not applied by default when '--filter' is specified.  See the '--no-markers section in the man page for more information.\n"
    ) unless grep({ /(region|branch_region)/ } @lcovutil::opt_filter);
}
parse_cov_filters(@lcovutil::opt_filter);

# Make sure test names only contain valid characters
if ($test_name =~ s/\W/_/g) {
    lcovutil::ignorable_warning($lcovutil::ERROR_FORMAT,
                                "invalid characters removed from testname");
}

# Adjust test name to include uname output if requested
if ($lcovutil::geninfo_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);
}

# 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) {
        if (-d $pattern) {
            $pattern =~
                s#$lcovutil::dirseparator$##;   # remove trailing slash - if any
            push(@dirs, $pattern);
            next;
        }
        $pattern =~ s/([^\\]) /$1\\ /g          # explicitly escape spaces
            unless $^O =~ /Win/;

        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_EMPTY, "$pattern does not match anything.");
        }
    }
    @data_directory = @dirs;
}

if ($gcov_version < $GCOV_VERSION_4_2_0) {
    die("Your toolchain version is too old and is no longer supported by lcov.  Please upgrade - or use an older lcov release."
    );
}

# Check output filename
$single_file = defined($output_filename);

# use absolute path: we change directories while processing files
$output_filename = catfile($cwd, $output_filename)
    if (defined($output_filename) &&
        ($output_filename ne "-") &&
        !file_name_is_absolute($output_filename));

# Build list of directories to identify external files
compute_internal_directories(@data_directory, $base_directory);

if ($initial && $lcovutil::br_coverage && !$intermediate) {
    lcovutil::ignorable_error($lcovutil::ERROR_USAGE,
        "--initial cannot generate branch coverage data with this compiler/toolchain version."
    );
}

lcovutil::ignorable_error($lcovutil::ERROR_USAGE,
                "--fail_under_lines not supported unless output file specified")
    if (defined($lcovutil::fail_under_lines) && !$single_file);

# 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;
eval { $processedFiles += gen_info(@data_directory); };
if ($@) {
    $exit_code = 1;
    print(STDERR $@);
}

if (0 == $exit_code) {
    eval {
        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();
            my $f = Time::HiRes::gettimeofday();
            $trace_data->add_comments(@lcovutil::comments);
            $trace_data->write_info_file($output_filename,
                                         $lcovutil::verify_checksum);
            $files_created++;
            my $then = Time::HiRes::gettimeofday();
            $lcovutil::profileData{filter} = $f - $now;
            $lcovutil::profileData{write}  = $then - $f;
        }
        if ($files_created == 0) {
            lcovutil::ignorable_error($lcovutil::ERROR_EMPTY,
                                      "no data generated\n");
        }
        my $then = Time::HiRes::gettimeofday();
        $lcovutil::profileData{emit} = $then - $now;
        if (defined($trace_data)) {
            info("Finished .info-file creation\n");
            $trace_data->print_summary()
                if ($lcovutil::verbose >= 0);
        }
        summarize_cov_filters();
        # print warnings
        lcovutil::warn_file_patterns();
        $buildDirSearchPath->warn_unused(
                              @lcovutil::build_directory ? '--build-directory' :
                                  'build_directory = ');
        ReadCurrentSource::warn_sourcedir_patterns();
    };
    if ($@) {
        # eval of filter application failed - set error return code, but
        #  still try to emit profile
        print(STDERR $@);
        $exit_code = 1;
    }
}
# $trace_data may be undef if no non-empty GCDA files found and the
#  'empty' warning is ignored
if (0 == $exit_code &&
    defined($trace_data) &&
    $single_file) {

    $trace_data->checkCoverageCriteria();
    CoverageCriteria::summarize();
    $exit_code = 1 if $CoverageCriteria::coverageCriteriaStatus;
}
summarize_messages();
my $end = Time::HiRes::gettimeofday();
$lcovutil::profileData{total} = $end - $start;

lcovutil::cleanup_callbacks();

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

# exit with non-zero status if --keep-going and some errors detected
$exit_code = 1
    if (0 == $exit_code &&
        (!(defined($trace_data) && $single_file) ||
         lcovutil::saw_error()));
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
  --all                             Capture both .gcda and lone .gcno 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
      --mcdc                        Enable MC/DC 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 revision control version
                                    ID of source file
      --resolve-script SCRIPTNAME   Call script to find source file frpm path
  -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

}

package IntervalMonitor;

use constant {
              TOTAL_FILES      => 0,
              TOTAL_CHUNKS     => 1,
              CHUNK_SIZE       => 2,
              PROCESSED_CHUNKS => 3,
              INTERVAL_LENGTH  => 4,
              START_TIME       => 5,    # start processing worklist
              LAST_UPDATE      => 6,    # number files processed at last update
              INTERVAL_START   => 7,
              INTERVAL_COUNTS  =>
                  8,    # source files and coverpoints found since update

              FILE_COUNT     => 0,
              LINE_COUNT     => 1,
              BRANCH_COUNT   => 2,
              FUNCTION_COUNT => 3,
};

sub new
{
    my ($class, $totalFiles, $nChunks, $chunkSize, $intervalLength) = @_;
    my $start = Time::HiRes::gettimeofday();
    my $self = [$totalFiles, $nChunks, $chunkSize, 0,
                $intervalLength, $start, 0, $start,
                [0, 0, 0, 0]
    ];
    bless $self, $class;
    return $self;
}

sub checkUpdate
{
    my ($self, $processedFiles) = @_;

    if ($self->[INTERVAL_LENGTH] &&
        $self->[INTERVAL_LENGTH] <= ($processedFiles - $self->[LAST_UPDATE])) {
        my $filesLastInterval = $processedFiles - $self->[LAST_UPDATE];
        $self->[LAST_UPDATE] = $processedFiles;
        my $now          = Time::HiRes::gettimeofday();
        my $elapsed      = $now - $self->[START_TIME];
        my $intervalTime = $now - $self->[INTERVAL_START];
        my $rate         = ($processedFiles) / $elapsed;
        my $average      = $totalChildCpuTime / ($processedFiles);
        my $interval     = $intervalChildCpuTime / $filesLastInterval;
        $intervalChildCpuTime = 0;
        $self->[INTERVAL_START] = $now;
        # compute average wall clock files/s and individual s/file (actual CPU)
        #  for overall processing of all files and for files during this
        #  interval.  Might be useful to observe performance issues during
        #  execution.
        my $total = $self->[TOTAL_FILES];
        lcovutil::info(
            "elapsed:%0.1fm: remaining:%d files %0.1fm: %0.2f files/s %0.2f s/file (interval:%0.2f f/s %0.2f s/f)\n",
            $elapsed / 60,
            $total - $processedFiles,
            ($total - $processedFiles) / ($rate * 60),
            $rate,
            $average,
            $filesLastInterval / $intervalTime,
            $interval);
        # how many new source files and new coverpoints have been found
        #   in this interval?
        if (defined($trace_data)) {
            my @counts       = $trace_data->count_totals();
            my $intervalData = $self->[INTERVAL_COUNTS];
            if ($counts[0] !=
                $intervalData->[FILE_COUNT] ||    # number source files
                $counts[1]->[0] !=
                $intervalData->[LINE_COUNT] ||    # line coverpoints
                $counts[2]->[0] !=
                $intervalData->[BRANCH_COUNT] ||    # branch coverpoints
                $counts[3]->[0] != $intervalData->[FUNCTION_COUNT]
                )    # function coverpoints
            {
                lcovutil::info(1,
                               "  added files:%d ln:%d br:%d fn:%d\n",
                               $counts[0] - $intervalData->[FILE_COUNT],
                               $counts[1]->[0] - $intervalData->[LINE_COUNT],
                               $counts[2]->[0] - $intervalData->[BRANCH_COUNT],
                               $counts[3]->[0] - $intervalData->[FUNCTION_COUNT]
                );
                $self->[INTERVAL_COUNTS] = [$counts[0], $counts[1]->[0],
                                            $counts[2]->[0], $counts[3]->[0]
                ];
            }
        }
    }
}

package BuildWorkList;

sub new
{
    my $class = shift;
    my $self  = [[], {}, {}];    # [worklist, processedFiles, messages]
    return bless $self, $class;
}

sub worklist
{
    my $self = shift;
    # we saved the messages for the end...
    foreach my $msg (values %{$self->[2]}) {
        lcovutil::ignorable_error($msg->[0], $msg->[1]);
    }
    return $self->[0];
}

sub find_corresponding_gcno_file
{
    my ($gcda_file, $searchdir) = @_;
    my ($name, $d, $e) = File::Basename::fileparse($gcda_file, qr/\.[^.]*/);
    my $gcno_file = File::Spec->catfile($d, $name . ".gcno");
    foreach ($gcno_file) {
        return $gcno_file if (-f $gcno_file || -l $gcno_file);

        my $alt    = lcovutil::subst_file_name($gcno_file);
        my $prefix = "looking for GCNO matching '$gcda_file':\n";
        if ($alt ne $gcno_file) {
            $gcno_file = $alt;
            lcovutil::info(1, "$prefix  at '$gcno_file'\n");
            $prefix = '';
            return $gcno_file if (-f $gcno_file || -l $gcno_file);
        }
        # check to see if this file is in a different directory
        my $dir = $d;
        $dir =~ s#^$searchdir##g;
        $prefix = "looking for GCNO matching '$gcda_file' prefix:'$dir':\n";
        # 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.
        foreach my $d (@$buildDirSearchPath) {
            # is the GCNO in the relative path from build_dir?
            my $build_directory = $d->[0];
            $gcno_file =
                File::Spec->catfile($build_directory, $dir, $name . ".gcno");

            lcovutil::info(1, "$prefix  at '$gcno_file'\n");
            $prefix = '';
            if (-f $gcno_file || -l $gcno_file) {
                ++$d->[1];
                return $gcno_file;
            }

            # is the GCNO in the relative path after we apply substitutions?
            $alt = lcovutil::subst_file_name($gcno_file);
            if ($alt ne $gcno_file) {
                $gcno_file = $alt;
                lcovutil::info(1, "$prefix  at '$gcno_file'\n");
                $prefix = '';
                if (-f $gcno_file || -l $gcno_file) {
                    ++$d->[1];
                    return $gcno_file;
                }
            }
        }    # foreach build_directory

        if (@lcovutil::resolveCallback) {
            $gcno_file = File::Spec->catfile($d, $name . ".gcno");

            $gcno_file = SearchPath::resolveCallback($gcno_file, 1);
            return $gcno_file if (-f $gcno_file || -l $gcno_file);
        }
    }    # foreach switch

    # skip the .gcda file if there is no .gcno
    lcovutil::ignorable_error(
        $lcovutil::ERROR_PATH,
        (lcovutil::is_ignored($lcovutil::ERROR_PATH) ? 'skipping' :
             'cannot process') .
            " .gcda file $gcda_file because corresponding .gcno file '$gcno_file' is missing"
            .
            (
            $lcovutil::verbose ||
                lcovutil::message_count($lcovutil::ERROR_PATH) == 0 ?
                " (see the '--build-directory' entry in 'man geninfo' for suggestions)"
            :
                '') .
            '.');

    return undef;
}

sub add_worklist_entry
{
    my ($self, $filename, $directory) = @_;
    if (exists($self->[1]->{$filename})) {
        lcovutil::ignorable_error(
                                 $lcovutil::ERROR_USAGE,
                                 "duplicate file $filename in both " .
                                     $self->[1]->{$filename} . " and $directory"
                                     .
                                     (lcovutil::is_ignored(
                                                       $lcovutil::ERROR_USAGE) ?
                                          ' (skip latter)' :
                                          ''));
        return;
    }
    $self->[1]->{$filename} = $directory;

    my ($gcda_file, $gcno_file);
    if ($filename !~ /$data_file_extension$/) {
        $gcno_file = $filename;
    } else {
        $gcda_file = $filename;
        $gcno_file = find_corresponding_gcno_file($filename, $directory);
        return unless $gcno_file;    # would have errored out

        my ($vol, $dir, $name) = File::Spec->splitpath($gcno_file);
        # remove trailing slash
        $self->[1]->{$gcno_file} = substr($dir, 0, -1);
    }
    push(@{$self->[0]}, [$directory, $gcda_file, $gcno_file]);
}

sub find_files
{
    my $self        = shift;
    my $processGcno = shift;
    my ($type, $ext);

    if ($processGcno) {
        $type = "graph";
        $ext  = $graph_file_extension;
    } else {
        $type = "data";
        $ext  = $data_file_extension;
    }
    foreach my $directory (@_) {
        unless (-e $directory) {
            # hold error until the end of processing - we might be looking
            #  for both .gcno and .gcda files - and don't want to generate
            #   the same 'no such directory' message twice.  Nor do we want
            #  to produce an error if only one type of file is in this
            # directory
            $self->[2]->{$directory} = [$lcovutil::ERROR_USAGE,
                                        "no such file or directory '$directory'"
            ];
            next;
        }
        if (-d $directory) {
            lcovutil::info("Scanning $directory for $ext files ...\n");

            my $now    = Time::HiRes::gettimeofday();
            my $follow = $lcovutil::opt_follow ? '-follow' : '';
            my ($stdout, $stderr, $code) = Capture::Tiny::capture {
                system(
                    "find '$directory' $maxdepth $follow -name \\*$ext -type f -o -name \\*$ext -type l"
                );
            };
            lcovutil::ignorable_error($lcovutil::ERROR_UTILITY,
                                  "error in 'find \"$directory\" ...': $stderr")
                if ($code);

            my $time = Time::HiRes::gettimeofday() - $now;
            if (exists($lcovutil::profileData{find}) &&
                exists($lcovutil::profileData{find}{$directory})) {
                $lcovutil::profileData{find}{$directory} += $time;
            } else {
                $lcovutil::profileData{find}{$directory} = $time;
            }
            # split on crlf
            my @found = split(/[\x0A\x0D]/, $stdout);
            if (!@found) {
                if (!defined($processGcno) || $processGcno != 2) {
                    # delay message: might be a file of other type here
                    $self->[2]->{$directory} = [
                                             $lcovutil::ERROR_EMPTY,
                                             "no $ext files found in $directory"
                    ];
                }

                next;
            }
            # we found something here - remove the pending error message, if any
            delete($self->[2]->{$directory})
                ;    # if exists($self->[2]->{$directory});
            lcovutil::info("Found %d %s files in %s\n",
                           scalar(@found), $type, $directory);
            # keep track of directory where we found the file
            foreach my $entry (@found) {
                # don't add gcno file again, if we already handled the .gcda
                $self->add_worklist_entry($entry, $directory)
                    unless (defined($processGcno) &&
                            $processGcno == 2 &&
                            exists($self->[1]->{$entry}));
            }
        } elsif (!defined($processGcno) || $processGcno == 1) {
            my ($name, $d, $e) =
                File::Basename::fileparse($directory, qr/\.[^.]*/);
            if ($e ne $ext &&
                (!$lcovutil::geninfo_captureAll || $e ne $graph_file_extension)
            ) {
                $self->[2]->{$directory} = [
                       $lcovutil::ERROR_USAGE,
                       "$directory has unsupported extension: expected '$ext'" .
                           ($initial ? " for initial capture" : "")
                ];
                next;
            }
            delete($self->[2]->{$directory});
            # use the directory where we find the file as the base dir
            $self->add_worklist_entry($directory, $d);
        }
    }
}

package main;

#
# gen_info(directory_list)
#
# Traverse each DIRECTORY in list 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.  See geninfo(1) man page for
# more details.
#
# 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.
#

my $chunkSize;

sub _process_one_chunk($$$$)
{
    my ($chunk, $chunkId, $combined, $pid) = @_;

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

    my $idx = 0;
    foreach my $data (@$chunk) {
        my $now = Time::HiRes::gettimeofday();
        if (defined($pid) &&
            0 != $pid) {
            # if parent died, then time for me to go
            lcovutil::check_parent_process();
        }

        my ($searchdir, $gcda_file, $gcno_file) = @$data;

        # "name" will be .gcno if "$initial" else will be $gcda
        my $name = defined($gcda_file) ? $gcda_file : $gcno_file;
        info(1,
             "Processing $name%s\n",
             defined($pid) ? " in child $pid" : "" . "\n");
        my $context = MessageContext->new("capturing from $name");

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

        # keep track of order - so we can estimate which files were processed
        # at same time
        $lcovutil::profileData{order}{$name} =
            ($chunkId * $chunkSize) + $idx;
        ++$idx;

        my $trace;
        if ($intermediate) {
            $trace =
                process_intermediate($searchdir, $gcda_file,
                                     $gcno_file, $tmp->dirname);
        } elsif (!defined($gcda_file)) {
            # 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}{$name} = $then - $now;

        if (defined($trace)) {

            if (!$single_file) {
                # Create one .info file per input file
                $trace->applyFilters();
                $trace->add_comments(@lcovutil::comments);
                $trace->write_info_file(solve_relative_path(
                                                           $cwd, $name . ".info"
                                        ),
                                        $lcovutil::verify_checksum);
                my $then = Time::HiRes::gettimeofday();
                $lcovutil::profileData{filter_file}{$name} = $then - $now;
                $files_created++;
            } else {
                if (defined($combined)) {
                    $combined->merge_tracefile($trace, TraceInfo::UNION);
                } else {
                    $combined = $trace;
                }
                my $end = Time::HiRes::gettimeofday();
                $lcovutil::profileData{append}{$name} = $end - $then;
            }
        }
        my $done = Time::HiRes::gettimeofday();
        die("unexpected duplicate file '$name'")
            if exists($lcovutil::profileData{file}{$name});
        $lcovutil::profileData{file}{$name} = $done - $now;
    }
    my $end = Time::HiRes::gettimeofday();

    return $combined;
}

sub _merge_one_child($$$)
{
    my ($children, $tempFileExt, $worklist) = @_;

    my $child       = wait();
    my $start       = Time::HiRes::gettimeofday();
    my $childstatus = $?;
    unless (exists($children->{$child})) {
        lcovutil::report_unknown_child($child);
        return 0;
    }

    debug(
        "_merge_one_child: $child (parent $$) status $childstatus from $tempFileDir\n"
    );
    my ($chunk, $forkAt, $chunkId) = @{$children->{$child}};
    my $dumped   = File::Spec->catfile($tempFileDir, "dumper_$child");
    my $childLog = File::Spec->catfile($tempFileDir, "geninfo_$child.log");
    my $childErr = File::Spec->catfile($tempFileDir, "geninfo_$child.err");

    foreach my $f ($childLog, $childErr) {
        if (!-f $f) {
            # no data was printed..
            $f = '';
            next;
        }
        if (open(RESTORE, "<", $f)) {
            # slurp into a string and eval..
            my $str = do { local $/; <RESTORE> };    # slurp whole thing
            close(RESTORE) or die("unable to close $f: $!\n");
            unlink $f;
            $f = $str;
        } else {
            $f = "unable to open $f: $!";
            if (0 == $childstatus) {
                report_parallel_error('geninfo', $ERROR_PARALLEL, $child, 0,
                                      $f, keys(%$children));
            }
        }
    }
    my $signal = $childstatus & 0xFF;
    print(STDOUT $childLog)
        if ((0 != $childstatus &&
             $signal != POSIX::SIGKILL &&
             $lcovutil::max_fork_fails != 0) ||
            $lcovutil::verbose);
    print(STDERR $childErr);
    # look for spaceout message in the gcov log
    if (0 == $signal                                &&
        0 != $childstatus                           &&
        0 != $lcovutil::max_fork_fails              &&
        lcovutil::is_ignored($lcovutil::ERROR_FORK) &&
        grep(
            { /(std::bad_alloc|annot allocate memory|out of memory|integretity check failed for compressed file)/
            } ($childLog, $childErr))
    ) {

        # pretend it was killed so we retry
        $signal = POSIX::SIGKILL;
    }
    my $data = Storable::retrieve($dumped)
        if (-f $dumped && 0 == $childstatus);
    # note that $data will not be defined (no data dumped) if there was
    #  no child data extracted (e.g., all files excluded)
    if (defined($data)) {
        eval {
            my ($childInfo, $buildDirCounts, $counts, $updates) = @$data;
            lcovutil::update_state(@$updates);
            $files_created  += $counts->[0];
            $processedFiles += $counts->[1];
            my $childFinish = $counts->[2];
            $buildDirSearchPath->update_count(@$buildDirCounts);
            my $childCpuTime = $lcovutil::profileData{child}{$chunkId};
            $totalChildCpuTime    += $childCpuTime;
            $intervalChildCpuTime += $childCpuTime;

            my $now = Time::HiRes::gettimeofday();
            $lcovutil::profileData{undump}{$chunkId} = $now - $start;
            if (defined($childInfo)) {
                if (defined($trace_data)) {
                    $trace_data->merge_tracefile($childInfo, TraceInfo::UNION);
                    my $final = Time::HiRes::gettimeofday();
                    $lcovutil::profileData{append}{$chunkId} = $final - $now;
                } else {
                    $trace_data = $childInfo;
                }
                my $end = Time::HiRes::gettimeofday();
                $lcovutil::profileData{merge}{$chunkId} = $end - $now;
                $lcovutil::profileData{queue}{$chunkId} = $start - $childFinish;
            }
            $intervalMonitor->checkUpdate($processedFiles);
        };
        if ($@) {
            $childstatus = 1 << 8 unless $childstatus;
            print STDOUT $@;
            report_parallel_error('geninfo', $ERROR_PARALLEL, $child,
                              $childstatus, "unable to deserialize $dumped: $@",
                              keys(%$children));
        }
    }
    if ($childstatus != 0) {
        if (POSIX::SIGKILL == $signal) {
            if (exists($childRetryCounts{$chunkId})) {
                $childRetryCounts{$chunkId} += 1;
            } else {
                $childRetryCounts{$chunkId} = 1;
            }
            lcovutil::report_fork_failure(
                                 "compute job $chunkId",
                                 "killed by OS - possibly due to out-of-memory",
                                 $childRetryCounts{$chunkId});
            push(@$worklist, $chunk);
        } else {
            report_parallel_error('geninfo', $ERROR_CHILD, $child, $childstatus,
                                  "ignoring data in chunk $chunkId",
                                  keys(%$children));
        }
    }
    foreach my $f ($dumped) {
        unlink $f
            if -f $f;
    }
    my $to = Time::HiRes::gettimeofday();
    $lcovutil::profileData{chunk}{$chunkId} = $to - $forkAt;
    if (exists($lcovutil::profileData{process}{$chunkId}) &&
        exists($lcovutil::profileData{merge}{$chunkId})) {
        $lcovutil::profileData{work}{$chunkId} =
            $lcovutil::profileData{process}{$chunkId} +
            $lcovutil::profileData{merge}{$chunkId};
    }
    return 0 == $childstatus;
}

sub gen_info(@)
{
    my $builder = BuildWorkList->new();

    $builder->find_files($initial, @_);
    if (!defined($initial) &&
        defined($lcovutil::geninfo_captureAll) &&
        $lcovutil::geninfo_captureAll) {
        $builder->find_files(2, @_);
    }
    my $filelist = $builder->worklist();
    my @sorted_filelist;
    if ($lcovutil::sort_inputs) {
        @sorted_filelist = sort({
                                    my $na = $a->[0] .
                                        (defined($a->[1]) ? $a->[1] : $a->[2]);
                                    my $nb = $b->[0] .
                                        (defined($b->[1]) ? $b->[1] : $b->[2]);
                                    $na cmp $nb
        } @$filelist);
        $filelist = \@sorted_filelist;
    }
    my $total = scalar(@$filelist);
    if (1 < $lcovutil::maxParallelism) {
        my $floor =
            $lcovutil::maxParallelism ?
            (int($total / $lcovutil::maxParallelism)) :
            1;
        if (defined($lcovutil::defaultChunkSize)) {
            if ($lcovutil::defaultChunkSize =~ /^(\d+)\s*(%?)$/) {
                if (defined($2) && $2) {
                    # a percentage
                    $chunkSize = int($total * $1 / 100);
                } else {
                    # an absolute value
                    $chunkSize = $1;
                }
            } else {
                lcovutil::ignorable_error($lcovutil::ERROR_FORMAT,
                    "geninfo_chunk_size '$lcovutil::defaultChunkSize' is not recognized"
                );
            }
        }
        # Need to balance time in child vs. time to merge child data -
        #   - if we have too many children, then they finish and wait in the
        #     queue to be merged.
        #   - if we have too few, then the merge time in child gets long
        # build up in set to 80% of
        $chunkSize = int(0.8 * $floor) unless defined($chunkSize);

        $chunkSize = 1 if $chunkSize < 2;
    } else {
        $chunkSize = 1;
    }
    my @worklist;
    my $serialChunk = [1, []];
    my $chunk       = [0, []];    # [isSerial, [fileList]]
    FILE: foreach my $j (@$filelist) {
        my ($dir, $gcda, $gcno) = @$j;
        foreach my $f ($gcda, $gcno) {
            next unless defined($f);    # might not be a GCDA file
            my $filename = $dir . $lcovutil::dirseparator . $f;
            if (grep({ $filename =~ $_ } @main::large_files)) {
                lcovutil::info(1, "large file: $filename\n");
                push(@{$serialChunk->[1]}, $j);
                next FILE;
            }
        }
        push(@{$chunk->[1]}, $j);
        if (scalar(@{$chunk->[1]}) == $chunkSize) {
            push(@worklist, $chunk);
            $chunk = [0, []];
        }
    }    #foreach DATA_FILE
    push(@worklist, $chunk) if @{$chunk->[1]};
    # serial chunk is at the top of the stack - so serial processing
    #  happens before we fork multiple processes
    push(@worklist, $serialChunk)
        if (@{$serialChunk->[1]});

    # Process all files in list
    my $currentParallel = 0;
    my %children;
    my $tempFileExt = '';
    $tempFileExt = ".gz"
        if (defined $output_filename) && $output_filename =~ /\.gz$/;

    my $totalChunks     = scalar(@worklist);
    my $processedChunks = 0;

    # process at least 5% of files before printing stats
    $lcovutil::defaultInterval = 5 unless defined($lcovutil::defaultInterval);

    my $intervalLength = int($total * $lcovutil::defaultInterval / 100);
    my $start          = Time::HiRes::gettimeofday();

    $intervalMonitor =
        IntervalMonitor->new($total, $totalChunks, $chunkSize, $intervalLength);

    lcovutil::info("using: chunkSize: %d, nchunks:%d, intervalLength:%d\n",
                   $chunkSize, $totalChunks, $intervalLength);
    $lcovutil::profileData{chunkSize} = $chunkSize;
    $lcovutil::profileData{nChunks}   = $totalChunks;
    $lcovutil::profileData{interval}  = $intervalLength;
    $lcovutil::profileData{nFiles}    = $total;
    $lcovutil::maxParallelism         = 1 unless scalar(@worklist) > 1;

    my $failedAttempts = 0;
    do {
        CHUNK: while (@worklist) {

            my $chunk = pop(@worklist);
            ++$processedChunks;

            if (1 < $lcovutil::maxParallelism &&
                1 != $chunk->[0]) {

                my $currentSize = 0;
                if (0 != $lcovutil::maxMemory) {
                    $currentSize = lcovutil::current_process_size();
                }
                if ($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);

                    $currentParallel -=
                        _merge_one_child(\%children, $tempFileExt, \@worklist);
                    # put the job back in the list
                    --$processedChunks;
                    push(@worklist, $chunk);
                    next CHUNK;
                }

                $lcovutil::deferWarnings = 1;
                my $now = Time::HiRes::gettimeofday();
                my $pid = fork();
                if (!defined($pid)) {
                    # fork failed
                    ++$failedAttempts;
                    lcovutil::report_fork_failure("process chunk",
                                                  $!, $failedAttempts);
                    --$processedChunks;
                    push(@worklist, $chunk);
                    next CHUNK;
                }
                $failedAttempts = 0;
                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();
                    my $currentState = lcovutil::initial_state();
                    $buildDirSearchPath->reset();
                    $output_filename =
                        File::Spec->catfile($tempFileDir,
                                            "geninfo_$$.info" . $tempFileExt);
                    my $childInfo;
                    # set count to zero so we know how many got created in
                    # the child process
                    $files_created = 0;
                    my $now = Time::HiRes::gettimeofday();
                    # 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 to directly open/reopen the STDOUT and STDERR
                    #   descriptors due to interactions between the child and parent
                    #   processes (see the Capture::Tiny doc for some details)
                    my $status = 0;
                    my ($stdout, $stderr, $code) = Capture::Tiny::capture {
                        eval {
                            $childInfo =
                                _process_one_chunk($chunk->[1],
                                              $processedChunks, $childInfo, $$);
                        };
                        if ($@) {
                            $status = 1;         # error
                            print(STDERR $@);    # capture messages in $stderr
                        }
                    };
                    # parent might have already caught an error, cleaned up and
                    #  removed the tempdir and exited.
                    lcovutil::check_parent_process();

                    # print stdout and stderr ...
                    foreach my $d (['kog', $stdout], ['err', $stderr]) {
                        my ($ext, $str) = @$d;
                        # only print if there is something to print
                        next
                            unless ($str);
                        my $tmpf =
                            File::Spec->catfile($tempFileDir,
                                                "geninfo_$$.$ext");
                        my $f = InOutFile->out($tmpf);
                        my $h = $f->hdl();
                        print($h $str);
                    }
                    my $dumpf = File::Spec->catfile($tempFileDir, "dumper_$$");
                    foreach my $f ($output_filename) {
                        unlink $f
                            if -f $f && !$lcovutil::preserve_intermediates;
                    }
                    my $buildDirCounts = $buildDirSearchPath->current_count();

                    my $then = 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}{$processedChunks} =
                        $then - $now;

                    $lcovutil::profileData{child}{$processedChunks} =
                        $then - $childStart;
                    # dump parsed data - then read back and merge
                    my $data;
                    eval {
                        # NOTE:  not storing anything if we extracted nothing/
                        #  there is no childInfo data
                        $data =
                            Storable::store(
                                       [$single_file ? $childInfo : undef,
                                        $buildDirCounts,
                                        [$files_created, scalar(@{$chunk->[1]}),
                                         $then
                                        ],
                                        lcovutil::compute_update($currentState)
                                       ],
                                       $dumpf) if defined($childInfo);
                    };
                    if ($@ || (defined($childInfo) && !defined($data))) {
                        lcovutil::ignorable_error($lcovutil::ERROR_PARALLEL,
                              "Child $$ serialize failed" . ($@ ? ": $@" : ''));
                    }
                    exit($status);
                } else {
                    # I'm the parent
                    $children{$pid} = [$chunk, $now, $processedChunks];
                    ++$currentParallel;
                }
            } else {
                # not parallel..
                my $saveParallel = $lcovutil::maxParallelism;
                $lcovutil::maxParallelism = 1;
                if ($chunk->[0]) {
                    my $num = scalar(@{$chunk->[1]});
                    lcovutil::info(
                               "Processing $num file" . ($num == 1 ? '' : 's') .
                                   " from chunk 0 serially\n");
                }
                my $now = Time::HiRes::gettimeofday();
                $trace_data =
                    _process_one_chunk($chunk->[1], $processedChunks,
                                       $trace_data, undef);
                $processedFiles += scalar(@{$chunk->[1]});
                if ($chunk->[0]) {
                    lcovutil::info("Finished processing chunk 0\n");
                }
                my $then = Time::HiRes::gettimeofday();
                $lcovutil::maxParallelism = $saveParallel;
                $lcovutil::profileData{process}{$processedChunks} =
                    $then - $now;
            }
        }    # end foreach

        while ($currentParallel != 0) {
            $currentParallel -=
                _merge_one_child(\%children, $tempFileExt, \@worklist);
        }
        # wrap in outer loop in case we get a spaceout/child failure
        #  during 'tail' processing.
    } while (@worklist);

    info("Finished processing %d "
             .
             ($initial ? 'GCNO' :
                  ($lcovutil::geninfo_captureAll ? 'GCDA/GCNO' : 'GCDA')) .
             " file%s\n",
         $processedFiles,
         1 == $processedFiles ? '' : 's');
    # 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 @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_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) {
        lcovutil::ignorable_error($lcovutil::ERROR_EMPTY, "empty $bb_filename");
        chdir($cwd) or die("can't cd back to $cwd: $!");
        return undef;
    }

    # 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.
    ($instr, $graph) = read_gcno($bb_filename);

    # Try to find base directory automatically if requested by user
    if ($lcovutil::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("cannot create link $gcda: $!\n");
        my $gcno = File::Spec->catfile($tempdir,
                                       File::Basename::basename($bb_filename));
        symlink($bb_filename, $gcno) or
            die("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) {
        lcovutil::ignorable_error($lcovutil::ERROR_EMPTY,
                      "gcov did not create any files for " . "$da_filename!\n");
    }

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

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

            my ($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 = ReadCurrentSource::resolve_path($source, 1);

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

            # Skip files that are not mentioned in the graph file
            if (!@matches) {
                lcovutil::ignorable_error($lcovutil::ERROR_MISMATCH,
                             "cannot find an entry for " .
                                 $gcov_file . " in $graph_file_extension file");
                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])) {
                lcovutil::ignorable_error($lcovutil::ERROR_CORRUPT,
                                          "$gcov_file is unreadable");
                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) {
                lcovutil::ignorable_error($lcovutil::ERROR_EMPTY,
                                          "$gcov_file is empty");
                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 =
                ReadCurrentSource::resolve_path($source_filename, 1);

            if (TraceFile::skipCurrentFile($source_filename)) {
                $lcovutil::excluded_files{$source_filename} = 1;
                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) {
                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, $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 ($lcovutil::opt_adjust_unexecuted_blocks) {
                                $hit = 0;
                            } elsif (lcovutil::warn_once(
                                                        "unexecuted block",
                                                        $ERROR_INCONSISTENT_DATA
                            )) {
                                lcovutil::ignorable_warning(
                                    $ERROR_INCONSISTENT_DATA,
                                    "$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."
                                );
                            }
                        }
                    }
                    $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()->union($lineMap);
            $fileData->sumbr()->union($branchMap);
            $fileData->func()->union($functionMap);

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

    return $traceFile;
}

#
# compute_internal_directories(list)
#
# Walk directory tree to find set of 'internal' directories
#   - soft links within the 'internal' tree which point to directories outside
#     the tree cause those target directories to be considered 'internal' if
#     the "--follow" flag is specified - else, 'external'
#

sub compute_internal_directories(@)
{
    my @dirstack;
    foreach my $entry (@_) {
        next unless (defined($entry));
        # do not apply substitution patterns to the dir name -
        #   if user really wants that, they can specify a substitution and/or
        #   include/exclude patterns
        my $p = solve_relative_path($cwd, $entry);
        push(@dirstack, $p)
            if $lcovutil::opt_follow && !grep($p eq $_, @dirstack);
        push(@lcovutil::internal_dirs, $p)
            unless
            grep($p eq $_ || ($lcovutil::case_insensitive && lc($p) eq lc($_)),
                 @lcovutil::internal_dirs);
        if (!file_name_is_absolute($entry) &&
            $entry ne $p) {
            push(@lcovutil::internal_dirs, $entry)
                unless grep($entry eq $_ || ($lcovutil::case_insensitive &&
                                             lc($entry) eq lc($_)),
                            @lcovutil::internal_dirs);
        }
    }

    my %visited;
    while (@dirstack) {
        my $top = pop(@dirstack);
        if (-l $top) {
            my $t = Cwd::realpath($top);
            die("expected directory found '$t'") unless -d $t;
            unless (exists($visited{$t})) {
                lcovutil::info(1,
                            "internal directory: target '$t' of link '$top'\n");
                $visited{$t} = $top;
                push(@dirstack, $t);
                push(@lcovutil::internal_dirs, $t)
                    if lcovutil::is_external($t);
            }
            next;
        }
        opendir(my $dh, $top) or die("can't open directory: $!");
        while (my $e = readdir($dh)) {
            next if $e eq '.' || $e eq '..';
            my $p = File::Spec->catfile($top, $e);
            if (-l $p) {
                my $l = Cwd::realpath($p);
                push(@dirstack, $p) if (-d $l);
            } elsif (-d $p) {
                push(@dirstack, $p) unless exists($visited{$p});
                $visited{$p} = $top;
            }
        }    # while
        closedir($dh);
    }

    if (@lcovutil::internal_dirs) {
        lcovutil::info("Recording 'internal' directories:\n\t" .
                       join("\n\t", @lcovutil::internal_dirs) . "\n");
    }

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

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

sub solve_relative_path($$)
{
    my ($path, $dir) = @_;

    # Convert from Windows path to msys path
    if ($^O eq "msys") {
        # search for a windows drive letter at the beginning
        my ($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);
            my @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);
        }
    }

    my $result = $dir;
    # Prepend path if not absolute
    if (!File::Spec->file_name_is_absolute($dir)) {
        $result = File::Spec->catfile($path, $result);
    }
    # can't just use Cwd::abs_path on the pathname because it understands
    #  soft links and resolves them - so we end up pointing to the actual file
    #  and not to the carefully constructed linked-build paths that the user
    #  wanted. Have to do it the hard way
    # return Cwd::abs_path($result);

    # Remove // in favor of /
    my $d = $lcovutil::dirseparator;
    $result =~ s#$d$d#$d#g;

    # Remove . in middle or at end of path
    while ($result =~ s#$d\.($d|$)#$d#) {
    }

    # Remove trailing /
    $result =~ s#$d$##g if ($result ne $d);

    # change "X/dirname/../Y" into "X/Y"
    while ($result =~ s#$d[^$d]+$d\.\.($d|$)#$d#) {
    }
    # change "dirname/../Y" into "./Y" (i.e,, at head of path)
    $result =~ s#^[^$d]+\/\.\.$d#.$d#;

    # Remove preceding ..
    $result =~ s#^$d\.\.$d#$d#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);
        # Filename component must match
        if ($efile ne $file) {
            next;
        }
        # Check directory components last to first for match
        my @ecomp  = splitdir($edir);
        my $ecomps = scalar(@ecomp);
        if ($ecomps < $comps) {
            next;
        }
        for (my $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];
    local *SOURCE;

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

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

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

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

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

        close(SOURCE) or die("unable to close $filename: $!\n");

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

    die("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) or die("unable to close $_[0]: $!\n");

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

    # 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;
    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 unless $lcovutil::br_coverage;
            # $1 is block ID, $2 is hit count
            my $count = $2;
            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 unless $lcovutil::br_coverage;
            # 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 unless $lcovutil::func_coverage;
            my $name  = $1;
            my $count = $2;
            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_FORMAT,
                    "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;
            # 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 {
                # Check for zero count
                if ($count =~ /^[#=]/) {
                    $count = 0;
                }
                push(@result, 1, $unexec ? [$count, $unexec] : $count, $code);
            }
        }
    }
    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;
            #filename will be simplified/sustituted in 'adjust_source_filenames'
        } 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) = @_;
    # intermediate JSON contains the demangled name
    my $json = JsonSupport::load($gcov_filename);    # imported from lcovutil.pm
    if (!defined($json) ||
        !exists($json->{"files"}) ||
        ref($json->{"files"} ne "ARRAY")) {
        die("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"};
        $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();

    while (my ($filename, $fileStr) = each(%$data)) {
        # note that we already substituted the source file name and handled
        # include/exclude directives - so no need to check here
        # see 'adjust_source_filenames() and 'filter_source_files()

        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) {
            my $version = lcovutil::extractFileVersion($filename);
            $fileData->version($version)
                if (defined($version) && $version ne "");
        }
        for my $line (split(/\n/, $fileStr)) {
            if ($line =~ /^lcount:(\d+),(\d+),?/) {
                # lcount:<line>,<count>
                # lcount:<line>,<count>,<has_unexecuted_blocks>
                my $lineNo = $1;
                my $hit    = $2;
                $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);
                my $func =
                    $functionMap->define_function($name, $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()->union($lineMap);
        $fileData->sumbr()->union($branchMap);
        $fileData->func()->union($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");
    while (my ($filename, $file_data) = each(%$data)) {
        # note that we already substituted the source file name and handled
        # include/exclude directives - so no need to check here
        # see 'adjust_source_filenames() and 'filter_source_files()
        # 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);
        my $mcdcMap     = $fileData->testcase_mcdc($test_name);
        lcovutil::debug(1, "parse $filename\n");

        if (@lcovutil::extractVersionScript) {
            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"};
                my $func =
                    $functionMap->define_function($name, $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"};
            my $conditions = $d->{'conditions'}
                if $lcovutil::mcdc_coverage && exists($d->{'conditions'});

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

            if (0 == scalar(@$branches) && $unexec && $count != 0) {
                lcovutil::debug(
                    "$filename:$line: unexecuted block on non-branch line with count=$count\n"
                );
                if ($lcovutil::opt_adjust_unexecuted_blocks) {
                    $count = 0;
                } elsif (
                         lcovutil::warn_once("unexecuted block",
                                             $ERROR_INCONSISTENT_DATA)
                ) {
                    lcovutil::ignorable_warning($ERROR_INCONSISTENT_DATA,
                        "$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."
                    );
                }
            }

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

                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 (!defined($brcount) || $unexec) {
                        $brcount = "-";
                    }
                    my $entry =
                        BranchBlock->new($branch_num, $brcount, undef,
                                  defined($is_exception) && $is_exception != 0);
                    # "block" is always zero for intermediate JSON data
                    $branchMap->append($line, 0, $entry, $filename);
                    ++$branch_num;
                }
            }
            if ($conditions && @$conditions) {
                #die("unexpected multiple conditions at $line") if scalar(@$conditions) > 1;
                #lcovutil::debug(1, "MCDC at $filename:$line\n");
                my $mcdc = $mcdcMap->new_mcdc($fileData, $line);

                foreach my $c (@$conditions) {
                    my $count    = $c->{count};
                    my $exprs    = $count / 2;
                    my $coverage = $c->{covered};
                    my $false    = $c->{not_covered_false};
                    my $true     = $c->{not_covered_true};
                    die("expected even condition count: $count") if $count % 2;
                    my @true;
                    my @false;

                    foreach my $m (sort @$false) {
                        $false[$m] = 0;
                    }
                    foreach my $m (sort @$true) {
                        $true[$m] = 0;
                    }
                    for (my $i = 0; $i < $exprs; ++$i) {
                        $mcdc->insertExpr($filename, $exprs, 0,
                                          !defined($false[$i]),
                                          $i, $i);
                        $mcdc->insertExpr($filename, $exprs, 1,
                                          !defined($true[$i]),
                                          $i, $i);
                    }
                }
                $mcdcMap->close_mcdcBlock($mcdc);
            }
        }
        # now go through lines, functions, branches - append to test_name data
        $fileData->sum()->union($lineMap);
        $fileData->sumbr()->union($branchMap);
        $fileData->mcdc()->union($mcdcMap);
        $fileData->func()->union($functionMap);
    }
    return $traceFile;
}

sub which($)
{
    my $filename = shift;

    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 $data_file;
    my $errmsg;
    my $errorType = $lcovutil::ERROR_GCOV;
    my %data;
    my $base;
    my $json_basedir;
    my $json_format;

    my $filename = defined($file) ? $file : $gcno_file;
    $file = solve_relative_path($cwd, $filename);
    my ($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: $!";
            $errorType = $lcovutil::ERROR_PATH;
            goto err;
        }
    } else {
        $gcno_file = solve_relative_path($cwd, $gcno_file);
        foreach my $f ($file, $gcno_file) {
            my $p = -l $f ? Cwd::abs_path($f) : $f;
            if (!-r $p) {
                $errmsg    = "$f does not exist/is not readable";
                $errorType = $lcovutil::ERROR_PATH;
                goto err;
            }
        }
        # if .gcda and .gcno files are in the same directory, then simply
        #   process in place - otherwise, link the .gcda and .gcno files
        #   into tempdir and run from here
        if (dirname($file) eq dirname($gcno_file)) {
            # Process data file in place
            $data_file = $file;
        } else {
            $data_file = basename($file);
            foreach my $f ($file, $gcno_file) {
                my $l = File::Spec->catfile($tempdir, basename($f));
                debug("create links to process $f in $tempdir\n");
                symlink($f, $l);
                # unclear why symlink returns an error code when it actually
                #  created the link
                if ($? && !-l $l) {
                    $errmsg = "unable to create link for $f: $!";
                    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();
    my ($out, $err, $rc) = system_no_output(1 + 2 + 4, @gcov_tool, $data_file);
    my $then = Time::HiRes::gettimeofday();
    $lcovutil::profileData{exec}{$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));

    my $gcovOutGlobPattern =
        "$tempdir/*.gcov $tempdir/.*.gcov $tempdir/*.gcov.json.gz $tempdir/.*gcov.json.gz";

    if (0 != $rc) {
        if (check_gcov_fail($err, $file)) {
            return;
        }
        $errmsg = "GCOV failed for $file";
        # can parse the error log to see if it spaced out - then return
        # code so parent can catch it
        if ($err =~ /out of memory allocating/) {
            lcovutil::info("spaceout calling gcov for '$data_file'\n");
            $errmsg .= ' out of memory';
            $errorType = $lcovutil::ERROR_CHILD
                if 1 != $lcovutil::maxParallelism;
        }
        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.
    my $start = Time::HiRes::gettimeofday();
    for my $gcov_filename (glob($gcovOutGlobPattern)) {
        eval {
            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("cannot rename $gcov_filename: $!");
            } else {
                unlink($gcov_filename);
            }
        };
        if ($@) {
            if (1 != $lcovutil::maxParallelism &&
                $@ =~ /(integrity check failed|cannot start)/) {
                # looks like we ran out of memory..
                # maybe need new error type ERROR_MEMORY
                #$errorType = $lcovutil::ERROR_GCOV;
                $errmsg = $@;
                goto err;
            } else {
                die("read_intermediate failed: $@");
            }
        }
    }
    my $end = Time::HiRes::gettimeofday();
    $lcovutil::profileData{read}{$filename} = $end - $start;

    if (!%data) {
        ignorable_warning($ERROR_GCOV,
                          "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 ($lcovutil::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);
    my $done = Time::HiRes::gettimeofday();
    $lcovutil::profileData{translate}{$filename} = $done - $end;

    return $trace;

    err:
    unlink(glob($gcovOutGlobPattern));    # clean up - in case gcov died
    ignorable_error($errorType, "$errmsg!");
    return undef;
}

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

sub map_llvm_version($)
{
    my $ver = shift;

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

    warn("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 = shift;

    my $a = $ver >> 16 & 0xff;
    my $b = $ver >> 8 & 0xff;
    my $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 ($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("cannot retrieve gcov version: $!\n");
    local $/;
    my $version_string = <GCOV_PIPE>;
    close(GCOV_PIPE) or die("unable to close gcov pipe: $!\n");

    # 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("cannot determine gcov version - assuming $a.$b.$c\n");
    }
    my $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 $source_dir;
    my $base_dir;

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

    # Get directory and basename of data file
    my ($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) {
        lcovutil::ignorable_error($lcovutil::ERROR_EMPTY,
                                  "empty $graph_filename");
        return undef;
    }

    my ($instr, $graph) = read_gcno($graph_filename);

    # Try to find base directory automatically if requested by user
    if ($lcovutil::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) {
            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, $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()->union($lineMap);
        $fileData->func()->union($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"
                        .
                        (lcovutil::is_ignored($ERROR_GRAPH) ? ' - skipping' : ''
                        ) .
                        '.');
}

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

    lcovutil::debug(2, $desc);
    if ($peek) {
        $pos = tell($handle);
        if ($pos == -1) {
            lcovutil::ignorable_error($lcovutil::ERROR_CORRUPT,
                                     "Could not get current file position: $!");
            return undef;
        }
    }
    my $result = read($handle, $data, $length);
    # LCOV_EXCL_START
    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");
    }
    # LCOV_EXCL_STOP
    if ($peek) {
        if (!seek($handle, $pos, 0)) {
            lcovutil::ignorable_error($lcovutil::ERROR_CORRUPT,
                                      "Could not set file position: $!");
            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) = @_;

    return defined(graph_read($handle, $length, $desc));
}

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

sub uniq(@)
{
    my @new_list;
    my %known;

    foreach my $item (@_) {
        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 %hash;

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

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

sub parent_dir($)
{
    my $dir = shift;
    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 = ReadCurrentSource::resolve_path($filename, 1);
        if ($lcovutil::opt_follow && $lcovutil::opt_follow_file_links) {
            $filename = Cwd::realpath($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 = shift;

    foreach my $filename (keys(%{$hash})) {
        # Skip external files if requested
        my $external = is_external($filename);
        if ($external ||
            TraceFile::skipCurrentFile($filename)) {
            if ($external) {
                lcovutil::info("Dropping 'external' file '$filename'\n");
            } else {
                lcovutil::info("Excluding file '$filename'\n");
            }
            # 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 = shift;
    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 = shift;
    my %file_count;
    my $basefile;

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

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

            # Count file occurrence
            $file_count{$file} = defined($count) ? $count + 1 : 1;
        }
    }
    my $count = 0;
    foreach my $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};
            $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 = graph_find_base($bb);
    # Create graph structure
    foreach my $func (keys(%{$bb})) {
        my $filedata = $bb->{$func};
        my $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 my $file (keys(%{$filedata})) {
            # Account for instrumented lines
            my $linedata = $filedata->{$file};
            push(@{$instr->{$file}}, @$linedata);
        }
    }
    # Clean up array of instrumented lines
    foreach my $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 $list = $fileorder->{$function};
    foreach my $item (@$list) {
        if ($item eq $filename) {
            return;
        }
    }
    push(@$list, $filename);
    $fileorder->{$function} = $list;
}

#
# 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 = read_gcno_word($handle, $desc, $peek);
    return undef unless defined($word);
    return unpack($big_endian ? 'N' : 'V', $word);
}

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

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

    lcovutil::debug(2, "string");
    # Read string length
    my $length = read_gcno_value($handle, $big_endian, "string length");
    return undef if (!defined($length));
    if ($length == 0) {
        return "";
    }
    $length *= 4;
    # Read string
    my $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)
        = @_;

    lcovutil::debug(2, "lines record");
    # Skip basic block index
    graph_skip($handle, 4, "basic block index") or return undef;
    while (1) {
        # Read line number
        my $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");
            my $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)) {
            lcovutil::ignorable_error($lcovutil::ERROR_FORMAT,
                                    "unassigned line number in $gcno_filename");
            next;
        }
        # Add to list
        push(@{$bb->{$function}->{$filename}}, $lineno);
        graph_add_order($fileorder, $function, $filename);
    }
}

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

    lcovutil::debug(2, "function record");
    # Skip ident and checksum
    graph_skip($handle, 8, "function ident and checksum") or return undef;
    # must be gcc > 4 - so only split checksum exists
    graph_skip($handle, 4, "function cfg checksum");
    # Read function name
    lcovutil::debug(2, "function name");
    my $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");
    my $filename = read_gcno_string($handle, $big_endian);
    return undef if (!defined($filename));
    # Read first line number
    my $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 = shift;
    my ($major, $minor);

    my $a = $version >> 24;
    my $b = $version >> 16 & 0xff;
    my $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");
            lcovutil::ignorable_error($lcovutil::ERROR_FORMAT,
                              "$gcno_filename: Overlong record at end of file");
            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) or die("unable to close $gcno_filename: $!\n");

    # 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("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 = shift;
    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 = shift;
    my @opt_list;
    my %specified;

    # Initialize with defaults
    %compat_value = %COMPAT_MODE_DEFAULTS;

    # Add old style specifications
    if (defined($lcovutil::opt_compat_libtool)) {
        $compat_value{$COMPAT_MODE_LIBTOOL} =
            $lcovutil::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("Unknown compatibility mode specification: " . "$directive!\n");
        }
        # Determine mode
        $mode = $COMPAT_NAME_TO_MODE{lc($1)};
        if (!defined($mode)) {
            die("Unknown compatibility mode '$1'!\n");
        }
        $specified{$mode} = 1;
        # Determine value
        if (defined($2)) {
            $value = $COMPAT_NAME_TO_VALUE{lc($2)};
            if (!defined($value)) {
                die("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("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");
            }
        }
    }
}

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

sub is_compat($)
{
    my $mode = shift;

    return $compat_value{$mode} == $COMPAT_VALUE_ON;
}
