#!/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 Storable;
use POSIX;

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

                 %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

use constant {
              GCOV_VERSION_8_0_0 => 0x80000,
              GCOV_VERSION_4_7_0 => 0x40700,
              GCOV_VERSION_4_2_0 => 0x40200,
};

# 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 $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 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 $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 $opt_no_compat_libtool;
our $initial;
our $no_recursion = 0;
our $maxdepth;
our $no_markers = 0;
our $gcov_caps;
our %compat_value;
our $intermediate;
our $intervalMonitor;             # class for progress reporting
our $totalChildCpuTime    = 0;
our $intervalChildCpuTime = 0;    # since last update

# for performance tweaking/tuning
# $defaultChunkSize and $defaultInterval held in $lcovutil - so they
# can be set from command line/RC file processing.
our %childRetryCounts;
our @large_files;
# $callFromLcov is set when lcov calls geninfo, then:
#   - picked up from --rc (in geninfo)
#   - written to the profile file (in the command line section
#   - then found by 'spreadsheet.py' - which can then do a better job of
#     figuring out who called what
our $callFromLcov;

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

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

#
# Code entry point
#

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

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

# retrieve settings from RC file - use these if not overridden on command line
my %geninfo_opts = ("test-name|t=s"       => \$test_name,
                    'call-from-lcov'      => \$callFromLcov,
                    "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,
                    "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 ($@);

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);
    }
    unless (defined($tool) && -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_intermediate = 0'."
    );
    $intermediate = 1;
}

if ($intermediate) {
    info("Using intermediate gcov format\n");
    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::br_coverage = 0;
}

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;
# gen_info() accumulates into the file-scope $processedFiles internally and
# returns the running total; use assignment (not +=) to avoid double-counting
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
      --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
      --(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 from 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" .
             (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|integrity 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 $nChunks =
        int(floor((scalar(@$filelist) + $chunkSize - 1) / $chunkSize));
    for (my $i = 0; $i < $nChunks; ++$i) {
        push(@worklist, [0, []]);
    }
    my $work = $filelist;    # default to the original order
    if ($lcovutil::profileHistoryCallback &&
        ($chunkSize > 1 || exists($ENV{LCOV_FORCE_PARALLEL}))) {
        my (@predicted, @unknown);
        foreach my $j (@$filelist) {
            my ($dir, $gcda, $gcno) = @$j;
            my $key       = defined($gcda) ? $gcda : $gcno;
            my $k         = substr($key, length($dir) + 1);
            my $predicted = $lcovutil::profileHistoryCallback->history($k);
            if (defined($predicted)) {
                push(@predicted, [$j, $predicted, $key]);
            } else {
                # put the ones we don't know about at the start of the list
                #  - work on them first, trusting that we will have parallelism
                #    capacity for the ones we know about
                push(@unknown, $j);
            }
        }
        @predicted =
            sort({ $b->[1] <=> $a->[1] or $a->[2] cmp $b->[2] } @predicted);

        lcovutil::info(scalar(@unknown) .
                           ' (of ' . (scalar(@predicted) + scalar(@unknown)) .
                           ") files do not appear in \'--history\' profile.\n")
            if (@unknown);
        if (@predicted) {
            my @order = map({ $_->[0] } @predicted);
            # unknown to the back...
            push(@order, @unknown);
            $work = \@order;
        }
    }
    my $chunkId = 0;
    FILE: foreach my $j (@$work) {
        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;
            }
        }
        die("unexpected chunkId $chunkId") if $chunkId >= $nChunks;
        my $chunk = $worklist[$chunkId++];
        push(@{$chunk->[1]}, $j);
        if ($chunkId == $nChunks) {
            $chunkId = 0;
        }
    }    #foreach DATA_FILE
         # 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 (['log', $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;
}

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

sub process_dafile($$$$)
{
    my ($dirname, $gcda_file, $gcno_file, $tempdir) = @_;
    my $da_filename;        # Name of data file to process
    my $source_dir;         # Directory of source file
    my $bb_filename;        # Name of respective 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 @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
    my ($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);
    # $da_filename is already absolute; catfile with a base would double-prefix it
    my $da_arg = $da_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;    # skip 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;
            }
            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);
            if (@lcovutil::extractVersionScript) {
                my $version = lcovutil::extractFileVersion($source_filename);
                $fileData->version($version)
                    if (defined($version) && $version ne "");
            }

            # read the GCNO file to find function decls
            if ($lcovutil::func_coverage &&
                exists($graph->{$source_filename})) {
                my $fn_data = $graph->{$source_filename};

                my $functionMap = $fileData->testfnc($test_name);
                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));
                    $functionMap->define_function($fn, $line);
                }
            }

            # Read in contents of gcov file
            my ($lineMap, $branchMap, $functionMap) =
                read_gcov_file($fileData, $gcov_file, $da_filename,
                               $source, $test_name);

            # Skip empty files
            if (0 == $lineMap->found()) {
                lcovutil::ignorable_error($lcovutil::ERROR_EMPTY,
                                          "$gcov_file is empty");
                $traceFile->remove($source_filename);
                unlink($gcov_file) unless $lcovutil::preserve_intermediates;
                next;
            }

            # append test-specific coverage to summary DB
            $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)
# Used by gcc/5.2.0
#
# 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 ($fileData, $gcovFilename, $da_filename, $source_filename, $test_name) =
        @_;
    my $lineMap   = $fileData->test($test_name);
    my $branchMap = $fileData->testbr($test_name)
        if $lcovutil::br_coverage;
    my $functionMap = $fileData->testfnc($test_name)
        if $lcovutil::func_coverage;

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

    # Expect gcov format as used in gcc >= 3.3
    debug("reading $gcovFilename\n");
    # line content "/*EOF*/" occurs when gcov knows the
    # number of lines in the file but can't find the source code -
    # most of the time, that is deliberate because we run gcov in a
    # temp directory in order to avoid conflicts from parallel execution
    # we do a bit of error checking that the content is not inconsistent
    # - e.g., if we find the file due to fully qualified paths, and find
    # a non-EOF line following an EOF line.
    my $foundEOF = 0;
    my %hasUnexecBlock;    # hash of lines which have 'unexec' marker
    my $branchBlock;
    my $branchId;
    my $branchLine  = -1;
    my $gcovBlockId = -1;
    my $prevLine    = 0;

    while (<$input>) {
        chomp($_);
        # Also remove CR from line-end
        s/\015$//;

        if (/^\s*(\d+|\$+|\%+):\s*(\d+)-block\s+(\d+)\s*$/) {
            next unless $lcovutil::br_coverage;
            # Block information - used to group related branches
            # line=$2, blockId=$3
            if ($branchLine != $2 ||
                $gcovBlockId != $3) {
                # gcov data contains "block ..." leaders in places which
                #   don't contain any branches
                if (defined($branchBlock) && !$branchBlock->empty()) {
                    $branchMap->insertBlock($branchBlock, $branchLine);
                    $branchBlock = undef;
                }
            }
            # 'block' doesn't necessarily contain any branches - e.g.,
            #  testcase 'genhtml/exception':  sees:
            #    - 2 blocks on line 11 both containing 2 branches (blocks 0
            #      and 1), followed by
            #    - 3 blocks which do not contain branches (blocks 2, 3, 4),
            #      followed by
            #    - blocks 5, 6, and 7 which contain 2 branches each, followed
            #      by
            #    - blocks 8, 9, 10, 11, 12 - containing no branches.
            $gcovBlockId = $3;
            my $existing = $branchMap->value($2);
            if (defined($branchBlock)) {
                die("expected empty reused block")
                    unless $branchBlock->empty();
            } else {
                $branchBlock = BranchBlock->new();
            }
            $branchLine = $2;
            $branchId   = 0;
        } elsif (/^branch\s+(\d+)\s+taken\s+(\d+)(?:\s+\(([^)]*)\))?/) {
            next unless $lcovutil::br_coverage;
            # $1 is branch ID, $2 is hit count
            my ($id, $count, $t) = ($1, $2, $3);
            $t =~ /^(throw|fallthrough)?$/ or
                die("unexpected $t")
                if defined($t);
            my $type;
            if (!defined($t) || '' eq $t) {
                $type = BranchElement::VANILLA;
            } elsif ($t eq 'throw') {
                $type = BranchElement::EXCEPT;
            } else {
                die("unexpected type '$t'") unless $t eq 'fallthrough';
                $type = BranchElement::FALLTHROUGH;
            }
            # expect that all branches within the block are contiguous -
            #   not interleaved with branches from other blocks
            $branchBlock->appendElement(
                         BranchElement->new($branchId++, $count, undef, $type));
        } elsif (/^branch\s+(\d+)\s+never\s+executed/) {
            next unless $lcovutil::br_coverage;
            # this branch not taken
            $branchBlock->appendElement(BranchElement->new($branchId++, '-'));
        } elsif (/^function\s+(.+)\s+called\s+(\d+)\s+/) {
            next unless $lcovutil::func_coverage;
            my $name  = $1;
            my $count = $2;
            my $fn    = filter_fn_name($name, 0);
            my $func  = $functionMap->findName($fn);
            if ($func) {
                $func->addAlias($fn, $count);
            } else {
                lcovutil::info("$source_filename: unknown function '$fn'" .
                               ($name ne $fn ? " ($name)" : '') .
                               " not in GCNO file  - skipped\n");
            }
        } elsif (/^call/) {
            # Function call return data
        } elsif (/^\s*([^:]+):\s*([^:]+):(.*)$/) {
            my ($count, $line, $code) = ($1, $2, $3);
            # Skip instance-specific counts
            next if $line <= $prevLine;
            $prevLine = $line;
            # 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 $gcovFilename:$. while processing $da_filename: '$code'"
                );
            }
            # 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 count for the 'if' clause
                #  of an untaken branch.
                $count = $1;
                $hasUnexecBlock{$line} = $1
                    unless $count == 0;
            }
            # <exec count>:<line number>:<source code>
            if ($line eq "0") {
                # Extra data
                next;
            } elsif ($count eq "-") {
                # Uninstrumented line
                next;
            } else {
                # Check for zero count
                if ($count =~ /^[#=]/) {
                    $count = 0;
                }
                $lineMap->append($line, $count);
            }

        }
    }
    if (defined($branchMap)) {
        $branchMap->insertBlock($branchBlock, $branchLine)
            if defined($branchBlock) && !$branchBlock->empty();
        $branchMap->updateCounts();
    }
    foreach my $line (sort keys %hasUnexecBlock) {
        my $hit = $hasUnexecBlock{$line};
        die("unexpected 'unexec' count") unless $hit != 0;
        next unless defined($branchMap->value($line));
        lcovutil::debug(
            "$source_filename:$line: unexecuted block on non-branch line with count=$hit\n"
        );

        if ($lcovutil::opt_adjust_unexecuted_blocks) {
            $lineMap->remove($line);
            $lineMap->append($line, 0);
        } elsif (lcovutil::warn_once(
                                "unexecuted block", $ERROR_INCONSISTENT_DATA)) {
            lcovutil::ignorable_warning($ERROR_INCONSISTENT_DATA,
                "$source_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."
            );
        }
    }
    return ($lineMap, $branchMap, $functionMap);
}

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

        if (TraceFile::skipCurrentFile($filename)) {
            $lcovutil::excluded_files{$filename} = 1;
            next;
        }
        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 "");
        }
        my $branch_num;
        my $branchBlock;
        my $branchLine = -1;
        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)/) {
                next
                    unless $lcovutil::br_coverage;
                my $lineNo = $1;
                my $c;
                # branch:<line>,taken|nottaken|notexec
                if ($2 eq "taken") {
                    $c = 1;
                } elsif ($2 eq "nottaken") {
                    $c = 0;
                } else {
                    $c = "-";
                }
                if (!defined($branchBlock) ||
                    $lineNo != $branchLine) {
                    $branchMap->insertBlock($branchBlock, $branchLine)
                        if $branchBlock;

                    # "block" is always zero for intermediate text
                    $branchBlock = BranchBlock->new();
                    $branch_num  = 0;
                    $branchLine  = $lineNo;
                }
                $branchBlock->appendElement(
                                         BranchElement->new($branch_num++, $c));
            }
        }
        $branchMap->insertBlock($branchBlock, $branchLine)
            if $branchBlock;
        $branchMap->updateCounts();
        # 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;
}

package ExprMap;

sub new
{
    my ($class, $filename) = @_;
    return bless [{}, $filename], $class;
}

sub recordOrMergeBlock
{
    # if new block has same expression as an existing block on this line,
    #   then merge the new one into the existing block and return undef.
    # Otherwise, return the new block (so it can be inserted into this
    #   location)
    my ($self, $line, $block) = @_;

    my $key = "$line:";
    my $sep = '';

    foreach my $br (@{$block->elements()}) {
        my $expr = $br->expr();
        $key .= $sep . $br->signature();
        $key .= $expr
            if defined($expr);
        $sep = '#';
    }
    my $table = $self->[0];
    if (exists($table->{$key})) {
        my $existing = $table->{$key};
        $existing->merge($block, $self->[1], $line);
        return undef;
    } else {
        $table->{$key} = $block;
        return $block;
    }
}

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

package main;

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

    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
        lcovutil::debug(1, "parse $filename\n");
        if (TraceFile::skipCurrentFile($filename)) {
            $lcovutil::excluded_files{$filename} = 1;
            next;
        }
        my $fileData    = $traceFile->data($filename);
        my $lineMap     = $fileData->test($test_name);
        my $functionMap = $fileData->testfnc($test_name)
            if $lcovutil::func_coverage;
        my $branchMap = $fileData->testbr($test_name)
            if $lcovutil::br_coverage;
        my $mcdcMap = $fileData->testcase_mcdc($test_name)
            if $lcovutil::mcdc_coverage;
        my $branchExprMap = ExprMap->new($filename);

        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);
            }
        }
        foreach 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...
                $unexec = (defined($unexec) && $unexec && $count == 0);

                # build a BranchEntry from this data - then either
                #  - insert it into the map:
                #     - if this is the first block we saw on this line, OR
                #     - this BranchEntry is not compatible with any existing
                #       entry on this line (different number of elements
                #       or elements have different expression or different
                #       'exception' tag
                #  - merge it into an existing compatible element
                if (@$branches) {

                    # branch data is not necessarily contiguous
                    #  e.g., 'genhtml/lambda' with gcc/14.2 sees 5
                    #  branches on functional:513, then one at 514
                    #  followed by another at 513 -
                    # so we check to see if we already have a block
                    # here - and merge (rather than appending) if we do
                    my $branchLocation = $branchMap->findOrCreate($line);

                    my $block      = BranchBlock->new();
                    my $branch_num = 0;
                    for my $b (@$branches) {
                        my $brcount = $b->{"count"};
                        if (!defined($brcount) || $unexec) {
                            $brcount = "-";
                        }
                        # need to keep track of whether compiler thinks this
                        #  branch is an exception - so we can skip it later.
                        my $type;
                        if (exists($b->{"throw"}) && $b->{"throw"}) {
                            $type = BranchElement::EXCEPT;
                        } elsif (exists($b->{"fallthrough"}) &&
                                 $b->{"fallthrough"}) {
                            $type = BranchElement::FALLTHROUGH;
                        } else {
                            $type = BranchElement::VANILLA;
                        }
                        my $from = $b->{'source_block_id'}
                            if exists($b->{'source_block_id'});
                        my $to = $b->{'destination_block_id'}
                            if exists($b->{'destination_block_id'});
                        # could record the from/to block address as the
                        #  expression?
                        my $expr = "$from -> $to"
                            if defined($from) && defined($to);
                        $block->appendElement(BranchElement->new(
                                           $branch_num++, $brcount, $expr, $type
                        ));
                    }
                    my $blk = $branchExprMap->recordOrMergeBlock($line, $block);
                    $branchLocation->insertBlock($block, $line)
                        if $blk;
                }
            }
            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);
            }
        }
        # update the branch counts in this file
        # now go through lines, functions, branches - append to test_name data
        $fileData->sum()->union($lineMap);
        if ($lcovutil::br_coverage) {
            $branchMap->updateCounts();
            $fileData->sumbr()->union($branchMap);
        }
        $fileData->mcdc()->union($mcdcMap)
            if $lcovutil::mcdc_coverage;
        $fileData->func()->union($functionMap)
            if $lcovutil::func_coverage;
    }
    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));
    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 $base_dir;

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

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

    my $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
#
# 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") or return undef;
    # 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 $filename;
    my $function;
    my $bb        = {};
    my $fileorder = {};
    my $artificial;
    my @artificial_fns;
    local *HANDLE;

    open(HANDLE, "<", $gcno_filename) or goto open_error;
    my $filelength = (stat(HANDLE))[7];
    binmode(HANDLE);
    # Read magic
    my $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
    my $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)) {
        # Read record tag
        my $tag = read_gcno_value(*HANDLE, $big_endian, "record tag");
        goto incomplete if (!defined($tag));
        # Read record length
        my $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
        my $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
        my $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);

    my ($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;
}
