摘要

  • RFC 3382 为 IPP 增加了可选的 collection 语法,让不同类型、各有名称的成员能够作为一个值共同传输;成员还可以继续嵌套集合,也可以为将来的可选扩展留出位置。
  • 成员次序明确不具权威性。Printer 可以换一种顺序返回同一批成员,因此语义必须跟随唯一名称与集合边界,而不能依赖第几个字节或第几个位置。

客户端送出一个含有三个成员的集合。Printer 返回的还是这三个成员,只是原来的第一个变成第三个,第三个又排到了第二个。如果客户端把位置当作身份,这次交换看上去就像数据被弄乱了。可在 RFC 3382 的规则里,Printer 完全可能做得正确。真正稳定的是成员名称、各自的语法和值,以及把这些成员圈在一起的集合边界。

RFC 3382 于 2002 年 9 月作为标准轨道文档发布,填补了 Internet Printing Protocol 的一个结构性缺口。IPP 当时已经能表示许多常见的标量和重复值,却缺少一种通用方式,把类型各异的属性组合成一个值。文档把这种结构类比为 PostScript dictionary 或 Java Map。类比强调的是“按名称组织成员”,并不是把整套编程语言对象模型搬进 IPP。

需求其实很朴素:用几个彼此相关的字段描述同一件事。某种介质可能同时需要尺寸、材质、颜色与来源信息。平铺清单当然能装下每个事实,可一旦同时出现多个介质实例,接收者就很难确认哪些数值属于哪一组。collection 提供的首先就是这条归属边界。

RFC 3382 把 collection 定义为一个或多个成员属性。每个成员都有名称,也保留自己的语法。一个成员可以是整数,另一个可以是关键字或范围,还有一个成员本身又可以是 collection,甚至是一组 collections。外层容器不会抹平这些类型;它让不同种类的值保持区别,同时声明它们共同组成一个结构。

这种分组不等于顺序契约。RFC 明确允许成员属性以任意顺序出现,Printer 也可以用与请求不同的顺序存储并返回它们。这不是无关紧要的编码细节,而是在决定语义权力放在哪里。接收者必须按名称查找成员,不能认定“第二个值永远是高度”或者“最后一个值一定是颜色”。

名称因此承担了更重的责任。真正定义某个 collection 属性的文档,必须说明集合名称、单值还是多值、出现语境、如何公布受支持成员,以及每个成员的名称、要求级别、语法、语义、约束、支持值和默认值。容器语法只是提供结构位置,并没有替定义者完成语义工作。

在同一个 collection 定义内,成员名称必须唯一。同一个名称可以出现在另一个 collection 中,也可以出现在更广的 IPP 属性命名空间里,因为集合边界给出了作用域。这种不对称很有价值:局部唯一允许不同结构复用词汇,同时又确保一个容器内部按名称查找时没有歧义。

如果一个 collection 值里出现两个同名成员,它就是格式错误的值。客户端不得发送,Printer 也不得返回。接收端的处理空间进一步揭示了限制:Printer 可以把请求作为 bad request 拒绝,也可以接受后只采用其中一个重复值,具体取决于实现。RFC 3382 从未规定可移植的“第一个优先”或“最后一个优先”。一旦唯一性失效,排列顺序并不能安全地补回语义。

这条边界也关系到安全与运维。如果两个组件选择了不同的重复成员,校验可能针对一个值完成,实际执行却使用另一个值。RFC 3382 没有用今天的术语描述这种攻击方式,文档发布本身也不能证明任何具体产品存在或不存在该行为。但其一致性规则方向清楚:生产者不应制造歧义,消费者也不应发明协议从未承诺的位置规则。

嵌套让模型有了更强的表达力。成员可以继续包含 collection,也可以包含一组 collections。整个集合没有统一的全局长度上限,但每个成员仍然受到自身语法限制。它因此不是任意扩张的自由文档,而是一个递归结构:叶子仍遵守 IPP 值规则,定义文档仍需列明可接受的语法。

扩展能力同样经过刻意设计。collection 定义可以包含可选成员;只要定义规则允许,后续文档还可增加新的可选成员。旧实现不必假装理解新字段,但它需要一种严谨方式标明哪里不受支持,并让失败信息继续附着在正确容器上。

RFC 3382 利用 Unsupported Attributes Group 做到这一点。不受支持或发生冲突的成员,可以放在一个 collection 里返回,从而保留“失败发生在哪个结构中”的边界。若把成员压平为一个无关的顶层错误,上下文便会丢失。相同名称可能出现在多个作用域,外围 collection 才能说明究竟是哪一次使用出了问题。

线缆上的编码也强化了这些边界。begin-collection、member-name 与 end-collection 标签给结构加上框架。设计还考虑了旧版 IPP 解析器,避免一个不认识 collection 的解析器随意把成员名称误认成普通顶层属性。这里的兼容并不表示旧实现突然理解新语义,而是让“不知道”较少滑向“误解”。

RFC 中的 media-col 与 job-sheet-col 只是说明性例子,用来展示作者可以怎样定义必需成员、可选成员、支持成员属性和嵌套。例子本身并未把这些名称标准化为已经部署的属性。后续规范仍须完成真正的定义工作。把示例名字复制进请求,并不会凭空产生互操作契约。

这也把 RFC 3382 与同一时期的两篇 IPP 扩展区分开来。RFC 3380 讨论作业属性修改是整体接受还是整体拒绝;RFC 3381 讨论进度计数器如何随整理顺序与观察边界改变含义;RFC 3382 提出的是更基础的表示问题:多个类型不同的值怎样作为一个单元传输,同时不把物理排列变成语义的一部分。

它同时更新了 RFC 2910 的 IPP/1.1 编码与传输规则,以及 RFC 2911 的模型与语义。更早的 RFC 2565 和 RFC 2566 定义过 IPP/1.0 对应部分,RFC 2567 记录需求,RFC 2568 解释设计目标。后来 RFC 8010 与 RFC 8011 分别整合编码和模型。这条时间线说明机制处在何处,却不能证明采用率、产品行为或打印结果。

“成员关系与顺序分离”之所以长久,是因为许多系统会不知不觉地让展示位置获得身份。解码器把字段读入数组,应用再按数组渲染,不久之后,第三项就被赋予规范从未给过的含义。只要序列化器整理顺序、插入一个新可选成员,或中间方从映射结构重建 collection,这种假定就会崩溃。

RFC 3382 的规则正好截断这种漂移。既然顺序可以变化,测试就应该主动打乱顺序;既然名称定义成员,重复名称就必须作为歧义被拒绝或隔离;既然嵌套提供作用域,日志和错误报告就必须保留通往嵌套 collection 的路径;既然允许可选扩展,未知成员就应继续明确地保持未知,而不是悄悄绑定到最近的熟悉位置。

用 Lu Heng 的“最小初始规范”视角来看,这项设计很节制。RFC 没有试图一次标准化未来所有结构化属性,而是标准化最小可复用容器、具体 collection 定义文档应承担的职责,以及可选成员出现时不必重写基础模型的规则。未来决策仍可留在未来的局部定义中。

他的“现实层次”视角则给出相应警告。序列化顺序只是展示层;成员身份、集合归属、支持状态、经过校验的语义和设备最终动作属于不同层次。一个被正确解码的 collection,并不能证明每个成员都受支持、请求已经获批或实体操作已经成功。分组语法提供的是结构证据,不是结果证书。

因此,RFC 3382 的历史意义并不只是给几个值加上括号。它教会 IPP 在允许表示形式移动时仍保存关系。collection 把成员留在一起;唯一名称和明确边界让成员继续可理解;顺序则终于回到它本来的位置:只是某一次编码的偶然属性。

来源