n 6、n ..19、ans ...999 这样的记号。它把两件事压在一个字符串里:这个域允许放什么字符,以及报文怎么说明它有多长。解析器出问题基本都出在后半截——点的个数不是排版,是长度头占几位数字,而那几位数字真实存在于报文里。
开头的字母表示这个域允许承载的字符集,可以组合——ans 就是字母、数字、特殊字符都允许。
| 字母 | 允许的内容 |
|---|---|
a | 字母 |
n | 数字 |
s | 特殊字符 |
b | 二进制数据,按比特计数 |
z | 磁道 2 / 磁道 3 数据,按磁条上的编码方式 |
x | 前置符号位,C 表示贷记、D 表示借记,总是写成 x+n |
所以 n 6 是 6 位数字,an 12 是 12 位字母数字,b 64 是 64 个比特,x+n 8 是一个符号位后跟 8 位数字。
点表示这是个变长字段,而且点的个数就是长度头占几位数字。点后面的数字是这个域的最大长度。
| 记号 | 长度头 | 能表达的最大长度 |
|---|---|---|
n 6 | 无(定长) | 固定 6 位 |
n ..19 | 2 位 | 99(此域上限 19) |
ans ...999 | 3 位 | 999 |
这一点最容易被扫过去。定长域就只有数据本身;变长域是长度数字 + 数据,两段都要消费掉。
两个真实例子。长度头单独标出只是为了看得清,在报文里它就是紧接着的下几个字节。
| 字段 | 字节 |
|---|---|
DE2 主账号n ..19 |
16 5413330089010001两位长度数字说明卡号 16 位,随后是 16 位数字 |
DE55 IC 卡数据ans ...999 |
028 <28 字节 EMV TLV>三位长度数字说明后面有 28 字节 |
注意第二个是 028 而不是 28——三位长度头永远占三个字符,前面补零。
ans..999 不可能存在?你会在一些字段表里看到 ans..999——两个点,最大 999。这个组合自相矛盾:两位数字最多只能数到 99,两位长度头根本没办法说出"后面有 426 字节"。最大值到 999 的域,必须写成 ans ...999。
后果不是排版问题。假设报文里真的是三位长度头,而解析器按两位读:
02,以为这个域只有 2 字节8,被当成数据的第一个字节吃掉报文不会在读错的那个域报错,而是在更靠后的某处崩——某个长度读成乱码,或者记录长度超出缓冲区,离真正的病因很远。长度头差一位,和一个多余的 trim() 剥掉尾部填充空格,症状完全一样、原因也完全一样:边界一移,后面全对不上。
很多实现用名字而不是点来标注长度处理方式。两者一一对应:
| 名称 | 点记号 | 含义 |
|---|---|---|
FIX | n 6 | 定长,无长度头 |
LLVAR | n ..19 | 2 位长度头(LL) |
LLLVAR | ans ...999 | 3 位长度头(LLL) |
那些 L 就是长度数字本身。两种写法没有语义差别——只是点记号额外告诉你字符集,LLVAR 这套名字不带这个信息。
还有一点要记住:那几位长度数字怎么编码是另一个问题。同一个 LLLVAR 域,长度头可能是三个 ASCII 字符,也可能是压缩 BCD(占更少字节),还可能是 EBCDIC。记号说的是数字个数,不是字节表示——用之前先确认对端的编码方式。分不清手上是哪种时,十六进制查看器能把同一段字节按 ASCII 和 EBCDIC 并排显示。