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.002001';
3 37     37   239791 use strict;
  37         88  
  37         1272  
4 37     37   157 use warnings;
  37         50  
  37         1728  
5 37     37   137 use Carp qw(croak);
  37         50  
  37         3144  
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 37     37   211 use overload '@{}' => sub { $_[0]->to_pairs }, fallback => 1;
  37     9   73  
  37         329  
  9         65  
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 948     948   1265 sub _fold { my $k = $_[0]; $k =~ tr/A-Z/a-z/; return $k }
  948         1490  
  948         2291  
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 250     250 0 318402 my ($class, $pairs) = @_;
66 250         311 my @p;
67 250 100       589 if (defined $pairs) {
68 62 50       175 croak("PAGI::Headers->new expects an arrayref of [name, value] pairs")
69             unless ref($pairs) eq 'ARRAY';
70 62         121 @p = map { [ $_->[0], $_->[1] ] } @$pairs;
  102         300  
71             }
72 250         2209 return bless { pairs => \@p }, $class;
73             }
74              
75 5     5 0 19 sub clone { return PAGI::Headers->new($_[0]->{pairs}) }
76              
77             # --- reads (case-insensitive) ---
78              
79             sub get {
80 160     160 0 1003 my ($self, $name) = @_;
81 160 100       377 croak("header name required") unless defined $name;
82 159         256 my $key = _fold($name);
83 159         203 my $val;
84 159 100       173 for my $p (@{$self->{pairs}}) { $val = $p->[1] if _fold($p->[0]) eq $key }
  159         299  
  239         357  
85 159         508 return $val;
86             }
87              
88             sub get_all {
89 25     25 0 66 my ($self, $name) = @_;
90 25 50       72 croak("header name required") unless defined $name;
91 25         103 my $key = _fold($name);
92 25         41 return map { $_->[1] } grep { _fold($_->[0]) eq $key } @{$self->{pairs}};
  39         171  
  52         106  
  25         77  
93             }
94              
95             sub has {
96 197     197 0 336 my ($self, $name) = @_;
97 197 50 33     640 return 0 unless defined $name && length $name;
98 197         442 my $key = _fold($name);
99 197 100       239 for my $p (@{$self->{pairs}}) { return 1 if _fold($p->[0]) eq $key }
  197         587  
  159         278  
100 97         316 return 0;
101             }
102              
103             sub names {
104 9     9 0 596 my ($self) = @_;
105 9         19 my (%seen, @names);
106 9         16 for my $p (@{$self->{pairs}}) {
  9         34  
107 18 100       46 push @names, $p->[0] unless $seen{ _fold($p->[0]) }++;
108             }
109 9         49 return @names;
110             }
111              
112 4     4 0 13 sub count { scalar @{ $_[0]->{pairs} } }
  4         24  
113 3 100   3 0 16 sub is_empty { @{ $_[0]->{pairs} } ? 0 : 1 }
  3         60  
114              
115             # --- writes (return $self) ---
116              
117             sub set {
118 46     46 0 507 my ($self, $name, @values) = @_;
119 46 50       96 croak("header name required") unless defined $name;
120 46 100       71 croak("header value must be defined") if grep { !defined } @values;
  46         215  
121 45         88 my $key = _fold($name);
122 45         94 @{$self->{pairs}} = grep { _fold($_->[0]) ne $key } @{$self->{pairs}};
  45         93  
  19         40  
  45         137  
123 45         107 push @{$self->{pairs}}, [ $name, $_ ] for @values;
  45         136  
124 45         157 return $self;
125             }
126              
127             sub add {
128 162     162 0 349 my ($self, $name, @values) = @_;
129 162 50       308 croak("header name required") unless defined $name;
130 162 100       250 croak("header value must be defined") if grep { !defined } @values;
  162         735  
131 160         258 push @{$self->{pairs}}, [ $name, $_ ] for @values;
  160         517  
132 160         338 return $self;
133             }
134              
135             sub set_default {
136 96     96 0 626 my ($self, $name, $value) = @_;
137 96 100       215 return $self if $self->has($name);
138 84         184 return $self->add($name, $value);
139             }
140              
141             sub remove {
142 5     5 0 24 my ($self, $name) = @_;
143 5 50       13 croak("header name required") unless defined $name;
144 5         12 my $key = _fold($name);
145 5         10 my @removed = map { $_->[1] } grep { _fold($_->[0]) eq $key } @{$self->{pairs}};
  5         15  
  9         18  
  5         16  
146 5         9 @{$self->{pairs}} = grep { _fold($_->[0]) ne $key } @{$self->{pairs}};
  5         15  
  9         18  
  5         11  
147 5         20 return @removed;
148             }
149              
150 2     2 0 5 sub clear { @{ $_[0]->{pairs} } = (); return $_[0] }
  2         9  
  2         4  
151              
152             sub remove_content_headers {
153 1     1 0 10 my ($self) = @_;
154 1         3 my @removed = grep { _fold($_->[0]) =~ /^content-/ } @{$self->{pairs}};
  3         8  
  1         4  
155 1         3 @{$self->{pairs}} = grep { _fold($_->[0]) !~ /^content-/ } @{$self->{pairs}};
  1         4  
  3         6  
  1         3  
156 1         4 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 10 my ($self) = @_;
163 1         10 my %drop = %HOP;
164 1         7 for my $conn ($self->get_all('connection')) {
165 1         6 for my $tok (split /,/, $conn) {
166 2         10 $tok =~ s/\A\s+//; $tok =~ s/\s+\z//;
  2         6  
167 2 50       9 $drop{ _fold($tok) } = 1 if length $tok;
168             }
169             }
170 1         2 @{$self->{pairs}} = grep { !$drop{ _fold($_->[0]) } } @{$self->{pairs}};
  1         5  
  4         9  
  1         5  
171 1         6 return $self;
172             }
173              
174             # --- output ---
175              
176 151     151 0 555 sub to_pairs { return [ map { [ $_->[0], $_->[1] ] } @{ $_[0]->{pairs} } ] }
  184         576  
  151         423  
177 2     2 0 6 sub flatten { return map { @$_ } @{ $_[0]->{pairs} } }
  4         20  
  2         8  
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 26 my ($self, $multi) = @_;
185 5 100       21 return { map { $_ => [ $self->get_all($_) ] } $self->names } if $multi;
  3         16  
186 3         12 return { map { $_ => $self->get($_) } $self->names };
  3         9  
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 9 sub to_string { return join('', map { "$_->[0]: $_->[1]\r\n" } @{ $_[0]->{pairs} }) }
  5         68  
  3         13  
193              
194             1;