CGI-Compress-Gzip-1.03/0000755000076500007650000000000011076263272013561 5ustar chrischrisCGI-Compress-Gzip-1.03/Build.PL0000444000076500007650000000111011076263272015044 0ustar chrischrisuse Module::Build; Module::Build->new( module_name => 'CGI::Compress::Gzip', dist_author => 'Chris Dolan ', license => 'perl', requires => { 'perl' => '5.006', 'CGI' => '2.00', 'IO::Zlib' => '1.01', 'Compress::Zlib' => 2, # Need v2 for coming fileno support }, build_requires => { 'Test::More' => '0.01', 'File::Temp' => 0, }, add_to_cleanup => [ 'CGI-Compress-Gzip-*' ], create_readme => 1, create_makefile_pl => 'traditional', )->create_build_script; CGI-Compress-Gzip-1.03/CHANGES0000444000076500007650000000677311076263271014566 0ustar chrischrisRevision history for Perl module CGI::Compress::Gzip 1.03 17 Oct 2008 [FIXES] - A speculative \n fix for Win32 1.02 16 Oct 2008 [FIXES] - Make the test code more robust against reordered headers 1.01 07 Oct 2008 [FIXES] - CGI.pm is downcasing headers on some smoke machines. I don't know why. srezic via http://www.nntp.perl.org/group/perl.cpan.testers/2008/10/msg2390972.html [PREREQUISITES] - Upped Compress::Zlib to v2. Paul Marquess has pointed out that I can use improvement in Compress::Zlib to get streaming under mod_perl 1.00 06 Oct 2008 [FIXES] - OK, the last one didn't really fix it. This time I think I nailed it with the help of: srezic via http://www.nntp.perl.org/group/perl.cpan.testers/2008/10/msg2382496.html (and many others) [PREREQUISITES] - Added Compress::Zlib. At least one user was bitten by IO::Zlib's support for shelling out to command-line gzip. 0.23 05 Oct 2008 [FIXES] - FINALLY fixed a long standing, spurious test failure due to envvars not passed to subprocesses. For example: http://www.nntp.perl.org/group/perl.cpan.testers/2008/10/msg2380213.html [INTERNALS] - Comply with Perl::Critic v1.093 - Clean up test code 0.22 07 Nov 2006 [FIXES] - Set binmode in tests [INTERNALS] - Comply with Perl::Critic v0.21 0.21 04 Oct 2005 [ENHANCEMENTS] - Support 5.6.0's CGI.pm - More regression tests [FIXES] - Unfreed scalar on 5.8.6 threaded on Linux - Explicitly state 5.6.0 minimum - Allow empty body - Some header handling bug fixes [INTERNALS] - Break helper package out to its own file 0.20 22 Apr 2005 [INCOMPATIBILITIES] - Relicensed to Artistic/GPL 0.19 21 Dec 2004 [FIXES] - Add workaround for unbuffered CGI output 0.18 30 Nov 2004 [ENHANCEMENTS] - Support more of the standard CGI->header() arguments 0.17 08 Jan 2004 [ENHANCEMENTS] - Support -type CGI header flag [DOCUMENTATION] - Add caveat about explicit use of filehandles 0.16 02 Jan 2004 [DOCUMENTATION] - Clean up documentation - Fix some URL errors 0.15 30 Dec 2003 [FIXES] - Fix README - Fix bug parsing CGI header() arguments - Add programmer-configurable mime-type selection 0.14 16 Sep 2003 [ENHANCEMENTS] - Support for Module::Build 0.13 28 Aug 2003 [FIXES] - Add support for CGI redirect() calls (if HTTP Status is not 200, content should not be compressed). 0.12 22 Aug 2003 [FIXES] - Headers were altered even when compression was inappropriate. 0.11 14 Mar 2003 [ENHANCEMENTS] - Handle default content-type of text/html - Add helpful diagnostic HTTP headers, if requested 0.10 13 Mar 2003 [FIXES] - Fix for mod_perl -- use in-memory compression 0.04 12 Feb 2003 [FIXES] - Disable for mod_perl - Case-insensitive content-encoding tests [DOCUMENTATION] - Acknowledge Windows failure [INTERNALS] - Rename the wrapper class to CGI::Compress::Gzip::wrapper 0.03 06 Feb 2003 [ENHANCEMENTS] - Header tweaks towards mod_perl functionality - Work on the compression decision 0.02 05 Feb 2003 [ENHANCEMENTS] - Substantial overhaul. Now it works under 5.6.0, but still not under mod_perl. 0.01 20 Jan 2003 Initial version CGI-Compress-Gzip-1.03/lib/0000755000076500007650000000000011076263272014327 5ustar chrischrisCGI-Compress-Gzip-1.03/lib/CGI/0000755000076500007650000000000011076263272014731 5ustar chrischrisCGI-Compress-Gzip-1.03/lib/CGI/Compress/0000755000076500007650000000000011076263272016524 5ustar chrischrisCGI-Compress-Gzip-1.03/lib/CGI/Compress/Gzip/0000755000076500007650000000000011076263272017435 5ustar chrischrisCGI-Compress-Gzip-1.03/lib/CGI/Compress/Gzip/FileHandle.pm0000444000076500007650000001217711076263272021774 0ustar chrischrispackage CGI::Compress::Gzip::FileHandle; use 5.006; use warnings; use strict; use English qw(-no_match_vars); use Compress::Zlib; use base qw(IO::Zlib); our $VERSION = '1.03'; #=encoding utf8 =for stopwords zlib =head1 NAME CGI::Compress::Gzip::FileHandle - CGI::Compress::Gzip helper package =head1 LICENSE Copyright 2006-2007 Clotho Advanced Media, Inc., Copyright 2007-2008 Chris Dolan This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =head1 SYNOPSIS use CGI::Compress::Gzip; my $cgi = new CGI::Compress::Gzip; print $cgi->header(); print " ..."; =head1 DESCRIPTION This is intended for internal use only! Use CGI::Compress::Gzip instead. This CGI::Compress::Gzip helper class subclasses IO::Zlib. It is is needed to make sure that output is not compressed until the CGI header is emitted. This filehandle delays the ignition of the zlib filter until it sees the exact same header generated by CGI::Compress::Gzip::header() pass through it's WRITE() method. If you change the header before printing it, this class will throw an exception. This class holds one global variable representing the previous default filehandle used before the gzip filter is put in place. This filehandle, usually STDOUT, is replaced after the gzip stream finishes (which is usually when the CGI object goes out of scope and is destroyed). =head1 FUNCTIONS =over =item OPEN Overrides IO::Zlib::OPEN. This method doesn't actually do anything -- it just stores it's arguments for a later call to SUPER::OPEN in WRITE(). The reason is that we may not have seen the header yet, so we don't yet know whether to compress output. =cut sub OPEN { my ($self, $fh, @args) = @_; # Delay opening until after the header is printed. $self->{out_fh} = $fh; $self->{openargs} = \@args; $self->{outtype} = undef; $self->{buffer} = q{}; $self->{pending_header} = q{}; return $self; } =item WRITE buffer, length, offset Emit the uncompressed header followed by the compressed body. =cut sub WRITE { my ($self, $buf, $length, $offset) = @_; # Appropriated from IO::Zlib: if ($length > length $buf) { die 'bad LENGTH'; } if (defined $offset && $offset != 0) { die 'OFFSET not supported'; } my $bytes = 0; if ($self->{pending_header}) { # Side effects: $buf and $self->{pending_header} are trimmed $bytes = $self->_print_header(\$buf, $length); $length -= $bytes; } return $bytes if (!$length); # if length is zero, there's no body content to print if (!defined $self->{outtype}) { # Determine whether we can stream data to the output filehandle # default case: no, cannot stream $self->{outtype} = 'block'; # Mod perl already does funky filehandle stuff, so don't stream my $is_mod_perl = ($ENV{MOD_PERL} || ($ENV{GATEWAY_INTERFACE} && $ENV{GATEWAY_INTERFACE} =~ m/ \A CGI-Perl\/ /xms)); my $type = ref $self->{out_fh}; if (!$is_mod_perl && $type) { my $is_glob = $type eq 'GLOB' && defined $self->{out_fh}->fileno(); my $is_filehandle = ($type !~ m/ \A GLOB|SCALAR|HASH|ARRAY|CODE \z /xms && $self->{out_fh}->can('fileno') && defined $self->{out_fh}->fileno()); if ($is_glob || $is_filehandle) { # Complete delayed open if (!$self->SUPER::OPEN($self->{out_fh}, @{$self->{openargs}})) { die 'Failed to open the compressed output stream'; } $self->{outtype} = 'stream'; } } } if ($self->{outtype} eq 'stream') { $bytes += $self->SUPER::WRITE($buf, $length, $offset); } else { $self->{buffer} .= $buf; $bytes += length $buf; } return $bytes; } sub _print_header { my ($self, $buf, $length) = @_; my $header = $self->{pending_header}; if ($length < length $header) { $self->{pending_header} = substr $header, $length; $header = substr $header, 0, $length; } else { $self->{pending_header} = q{}; } if (${$buf} !~ s/ \A \Q$header\E //xms) { die 'Expected to print the CGI header'; } my $out_fh = $self->{out_fh}; if (!print {$out_fh} $header) { die 'Failed to print the uncompressed CGI header'; } return length $header; } =item CLOSE Flush the compressed output. =cut sub CLOSE { my ($self) = @_; my $out_fh = $self->{out_fh}; $self->{out_fh} = undef; # clear it, so we can't write to it after this method ends my $result; if ($self->{outtype} && $self->{outtype} eq 'stream') { $result = $self->SUPER::CLOSE(); if (!$result) { die "Failed to close gzip $OS_ERROR"; } } else { print {$out_fh} Compress::Zlib::memGzip($self->{buffer}); $result = 1; } return $result; } 1; __END__ =back =head1 AUTHOR Clotho Advanced Media, I Primary developer: Chris Dolan =cut CGI-Compress-Gzip-1.03/lib/CGI/Compress/Gzip.pm0000444000076500007650000003740711076263272020004 0ustar chrischrispackage CGI::Compress::Gzip; use 5.006; use warnings; use strict; use English qw(-no_match_vars); use CGI::Compress::Gzip::FileHandle; use base 'CGI'; our $VERSION = '1.03'; # Package globals - testing and debugging flags # These should only be used for extreme circumstances (e.g. testing) our $global_use_compression = 1; # user-settable our $global_can_compress = undef; # 1 = yes, 0 = no, undef = don't know yet # If true, add an outgoing HTTP header explaining why we are not # compressing if gzip turns itself off. our $global_give_reason = 0; #=encoding utf8 =head1 NAME CGI::Compress::Gzip - CGI with automatically compressed output =head1 LICENSE Copyright 2006-2007 Clotho Advanced Media, Inc., Copyright 2007-2008 Chris Dolan This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =head1 SYNOPSIS use CGI::Compress::Gzip; my $cgi = new CGI::Compress::Gzip; print $cgi->header(); print " ..."; See the CAVEATS section below! =head1 DESCRIPTION CGI::Compress::Gzip extends the CGI module to auto-detect whether the client browser wants compressed output and, if so and if the script chooses HTML output, apply gzip compression on any content header for STDOUT. This module is intended to be a drop-in replacement for CGI.pm. Apache mod_perl users may wish to consider the Apache::Compress or Apache::GzipChain modules, which allow more transparent output compression than this module can provide. However, as of this writing those modules are more aggressive about compressing, regardless of Content-Type. =head2 Headers At the time that a header is requested, CGI::Compress::Gzip checks the HTTP_ACCEPT_ENCODING environment variable (passed by Apache). If this variable includes the flag "gzip" and the outgoing mime-type is "text/*", then gzipped output is preferred. [the default mime-type selection of text/* can be changed by subclassing -- see below] The header is altered to add the "Content-Encoding: gzip" flag which indicates that compression is turned on. Naturally, it is crucial that the CGI application output nothing before the header is printed. If this is violated, things will go badly. =head2 Compression When the header is created, this module sets up a new filehandle to accept data. STDOUT is redirected through that filehandle. The new filehandle passes data verbatim until it detects the end of the CGI header. At that time, it switches over to Gzip output for the remainder of the CGI run. Note that the Zlib library on which this code is ultimately based requires a fileno for the output filehandle. Where the output filehandle is faked (i.e. in mod_perl), we instead use in-memory compression. This is more wasteful of RAM, but it is the only solution I've found (and it is one shared by the Apache::* compression modules). Debugging note: if you set B<$CGI::Compress::Gzip::global_give_reason> to a true value, then this module will add an HTTP header entry called B with an explanation of why it chose not to gzip the output stream. =head2 Buffering The Zlib library introduces latencies. In some cases, this module may delay output until the CGI object is garbage collected, presumably at the end of the program. This buffering can be detrimental to long-lived programs which are supposed to have incremental output, causing browser timeouts. To compensate, compression is automatically disabled when autoflush (i.e. the $| variable) is set to true. Future versions may try to enable autoflushing on the Zlib filehandles, if possible [Help wanted]. =head1 CLASS METHODS =over 4 =item $pkg->new([CGI ARGS]) Create a new object. This resets the environment before creating a CGI.pm object. This should not be called more than once per script run! All arguments are passed to the parent class. =cut sub new { my ($pkg, @args) = @_; select STDOUT; my $self = $pkg->SUPER::new(@args); $self->{'.CGIgz'} = { ext_fh => undef, zlib_fh => undef, header_done => 0, use_compression => undef, }; return $self; } =item $pkg->useCompression($boolean) =item $self->useCompression($boolean) This can be used as a class method or an instance method. The former is included for backward compatibility, and is NOT recommended. As a class method, this changes the default value. As an instance method it affects only the specified instance. Turn compression on/off for the target. If turned on, compression will be used only if the prerequisite compression libraries are available and if the client browser requests compression. Defaults to on. =cut sub useCompression { my ($pkg_or_self, $value) = @_; if (ref $pkg_or_self) { $pkg_or_self->{'.CGIgz'}->{use_compression} = $value ? 1 : 0; } else { $global_use_compression = $value ? 1 : 0; } return $pkg_or_self; } =back =head1 INSTANCE METHODS =over 4 =item $self->useFileHandle($filehandle) Manually set the output filehandle. Because of limitations of Zlib, this MUST be a real filehandle (with valid results from fileno()) and not a pseudo filehandle like IO::String. If this is not set, STDOUT is used. =cut sub useFileHandle { my ($self, $fh) = @_; $self->{'.CGIgz'}->{ext_fh} = $fh; return $self; } =item $self->isCompressibleType($content_type) Given a MIME type (with possible charset attached), return a boolean indicating if this media type is a good candidate for compression. This implementation is simply: return $type =~ /^text\//; Subclasses may wish to override this method to apply different criteria. =cut sub isCompressibleType { my ($self, $type) = @_; return ($type || q{}) =~ m/ \A text\/ /xms; } =item $self->header([HEADER ARGS]) Overrides the C method in L. Return a CGI header with the compression flags set properly. Returns an empty string is a header has already been printed. This method engages the Gzip output by fiddling with the default output filehandle. All subsequent output via usual Perl print() will be automatically gzipped except for this header (which must go out as plain text). Any arguments will be passed on to CGI::header. This method should NOT be called if you don't want your header or STDOUT to be fiddled with. =cut sub header { my ($self, @args) = @_; my ($compress, $reason) = $self->_can_compress(\@args); if (!$compress && $global_give_reason && $reason) { push @args, '-X_non_gzip_reason', $reason; } my $header = $self->SUPER::header(@args); if (!defined $header) # workaround for problem found on 5.6.0 on Linux { $header = q{}; } if (!$self->{'.CGIgz'}->{header_done}++ && $compress) { $self->_start_compression($header); } return $header; } # Enable the compression filehandle if: # - The mime-type is appropriate (text/* is the default) # - The programmer wants compression, indicated by the useCompression() # method # - Client wants compression, indicated by the Accepted-Encoding HTTP field # - The IO::Zlib compression library is available # Returns: (boolean, reason) -- reason is a string if boolean is false # Side effects: # - may alter $header to add gzip flag if boolean is true # - may set $global_can_compress if not yet set sub _can_compress ## no critic(Subroutines::ProhibitExcessComplexity) { my ($self, $header) = @_; # $header is an array ref my $settings = $self->{'.CGIgz'}; # Check programmer preference if (defined $settings->{use_compression} ? !$settings->{use_compression} : !$global_use_compression) { return (0, 'programmer request'); } # save it in case we change it $settings->{flush} = $OUTPUT_AUTOFLUSH; # Check buffering (disable if autoflushing) if ($settings->{flush}) { return (0, 'programmer wants unbuffered output'); } # Check that browser supports gzip my $acc = $ENV{HTTP_ACCEPT_ENCODING}; if (!$acc || $acc !~ m/ \bgzip\b /ixms) { return (0, 'user agent does not want gzip'); } # Parse the header data and look for indicators of compressibility: # * appropriate content type # * already set for compression # * HTTP status not 200 my @newheader; my $content_type; # This search reproduces the header parsing done by CGI.pm if (@{$header} && $header->[0] =~ m/ \A [a-z] /xms) ## no critic (ProhibitEnumeratedClasses) { # Using unkeyed version of arguments - convert to the keyed version # arg order comes from the header() function in CGI.pm my @flags = qw( Content_Type Status Cookie Target Expires NPH Charset Attachment P3P ); for my $i (0 .. $#{$header}) { if ($i < @flags) { push @newheader, q{-} . $flags[$i], $header->[$i]; } else { # Extra args push @newheader, $header->[$i]; } } } else { @newheader = @{$header}; } # gets set if we find an existing encoding directive my $encoding_index; for (my $i = 0; $i < @newheader; $i++) { next if (!defined $newheader[$i]); if ($newheader[$i] =~ m/ \A -?(?:Content[-_]Type|Type)(.*) \z /ixms) { $content_type = $1; if ($content_type !~ s/ \A :\s* //xms) { $content_type = $newheader[++$i]; } } elsif ($newheader[$i] =~ m/ \A -?Status(.*) \z /ixms) { my $content = $1; if ($content !~ s/ \A :\s* //xms) { $content = $newheader[++$i]; } my ($status) = $content =~ m/ \A (\d+) /xms; if (!defined $status || $status ne '200') { return (0, 'HTTP status not 200'); } } elsif ($newheader[$i] =~ m/ \A -?Content[-_]Encoding(.*) \z /ixms) { my $content = $1; if ($content !~ s/ \A :\s* //xms) { $content = $newheader[++$i]; } $encoding_index = $i; if ($content =~ m/ \bgzip\b /ixms) { # Already gzip compressed return (0, 'someone already requested gzip'); } } } if (defined $encoding_index) { # prepend gzip encoding to the existing encoding list $newheader[$encoding_index] =~ s/ \A ((?:-?Content[-_]Encoding:\s*)?) /$1gzip, /ioxms; } else { push @newheader, '-Content_Encoding', 'gzip'; } $content_type ||= 'text/html'; if (!$self->isCompressibleType($content_type)) { # Not compressible media return (0, 'incompatible content-type ' . $content_type); } # Check that IO::Zlib is available if (!defined $global_can_compress) { local $SIG{__WARN__} = 'DEFAULT'; local $SIG{__DIE__} = 'DEFAULT'; eval { require IO::Zlib; }; ## no critic (RequireCheckingReturnValueOfEval) $global_can_compress = $EVAL_ERROR ? 0 : 1; } if (!$global_can_compress) { return (0, 'IO::Zlib not found'); } # Commit any changes made above @{$header} = @newheader; return (1, undef); } sub _start_compression { my ($self, $header) = @_; my $settings = $self->{'.CGIgz'}; $settings->{ext_fh} ||= \*STDOUT; binmode $settings->{ext_fh}; my $filehandle = CGI::Compress::Gzip::FileHandle->new($settings->{ext_fh}, 'wb'); if (!$filehandle) { warn 'Failed to open Zlib output, reverting to uncompressed output'; return; } # All output from here on goes to our new filehandle ## Autoflush makes no sense since compression is disabled if autoflush is on #if ($filehandle->can('autoflush')) #{ # $filehandle->autoflush(); #} select $filehandle; $settings->{zlib_fh} = $filehandle; # needed for destructor my $tied = tied ${$filehandle}; $tied->{pending_header} = $header; return $self; } =item $self->DESTROY() Override the L destructor so we can close the Gzip output stream, if there is one open. =cut sub DESTROY { my ($self) = @_; if ($self->{'.CGIgz'}->{zlib_fh}) { $self->{'.CGIgz'}->{zlib_fh}->close() or die 'Failed to close the Zlib filehandle'; } if ($self->{'.CGIgz'}->{ext_fh}) { select $self->{'.CGIgz'}->{ext_fh}; } return $self->SUPER::DESTROY(); } 1; __END__ =back =head1 CAVEATS =head2 Apache::Registry Under Apache::Registry, global variables may not go out of scope in time. This may causes timing bugs, since this module makes use of the DESTROY() method. To avoid this issue, make sure your CGI object is stored in a scoped variable. # BROKEN CODE use CGI::Compress::Gzip; $q = CGI::Compress::Gzip->new; print $q->header; print "Hello, world\n"; # WORKAROUND CODE use CGI::Compress::Gzip; do { my $q = CGI::Compress::Gzip->new; print $q->header; print "Hello, world\n"; } =head2 Filehandles This module works by changing the default filehandle. It does not change STDOUT at all. As a consequence, your programs should call C without a filehandle argument. # BROKEN CODE use CGI::Compress::Gzip; my $q = CGI::Compress::Gzip->new; print STDOUT $q->header; print STDOUT "Hello, world\n"; # WORKAROUND CODE use CGI::Compress::Gzip; my $q = CGI::Compress::Gzip->new; print $q->header; print "Hello, world\n"; Future versions may steal away STDOUT and replace it with the compression filehandle, but that seemed too risky for this version. =head2 Header Munging When sending compressed output, the HTTP headers must remain uncompressed. So, this module goes to great effort to keep the headers and body separate. That has led to CGI::header() emulation code that is a little brittle. Most potential problems arise because STDOUT gets tweaked as soon as header() is called. If you use the CGI.pm header() API as specified in CGI.pm, then all should go well. But if you do anything unusual, this module may break. For example: # BROKEN CODE use CGI::Compress::Gzip; my $q = CGI::Compress::Gzip->new; print "Set-Cookie: foo=bar\n" . $q->header; print "Hello, world\n"; # WORKAROUND 1 (preferred) use CGI::Compress::Gzip; my $q = CGI::Compress::Gzip->new; print $q->header("-Set_Cookie" => "foo=bar"); print "Hello, world\n"; # WORKAROUND 2 use CGI::Compress::Gzip; my $q = CGI::Compress::Gzip->new; print "Set-Cookie: foo=bar\n"; print $q->header; print "Hello, world\n"; Future versions could try to parse the header to look for its end rather than insisting that the printed version match the version returned by header(). Patches would be very welcome. =head1 SEE ALSO CGI::Compress::Gzip depends on CGI and IO::Zlib. Similar functionality is available from mod_gzip, Apache::Compress or Apache::GzipChain, however all of those require changes to the webserver configuration. =head1 AUTHOR Chris Dolan This module was originally developed by me at Clotho Advanced Media Inc. Now I maintain it in my spare time. =head1 ACKNOWLEDGMENTS Clotho greatly appreciates the assistance and feedback the community has extended to help refine this module. Thanks to Rhesa Rozendaal who noticed the -Type omission in v0.17. Thanks to Laga Mahesa who did some Windows testing and experimentation. Thanks to Slaven Rezic who 1) found several header handling bugs, 2) discovered the Apache::Registry and Filehandle caveats, 3) provided a patch incorporated into v0.17, and 4) persisted with smoke tests that reproduced the envvar problem fixed in v0.23. Thanks to Jan Willamowius who found a header handling bug. Thanks to Andreas J. Koenig and brian d foy for module naming advice. =head1 HELP WANTED If you like this module, please help by testing on Windows or in a C environment, since I have neither available for easy testing. Personally, I don't use this module much anymore as all of my work is on Catalyst and mod_perl now. =cut CGI-Compress-Gzip-1.03/LICENSE0000444000076500007650000000025411076263272014565 0ustar chrischrisCopyright 2005 Clotho Advanced Media, Inc., This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. CGI-Compress-Gzip-1.03/Makefile.PL0000444000076500007650000000115011076263272015526 0ustar chrischris# Note: this file was auto-generated by Module::Build::Compat version 0.03 use ExtUtils::MakeMaker; WriteMakefile ( 'NAME' => 'CGI::Compress::Gzip', 'VERSION_FROM' => 'lib/CGI/Compress/Gzip.pm', 'PREREQ_PM' => { 'CGI' => '2.00', 'Compress::Zlib' => '2', 'File::Temp' => '0', 'IO::Zlib' => '1.01', 'Test::More' => '0.01' }, 'INSTALLDIRS' => 'site', 'EXE_FILES' => [], 'PL_FILES' => {} ) ; CGI-Compress-Gzip-1.03/MANIFEST0000444000076500007650000000025011076263272014705 0ustar chrischrisBuild.PL CHANGES lib/CGI/Compress/Gzip.pm lib/CGI/Compress/Gzip/FileHandle.pm LICENSE Makefile.PL MANIFEST META.yml README t/gzip.t t/pod-coverage.t t/pod.t t/testhelp CGI-Compress-Gzip-1.03/META.yml0000444000076500007650000000120411076263272015025 0ustar chrischris--- name: CGI-Compress-Gzip version: 1.03 author: - 'Chris Dolan ' abstract: CGI with automatically compressed output license: perl resources: license: http://dev.perl.org/licenses/ requires: CGI: 2.00 Compress::Zlib: 2 IO::Zlib: 1.01 perl: 5.006 build_requires: File::Temp: 0 Test::More: 0.01 provides: CGI::Compress::Gzip: file: lib/CGI/Compress/Gzip.pm version: 1.03 CGI::Compress::Gzip::FileHandle: file: lib/CGI/Compress/Gzip/FileHandle.pm version: 1.03 generated_by: Module::Build version 0.2808 meta-spec: url: http://module-build.sourceforge.net/META-spec-v1.2.html version: 1.2 CGI-Compress-Gzip-1.03/README0000444000076500007650000002223711076263272014445 0ustar chrischrisNAME CGI::Compress::Gzip - CGI with automatically compressed output LICENSE Copyright 2006-2007 Clotho Advanced Media, Inc., Copyright 2007-2008 Chris Dolan This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. SYNOPSIS use CGI::Compress::Gzip; my $cgi = new CGI::Compress::Gzip; print $cgi->header(); print " ..."; See the CAVEATS section below! DESCRIPTION CGI::Compress::Gzip extends the CGI module to auto-detect whether the client browser wants compressed output and, if so and if the script chooses HTML output, apply gzip compression on any content header for STDOUT. This module is intended to be a drop-in replacement for CGI.pm. Apache mod_perl users may wish to consider the Apache::Compress or Apache::GzipChain modules, which allow more transparent output compression than this module can provide. However, as of this writing those modules are more aggressive about compressing, regardless of Content-Type. Headers At the time that a header is requested, CGI::Compress::Gzip checks the HTTP_ACCEPT_ENCODING environment variable (passed by Apache). If this variable includes the flag "gzip" and the outgoing mime-type is "text/*", then gzipped output is preferred. [the default mime-type selection of text/* can be changed by subclassing -- see below] The header is altered to add the "Content-Encoding: gzip" flag which indicates that compression is turned on. Naturally, it is crucial that the CGI application output nothing before the header is printed. If this is violated, things will go badly. Compression When the header is created, this module sets up a new filehandle to accept data. STDOUT is redirected through that filehandle. The new filehandle passes data verbatim until it detects the end of the CGI header. At that time, it switches over to Gzip output for the remainder of the CGI run. Note that the Zlib library on which this code is ultimately based requires a fileno for the output filehandle. Where the output filehandle is faked (i.e. in mod_perl), we instead use in-memory compression. This is more wasteful of RAM, but it is the only solution I've found (and it is one shared by the Apache::* compression modules). Debugging note: if you set $CGI::Compress::Gzip::global_give_reason to a true value, then this module will add an HTTP header entry called X-non-gzip-reason with an explanation of why it chose not to gzip the output stream. Buffering The Zlib library introduces latencies. In some cases, this module may delay output until the CGI object is garbage collected, presumably at the end of the program. This buffering can be detrimental to long-lived programs which are supposed to have incremental output, causing browser timeouts. To compensate, compression is automatically disabled when autoflush (i.e. the $| variable) is set to true. Future versions may try to enable autoflushing on the Zlib filehandles, if possible [Help wanted]. CLASS METHODS $pkg->new([CGI ARGS]) Create a new object. This resets the environment before creating a CGI.pm object. This should not be called more than once per script run! All arguments are passed to the parent class. $pkg->useCompression($boolean) $self->useCompression($boolean) This can be used as a class method or an instance method. The former is included for backward compatibility, and is NOT recommended. As a class method, this changes the default value. As an instance method it affects only the specified instance. Turn compression on/off for the target. If turned on, compression will be used only if the prerequisite compression libraries are available and if the client browser requests compression. Defaults to on. INSTANCE METHODS $self->useFileHandle($filehandle) Manually set the output filehandle. Because of limitations of Zlib, this MUST be a real filehandle (with valid results from fileno()) and not a pseudo filehandle like IO::String. If this is not set, STDOUT is used. $self->isCompressibleType($content_type) Given a MIME type (with possible charset attached), return a boolean indicating if this media type is a good candidate for compression. This implementation is simply: return $type =~ /^text\//; Subclasses may wish to override this method to apply different criteria. $self->header([HEADER ARGS]) Overrides the `header()' method in CGI. Return a CGI header with the compression flags set properly. Returns an empty string is a header has already been printed. This method engages the Gzip output by fiddling with the default output filehandle. All subsequent output via usual Perl print() will be automatically gzipped except for this header (which must go out as plain text). Any arguments will be passed on to CGI::header. This method should NOT be called if you don't want your header or STDOUT to be fiddled with. $self->DESTROY() Override the CGI destructor so we can close the Gzip output stream, if there is one open. CAVEATS Apache::Registry Under Apache::Registry, global variables may not go out of scope in time. This may causes timing bugs, since this module makes use of the DESTROY() method. To avoid this issue, make sure your CGI object is stored in a scoped variable. # BROKEN CODE use CGI::Compress::Gzip; $q = CGI::Compress::Gzip->new; print $q->header; print "Hello, world\n"; # WORKAROUND CODE use CGI::Compress::Gzip; do { my $q = CGI::Compress::Gzip->new; print $q->header; print "Hello, world\n"; } Filehandles This module works by changing the default filehandle. It does not change STDOUT at all. As a consequence, your programs should call `print' without a filehandle argument. # BROKEN CODE use CGI::Compress::Gzip; my $q = CGI::Compress::Gzip->new; print STDOUT $q->header; print STDOUT "Hello, world\n"; # WORKAROUND CODE use CGI::Compress::Gzip; my $q = CGI::Compress::Gzip->new; print $q->header; print "Hello, world\n"; Future versions may steal away STDOUT and replace it with the compression filehandle, but that seemed too risky for this version. Header Munging When sending compressed output, the HTTP headers must remain uncompressed. So, this module goes to great effort to keep the headers and body separate. That has led to CGI::header() emulation code that is a little brittle. Most potential problems arise because STDOUT gets tweaked as soon as header() is called. If you use the CGI.pm header() API as specified in CGI.pm, then all should go well. But if you do anything unusual, this module may break. For example: # BROKEN CODE use CGI::Compress::Gzip; my $q = CGI::Compress::Gzip->new; print "Set-Cookie: foo=bar\n" . $q->header; print "Hello, world\n"; # WORKAROUND 1 (preferred) use CGI::Compress::Gzip; my $q = CGI::Compress::Gzip->new; print $q->header("-Set_Cookie" => "foo=bar"); print "Hello, world\n"; # WORKAROUND 2 use CGI::Compress::Gzip; my $q = CGI::Compress::Gzip->new; print "Set-Cookie: foo=bar\n"; print $q->header; print "Hello, world\n"; Future versions could try to parse the header to look for its end rather than insisting that the printed version match the version returned by header(). Patches would be very welcome. SEE ALSO CGI::Compress::Gzip depends on CGI and IO::Zlib. Similar functionality is available from mod_gzip, Apache::Compress or Apache::GzipChain, however all of those require changes to the webserver configuration. AUTHOR Chris Dolan This module was originally developed by me at Clotho Advanced Media Inc. Now I maintain it in my spare time. ACKNOWLEDGMENTS Clotho greatly appreciates the assistance and feedback the community has extended to help refine this module. Thanks to Rhesa Rozendaal who noticed the -Type omission in v0.17. Thanks to Laga Mahesa who did some Windows testing and experimentation. Thanks to Slaven Rezic who 1) found several header handling bugs, 2) discovered the Apache::Registry and Filehandle caveats, 3) provided a patch incorporated into v0.17, and 4) persisted with smoke tests that reproduced the envvar problem fixed in v0.23. Thanks to Jan Willamowius who found a header handling bug. Thanks to Andreas J. Koenig and brian d foy for module naming advice. HELP WANTED If you like this module, please help by testing on Windows or in a `FastCGI' environment, since I have neither available for easy testing. Personally, I don't use this module much anymore as all of my work is on Catalyst and mod_perl now. CGI-Compress-Gzip-1.03/t/0000755000076500007650000000000011076263272014024 5ustar chrischrisCGI-Compress-Gzip-1.03/t/gzip.t0000444000076500007650000003030711076263272015163 0ustar chrischris#!/usr/bin/perl -w ## no critic (ProhibitExcessMainComplexity) ## no critic (ProhibitBacktickOperators) ## no critic (ProhibitCommentedOutCode) ## no critic (ProhibitQuotedWordLists) ## no critic (ProhibitLocalVars) use 5.006; use strict; use warnings; use File::Temp qw(tempfile); use IO::Zlib; use Compress::Zlib; use English qw(-no_match_vars); BEGIN { use Test::More tests => 44; use_ok('CGI::Compress::Gzip'); } # This module behaves differently whether autoflush is on or off # Make sure it is off $OUTPUT_AUTOFLUSH = 0; my $compare = 'Hello World!'; # expected output # Have to use a temp file since Compress::Zlib doesn't like IO::String my ($testfh, $testfile) = tempfile(UNLINK => 1); close $testfh or die; ## Zlib sanity tests my $zcompare = Compress::Zlib::memGzip($compare); my $testbuf = $zcompare; $testbuf = Compress::Zlib::memGunzip($testbuf); is ($testbuf, $compare, 'Compress::Zlib double-check'); { ## no critic (ProhibitBarewordFileHandles,RequireInitializationForLocalVars) local *OUT_FILE; open OUT_FILE, '>', $testfile or die 'Cannot write a temp file'; binmode OUT_FILE; local *STDOUT = *OUT_FILE; my $fh = IO::Zlib->new(\*OUT_FILE, 'wb') or die; print {$fh} $compare; close $fh or die; close OUT_FILE ## no critic (RequireCheckedClose,RequireCheckedSyscalls) and diag('Unexpected success closing already closed filehandle'); my $in_fh; open $in_fh, '<', $testfile or die 'Cannot read temp file'; binmode $in_fh; local $INPUT_RECORD_SEPARATOR = undef; my $out = <$in_fh>; close $in_fh or die; is($out, $zcompare, 'IO::Zlib test'); } ## Header tests { my $dummy = CGI::Compress::Gzip->new(); ok(!$dummy->isCompressibleType(), 'compressible types'); ok($dummy->isCompressibleType('text/html'), 'compressible types'); ok($dummy->isCompressibleType('text/plain'), 'compressible types'); ok(!$dummy->isCompressibleType('image/jpg'), 'compressible types'); ok(!$dummy->isCompressibleType('application/octet-stream'), 'compressible types'); { local $ENV{HTTP_ACCEPT_ENCODING} = q{}; my @headers; my ($compress, $reason) = $dummy->_can_compress(\@headers); is_deeply([$compress, \@headers], [0, []], 'header test - env'); } { local $ENV{HTTP_ACCEPT_ENCODING} = 'bzip2'; my @headers; my ($compress, $reason) = $dummy->_can_compress(\@headers); is_deeply([$compress, \@headers], [0, []], 'header test - env'); } # For the rest of the tests, pretend browser told us to turn on gzip local $ENV{HTTP_ACCEPT_ENCODING} = 'gzip'; { my @headers; my ($compress, $reason) = $dummy->_can_compress(\@headers); is_deeply([$compress, \@headers], [1, ['-Content_Encoding', 'gzip']], 'header test - env'); } { local $CGI::Compress::Gzip::global_give_reason = 1; my @headers; my ($compress, $reason) = $dummy->_can_compress(\@headers); is_deeply([$compress, \@headers], [1, ['-Content_Encoding', 'gzip']], 'header test - reason'); } # Turn off compression CGI::Compress::Gzip->useCompression(0); { my @headers; my ($compress, $reason) = $dummy->_can_compress(\@headers); is_deeply([$compress, \@headers], [0, []], 'header test - override'); } CGI::Compress::Gzip->useCompression(1); # Turn off compression $dummy->useCompression(0); { my @headers; my ($compress, $reason) = $dummy->_can_compress(\@headers); is_deeply([$compress, \@headers], [0, []], 'header test - override'); } $dummy->useCompression(1); { local $OUTPUT_AUTOFLUSH = 1; my @headers; my ($compress, $reason) = $dummy->_can_compress(\@headers); is_deeply([$compress, \@headers], [0, []], 'header test - autoflush'); } { my @headers = ('text/plain'); my ($compress, $reason) = $dummy->_can_compress(\@headers); is_deeply([$compress, \@headers], [1, ['-Content_Type', 'text/plain', '-Content_Encoding', 'gzip']], 'header test - type'); } { my @headers = ('-Content_Type', 'text/plain'); my ($compress, $reason) = $dummy->_can_compress(\@headers); is_deeply([$compress, \@headers], [1, ['-Content_Type', 'text/plain', '-Content_Encoding', 'gzip']], 'header test - type'); } { my @headers = ('Content_Type', 'text/plain'); my ($compress, $reason) = $dummy->_can_compress(\@headers); is_deeply([$compress, \@headers], [1, ['Content_Type', 'text/plain', '-Content_Encoding', 'gzip']], 'header test - type'); } { my @headers = ('-type', 'text/plain'); my ($compress, $reason) = $dummy->_can_compress(\@headers); is_deeply([$compress, \@headers], [1, ['-type', 'text/plain', '-Content_Encoding', 'gzip']], 'header test - type'); } { my @headers = ('Content_Type: text/plain'); my ($compress, $reason) = $dummy->_can_compress(\@headers); is_deeply([$compress, \@headers], [1, ['Content_Type: text/plain', '-Content_Encoding', 'gzip']], 'header test - type'); } { my @headers = ('Content_Type: image/gif'); my ($compress, $reason) = $dummy->_can_compress(\@headers); is_deeply([$compress, \@headers], [0, ['Content_Type: image/gif']], 'header test - type'); } { my @headers = ('-Content_Encoding', 'foo'); my ($compress, $reason) = $dummy->_can_compress(\@headers); is_deeply([$compress, \@headers], [1, ['-Content_Encoding', 'gzip, foo']], 'header test - encoding'); } { my @headers = ('-Content_Encoding', 'gzip'); my ($compress, $reason) = $dummy->_can_compress(\@headers); is_deeply([$compress, \@headers], [0, ['-Content_Encoding', 'gzip']], 'header test - encoding'); } { my @headers = ('Content-Encoding: foo'); my ($compress, $reason) = $dummy->_can_compress(\@headers); is_deeply([$compress, \@headers], [1, ['Content-Encoding: gzip, foo']], 'header test - encoding'); } { my @headers = ('-Status', '200'); my ($compress, $reason) = $dummy->_can_compress(\@headers); is_deeply([$compress, \@headers], [1, ['-Status', '200', '-Content_Encoding', 'gzip']], 'header test - status'); } { my @headers = ('Status: 200'); my ($compress, $reason) = $dummy->_can_compress(\@headers); is_deeply([$compress, \@headers], [1, ['Status: 200', '-Content_Encoding', 'gzip']], 'header test - status'); } { my @headers = ('Status: 200 OK'); my ($compress, $reason) = $dummy->_can_compress(\@headers); is_deeply([$compress, \@headers], [1, ['Status: 200 OK', '-Content_Encoding', 'gzip']], 'header test - status'); } { my @headers = ('Status: 500'); my ($compress, $reason) = $dummy->_can_compress(\@headers); is_deeply([$compress, \@headers], [0, ['Status: 500']], 'header test - status'); } { my @headers = ('-Status', 'junk'); my ($compress, $reason) = $dummy->_can_compress(\@headers); is_deeply([$compress, \@headers], [0, ['-Status', 'junk']], 'header test - status'); } { my @headers = ('Status: junk'); my ($compress, $reason) = $dummy->_can_compress(\@headers); is_deeply([$compress, \@headers], [0, ['Status: junk']], 'header test - status'); } { my @headers = ('-Irrelevent', '1'); my ($compress, $reason) = $dummy->_can_compress(\@headers); is_deeply([$compress, \@headers], [1, ['-Irrelevent', '1', '-Content_Encoding', 'gzip']], 'header test - other'); } } ## Tests that are as real-life as we can manage # Older versions of this test used to set # local $ENV{HTTP_ACCEPT_ENCODING} = 'gzip' # and expected subshells to propagate that value. I had thought that # caused some smoke environments to fail, so I switched to passing # that value as a cmdline argument. It turns out I was wrong (it was # $| that caused the failures) but I left it anyway. # Turn off compression ok(CGI::Compress::Gzip->useCompression(0), 'Turn off compression'); my $eol = "\015\012"; ## no critic (ProhibitEscapedCharacters) my $redir = 'http://www.foo.com/'; my $interp = "$^X -Iblib/arch -Iblib/lib"; if (defined $Devel::Cover::VERSION) { $interp .= ' -MDevel::Cover'; } my $basecmd = "$interp t/testhelp"; # Get CGI header for comparison in basic case my $compareheader = CGI->new(q{})->header(); my $gzip = 'Content-Encoding: gzip' . $eol; # no compression { my $reason = 'x-non-gzip-reason: user agent does not want gzip' . $eol; my $out = `$basecmd simple "$compare"`; msgs_match($out, $reason . $compareheader . $compare, 'CGI template'); } # no body { my $zempty = Compress::Zlib::memGzip(q{}); my $out = `$basecmd -DHTTP_ACCEPT_ENCODING=gzip empty "$compare"`; msgs_match($out, $gzip . $compareheader . $zempty, 'no body'); } # CGI and compression { my $out = `$basecmd -DHTTP_ACCEPT_ENCODING=gzip simple "$compare"`; msgs_match($out, $gzip . $compareheader.$zcompare, 'Gzipped CGI template'); } # CGI with charset and compression { my $header = CGI->new(q{})->header(-Content_Type => 'text/html; charset=UTF-8'); my $out = `$basecmd -DHTTP_ACCEPT_ENCODING=gzip charset "$compare"`; msgs_match($out, $gzip . $header . $zcompare, 'Gzipped CGI template with charset'); } # CGI with arguments { my $reason = 'x-non-gzip-reason: incompatible content-type foo/bar' . $eol; my $header = CGI->new(q{})->header(-Type => 'foo/bar'); my $out = `$basecmd -DHTTP_ACCEPT_ENCODING=gzip type "$compare"`; msgs_match($out, $reason . $header.$compare, 'Un-Gzipped with -Type flag'); } # CGI redirection and compression { my $reason = 'x-non-gzip-reason: HTTP status not 200' . $eol; my $expected_header = CGI->new(q{})->redirect($redir); my $out = `$basecmd -DHTTP_ACCEPT_ENCODING=gzip redirect "$redir"`; msgs_match($out, $reason . $expected_header, 'CGI redirect'); } # unbuffered CGI { my $reason = 'x-non-gzip-reason: user agent does not want gzip' . $eol; my $out = `$basecmd simple "$compare"`; msgs_match($out, $reason . $compareheader.$compare, 'unbuffered CGI'); } # Simulated mod_perl { my $out = `$basecmd -DHTTP_ACCEPT_ENCODING=gzip mod_perl "$compare"`; msgs_match($out, $gzip . $compareheader . $zcompare, 'mod_perl simulation'); } # Double print header { my $out = `$basecmd -DHTTP_ACCEPT_ENCODING=gzip doublehead "$compare"`; msgs_match($out, $gzip . $compareheader . $zcompare, 'double header'); } # redirected filehandle { my $out = `$basecmd -DHTTP_ACCEPT_ENCODING=gzip fh1 "$compare"`; msgs_match($out, $gzip . $compareheader . $zcompare, 'filehandle, fh=STDOUT plus select'); } # redirected filehandle { local $TODO = 'Explicit use of filehandles not yet supported'; my $out = `$basecmd -DHTTP_ACCEPT_ENCODING=gzip fh2 "$compare"`; msgs_match($out, $gzip . $compareheader . $zcompare, 'filehandle, explict STDOUT'); } # redirected filehandle { local $TODO = 'Explicit use of filehandles not yet supported'; my $out = `$basecmd -DHTTP_ACCEPT_ENCODING=gzip fh3 "$compare"`; msgs_match($out, $gzip . $compareheader . $zcompare, 'filehandle, explicit fh'); } sub msgs_match { my ($got, $expected, $message) = @_; ## no critic (RegularExpressions::RequireLineBoundaryMatching) my ($got_head, $got_body) = split m/\015\012\015\012/xs, $got, 2; my ($exp_head, $exp_body) = split m/\015\012\015\012/xs, $expected, 2; my %exp = map {lc($_) => 1} split m/\015\012/xs, $exp_head; for my $got_head_line (split m/\015\012/xs, $got_head) { if (!delete $exp{lc $got_head_line}) { return is($got, $expected, $message . ' -- extra header: ' . $got_head_line); # fail } } if (scalar keys %exp) { return is($got, $expected, $message . ' -- missing header: ' . [keys %exp]->[0]); # fail } if ($got_body ne $exp_body) { return is($got, $expected, $message . ' -- bodies do not match'); # fail } return pass($message); } CGI-Compress-Gzip-1.03/t/pod-coverage.t0000444000076500007650000000062311076263272016563 0ustar chrischris#!perl use warnings; use strict; use Test::More; if ((!$ENV{AUTHOR_TEST} && !$ENV{AUTHOR_TEST_CDOLAN}) || $ENV{AUTOMATED_TESTING}) { plan skip_all => 'Author test'; } eval 'use Pod::Coverage 0.17 ()'; plan skip_all => 'Optional Pod::Coverage 0.17 not found' if $@; eval 'use Test::Pod::Coverage 1.04'; plan skip_all => 'Optional Test::Pod::Coverage 1.04 not found' if $@; all_pod_coverage_ok(); CGI-Compress-Gzip-1.03/t/pod.t0000444000076500007650000000043211076263272014770 0ustar chrischris#!perl use warnings; use strict; use Test::More; if ((!$ENV{AUTHOR_TEST} && !$ENV{AUTHOR_TEST_CDOLAN}) || $ENV{AUTOMATED_TESTING}) { plan skip_all => 'Author test'; } eval 'use Test::Pod 1.14'; plan skip_all => 'Optional Test::Pod 1.14 not found' if $@; all_pod_files_ok(); CGI-Compress-Gzip-1.03/t/testhelp0000555000076500007650000000316511076263272015605 0ustar chrischris#!perl -w ## no critic (RequireVersionVar) # This is a simple script that is used by t/gzip.t use 5.006; use warnings; use strict; use English qw(-no_match_vars); use CGI::Compress::Gzip; $CGI::Compress::Gzip::global_give_reason = 1; $OUTPUT_AUTOFLUSH = 0; # aka $| # Pull out "-DVAR=val" arguments while (@ARGV && $ARGV[0] =~ m/\A -D([^=]+)=(.+) \z/xms) { $ENV{$1} = $2; shift @ARGV; } my ($op, @msg) = @ARGV; $op ||= q{}; my $cgi = CGI::Compress::Gzip->new(q{}); if ($op eq 'redirect') { ## no critic (ProhibitCascadingIfElse) print $cgi->redirect(@msg); } elsif ($op eq 'charset') { print $cgi->header('text/html; charset=UTF-8'); print @msg; } elsif ($op eq 'type') { print $cgi->header(-Type => 'foo/bar'); print @msg; } elsif ($op eq 'mod_perl') { # Deliberately initialize this AFTER creating the $cgi instance $ENV{MOD_PERL} = 1; print $cgi->header(); print @msg; } elsif ($op eq 'empty') { print $cgi->header(); } elsif ($op eq 'doublehead') { $CGI::HEADERS_ONCE = 1; print $cgi->header(); print $cgi->header(); print @msg; } elsif ($op eq 'unbuffer') { $OUTPUT_AUTOFLUSH = 1; # aka $| print $cgi->header(); print @msg; } elsif ($op eq 'fh1') { my $fh = \*STDOUT; $cgi->useFileHandle($fh); print $cgi->header('text/html'); print @msg; } elsif ($op eq 'fh2') { $cgi->useFileHandle(\*STDOUT); print {*STDOUT} $cgi->header('text/html'); print {*STDOUT} @msg; } elsif ($op eq 'fh3') { my $fh = \*STDOUT; $cgi->useFileHandle($fh); print {$fh} $cgi->header('text/html'); print {$fh} @msg; } else { print $cgi->header(); print @msg; }