自定义报文解析器

报文格式配置文件说明

JSON文件描述报文格式,作为解析器解析报文的模版。JSON文件的编写方法,涵盖顶层结构、字段属性、所有支持的类型、位域、枚举、条件分支、数组、表达式求值等全部语法。

一、配置文件整体结构

配置文件是一个 JSON 对象,顶层包含以下键:

json 复制代码
{
  "name": "报文名称",
  "endian": "big",
  "definitions": { ... },
  "fields": [ ... ]
}
键 必填 类型 说明
name 否 字符串 报文名称,显示在报告头部
endian 否 字符串 默认字节序,"big"(默认)或 "little"
definitions 否 对象 可复用的结构体定义,键为结构体名
fields 是 数组 根字段列表,按顺序解析

二、字段的通用属性

每个字段是一个 JSON 对象,通用属性如下:

属性 必填 类型 说明
name 是 字符串 字段名,用于显示和上下文引用
type 否 字符串 字段类型,默认 "bytes"
desc 否 字符串 字段描述,显示在报告尾部注释
length 否 数字/字符串 字节长度,见第五节
endian 否 字符串 该字段单独的字节序,覆盖顶层设置
enum 否 对象 取值映射,见第七节
bits 否 数组 位域定义,见第六节
format 否 字符串 显示格式,"hex" 或 "dec"

三、字段类型一览

类型 字节数 说明
uint8 / uint16 / uint24 / uint32 / uint64 / uint128 1/2/3/4/8/16 无符号整数
int8 / int16 / int24 / int32 / int64 / int128 1/2/3/4/8/16 有符号整数
bytes / hex / raw 可变 原始字节,输出十六进制字符串
string 可变 字符串,可指定 encoding(默认 UTF-8)
mac 6 MAC 地址,输出 XX:XX:XX:XX:XX:XX
ipv4 4 IPv4 地址,输出点分十进制
ipv6 16 IPv6 地址,输出压缩格式
struct 由引用决定 引用 definitions 中的结构体
switch 由分支决定 条件分支,根据字段值选择解析路径
array 由元素决定 重复解析同一字段
padding / pad 由长度决定 跳过若干字节,输出为原始字节

四、整数类型与字节序

4.1 基本用法

bash 复制代码
{ "name": "src_port", "type": "uint16", "desc": "源端口" }

解析器按 endian 指定的字节序读取。顶层 "endian": "big" 时,0x1F90 解析为 8080;"little" 时解析为 36991。

4.2 单字段字节序覆盖

某些协议字段使用与其他字段不同的字节序,可在字段级覆盖:

bash 复制代码
{ "name": "checksum", "type": "uint16", "endian": "little" }

4.3 显示格式

bash 复制代码
{ "name": "flags", "type": "uint8", "format": "hex" }   // 0x45
{ "name": "count", "type": "uint16", "format": "dec" }  // 1234

不指定时默认同时显示十六进制和十进制:0x0045 (69)。

五、长度(length)

length 属性决定 bytes、string、padding、array 等类型的读取字节数或次数。

5.1 固定数字

bash 复制代码
json
{ "name": "reserved", "type": "bytes", "length": 4 }

5.2 读取剩余全部

bash 复制代码
json
{ "name": "payload", "type": "bytes", "length": "rest" }

"rest"、"remaining"、"*"、"auto" 等价。

5.3 表达式

表达式中可以引用在此之前已解析的字段名(包括位域拆出的子字段名):

bash 复制代码
json
{ "name": "options", "type": "bytes", "length": "ihl*4 - 20" }
{ "name": "payload", "type": "bytes", "length": "total_length - ihl*4" }

表达式在受限环境中求值,只允许算术运算和已解析字段的引用,禁止访问 __ 开头的属性,禁止使用内置函数。

5.4 不指定 length

对于 bytes、string、padding,不指定 length 等价于 "rest",即读取剩余全部字节。

六、位域(bits)

bits 用于把整数类型拆解成多个子字段。

6.1 基本语法

bash 复制代码
json
{
  "name": "version_ihl",
  "type": "uint8",
  "bits": [
    { "name": "version", "hi": 7, "lo": 4, "desc": "版本" },
    { "name": "ihl",     "hi": 3, "lo": 0, "desc": "首部长度" }
  ]
}

hi 和 lo 是相对于整个整数值的位索引,最低位为 0。

对于 uint8,合法范围是 0~7;对于 uint32,是 0~31;对于 uint128,是 0~127。

子字段的值会被存入上下文,供后续 length 表达式和 switch 引用。

6.2 在报告中的显示

bash 复制代码
0x000E    version_ihl               uint8                1  0x45 (69)
0x000E      version                 bits[7:4]            1  0x4 (4)
0x000E      ihl                     bits[3:0]            1  0x5 (5)

6.3 位域嵌套枚举

子字段同样支持 enum:

bash 复制代码
json
{
  "name": "flags",
  "type": "uint8",
  "bits": [
    { "name": "df", "hi": 6, "lo": 6, "desc": "不分片",
      "enum": { "0": "允许分片", "1": "禁止分片" } }
  ]
}

七、枚举(enum)

enum 把数值映射为可读字符串,显示在值后面。

bash 复制代码
json
{
  "name": "ether_type",
  "type": "uint16",
  "enum": {
    "0x0800": "IPv4",
    "0x0806": "ARP",
    "0x8100": "VLAN",
    "0x86DD": "IPv6"
  }
}

键支持十进制、十六进制(0x 前缀)、八进制(0o 前缀)、二进制(0b 前缀)。

显示效果:0x0800 (2048) = IPv4

八、结构体(struct)

definitions 中定义可复用的结构体,字段中用 type: "struct" 引用。

8.1 定义结构体

bash 复制代码
json
"definitions": {
  "ipv4": {
    "desc": "IPv4 头部",
    "fields": [ ... ]
  }
}

8.2 引用结构体

bash 复制代码
json
{ "name": "nth_ipv4", "type": "struct", "struct": "ipv4", "desc": "IPv4 头" }

结构体内部字段的路径会加上前缀,例如 payload.nth_ipv4.version_ihl。

8.3 结构体内的字段引用

结构体内的 length 表达式只能引用同一结构体内已解析的字段和外层已解析的字段。

九、条件分支(switch)

switch 根据已解析字段的值选择不同的解析路径。

9.1 单字段匹配

bash 复制代码
json
{
  "name": "payload",
  "type": "switch",
  "switch_on": "ether_type",
  "cases": {
    "0x0800": [ { "name": "ipv4", "type": "struct", "struct": "ipv4" } ],
    "0x0806": [ { "name": "arp",  "type": "struct", "struct": "arp"  } ]
  },
  "default": [
    { "name": "raw", "type": "bytes", "length": "rest" }
  ]
}

switch_on 引用的字段必须在此之前已解析。

每个 case 的值是字段列表,匹配后会依次解析。

default 是可选的,没有匹配项时使用;如果也没有 default,解析器会报错。

9.2 多字段组合匹配

switch_on 可以是一个列表,case 的键用逗号分隔:

bash 复制代码
json
{
  "name": "ub_transport",
  "type": "switch",
  "switch_on": ["cfg", "nth_nlp"],
  "cases": {
    "3,0": [ { "name": "rtph", "type": "struct", "struct": "ub_tph_rtp" } ],
    "3,1": [ { "name": "utph", "type": "struct", "struct": "ub_tph_utp" } ],
    "6,0": [ { "name": "rtph", "type": "struct", "struct": "ub_tph_rtp" } ],
    "6,4": [ { "name": "bypass", "type": "bytes", "length": "0" } ]
  }
}

case 键的分量按 switch_on 列表顺序对应。

每个分量独立按字段类型匹配(整数支持十进制/十六进制写法)。

匹配时要求所有分量同时相等。

9.3 case 键的写法规则

实际字段类型 case 键写法

整数 "0x0800"、"2048"、"0b1000" 等

布尔 "1"、"0"、"true"、"false"

字符串 直接写字符串

bytes 十六进制串,分隔符(:、空格、-、_)会被忽略

十、数组(array)

array 重复解析同一个字段定义。

bash 复制代码
{
  "name": "entries",
  "type": "array",
  "count": "entry_count",
  "item": {
    "name": "entry",
    "type": "struct",
    "struct": "some_entry"
  }
}

count 可以是数字或表达式(引用已解析字段)。

item 是要重复的字段定义,其 name 会被自动加上 i 索引后缀。

解析结果中会先输出一个 array 头,再逐个输出各元素。

十一、填充(padding)

json

bash 复制代码
{ "name": "pad", "type": "padding", "length": 4, "desc": "对齐填充" }

跳过指定字节,在报告中显示为原始十六进制。常用于对齐或保留字段。

十二、完整示例:以太网帧

json

bash 复制代码
{
  "name": "Ethernet / IPv4 / IPv6 / VLAN Full Stack (Enhanced)",
  "endian": "big",
  "definitions": {

    "vlan_tag": {
      "desc": "802.1Q VLAN Tag(支持 QinQ 递归)",
      "fields": [
        {
          "name": "tci",
          "type": "uint16",
          "desc": "标签控制信息",
          "bits": [
            { "name": "pcp", "hi": 15, "lo": 13, "desc": "优先级 PCP" },
            { "name": "dei", "hi": 12, "lo": 12, "desc": "丢弃资格 DEI" },
            { "name": "vid", "hi": 11, "lo": 0,  "desc": "VLAN ID" }
          ]
        },
        {
          "name": "inner_ether_type",
          "type": "uint16",
          "desc": "内层以太网类型",
          "enum": {
            "0x0800": "IPv4",
            "0x0806": "ARP",
            "0x86DD": "IPv6",
            "0x8100": "VLAN",
            "0x88A8": "QinQ"
          }
        },
        {
          "name": "inner_payload",
          "type": "switch",
          "switch_on": "inner_ether_type",
          "cases": {
            "0x0800": [ { "name": "ipv4", "type": "struct", "struct": "ipv4" } ],
            "0x0806": [ { "name": "arp",  "type": "struct", "struct": "arp"  } ],
            "0x86DD": [ { "name": "ipv6", "type": "struct", "struct": "ipv6" } ],
            "0x8100": [ { "name": "vlan_inner",  "type": "struct", "struct": "vlan_tag" } ],
            "0x88A8": [ { "name": "qinq_inner",  "type": "struct", "struct": "vlan_tag" } ]
          },
          "default": [
            { "name": "raw", "type": "bytes", "length": "rest" }
          ]
        }
      ]
    },

    "arp": {
      "desc": "ARP / RARP 报文(动态 hw_len / proto_len)",
      "fields": [
        { "name": "hw_type",    "type": "uint16", "desc": "硬件类型",
          "enum": { "1": "Ethernet" } },
        { "name": "proto_type", "type": "uint16", "desc": "协议类型",
          "enum": { "0x0800": "IPv4" } },
        { "name": "hw_len",     "type": "uint8",  "desc": "硬件地址长度" },
        { "name": "proto_len",  "type": "uint8",  "desc": "协议地址长度" },
        { "name": "opcode",     "type": "uint16", "desc": "操作码",
          "enum": {
            "1": "ARP Request", "2": "ARP Reply",
            "3": "RARP Request", "4": "RARP Reply"
          }
        },
        { "name": "sender_hw_addr",    "type": "bytes", "length": "hw_len",
          "desc": "发送方硬件地址" },
        { "name": "sender_proto_addr", "type": "bytes", "length": "proto_len",
          "desc": "发送方协议地址" },
        { "name": "target_hw_addr",    "type": "bytes", "length": "hw_len",
          "desc": "目标硬件地址" },
        { "name": "target_proto_addr", "type": "bytes", "length": "proto_len",
          "desc": "目标协议地址" }
      ]
    },

    "ipv4": {
      "desc": "IPv4 头部 + 上层分派",
      "fields": [
        {
          "name": "version_ihl", "type": "uint8", "desc": "版本和首部长度",
          "bits": [
            { "name": "version", "hi": 7, "lo": 4 },
            { "name": "ihl",     "hi": 3, "lo": 0 }
          ]
        },
        {
          "name": "tos", "type": "uint8", "desc": "服务类型",
          "bits": [
            { "name": "dscp", "hi": 7, "lo": 2 },
            { "name": "ecn",  "hi": 1, "lo": 0 }
          ]
        },
        { "name": "total_length",   "type": "uint16", "desc": "总长度" },
        { "name": "identification", "type": "uint16", "desc": "标识" },
        {
          "name": "flags_frag", "type": "uint16", "desc": "标志和片偏移",
          "bits": [
            { "name": "reserved",    "hi": 15, "lo": 15 },
            { "name": "df",          "hi": 14, "lo": 14, "desc": "不分片" },
            { "name": "mf",          "hi": 13, "lo": 13, "desc": "更多分片" },
            { "name": "frag_offset", "hi": 12, "lo": 0  }
          ]
        },
        { "name": "ttl", "type": "uint8", "desc": "生存时间" },
        {
          "name": "protocol", "type": "uint8", "desc": "上层协议",
          "enum": {
            "1": "ICMP", "2": "IGMP", "6": "TCP", "17": "UDP",
            "41": "IPv6-in-IPv4", "47": "GRE", "50": "ESP",
            "51": "AH", "89": "OSPF"
          }
        },
        { "name": "checksum", "type": "uint16", "desc": "首部校验和" },
        { "name": "src_ip",   "type": "ipv4",   "desc": "源 IP" },
        { "name": "dst_ip",   "type": "ipv4",   "desc": "目的 IP" },
        { "name": "options",  "type": "bytes",  "length": "ihl*4 - 20",
          "desc": "IP 选项" },
        {
          "name": "payload",
          "type": "switch",
          "switch_on": "protocol",
          "cases": {
            "1":  [ { "name": "icmp", "type": "struct", "struct": "icmp"     } ],
            "6":  [ { "name": "tcp",  "type": "struct", "struct": "tcp_ipv4" } ],
            "17": [ { "name": "udp",  "type": "struct", "struct": "udp_ipv4" } ]
          },
          "default": [
            { "name": "raw", "type": "bytes", "length": "rest" }
          ]
        }
      ]
    },

    "ipv6": {
      "desc": "IPv6 基本头 + 下一层分派(含扩展头链)",
      "fields": [
        {
          "name": "version_tc_flow", "type": "uint32",
          "desc": "版本 / 流量类别 / 流标签",
          "bits": [
            { "name": "version",       "hi": 31, "lo": 28 },
            { "name": "traffic_class", "hi": 27, "lo": 20 },
            { "name": "flow_label",    "hi": 19, "lo": 0  }
          ]
        },
        { "name": "payload_length", "type": "uint16",
          "desc": "有效载荷长度(含扩展头,不含基本头)" },
        {
          "name": "next_header", "type": "uint8", "desc": "下一个头部",
          "enum": {
            "0":  "Hop-by-Hop Options",
            "6":  "TCP",
            "17": "UDP",
            "43": "Routing",
            "44": "Fragment",
            "58": "ICMPv6",
            "59": "No Next Header",
            "60": "Destination Options"
          }
        },
        { "name": "hop_limit", "type": "uint8", "desc": "跳数限制" },
        { "name": "src_ip",    "type": "ipv6",  "desc": "源 IPv6 地址" },
        { "name": "dst_ip",    "type": "ipv6",  "desc": "目的 IPv6 地址" },
        { "name": "dispatch",  "type": "struct", "struct": "ipv6_dispatch",
          "desc": "按 next_header 分派下一层" }
      ]
    },

    "ipv6_dispatch": {
      "desc": "IPv6 下一层分派(基本头与扩展头共用)",
      "fields": [
        {
          "name": "layer",
          "type": "switch",
          "switch_on": "next_header",
          "cases": {
            "0":  [ { "name": "hopbyhop", "type": "struct", "struct": "ipv6_ext_options"  } ],
            "43": [ { "name": "routing",  "type": "struct", "struct": "ipv6_ext_routing"  } ],
            "44": [ { "name": "fragment", "type": "struct", "struct": "ipv6_ext_fragment" } ],
            "60": [ { "name": "destopt",  "type": "struct", "struct": "ipv6_ext_options"  } ],
            "6":  [ { "name": "tcp",      "type": "struct", "struct": "tcp_ipv6"         } ],
            "17": [ { "name": "udp",      "type": "struct", "struct": "udp_ipv6"         } ],
            "58": [ { "name": "icmpv6",   "type": "struct", "struct": "icmpv6"           } ]
          },
          "default": [
            { "name": "raw", "type": "bytes", "length": "rest",
              "desc": "未知下一头部载荷" }
          ]
        }
      ]
    },

    "ipv6_ext_options": {
      "desc": "IPv6 Hop-by-Hop / Destination Options 扩展头(递归)",
      "fields": [
        {
          "name": "next_header", "type": "uint8", "desc": "下一头部",
          "enum": {
            "0":  "Hop-by-Hop", "6": "TCP", "17": "UDP",
            "43": "Routing", "44": "Fragment", "58": "ICMPv6",
            "59": "No Next Header", "60": "Destination Options"
          }
        },
        { "name": "hdr_ext_len", "type": "uint8",
          "desc": "扩展长度(单位 8 字节,不含前 8 字节)" },
        { "name": "options_data", "type": "bytes", "length": "hdr_ext_len*8 + 6",
          "desc": "选项数据" },
        { "name": "dispatch", "type": "struct", "struct": "ipv6_dispatch" }
      ]
    },

    "ipv6_ext_routing": {
      "desc": "IPv6 Routing 扩展头(递归)",
      "fields": [
        {
          "name": "next_header", "type": "uint8", "desc": "下一头部",
          "enum": {
            "0": "Hop-by-Hop", "6": "TCP", "17": "UDP", "43": "Routing",
            "44": "Fragment", "58": "ICMPv6", "60": "Destination Options"
          }
        },
        { "name": "routing_type",   "type": "uint8", "desc": "路由类型" },
        { "name": "segments_left",  "type": "uint8", "desc": "剩余段数" },
        { "name": "hdr_ext_len",    "type": "uint8",
          "desc": "扩展长度(单位 8 字节)" },
        { "name": "type_data", "type": "bytes", "length": "hdr_ext_len*8 + 4",
          "desc": "类型相关数据" },
        { "name": "dispatch", "type": "struct", "struct": "ipv6_dispatch" }
      ]
    },

    "ipv6_ext_fragment": {
      "desc": "IPv6 Fragment 扩展头(固定 8 字节)",
      "fields": [
        {
          "name": "next_header", "type": "uint8", "desc": "下一头部",
          "enum": {
            "6": "TCP", "17": "UDP", "58": "ICMPv6",
            "43": "Routing", "60": "Destination Options"
          }
        },
        { "name": "reserved", "type": "uint8", "desc": "保留" },
        {
          "name": "frag_info", "type": "uint16", "desc": "分片信息",
          "bits": [
            { "name": "frag_offset", "hi": 15, "lo": 3, "desc": "片偏移(单位 8 字节)" },
            { "name": "res",         "hi": 2,  "lo": 1, "desc": "保留" },
            { "name": "m_flag",      "hi": 0,  "lo": 0, "desc": "更多分片 M" }
          ]
        },
        { "name": "identification", "type": "uint32", "desc": "分片标识" },
        { "name": "dispatch", "type": "struct", "struct": "ipv6_dispatch" }
      ]
    },

    "icmp": {
      "desc": "ICMP 报文",
      "fields": [
        {
          "name": "type", "type": "uint8", "desc": "ICMP 类型",
          "enum": {
            "0": "Echo Reply", "3": "Destination Unreachable",
            "4": "Source Quench", "5": "Redirect", "8": "Echo Request",
            "9": "Router Advertisement", "10": "Router Solicitation",
            "11": "Time Exceeded", "12": "Parameter Problem",
            "13": "Timestamp Request", "14": "Timestamp Reply"
          }
        },
        { "name": "code",     "type": "uint8",  "desc": "ICMP 代码" },
        { "name": "checksum", "type": "uint16", "desc": "校验和" },
        {
          "name": "body",
          "type": "switch",
          "switch_on": "type",
          "cases": {
            "0": [
              { "name": "identifier", "type": "uint16" },
              { "name": "sequence",   "type": "uint16" },
              { "name": "data",       "type": "bytes", "length": "rest" }
            ],
            "8": [
              { "name": "identifier", "type": "uint16" },
              { "name": "sequence",   "type": "uint16" },
              { "name": "data",       "type": "bytes", "length": "rest" }
            ],
            "3": [
              { "name": "unused", "type": "uint32" },
              { "name": "original_datagram", "type": "bytes", "length": "rest" }
            ],
            "11": [
              { "name": "unused", "type": "uint32" },
              { "name": "original_datagram", "type": "bytes", "length": "rest" }
            ],
            "5": [
              { "name": "gateway_ip", "type": "ipv4" },
              { "name": "original_datagram", "type": "bytes", "length": "rest" }
            ]
          },
          "default": [ { "name": "raw", "type": "bytes", "length": "rest" } ]
        }
      ]
    },

    "icmpv6": {
      "desc": "ICMPv6 报文",
      "fields": [
        {
          "name": "type", "type": "uint8", "desc": "ICMPv6 类型",
          "enum": {
            "1": "Destination Unreachable", "2": "Packet Too Big",
            "3": "Time Exceeded", "4": "Parameter Problem",
            "128": "Echo Request", "129": "Echo Reply",
            "133": "Router Solicitation", "134": "Router Advertisement",
            "135": "Neighbor Solicitation", "136": "Neighbor Advertisement",
            "137": "Redirect"
          }
        },
        { "name": "code",     "type": "uint8",  "desc": "代码" },
        { "name": "checksum", "type": "uint16", "desc": "校验和" },
        {
          "name": "body",
          "type": "switch",
          "switch_on": "type",
          "cases": {
            "128": [
              { "name": "identifier", "type": "uint16" },
              { "name": "sequence",   "type": "uint16" },
              { "name": "data",       "type": "bytes", "length": "rest" }
            ],
            "129": [
              { "name": "identifier", "type": "uint16" },
              { "name": "sequence",   "type": "uint16" },
              { "name": "data",       "type": "bytes", "length": "rest" }
            ],
            "133": [
              { "name": "reserved", "type": "uint32" },
              { "name": "options",  "type": "bytes", "length": "rest" }
            ],
            "134": [
              { "name": "cur_hop_limit", "type": "uint8" },
              {
                "name": "ra_flags", "type": "uint8",
                "bits": [
                  { "name": "m_flag", "hi": 7, "lo": 7 },
                  { "name": "o_flag", "hi": 6, "lo": 6 },
                  { "name": "reserved", "hi": 5, "lo": 0 }
                ]
              },
              { "name": "router_lifetime", "type": "uint16" },
              { "name": "reachable_time",  "type": "uint32" },
              { "name": "retrans_timer",   "type": "uint32" },
              { "name": "options", "type": "bytes", "length": "rest" }
            ],
            "135": [
              { "name": "reserved", "type": "uint32" },
              { "name": "target_address", "type": "ipv6" },
              { "name": "options", "type": "bytes", "length": "rest" }
            ],
            "136": [
              {
                "name": "na_flags", "type": "uint32",
                "bits": [
                  { "name": "r_flag", "hi": 31, "lo": 31 },
                  { "name": "s_flag", "hi": 30, "lo": 30 },
                  { "name": "o_flag", "hi": 29, "lo": 29 },
                  { "name": "reserved", "hi": 28, "lo": 0 }
                ]
              },
              { "name": "target_address", "type": "ipv6" },
              { "name": "options", "type": "bytes", "length": "rest" }
            ],
            "137": [
              { "name": "reserved", "type": "uint32" },
              { "name": "target_address",      "type": "ipv6" },
              { "name": "destination_address", "type": "ipv6" },
              { "name": "options", "type": "bytes", "length": "rest" }
            ],
            "1": [
              { "name": "unused", "type": "uint32" },
              { "name": "original_datagram", "type": "bytes", "length": "rest" }
            ],
            "2": [
              { "name": "mtu", "type": "uint32" },
              { "name": "original_datagram", "type": "bytes", "length": "rest" }
            ],
            "3": [
              { "name": "unused", "type": "uint32" },
              { "name": "original_datagram", "type": "bytes", "length": "rest" }
            ],
            "4": [
              { "name": "pointer", "type": "uint32" },
              { "name": "original_datagram", "type": "bytes", "length": "rest" }
            ]
          },
          "default": [
            { "name": "unused", "type": "uint32" },
            { "name": "original_datagram", "type": "bytes", "length": "rest" }
          ]
        }
      ]
    },

    "tcp_ipv4": {
      "desc": "TCP 报文(IPv4 承载,payload 精确切分)",
      "fields": [
        { "name": "src_port", "type": "uint16", "desc": "源端口" },
        { "name": "dst_port", "type": "uint16", "desc": "目的端口" },
        { "name": "seq",      "type": "uint32", "desc": "序列号" },
        { "name": "ack_num",  "type": "uint32", "desc": "确认号" },
        {
          "name": "data_offset_reserved", "type": "uint8",
          "bits": [
            { "name": "data_offset", "hi": 7, "lo": 4 },
            { "name": "reserved",    "hi": 3, "lo": 1 },
            { "name": "ns",          "hi": 0, "lo": 0 }
          ]
        },
        {
          "name": "flags", "type": "uint8", "desc": "标志位",
          "bits": [
            { "name": "cwr", "hi": 7, "lo": 7 },
            { "name": "ece", "hi": 6, "lo": 6 },
            { "name": "urg", "hi": 5, "lo": 5 },
            { "name": "ack", "hi": 4, "lo": 4 },
            { "name": "psh", "hi": 3, "lo": 3 },
            { "name": "rst", "hi": 2, "lo": 2 },
            { "name": "syn", "hi": 1, "lo": 1 },
            { "name": "fin", "hi": 0, "lo": 0 }
          ]
        },
        { "name": "window",         "type": "uint16", "desc": "窗口大小" },
        { "name": "checksum",       "type": "uint16", "desc": "校验和" },
        { "name": "urgent_pointer", "type": "uint16", "desc": "紧急指针" },
        { "name": "options", "type": "bytes", "length": "data_offset*4 - 20",
          "desc": "TCP 选项" },
        {
          "name": "payload",
          "type": "bytes",
          "length": "total_length - ihl*4 - data_offset*4",
          "desc": "TCP 载荷(精确切分,去除以太网填充)"
        }
      ]
    },

    "udp_ipv4": {
      "desc": "UDP 报文(IPv4 承载,payload 精确切分)",
      "fields": [
        { "name": "src_port", "type": "uint16", "desc": "源端口" },
        { "name": "dst_port", "type": "uint16", "desc": "目的端口" },
        { "name": "length",   "type": "uint16", "desc": "UDP 长度(含首部)" },
        { "name": "checksum", "type": "uint16", "desc": "校验和" },
        {
          "name": "payload", "type": "bytes", "length": "length - 8",
          "desc": "UDP 载荷(精确切分,去除以太网填充)"
        }
      ]
    },

    "tcp_ipv6": {
      "desc": "TCP 报文(IPv6 无扩展头承载,payload 精确切分)",
      "fields": [
        { "name": "src_port", "type": "uint16", "desc": "源端口" },
        { "name": "dst_port", "type": "uint16", "desc": "目的端口" },
        { "name": "seq",      "type": "uint32", "desc": "序列号" },
        { "name": "ack_num",  "type": "uint32", "desc": "确认号" },
        {
          "name": "data_offset_reserved", "type": "uint8",
          "bits": [
            { "name": "data_offset", "hi": 7, "lo": 4 },
            { "name": "reserved",    "hi": 3, "lo": 1 },
            { "name": "ns",          "hi": 0, "lo": 0 }
          ]
        },
        {
          "name": "flags", "type": "uint8", "desc": "标志位",
          "bits": [
            { "name": "cwr", "hi": 7, "lo": 7 },
            { "name": "ece", "hi": 6, "lo": 6 },
            { "name": "urg", "hi": 5, "lo": 5 },
            { "name": "ack", "hi": 4, "lo": 4 },
            { "name": "psh", "hi": 3, "lo": 3 },
            { "name": "rst", "hi": 2, "lo": 2 },
            { "name": "syn", "hi": 1, "lo": 1 },
            { "name": "fin", "hi": 0, "lo": 0 }
          ]
        },
        { "name": "window",         "type": "uint16", "desc": "窗口大小" },
        { "name": "checksum",       "type": "uint16", "desc": "校验和" },
        { "name": "urgent_pointer", "type": "uint16", "desc": "紧急指针" },
        { "name": "options", "type": "bytes", "length": "data_offset*4 - 20",
          "desc": "TCP 选项" },
        {
          "name": "payload", "type": "bytes",
          "length": "payload_length - data_offset*4",
          "desc": "TCP 载荷(假设无扩展头;有扩展头时长度偏大)"
        }
      ]
    },

    "udp_ipv6": {
      "desc": "UDP 报文(IPv6 承载,payload 精确切分)",
      "fields": [
        { "name": "src_port", "type": "uint16", "desc": "源端口" },
        { "name": "dst_port", "type": "uint16", "desc": "目的端口" },
        { "name": "length",   "type": "uint16", "desc": "UDP 长度(含首部)" },
        { "name": "checksum", "type": "uint16", "desc": "校验和" },
        {
          "name": "payload", "type": "bytes", "length": "length - 8",
          "desc": "UDP 载荷(精确切分)"
        }
      ]
    }
  },

  "fields": [
    { "name": "dst_mac", "type": "mac", "desc": "目的 MAC" },
    { "name": "src_mac", "type": "mac", "desc": "源 MAC" },
    {
      "name": "ether_type", "type": "uint16", "desc": "以太网类型",
      "enum": {
        "0x0800": "IPv4",
        "0x0806": "ARP",
        "0x8035": "RARP",
        "0x8100": "VLAN (802.1Q)",
        "0x86DD": "IPv6",
        "0x88A8": "QinQ (802.1ad)",
        "0x8847": "MPLS Unicast",
        "0x8848": "MPLS Multicast",
        "0x8863": "PPPoE Discovery",
        "0x8864": "PPPoE Session"
      }
    },
    {
      "name": "payload",
      "type": "switch",
      "switch_on": "ether_type",
      "cases": {
        "0x0800": [ { "name": "ipv4", "type": "struct", "struct": "ipv4" } ],
        "0x0806": [ { "name": "arp",  "type": "struct", "struct": "arp"  } ],
        "0x86DD": [ { "name": "ipv6", "type": "struct", "struct": "ipv6" } ],
        "0x8100": [ { "name": "vlan", "type": "struct", "struct": "vlan_tag" } ],
        "0x88A8": [ { "name": "qinq", "type": "struct", "struct": "vlan_tag" } ]
      },
      "default": [
        { "name": "raw_payload", "type": "bytes", "length": "rest",
          "desc": "未识别或暂不解析的载荷" }
      ]
    }
  ]
}

十三、编写建议

命名规则:字段名建议使用小写加下划线,避免与 Python 关键字冲突(如 class、def)。位域子字段名不要与其他字段重名,否则会互相覆盖上下文。

字节序:以太网/IP/TCP/UDP 等标准协议使用大端("big");某些芯片内部寄存器使用小端("little"),需按实际报文设置。

位域方向:hi 是高位索引,lo 是低位索引,均以整数值的最低位为 0。对于 uint32,位 31 是最高位。

长度表达式:表达式中可以引用已解析字段(含位域子字段),但必须确保引用字段在表达式所在字段之前已解析。

switch 的 default:如果 switch_on 的取值可能超出已知范围,务必提供 default 分支,否则解析器会报错终止。

结构体复用:同一种报文头(如 IPv4)可被多处 struct 引用,definitions 中只定义一次。

调试技巧:如果解析报错,可以先注释掉后面的字段,只保留基础部分,逐步增加复杂度。解析器报错信息会指出出错字段名和偏移。

验证配置:用已知的十六进制报文测试,对照预期字段值检查解析结果。解析报告末尾会显示原始十六进制和 ASCII,便于人工核对。

十四、命令行使用

bash 复制代码
# 从文件读取十六进制文本
python packet_parser.py -c ethernet.json -i frame.hex -o result.txt

# 直接传入十六进制串
python packet_parser.py -c ethernet.json -d "FFFFFFFFFFFF000C29A1B2C30800..." -o result.txt

# 读取原始二进制文件
python packet_parser.py -c ethernet.json -i frame.bin --binary -o result.txt

# 同时把报告打印到终端
python packet_parser.py -c ethernet.json -i frame.hex -o result.txt --stdout

参数 说明

  • -c / --config 必填,配置文件路径
  • -i / --input 输入文件路径
  • -d / --data 直接传入十六进制字符串(与 -i 互斥)
  • --binary 配合 -i 使用,表示输入是原始二进制
  • -o / --output 输出文件路径,默认 parse_result.txt
  • --stdout 同时打印到终端

十五、常见问题

Q1:length 表达式报"引用了尚未解析的字段"怎么办?

检查引用字段是否在当前位置之前定义。位域子字段也算已解析字段,可以直接引用。

Q2:switch 匹配不到任何 case 怎么办?

在 cases 中检查键的写法(十六进制需加 0x 前缀),或添加 default 分支。

Q3:解析结束后提示"仍有 N 字节未使用"是什么意思?

说明配置只解析了报文的一部分,剩余字节未被任何字段消费。可能是帧填充、尾部数据,或配置遗漏。可以在最后加一个 "length": "rest" 的 bytes 字段收集剩余内容。

Q4:如何解析变长数组?

使用 array 类型,count 引用前面已解析的计数字段。

Q5:如何按字节流解析 flit 结构?

用 array 配合 count 逐项解析;如果每个 flit 大小固定,可以把 item 定义为一个 struct。

报文格式解析脚本

一、脚本

python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
packet_parser.py ------ 通用十六进制报文解析器

根据 JSON 格式的报文格式配置文件,把十六进制字节流解析成一个个字段,
并将结果输出到文本文件。

用法::

    python packet_parser.py -c config.json -i frame.hex -o result.txt
    python packet_parser.py -c config.json -d "ffffffff..." -o result.txt
    python packet_parser.py -c config.json -i frame.bin --binary -o result.txt
    python packet_parser.py -c config.json -i frame.hex -o result.txt --stdout

配置文件语法详见随附的《报文格式配置文件编写说明书》。
"""

from __future__ import annotations

import argparse
import datetime
import json
import re
import socket
import sys
from typing import Any, Dict, List, Optional, Tuple


# ----------------------------------------------------------------------
# 异常
# ----------------------------------------------------------------------
class ParseError(Exception):
    """解析过程中的错误。"""
    pass


# ----------------------------------------------------------------------
# 常量
# ----------------------------------------------------------------------
NUM_TYPES: Dict[str, Tuple[int, bool]] = {
    "uint8":   (1,  False), "uint16":  (2,  False),
    "uint24":  (3,  False), "uint32":  (4,  False),
    "uint64":  (8,  False), "uint128": (16, False),
    "int8":    (1,  True),  "int16":   (2,  True),
    "int24":   (3,  True),  "int32":   (4,  True),
    "int64":   (8,  True),  "int128":  (16, True),
}

ENDIAN_ALIAS = {
    "big": "big", "be": "big",
    "little": "little", "le": "little",
}

BYTE_TYPES = ("bytes", "hex", "raw")

REST_KEYWORDS = ("rest", "remaining", "*", "auto")


# ----------------------------------------------------------------------
# 通用工具
# ----------------------------------------------------------------------
def clean_hex(text: str) -> bytes:
    """把各种写法的十六进制串转成 bytes。"""
    s = re.sub(r"(?i)\b0x", "", text)
    s = re.sub(r"[\s,;:|_\-]+", "", s)
    if not s:
        raise ParseError("输入内容为空")
    if len(s) % 2:
        raise ParseError(f"十六进制字符串长度为奇数({len(s)}),无法按字节解析")
    try:
        return bytes.fromhex(s)
    except ValueError as exc:
        raise ParseError(f"包含非法的十六进制字符: {exc}") from exc


def disp_width(s: str) -> int:
    """粗略计算字符串的显示宽度(中文按 2 计)。"""
    return sum(2 if ord(ch) > 0x2E7F else 1 for ch in s)


def pad_to(s: str, width: int) -> str:
    """按显示宽度左对齐补空格。"""
    return s + " " * max(1, width - disp_width(s))


def fmt_any(v: Any) -> str:
    """把任意值格式化成简短字符串。"""
    if isinstance(v, bool):
        return str(v)
    if isinstance(v, int):
        return f"0x{v:X}"
    if isinstance(v, (bytes, bytearray)):
        return v.hex().upper()
    return str(v)


# ----------------------------------------------------------------------
# 字节读取器
# ----------------------------------------------------------------------
class Reader:
    """带游标和边界检查的字节读取器。"""

    def __init__(self, data: bytes):
        self.data = data
        self.pos = 0
        self.size = len(data)

    @property
    def remaining(self) -> int:
        return self.size - self.pos

    def read(self, n: int, what: str = "") -> bytes:
        if n < 0:
            raise ParseError(f"字段 '{what}' 长度为负数({n})")
        if self.pos + n > self.size:
            raise ParseError(
                f"数据不足:字段 '{what}' 需要 {n} 字节,"
                f"但偏移 0x{self.pos:04X} 处只剩 {self.remaining} 字节"
            )
        chunk = self.data[self.pos:self.pos + n]
        self.pos += n
        return chunk


def make_record(path: str, ftype: str, offset: int, length: int,
                value: Any, display: str, desc: str, level: int) -> Dict[str, Any]:
    return {
        "path": path,
        "type": ftype,
        "offset": offset,
        "length": length,
        "value": value,
        "display": display,
        "desc": desc,
        "level": level,
    }


# ----------------------------------------------------------------------
# 核心解析器
# ----------------------------------------------------------------------
class PacketParser:
    """按配置解析报文。"""

    def __init__(self, config: Dict[str, Any]):
        if not isinstance(config, dict) or "fields" not in config:
            raise ParseError("配置文件缺少顶层 'fields' 定义")
        self.cfg = config
        self.defs: Dict[str, Any] = config.get("definitions", {})
        self.root_fields: List[Dict[str, Any]] = config["fields"]
        self.endian = ENDIAN_ALIAS.get(
            str(config.get("endian", "big")).lower(), "big")
        self.reader: Optional[Reader] = None

    # ---------------- 对外接口 ----------------
    def parse_bytes(self, data: bytes) -> Tuple[List[Dict[str, Any]], int]:
        """返回 (字段记录列表, 剩余未解析字节数)。"""
        self.reader = Reader(data)
        ctx: Dict[str, Any] = {}
        records: List[Dict[str, Any]] = []
        self._parse_fields(self.root_fields, ctx, records, prefix="", level=0)
        return records, self.reader.remaining

    def parse_hex(self, hexstr: str) -> Tuple[List[Dict[str, Any]], int]:
        return self.parse_bytes(clean_hex(hexstr))

    # ---------------- 内部实现 ----------------
    def _parse_fields(self, fields, ctx, records, prefix, level):
        for f in fields:
            self._parse_field(f, ctx, records, prefix, level)

    def _parse_field(self, f: Dict[str, Any], ctx: Dict[str, Any],
                     records: List[Dict[str, Any]],
                     prefix: str, level: int) -> None:
        if "name" not in f:
            raise ParseError(f"字段缺少 'name' 属性: {f}")

        name = f["name"]
        ftype = f.get("type", "bytes")
        path = prefix + name
        start = self.reader.pos
        desc = f.get("desc", "")

        # ---------- 整数 / 位域 ----------
        if ftype in NUM_TYPES:
            size, signed = NUM_TYPES[ftype]
            raw = self.reader.read(size, path)
            value = int.from_bytes(raw, self._endian_of(f), signed=signed)
            records.append(make_record(
                path, ftype, start, size, value,
                self._format_value(f, value, size), desc, level))
            ctx[name] = value

            for b in f.get("bits", []):
                if "name" not in b:
                    raise ParseError(f"字段 '{path}' 的位域缺少 'name'")
                hi, lo = int(b["hi"]), int(b["lo"])
                if not (0 <= lo <= hi <= size * 8 - 1):
                    raise ParseError(
                        f"字段 '{path}' 的位域 {b['name']} 范围非法: [{hi}:{lo}]")
                width = hi - lo + 1
                bval = (value >> lo) & ((1 << width) - 1)
                records.append(make_record(
                    f"{path}.{b['name']}",
                    f"bits[{hi}:{lo}]",
                    start, size, bval,
                    self._format_value(b, bval, (width + 7) // 8),
                    b.get("desc", ""), level + 1))
                ctx[b["name"]] = bval
            return

        # ---------- 原始字节 ----------
        if ftype in BYTE_TYPES:
            n = self._resolve_length(f, ctx, path)
            raw = self.reader.read(n, path)
            hexstr = raw.hex().upper()
            display = hexstr if hexstr else "<空>"
            if len(display) > 96:
                display = display[:96] + f"... (共 {n} 字节)"
            records.append(make_record(
                path, "bytes", start, n, hexstr, display, desc, level))
            ctx[name] = raw
            return

        # ---------- 字符串 ----------
        if ftype == "string":
            n = self._resolve_length(f, ctx, path)
            raw = self.reader.read(n, path)
            enc = f.get("encoding", "utf-8")
            text = raw.decode(enc, errors="replace").rstrip("\x00")
            records.append(make_record(
                path, "string", start, n, text, repr(text), desc, level))
            ctx[name] = text
            return

        # ---------- MAC ----------
        if ftype == "mac":
            raw = self.reader.read(6, path)
            s = ":".join(f"{b:02X}" for b in raw)
            records.append(make_record(
                path, "mac", start, 6, s, s, desc, level))
            ctx[name] = s
            return

        # ---------- IPv4 ----------
        if ftype == "ipv4":
            raw = self.reader.read(4, path)
            s = ".".join(str(b) for b in raw)
            records.append(make_record(
                path, "ipv4", start, 4, s, s, desc, level))
            ctx[name] = s
            return

        # ---------- IPv6 ----------
        if ftype == "ipv6":
            raw = self.reader.read(16, path)
            try:
                s = socket.inet_ntop(socket.AF_INET6, raw)
            except OSError:
                s = raw.hex().upper()
            records.append(make_record(
                path, "ipv6", start, 16, s, s, desc, level))
            ctx[name] = s
            return

        # ---------- 填充 ----------
        if ftype in ("padding", "pad"):
            n = self._resolve_length(f, ctx, path)
            raw = self.reader.read(n, path)
            records.append(make_record(
                path, "padding", start, n,
                raw.hex().upper(), f"跳过 {n} 字节", desc, level))
            return

        # ---------- 结构体 ----------
        if ftype == "struct":
            ref = f.get("struct") or f.get("ref")
            if ref not in self.defs:
                raise ParseError(f"字段 '{path}' 引用了未定义的结构体: {ref}")
            sub = self.defs[ref]
            idx = len(records)
            label = f"<{ref}>" + (f" {sub['desc']}" if sub.get("desc") else "")
            records.append(make_record(
                path, f"struct<{ref}>", start, 0, ref, label, desc, level))
            self._parse_fields(sub.get("fields", []), ctx, records,
                               path + ".", level + 1)
            records[idx]["length"] = self.reader.pos - start
            return

        # ---------- 条件分支(支持单字段或多字段组合)----------
        if ftype == "switch":
            on = f.get("switch_on") or f.get("switch")
            if on is None:
                raise ParseError(f"字段 '{path}': switch 缺少 switch_on 属性")

            if isinstance(on, str):
                on_fields = [on]
            elif isinstance(on, (list, tuple)):
                on_fields = [str(x) for x in on]
                if not on_fields:
                    raise ParseError(f"字段 '{path}': switch_on 列表为空")
            else:
                raise ParseError(
                    f"字段 '{path}': switch_on 必须是字符串或字符串列表")

            actual_values: List[Any] = []
            for fname in on_fields:
                if fname not in ctx:
                    raise ParseError(
                        f"字段 '{path}' 的 switch_on 引用了尚未解析的字段: "
                        f"{fname}")
                actual_values.append(ctx[fname])

            cases = f.get("cases", {})
            chosen, chosen_key = None, None

            if len(on_fields) == 1:
                single = actual_values[0]
                for k, v in cases.items():
                    if self._match_value(single, k):
                        chosen, chosen_key = v, k
                        break
                display_key = f"{on_fields[0]}={fmt_any(single)}"
                stored_value: Any = single
            else:
                for k, v in cases.items():
                    parts = [p.strip() for p in str(k).split(",")]
                    if len(parts) != len(on_fields):
                        continue
                    if all(self._match_value(a, p)
                           for a, p in zip(actual_values, parts)):
                        chosen, chosen_key = v, k
                        break
                display_key = ", ".join(
                    f"{n}={fmt_any(v)}"
                    for n, v in zip(on_fields, actual_values))
                stored_value = dict(zip(on_fields, actual_values))

            if chosen is None:
                chosen, chosen_key = f.get("default"), "default"

            idx = len(records)
            records.append(make_record(
                path, "switch", start, 0, stored_value,
                f"<{display_key} -> case {chosen_key}>", desc, level))

            if chosen is None:
                raise ParseError(
                    f"字段 '{path}': switch_on={display_key} "
                    f"没有匹配的 case,也没有配置 default")

            if not isinstance(chosen, list):
                chosen = [chosen]
            self._parse_fields(chosen, ctx, records, path + ".", level + 1)
            records[idx]["length"] = self.reader.pos - start
            return

        # ---------- 数组 ----------
        if ftype == "array":
            item = f.get("item")
            if not isinstance(item, dict):
                raise ParseError(f"字段 '{path}': array 需要 'item' 定义")
            if "count" not in f:
                raise ParseError(f"字段 '{path}': array 需要 'count'")
            cnt_expr = f["count"]
            count = (int(self._eval(cnt_expr, ctx))
                     if isinstance(cnt_expr, str)
                     else int(cnt_expr))
            if count < 0:
                raise ParseError(f"字段 '{path}': array count 为负数({count})")

            idx = len(records)
            records.append(make_record(
                path, "array", start, 0, count,
                f"<{count} 项>", desc, level))
            base_name = item.get("name", "item")
            for i in range(count):
                sub_item = dict(item)
                sub_item["name"] = f"{base_name}[{i}]"
                self._parse_field(sub_item, ctx, records,
                                  path + ".", level + 1)
            records[idx]["length"] = self.reader.pos - start
            return

        raise ParseError(f"字段 '{path}' 使用了不支持的类型: {ftype}")

    # ---------------- 辅助方法 ----------------
    def _endian_of(self, f: Dict[str, Any]) -> str:
        e = f.get("endian")
        if e is None:
            return self.endian
        return ENDIAN_ALIAS.get(str(e).lower(), self.endian)

    def _resolve_length(self, f: Dict[str, Any], ctx: Dict[str, Any],
                        path: str, default: Optional[int] = None) -> int:
        L = f.get("length", None)
        if L is None:
            return self.reader.remaining if default is None else default
        if isinstance(L, str):
            if L.strip().lower() in REST_KEYWORDS:
                return self.reader.remaining
            n = int(self._eval(L, ctx))
        else:
            n = int(L)
        if n < 0:
            raise ParseError(
                f"字段 '{path}': 计算出的长度为负数({n}),"
                f"请检查表达式 '{L}'")
        return n

    @staticmethod
    def _eval(expr: str, ctx: Dict[str, Any]) -> Any:
        """在受控环境中求值表达式。"""
        if "__" in expr:
            raise ParseError(f"表达式中含有非法内容: {expr}")
        try:
            return eval(expr, {"__builtins__": {}}, dict(ctx))  # noqa: S307
        except Exception as exc:  # noqa: BLE001
            raise ParseError(f"表达式求值失败 '{expr}': {exc}") from exc

    @staticmethod
    def _to_int(k: Any) -> Optional[int]:
        if isinstance(k, bool):
            return int(k)
        if isinstance(k, int):
            return k
        if isinstance(k, str):
            try:
                return int(k.strip(), 0)
            except ValueError:
                return None
        return None

    @staticmethod
    def _match_value(actual: Any, key_str: Any) -> bool:
        """判断已解析字段的实际值是否匹配配置中的 case 键。"""
        key = str(key_str).strip()

        if isinstance(actual, bool):
            parsed = PacketParser._to_int(key)
            if parsed is not None and parsed == int(actual):
                return True
            return key.lower() == str(actual).lower()

        if isinstance(actual, int):
            parsed = PacketParser._to_int(key)
            return parsed is not None and parsed == actual

        if isinstance(actual, str):
            return actual == key

        if isinstance(actual, (bytes, bytearray)):
            norm = re.sub(r"[\s:,_\-]", "", key).upper()
            return actual.hex().upper() == norm

        return str(actual) == key

    def _format_value(self, f: Dict[str, Any], value: int, size: int) -> str:
        fmt = str(f.get("format", "")).lower()
        if fmt == "dec":
            disp = str(value)
        elif fmt == "hex":
            disp = f"0x{value:0{size * 2}X}"
        else:
            disp = f"0x{value:0{size * 2}X} ({value})"

        enum = f.get("enum")
        if enum:
            table = {}
            for k, v in enum.items():
                iv = self._to_int(k)
                if iv is not None:
                    table[iv] = v
            if value in table:
                disp += f" = {table[value]}"
        return disp


# ----------------------------------------------------------------------
# 报告格式化
# ----------------------------------------------------------------------
def format_report(records: List[Dict[str, Any]],
                  meta: Dict[str, Any],
                  data: bytes) -> str:
    W = 110
    out: List[str] = []
    out.append("=" * W)
    out.append("报文解析报告".center(W))
    out.append("=" * W)
    out.append(f"报文名称 : {meta.get('name', '-')}")
    out.append(f"配置文件 : {meta.get('config', '-')}")
    out.append(f"输入来源 : {meta.get('source', '-')}")
    out.append(f"报文长度 : {len(data)} 字节")
    out.append(f"解析时间 : {meta.get('time', '-')}")
    if meta.get("warn"):
        out.append(f"提示     : {meta['warn']}")
    out.append("-" * W)
    out.append(
        f"{pad_to('偏移', 8)}"
        f"{pad_to('字段名', 34)}"
        f"{pad_to('类型', 16)}"
        f"{'长度':>6}  值"
    )
    out.append("-" * W)

    for rec in records:
        indent = "  " * rec["level"]
        leaf = rec["path"].split(".")[-1]
        line = (f"0x{rec['offset']:04X}  "
                f"{pad_to(indent + leaf, 34)}"
                f"{pad_to(rec['type'], 16)}"
                f"{rec['length']:>6}  "
                f"{rec['display']}")
        if rec["desc"]:
            line += f"   # {rec['desc']}"
        out.append(line)

    out.append("-" * W)
    out.append("原始报文 (Hex / ASCII):")
    for i in range(0, len(data), 16):
        chunk = data[i:i + 16]
        hexpart = " ".join(f"{b:02X}" for b in chunk)
        asciipart = "".join(
            chr(b) if 32 <= b < 127 else "." for b in chunk)
        out.append(f"  {i:04X}  {hexpart:<47}  |{asciipart}|")
    out.append("=" * W)
    return "\n".join(out)


# ----------------------------------------------------------------------
# 命令行入口
# ----------------------------------------------------------------------
def build_argparser() -> argparse.ArgumentParser:
    ap = argparse.ArgumentParser(
        prog="packet_parser",
        description="通用十六进制报文解析器:按 JSON 配置解析报文并输出文本报告",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    ap.add_argument("-c", "--config", required=True,
                    help="报文格式配置文件(JSON)")
    src = ap.add_mutually_exclusive_group(required=True)
    src.add_argument("-i", "--input",
                     help="输入文件(默认按十六进制文本读取)")
    src.add_argument("-d", "--data",
                     help="直接传入十六进制字符串")
    ap.add_argument("--binary", action="store_true",
                    help="配合 -i 使用,表示输入文件是原始二进制")
    ap.add_argument("-o", "--output", default="parse_result.txt",
                    help="结果输出文件(默认 parse_result.txt)")
    ap.add_argument("--stdout", action="store_true",
                    help="同时把报告打印到终端")
    return ap


def main(argv: Optional[List[str]] = None) -> int:
    args = build_argparser().parse_args(argv)

    # 1) 读配置
    try:
        with open(args.config, "r", encoding="utf-8") as fp:
            config = json.load(fp)
    except OSError as exc:
        print(f"[错误] 无法读取配置文件: {exc}", file=sys.stderr)
        return 2
    except json.JSONDecodeError as exc:
        print(f"[错误] 配置文件 JSON 格式错误: {exc}", file=sys.stderr)
        return 2

    # 2) 读报文
    try:
        if args.data is not None:
            data = clean_hex(args.data)
            source = "命令行 -d 参数"
        elif args.binary:
            with open(args.input, "rb") as fp:
                data = fp.read()
            source = f"{args.input} (原始二进制)"
        else:
            with open(args.input, "r", encoding="utf-8",
                      errors="ignore") as fp:
                data = clean_hex(fp.read())
            source = f"{args.input} (十六进制文本)"
    except OSError as exc:
        print(f"[错误] 无法读取输入: {exc}", file=sys.stderr)
        return 2
    except ParseError as exc:
        print(f"[错误] {exc}", file=sys.stderr)
        return 2

    # 3) 解析
    try:
        parser = PacketParser(config)
        records, leftover = parser.parse_bytes(data)
    except ParseError as exc:
        print(f"[错误] 解析失败: {exc}", file=sys.stderr)
        return 1

    # 4) 生成报告
    meta = {
        "name": config.get("name", "-"),
        "config": args.config,
        "source": source,
        "time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
    }
    if leftover:
        meta["warn"] = (f"解析结束后仍有 {leftover} 字节未使用"
                        f"(可能是帧填充或尾部数据)")

    report = format_report(records, meta, data)

    try:
        with open(args.output, "w", encoding="utf-8") as fp:
            fp.write(report + "\n")
    except OSError as exc:
        print(f"[错误] 无法写出结果文件: {exc}", file=sys.stderr)
        return 2

    print(f"解析完成:共 {len(records)} 条字段记录,"
          f"结果已写入 {args.output}")
    if args.stdout:
        print(report)
    return 0


if __name__ == "__main__":
    sys.exit(main())

二、脚本功能汇总

功能 说明
整数类型 uint8/16/24/32/64/128、int8/16/24/32/64/128
地址类型 mac、ipv4、ipv6
复合类型 struct(引用 definitions)、switch(单/多字段)、array
特殊类型 bytes/hex/raw、string、padding/pad
位域 整数类型的 bits 拆解,支持嵌套 enum 和 desc
长度 数字、rest/remaining/*/auto、表达式
字节序 顶层默认 + 字段级覆盖,支持 big/little/be/le
switch 匹配 整数(十进制/十六进制/八进制/二进制)、字符串、bytes、bool
表达式 在受限环境中求值,可用已解析字段(含位域子字段)
报错信息 包含字段名、偏移、可用字节数等细节
报告 字段表 + 原始 Hex/ASCII 视图 + 未消费字节提示

三、使用示例

bash 复制代码
# 直接传十六进制串
python packet_parser.py -c ethernet.json \
  -d "FFFFFFFFFFFF000C29A1B2C308004500001C0001000040110000C0A80101C0A8010204D2005000080000" \
  -o result.txt --stdout

# 从十六进制文本文件读取
python packet_parser.py -c ethernet.json -i frame.hex -o result.txt

# 从原始二进制文件读取
python packet_parser.py -c ethernet.json -i frame.bin --binary -o result.txt
相关推荐
工作10年+,存储芯片行业2 小时前
企业级 SSD 耐久度评测(JESD218 / JESD219 + FIO)
网络·ssd·nvme·存储·pcie·nand·fio
Eloudy3 小时前
RoCE 的 UC SEND/RECEIVE 的完整交互过程分析
网络·rdma·roce
花间相见3 小时前
【计算基础|网络10】TCP(中):可靠传输
网络·网络协议·tcp/ip
谢亮_vipxieliang3 小时前
多智能体系统的“TCP/IP时刻”:MCP与A2A协议如何定义Agent时代
网络·tcp/ip·ai·php
草莓熊Lotso4 小时前
【C++ 基于微服务的即时通讯系统】基建三件套:gflags + gtest + spdlog 源码编译安装与实战入门
linux·网络·数据库·c++·微服务·log4j
通信大模型12 小时前
IEEE TCCN | 面向低空经济网络的Agentic AI驱动多无人机轨迹优化
网络·人工智能·无人机
wdfk_prog13 小时前
LWIP教程 09:从 `tcp_slowtmr()` 到 Fast Retransmit——TCP 超时重传与重复 ACK
服务器·网络·数据库·网络协议·tcp/ip
15Moonlight13 小时前
Linux网络(02):Socket 编程 UDP
linux·网络·udp