Skip to content

Commit daed28c

Browse files
committed
reserved bit parsing and serializing: add comments, tests, and changelog
1 parent af84643 commit daed28c

File tree

3 files changed

+61
-6
lines changed

3 files changed

+61
-6
lines changed

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ Release History
44
dev
55
---
66

7+
**API Changes (Backward Compatible)**
8+
9+
- GoAwayFrame and WindowUpdateFrame now correctly mask off the reserved bit during
10+
parsing and serialization of stream IDs and window increments, as per RFC 9113,
11+
Sections 6.8 and 6.9.
12+
713
**API Changes (Backward Incompatible)**
814

915
-

src/hyperframe/frame.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ def parse_frame_header(header: memoryview, strict: bool = False) -> tuple[Frame,
132132
length = (fields[0] << 8) + fields[1]
133133
typ_e = fields[2]
134134
flags = fields[3]
135-
stream_id = fields[4] & 0x7FFFFFFF
135+
stream_id = fields[4] & 0x7FFFFFFF # mask off the reserved bit, RFC 9113, Section 4.1
136136

137137
try:
138138
frame = FRAMES[typ_e](stream_id)
@@ -172,7 +172,7 @@ def serialize(self) -> bytes:
172172
self.body_len & 0xFF,
173173
self.type,
174174
flags,
175-
self.stream_id & 0x7FFFFFFF, # Stream ID is 32 bits.
175+
self.stream_id & 0x7FFFFFFF, # mask off the reserved bit, RFC 9113, Section 4.1
176176
)
177177

178178
return header + body
@@ -271,7 +271,7 @@ def parse_priority_data(self, data: memoryview) -> int:
271271
raise InvalidFrameError(msg) from err
272272

273273
self.exclusive = bool(self.depends_on >> 31)
274-
self.depends_on &= 0x7FFFFFFF
274+
self.depends_on &= 0x7FFFFFFF # mask off the exclusive bit, RFC 9113, Section 6.3
275275
return 5
276276

277277

@@ -620,7 +620,7 @@ def _body_repr(self) -> str:
620620

621621
def serialize_body(self) -> bytes:
622622
data = _STRUCT_LL.pack(
623-
self.last_stream_id & 0x7FFFFFFF,
623+
self.last_stream_id & 0x7FFFFFFF, # mask off the reserved bit, RFC 9113, Section 6.8
624624
self.error_code,
625625
)
626626
data += self.additional_data
@@ -636,6 +636,7 @@ def parse_body(self, data: memoryview) -> None:
636636
msg = "Invalid GOAWAY body."
637637
raise InvalidFrameError(msg) from err
638638

639+
# mask off the reserved bit, RFC 9113, Section 6.8
639640
self.last_stream_id = self.last_stream_id & 0x7FFFFFFF
640641
self.body_len = len(data)
641642

@@ -675,7 +676,9 @@ def _body_repr(self) -> str:
675676
return f"window_increment={self.window_increment}"
676677

677678
def serialize_body(self) -> bytes:
678-
return _STRUCT_L.pack(self.window_increment & 0x7FFFFFFF)
679+
return _STRUCT_L.pack(
680+
self.window_increment & 0x7FFFFFFF, # mask off the reserved bit, RFC 9113, Section 6.9
681+
)
679682

680683
def parse_body(self, data: memoryview) -> None:
681684
if len(data) > 4:
@@ -688,6 +691,7 @@ def parse_body(self, data: memoryview) -> None:
688691
msg = "Invalid WINDOW_UPDATE body"
689692
raise InvalidFrameError(msg) from err
690693

694+
# mask off the reserved bit, RFC 9113, Section 6.9
691695
self.window_increment = self.window_increment & 0x7FFFFFFF
692696

693697
if not 1 <= self.window_increment <= 2**31-1:
@@ -907,7 +911,7 @@ def serialize(self) -> bytes:
907911
self.body_len & 0xFF,
908912
self.type,
909913
flags,
910-
self.stream_id & 0x7FFFFFFF, # Stream ID is 32 bits.
914+
self.stream_id & 0x7FFFFFFF, # mask off the reserved bit, RFC 9113, Section 4.1
911915
)
912916

913917
return header + self.body

tests/test_frames.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,35 @@ def test_short_goaway_frame_errors(self):
647647
with pytest.raises(InvalidFrameError):
648648
decode_frame(s)
649649

650+
def test_goaway_frame_with_reserved_bit_set_parses_properly(self):
651+
s = (
652+
b'\x00\x00\x0D\x07\x00\x00\x00\x00\x00' + # Frame header
653+
b'\x80\x00\x00\x40' + # Last Stream ID with reserved bit set
654+
b'\x00\x00\x00\x20' + # Error Code
655+
b'hello' # Additional data
656+
)
657+
f = decode_frame(s)
658+
659+
assert isinstance(f, GoAwayFrame)
660+
assert f.flags == set()
661+
assert f.additional_data == b'hello'
662+
assert f.body_len == 13
663+
assert f.last_stream_id == 64
664+
665+
def test_goaway_frame_with_reserved_bit_set_serializes_properly(self):
666+
f = GoAwayFrame()
667+
f.last_stream_id = 64
668+
f.error_code = 32
669+
f.additional_data = b'hello'
670+
671+
s = f.serialize()
672+
assert s == (
673+
b'\x00\x00\x0D\x07\x00\x00\x00\x00\x00' + # Frame header
674+
b'\x00\x00\x00\x40' + # Last Stream ID
675+
b'\x00\x00\x00\x20' + # Error Code
676+
b'hello' # Additional data
677+
)
678+
650679

651680
class TestWindowUpdateFrame:
652681
def test_repr(self):
@@ -694,6 +723,22 @@ def test_short_windowupdate_frame_errors(self):
694723
with pytest.raises(InvalidDataError):
695724
decode_frame(WindowUpdateFrame(2**31).serialize())
696725

726+
def test_window_update_frame_with_reserved_bit_set_parses_properly(self):
727+
s = b'\x00\x00\x04\x08\x00\x00\x00\x00\x80\x00\x00\x02\x00'
728+
f = decode_frame(s)
729+
730+
assert isinstance(f, WindowUpdateFrame)
731+
assert f.flags == set()
732+
assert f.window_increment == 512
733+
assert f.body_len == 4
734+
735+
def test_window_update_frame_with_reserved_bit_set_serializes_properly(self):
736+
f = WindowUpdateFrame(0)
737+
f.window_increment = 512
738+
739+
s = f.serialize()
740+
assert s == b'\x00\x00\x04\x08\x00\x00\x00\x00\x00\x00\x00\x02\x00'
741+
697742

698743
class TestHeadersFrame:
699744
def test_repr(self):

0 commit comments

Comments
 (0)