news 2026/9/13 11:40:19

Ruffle AVM1 AMF0 严格数组序列化测试指南:用 NetConnection 验证网络上的真实字节流

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Ruffle AVM1 AMF0 严格数组序列化测试指南:用 NetConnection 验证网络上的真实字节流

Ruffle AVM1 AMF0 严格数组序列化测试指南:用 NetConnection 验证网络上的真实字节流

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

本指南围绕 Ruffle 仓库中tests/tests/swfs/avm1/amf_strict_array_serialization/测试目录展开,讲解如何通过本地 HTTP 服务器抓取NetConnection.call实际发出的 AMF0 载荷,逐字节验证"严格数组(Strict Array)"与"混合数组(ECMA Array)"的序列化规则。读完本文,你将掌握该测试的完整运行流程(Flash Player 与 Ruffle Desktop 两种方式)、Linux 下的 FlashPlayerTrust 网络授权配置,以及 Ruffle 在 core/src/avm1/amf.rs 中实现数组序列化的底层判定逻辑,并学会如何借助test.tomloutput.txt解读测试期望。

一、测试背景:为什么需要"严格数组"序列化测试

AMF0(Action Message Format 0)是 Flash 时代NetConnectionLocalConnectionSharedObject等网络与本地存储 API 共同使用的二进制数据格式。AMF0 对数组有两种截然不同的编码方式:

  • StrictArray(严格数组)0x0A,带一个 4 字节的array-length,随后是连续的元素,适用于索引连续、没有额外自定义属性的"纯数组";
  • ECMAArray(混合数组)0x08,带一个 4 字节的associative-count和数量不定的键值对,适用于带有非数字自定义属性的"混合数组"。

真实 Flash Player 在选择编码方式时有一套微妙的行为:密集数组输出 StrictArray,含自定义字符串属性的数组输出 ECMAArray,而稀疏数组则会把空洞用undefined补齐。这套行为此前在 Ruffle 中存在兼容性缺陷,因此仓库特意建立了这个回归测试目录,把 AVM1 侧的多种"数组形态"一次性通过NetConnection.call发送出去,再用本地服务器捕获原始字节,作为判定依据。

二、测试目录结构总览

该测试目录位于 tests/tests/swfs/avm1/amf_strict_array_serialization/,共包含 6 个文件:

文件作用
README.md运行说明(本文讲解的关联文档)
server.py本地 HTTP 服务器,捕获并打印 POST 请求的十六进制字节
test.asActionScript 1 源码,构造 8 种数组形态并通过NetConnection.call发送
test.swftest.as编译出的 SWF(AVM1,SWF 版本 8)
test.tomlRuffle 回归测试框架的配置文件
output.txtRuffle 运行时的期望输出,内含完整的 AMF0 字节载荷

三、如何运行该测试

3.1 启动本地抓包服务器

server.py是一个基于 Python 标准库http.server的极简 HTTP 服务器,监听0.0.0.0:8000。它只处理POST请求,读取Content-Length指定的请求体后,将每个字节格式化为大写十六进制并打印到标准输出:

request = self.rfile.read(int(self.headers['Content-Length'])) request_hex = ", ".join([f"{byte:02X}" for byte in request]) print(f" Body: [{request_hex}]")

从 server.py 的实现可以看出,服务器会同时打印请求的URLMethodMime-Type(即Content-Type头)和十六进制Body。运行方式:

python3 server.py # 输出:Running server on port 8000...

3.2 运行 test.swf

README 指出,服务器启动后,需要在 Flash Player 或 Ruffle Desktop 播放器中运行test.swf。测试脚本执行时会通过NetConnectionhttp://localhost:8000/发起一次POST请求(详见test.as第 55–65 行):

var nc = new NetConnection(); nc.connect("http://localhost:8000/"); var responder = new Object(); responder.onResult = function(res) { /* Pass */ }; nc.call("test.avm1", responder, denseArray, sparseArray, mixedArray, fakeArray, throwingArray, superFakeArray, demotedArray, protoOnlyArray);

服务器端随即打印出Navigator::fetch:段,包含完整的 AMF0 请求载荷。

3.3 Linux 下 Flash Player 的网络授权配置

如果使用官方 Flash Player(而非 Ruffle),其沙箱默认禁止 SWF 发起网络连接。README 给出了 Linux 上的解决方案:创建全局信任配置文件/etc/adobe/FlashPlayerTrust/test.cfg,内容写入test.swf所在路径的任意祖先目录:

/ancestor/of/swf/path

例如test.swf位于/home/username/tests/amf_strict_array_serialization/test.swf,则写入/home/username/即可。/etc/adobe/FlashPlayerTrust/目录下的.cfg文件是 Flash Player 在 Linux 上的标准信任机制:其中的每个路径都会被加入白名单,位于该路径之下的 SWF 可以无提示地访问网络。

使用 Ruffle 时无需此步骤:Ruffle 的回归测试框架通过log_fetch选项直接记录网络请求,不走 Flash Player 的沙箱信任机制。

3.4 通过 Ruffle 测试框架自动运行

除了手动运行,该测试还直接接入了 Ruffle 的 SWF 回归测试框架。其 test.toml 配置如下:

num_ticks = 10 log_fetch = true [[compilers]] type = "Rascal" target = "test.swf" scripts = ["test.as"] swf_version = 8

各字段含义(依据 tests/README.md 与 tests/framework/src/options.rs):

  • num_ticks = 10:运行 SWF 的帧数。NetConnection.call是异步操作,需要足够的 tick 让网络请求完成并输出Navigator::fetch日志;这也是该配置中唯一必填字段。
  • log_fetch = true:将所有网络请求(URL、Method、Mime-Type、Body)纳入测试输出。这正是output.txt中能够出现完整 AMF0 字节流的原因——在 tests/framework/src/runner.rs 中,log_fetch为 true 时框架会挂接日志收集器(test.options.log_fetch.then(|| log.clone()))。
  • [[compilers]]段:指定用 Rascal 编译器将test.as编译为test.swf,目标 SWF 版本为 8(AVM1 特性集)。

框架会将实际输出与 output.txt 逐行比对,作为回归断言。

四、测试用例:8 种"数组形态"的构造

test.as 精心构造了 8 个对象,覆盖 AVM1 中数组类型判定的全部边界情况:

变量构造方式预期编码
denseArraypush("dense_0", "dense_1")典型密集数组 → StrictArray(0x0A),长度 2
sparseArray仅赋值下标 0 与 5稀疏数组 → StrictArray(0x0A),长度扩展为 6,空洞用undefined0x06)填充
mixedArraypush("mixed_0")后再加自定义属性custom_prop含非数字键 → ECMAArray(0x08),附带custom_prop键值对
fakeArray普通Object,手动设置"0"length非原生数组 → 匿名 Object(0x03),逐属性序列化
throwingArray"1""length"上注册抛异常的 gettergetter 属性 → 序列化为undefined0x06),不触发求值
superFakeArray通过__proto____initializeNative()模拟"super() 升级"内部判定的 Array(长度 6)→ StrictArray
demotedArray__proto__/__constructor__改成Object被"降级"为非数组 → 匿名 Object
protoOnlyArray仅把__proto__指向Array.prototype的普通对象非原生数组 → 匿名 Object(0x03

其中superFakeArray用例还在第 38 行输出一条关键 trace 用于判定运行时行为:

trace("Did super() upgrade instance? " + (superFakeArray.length === 6 ? "YES" : "NO"));

对应output.txt第一行Did super() upgrade instance? YES,验证了 Ruffle 的 AVM1 对象系统在super()调用后会正确地把实例内部升级为原生数组。

五、期望输出解读:逐字节看懂 AMF0 载荷

output.txt是框架比对基准,其核心是Navigator::fetch段打印出的完整请求:

Navigator::fetch: URL: http://localhost:8000/ Method: POST Mime-Type: application/x-amf Body: [00, 00, 00, 00, 00, 01, 00, 09, 74, 65, 73, 74, 2E, 61, 76, 6D, 31, ...]

前 18 字节是 AMF0 的invoke消息头(远程方法调用格式):4 字节消息大小 + 2 字节version=0x0001+ 2 字节header-count=0x0000+ 2 字节message-count=0x0001+ 2 字节target-uri长度0x0009+ ASCII 字符串test.avm1,随后是response-uri与消息体。消息体中 8 个参数依次编码为:

  1. denseArray0x0A 00000002StrictArray,长度 2,两个 UTF 字符串dense_0dense_1
  2. sparseArray0x0A 00000006StrictArray,长度被扩展到 6,sparse_0之后连续出现 4 个0x06undefined填充空洞),末尾sparse_5
  3. mixedArray0x08 00000001ECMAArray,先有 1 个元素mixed_0,随后键值对custom_prop/custom_value,最后0x00 0x0009结束标记;
  4. fakeArray0x03:匿名Object,含"0"fake_0length1.0(AMF0 数字为 8 字节 IEEE 754 双精度);
  5. throwingArray0x09 0x0A 00000002StrictArraysafe_0之后是0x06(即抛异常的 getter"1"被序列化为undefined,且length的 getter 未被求值);
  6. superFakeArray0x0A 00000001:StrictArray,仅含super_fake_0length被重置为 1 后序列化);
  7. demotedArray0x0A 00000002:StrictArray,两个元素(AVM1 内部仍判定为数组,编码不依赖__constructor__);
  8. protoOnlyArray0x03:匿名Object,含"0"length两个属性。

这份字节流与core/src/avm1/amf.rs的序列化分支一一对应,是验证 Ruffle 与 Flash Player 行为一致性的黄金标准。

六、源码实现:Ruffle 如何判定"数组"并选择编码

output.txt中的每个字节都对应 core/src/avm1/amf.rs 的具体实现逻辑。该文件同时服务于NetConnectionLocalConnectionSharedObject三类 API(见 core/src/avm1/globals/netconnection.rs、core/src/avm1/globals/local_connection.rs、core/src/avm1/globals/shared_object.rs 对serialize/deserialize_value/recursive_serialize的调用)。

6.1 顶层分派:serialize

serialize首先按值类型分派:Undefined/Null/Bool/Number/String直接映射为对应 AMF0 标量;MovieClip与显示对象被序列化为undefined。对于Object,则按优先级检查:

  1. 是否为显示对象 →undefined
  2. 是否为原生数组NativeObject::Array)→ 进入serialize_array,这是本文的核心路径;
  3. 是否为 XML 节点 → AMF0 XML(0x0F);
  4. 是否为 Date → AMF0 Date(0x0B);
  5. 否则 → 匿名对象或带类名的 TypedObject,逐属性序列化。

6.2 数组分支:StrictArray 与 ECMAArray 的判定

serialize_array是"严格数组"行为的核心实现,其判定规则为:

  1. 长度扩展:遍历所有键,凡是能被解析为usize的数字键,若index + 1超过当前length,则扩展序列化长度(对应sparseArray输出长度 6 的行为);
  2. 自定义属性检测:任何完全非数字的键都会使has_custom_properties = true
  3. 编码选择
    • 含自定义属性 →AmfValue::ECMAArray0x08,对应mixedArray);
    • 纯数字键 →AmfValue::StrictArray0x0A),并显式用undefined填充空洞以保持索引连续(对应sparseArray中 4 个0x06的来源)。

源码注释还特别说明了两点与 Flash 的兼容细节:属性按插入顺序的反序收集(keys.into_iter().rev()),以保证与 Flash Player 的字段顺序一致;数字键即使超出length也会扩展数组序列化长度。

6.3 Getter 与异常处理:为什么throwingArray不崩溃

recursive_serialize(core/src/avm1/amf.rs)在收集对象属性时,对虚拟属性(getter)一律不执行求值,直接序列化为undefinedValue::Undefined),并在注释中说明"Flash never evaluates getters during AMF serialization"。因此test.as中注册了抛异常 getter 的"1""length"属性不会触发异常,throwingArray才得以完整输出——这是该测试用例与实现代码互相印证的关键点。在serialize_value_to_writer中,函数对象则被整体跳过("Flash entirely skips functions")。

6.4 类名解析:object_class_name

object_class_name负责 TypedObject 的类名解析:先查找对象自身的constructor属性(若为虚拟属性则放弃),再回退到隐藏的__constructor__属性,最后通过context.avm1.get_class_name_by_constructor查找注册的类名。这与fakeArrayprotoOnlyArray被编码为匿名Object0x03,无类名)的行为吻合——普通Object没有可映射到注册类的构造函数。

6.5 反序列化路径:deserialize_value

deserialize_value是响应方向的对称实现:ECMAArray先以len为参数构造 AVM1 数组,再按元素名是否为整数决定走set_element还是define_valueStrictArray则以元素数量构造数组后逐下标赋值。二者都会把对象登记进reference_cache,以支持 AMF0 的对象引用(0x09)。该函数在NetConnectionsend_callback中被调用(core/src/avm1/globals/netconnection.rs),响应结果经deserialize_value还原后回调给responder.onResult——这正是test.asresponder对象的作用。

七、对比参考:AVM2 侧的 AMF0 序列化

作为对照,AVM2 也有独立的 AMF 序列化实现 core/src/avm2/amf.rs,被 flash/net/net_connection.rs、flash/net/local_connection.rs、flash/net/shared_object.rs 与 flash/net/socket.rs 复用。AVM2 侧有一个 AVM1 不存在的规则——整数在 AMF0 中必须转换为 NumberValue::Integer(i) if amf_version == AMFVersion::AMF0 => AmfValue::Number(i as f64))。此外flash.net.ObjectEncoding类(core/src/avm2/globals/flash/net/ObjectEncoding.as)定义了AMF0 = 0常量,网络套接字 API 会根据ObjectEncoding在 AMF0/AMF3 之间切换(见 core/src/avm2/globals/flash/net/socket.rs 中ObjectEncoding::Amf0 => AMFVersion::AMF0及 AMF0 消息头长度8的分支)。

本测试聚焦 AVM1(SWF 版本 8),但理解两侧共享的flash_lsoAMF0 编解码基础,有助于读者把amf_strict_array_serialization的结论推广到整个 Ruffle 网络栈。

八、运行环境与适用前提

  • 手动验证方式:需要 Python 3(标准库即可)启动server.py;播放端可以是 Flash Player(需按 README 配置/etc/adobe/FlashPlayerTrust/test.cfg)或 Ruffle Desktop 播放器。
  • 自动化回归方式:通过 Ruffle 测试框架运行该目录下的测试,由test.tomllog_fetch = truenum_ticks = 10驱动,以output.txt为期望输出比对。
  • 测试性质output.txt中的Navigator::fetch日志(URL、Method、Mime-Type、Body)是 Ruffle 测试框架对网络请求的记录,仅在log_fetch开启时产出,并非 Flash Player 的原始抓包结果;Flash Player 下的真实行为需按 3.3 节配置信任后通过server.py自行验证。

通过本文的流程,你可以复现 Ruffle 对 AVM1 数组 AMF0 序列化的完整验证闭环:从test.as构造边界用例、到server.py抓取真实字节、再到output.txtamf.rs实现逐条对照,最终确认严格数组与混合数组在网络层的行为与 Flash Player 一致。

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

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

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

如何为 MLflow 接入 OIDC 身份提供商实现 SSO 单点登录?

如何为 MLflow 接入 OIDC 身份提供商实现 SSO 单点登录? 【免费下载链接】mlflow The open source AI engineering platform for agents, LLMs, and ML models. MLflow enables teams of all sizes to debug, evaluate, monitor, and optimize production-quality A…

作者头像 李华
网站建设 2026/9/13 11:35:43

如何用 PandasAI v3 语义层接入 PostgreSQL 表并用自然语言查询

如何用 PandasAI v3 语义层接入 PostgreSQL 表并用自然语言查询 【免费下载链接】pandas-ai Chat with your database or your datalake (SQL, CSV, parquet). PandasAI makes data analysis conversational using LLMs and RAG. 项目地址: https://gitcode.com/GitHub_Trend…

作者头像 李华
网站建设 2026/9/13 11:34:00

ANT9921内置升压H类功放芯片原理与实战设计指南

1. 为什么ANT9921不是“又一款普通功放芯片”——从30W单声道背后的真实供电瓶颈说起 你拆过市面上那些标称“30W输出”的小体积音频板吗?我拆过不下二十块,八成以上在满功率播放时,电源端口会明显发烫,用万用表一测,V…

作者头像 李华
网站建设 2026/9/13 11:30:55

MogaNet图像分类实战:从模型结构到训练与遮挡分析

简介:面向深度学习研究者与计算机视觉初学者的MogaNet图像分类实战资源包,聚焦多阶博弈论交互视角下的纯卷积神经网络设计。MogaNet在ImageNet等基准上兼顾精度与参数效率,这套资料围绕其训练、评估与推理流程,帮助读者快速上手并…

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

CharLS源码解析:JPEG-LS无损压缩编码链路与工程实践

简介:面向图像压缩算法研究者和C开发者的CharLS开源库1.0源码包,专门实现JPEG-LS无损/近无损压缩标准,提供编码解码、头文件接口及算法仿真分析所需的核心模块。压缩包共78个文件,约4.48MB,包括接口实现、核心jpegls算…

作者头像 李华