# Copyright 2010, 2011, 2013, 2014, 2019 Kevin Ryde

# This file is part of Filter-gunzip.
#
# Filter-gunzip 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 3, or (at your option) any later
# version.
#
# Filter-gunzip 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 Filter-gunzip.  If not, see <http://www.gnu.org/licenses/>.

package Filter::gunzip::Filter;
use strict;
use Carp;
use Filter::Util::Call qw(filter_add filter_read filter_del);
use Compress::Raw::Zlib qw(Z_OK Z_STREAM_END Z_BUF_ERROR);

use vars '$VERSION';
$VERSION = 8;

# uncomment this to run the ### lines
# use Smart::Comments;

sub import {
  my ($class) = @_;

  # Filter::Util::Call 1.37 filter_add() rudely re-blesses the object into the
  # callers package.  Doesn't affect plain use here, but a subclass would want
  # to fix it up again.
  #
  ### filter_add()
  filter_add ($class->new);
}

sub new {
  my $class = shift;
  ### gunzip new(): $class

  # LimitOutput helps avoid growing $_ to a huge size if a few input bytes
  # expand to a lot of output.  (That option new in Compress::Raw::Zlib 2.018.
  #
  # Crib note: Must have parens on MAX_WBITS() because it's unprototyped
  # (generated by Compress::Raw::Zlib::AUTOLOAD()) and hence without them
  # the "+ WANT_GZIP_OR_ZLIB" is passed as a parameter instead of adding.
  #
  # Docs of the wbits: /usr/include/zlib.h
  #
  # Think MAX_WBITS is right for gunzip.  Think gzip format (RFC 1952)
  # doesn't have a window bits in its header to tell an inflator how much
  # needed.  MAX_WBITS is 32kbytes which is not too bad.
  #
  my ($inf, $zerr) = Compress::Raw::Zlib::Inflate->new
    (-ConsumeInput => 1,
     -LimitOutput  => 1,
     -WindowBits   => (Compress::Raw::Zlib::MAX_WBITS()
                       + Compress::Raw::Zlib::WANT_GZIP_OR_ZLIB()));
  $inf or croak __PACKAGE__," cannot create inflator: $zerr";

  return bless { inflator => $inf,
                 input    => '',
                 @_ }, $class;
}

sub filter {
  my ($self) = @_;
  ### gunzip filter(): $self

  if (! $self->{'inflator'}) {
    ### inflator got to EOF, remove self
    filter_del();
    if ($self->{'input_eof'}) {
      ### input_eof
      return 0;
    } else {
      $_ = delete $self->{'input'};
      ### remaining input: $_
      ### return: 1
      return 1;
    }
  }

  # get more input data, if haven't seen input eof and if don't already have
  # some data to use
  #
  ### input length: length($self->{'input'})
  if (! $self->{'input_eof'} && ! length ($self->{'input'})) {
    my $status = filter_read(4096);  # input block size
    ### filter_read() returns: $status
    if ($status < 0) {
      return $status;
    }
    if ($status == 0) {
      $self->{'input_eof'} = 1;
    } else {
      $self->{'input'} = $_;
      # open my $fh, '>', '/tmp/x.dat' or die;
      # print $fh $_ or die;
      # close $fh or die;
    }
  }

  my $input_len_before = length($self->{'input'});
  ### $input_len_before
  my $zerr = $self->{'inflator'}->inflate ($self->{'input'}, $_);
  ### zinflate: $zerr+0, "$zerr"
  ### _ output length: length($_)
  ### leaving input len: length($self->{'input'})

  if ($zerr == Z_STREAM_END) {
    # inflator at eof, return final output now, next call will consider
    # balance of $self->{'input'}
    delete $self->{'inflator'};
    ### return final inflate: $_
    ### return: 1
    return 1;
  }

  my $status;
  if ($zerr == Z_OK || $zerr == Z_BUF_ERROR) {
    if (length($_) == 0) {
      if ($input_len_before == length($self->{'input'})) {
        # protect against infinite loop
        carp __PACKAGE__,
          ' oops, inflator produced nothing and consumed nothing';
        return -1;
      }
      if ($self->{'input_eof'}) {
        # EOF on the input side (and $self->{'input_eof'} is only set when
        # $self->{'input'} is empty) but the inflator is not at EOF and has
        # no further output at this point
        carp __PACKAGE__," incomplete input";
        return -1;
      }
    }
    # It's possible $_ output is empty at this point if the inflator took
    # some input but had nothing to output just yet.  This is unlikely, but
    # if it happens there'll be another call to us immediately, no need to
    # do anything special.
    #### return continuing: $_
    return 1;
  }

  # $zerr not Z_OK and not Z_STREAM_END
  carp __PACKAGE__," zlib error: $zerr";
  return -1;
}

1;
__END__

=for stopwords Filter-gunzip uncompresses gzip gunzip gunzipping Ryde

=head1 NAME

Filter::gunzip::Filter - an internal part of Filter::gunzip

=head1 SYNOPSIS

 perl -MFilter::gunzip::Filter foo.pl.gz

 # or in a script
 use Filter::gunzip::Filter;
 ... # inline gzipped source code bytes

=head1 DESCRIPTION

This is an internal part of L<Filter::gunzip> not intended for other use.
The description here is what it presently does, but it may change wildly or
be removed entirely.

C<Filter::gunzip::Filter> installs gunzipping of compressed source code
bytes using a Perl source code filter per L<perlfilter>.  It is a "block
based" filter, setup by L<Filter::Util::Call> and inflating with
L<Compress::Raw::Zlib>.  C<Filter::gunzip> uses this method as a fallback
when it's not possible to install a ":gzip" layer on the source code file
handle.

=head1 SEE ALSO

L<Filter::gunzip>

=head1 HOME PAGE

L<http://user42.tuxfamily.org/filter-gunzip/index.html>

=head1 LICENSE

Filter-gunzip is Copyright 2010, 2011, 2013, 2014, 2019 Kevin Ryde

Filter-gunzip 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 3, or (at your option) any later
version.

Filter-gunzip 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
Filter-gunzip.  If not, see <http://www.gnu.org/licenses/>.

=cut
