报文格式配置文件说明
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