1# Copyright 2023 Google LLC 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15from dataclasses import dataclass, field 16from typing import Optional, List, Dict, Tuple 17 18constructors_ = dict() 19 20 21def node(kind: str): 22 23 def decorator(cls): 24 cls = dataclass(cls) 25 constructors_[kind] = cls 26 return cls 27 28 return decorator 29 30 31@dataclass 32class SourceLocation: 33 offset: int 34 line: int 35 column: int 36 37 38@dataclass 39class SourceRange: 40 file: int 41 start: SourceLocation 42 end: SourceLocation 43 44 45@dataclass 46class Node: 47 kind: str 48 loc: SourceLocation 49 50 51@node('tag') 52class Tag(Node): 53 id: str 54 value: Optional[int] = field(default=None) 55 range: Optional[Tuple[int, int]] = field(default=None) 56 tags: Optional[List['Tag']] = field(default=None) 57 58 59@node('constraint') 60class Constraint(Node): 61 id: str 62 value: Optional[int] 63 tag_id: Optional[str] 64 65 66@dataclass 67class Field(Node): 68 parent: Node = field(init=False) 69 70 71@node('checksum_field') 72class ChecksumField(Field): 73 field_id: str 74 75 76@node('padding_field') 77class PaddingField(Field): 78 size: int 79 80 81@node('size_field') 82class SizeField(Field): 83 field_id: str 84 width: int 85 86 87@node('count_field') 88class CountField(Field): 89 field_id: str 90 width: int 91 92 93@node('body_field') 94class BodyField(Field): 95 id: str = field(init=False, default='_body_') 96 97 98@node('payload_field') 99class PayloadField(Field): 100 size_modifier: Optional[str] 101 id: str = field(init=False, default='_payload_') 102 103 104@node('fixed_field') 105class FixedField(Field): 106 width: Optional[int] = None 107 value: Optional[int] = None 108 enum_id: Optional[str] = None 109 tag_id: Optional[str] = None 110 111 @property 112 def type(self) -> Optional['Declaration']: 113 return self.parent.file.typedef_scope[self.enum_id] if self.enum_id else None 114 115 116@node('reserved_field') 117class ReservedField(Field): 118 width: int 119 120 121@node('array_field') 122class ArrayField(Field): 123 id: str 124 width: Optional[int] 125 type_id: Optional[str] 126 size_modifier: Optional[str] 127 size: Optional[int] 128 padded_size: Optional[int] = field(init=False, default=None) 129 130 @property 131 def type(self) -> Optional['Declaration']: 132 return self.parent.file.typedef_scope[self.type_id] if self.type_id else None 133 134 135@node('scalar_field') 136class ScalarField(Field): 137 id: str 138 width: int 139 140 141@node('typedef_field') 142class TypedefField(Field): 143 id: str 144 type_id: str 145 146 @property 147 def type(self) -> 'Declaration': 148 return self.parent.file.typedef_scope[self.type_id] 149 150 151@node('group_field') 152class GroupField(Field): 153 group_id: str 154 constraints: List[Constraint] 155 156 157@dataclass 158class Declaration(Node): 159 file: 'File' = field(init=False) 160 161 def __post_init__(self): 162 if hasattr(self, 'fields'): 163 for f in self.fields: 164 f.parent = self 165 166 167@node('endianness_declaration') 168class EndiannessDeclaration(Node): 169 value: str 170 171 172@node('checksum_declaration') 173class ChecksumDeclaration(Declaration): 174 id: str 175 function: str 176 width: int 177 178 179@node('custom_field_declaration') 180class CustomFieldDeclaration(Declaration): 181 id: str 182 function: str 183 width: Optional[int] 184 185 186@node('enum_declaration') 187class EnumDeclaration(Declaration): 188 id: str 189 tags: List[Tag] 190 width: int 191 192 193@node('packet_declaration') 194class PacketDeclaration(Declaration): 195 id: str 196 parent_id: Optional[str] 197 constraints: List[Constraint] 198 fields: List[Field] 199 200 @property 201 def parent(self) -> Optional['PacketDeclaration']: 202 return self.file.packet_scope[self.parent_id] if self.parent_id else None 203 204 205@node('struct_declaration') 206class StructDeclaration(Declaration): 207 id: str 208 parent_id: Optional[str] 209 constraints: List[Constraint] 210 fields: List[Field] 211 212 @property 213 def parent(self) -> Optional['StructDeclaration']: 214 return self.file.typedef_scope[self.parent_id] if self.parent_id else None 215 216 217@node('group_declaration') 218class GroupDeclaration(Declaration): 219 id: str 220 fields: List[Field] 221 222 223@dataclass 224class File: 225 endianness: EndiannessDeclaration 226 declarations: List[Declaration] 227 packet_scope: Dict[str, Declaration] = field(init=False) 228 typedef_scope: Dict[str, Declaration] = field(init=False) 229 group_scope: Dict[str, Declaration] = field(init=False) 230 231 def __post_init__(self): 232 self.packet_scope = dict() 233 self.typedef_scope = dict() 234 self.group_scope = dict() 235 236 # Construct the toplevel declaration scopes. 237 for d in self.declarations: 238 d.file = self 239 if isinstance(d, PacketDeclaration): 240 self.packet_scope[d.id] = d 241 elif isinstance(d, GroupDeclaration): 242 self.group_scope[d.id] = d 243 else: 244 self.typedef_scope[d.id] = d 245 246 @staticmethod 247 def from_json(obj: object) -> 'File': 248 """Import a File exported as JSON object by the PDL parser.""" 249 endianness = convert_(obj['endianness']) 250 declarations = convert_(obj['declarations']) 251 return File(endianness, declarations) 252 253 @property 254 def byteorder(self) -> str: 255 return 'little' if self.endianness.value == 'little_endian' else 'big' 256 257 @property 258 def byteorder_short(self, short: bool = False) -> str: 259 return 'le' if self.endianness.value == 'little_endian' else 'be' 260 261 262def convert_(obj: object) -> object: 263 if obj is None: 264 return None 265 if isinstance(obj, (int, str)): 266 return obj 267 if isinstance(obj, list): 268 return [convert_(elt) for elt in obj] 269 if isinstance(obj, object): 270 if 'start' in obj.keys() and 'end' in obj.keys(): 271 return (objs.start, obj.end) 272 kind = obj['kind'] 273 loc = obj['loc'] 274 loc = SourceRange(loc['file'], SourceLocation(**loc['start']), SourceLocation(**loc['end'])) 275 constructor = constructors_.get(kind) 276 members = {'loc': loc, 'kind': kind} 277 for name, value in obj.items(): 278 if name != 'kind' and name != 'loc': 279 members[name] = convert_(value) 280 return constructor(**members) 281 raise Exception('Unhandled json object type') 282