File Coverage

lib/Hypersonic/Compress.pm
Criterion Covered Total %
statement 20 31 64.5
branch 2 2 100.0
condition 9 27 33.3
subroutine 6 10 60.0
pod 0 7 0.0
total 37 77 48.0


line stmt bran cond sub pod time code
1             package Hypersonic::Compress;
2              
3 1     1   3640 use strict;
  1         9  
  1         86  
4 1     1   10 use warnings;
  1         3  
  1         103  
5 1     1   25 use 5.010;
  1         9  
6              
7             our $VERSION = '0.15';
8              
9             # JIT-compiled gzip compression for Hypersonic
10             # All compression happens in C via zlib for maximum performance
11              
12             # Configuration
13             my $COMPRESS_CONFIG;
14              
15             # Cache for zlib detection result
16             my $ZLIB_DETECTION;
17              
18             # Check if zlib is available and actually linkable (uses centralized detection)
19             sub check_zlib {
20 2 100   2 0 242282 return $ZLIB_DETECTION->{available} if defined $ZLIB_DETECTION;
21              
22 1         32 require Hypersonic::JIT::Util;
23 1         48 $ZLIB_DETECTION = Hypersonic::JIT::Util->detect_zlib();
24 1         35 return $ZLIB_DETECTION->{available};
25             }
26              
27             # Get zlib compiler flags (uses centralized detection).
28             #
29             # detect_zlib returns ldflags='' (defined empty string) when neither
30             # Alien, pkg-config, nor the manual paths probe found zlib *but* the
31             # caller still wants to attempt a link. Use `||` (string-falsy) rather
32             # than `//` (defined-or) so an empty detection still falls back to
33             # `-lz`. Without this, when pkg-config returned an unparseable result
34             # on some Debian configs, the link line ended up missing -lz entirely
35             # and the JIT-compiled .so failed at dlopen with `undefined symbol:
36             # deflate`.
37             sub get_zlib_flags {
38 0     0 0 0 require Hypersonic::JIT::Util;
39 0   0     0 $ZLIB_DETECTION //= Hypersonic::JIT::Util->detect_zlib();
40 0   0     0 return ($ZLIB_DETECTION->{cflags} || '', $ZLIB_DETECTION->{ldflags} || '-lz');
      0        
41             }
42              
43             # Configure compression
44             sub configure {
45 3     3 0 25418 my ($class, %opts) = @_;
46            
47             $COMPRESS_CONFIG = {
48             enabled => $opts{enabled} // 1,
49             min_size => $opts{min_size} // 1024, # Don't compress < 1KB
50             level => $opts{level} // 6, # Compression level 1-9
51             types => $opts{types} // [ # MIME types to compress
52 3   50     133 'text/html',
      100        
      100        
      50        
53             'text/css',
54             'text/plain',
55             'text/xml',
56             'text/javascript',
57             'application/json',
58             'application/javascript',
59             'application/xml',
60             'application/xhtml+xml',
61             'image/svg+xml',
62             ],
63             };
64            
65 3         15 return $COMPRESS_CONFIG;
66             }
67              
68             # Get config
69 0     0 0 0 sub config { $COMPRESS_CONFIG }
70              
71             # Generate C code for JIT inclusion
72             # Returns the C function and required includes
73             sub generate_c_code {
74 1     1 0 5 my ($class, $level) = @_;
75 1   50     10 $level //= $COMPRESS_CONFIG->{level} // 6;
      33        
76 1   50     3 my $min_size = $COMPRESS_CONFIG->{min_size} // 1024;
77            
78 1         6 my $c_code = <<"END_C";
79             #include
80             #include
81             #include
82              
83             /* Thread-local compression buffer */
84             static __thread unsigned char gzip_out_buf[131072]; /* 128KB max compressed */
85             static __thread z_stream zstrm;
86             static __thread int zstrm_initialized = 0;
87              
88             /* Check if client accepts gzip */
89             static int accepts_gzip(const char* accept_encoding, size_t len) {
90             if (!accept_encoding || len == 0) return 0;
91             /* Simple check for "gzip" substring */
92             const char* p = accept_encoding;
93             const char* end = accept_encoding + len;
94             while (p < end - 3) {
95             if (p[0] == 'g' && p[1] == 'z' && p[2] == 'i' && p[3] == 'p') {
96             return 1;
97             }
98             p++;
99             }
100             return 0;
101             }
102              
103             /* Gzip compress data - returns compressed length or 0 on failure */
104             static size_t gzip_compress(const char* input, size_t input_len,
105             unsigned char** output, int level) {
106             /* Don't compress small responses */
107             if (input_len < $min_size) return 0;
108            
109             /* Max output size */
110             size_t max_out = compressBound(input_len) + 18; /* gzip header/footer */
111             if (max_out > sizeof(gzip_out_buf)) return 0;
112            
113             /* Initialize deflate with gzip wrapper */
114             z_stream strm;
115             memset(&strm, 0, sizeof(strm));
116            
117             /* 15 + 16 = gzip format */
118             if (deflateInit2(&strm, level, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY) != Z_OK) {
119             return 0;
120             }
121            
122             strm.next_in = (Bytef*)input;
123             strm.avail_in = input_len;
124             strm.next_out = gzip_out_buf;
125             strm.avail_out = sizeof(gzip_out_buf);
126            
127             int ret = deflate(&strm, Z_FINISH);
128             size_t compressed_len = strm.total_out;
129            
130             deflateEnd(&strm);
131            
132             if (ret != Z_STREAM_END) return 0;
133            
134             /* Only use compression if it actually saves space */
135             if (compressed_len >= input_len) return 0;
136            
137             *output = gzip_out_buf;
138             return compressed_len;
139             }
140             END_C
141              
142 1         2 return $c_code;
143             }
144              
145             # Generate the response building code with compression
146             sub generate_response_code {
147 0     0 0   my ($class, $builder, $level) = @_;
148 0   0       $level //= $COMPRESS_CONFIG->{level} // 6;
      0        
149            
150             # This generates the compression block that goes after body is determined
151             # but before headers are written
152            
153 0           $builder
154             ->blank
155             ->comment('Check for gzip compression support')
156             ->line('int use_gzip = 0;')
157             ->line('unsigned char* compressed_body = NULL;')
158             ->line('size_t compressed_len = 0;')
159             ->line('const char* accept_enc = NULL;')
160             ->line('size_t accept_enc_len = 0;')
161             ->blank
162             ->comment('Get Accept-Encoding from request headers')
163             ->line('if (req_headers) {')
164             ->line(' SV** ae = hv_fetch(req_headers, "accept_encoding", 15, 0);')
165             ->line(' if (ae && SvOK(*ae)) {')
166             ->line(' accept_enc = SvPV(*ae, accept_enc_len);')
167             ->line(' }')
168             ->line('}')
169             ->blank
170             ->comment('Compress if client accepts gzip and response is large enough')
171             ->line("if (accepts_gzip(accept_enc, accept_enc_len) && len >= $COMPRESS_CONFIG->{min_size}) {")
172             ->line(" compressed_len = gzip_compress(body_str, len, &compressed_body, $level);")
173             ->line(' if (compressed_len > 0) {')
174             ->line(' use_gzip = 1;')
175             ->line(' }')
176             ->line('}');
177            
178 0           return $builder;
179             }
180              
181             # Modify header building to include Content-Encoding
182             sub generate_header_code {
183 0     0 0   my ($class, $builder) = @_;
184            
185             # This adds Content-Encoding: gzip header when compression is used
186 0           $builder
187             ->line('if (use_gzip) {')
188             ->line(' hdr_len += snprintf(resp_buf + hdr_len, sizeof(resp_buf) - hdr_len,')
189             ->line(' "Content-Encoding: gzip\\r\\n");')
190             ->line('}');
191            
192 0           return $builder;
193             }
194              
195             1;
196              
197             __END__