package Net::WebSocket::PMCE::deflate::Data;
use strict;
use warnings;
use parent qw( Net::WebSocket::PMCE::Data );
use Net::WebSocket::FrameTypeName ();
use Net::WebSocket::Message ();
use Net::WebSocket::PMCE::deflate::Constants ();
use constant {
_ZLIB_SYNC_TAIL => "\0\0\xff\xff",
_DEBUG => 0,
};
=encoding utf-8
=head2 I<CLASS>->new( %OPTS )
%OPTS is:
=over
=item C<deflate_max_window_bits> - optional; the number of window bits to use
for compressing messages. This should correspond with the local endpoint’s
behavior; i.e., for a server, this should match the C<server_max_window_bits>
extension parameter in the WebSocket handshake.
=item C<inflate_max_window_bits> - optional; the number of window bits to use
for decompressing messages. This should correspond with the remote peer’s
behavior; i.e., for a server, this should match the C<client_max_window_bits>
extension parameter in the WebSocket handshake.
=item C<deflate_no_context_takeover> - corresponds to either the
C<client_no_context_takeover> or C<server_no_context_takeover> parameter,
to match the local endpoint’s role. When this flag is set, the object
will do a full flush at the end of each C<compress()> call.
=back
=cut
sub new {
my ($class, %opts) = @_;
#Validate deflate_max_window_bits/inflate_max_window_bits?
my $compress_func = '_compress_';
$compress_func .= $opts{'deflate_no_context_takeover'} ? 'full' : 'sync';
$compress_func .= '_flush_chomp';
$opts{'final_frame_compress_func'} = $compress_func;
return bless \%opts, $class;
}
#----------------------------------------------------------------------
=head2 $msg = I<OBJ>->create_message( FRAME_TYPE, PAYLOAD )
Creates an unfragmented, compressed message. The message will be an
instance of L<Net::WebSocket::Message>.
FRAME_TYPE can be either C<text> or C<binary> (for Net::WebSocket’s
default frame classes) or full package names (e.g., to use a custom
frame class).
This method cannot be called while a streamer object has yet to create its
final frame.
B<NOTE:> This function alters PAYLOAD.
=cut
sub create_message {
my ($self, $frame_type) = @_; #$_[2] = payload
die "A streamer is active!" if $self->{'_streamer_mode'};
my $compress_func = $self->{'final_frame_compress_func'};
my $payload_sr = \($self->$compress_func( $_[2] ));
my $frame_class = Net::WebSocket::FrameTypeName::get_module($frame_type);
return Net::WebSocket::Message->new(
$frame_class->new(
payload => $payload_sr,
rsv => $self->INITIAL_FRAME_RSV(),
$self->FRAME_MASK_ARGS(),
),
);
}
#----------------------------------------------------------------------
=head2 $msg = I<OBJ>->create_streamer( FRAME_TYPE )
FRAME_TYPE can be either C<text> or C<binary> (for Net::WebSocket’s
default frame classes) or full package names (e.g., to use a custom
frame class).
Returns an instance of L<Net::WebSocket::PMCE::deflate::Data::Streamer> based
on this object.
=cut
sub create_streamer {
my ($self, $frame_type) = @_;
$self->{'_streamer_mode'} = 1;
require Net::WebSocket::PMCE::deflate::Data::Streamer;
my $frame_class = Net::WebSocket::FrameTypeName::get_module($frame_type);
return Net::WebSocket::PMCE::deflate::Data::Streamer->new($self, $frame_class);
}
#----------------------------------------------------------------------
=head2 $decompressed = I<OBJ>->decompress( COMPRESSED_PAYLOAD )
Decompresses the given string and returns the result.
B<NOTE:> This function alters COMPRESSED_PAYLOAD, such that
it’s probably not useful afterward.
=cut
#cf. RFC 7692, 7.2.2
sub decompress {
my ($self) = @_; #$_[1] = payload
$self->{'i'} ||= $self->_create_inflate_obj();
_DEBUG && _debug(sprintf "inflating: %v.02x\n", $_[1]);
$_[1] .= _ZLIB_SYNC_TAIL;
my $status = $self->{'i'}->inflate($_[1], my $v);
die $status if $status != Compress::Raw::Zlib::Z_OK();
_DEBUG && _debug(sprintf "inflate output: [%v.02x]\n", $v);
return $v;
}
#----------------------------------------------------------------------
my $_payload_sr;
#cf. RFC 7692, 7.2.1
#Use for non-final fragments.
sub _compress_non_final_fragment {
$_[0]->{'d'} ||= $_[0]->_create_deflate_obj();
return $_[0]->_compress( $_[1] );
}
#Preserves sliding window to the next message.
#Use for final fragments when deflate_no_context_takeover is OFF
sub _compress_sync_flush_chomp {
$_[0]->{'d'} ||= $_[0]->_create_deflate_obj();
return _chomp_0000ffff_or_die( $_[0]->_compress( $_[1], Compress::Raw::Zlib::Z_SYNC_FLUSH() ) );
}
#Flushes the sliding window.
#Use for final fragments when deflate_no_context_takeover is ON
sub _compress_full_flush_chomp {
$_[0]->{'d'} ||= $_[0]->_create_deflate_obj();
return _chomp_0000ffff_or_die( $_[0]->_compress( $_[1], Compress::Raw::Zlib::Z_FULL_FLUSH() ) );
}
sub _chomp_0000ffff_or_die {
if ( rindex( $_[0], _ZLIB_SYNC_TAIL ) == length($_[0]) - 4 ) {
substr($_[0], -4) = q<>;
}
else {
die sprintf('deflate/flush didn’t end with expected SYNC tail (00.00.ff.ff): %v.02x', $_[0]);
}
return $_[0];
}
sub _compress {
my ($self) = @_; # $_[1] = payload; $_[2] = flush method
$_payload_sr = \$_[1];
_DEBUG && _debug(sprintf "to deflate: [%v.02x]", $$_payload_sr);
my $out;
my $dstatus = $self->{'d'}->deflate( $$_payload_sr, $out );
die "deflate: $dstatus" if $dstatus != Compress::Raw::Zlib::Z_OK();
_DEBUG && _debug(sprintf "post-deflate output: [%v.02x]", $out);
if ($_[2]) {
$dstatus = $self->{'d'}->flush($out, $_[2]);
die "deflate flush: $dstatus" if $dstatus != Compress::Raw::Zlib::Z_OK();
undef $self->{'_streamer_mode'};
_DEBUG && _debug(sprintf "post-flush output: [%v.02x]", $out);
}
#NB: The RFC directs at this point that:
#
#If the resulting data does not end with an empty DEFLATE block
#with no compression (the "BTYPE" bits are set to 00), append an
#empty DEFLATE block with no compression to the tail end.
#
#… but I don’t know the protocol well enough to detect that??
#
#NB:
#> perl -MCompress::Raw::Zlib -e' my $deflate = Compress::Raw::Zlib::Deflate->new( -WindowBits => -8, -AppendOutput => 1, -Level => Compress::Raw::Zlib::Z_NO_COMPRESSION ); $deflate->deflate( "", my $out ); $deflate->flush( $out, Compress::Raw::Zlib::Z_SYNC_FLUSH()); print $out' | xxd
#00000000: 0000 00ff ff .....
# if ( $_[2] == Compress::Raw::Zlib::Z_FULL_FLUSH() ) {
# if ( substr($out, -4) eq _ZLIB_SYNC_TAIL ) {
# substr($out, -4) = q<>;
# }
# else {
# die sprintf('deflate/flush didn’t end with expected SYNC tail (00.00.ff.ff): %v.02x', $out);
# }
# }
return $out;
}
#----------------------------------------------------------------------
sub _create_inflate_obj {
my ($self) = @_;
my $window_bits = $self->{'inflate_max_window_bits'} || ( Net::WebSocket::PMCE::deflate::Constants::VALID_MAX_WINDOW_BITS() )[-1];
require Compress::Raw::Zlib;
my ($inflate, $istatus) = Compress::Raw::Zlib::Inflate->new(
-WindowBits => -$window_bits,
-AppendOutput => 1,
);
die "Inflate: $istatus" if $istatus != Compress::Raw::Zlib::Z_OK();
return $inflate;
}
sub _create_deflate_obj {
my ($self) = @_;
my $window_bits = $self->{'deflate_max_window_bits'} || ( Net::WebSocket::PMCE::deflate::Constants::VALID_MAX_WINDOW_BITS() )[-1];
require Compress::Raw::Zlib;
my ($deflate, $dstatus) = Compress::Raw::Zlib::Deflate->new(
-WindowBits => -$window_bits,
-AppendOutput => 1,
);
die "Deflate: $dstatus" if $dstatus != Compress::Raw::Zlib::Z_OK();
return $deflate;
}
sub _debug {
print STDERR "$_[0]$/";
}
1;