# Hey emacs! This is a -*- Perl -*- script!
#
# Lintian::IO::Select -- Perl utility functions for lintian
#
# Copyright (C) 2020 Felix Lechner
#
# 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, you can find it on the World Wide
# Web at https://www.gnu.org/copyleft/gpl.html, or write to the Free
# Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
# MA 02110-1301, USA.

package Lintian::IO::Select;

use v5.20;
use warnings;
use utf8;

use Exporter qw(import);

our @EXPORT_OK;

BEGIN {

    @EXPORT_OK = qw(
      unpack_and_index_piped_tar
    );
}

use Const::Fast;
use IPC::Open3;
use IO::Select;
use Symbol;
use Syntax::Keyword::Try;
use Unicode::UTF8 qw(encode_utf8);

# read up to 40kB at a time.  this happens to be 4096 "tar records"
# (with a block-size of 512 and a block factor of 20, which appear to
# be the defaults).  when we do full reads and writes of READ_SIZE (the
# OS willing), the receiving end will never be with an incomplete
# record.
const my $TAR_RECORD_SIZE => 20 * 512;

# using 4096 * $TAR_RECORD_SIZE tripped up older kernels < 5.7
const my $READ_CHUNK => 4 * 1024;

const my $EMPTY => q{};

=head1 NAME

Lintian::IO::Select - process functions based on IO::Select

=head1 SYNOPSIS

 use Lintian::IO::Select;

=head1 DESCRIPTION

This module contains process functions based on IO::Select.

=head1 FUNCTIONS

=over 4

=item unpack_and_index_piped_tar

=cut

sub unpack_and_index_piped_tar {
    my ($command, $basedir) = @_;

    my @pids;

    my $select = IO::Select->new;

    my $produce_stdin;
    my $produce_stdout;
    my $produce_stderr = gensym;

    my @produce_command = @{$command};

    my $produce_pid;
    try {
        $produce_pid = open3(
            $produce_stdin, $produce_stdout,
            $produce_stderr, @produce_command
        );
    } catch {
        die map { encode_utf8($_) } $@;
    }

    close $produce_stdin;

    push(@pids, $produce_pid);

    $select->add($produce_stdout, $produce_stderr);

    my $extract_stdin;
    my $extract_stdout;
    my $extract_stderr = gensym;

    my @extract_command = (
        qw(tar --no-same-owner --no-same-permissions --touch --extract --file - -C),
        $basedir
    );

    my $extract_pid;
    try {
        $extract_pid = open3(
            $extract_stdin, $extract_stdout,
            $extract_stderr, @extract_command
        );
    } catch {
        die map { encode_utf8($_) } $@;
    }

    push(@pids, $extract_pid);

    $select->add($extract_stdout, $extract_stderr);

    my @index_options
      = qw(--list --verbose --utc --full-time --quoting-style=c --file -);

    my $named_stdin;
    my $named_stdout;
    my $named_stderr = gensym;

    my @named_command = ('tar', @index_options);

    my $named_pid;
    try {
        $named_pid
          = open3($named_stdin, $named_stdout, $named_stderr, @named_command);
    } catch {
        die map { encode_utf8($_) } $@;
    }

    push(@pids, $named_pid);

    $select->add($named_stdout, $named_stderr);

    my $numeric_stdin;
    my $numeric_stdout;
    my $numeric_stderr = gensym;

    my @numeric_command = ('tar', '--numeric-owner', @index_options);

    my $numeric_pid;
    try {
        $numeric_pid = open3(
            $numeric_stdin, $numeric_stdout,
            $numeric_stderr, @numeric_command
        );
    } catch {
        die map { encode_utf8($_) } $@;
    }

    push(@pids, $numeric_pid);

    $select->add($numeric_stdout, $numeric_stderr);

    my $named = $EMPTY;
    my $numeric = $EMPTY;

    my $produce_errors = $EMPTY;
    my $extract_errors = $EMPTY;
    my $named_errors = $EMPTY;

    while (my @ready = $select->can_read) {

        for my $handle (@ready) {

            my $buffer;
            my $length = sysread($handle, $buffer, $READ_CHUNK);

            die encode_utf8("Error from child: $!\n")
              unless defined $length;

            if ($length == 0){
                if ($handle == $produce_stdout) {
                    close $extract_stdin;
                    close $named_stdin;
                    close $numeric_stdin;
                }
                $select->remove($handle);
                next;
            }

            if ($handle == $produce_stdout) {
                print {$extract_stdin} $buffer;
                print {$named_stdin} $buffer;
                print {$numeric_stdin} $buffer;

            } elsif ($handle == $named_stdout) {
                $named .= $buffer;

            } elsif ($handle == $numeric_stdout) {
                $numeric .= $buffer;

            } elsif ($handle == $produce_stderr) {
                $produce_errors .= $buffer;

            } elsif ($handle == $extract_stderr) {
                $extract_errors .= $buffer;

            } elsif ($handle == $named_stderr) {
                $named_errors .= $buffer;

                # } else {
                #   die encode_utf8("Shouldn't be here\n");
            }
        }
    }

    close $produce_stdout;
    close $produce_stderr;

    close $extract_stdout;
    close $extract_stderr;

    close $named_stdout;
    close $named_stderr;

    close $numeric_stdout;
    close $numeric_stderr;

    waitpid($_, 0) for @pids;

    my $tar_errors = ($produce_errors // $EMPTY) . ($extract_errors // $EMPTY);
    my $index_errors = $named_errors;

    return ($named, $numeric, $tar_errors, $index_errors);
}

=back

=head1 SEE ALSO

lintian(1)

=cut

1;

# Local Variables:
# indent-tabs-mode: nil
# cperl-indent-level: 4
# End:
# vim: syntax=perl sw=4 sts=4 sr et
