chronaxis.legacy.records
源码基线:8904c2f744951857659abd40aaa3535d57faf53c
1"""保留单BLF协议的报文、解码值、定位和有界IPC字段。23输入为同次can.Message的公开写出字段、原parse_frame返回值与用户定位声明;4输出是冻结dataclass及最大65536字节的JSON包。data以hex无损表达,非有限5解码浮点使用明确标签;不执行eval、pickle或任意模型反序列化,不应用DBC缩放。6本文件只依赖标准库,无来源/DBC读取或进程副作用。父进程承担wire/记录副本预算,7worker在INIT后才加载真实库;记录模型不声称原reader已关闭或旧进程RSS受控。8"""910# 模型和wire不导入CAN库或数值核心,spawn加载本文件不会提前打开用户输入。11from collections.abc import Mapping12from dataclasses import dataclass13import json14from math import isfinite, isnan15from pathlib import Path16from re import fullmatch17from types import MappingProxyType1819# profile标记既有解码语义,当前读取与写出已共用实现;wire限额仍是协议固定事实。20PROFILE_ID = 'sclicer-functions-v1'21MAX_WIRE_BYTES = 6553622_RAW_FIELDS = ('timestamp', 'arbitration_id', 'is_extended_id', 'is_remote_frame', 'is_error_frame',23 'channel', 'dlc', 'is_fd', 'is_rx', 'bitrate_switch', 'error_state_indicator')24_LOCATOR_FIELDS = ('channel', 'signal', 'frame_id', 'is_extended', 'is_fd')252627def normalize_frame_id(value):28 """将配置报文ID统一成整数;无前缀始终按十进制。2930 参数: value为原生int或十进制、0x十六进制、0b二进制文本。31 返回: 0至0x1FFFFFFF的原生int。异常: TypeError为其他类型;ValueError为格式或范围非法。32 副作用: 无;不用于ASC等原始文件的专有进制解析。33 """34 # bool也是int子类,必须先按精确类型排除,不能把开关当报文ID。35 if type(value) is str:36 if fullmatch(r'[0-9]+', value):37 value = int(value, 10)38 elif fullmatch(r'0[xX][0-9a-fA-F]+', value):39 value = int(value[2:], 16)40 elif fullmatch(r'0[bB][01]+', value):41 value = int(value[2:], 2)42 else:43 raise ValueError('frame_id需要十进制整数、0x十六进制或0b二进制')44 elif type(value) is not int:45 raise TypeError('frame_id需要整数或进制文本,不接受bool或浮点数')46 # 范围只证明是合法CAN数值;小ID仍可能是扩展帧,不在这里猜类型。47 if not 0 <= value <= 0x1FFFFFFF:48 raise ValueError('frame_id超出CAN范围0至0x1FFFFFFF')49 return value505152# 只计算固定键名、标点、item文本、空values花括号和data_hex两端引号。53# 动态标量的位置故意留空,这不是待发送的JSON;字段顺序不影响字节总数。54# 下方独立编码器对照测试会捕捉原ITEM结构变动,不能另行放宽单条能力。55_ITEM_FIXED_BYTES = len(56 b'{"op":"item","request_id":,"record":{"sequence":,"frame":{'57 b'"timestamp":,"arbitration_id":,"is_extended_id":,"is_remote_frame":,'58 b'"is_error_frame":,"channel":,"dlc":,"is_fd":,"is_rx":,'59 b'"bitrate_switch":,"error_state_indicator":,"data_hex":""},"values":{}}}')606162# 固定语法已包含values的{}与data_hex两引号。动态字段取确定上界:两个非负63# 64bit序号各20字符;有限binary64用32;29bit ID用9;DLC用2;64字节hex用128;64# 七个bool最多35。这里故意多算,只有完整上界不超过原限额时才能省掉精确计长。65_SMALL_ITEM_BASE_UPPER = _ITEM_FIXED_BYTES + 40 + 32 + 9 + 2 + 128 + 35666768def _small_item_fits(values, sequence, channel):69 """证明已核原字段的小ITEM符合原限额;不能证明时返回False,不拒绝输入。7071 参数: values已确认为Mapping;sequence已核原生非负int;channel已核原帧类型。72 同次原帧的有限float时间、29bit ID、DLC/data不超过64及七个bool也须已核。73 返回: bool。True表示完整原生dict均已检查且整个ITEM的保守上界不超过65536。74 异常: 取消、分配失败或原生dict并发变化等运行时错误原样传播,不伪装成正常回退。75 边界: 只接受非空原生ASCII键及原生bool、有限float或绝对值不超过64bit的int。76 非有限诊断、Unicode、大整数和近限额记录都交回原精确路径;不复制或缓存值。77 """78 name = value = None79 try:80 # 自定义Mapping/dict子类不在此额外调用len/items,仍只按原路径观察一次。81 if type(values) is not dict or sequence.bit_length() > 64:82 return False83 upper = _SMALL_ITEM_BASE_UPPER84 if channel is None:85 upper += 486 elif type(channel) is int:87 if channel.bit_length() > 64:88 return False89 upper += 21 # 64bit绝对值最多20位十进制数字,再加可能的负号。90 elif type(channel) is str:91 upper += 6 * len(channel) + 292 if upper > MAX_WIRE_BYTES or not channel.isascii():93 return False94 else:95 return False96 # ASCII字符最坏为六字节转义;键引号与冒号共3;数值取32;多计一个逗号。97 # 因而每个非空键至少占本证明的42字节。门禁失败只是需要精确检查。98 if len(values) > (MAX_WIRE_BYTES - upper) // 42:99 return False100 for name, value in values.items():101 if type(name) is not str or not name:102 return False103 upper += 6 * len(name) + 36104 if upper > MAX_WIRE_BYTES or not name.isascii():105 return False106 value_type = type(value)107 if value_type is float:108 # 有限binary64的JSON表示至多24字符,此处用32覆盖全部符号/指数格式。109 if not isfinite(value):110 return False111 elif value_type is int:112 if value.bit_length() > 64:113 return False114 elif value_type is not bool:115 return False116 return True117 finally:118 values = sequence = channel = name = value = None119120121def _integer(value, name, minimum=0, maximum=None):122 """检查原生整数边界,bool不能充当序号、通道或ID。123124 参数: value为候选值;name定位字段;minimum/maximum为含端点范围,None无上界。125 返回: 原int。异常: TypeError为非int,ValueError为超范围;无转换或I/O。126 """127 if type(value) is not int:128 raise TypeError(name + '需要原生int')129 if value < minimum or maximum is not None and value > maximum:130 raise ValueError(name + '超出允许范围')131 return value132133134def _text(value, name):135 """检查有限非空原文本,不将DBC名称改成表达式标识符。136137 参数: value为候选文本;name定位字段。返回: 原str。138 异常: TypeError/ValueError为类型、空文本或超过单包字符上限;不改大小写/空格。139 """140 if type(value) is not str:141 raise TypeError(name + '需要原生str')142 if not value or len(value) > MAX_WIRE_BYTES:143 raise ValueError(name + '为空或过长')144 return value145146147def _shape(value, names, name):148 """校验wire字典精确字段,拒绝额外对象标签或丢失字段。149150 参数: value为候选dict;names为固定字段名容器;name定位对象。151 返回: 原dict。异常: TypeError/ValueError为容器或字段不符。152 """153 if type(value) is not dict:154 raise TypeError(name + '需要dict')155 if len(value) != len(names) or any(key not in names for key in value):156 raise ValueError(name + '字段缺失或多余')157 return value158159160@dataclass(frozen=True)161class LegacyRawFrame:162 """保留同次原msg全部公开写出字段,不重构消息或推断DBC质量。"""163164 timestamp: float # 原浮点秒;有限性不代表上游精度可恢复。165 arbitration_id: int # 原29位以内报文ID,不把channel拼进ID。166 is_extended_id: bool # 原标准/扩展帧标志。167 is_remote_frame: bool # 原远程帧标志,不按投影滤掉。168 is_error_frame: bool # 原错误帧标志。169 channel: int | str | None # 原公开通道值,不猜缺失通道。170 dlc: int # 原公开DLC,不根据data长度重算。171 data: bytes # 原payload的独立无损bytes副本。172 is_fd: bool # 原CAN FD标志。173 is_rx: bool | None # 原收发方向;CSV未提供时为None,不能据此导出Rx/Tx。174 bitrate_switch: bool # 原FD速率切换标志。175 error_state_indicator: bool # 原FD错误状态指示。176177 def __post_init__(self):178 """校验公开字段类型,不补帧、缩放或改变原顺序。179180 参数: 无。返回: None。异常: TypeError/ValueError为非法字段或超过CAN最大payload。181 """182 if type(self.timestamp) is not float or not isfinite(self.timestamp):183 raise ValueError('原报文timestamp需要有限float秒')184 _integer(self.arbitration_id, 'arbitration_id', maximum=0x1FFFFFFF)185 _integer(self.dlc, 'dlc', maximum=64)186 if type(self.data) is not bytes or len(self.data) > 64:187 raise ValueError('原报文data需要不超过64字节的bytes')188 if self.channel is not None and type(self.channel) not in (int, str):189 raise TypeError('原报文channel需要int/str/None')190 if type(self.channel) is str and len(self.channel) > MAX_WIRE_BYTES:191 raise ValueError('原报文channel文本过长')192 if self.is_rx is not None and type(self.is_rx) is not bool:193 raise TypeError('is_rx需要bool或未知None')194 for name in ('is_extended_id', 'is_remote_frame', 'is_error_frame', 'is_fd',195 'bitrate_switch', 'error_state_indicator'):196 if type(getattr(self, name)) is not bool:197 raise TypeError(name + '需要bool')198199200@dataclass(frozen=True)201class LegacyDecodedFrame:202 """保存一次原yield和一次parse的结果,values独立只读且保留非有限诊断值。"""203204 sequence: int # 本次来源从0开始的原yield序号,不是物理文件行号。205 frame: LegacyRawFrame # 同次原msg公开快照。206 values: Mapping[str, int | float | bool] # 原返回字典,空dict不推断错误原因。207208 def __post_init__(self):209 """冻结原数值映射,不把缺键或非有限浮点替换为零。210211 参数: 无。返回: None。异常: TypeError/ValueError为序号、frame或数值类型错误。212 """213 _integer(self.sequence, 'sequence')214 if type(self.frame) is not LegacyRawFrame or not isinstance(self.values, Mapping):215 raise TypeError('解码记录需要LegacyRawFrame与数值Mapping')216 # ITEM外壳也属于64KiB能力;先完整预检,再复制有界映射,避免大Mapping先复制后拒绝。217 base = _wire_tree({'op': 'item', 'request_id': self.sequence,218 'record': {'sequence': self.sequence, 'frame': frame_to_dict(self.frame), 'values': {}}})219 available = MAX_WIRE_BYTES - base + 2220 _decoded_values_size(self.values, available)221 values = {}222 used = 2223 for name, value in self.values.items():224 # 第二遍仍逐项检查后才插入,Mapping在两遍间变化也不能扩大已复制前缀。225 separator = 1 if values else 0226 used += separator + _decoded_entry_size(name, value, available - used - separator)227 values[name] = value228 object.__setattr__(self, 'values', MappingProxyType(values))229230231@dataclass(frozen=True)232class StoredRawFrame:233 """为首次读取保存原frame与分析坐标,供后续导出从新暂存回读。"""234235 sequence: int # 原yield顺序,含同刻不同消息。236 time_ns: int # 明确映射后的int64纳秒,不改变原frame.timestamp。237 frame: LegacyRawFrame # 原公开写出字段。238239 def __post_init__(self):240 """检查原序号和新分析坐标,拒绝越界或替代模型。241242 参数: 无。返回: None。异常: TypeError/ValueError为字段或int64范围不符。243 """244 _integer(self.sequence, 'sequence')245 _integer(self.time_ns, 'time_ns', -(1 << 63), (1 << 63) - 1)246 if type(self.frame) is not LegacyRawFrame:247 raise TypeError('原始暂存需要LegacyRawFrame')248249250@dataclass(frozen=True)251class LegacyCANField:252 """按通道、ID和信号定位;可选帧类型由运行时实际记录消除歧义。"""253254 channel: int # 用户明确通道;首版定位不接受缺失或自由文本。255 signal: str # DBC原signal名称。256 frame_id: int # 同次DBC核对的原报文ID。257 is_extended: bool | None = None # None表示未限制帧类型,不等于标准帧。258 is_fd: bool | None = None # None表示未限制经典CAN/FD。259260 def __post_init__(self):261 """校验定位形状,实际唯一性由worker同次公开DBC元数据核实。262263 参数: 无。返回: None。异常: TypeError/ValueError为声明字段非法。264 """265 _integer(self.channel, 'field.channel')266 object.__setattr__(self, 'frame_id', normalize_frame_id(self.frame_id))267 _text(self.signal, 'field.signal')268 if any(value is not None and type(value) is not bool for value in (self.is_extended, self.is_fd)):269 raise TypeError('字段is_extended/is_fd需要bool或省略')270 if self.is_extended is False and self.frame_id > 0x7FF:271 raise ValueError('标准帧frame_id不能超过0x7FF')272273 @property274 def key(self):275 """提供稳定原定位键,供新绑定层引用;不改signal名称。276277 参数: 无。返回: 规范紧凑JSON str。异常: 原json编码错误;无I/O或DBC查询。278 """279 return encode_packet(field_to_dict(self)).decode('utf-8')280281282@dataclass(frozen=True)283class LegacyCANRequest:284 """声明单BLF、单DBC和有限原定位;构造只校验模型,不访问文件。"""285286 input_path: Path # 原BLF路径,不在模型中resolve或新增扫描。287 dbc_path: Path # 只供worker加载一次DBC。288 fields: tuple[LegacyCANField, ...] # 空tuple可原样读取全部帧,不省decode。289290 def __post_init__(self):291 """拒绝替代路径/定位及重复完整定位,保留原tuple顺序。292293 参数: 无。返回: None。异常: TypeError/ValueError为路径类型或重复定位错误。294 """295 if not isinstance(self.input_path, Path) or not isinstance(self.dbc_path, Path):296 raise TypeError('原CAN请求路径需要Path')297 if type(self.fields) is not tuple:298 raise TypeError('原CAN请求fields需要LegacyCANField tuple')299 if len(self.fields) > MAX_WIRE_BYTES // 2:300 raise ValueError('原CAN定位数量超过wire能力')301 if any(type(item) is not LegacyCANField for item in self.fields):302 raise TypeError('原CAN请求fields需要LegacyCANField tuple')303 # 路径和字段集先累计INIT字节,再创建重复键集合;不为任意大fields复制全部key。304 for path in (self.input_path, self.dbc_path):305 if sum(len(part) for part in path.parts) > MAX_WIRE_BYTES:306 raise ValueError('原CAN路径超过wire能力')307 used = _wire_tree({'op': 'init', 'version': 1, 'path': str(self.input_path),308 'dbc_path': str(self.dbc_path), 'fields': []})309 for index, field in enumerate(self.fields):310 separator = 1 if index else 0311 used += separator + _wire_tree(field_to_dict(field), remaining=MAX_WIRE_BYTES - used - separator)312 if len({item.key for item in self.fields}) != len(self.fields):313 raise ValueError('原CAN请求完整定位重复')314315 @property316 def profile(self):317 """读取唯一受支持profile。参数: 无。返回: 固定str。异常: 无。318 """319 return PROFILE_ID320321322def frame_to_dict(frame):323 """将原frame编码为固定JSON字段,data_hex不丢失任一字节。324325 参数: frame为LegacyRawFrame。返回: 新dict。异常: TypeError为非法模型。326 """327 if type(frame) is not LegacyRawFrame:328 raise TypeError('需要LegacyRawFrame')329 return {**{name: getattr(frame, name) for name in _RAW_FIELDS}, 'data_hex': frame.data.hex()}330331332def frame_from_dict(value):333 """只从固定字段重建原frame,不加载任意类或调用CAN构造器。334335 参数: value为wire dict。返回: LegacyRawFrame。336 异常: TypeError/ValueError为形状、hex或原frame字段非法。337 """338 _shape(value, (*_RAW_FIELDS, 'data_hex'), 'raw frame')339 text = value['data_hex']340 if type(text) is not str or len(text) > 128 or len(text) % 2 or any(char not in '0123456789abcdefABCDEF' for char in text):341 raise ValueError('data_hex需要不超过64字节的完整hex')342 return LegacyRawFrame(**{name: value[name] for name in _RAW_FIELDS}, data=bytes.fromhex(text))343344345def _value_to_wire(value):346 """给非有限原解码float明确标签,有限数字和bool保原类型。347348 参数: value为原生int/float/bool。返回: JSON标量或diagnostic_float字典。349 异常: TypeError为未支持值;不按数值truthiness丢弃False或0。350 """351 if type(value) not in (int, float, bool):352 raise TypeError('解码wire值仅支持数值或bool')353 if type(value) is float and not isfinite(value):354 return {'diagnostic_float': 'nan' if isnan(value) else 'inf' if value > 0 else '-inf'}355 return value356357358def _value_from_wire(value):359 """恢复固定非有限标签,拒绝裸NaN和任意标签对象。360361 参数: value为JSON数值/bool或固定诊断dict。返回: 原生int/float/bool。362 异常: TypeError/ValueError为未知类型、标签或未标记非有限数值。363 """364 if type(value) is dict:365 _shape(value, ('diagnostic_float',), 'decoded diagnostic')366 if value['diagnostic_float'] not in ('nan', 'inf', '-inf'):367 raise ValueError('未知非有限解码标签')368 return float(value['diagnostic_float'])369 if type(value) not in (int, float, bool) or type(value) is float and not isfinite(value):370 raise TypeError('解码wire值非法或非有限标签缺失')371 return value372373374def decoded_to_dict(record):375 """编码一次原yield的完整解码记录,不投影或过滤返回键。376377 参数: record为LegacyDecodedFrame。返回: 固定dict。378 异常: TypeError为非法模型;原值编码错误传播。379 """380 if type(record) is not LegacyDecodedFrame:381 raise TypeError('需要LegacyDecodedFrame')382 return {'sequence': record.sequence, 'frame': frame_to_dict(record.frame),383 'values': {name: _value_to_wire(value) for name, value in record.values.items()}}384385386def decoded_from_dict(value):387 """重建单次原返回,空dict与非有限诊断均保持,不猜解码失败原因。388389 参数: value为固定wire dict。返回: LegacyDecodedFrame。390 异常: TypeError/ValueError为形状或值非法。391 """392 _shape(value, ('sequence', 'frame', 'values'), 'decoded frame')393 if type(value['values']) is not dict:394 raise TypeError('decoded values需要dict')395 # direct helper调用也不能绕过decode_packet,在新values字典复制前核原JSON树上限。396 _wire_tree(value)397 return LegacyDecodedFrame(value['sequence'], frame_from_dict(value['frame']),398 {name: _value_from_wire(item) for name, item in value['values'].items()})399400401def _decoded_from_checked_dict(value):402 """在同次严格JSON解包后重建原记录,原值只检查一次后冻结自有字典。403404 参数: value为消息仓或批次解包中同次decode_packet刚返回的原生dict;405 两者之间不能调用外部代码、修改子树或把检查结果缓存到其他请求。406 返回: LegacyDecodedFrame,原字段、数值类型及非有限诊断按公共入口相同方式恢复。407 异常: 原形状、frame、数值、模型限额或分配/取消错误;调用方保持原错误包装。408 约束: 首次解包已检查UTF-8、重复键、深度和64KiB;本函数不是独立解析入口,409 不开放trusted参数。公共decoded_from_dict仍独立完整检查,不能改走本路径省略边界。410 本层新建的values只含原生不可变标量,未向外暴露;常见完整值可用保守上界411 证明ITEM不超限,不能证明时沿原精确路径。核完ITEM外壳/全部值412 后可直接冻结,不需要公开构造器为外部可变Mapping设置的第二遍检查和复制。413 副作用: 仅建立本条有界模型;不持有跨调用状态,不读写文件或改变预算。414 清理只断开本模块已结束异常frame的临时引用,保留原异常、诊断与traceback结构。415 """416 sequence = frame = values = record = current = None417 try:418 _shape(value, ('sequence', 'frame', 'values'), 'decoded frame')419 if type(value['values']) is not dict:420 raise TypeError('decoded values需要dict')421 # 原构造表达式先取sequence,再构造frame及转换全部values;sequence的类型422 # 检查在这三项求值之后,不能因合并步骤改变同时存在多个错误时的优先级。423 sequence = value['sequence']424 frame = frame_from_dict(value['frame'])425 values = {name: _value_from_wire(item) for name, item in value['values'].items()}426 _integer(sequence, 'sequence')427 if type(frame) is not LegacyRawFrame:428 raise TypeError('解码记录需要LegacyRawFrame与数值Mapping')429 # 原decoded包在64KiB内,不等于加ITEM外壳后仍在限额内。此处frame及序号430 # 已完成原检查,values也已在本层转换;只在完整保守上界成立时省精确计长。431 # Unicode/非有限诊断/大整数/近限额沿原公式和错误先后,不凭wire成功放行。432 if not _small_item_fits(values, sequence, frame.channel):433 base = _wire_tree({'op': 'item', 'request_id': sequence,434 'record': {'sequence': sequence, 'frame': frame_to_dict(frame), 'values': {}}})435 _decoded_values_size(values, MAX_WIRE_BYTES - base + 2)436 # 字典只在本层创建和检查,无用户Mapping回调或借入可变别名;不调用公开437 # 构造器重做第二遍。同一真实record持有冻结后的唯一dict,不借原parsed树。438 record = object.__new__(LegacyDecodedFrame)439 object.__setattr__(record, 'sequence', sequence)440 object.__setattr__(record, 'frame', frame)441 object.__setattr__(record, 'values', MappingProxyType(values))442 return record443 except BaseException as exc:444 # 调用方可能保留原取消/错误;只清本模块已结束的辅助frame,不清用户或445 # 标准库诊断,不沿cause/context扩展遍历。活跃的本函数由finally清其责任槽。446 current = exc.__traceback__447 while current is not None:448 if current.tb_frame.f_globals.get('__name__') == __name__:449 try:450 current.tb_frame.clear()451 except RuntimeError:452 pass453 current = current.tb_next454 raise455 finally:456 value = sequence = frame = values = record = current = None457458459def _try_decoded_small_packet(payload):460 """解析并证明常见完整decoded小包合法,不能证明时返回None交回原入口。461462 参数: payload为本次已预付工作空间内的有界bytes,不借外部JSON树或验证标志。463 返回: 独立冻结的LegacyDecodedFrame;None只表示需走完整原路径,不表示输入非法。464 异常: 原JSON/模型错误、MemoryError或取消;外层只对普通错误重走原错误顺序。465 流程: 保留UTF-8、重复键、裸非有限常量检查及完整frame/sequence校验;只有466 原生ASCII键和有限数值/bool满足完整ITEM保守上界,才省去重复JSON树遍历。467 固定字段结构的深度与标量已由模型核定,ITEM上界也覆盖较小decoded包。468 复杂值、Unicode、近限额等均返回原路径;成功只创建一份frame及自有values。469 副作用: 无I/O、跨行缓存或新预算owner。回退前释放本次临时模型,原错误仅清理470 本模块已结束frame的借入引用;取消及内存错误不重试,不清用户诊断frame。471 """472 parsed = raw_values = values = sequence = frame = result = current = None473 try:474 if type(payload) is not bytes or not payload or len(payload) > MAX_WIRE_BYTES:475 return None476 parsed = json.loads(payload.decode('utf-8'), object_pairs_hook=_unique_object,477 parse_constant=_reject_constant)478 _shape(parsed, ('sequence', 'frame', 'values'), 'decoded frame')479 raw_values = parsed['values']480 if type(raw_values) is not dict:481 return None482 frame = frame_from_dict(parsed['frame'])483 sequence = parsed['sequence']484 _integer(sequence, 'sequence')485 if not _small_item_fits(raw_values, sequence, frame.channel):486 return None487 # 正证明仅接受原生有限数值/bool;_value_from_wire对此域原样返回。488 # parsed未暴露给调用方,复制一次可使最终冻结字典与解析树独立。489 values = raw_values.copy()490 result = object.__new__(LegacyDecodedFrame)491 object.__setattr__(result, 'sequence', sequence)492 object.__setattr__(result, 'frame', frame)493 object.__setattr__(result, 'values', MappingProxyType(values))494 return result495 except BaseException as exc:496 current = exc.__traceback__497 while current is not None:498 if current.tb_frame.f_globals.get('__name__') == __name__:499 try:500 current.tb_frame.clear()501 except RuntimeError:502 pass503 current = current.tb_next504 raise505 finally:506 payload = parsed = raw_values = values = sequence = frame = result = current = None507508509def _decoded_from_packet(payload):510 """在同次专用decoded包解析中复用小包证明,保原完整入口的输出及错误顺序。511512 参数: payload为完整decoded JSON bytes,仍受原64KiB及ITEM外壳限制。513 返回: LegacyDecodedFrame;完整字段、原值类型、量化前时间及非有限诊断不变。514 异常: 与decode_packet后接私有构建相同的原错误;MemoryError/取消原样传播。515 边界: 普通失败退出except并释放试算对象后,重新由原完整解析/构建决定报错;516 不提前把frame错误置于wire错误前。公共decode_packet及decoded_from_dict不变。517 """518 result = None519 try:520 try:521 result = _try_decoded_small_packet(payload)522 except (TypeError, ValueError, OverflowError, RecursionError):523 pass524 if result is not None:525 return result526 return _decoded_from_checked_dict(decode_packet(payload))527 finally:528 payload = result = None529530531def field_to_dict(field):532 """按唯一定位合同输出原字段,不带样本或隐式别名。533534 参数: field为LegacyCANField。返回: 固定dict。异常: TypeError为非法模型。535 """536 if type(field) is not LegacyCANField:537 raise TypeError('需要LegacyCANField')538 return {name: getattr(field, name) for name in _LOCATOR_FIELDS if getattr(field, name) is not None}539540541def field_from_dict(value):542 """只构建固定定位模型,不接受多余元数据或对象标签。543544 参数: value为固定dict。返回: LegacyCANField。异常: TypeError/ValueError为形状或字段非法。545 """546 # 省略与显式null区分;旧message不能静默忽略,避免误以为它仍约束匹配。547 if type(value) is not dict:548 raise TypeError('CAN field需要dict')549 if 'message' in value:550 raise ValueError('message参数已删除;请删除message,保留signal并用frame_id定位')551 if set(value) - set(_LOCATOR_FIELDS) or not {'channel', 'signal', 'frame_id'} <= set(value):552 raise ValueError('CAN field需要channel、frame_id、signal;仅可附加is_extended/is_fd')553 if any(name in value and type(value[name]) is not bool for name in ('is_extended', 'is_fd')):554 raise TypeError('is_extended/is_fd填写时必须为bool,不能为null')555 return LegacyCANField(**value)556557558def _scalar_size(value, remaining):559 """计算原生JSON标量字节,普通标量直接计数,复杂文本仍交原JSON编码器。560561 参数: value为JSON标量;remaining为本包剩余字节。返回: 精确UTF-8编码长度。562 异常: TypeError/ValueError/UnicodeError为类型/数值/限额;标量暂态最多单包有限倍数。563 """564 if type(value) is str:565 if len(value) + 2 > remaining:566 raise ValueError('wire文本超过单包上限')567 # 可打印ASCII中只需排除引号和反斜杠,其他字符一字节且不转义,另计两端引号。568 # 控制字符、DEL、Unicode及转义文本保留原编码路径,不重写JSON转义/UTF-8规则。569 if value.isascii() and value.isprintable() and '"' not in value and '\\' not in value:570 size = len(value) + 2571 else:572 size = len(json.dumps(value, ensure_ascii=False, allow_nan=False, separators=(',', ':')).encode('utf-8'))573 elif value is None:574 size = 4 # JSON的null固定四个ASCII字节,不为尺寸检查重复构造编码器。575 elif type(value) is bool:576 size = 4 if value else 5 # JSON原样写true/false,不能按整数1/0的长度计算。577 elif type(value) is int:578 if value.bit_length() > MAX_WIRE_BYTES * 4:579 raise ValueError('wire整数超过单包能力')580 # 原生int的十进制表示与JSON一致,保符号及Python本身的超长整数转换限制。581 size = len(str(value))582 elif type(value) is float:583 if not isfinite(value):584 raise ValueError('wire非有限数值必须显式标签')585 # 标准库JSON对有限原生float使用其repr,包含指数及-0.0;全为ASCII。586 # 此处只计长度,最终输出仍由JSON编码器产生,非有限值继续由上方拒绝。587 size = len(repr(value))588 else:589 raise TypeError('wire标量类型不受支持')590 if size > remaining:591 raise ValueError('wire包超过65536字节')592 return size593594595def _decoded_entry_size(name, value, remaining):596 """在插入新values字典前核单项完整wire成本,保0/False和非有限原值。597598 参数: name为原信号str;value为原数值/bool;remaining为剩余值域字节。599 返回: 键、冒号和值的精确长度。异常: 原类型、非空名称或wire限额错误。600 """601 _text(name, 'decoded signal')602 size = _scalar_size(name, remaining)603 return size + 1 + _wire_tree(_value_to_wire(value), remaining=remaining - size - 1)604605606def _decoded_values_size(values, remaining):607 """不复制Mapping先预检全部解码值,累计成本限制无界或动态items前缀。608609 参数: values为原Mapping;remaining为含两花括号的wire可用字节。610 返回: 精确值字典长度。异常: 类型/限额错误,尚未创建values副本。611 """612 if len(values) > remaining // 4:613 raise ValueError('解码信号数量超过64KiB wire能力')614 used = 2615 for index, (name, value) in enumerate(values.items()):616 separator = 1 if index else 0617 used += separator + _decoded_entry_size(name, value, remaining - used - separator)618 return used619620621def _wire_tree(value, depth=0, remaining=MAX_WIRE_BYTES):622 """在JSON复制前累计精确包大小,嵌套子树共享剩余额度而非各自重置上限。623624 参数: value为JSON树;depth为递归层;remaining为整包尚余字节。625 返回: 紧凑UTF-8 JSON精确长度。异常: TypeError/ValueError为类型/深度/总量;无任意类构建。626 """627 if depth > 16 or remaining < 0:628 raise ValueError('wire嵌套或累计大小超过上限')629 if value is None or type(value) in (bool, int, float, str):630 return _scalar_size(value, remaining)631 if type(value) not in (dict, list):632 raise TypeError('wire仅接JSON结构,不接任意对象')633 if len(value) > remaining // 2:634 raise ValueError('wire容器超过单包上限')635 used = 2636 if type(value) is dict:637 for index, (name, item) in enumerate(value.items()):638 if type(name) is not str:639 raise TypeError('wire字典键需要str')640 used += 1 if index else 0641 # 键已确认为原生str,直接计数;仍先执行原递归入口的深度/剩余守卫。642 # 守卫顺序及错误消息不变,不能因省去类型分流而放过第17层或负剩余额度。643 key_remaining = remaining - used644 if depth + 1 > 16 or key_remaining < 0:645 raise ValueError('wire嵌套或累计大小超过上限')646 used += _scalar_size(name, key_remaining)647 used += 1648 used += _wire_tree(item, depth + 1, remaining - used)649 else:650 for index, item in enumerate(value):651 used += 1 if index else 0652 used += _wire_tree(item, depth + 1, remaining - used)653 if used > remaining:654 raise ValueError('wire包超过65536字节')655 return used656657658def encode_packet(value):659 """编码最大65536字节的单个JSON对象包,不带换行或pickle。660661 参数: value为dict根、有限JSON树。返回: UTF-8 bytes。662 异常: TypeError/ValueError为类型/非有限/深度/字节上限或JSON编码失败。663 """664 if type(value) is not dict:665 raise TypeError('wire包根需要dict')666 _wire_tree(value)667 # 树预检已限制整包精确长度;一次编码避免逐JSON片段调用Python循环和UTF-8转换。668 # 保留原排序、紧凑格式和严格UTF-8,并再次核对实际字节,不依赖尺寸预检独自守边界。669 result = json.dumps(value, ensure_ascii=False, allow_nan=False, sort_keys=True,670 separators=(',', ':')).encode('utf-8')671 if len(result) > MAX_WIRE_BYTES:672 raise ValueError('wire包超过65536字节')673 return result674675676def _unique_object(pairs):677 """拒绝重复JSON键,防止后值覆盖已核过的协议字段。678679 参数: pairs为解析器键值对。返回: 新dict。异常: ValueError为重复字段。680 """681 result = {}682 for name, value in pairs:683 if name in result:684 raise ValueError('wire存在重复JSON键')685 result[name] = value686 return result687688689def _reject_constant(value):690 """拒绝NaN/Infinity非标准JSON常量。691692 参数: value为解析器常量文本。返回: 无。异常: ValueError总是抛出。693 """694 raise ValueError('wire非法JSON数值: ' + value)695696697def decode_packet(payload):698 """从有界JSON bytes校验包形状,不反序列化任意Python对象。699700 参数: payload为不超过MAX_WIRE_BYTES的bytes。返回: 已校验dict。701 异常: TypeError/ValueError/UnicodeError为类型、大小、格式或结构非法。702 """703 if type(payload) is not bytes or not payload or len(payload) > MAX_WIRE_BYTES:704 raise ValueError('wire包需要1至65536字节的bytes')705 value = json.loads(payload.decode('utf-8'), object_pairs_hook=_unique_object, parse_constant=_reject_constant)706 if type(value) is not dict:707 raise TypeError('wire包根需要dict')708 _wire_tree(value)709 return value710