protocol-http 0.66.0 → 0.67.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 92d72520175c17717320ecbee394b356a159c22d825f2b4e9c5cb5ef93aa2fed
4
- data.tar.gz: d0fb84ab33b22734ef0aa72a0d1c5f3af36011106154e415aa1d9aed8f84c570
3
+ metadata.gz: d934169186f9f7d3d55577ad02463aca42c88ba3e00b3273e3b6727f8c1b0e06
4
+ data.tar.gz: b8a13b51e95c6cda67717ad822cdc4b46828feefe6802374de6446c6d1a34f9d
5
5
  SHA512:
6
- metadata.gz: d4ef6e02a8dc285cf609e5c5be4b3d0d214a4fa6a13e2a270ffbe49935e4253473cfe189d854bdd7c886fee802f5bba08710afe831ef601e0ea30051292b1987
7
- data.tar.gz: 3986994c21ef7bafc0606f3b07f21678bddff510389bb4edd1111d79e10e702638002f1b5577d20cfac1a1c1a67144fc2ab0808d71ece4866dd3065b847d7a0d
6
+ metadata.gz: d3bc1abcbef44eef390a2de0d4b17d8ebbec0a20180022f43d7e9aaeff0cf153027fec6d7e6b7968fd546dd9b8a321139f8966008acc2f7502f751af34bc5f1a
7
+ data.tar.gz: 733a70b325bcb1db25f9d21b2fd93be191d9cedf1a521bb386f88e028b47411c692a51f38d25f5381f840a143a9296e1e7a04d444bbf3089296b1e7bd02f9e41
checksums.yaml.gz.sig CHANGED
Binary file
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require_relative "../error"
7
+
8
+ module Protocol
9
+ module HTTP
10
+ module Header
11
+ # Represents a `range` request header.
12
+ class Range
13
+ ParseError = Class.new(Error)
14
+
15
+ TOKEN = /[!#$%&'*+\-.0-9A-Z^_`a-z|~]+/
16
+ HEADER = /\A(?<unit>#{TOKEN})=(?<ranges>.*)\z/
17
+ BYTE_RANGE = /\A(?:(?<first>\d+)-(?<last>\d*)|-(?<suffix>\d+))\z/
18
+ OTHER_RANGE = /\A[\x21-\x2B\x2D-\x7E]+\z/
19
+ SEPARATOR = /\s*,\s*/
20
+
21
+ # Represents one byte-range-spec or suffix-byte-range-spec.
22
+ ByteRange = Struct.new(:first, :last) do
23
+ # Parse one byte range.
24
+ # @parameter value [String] The byte range to parse.
25
+ # @returns [ByteRange] The parsed byte range.
26
+ def self.parse(value)
27
+ unless match = BYTE_RANGE.match(value)
28
+ raise ParseError, "Invalid byte range: #{value.inspect}"
29
+ end
30
+
31
+ if suffix = match[:suffix]
32
+ return self.new(nil, Integer(suffix))
33
+ else
34
+ first = Integer(match[:first])
35
+ last = match[:last]
36
+ last = last.empty? ? nil : Integer(last)
37
+
38
+ if last && last < first
39
+ raise ParseError, "Invalid byte range: #{value.inspect}"
40
+ end
41
+
42
+ return self.new(first, last)
43
+ end
44
+ end
45
+
46
+ # Resolve this byte range against the selected representation size.
47
+ # @parameter size [Integer] The size of the selected representation.
48
+ # @returns [::Range | Nil] The resolved range, or `nil` when it is unsatisfiable.
49
+ def resolve(size)
50
+ if first
51
+ if first < size
52
+ return first..[last || size - 1, size - 1].min
53
+ end
54
+ elsif last > 0 && size > 0
55
+ return [0, size - last].max..size - 1
56
+ end
57
+
58
+ return nil
59
+ end
60
+
61
+ # Convert this byte range to its wire representation.
62
+ # @returns [String] The serialized byte range.
63
+ def to_s
64
+ if first
65
+ "#{first}-#{last}"
66
+ else
67
+ "-#{last}"
68
+ end
69
+ end
70
+ end
71
+
72
+ # Parse a raw range header value.
73
+ # @parameter value [String] The raw header value.
74
+ # @returns [Range] The parsed range header.
75
+ def self.parse(value)
76
+ unless match = HEADER.match(value)
77
+ raise ParseError, "Invalid range header: #{value.inspect}"
78
+ end
79
+
80
+ unit = match[:unit].downcase
81
+ ranges = match[:ranges].split(SEPARATOR, -1)
82
+
83
+ if ranges.empty? || ranges.any?(&:empty?)
84
+ raise ParseError, "Invalid range set: #{match[:ranges].inspect}"
85
+ end
86
+
87
+ if unit == "bytes"
88
+ ranges.map!{|range| ByteRange.parse(range)}
89
+ elsif ranges.any?{|range| !OTHER_RANGE.match?(range)}
90
+ raise ParseError, "Invalid range set: #{match[:ranges].inspect}"
91
+ end
92
+
93
+ return self.new(unit, ranges)
94
+ end
95
+
96
+ # Coerce a value into a range header.
97
+ # @parameter value [Object] The value to coerce.
98
+ # @returns [Range] The parsed range header.
99
+ def self.coerce(value)
100
+ self.parse(value.to_s)
101
+ end
102
+
103
+ # Initialize a range header.
104
+ # @parameter unit [String] The range unit.
105
+ # @parameter ranges [Array] The range specifiers.
106
+ def initialize(unit, ranges)
107
+ @unit = unit
108
+ @ranges = ranges
109
+ end
110
+
111
+ # @attribute [String] The range unit.
112
+ attr :unit
113
+
114
+ # @attribute [Array] The range specifiers.
115
+ attr :ranges
116
+
117
+ # Whether this header contains byte ranges.
118
+ # @returns [Boolean] Whether the range unit is `bytes`.
119
+ def bytes?
120
+ @unit == "bytes"
121
+ end
122
+
123
+ # Resolve all byte ranges against the selected representation size.
124
+ # @parameter size [Integer] The size of the selected representation.
125
+ # @returns [Array(::Range)] The satisfiable byte ranges.
126
+ def resolve(size)
127
+ unless bytes?
128
+ raise ArgumentError, "Cannot resolve #{@unit.inspect} ranges as byte ranges!"
129
+ end
130
+
131
+ size = Integer(size)
132
+ raise ArgumentError, "Size must not be negative!" if size < 0
133
+
134
+ @ranges.filter_map{|range| range.resolve(size)}
135
+ end
136
+
137
+ # Combine another raw range header value with this one.
138
+ # @parameter value [String] The raw range header value.
139
+ def <<(value)
140
+ other = self.class.parse(value)
141
+
142
+ unless other.unit == @unit
143
+ raise ParseError, "Cannot combine range units: #{@unit.inspect} and #{other.unit.inspect}"
144
+ end
145
+
146
+ @ranges.concat(other.ranges)
147
+
148
+ return self
149
+ end
150
+
151
+ # Convert this header to its wire representation.
152
+ # @returns [String] The serialized range header.
153
+ def to_s
154
+ "#{@unit}=#{@ranges.join(",")}"
155
+ end
156
+
157
+ # Whether this header is acceptable in HTTP trailers.
158
+ # @returns [Boolean] `false`, as range headers apply to a selected representation.
159
+ def self.trailer?
160
+ false
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
@@ -18,6 +18,7 @@ require_relative "header/vary"
18
18
  require_relative "header/authorization"
19
19
  require_relative "header/date"
20
20
  require_relative "header/priority"
21
+ require_relative "header/range"
21
22
  require_relative "header/trailer"
22
23
  require_relative "header/server_timing"
23
24
  require_relative "header/digest"
@@ -347,7 +348,7 @@ module Protocol
347
348
  "host" => false,
348
349
  "location" => false,
349
350
  "max-forwards" => false,
350
- "range" => false,
351
+ "range" => Header::Range,
351
352
  "referer" => false,
352
353
  "retry-after" => false,
353
354
  "server" => false,
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Protocol
7
7
  module HTTP
8
- VERSION = "0.66.0"
8
+ VERSION = "0.67.0"
9
9
  end
10
10
  end
data/readme.md CHANGED
@@ -30,6 +30,10 @@ Please see the [project documentation](https://socketry.github.io/protocol-http/
30
30
 
31
31
  Please see the [project releases](https://socketry.github.io/protocol-http/releases/index) for all releases.
32
32
 
33
+ ### v0.67.0
34
+
35
+ - Parse and resolve HTTP `Range` header values according to the default headers policy.
36
+
33
37
  ### v0.66.0
34
38
 
35
39
  - Introduce `Protocol::HTTP::RemoteError` for remote endpoint failures where application processing may have occurred.
@@ -68,12 +72,6 @@ Please see the [project releases](https://socketry.github.io/protocol-http/relea
68
72
 
69
73
  - `Protocol::HTTP::DuplicateHeaderError` now includes the existing and new values for better debugging.
70
74
 
71
- ### v0.58.0
72
-
73
- - Move trailer validation to `Headers#add` method to ensure all additions are checked at the time of addition as this is a hard requirement.
74
- - Introduce `Headers#header` method to enumerate only the main headers, excluding trailers. This can be used after invoking `Headers#trailer!` to avoid race conditions.
75
- - Fix `Headers#to_h` so that indexed headers are not left in an inconsistent state if errors occur during processing.
76
-
77
75
  ## See Also
78
76
 
79
77
  - [protocol-http1](https://github.com/socketry/protocol-http1) — HTTP/1 client/server implementation using this
data/releases.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Releases
2
2
 
3
+ ## v0.67.0
4
+
5
+ - Parse and resolve HTTP `Range` header values according to the default headers policy.
6
+
3
7
  ## v0.66.0
4
8
 
5
9
  - Introduce `Protocol::HTTP::RemoteError` for remote endpoint failures where application processing may have occurred.
data.tar.gz.sig CHANGED
@@ -1 +1 @@
1
- /��y:
1
+ �h��H���R��KS�2�v���~��V��ʜ��N�J>�j� !���h2��11���ax ��'��,O�y�R�M89�&[�A�7��l�N]�/븂,�z^"F�Q�,���]����]���!�^���*��3�߽�>����[�c���y;0X���Ǚ��s^\���,4�� ު
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: protocol-http
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.66.0
4
+ version: 0.67.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -97,6 +97,7 @@ files:
97
97
  - lib/protocol/http/header/generic.rb
98
98
  - lib/protocol/http/header/multiple.rb
99
99
  - lib/protocol/http/header/priority.rb
100
+ - lib/protocol/http/header/range.rb
100
101
  - lib/protocol/http/header/server_timing.rb
101
102
  - lib/protocol/http/header/set_cookie.rb
102
103
  - lib/protocol/http/header/split.rb
metadata.gz.sig CHANGED
Binary file