Skip to content

Commit 9156696

Browse files
authored
moq_decode: parse draft-18 SETUP and name extension types (#497)
1 parent 7781951 commit 9156696

1 file changed

Lines changed: 108 additions & 24 deletions

File tree

tools/moq_decode.py

Lines changed: 108 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,25 @@
6767
0: "ROLE",
6868
1: "PATH",
6969
2: "MAX_REQUEST_ID",
70+
3: "AUTHORIZATION_TOKEN",
71+
4: "MAX_AUTH_TOKEN_CACHE_SIZE",
72+
5: "AUTHORITY",
73+
7: "MOQT_IMPLEMENTATION",
74+
}
75+
76+
# Extension / Track-Property types (MoQTypes.h). The same parseExtensions()
77+
# grammar carries both object-header extensions and the trailing Track
78+
# Properties on SUBSCRIBE_OK / PUBLISH / FETCH_OK / REQUEST_OK, so one registry
79+
# covers both. Unknown types are extension points — decoded generically.
80+
EXTENSION_TYPES = {
81+
0x02: "DELIVERY_TIMEOUT", # OBJECT_DELIVERY_TIMEOUT (draft-18+)
82+
0x04: "MAX_CACHE_DURATION",
83+
0x0B: "IMMUTABLE_EXTENSIONS", # container: nested KV block, flattened
84+
0x0E: "PUBLISHER_PRIORITY",
85+
0x22: "PUBLISHER_GROUP_ORDER",
86+
0x30: "DYNAMIC_GROUPS",
87+
0x3C: "PRIOR_GROUP_ID_GAP",
88+
0x3E: "PRIOR_OBJECT_ID_GAP",
7089
}
7190

7291
GROUP_ORDER = {0: "Default", 1: "OldestFirst", 2: "NewestFirst"}
@@ -367,7 +386,16 @@ def p_subscription_filter_blob(raw_bytes, outer_base, annot, idx, draft):
367386
pass
368387

369388

370-
def p_params(cursor, annot, draft, count, param_keys):
389+
def p_params(cursor, annot, draft, count, param_keys, setup=False):
390+
# Count-prefixed message parameters: Request/Track params, plus legacy
391+
# draft≤16 CLIENT/SERVER_SETUP. Draft-17+ SETUP and extensions have no count
392+
# and use p_options() instead.
393+
#
394+
# setup=True selects ParamsType::Setup semantics: moxygen only applies the
395+
# known-key check (MoQFramer.cpp:907) and the Request value shapes
396+
# (LARGEST_OBJECT, SUBSCRIPTION_FILTER, uint8 FORWARD/PRIORITY/GROUP_ORDER)
397+
# to ParamsType::Request, so for setup unknown keys are extension points and
398+
# no value shapes apply.
371399
prev_key = 0
372400
for i in range(count):
373401
raw_key, s = cursor.read_varint()
@@ -386,7 +414,7 @@ def p_params(cursor, annot, draft, count, param_keys):
386414
# draft 18 Message Parameters are Type+Value (no length envelope),
387415
# so unknown keys cannot be skipped — section 10.2 says receivers
388416
# MUST close the session with PROTOCOL_VIOLATION.
389-
is_unknown = draft >= 16 and key_name is None
417+
is_unknown = draft >= 16 and key_name is None and not setup
390418
label = key_name if key_name else f"UNKNOWN({abs_key})"
391419
annot.add(
392420
s,
@@ -400,17 +428,17 @@ def p_params(cursor, annot, draft, count, param_keys):
400428
f"Unknown parameter key {abs_key} in v{draft} (delta={raw_key})", s
401429
)
402430

403-
if abs_key == 9: # LARGEST_OBJECT: length-prefixed AbsoluteLocation
431+
if not setup and abs_key == 9: # LARGEST_OBJECT: length-prefixed AbsoluteLocation
404432
vlen, vs = cursor.read_varint()
405433
annot.add(vs, cursor.pos, f"param[{i}].length", str(vlen))
406434
p_location(cursor, annot, f"param[{i}].largest_object")
407-
elif abs_key == 0x21: # SUBSCRIPTION_FILTER: decode blob inline
435+
elif not setup and abs_key == 0x21: # SUBSCRIPTION_FILTER: decode blob inline
408436
vlen, vs = cursor.read_varint()
409437
annot.add(vs, cursor.pos, f"param[{i}].length", str(vlen))
410438
blob_start = cursor.pos
411439
raw = cursor.read_bytes(vlen)
412440
p_subscription_filter_blob(raw, blob_start, annot, i, draft)
413-
elif draft >= 18 and abs_key in (0x10, 0x20, 0x22):
441+
elif not setup and draft >= 18 and abs_key in (0x10, 0x20, 0x22):
414442
# d18 §10.2.7/8/12: FORWARD (0x10), SUBSCRIBER_PRIORITY (0x20) and
415443
# GROUP_ORDER (0x22) values are a fixed uint8, overriding the
416444
# generic even-key varint (§1.4.3). Diverges at value >= 64.
@@ -433,28 +461,66 @@ def p_params(cursor, annot, draft, count, param_keys):
433461
)
434462

435463

436-
def p_extensions(cursor, annot, payload_end):
437-
"""Reads delta-encoded extension key-value pairs until payload_end (draft-16+)."""
438-
prev_type = 0
464+
def p_options(cursor, annot, payload_end, names, prefix="ext",
465+
key_field="type", immutable=True):
466+
"""Delta-encoded key/value options that span the message length with NO
467+
count field. Draft-17+ SETUP options and object / Track-Property extensions
468+
share this exact grammar (MoQFramer.cpp writeSetup / parseExtensionKvPairs):
469+
even keys carry a varint value; odd keys are length-prefixed byte arrays.
470+
471+
`names` is the label registry (SETUP_PARAM_KEYS or EXTENSION_TYPES); unknown
472+
keys are extension points, decoded generically. There are NO per-key value
473+
shapes here — those are Request-param only (see p_params). When immutable is
474+
True, key 0xB is an immutable container whose nested block is flattened, with
475+
the delta base reset to 0 inside and 0xB after (parseExtension:3793)."""
476+
prev = 0
439477
i = 0
440478
while cursor.pos < payload_end:
441-
raw_type, s = cursor.read_varint()
442-
abs_type = prev_type + raw_type
443-
prev_type = abs_type
444-
delta_str = f" (Δ={raw_type})" if raw_type else ""
445-
if abs_type % 2 == 0: # even: varint value
446-
annot.add(s, cursor.pos, f"ext[{i}].type", f"{abs_type}{delta_str}")
479+
raw_key, s = cursor.read_varint()
480+
abs_key = prev + raw_key
481+
prev = abs_key
482+
delta_str = f" (Δ={raw_key})" if raw_key else ""
483+
label = names.get(abs_key) or f"UNKNOWN({abs_key})"
484+
key_str = f"{abs_key}{delta_str} [{label}]"
485+
486+
if immutable and abs_key == 0x0B:
487+
# Immutable container: length-prefixed block of nested KV pairs,
488+
# flattened by moxygen. Bracket it so the boundary is visible.
489+
annot.add(s, cursor.pos, f"{prefix}[{i}].{key_field}", key_str)
490+
vlen, vs = cursor.read_varint()
491+
annot.add(vs, cursor.pos, f"{prefix}[{i}].length", str(vlen))
492+
block_start = cursor.pos
493+
block_end = block_start + vlen
494+
annot.add(
495+
block_start,
496+
block_start,
497+
f"{prefix}[{i}].imm",
498+
f"╭─ immutable block start ({vlen} bytes, [{block_start:04x}..{block_end:04x}))",
499+
)
500+
p_options(
501+
cursor, annot, block_end, names,
502+
f"{prefix}[{i}].imm", key_field, immutable=False,
503+
)
504+
annot.add(
505+
block_end, block_end, f"{prefix}[{i}].imm", "╰─ immutable block end"
506+
)
507+
prev = 0x0B
508+
i += 1
509+
continue
510+
511+
if abs_key % 2 == 0: # even: varint value
512+
annot.add(s, cursor.pos, f"{prefix}[{i}].{key_field}", key_str)
447513
val, vs = cursor.read_varint()
448-
annot.add(vs, cursor.pos, f"ext[{i}].value", str(val))
514+
annot.add(vs, cursor.pos, f"{prefix}[{i}].value", str(val))
449515
else: # odd: length-prefixed bytes
450-
annot.add(s, cursor.pos, f"ext[{i}].type", f"{abs_type}{delta_str}")
516+
annot.add(s, cursor.pos, f"{prefix}[{i}].{key_field}", key_str)
451517
vlen, vs = cursor.read_varint()
452-
annot.add(vs, cursor.pos, f"ext[{i}].length", str(vlen))
518+
annot.add(vs, cursor.pos, f"{prefix}[{i}].length", str(vlen))
453519
raw = cursor.read_bytes(vlen)
454520
annot.add(
455521
cursor.pos - vlen,
456522
cursor.pos,
457-
f"ext[{i}].value",
523+
f"{prefix}[{i}].value",
458524
repr(raw.decode("utf-8", errors="replace")),
459525
)
460526
i += 1
@@ -541,7 +607,7 @@ def parse_subscribe_ok(cursor, annot, draft, payload_end):
541607
p_params(cursor, annot, draft, count, TRACK_PARAM_KEYS)
542608

543609
if draft >= 16 and cursor.pos < payload_end:
544-
p_extensions(cursor, annot, payload_end)
610+
p_options(cursor, annot, payload_end, EXTENSION_TYPES)
545611

546612

547613
def parse_request_error(cursor, annot, draft, payload_end):
@@ -583,7 +649,7 @@ def parse_request_ok(cursor, annot, draft, payload_end):
583649
# In practice the writer only emits these for TRACK_STATUS_OK responses,
584650
# but the parser side just looks for trailing bytes.
585651
if draft >= 18 and cursor.pos < payload_end:
586-
p_extensions(cursor, annot, payload_end)
652+
p_options(cursor, annot, payload_end, EXTENSION_TYPES)
587653

588654

589655
def parse_namespace_or_publish_ns_error(cursor, annot, draft, payload_end):
@@ -797,7 +863,7 @@ def parse_fetch_ok(cursor, annot, draft, payload_end):
797863
p_params(cursor, annot, draft, count, TRACK_PARAM_KEYS)
798864

799865
if draft >= 16 and cursor.pos < payload_end:
800-
p_extensions(cursor, annot, payload_end)
866+
p_options(cursor, annot, payload_end, EXTENSION_TYPES)
801867

802868

803869
def parse_requests_blocked(cursor, annot, draft, payload_end):
@@ -827,7 +893,7 @@ def parse_publish(cursor, annot, draft, payload_end):
827893
p_params(cursor, annot, draft, count, TRACK_PARAM_KEYS)
828894

829895
if draft >= 16 and cursor.pos < payload_end:
830-
p_extensions(cursor, annot, payload_end)
896+
p_options(cursor, annot, payload_end, EXTENSION_TYPES)
831897

832898

833899
def parse_publish_ok(cursor, annot, draft, payload_end):
@@ -871,7 +937,7 @@ def parse_client_setup(cursor, annot, draft, payload_end=None):
871937

872938
count, s = cursor.read_varint()
873939
annot.add(s, cursor.pos, "num_params", str(count))
874-
p_params(cursor, annot, draft, count, SETUP_PARAM_KEYS)
940+
p_params(cursor, annot, draft, count, SETUP_PARAM_KEYS, setup=True)
875941

876942

877943
def parse_server_setup(cursor, annot, draft, payload_end=None):
@@ -881,7 +947,24 @@ def parse_server_setup(cursor, annot, draft, payload_end=None):
881947

882948
count, s = cursor.read_varint()
883949
annot.add(s, cursor.pos, "num_params", str(count))
884-
p_params(cursor, annot, draft, count, SETUP_PARAM_KEYS)
950+
p_params(cursor, annot, draft, count, SETUP_PARAM_KEYS, setup=True)
951+
952+
953+
def parse_setup(cursor, annot, draft, payload_end):
954+
# Unified SETUP (0x2f00), draft-17+. Version is negotiated via ALPN (no
955+
# version list on the wire) and draft-17 dropped the Number-of-Options field,
956+
# so setup options are the same count-less delta-KV grammar as extensions
957+
# (MoQFramer.cpp writeSetup). Parsed with the SetupKey registry; no immutable
958+
# container applies to setup.
959+
p_options(
960+
cursor,
961+
annot,
962+
payload_end,
963+
SETUP_PARAM_KEYS,
964+
prefix="param",
965+
key_field="key",
966+
immutable=False,
967+
)
885968

886969

887970
PARSERS = {
@@ -915,6 +998,7 @@ def parse_server_setup(cursor, annot, draft, payload_end=None):
915998
0x1F: parse_request_error,
916999
0x20: parse_client_setup,
9171000
0x21: parse_server_setup,
1001+
0x2F00: parse_setup,
9181002
}
9191003

9201004

0 commit comments

Comments
 (0)