news 2026/9/13 17:31:18

Ruffle AVM1/AMF0 序列化回归测试套件:用 SharedObject、LocalConnection 与 NetConnection 三通道逐字节校验 Flash 数据序列化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Ruffle AVM1/AMF0 序列化回归测试套件:用 SharedObject、LocalConnection 与 NetConnection 三通道逐字节校验 Flash 数据序列化

Ruffle AVM1/AMF0 序列化回归测试套件:用 SharedObject、LocalConnection 与 NetConnection 三通道逐字节校验 Flash 数据序列化

【免费下载链接】ruffleA Flash Player emulator written in Rust项目地址: https://gitcode.com/GitHub_Trending/ru/ruffle

本文以 tests/tests/swfs/avm1/amf0_serde_suite 目录下的 AMF0 序列化回归测试为核心,讲解 Ruffle 如何通过一套 ActionScript 2 测试脚本,把同一份覆盖原语、Date/XML 原生对象、稀疏数组、注册类、循环引用与 DAG 引用缓存的“压力载荷”,分别经由 SharedObject(磁盘)、LocalConnection(内存)和 NetConnection(网络)三条通道序列化,并与 Flash Player 的真实输出逐字节对比。读完本篇,你将掌握该测试的完整运行方法(含时区要求与 Flash Player 网络信任配置)、测试配置项含义,以及 Ruffle 在 core/src/avm1/amf.rs 中的 AMF0 序列化实现细节。

一、测试套件定位:AVM1/AMF0 协议回归

AMF0 是 Flash 用于跨进程、跨网络传输数据的序列化协议。Ruffle 需要保证自己读写的 AMF0 字节流与 Adobe Flash Player 完全一致,否则与真实 Flash 服务互操作(如NetConnection调远程网关、SharedObject读写磁盘)就会失败。test.as 文件头部自述为 “THE AVM1 / AMF0 PROTOCOL REGRESSION SUITE”,即 AVM1/AMF0 协议回归套件。

套件的核心思路是:构造一份尽量“刁钻”的载荷对象,通过三条通道各走一遍序列化/反序列化往返,再用trace输出逐项断言,与 Flash Player 的输出快照逐行对比

1.1 三条数据通道

test.as 将载荷分别送入三个通道:

通道机制测试重点
Channel A:SharedObjectSharedObject.getLocal("avm1_test_suite", "/")+flush()写盘,再重新getLocal读回AMF0 磁盘持久化格式(.sol 文件)
Channel B:LocalConnectionlcSend.send("test_lc_pipe", "onMainPacket", payload, topFunc, topMc)内存内序列化;另测试顶层参数传 function / MovieClip 时的中止行为
Channel C:NetConnectionnc.call("test.avm1", responder, payload)http://localhost:8000/发 POST网络传输的原始 AMF0 字节流,由 server.py 捕获并以十六进制打印

注意 Channel C 前的注释:在发送前delete掉了arr_strict_ref/arr_mixed_ref/t_reg_ref三个 DAG 引用字段,注释明确说明 Ruffle 的 AMF0 引用计数器(reference bytes)在原始值上目前与 Flash Player 存在失步,会造成逐字节不匹配——这是套件当前标记known_failure的已知原因之一。

1.2 载荷覆盖的数据类型

test.as 第 112 行起构造payload,共八组:

  1. 原语(Primitives):正负浮点、正负整数、+0/-0(并用1/x输出Infinity验证符号零语义)、Infinity/-Infinity、普通字符串、空串、非 ASCII 串(Jalapeño Mjölnir)、代理对\uD83D\uDE00、未配对代理项\uD83D、含不可打印字符的串、true/falsenullundefined
  2. 原生对象(Natives)new Date(1672531200000)(2023-01-01 UTC)验证 Date 的时间戳与时区偏移字段;new XML(...)验证 XML 节点以字符串形式传输后仍可解析出nodeName与属性。
  3. 问题类型(Problematic Types):function 与 MovieClip。AMF0 规范下 function 应被丢弃或变成undefined,MovieClip 的自定义变量行为因通道而异(见第四节结果对比)。
  4. 数组(Arrays):严格数组;稀疏数组(索引取i32_maxi32_max+1u32_maxu32_max+1i64_maxi64_max+1u64_maxu64_max+1等极端值);混合数组(数值下标 + 字符串键);负数与小数索引;以及手工伪造的“假数组”(普通 Object 带length=2"0"/"1"键)。
  5. 类型化对象(Typed Objects):未注册类、Object.registerClass("com.tests.RegClass", RegClass)注册类、注册名不匹配的WeirdAliasString、同一构造器注册两次(Alias.One/Alias.Two)、伪造constructor属性与伪造__proto__的两种“冒充”对象。
  6. 深层嵌套(Deep Nesting)o.prop = o循环引用对象;RegClass 实例内再嵌含 Date、XML 的严格数组和稀疏数组。
  7. AMF0 引用缓存(DAGs)payload.arr_strict_ref = payload.arr_strict等三组自引用,利用 AS2 中复杂类型的===做内存同一性检查,验证 AMF0 的 object id 引用机制是否让两次出现解析为同一对象。
  8. 嵌套显示对象:普通对象中塞入movie_clip_prop,验证 DisplayObject 在序列化时被剥离(应为undefined)。

验证函数verifyPayload(prefix, d)(test.as 第 11–104 行)对八组数据逐条trace输出,形成每通道 60 余行的可 diff 断言。

二、运行测试(完整继承原 README)

以下内容直接继承自 README.md,是该套件手动运行的权威说明:

时区前置条件:需要将系统时区(或 Flash Player 的时区)设置为 UTC,才能获得正确输出。原因是 AMF0 的 Date 数据中带有 timezone offset 信息,时区不同会导致输出差异。

验证网络通道的原始字节:在 tests/tests/swfs/avm1/amf0_serde_suite 目录下运行server.py,然后分别用 Flash Player 或 Ruffle Desktop 播放器运行test.swf。server.py 是一个 29 行的 Python HTTP 服务,监听 8000 端口;收到 POST 后打印Navigator::fetch的 URL、方法、Content-Type(应为application/x-amf),并把请求体按大写字节十六进制逐字节打印出来,返回空 200 响应——这正是 output.txt 中Body: [...]那一大串字节的来源。

Flash Player 网络信任配置(Linux):在 Flash Player 下运行时,需要允许该 SWF 发起网络请求。在 Linux 上,创建/etc/adobe/FlashPlayerTrust/test.cfg文件,内容为:

/ancestor/of/swf/path

其中/ancestor/of/swf/pathtest.swf路径的任意祖先目录(例如/home/username/)。

三、回归框架配置:test.toml 与共享对象快照

test.toml 是该测试在 Ruffle 回归测试框架(tests/framework)中的声明:

num_ticks = 10 log_fetch = true known_failure = true [[compilers]] type = "Rascal" target = "test.swf" scripts = ["test.as"] swf_version = 8 [shared_objects."localhost//avm1_test_suite"] expected = "avm1_test_suite" known_failure = true

逐项说明:

  • num_ticks = 10:测试运行 10 个时钟 tick 后收集结果,给异步通道(LocalConnection/NetConnection)留出执行时间。
  • log_fetch = true:捕获Navigator::fetch(即 NetConnection 发出的网络请求)日志,对应框架选项 log_fetch 字段,使server.py捕获的字节流进入输出对比。
  • [[compilers]]声明由test.as经 Rascal 编译器编译生成 test.swf(SWF 版本 8),说明该套件支持从源码重新编译验证。
  • [shared_objects."localhost//avm1_test_suite"]:断言运行后磁盘上的共享对象avm1_test_suite与期望文件avm1_test_suite(即 avm1_test_suite.sol)逐字节一致,由框架的 check_shared_objects 实现校验。
  • 顶层与 shared_object 均标记known_failure = true:表明当前 Ruffle 输出与 Flash Player 快照尚不完全一致(框架会把它记为“已知的失败”而非 CI 阻塞),与 test.as 中关于引用计数字节失步的注释相互印证。

3.1 两份 1579 字节的 .sol 快照

目录内同时存放 Flash Player 写出的 avm1_test_suite.sol 和 Ruffle 写出的 avm1_test_suite.ruffle.sol,二者文件大小均为 1579 字节,与 test.as 中so.getSize()打印的 1579 一致。用od查看两份文件头部,前 120 字节完全相同:

000000 00 bf 00 00 06 25 54 43 53 4f 00 04 00 00 00 00 000010 00 0f 61 76 6d 31 5f 74 65 73 74 5f 73 75 69 74 000020 65 00 00 00 00 00 07 70 61 79 6c 6f 61 64 03 00

即 AMF0 魔数00 BF 00 00,其后对象结构里可见avm1_test_suite(键名,15 字节 = 0x0f)、payload属性及p_num的 IEEE 754 双精度40 94 E6 ...(1337.5)。这说明 SharedObject 磁盘格式这一“最严格的通道”上,Ruffle 与 Flash Player 当前至少在本快照范围内达到了逐字节一致。

3.2 输出快照对比

output.txt 是 Flash Player 的基准输出,output.ruffle.txt 是 Ruffle 的实际输出。两者 diff 出的差异点正是当前待修复的行为差异,例如:

  • ref_strict_is_exact等三条 DAG 引用断言在 Ruffle 输出中为false(Flash 为true):Ruffle 的 LocalConnection 通道引用缓存尚未让重复对象解析到同一内存实例;
  • sp_func_type/sp_mc_var在 Ruffle 的 LocalConnection 通道为undefined(Flash 保留了 function 类型与自定义变量);
  • 两条Body: [...]十六进制流在arr_strictnest_deep段存在字节差异(如 Flash 版本中引用字节0A与 Ruffle 版本中重复编码00/01的差异),这正是known_failure的字节级证据。

四、Ruffle 侧实现:AMF0 序列化路径

套件断言的行为,其 Ruffle 实现位于 core/src/avm1/amf.rs(共 343 行),底层依赖独立的flash-lsocrate(AMF0DecoderAmf0WriterObjWriter)。关键逻辑与测试载荷一一对应:

  • 通道判定点:serialize 函数注释明确其服务于 “NetConnection.addHeader, NetConnection.call, and LocalConnection.send” 三类调用。它把Value::Undefined/Null/Bool/Number/String直接映射为 AMF0 值;MovieClip映射为Undefined;DisplayObject 属性也被替换为Undefined——这解释了 Channel A 中sp_mc_var: i_am_a_display_object保留自定义变量(MovieClip 作为对象属性被序列化其属性表)而 Channel B 中nested_mc_type: undefined(显示对象在属性遍历中被剥离)的行为分岔。
  • 类型化对象类名解析:object_class_name 复刻了 Flash 的构造函数解析规则:先查自身constructor属性,再回退到隐藏的__constructor__属性(可能是继承来的),然后查registerClass注册表。这正对应测试中t_forged_ctor(伪造constructor)、t_forged_proto(伪造__proto__)与t_mismatch(注册名不匹配)三组“冒充”用例——从快照输出看,这些对象最终都回退为匿名对象序列化(instanceof断言为false),Ruffle 与该行为一致。
  • 数组序列化:serialize_array 按 Flash 规则处理“任何数值键都是数组元素、超出 length 的数值键会扩展序列化长度”,并区分纯密集数组(StrictArray,空洞补undefined)与含字符串键的混合数组(ECMAArray)——对应arr_strict/arr_sparse/arr_mixed/arr_fake各组的断言,以及 16/32/64 位边界索引在稀疏数组中的降级表现(快照中arr_sparse[9223372036854775807]被字符串键"9.2233720368547758e+18"承接)。
  • 引用计数Amf0Writer通过CacheKey/ObjectId跟踪已写对象,commit_lso生成最终元素表;DAG 引用断言在 Ruffle 输出中的false即该计数器与 Flash Player 语义尚未对齐的部分。

五、小结与使用建议

该套件是一份典型的“行为快照 + 字节快照”双层回归测试:output*.txt对比逻辑层行为(哪些值保留、哪些类型被丢弃、引用是否同一),*.solserver.py的十六进制体对比字节层格式(磁盘 AMF0 与网络 AMF0)。复现步骤归纳为:

  1. 将系统时区设为 UTC;
  2. 在 tests/tests/swfs/avm1/amf0_serde_suite 下运行python3 server.py(监听 8000 端口);
  3. Linux 上按 README 建立/etc/adobe/FlashPlayerTrust/test.cfg信任test.swf的祖先路径(仅 Flash Player 需要);
  4. 分别用 Flash Player 与 Ruffle Desktop 打开 test.swf,对照 output.txt / output.ruffle.txt 与两份 .sol 快照定位差异;
  5. 在 CI 中则直接由回归框架按 test.toml 执行,known_failure项会在对应行为修复后转为强断言。

对于希望贡献修复(尤其是ref_*引用同一性与 NetConnection 字节对齐)的开发者,入口就是 core/src/avm1/amf.rs 的 serialize/deserialize 两条路径与flash-lsoAmf0Writer计数器逻辑,本目录的三份快照文件则提供了随时可复验的基准。

【免费下载链接】ruffleA Flash Player emulator written in Rust项目地址: https://gitcode.com/GitHub_Trending/ru/ruffle

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 17:30:15

IDEA打jar包全攻略:从普通Java到Spring Boot及外部依赖处理

干Java这行的,几乎没人能绕开“打jar包”这三个字。不管是把自己写的工具类发给同事,还是把一个Spring Boot服务部署到Windows服务器上,最后一步基本都得落到“怎么打出一个能跑的jar包”上。但我发现一个很有意思的现象:同样问“…

作者头像 李华
网站建设 2026/9/13 17:29:23

嵌入式开发三大方向:单片机、Linux驱动与汽车电子如何选择

1. 这不是危言耸听:为什么嵌入式入门前必须厘清这三个方向?“搞不懂这三个方向,千万别碰嵌入式!”——这句话在嵌入式圈子流传多年,不是导师吓唬新人,而是无数人踩坑后用项目延期、芯片烧毁、驱动崩溃换来的…

作者头像 李华
网站建设 2026/9/13 17:29:20

Django学生选课系统:事务、行锁与并发控制实战解析

简介:基于Python语言与Django框架的学生选课管理系统实战项目,面向刚接触Python Web开发的初学者,帮助理解Django的模型-视图-控制器架构与完整开发流程。项目涵盖数据模型设计、视图逻辑、网址路由、模板渲染、表单处理、用户认证及对象关系…

作者头像 李华
网站建设 2026/9/13 17:28:50

理解rebase和代码合并操作流程

工具操作入口 idea里合并代码选项提供了rebase和merge,其中有一个rebase xx onto yy,这个的意思是把xx分支进行rebase,参照的分支是yy分支最新记录重新变更提交记录,开始的开始点是公共的第一个祖先节点,使变更记录变得…

作者头像 李华