The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
# please insert nothing before this line: -*- mode: cperl; cperl-indent-level: 4; cperl-continued-statement-offset: 4; indent-tabs-mode: nil -*-
package TestFilter::in_bbs_underrun;

# this test exercises the underrun filter concept. Sometimes filters
# need to read at least N bytes before they can apply their
# transformation. It's quite possible that reading one bucket brigade
# is not enough. But two or more are needed.
#
# When the filter realizes that it doesn't have enough data, it can
# stash the read data in the context, and wait for the next
# invocation, meanwhile it must return an empty bb to the filter that
# has called it. This is not efficient. Instead of returning an empty
# bb to a caller, the input filter can initiate the retrieval of extra
# bucket brigades, after one was received. Notice that this is
# absolutely transparent to any filters before or after the current
# filter.
#
# to see the filter at work, run it as:
# t/TEST -trace=debug -v filter/in_bbs_underrun
#
# and look in the error_log. You will see something like:
#
# ==> TestFilter::in_bbs_underrun::handler : filter called
# ==> asking for a bb
# ==> asking for a bb
# ==> asking for a bb
# ==> storing the remainder: 7611 bytes
# ==> TestFilter::in_bbs_underrun::handler : filter called
# ==> asking for a bb
# ==> asking for a bb
# ==> storing the remainder: 7222 bytes
# ==> TestFilter::in_bbs_underrun::handler : filter called
# ==> asking for a bb
# ==> seen eos, flushing the remaining: 8182 bytes
#
# it's clear from the log that the filter was invoked 3 times, however
# it has consumed 6 bucket brigades
#
# finally, we have to note that this is impossible to do with
# streaming filters, since they can only read data from one bucket
# brigade. So you must process bucket brigades.
#

use strict;
use warnings FATAL => 'all';

use Apache2::RequestRec ();
use Apache2::RequestIO ();
use Apache2::Filter ();

use Apache::TestTrace;

use TestCommon::Utils ();

use Apache2::Const -compile => qw(OK M_POST);

use constant SIZE => 1024*16 + 5; # ~16k

sub handler {
    my ($filter, $bb, $mode, $block, $readbytes) = @_;
    my $ba = $filter->r->connection->bucket_alloc;
    my $ctx = $filter->ctx;
    my $buffer = defined $ctx ? $ctx : '';
    $ctx = '';  # reset
    my $seen_eos = 0;
    my $data;
    debug_sub "filter called";

    # fetch and consume bucket brigades untill we have at least SIZE
    # bytes to work with
    do {
        my $tbb = APR::Brigade->new($filter->r->pool, $ba);
        $filter->next->get_brigade($tbb, $mode, $block, $readbytes);
        debug "asking for a bb";
        ($data, $seen_eos) = flatten_bb($tbb);
        $tbb->destroy;
        $buffer .= $data;
    } while (!$seen_eos && length($buffer) < SIZE);

    # now create a bucket per chunk of SIZE size and put the remainder
    # in ctx
    for (split_buffer($buffer)) {
        if (length($_) == SIZE) {
            $bb->insert_tail(APR::Bucket->new($bb->bucket_alloc, $_));
        }
        else {
            $ctx .= $_;
        }
    }

    if ($seen_eos) {
        # flush the remainder
        $bb->insert_tail(APR::Bucket->new($bb->bucket_alloc, $ctx));
        $bb->insert_tail(APR::Bucket::eos_create($ba));
        debug "seen eos, flushing the remaining: " . length($ctx) . " bytes";
    }
    else {
        # will re-use the remainder on the next invocation
        $filter->ctx($ctx);
        debug "storing the remainder: " . length($ctx) . " bytes";
    }

    return Apache2::Const::OK;
}

# split in words of SIZE chars and a remainder
sub split_buffer {
    my $buffer = shift;
    if ($] < 5.007) {
        my @words = $buffer =~ /(.{@{[SIZE]}}|.+)/g;
        return @words;
    }
    else {
        # available only since 5.7.x+
        return unpack "(A" . SIZE . ")*", $buffer;
    }
}

sub flatten_bb {
    my ($bb) = shift;

    my $seen_eos = 0;

    my @data;
    for (my $b = $bb->first; $b; $b = $bb->next($b)) {
        $seen_eos++, last if $b->is_eos;
        $b->read(my $bdata);
        push @data, $bdata;
    }
    return (join('', @data), $seen_eos);
}


sub response {
    my $r = shift;

    $r->content_type('text/plain');

    if ($r->method_number == Apache2::Const::M_POST) {
        my $data = TestCommon::Utils::read_post($r);
        #warn "HANDLER READ: $data\n";
        my $length = length $data;
        $r->print("read $length chars");
    }

    return Apache2::Const::OK;
}
1;
__DATA__
SetHandler modperl
PerlModule          TestFilter::in_bbs_underrun
PerlResponseHandler TestFilter::in_bbs_underrun::response
PerlInputFilterHandler TestFilter::in_bbs_underrun::handler
#PerlInputFilterHandler TestCommon::FilterDebug::snoop_request