Chronaxis用户手册English
本页目录

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
Chronaxis 用户手册 · 离线阅读