File Coverage

blib/lib/PAGI/Headers.pm
Criterion Covered Total %
statement 128 128 100.0
branch 27 34 79.4
condition 1 3 33.3
subroutine 25 25 100.0
pod 1 19 5.2
total 182 209 87.0


line stmt bran cond sub pod time code
1             package PAGI::Headers;
2             $PAGI::Headers::VERSION = '0.002000';
3 36     36   142289 use strict;
  36         68  
  36         1162  
4 36     36   144 use warnings;
  36         147  
  36         1510  
5 36     36   171 use Carp qw(croak);
  36         41  
  36         2881  
6              
7             # Iterating @{$headers} yields the [name,value] pairs (the PAGI wire form), so
8             # `@{$res->headers}` callers keep working. READ-ONLY: it returns a COPY, so
9             # pushing onto it does not mutate the container -- use add(). Emission must use
10             # to_pairs, never this overload.
11 36     36   204 use overload '@{}' => sub { $_[0]->to_pairs }, fallback => 1;
  36     9   106  
  36         333  
  9         37  
12              
13             =head1 NAME
14              
15             PAGI::Headers - ordered, case-insensitive, multi-value HTTP header container
16              
17             =head1 DESCRIPTION
18              
19             Holds HTTP headers as an ordered list of C<[name, value]> byte pairs -- the PAGI
20             wire form. Lookup is case-insensitive (ASCII fold; field names are ASCII tokens);
21             original casing is preserved on output. Insertion order is preserved (never
22             sorted). Multiple values per name are first-class (e.g. C).
23              
24             Lookups scan the ordered list -- header sets are small, so this is deliberately
25             indexless.
26              
27             This container is B a hash and does not overload hash dereference; iterate
28             names with C and read values with C/C, or take an explicit
29             plain-hash snapshot with C.
30              
31             =head1 METHODS
32              
33             =head2 to_hash
34              
35             my $flat = $headers->to_hash; # { Name => last-value }
36             my $multi = $headers->to_hash(1); # { Name => [ all values ] }
37              
38             Returns a plain hashref snapshot keyed by distinct header name (grouped
39             case-insensitively, using the casing and order C reports). The flat form
40             mirrors C -- one value per name, last wins. Passing a true argument returns
41             the multi-value form, mirroring C -- an arrayref of every value for each
42             name. Values are B comma-joined (unlike L/L).
43              
44             Header values are opaque bytes and pass through untouched -- including C,
45             C, and C. This container does B validate or sanitize them; rejecting
46             injection bytes on the wire is the server's job, which it B do when emitting
47             a response (see L). A value must,
48             however, be B: C, C, and C C on an C
49             value rather than storing it, since an undefined header value is a caller bug, not
50             data.
51              
52             =cut
53              
54             # ASCII-only lowercase for name keying. Field names are ASCII tokens (RFC 7230);
55             # Perl's lc() is Unicode-aware and could mis-fold stray bytes.
56 943     943   1023 sub _fold { my $k = $_[0]; $k =~ tr/A-Z/a-z/; return $k }
  943         1232  
  943         1823  
57              
58             # Hop-by-hop headers (RFC 7230 ยง6.1) -- not safe to forward through a proxy.
59             my %HOP = map { $_ => 1 } qw(
60             connection keep-alive proxy-authenticate proxy-authorization
61             te trailer transfer-encoding upgrade
62             );
63              
64             sub new {
65 248     248 0 182739 my ($class, $pairs) = @_;
66 248         313 my @p;
67 248 100       486 if (defined $pairs) {
68 62 50       145 croak("PAGI::Headers->new expects an arrayref of [name, value] pairs")
69             unless ref($pairs) eq 'ARRAY';
70 62         112 @p = map { [ $_->[0], $_->[1] ] } @$pairs;
  102         255  
71             }
72 248         2008 return bless { pairs => \@p }, $class;
73             }
74              
75 5     5 0 16 sub clone { return PAGI::Headers->new($_[0]->{pairs}) }
76              
77             # --- reads (case-insensitive) ---
78              
79             sub get {
80 159     159 0 709 my ($self, $name) = @_;
81 159 100       352 croak("header name required") unless defined $name;
82 158         247 my $key = _fold($name);
83 158         282 my $val;
84 158 100       165 for my $p (@{$self->{pairs}}) { $val = $p->[1] if _fold($p->[0]) eq $key }
  158         276  
  238         335  
85 158         451 return $val;
86             }
87              
88             sub get_all {
89 25     25 0 46 my ($self, $name) = @_;
90 25 50       48 croak("header name required") unless defined $name;
91 25         40 my $key = _fold($name);
92 25         33 return map { $_->[1] } grep { _fold($_->[0]) eq $key } @{$self->{pairs}};
  39         114  
  52         69  
  25         53  
93             }
94              
95             sub has {
96 195     195 0 264 my ($self, $name) = @_;
97 195 50 33     564 return 0 unless defined $name && length $name;
98 195         325 my $key = _fold($name);
99 195 100       217 for my $p (@{$self->{pairs}}) { return 1 if _fold($p->[0]) eq $key }
  195         717  
  158         219  
100 96         239 return 0;
101             }
102              
103             sub names {
104 9     9 0 428 my ($self) = @_;
105 9         12 my (%seen, @names);
106 9         13 for my $p (@{$self->{pairs}}) {
  9         32  
107 18 100       31 push @names, $p->[0] unless $seen{ _fold($p->[0]) }++;
108             }
109 9         34 return @names;
110             }
111              
112 4     4 0 7 sub count { scalar @{ $_[0]->{pairs} } }
  4         17  
113 3 100   3 0 11 sub is_empty { @{ $_[0]->{pairs} } ? 0 : 1 }
  3         56  
114              
115             # --- writes (return $self) ---
116              
117             sub set {
118 46     46 0 468 my ($self, $name, @values) = @_;
119 46 50       94 croak("header name required") unless defined $name;
120 46 100       66 croak("header value must be defined") if grep { !defined } @values;
  46         197  
121 45         79 my $key = _fold($name);
122 45         58 @{$self->{pairs}} = grep { _fold($_->[0]) ne $key } @{$self->{pairs}};
  45         68  
  19         30  
  45         134  
123 45         77 push @{$self->{pairs}}, [ $name, $_ ] for @values;
  45         132  
124 45         100 return $self;
125             }
126              
127             sub add {
128 160     160 0 340 my ($self, $name, @values) = @_;
129 160 50       253 croak("header name required") unless defined $name;
130 160 100       219 croak("header value must be defined") if grep { !defined } @values;
  160         647  
131 158         227 push @{$self->{pairs}}, [ $name, $_ ] for @values;
  158         495  
132 158         290 return $self;
133             }
134              
135             sub set_default {
136 95     95 0 506 my ($self, $name, $value) = @_;
137 95 100       208 return $self if $self->has($name);
138 83         187 return $self->add($name, $value);
139             }
140              
141             sub remove {
142 5     5 0 19 my ($self, $name) = @_;
143 5 50       25 croak("header name required") unless defined $name;
144 5         23 my $key = _fold($name);
145 5         20 my @removed = map { $_->[1] } grep { _fold($_->[0]) eq $key } @{$self->{pairs}};
  5         11  
  9         17  
  5         13  
146 5         8 @{$self->{pairs}} = grep { _fold($_->[0]) ne $key } @{$self->{pairs}};
  5         11  
  9         12  
  5         9  
147 5         16 return @removed;
148             }
149              
150 2     2 0 2 sub clear { @{ $_[0]->{pairs} } = (); return $_[0] }
  2         7  
  2         4  
151              
152             sub remove_content_headers {
153 1     1 0 6 my ($self) = @_;
154 1         2 my @removed = grep { _fold($_->[0]) =~ /^content-/ } @{$self->{pairs}};
  3         5  
  1         3  
155 1         1 @{$self->{pairs}} = grep { _fold($_->[0]) !~ /^content-/ } @{$self->{pairs}};
  1         2  
  3         4  
  1         2  
156 1         2 return PAGI::Headers->new(\@removed);
157             }
158              
159             # Strip hop-by-hop headers: the fixed RFC 7230 set PLUS any field NAMED by the
160             # Connection header (e.g. "Connection: X-Secret" makes X-Secret hop-by-hop).
161             sub dehop {
162 1     1 0 6 my ($self) = @_;
163 1         8 my %drop = %HOP;
164 1         4 for my $conn ($self->get_all('connection')) {
165 1         4 for my $tok (split /,/, $conn) {
166 2         6 $tok =~ s/\A\s+//; $tok =~ s/\s+\z//;
  2         3  
167 2 50       6 $drop{ _fold($tok) } = 1 if length $tok;
168             }
169             }
170 1         1 @{$self->{pairs}} = grep { !$drop{ _fold($_->[0]) } } @{$self->{pairs}};
  1         3  
  4         5  
  1         2  
171 1         3 return $self;
172             }
173              
174             # --- output ---
175              
176 150     150 0 492 sub to_pairs { return [ map { [ $_->[0], $_->[1] ] } @{ $_[0]->{pairs} } ] }
  182         568  
  150         366  
177 2     2 0 3 sub flatten { return map { @$_ } @{ $_[0]->{pairs} } }
  4         13  
  2         7  
178              
179             # Plain-hash snapshot, keyed by distinct name (case-insensitively grouped, in
180             # names() order/casing). Flat form mirrors get() -- one value per name, last
181             # wins; multi form (truthy arg) mirrors get_all() -- an arrayref of every value.
182             # This is the explicit "I want a hash" path; the container itself is NOT a hash.
183             sub to_hash {
184 5     5 1 17 my ($self, $multi) = @_;
185 5 100       16 return { map { $_ => [ $self->get_all($_) ] } $self->names } if $multi;
  3         7  
186 3         9 return { map { $_ => $self->get($_) } $self->names };
  3         7  
187             }
188              
189             # Debug/inspection only -- NOT a wire-emission helper. It does not validate or
190             # strip CR/LF, so it is unsafe for untrusted header values; wire safety is the
191             # server's job (it validates http.response.start). The real output is to_pairs.
192 3     3 0 6 sub to_string { return join('', map { "$_->[0]: $_->[1]\r\n" } @{ $_[0]->{pairs} }) }
  5         43  
  3         13  
193              
194             1;