Skip to content

Commit e2f31ad

Browse files
authored
Merge pull request #211 from ianegordon/ian/chunked-decoder-tests
Add direct unit tests for HTTPChunkedTransferDecoder
2 parents 5295843 + 1d1f0a9 commit e2f31ad

1 file changed

Lines changed: 112 additions & 0 deletions

File tree

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
//
2+
// HTTPChunkedDecodedSequenceTests.swift
3+
// FlyingFox
4+
//
5+
// Created by Ian Gordon on 27/04/2026.
6+
// Copyright © 2026 Simon Whitty. All rights reserved.
7+
//
8+
// Distributed under the permissive MIT license
9+
// Get the latest version from here:
10+
//
11+
// https://github.com/swhitty/FlyingFox
12+
//
13+
// Permission is hereby granted, free of charge, to any person obtaining a copy
14+
// of this software and associated documentation files (the "Software"), to deal
15+
// in the Software without restriction, including without limitation the rights
16+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17+
// copies of the Software, and to permit persons to whom the Software is
18+
// furnished to do so, subject to the following conditions:
19+
//
20+
// The above copyright notice and this permission notice shall be included in all
21+
// copies or substantial portions of the Software.
22+
//
23+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29+
// SOFTWARE.
30+
//
31+
32+
@testable import FlyingFox
33+
import FlyingSocks
34+
import Foundation
35+
import Testing
36+
37+
struct HTTPChunkedDecodedSequenceTests {
38+
39+
// RFC 9112 §7.1 — once the trailer's terminating CRLF is consumed, the
40+
// decoder must stop. Over-consuming would eat the next pipelined request
41+
// on a keep-alive connection.
42+
@Test
43+
func decoder_DoesNotConsumeBeyondTerminator() async throws {
44+
let wire: [UInt8] = Array("5\r\nHello\r\n0\r\n\r\nNEXT".utf8)
45+
let source = ConsumingAsyncSequence(bytes: wire)
46+
47+
var decoded = [UInt8]()
48+
var iterator = HTTPChunkedTransferDecoder(bytes: source).makeAsyncIterator()
49+
while let buffer = try await iterator.nextBuffer(suggested: 1024) {
50+
decoded.append(contentsOf: buffer)
51+
}
52+
#expect(decoded == Array("Hello".utf8))
53+
54+
var trailing = [UInt8]()
55+
var sourceIterator = source.makeAsyncIterator()
56+
while let buffer = try await sourceIterator.nextBuffer(suggested: 1024) {
57+
trailing.append(contentsOf: buffer)
58+
}
59+
#expect(trailing == Array("NEXT".utf8))
60+
}
61+
62+
@Test
63+
func decoder_HonorsSuggestedBufferCount() async throws {
64+
let payload = String(repeating: "x", count: 100)
65+
let wire: [UInt8] = Array("64\r\n\(payload)\r\n0\r\n\r\n".utf8)
66+
67+
var iterator = HTTPChunkedTransferDecoder(
68+
bytes: ConsumingAsyncSequence(bytes: wire)
69+
).makeAsyncIterator()
70+
var sizes = [Int]()
71+
while let buffer = try await iterator.nextBuffer(suggested: 16) {
72+
sizes.append(buffer.count)
73+
}
74+
75+
#expect(sizes.allSatisfy { $0 <= 16 })
76+
#expect(sizes.reduce(0, +) == 100)
77+
}
78+
79+
// RFC 9112 §7.1 — `chunk-size = 1*HEXDIG`. Per RFC 5234 §2.3, ABNF literal
80+
// strings match case-insensitively, so lowercase `a-f` is also valid.
81+
@Test
82+
func decoder_AcceptsUppercaseHexChunkSize() async throws {
83+
let payload = String(repeating: "x", count: 0xFF)
84+
let wire: [UInt8] = Array("FF\r\n\(payload)\r\n0\r\n\r\n".utf8)
85+
86+
var iterator = HTTPChunkedTransferDecoder(
87+
bytes: ConsumingAsyncSequence(bytes: wire)
88+
).makeAsyncIterator()
89+
var decoded = [UInt8]()
90+
while let buffer = try await iterator.nextBuffer(suggested: 1024) {
91+
decoded.append(contentsOf: buffer)
92+
}
93+
94+
#expect(decoded.count == 0xFF)
95+
}
96+
97+
// A chunk-size larger than `Int.max` cannot be represented; `Int(_, radix:)`
98+
// returns nil and the decoder must reject it as a framing error rather than
99+
// silently truncating or trapping.
100+
@Test
101+
func decoder_RejectsChunkSizeExceedingIntMax() async throws {
102+
let wire: [UInt8] = Array("FFFFFFFFFFFFFFFFFFFF\r\n".utf8)
103+
104+
var iterator = HTTPChunkedTransferDecoder(
105+
bytes: ConsumingAsyncSequence(bytes: wire)
106+
).makeAsyncIterator()
107+
108+
await #expect(throws: HTTPDecoder.Error.self) {
109+
_ = try await iterator.nextBuffer(suggested: 1024)
110+
}
111+
}
112+
}

0 commit comments

Comments
 (0)