diff --git a/FU.pm b/FU.pm index 999dda2..40dd19e 100644 --- a/FU.pm +++ b/FU.pm @@ -400,7 +400,7 @@ sub _do_req($c) { ($REQ->{trace_sqlexec}||0)*1000, ($REQ->{trace_sqlprep}||0)*1000, $REQ->{trace_nsqldirect}||0, $REQ->{trace_nsqlprep}||0, $REQ->{trace_nsql} : '', $REQ->{status}, ($REQ->{reshdr}{'content-type'}//'-') =~ s/;.+$//r, - length($REQ->{resbody}), substr($REQ->{reshdr}{'content-encoding'}//'bytes', 0, 1) + length($REQ->{resbody}), substr($REQ->{reshdr}{'content-encoding'}//'r', 0, 1) ) if FU::debug || $proc_ms > (FU::log_slow_reqs||1e10); } @@ -850,6 +850,8 @@ sub _error_page($, $code, $title, $msg) { } sub _finalize { + state $hasgzip = FU::Util::gzip_lib(); + state $hasbrotli = eval { FU::Util::brotli_compress(6, ''); 1 }; my $r = $FU::REQ; fu->add_header('set-cookie', $_) for $r->{rescookie} ? sort values $r->{rescookie}->%* : (); @@ -861,13 +863,19 @@ sub _finalize { $r->{resbody} = ''; } else { - if (FU::Util::gzip_lib() && length($r->{resbody}) > 256 - && !defined $r->{reshdr}{'content-encoding'} && FU::compress_mimes->{$r->{reshdr}{'content-type'}}) { + if (($hasgzip || $hasbrotli) && length($r->{resbody}) > 256 + && !defined $r->{reshdr}{'content-encoding'} + && FU::compress_mimes->{$r->{reshdr}{'content-type'}} + ) { $r->{reshdr}{'vary'} = ($r->{reshdr}{'vary'} ? $r->{reshdr}{'vary'}.', ' : '').'accept-encoding' if ($r->{reshdr}{'vary'}||'') !~ /accept-encoding/i; - if (($r->{hdr}{'accept-encoding'}||'') =~ /gzip/) { + if ($hasbrotli && ($r->{hdr}{'accept-encoding'}||'') =~ /\bbr\b/) { + $r->{resbody} = FU::Util::brotli_compress(6, $r->{resbody}); + $r->{reshdr}{'content-encoding'} = 'br'; + + } elsif ($hasgzip && ($r->{hdr}{'accept-encoding'}||'') =~ /\bgzip\b/) { $r->{resbody} = FU::Util::gzip_compress(6, $r->{resbody}); $r->{reshdr}{'content-encoding'} = 'gzip'; } @@ -991,6 +999,9 @@ C. =item * C or C or C - required for C in L and used for HTTP output compression. +=item * C - required for C in L +and used for HTTP output compression. + =back diff --git a/FU.xs b/FU.xs index b775f3c..3e7a0b2 100644 --- a/FU.xs +++ b/FU.xs @@ -126,6 +126,10 @@ void gzip_compress(IV level, SV *in) CODE: ST(0) = fugz_compress(aTHX_ level, in); +void brotli_compress(IV level, SV *in) + CODE: + ST(0) = fubr_compress(aTHX_ level, in); + void fdpass_send(int socket, int fd, SV *data) CODE: STRLEN buflen; diff --git a/FU/Util.pm b/FU/Util.pm index 4ff1c2b..27a0b62 100644 --- a/FU/Util.pm +++ b/FU/Util.pm @@ -13,7 +13,7 @@ our @EXPORT_OK = qw/ utf8_decode uri_escape uri_unescape query_decode query_encode httpdate_format httpdate_parse - gzip_lib gzip_compress + gzip_lib gzip_compress brotli_compress fdpass_send fdpass_recv /; @@ -401,6 +401,8 @@ I the level is capped at 9. 6 is typically used as a default. Throws an error if no suitable library was found. +This function is B safe to use from multiple threads! + =back This module does not currently implement decompression. If you need that, or @@ -409,6 +411,22 @@ L and L in the core Perl distribution and L on CPAN. +=head2 Brotli Compression + +Just a small wrapper around C's one-shot compression +interface. + +=over + +=item brotli_compress($level, $data) + +Returns a byte string with the brotli-compressed version of C<$data> at the +given quality C<$level> (between 0 and 11). + +Throws an error if C could not be found or loaded. + +=back + =head2 File Descriptor Passing diff --git a/c/compress.c b/c/compress.c index 9c4a1a9..20a59be 100644 --- a/c/compress.c +++ b/c/compress.c @@ -124,5 +124,38 @@ static SV *fugz_compress(pTHX_ IV level, SV *in) { if (fugz_imp == 1) return fugz_compress_ld(aTHX_ level, bytes, inlen); else return fugz_compress_zlib(aTHX_ level, bytes, inlen); - return &PL_sv_undef; +} + + + +/* Brotli */ + +typedef enum { BROTLI_MODE_GENERIC = 0, BROTLI_MODE_TEXT = 1, BROTLI_MODE_FONT = 2 } BrotliEncoderMode; + +static size_t (*BrotliEncoderMaxCompressedSize)(size_t); +static int (*BrotliEncoderCompress)(int, int, BrotliEncoderMode, size_t, const char *, size_t *, char *); + +static SV *fubr_compress(pTHX_ IV level, SV *in) { + if (!BrotliEncoderCompress) { + void *handle; + if (!(handle = dlopen("libbrotlienc.so", RTLD_LAZY)) + || !(BrotliEncoderMaxCompressedSize = dlsym(handle, "BrotliEncoderMaxCompressedSize")) + || !(BrotliEncoderCompress = dlsym(handle, "BrotliEncoderCompress"))) + fu_confess("Unable to load libbrotlienc.so: %s", dlerror()); + } + if (level < 0 || level > 11) fu_confess("Invalid compression level: %"IVdf, level); + + STRLEN inlen; + const char *bytes = SvPVbyte(in, inlen); + + size_t outlen = BrotliEncoderMaxCompressedSize(inlen); + /* "Result is only valid if quality is at least 2", so let's use a (more conservative?) fallback */ + if (level < 2 && outlen < inlen + 256) outlen = inlen + 256; + + SV *out = sv_2mortal(newSV(outlen)); + SvPOK_only(out); + if (!BrotliEncoderCompress(level, 22, BROTLI_MODE_GENERIC, inlen, bytes, &outlen, SvPVX(out))) + fu_confess("Brotli compression failed"); + SvCUR_set(out, outlen); + return out; } diff --git a/t/compress.t b/t/compress.t index ef17c77..ae236ae 100644 --- a/t/compress.t +++ b/t/compress.t @@ -1,18 +1,34 @@ use v5.36; use Test::More; -use FU::Util qw/gzip_lib gzip_compress/; +use FU::Util qw/gzip_lib gzip_compress brotli_compress/; like gzip_lib, qr/^(|libdeflate|zlib-ng|zlib)$/, gzip_lib; -plan skip_all => 'No suitable gzip library found' if !gzip_lib; -plan skip_all => 'Compress::Zlib not found' if !eval { require Compress::Zlib }; +my $incompressible; -my $incompressible = Compress::Zlib::memGzip(join '', map chr(rand 256), 0..93123); +subtest 'gzip_compress', sub { + plan skip_all => 'No suitable gzip library found' if !gzip_lib; + plan skip_all => 'Compress::Zlib not found' if !eval { require Compress::Zlib }; -for my $str ('', 'Hello world!', 'x'x4096, $incompressible) { - is Compress::Zlib::memGunzip(gzip_compress(0, $str)), $str; - is Compress::Zlib::memGunzip(gzip_compress(12, $str)), $str; -} + $incompressible = Compress::Zlib::memGzip(join '', map chr(rand 256), 0..93123); + + for my $str ('', 'Hello world!', 'x'x4096, $incompressible) { + is Compress::Zlib::memGunzip(gzip_compress(0, $str)), $str; + is Compress::Zlib::memGunzip(gzip_compress(12, $str)), $str; + } +}; + + +subtest 'brotli_compress', sub { + plan skip_all => 'libbrotlienc not available' + if !eval { brotli_compress 6, '' } && $@ =~ /Unable to load/; + + ok length(brotli_compress 0, '') > 0; + ok length(brotli_compress 11, '') > 0; + # '0' does not disable compression... + ok length(brotli_compress 0, 'Hello world!'x100) < 200; + ok length(brotli_compress 11, 'Hello world!'x100) < 100; +}; done_testing; @@ -29,6 +45,8 @@ for (0..1000) { local $_ = gzip_lib; $_ = gzip_compress(0, $str); $_ = gzip_compress(12, $str); + $_ = brotli_compress(0, $str); + $_ = brotli_compress(11, $str); } } diag count_sv; @@ -44,4 +62,5 @@ my $data = <$F>; cmpthese -3, { memGzip => 'Compress::Zlib::memGzip($data)', gzip_compress => 'gzip_compress(6, $data)', + brotli_compress => 'brotli_compress(6, $data)', };