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
7291GROUP_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
547613def 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
589655def 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
803869def 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
833899def 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
877943def 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
887970PARSERS = {
@@ -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