@@ -479,6 +479,170 @@ def _getter_span(
479479 or self ._getter_byte_span (getter )
480480 )
481481
482+ @staticmethod
483+ def _field_ivar_name (field : FieldDecl ) -> str :
484+ return f"m{ field .name [:1 ].upper ()} { field .name [1 :]} _"
485+
486+ def _serializer_primitive_width (
487+ self ,
488+ serializer : JavaMethod | MethodSymbol | None ,
489+ field : FieldDecl ,
490+ ) -> int | None :
491+ if serializer is None :
492+ return None
493+ text = self ._decompile_text (serializer )
494+ ivar = re .escape (self ._field_ivar_name (field ))
495+ writes = re .findall (
496+ r"objc_msgSend\([^;]*?\"writeWithInt:\"\s*,\s*([^;]*?)\);" ,
497+ text ,
498+ re .DOTALL ,
499+ )
500+ matching = [
501+ expression
502+ for expression in writes
503+ if re .search (rf"(?:self|[A-Za-z_][A-Za-z0-9_]*)->{ ivar } \b" ,
504+ expression )
505+ ]
506+ return len (matching ) if 1 <= len (matching ) <= 8 else None
507+
508+ def _parser_primitive_span (
509+ self ,
510+ parser : JavaMethod ,
511+ field : FieldDecl ,
512+ ) -> tuple [int , int ] | None :
513+ text = self ._decompile_text (parser )
514+ ivar = re .escape (self ._field_ivar_name (field ))
515+ target = re .search (
516+ rf"(?:self|[A-Za-z_][A-Za-z0-9_]*)->{ ivar } \s*=\s*([^;]+);" ,
517+ text ,
518+ )
519+ if target is None :
520+ return None
521+ assignments = {
522+ name : expression
523+ for name , expression in re .findall (
524+ r"(?m)^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([^;]+);" ,
525+ text ,
526+ )
527+ }
528+
529+ def byte_indices (expression : str , seen : set [str ]) -> set [int ]:
530+ indices = {
531+ int (value , 0 )
532+ for value in re .findall (
533+ r"IOSByteArray_buffer_\s*\+\s*"
534+ r"(0x[0-9A-Fa-f]+|\d+)" ,
535+ expression ,
536+ )
537+ }
538+ if re .search (r"IOSByteArray_buffer_\s*\]" , expression ):
539+ indices .add (0 )
540+ for name in re .findall (r"\b[A-Za-z_][A-Za-z0-9_]*\b" ,
541+ expression ):
542+ if name in seen or name not in assignments :
543+ continue
544+ indices .update (
545+ byte_indices (assignments [name ], {* seen , name })
546+ )
547+ return indices
548+
549+ indices = sorted (byte_indices (target .group (1 ), set ()))
550+ if not indices or indices != list (range (indices [0 ], indices [- 1 ] + 1 )):
551+ return None
552+ return indices [0 ], len (indices )
553+
554+ def _primitive_span_evidence (
555+ self ,
556+ class_name : str ,
557+ serializer : JavaMethod | MethodSymbol | None ,
558+ parsers : list [JavaMethod ],
559+ fields : list [FieldDecl ],
560+ ) -> tuple [list [FieldDecl ], dict [str , int ], list [str ]]:
561+ output : list [FieldDecl ] = []
562+ offsets : dict [str , int ] = {}
563+ evidence : list [str ] = []
564+ shift = 0
565+ for field in fields :
566+ adjusted_offset = (
567+ field .offset + shift if field .offset is not None else None
568+ )
569+ descriptor = (field .source_type or "" ).lstrip ("+-" )
570+ if descriptor not in ("I" , "J" ):
571+ output .append (replace (field , offset = adjusted_offset ))
572+ continue
573+ serializer_width = self ._serializer_primitive_width (
574+ serializer , field
575+ )
576+ parser_spans = {
577+ span
578+ for parser in parsers
579+ if (span := self ._parser_primitive_span (parser , field ))
580+ is not None
581+ }
582+ if len (parser_spans ) > 1 :
583+ raise ExtractionError (
584+ f"contradictory parser spans for "
585+ f"{ class_name } .{ field .name } : "
586+ f"{ sorted (parser_spans )} "
587+ )
588+ parser_span = next (iter (parser_spans ), None )
589+ parser_width = parser_span [1 ] if parser_span is not None else None
590+ widths = {
591+ width
592+ for width in (serializer_width , parser_width )
593+ if width is not None
594+ }
595+ if len (widths ) > 1 :
596+ raise ExtractionError (
597+ f"serializer/parser width mismatch for "
598+ f"{ class_name } .{ field .name } : "
599+ f"serializer={ serializer_width } , parser={ parser_width } "
600+ )
601+ if not widths :
602+ output .append (replace (field , offset = adjusted_offset ))
603+ continue
604+ width = next (iter (widths ))
605+ sized = self ._sized_primitive (descriptor , width )
606+ if sized is None :
607+ raise ExtractionError (
608+ f"unsupported inferred width { width } for "
609+ f"{ class_name } .{ field .name } ({ descriptor } )"
610+ )
611+ if sized == field .cpp_type :
612+ output .append (replace (field , offset = adjusted_offset ))
613+ continue
614+ # Nested parsers receive a child slice after its discriminator,
615+ # so parser indices prove width but are not always absolute wire
616+ # offsets. Preserve the declaration's accumulated offset.
617+ start = adjusted_offset
618+ old_size = _FIXED_SIZES .get (field .cpp_type )
619+ if old_size is None :
620+ output .append (replace (field , offset = start ))
621+ continue
622+ output .append (
623+ replace (
624+ field ,
625+ cpp_type = sized ,
626+ wire_kind = "pod" ,
627+ offset = start ,
628+ )
629+ )
630+ if start is not None :
631+ offsets [field .name ] = start
632+ shift += width - old_size
633+ if parser_span is not None :
634+ evidence .append (
635+ f"parser reads { field .name } from local bytes "
636+ f"{ parser_span [0 ]} .."
637+ f"{ parser_span [0 ] + parser_span [1 ] - 1 } "
638+ )
639+ if serializer_width is not None :
640+ evidence .append (
641+ f"serializer writes { serializer_width } byte(s) "
642+ f"for { field .name } "
643+ )
644+ return output , offsets , evidence
645+
482646 def _fixed_count_from_getter (self , getter : JavaMethod ) -> int | None :
483647 text = self ._decompile_text (getter )
484648 if '"size"' not in text and " size" not in text :
@@ -2606,6 +2770,21 @@ def extract_payload(
26062770 getter_offsets [name ] = static_offset
26072771 field_evidence .extend (static_evidence )
26082772
2773+ raw_fields , primitive_offsets , primitive_evidence = (
2774+ self ._primitive_span_evidence (
2775+ class_name , serializer_method , parser_methods , raw_fields
2776+ )
2777+ )
2778+ for name , primitive_offset in primitive_offsets .items ():
2779+ known_offset = getter_offsets .get (name )
2780+ if known_offset is not None and known_offset != primitive_offset :
2781+ raise ExtractionError (
2782+ f"contradictory offsets for { class_name } .{ name } : "
2783+ f"inferred={ known_offset } , parser={ primitive_offset } "
2784+ )
2785+ getter_offsets [name ] = primitive_offset
2786+ field_evidence .extend (primitive_evidence )
2787+
26092788 if (
26102789 serializer_method is None
26112790 and not has_command_stream
0 commit comments