摘要

  • RFC 1556 把双向文本显示分成三种责任结构:visual 模式由写信端预排可见顺序,implicit 模式由阅读端算法推导,explicit 模式则靠夹在正文中的控制序列指挥。
  • MIME 可以证明非 ASCII 字符被编码和传送,却不能单凭这一点证明收件人看见了作者预期的阅读顺序;字符集、方向模式与渲染上下文必须分别留证。
  • 这是一份 1993 年 12 月的 Informational RFC,不是 Internet Standard。后来的 HTML、Unicode 与 UTF-8 邮件仍面对同一层次边界,但不能被写成 RFC 1556 的直系实现史。

归档文件没有坏,句子却换了位置

先不要想象丢包。想象一份保存完好的旧邮件:正文里有希伯来语、一个英语产品名、一串数字和句末标点。归档系统能给出原始文件的哈希;MIME 解码器得到的每个字符也都正确。可是在旧终端、现代阅读器与复制后的文本框里,英语词组和数字分别落在不同位置。

“字节一致”没有回答争议。它只证明运输没有改写序列。屏幕还要回答另一个问题:一行里哪些字符向右推进,哪些片段向左推进,中性标点跟随哪一侧,换行之后又从哪一方向开始。

RFC 1556 的题目是 Handling of Bi-directional Texts in MIME。Hank Nussbacher 于 1993 年 12 月发布它;RFC Editor 记录与 IETF Datatracker都将其列为 Informational。正文也明确说,它不规定任何 Internet Standard。

这份文件的语法野心很小:说明 MIME 双向文本可使用的 direction 约定。它揭开的工程边界却很大——能够把字符送到对面,不等于已经决定字符如何成为人读到的句子。

MIME 先修通了运输层

RFC 1521 让邮件正文不再只能是平坦的 ASCII。正文部件可以声明媒体类型和字符集,并用 Base64 或 Quoted-Printable 穿过仍按旧假设工作的邮件设施。RFC 1521 的信息页保存了这段 MIME Part One 的历史身份。

RFC 1522 又把非 ASCII 表达带到部分邮件头。写信程序把原文变成 encoded-word,支持它的阅读器识别编码、还原字符,再按指定字符集显示。信息页说明它是与正文机制相配套的 Header Extensions。

这两步解决的是“怎样让对方拿到这些字符”。charset 说明编码值对应哪些字符,transfer encoding 保护这些值怎样通过旧链路。对于英语独占的一行,这常常已经接近用户期待;但 ISO-8859-6 的阿拉伯语或 ISO-8859-8 的希伯来语与拉丁文本同处一行时,字符身份并不包含唯一的可见排列。

RFC 1556 因而站在 MIME 成功的终点上继续提问。邮件可以解码成功,阅读仍可以失败。

RFC 1555 留下了一份视觉顺序样本

同月发布的 RFC 1555 由 Nussbacher 与 Yehavi Bourvine 合著,讨论用 MIME 与 ISO-8859-8 传输希伯来语。RFC Editor 信息页同样把它标作 Informational。

它说默认方向是 visual。希伯来文录入时从右向左,但编码时先排成从左向右的视觉序列,再由标准 MIME 机制从左向右传送。阅读器不必理解句子的逻辑结构;写信程序已经替预期屏幕完成排列。

这不是“没有方向性”,而是方向决定被提前了。只要收件端老老实实按单向文本显示,画面可以正确。一旦后来的程序把这份 visual-order 数据当成 logical-order 数据,再运行一次双向算法,原本已经排列过的文字就会被二次重排。

RFC 1555 还记录了邮件列表软件的具体破坏路径:有的 Listserv 转发会省略完整 MIME 头,自动归档也可能不保留它们。编码后的主体还在,说明如何解释它的元数据却丢了。保存了文件,不代表保存了文件与阅读规则之间的联系。

更重要的是它对 8-bit mail 的预期。即使未来透明的 8 位传输让 Base64 或 Quoted-Printable 部分退出,Content-Type 和 directionality 仍然需要。管道更透明,只会减少一种运输转换,不会替屏幕选择顺序。

Visual:写信端已经替收信端做了决定

RFC 1556 把 visual 定为默认模式,仍使用普通 ISO-8859 字符集名,不加后缀。显示设备只有一个从左到右的主方向;阅读程序不知道内容本身的方向,把它当作单向文本。写信程序必须事先把字符排成显示正确的序列,显示时不用控制字符,也不用次序算法。

它的现实优势是兼容简单终端。旧阅读器无需理解希伯来语或阿拉伯语的方向属性,也能把已排好的行照样放到屏幕上。

代价则藏在后续动作里。光标移动、搜索、编辑、复制和重新换行面对的是为某一屏幕宽度制作的视觉序列,而不是句子的逻辑序列。插入一个拉丁缩写,或让窗口在另一个位置折行,原来的排列承诺便可能失效。

因此 visual 模式的责任方是 composer。运输只需忠实,viewer 则必须保持“不聪明”。

Implicit:规则引擎在收件端接手

隐式方向使用 ISO-8859-6-i 与 ISO-8859-8-i。这里的文本不必由写信端记录每一次局部移动;显示算法根据字符类型、字符与相邻字符的位置关系以及主方向来计算呈现次序。RFC 1556 因完整算法复杂而让实现者查阅 ECMA TR/53。

责任从 composer 移向 renderer。收件端要认识后缀,给字符赋予相容的方向属性,还要知道段落的基方向和边界。数字、标点和其他中性字符依靠上下文决定位置;换行在哪里发生,也会改变算法处理的单元。

隐式规则并非随意。共同算法正是为了让实现可以互操作。但它产生的是新的可核验结果,而非传输结果的自动延伸。两个客户端即使拿到相同字符,也可能因为算法版本、基方向、标记边界或折行位置不同而显示不同。

这时只保存 payload hash 不够。要复现画面,必须同时保存决定环境。

Explicit:命令进入正文,解释器必须保住状态

显式方向使用 ISO-8859-6-e 与 ISO-8859-8-e。控制序列夹在正文中,直接规定方向上下文。发送者可以明确开始、改变或结束一段方向,而不是完全依赖相邻字符的类型。

“显式”并不等于“天然安全”。控制序列没有可见字形,清洗器可能把它当作多余或危险内容删除;转换器可能原样保留,却交给不认识这些指令的显示程序;作用域若没有正确结束,方向影响还可能越过作者原本设想的边界。

RFC 1556 说,它所依据的 ECMA TR/53 新增了三个控制功能,并修改了 ECMA-48 中 22 个既有功能。这不是加一个“向右读”的按钮。方向会影响活动位置、字符推进和数据表面与显示表面之间的映射。

显式模式于是有两份责任:发送端要放入正确控制,接收端要保存并执行它们。任何一方只保住可见字母,都还没有保住句子。

ECMA 模型把“数据位置”与“显示位置”分开

ECMA TR/53 把双向设备画成一台接收图形字符与控制函数、再产生供人阅读图像的设备。模型可以同时拥有 data component 与 presentation component,并分别维护 active data position 和 active presentation position。

这组区分解释了为何问题不能缩成字体选择。字体回答某个字符用什么字形。呈现模型回答字形落在哪里、光标如何移动、回车或退格作用于哪个位置,以及一组存储字符如何成为一行图像。

RFC 1556 把这套较深的设备机制压缩成 MIME 侧的模式声明。接收端至少需要知道,它拿到的是 composer 已经排好的视觉序列、等待算法处理的隐式序列,还是携带状态命令的显式序列。模式丢失,就可能把完全正确的数据送进错误的显示机器。

四种故障都可能保住全部字母

第一种是元数据消失。网关保住正文,却把 ISO-8859-8-i 归一化成 ISO-8859-8,或者转发时省略 Content-Type。字符没少,模式选择没了。

第二种是二次重排。旧 composer 已经制作 visual order,现代 viewer 却按 logical order 运行 implicit 算法。双方都执行了自己的设计,合在一起反而错误。

第三种是控制字符被剥离。显式方向命令被安全过滤器删除;若比较时只看可见字符,报告会说内容相同,真正的显示指令却已经不存在。

第四种是上下文漂移。两个 implicit renderer 使用不同基方向、不同换行或不同高层标记边界。它们的 decoded sequence 相同,resolved visual sequence 不同。

数字、标点和标识符让这件事超出排版美观。一个大段落反向很容易发现;标点粘到编号的另一侧,或复制出来的逻辑顺序与眼睛所见不同,却可能进入地址、金额、日期或认证提示。完整字符集合不能证明人读到的关系完整。

后来的机制延续边界,不提供血统证明

1997 年的 RFC 2070 为 HTML 加入 DIR 属性和 BDO override 等国际化机制,信息页说明它属于另一套 Web 标记语境。它可以证明高层上下文仍需承担方向责任,不能证明 HTML 机制由 RFC 1556 直接演化。

早期的 Unicode Bidirectional Algorithm 报告把 Unicode 的内存表示称为 logical order,并说明方向格式字符只影响显示次序。现行 UAX #9已经历大量演进,也明确规定高层协议可以怎样提供段落级别或方向覆盖。

2012 年的 RFC 6532允许在端到端支持 UTF-8 的邮件头中直接使用 UTF-8;信息页记录了它的独立标准身份。字符可以更直接地进入邮件,并不表示存储顺序和显示顺序合并成了一层。

这些资料只证明问题边界持续存在。它们不证明 RFC 1556 的 -i、-e 名称获得普遍部署,更不证明它是 Unicode、HTML dir 或 SMTPUTF8 的单一起点。

调试需要三张收据

运输收据保存原始 octets、哈希、transfer encoding 与解码后的 code-point sequence。它回答途中是否改变。

解释收据保存 Content-Type、charset 与方向模式,所有显式控制,段落基方向,高层 markup/style,以及 renderer 和算法版本。它回答接收方认为自己该使用什么规则。

呈现收据保存换行边界、resolved visual order、截图和复制粘贴后的结果。它回答某一环境里实际出现了什么。

截图单独只能证明某块屏幕曾显示某种排列;payload hash 单独只能证明序列一致。测试样本应把阿拉伯语或希伯来语与拉丁片段、数字、标点、中性字符、嵌套方向和不同窗口宽度组合起来,同时比较逻辑序列与视觉序列。

迁移 renderer 时,应保留新旧结果直到差异得到解释。若为了“清理”旧档案而原地重排,最关键的原始证据会消失,错误迁移也就失去回退路径。

发布文档仍不是运行现实

Heng Lu 的 Running-Code Primacy 提供了一条今日可用的纪律:规范是协调材料,仍需经过实现、运行与采用检验。RFC 1556 被发表,只能证明方向约定写进了公开记录,不能证明哪些客户端执行过它。

Minimum Initial Specification、Localized Future Decision 与 Voluntary Adoption追问,公共规则中究竟什么是最小、确定、可本地核验的。RFC 1556 的教训恰是:传输字符相等,不足以构成双向邮件可读的共同不变量;模式、上下文和相容 renderer 也需成为证据。

Reality Layers要求表示、运输、解释和结果各自留证。这些是后来的分析语言,不是对 Nussbacher 或 ECMA 主观意图的证明。它们的作用,是阻止我们用一层的成功替另一层背书。

三个责任方不能被一句“送达”吞掉

Visual、implicit 与 explicit 不只是三种排版选项。Visual 表示 composer 已经行动;implicit 表示 renderer 的算法将行动;explicit 表示发送端写入命令,接收端执行命令。若系统不知道自己接手的是哪一种,完全忠于本地规则也会背叛原文。

所以 RFC 1556 的历史不是“MIME 无法传希伯来语或阿拉伯语”。MIME 修通了字符的旅程。这份短 RFC 只是拒绝让旅程的收据冒充阅读的收据。

原始序列一致,证明消息完整抵达。方向模式和控制仍在,证明解释上下文被保存。指定 renderer 的成对测试通过,才证明某个可见结果。只有三者同时成立,“正确送达”才不只是“字节到了”。

来源