From 1aba9875f07d8e609eb5bf42810a4d8f40d15d3f Mon Sep 17 00:00:00 2001 From: tei Date: Wed, 21 Oct 2026 15:38:35 +0900 Subject: [PATCH] feat(ios): expose lossless-or-reject style filter API --- ..._iOS原始Filter无损Native接口_Codex任务_v1.md | 33 ++ ..._iOS原始Filter无损Native接口_Codex任务_v2.md | 20 + .../STEP-120_iOS原始Filter无损Native接口_v1.md | 43 ++ .../STEP-120_iOS原始Filter无损Native接口_v2.md | 43 ++ ...原始Filter无损Native接口_Codex完成报告_v1.md | 147 +++++++ ...原始Filter无损Native接口_Codex完成报告_v2.md | 219 ++++++++++ ..._iOS原始Filter无损接口_RDC编译验收报告_v1.md | 23 ++ platform/darwin/src/MLNVectorStyleLayer.h | 54 +++ platform/darwin/src/MLNVectorStyleLayer.mm | 374 ++++++++++++++++++ 粒子开发记录.md | 15 + 10 files changed, 971 insertions(+) create mode 100644 docs/40_任务/STEP-120_iOS原始Filter无损Native接口_Codex任务_v1.md create mode 100644 docs/40_任务/STEP-120_iOS原始Filter无损Native接口_Codex任务_v2.md create mode 100644 docs/50_Step记录/STEP-120_iOS原始Filter无损Native接口_v1.md create mode 100644 docs/50_Step记录/STEP-120_iOS原始Filter无损Native接口_v2.md create mode 100644 docs/报告/STEP-120_iOS原始Filter无损Native接口_Codex完成报告_v1.md create mode 100644 docs/报告/STEP-120_iOS原始Filter无损Native接口_Codex完成报告_v2.md create mode 100644 docs/报告/STEP-120_iOS原始Filter无损接口_RDC编译验收报告_v1.md diff --git a/docs/40_任务/STEP-120_iOS原始Filter无损Native接口_Codex任务_v1.md b/docs/40_任务/STEP-120_iOS原始Filter无损Native接口_Codex任务_v1.md new file mode 100644 index 000000000..f32d1473d --- /dev/null +++ b/docs/40_任务/STEP-120_iOS原始Filter无损Native接口_Codex任务_v1.md @@ -0,0 +1,33 @@ +# STEP-120 — iOS 原始 Style Layer Filter 无损 Native 公共接口(JOINT-PBF-001 D4-13 支持方) + +## 1. 主责和环境 + +- 联调唯一业务主责:NavSea;本任务专业支持方:MapLibreParticleLab,只增加 iOS Native SDK 的可复用无损 Filter 公共接口,不能把 NavSea Provider/CM95 逻辑迁到 Native。 +- Mac 原仓库 `/Users/tei/dev/MapLibreParticleLab`,原分支 `particle/main`,当前 HEAD `9d6c9e9684e4c71cc2fb3f08bfb27e1638be4188`,开工必须重新检查 Git status/worktree/submodule,预期干净。**禁止新建/切换 branch/worktree**、reset/clean、stage/commit/push/tag、升级依赖或发行,保留其它项目。 +- Codex **GPT-6.1 Sol High**,由外部 CLI 明确配置;只实现/自检,不执行 Bazel/Xcode/Android Gradle 构建或发布。后续 RDC 负责独立编译和版本发行管理。 +- 开始前必须完整读取本仓 `AGENTS.md`、`PARTICLE_BASELINE.md`、`docs/00_项目规范/`、`docs/00_项目总则/README.md` 及 README 中现行全部规范、`docs/00_项目总则/当前基线与开发路线_v2.md`、本任务和最新 iOS 正式发行文档 `release/maplibre-particle/13.6.1-particle.27/README.md`、当前 C++/Darwin 实际源码。本仓早期 Linux 说明与当前 Mac 实际基线冲突时,以已确认 Mac 主机事实为准;报告真实差异,不自行编辑项目总则。 + +## 2. 真实问题与任务目标 + +NavSea MLRN11.5.0 D4-11 Android 已支持 existing Style Layer 三个 Filter 接口且 Mac Kotlin/Java/AAR 编译通过;D4-12 iOS 支持被明确阻断:`MLNVectorStyleLayer.predicate` 的 C++ `Filter` → NSPredicate 转换对某些复杂 `all` / 带 `locale` 条件会有损,加载时 `styleJSON` 不是当前 Layer 状态,无法保证外部修改检测和原图层 Filter 精确恢复。不得使用 `NSPredicate` getter 或 Style 初始化 JSON 伪造安全快照。 + +本 Step 仅在 MapLibre iOS Native SDK 的 `MLNVectorStyleLayer`(或同一现有 Style Layer 类层次)提供 **C++ `style::Filter` → 原始 JSON 对象无损读取 + JSON 对象 → C++ `style::Filter` 直接设置** 的最小公开 ObjC API,让 MLRN 按已冻结单一 Context 语义完成操作。直接使用 `self.rawLayer->getFilter().serialize()` 和真实 `MLNJSONObjectFromMBGLValue`,设置通过 `mln::style::conversion::convert(mln::style::conversion::makeConvertible(json),error)` 解析并且只在成功后 `self.rawLayer->setFilter()`,不经过 NSPredicate 的有损中转。具体以真实可编译 SDK API 为准。 + +## 3. 严格修改范围及安全条件 + +允许修改:`platform/darwin/src/MLNVectorStyleLayer.h`、`platform/darwin/src/MLNVectorStyleLayer.mm`,如果必须引用已有内用辅助头可包含,不修改具体 Style Layer 生成器,不增加 NavSea 专用 API、不修改 C++ 算法/滤镜语义、不修改 Android/Particle/RenderCore。若真实源码要求触及其他文件,先报阻断,不自动扩大。 +- 暴露稳定、简洁、明确的 ObjC 方法,例如 `- (nullable id)rawStyleFilterJSONObject`、`- (BOOL)setRawStyleFilterJSONObject:(id)json error:(NSError **)error`;最终名称与语义请依据当前代码风格确定,务必公开到随 XCFramework 构建的 Headers。 +- **未修改的原始 Filter** 必须从当前 rawLayer 的 Filter 精确读取,不能触发模型折叠/locale/条件丢失;序列化只允许无损 `mln::Value` 到 Foundation JSON(含 NSNull)。过深/不合法输入应拒绝且不修改原层。源 Layer 已 invalid/detach、非 Vector Layer、解析错误必须拒绝或抛出明确错误。Style generation/Layer ID/revision 上下文属于 MLRN Host,SDK 不自行创建另一套状态管理。 +- 新接口只改 Filter,绝不可删除 Layer、改变 Paint/Layout/Source/Z-order、重载 Style 或触碰 Particle 渲染。保持既有 `predicate` 原功能/签名及其旧行为不变。 +- 提供至少三组源码级安全样例(含 `all` 第二个属性与 `locale` 参数、一般 `==`、原为空)作为受限测试/报告中的验证条件;可以用现存 Core/Style 单测框架若实际可用,但本任务不涉及 Jest。无可运行单测时记录待 RDC Test Gate,不编造 PASS。 + +## 4. Codex 验收输出 + +- 静态检查(无构建时可做 C++/ObjC 语法和源码审查,绝不可冒称 iOS 编译);`git diff --check`。 +- 所有已完成/未完成/无法验证项和当前 ABI 兼容/新 API 约束写入中文报告: + `docs/报告/STEP-120_iOS原始Filter无损Native接口_Codex完成报告_v1.md`,如果同题已存在须递增版本,禁止覆盖。 +- Step 文件 `docs/50_Step记录/STEP-120_iOS原始Filter无损Native接口_v1.md`,以及 `粒子开发记录.md` 仅追加。不能编辑现有旧报告、Release README/SOURCE_LOCK 或产物记录。 +- 结论 `IMPLEMENTED_WAITING_RDC_IOS_BUILD` 或明确 BLOCKED;不标记 iOS 可打包、发版、真机 PASS;不创建/切换 Git 分支/worktree、不执行 Commit/Push/Tag。 +- GPT/RDC 将独立复核并执行实际 Mac Bazel iOS SDK 精确编译,复测无损接口可用性;如通过,另起正式发行/NavSea Host 集成步骤,遵守 GSRL 与跨项目发布边界。主责仍 NavSea。 + +目标不漂移:本 Step 为双平台原始 Style Layer Filter 功能提供底层无损基础,不是原始多源 Provider 项目的最终完成。 diff --git a/docs/40_任务/STEP-120_iOS原始Filter无损Native接口_Codex任务_v2.md b/docs/40_任务/STEP-120_iOS原始Filter无损Native接口_Codex任务_v2.md new file mode 100644 index 000000000..42b6e3b85 --- /dev/null +++ b/docs/40_任务/STEP-120_iOS原始Filter无损Native接口_Codex任务_v2.md @@ -0,0 +1,20 @@ +# STEP-120 v2 — iOS Style Filter 无损或拒绝的最小 Native 接口(续行) +唯一业务主责 NavSea;专业支持仓 Mac `/Users/tei/dev/MapLibreParticleLab`,原 `particle/main@9d6c9e9684e4c71cc2fb3f08bfb27e1638be4188`,禁止创建/切换分支/worktree、reset/clean、Git stage/commit/push/tag/发布。Codex 必须 **GPT-6.1 Sol High**。用户授权 Mac 自动完成双平台以便之后自行打包。本续行任务针对 v1 的真实阻断**收敛准入范围**,不是降低安全条件。 + +## 必读和原始目标 +阅读仓库 AGENTS.md、PARTICLE_BASELINE.md、docs/00_项目规范、docs/00_项目总则 README 及全部现行总则、当前路线、v1任务 `docs/40_任务/STEP-120_iOS原始Filter无损Native接口_Codex任务_v1.md`、v1报告 `docs/报告/STEP-120_iOS原始Filter无损Native接口_Codex完成报告_v1.md`、当前 native iOS Filter源码及 Git status。原目标仍是 NavSea 多 PBF Provider/唯一 Mapping/原始 Style/四档/Pickup/Danger/Planning;本 Step 只解 iOS Filter 快照/修改底层能力,不碰业务内容。 + +## v1 阻断的确认与限定设计裁决 + +1. 当前 `MLNJSONObjectFromMBGLValue` 使用 c_str 导致 Foundation 对合法内嵌 NUL 字符串/键可能截断(另有数值和 legacy Filter 折叠风险)。**无需为了支持所有 JSON 扩大修改到全局转换器**。只对本次 API 的数据执行 preflight 与后验验证:支持被证明转换无损的 Filter;任何包含 U+0000、超出大小/深度/节点限制、数字不能无损转换、Core 重新转换/serialize 与原始 C++ `Filter.serialize()` 不等价、或无法证明互逆的输入必须 **E_FILTER_UNSUPPORTED / NSError**,不能返回截断后的成功对象。用 C++ `mln::Value` 结构/语义等价检查和反向 Core `conversion::convert` 验证;仅对可重放的子集返回结果。 +2. Layer 在正常 `removeLayer:` 后 `rawLayer` 可能仍合法但已不属于 Style;SDK 只有通用“rawLayer 是否有效”检验,**Style 成员关系必须由 MLRN Host 调用侧校验**(当前 Style 根据 Layer ID 返回的对象身份、Token 等)。SDK API 只操作**当前有效 rawLayer**,不能伪称它能证明当前 MapStyle Membership。严格记录此边界到 API Doc 和报告,不因单独 Native SDK 无 MapView 访问而阻断可行实现。已销毁的 rawLayer 要 reject。 +3. 保留 `NSPredicate` getter/setter 现有行为不变。提供新的独立 ObjC API,在现有 `platform/darwin/src/MLNVectorStyleLayer.h`/.mm 内使用真实 C++ Filter 与现有转换器,无 NSExpression/NSPredicate 读写中转。空 filter 可返回 NSNull,恢复 set Filter(),上游 MLRN 如遵循 Android 约束可选择 fail-closed 不下发 null 修改。 +4. 新 Native Setter 必须先确保输入 JSON 是支持类型(数组/NSNull 等),在调用 `rawLayer->setFilter` 之前转换、完整验签;绝对不能先写再发现数据损失。若 Core setter 后与预期不符应作可验证原状态回滚/抛错误,必要时由 Host 毒化 context。方法命名稳定、NSError 代码明确,公开头将由当前已确认 Bazel 目标收录。 +5. 修改范围仍仅 `platform/darwin/src/MLNVectorStyleLayer.h` 与 `.mm`,另只新增中文 Step、报告、追加开发记录;不允许修改全局 `MLNStyleValue.mm` 等辅助文件、不改 C++ Filter、生成器、iOS Build、Android、Particle。若确实无法在两文件实现“无损或拒绝”,再次明确阻断,不制造占位。 + +## 实施与校验要求 +- 实现并精确记录拒绝域:包含 NUL 的字符串/键、特殊数值、大/深对象、legacy filter、SDK不支持的复杂表达式;尽可能不阻断常见 `all / any / ! / get / has / == / literal`,但宁可拒绝也不得删语义。 +- 静态证明输入与原始 Core `Filter.serialize()` 一致性,至少采集简单等值、范围两个属性、locale,以及 NUL 拒绝例;如果不能在 Codex 环境运行 Native 单测,标记 RDC 待测,不冒称通过。 +- Codex 不运行 Bazel/Xcode/Gradle 长构建或 Jest;只做 `git diff --check` 等精确源码检查,RDC 后续负责真实 iOS Metal 编译和产物。 +- 必须输出中文 `docs/报告/STEP-120_iOS原始Filter无损Native接口_Codex完成报告_v2.md`(若已存在则新版本),`docs/50_Step记录/STEP-120_iOS原始Filter无损Native接口_v2.md`,追加 `粒子开发记录.md` 而不覆盖 v1;报告逐项完成/未完成/无法验证,Git/源码/编译/设备证据边界明确。 +- 禁止任何 Git 分支切换、stage/commit/push/tag、正式发行、破坏其它工作区修改;Codex 只实现;GPT/RDC 完成后严格独立验收。原始多源 Provider 目标仍开放。 diff --git a/docs/50_Step记录/STEP-120_iOS原始Filter无损Native接口_v1.md b/docs/50_Step记录/STEP-120_iOS原始Filter无损Native接口_v1.md new file mode 100644 index 000000000..bc0064bef --- /dev/null +++ b/docs/50_Step记录/STEP-120_iOS原始Filter无损Native接口_v1.md @@ -0,0 +1,43 @@ +# STEP-120 iOS 原始 Filter 无损 Native 接口 v1 + +日期:2026-10-21(JST)。状态:**阻塞(BLOCKED)**。 + +## 目标与基线 + +为 NavSea JOINT-PBF-001 D4-13 提供通用 iOS Native 原始 Style Layer Filter 读取/设置接口,保留全部条件与 locale,由 NavSea 负责业务及单一 Host Context。 + +Mac.lan / macOS 15.7.7 / x86_64,原仓库 `/Users/tei/dev/MapLibreParticleLab`;`particle/main@9d6c9e9684e4c71cc2fb3f08bfb27e1638be4188`。原 worktree 唯一,子模块提交正常。预存未跟踪 STEP-120 任务书,tracked 与暂存区开工无差异。既有 iOS 发行版本 `6.29.0-particle.27`,其构建源 `970e3ad606e59873a32e9389e65ef257e1c259bb`,不包含本 Step 接口。 + +用户指定 `GPT-6.1 Sol High`;执行器实际配置未取得可独立核实元数据,没有主动切换模型或启动子代理。 + +## 范围与禁止事项 + +源码只允许 `platform/darwin/src/MLNVectorStyleLayer.h/.mm`。允许新增本 Step、递增完成报告及末尾追加开发记录。禁止修改辅助实现、Layer 生成器、C++ Core、Android、Particle、RenderCore、发行记录及跨项目代码;禁止分支/worktree 变更、stage/commit/push/tag、reset/clean、升级依赖、Bazel/Xcode/Gradle 构建或发布。 + +## 必做项 + +- [x] 完整读取仓库规则、现行总则、任务书、V27 README 与相关实际 Darwin/Core 源码。 +- [x] 重新检查主机、branch、HEAD、status、worktree、submodule;保留预存任务书。 +- [x] 静态确认现有 Core Filter 与 Foundation 转换接口及公开 Headers 构建链。 +- [x] 报告严格无损与 detach 条件的源码级阻塞,不自行扩大范围。 +- [x] 在完成报告提供复杂 all/第二属性/locale、一般等号、原为空三组条件及安全反例。 +- [ ] 实现无损 getter / setter 及公开声明。 +- [ ] 完成深度/输入/失效/detach/非 Vector 防护。 +- [ ] RDC 独立语法编译、iOS SDK 构建及样例实际验证。 +- [x] 新增中文报告并仅追加开发记录;落盘后静态检查结果记录在完成报告末尾。 + +## 当前结果与阻塞 + +未修改 SDK 源码,公共接口未实现,现有 `predicate` 与 ABI 保持原状。 + +1. 指定 `MLNJSONObjectFromMBGLValue()` 对字符串与键使用 `c_str()`,当前反向 Foundation 转换同样不传字符串长度;合法 JSON 的嵌入 NUL 会被截断,不能直接包装后承诺完整无损。修复既有辅助实现超出本轮源码范围;另写独立转换则需明确调整指定复用路径。整数转 double 的精度也需纳入后续门槛。 +2. 正常 `removeLayer:` 后 `_pendingLayer` 仍拥有有效 rawLayer;现有失效宏只检查 rawLayer 非空,没有权威 Style 成员关系查询。不能将其写成已拒绝 detach。已发出语义澄清,未答复不视为授权。 +3. 模型/推理参数只有用户指定值,无独立运行元数据证据;如需严格核实,由外部执行器补齐。 + +## 检查、输出与下一步 + +本轮仅做只读调查和静态自检,未执行任何编译、链接、codegen、单测、设备或发布操作。不存在 iOS 构建、运行或打包 PASS。 + +完成报告:`docs/报告/STEP-120_iOS原始Filter无损Native接口_Codex完成报告_v1.md`,含具体源码位置、三组样例、空 Filter 特殊处理、ABI 状态、准确 RDC Bazel 单项命令和独立证据清单。 + +先冻结 B1/B2 的契约与实现范围,再由 Codex 实现、RDC 独立验证;如通过,另起发行/NavSea Host 集成步骤。本 Step 不进入后续发布阶段,联调唯一业务主责仍 NavSea。 diff --git a/docs/50_Step记录/STEP-120_iOS原始Filter无损Native接口_v2.md b/docs/50_Step记录/STEP-120_iOS原始Filter无损Native接口_v2.md new file mode 100644 index 000000000..dc8b1de57 --- /dev/null +++ b/docs/50_Step记录/STEP-120_iOS原始Filter无损Native接口_v2.md @@ -0,0 +1,43 @@ +# STEP-120 iOS 原始 Filter 无损 Native 接口 v2 + +日期:2026-10-21(JST)。Step 状态:**进行中**;Codex 实现交付:**IMPLEMENTED_WAITING_RDC_IOS_BUILD**。 + +## 目标、基线与授权裁决 + +NavSea 是 JOINT-PBF-001 D4-13 唯一业务主责。本轮只实现通用 iOS Native Filter 的安全快照/修改能力,原始多源 Provider、唯一 Mapping、原始 Style、四档及 Pickup/Danger/Planning 总目标仍开放。 + +实际 Mac.lan / macOS 15.7.7 / x86_64;原仓 `/Users/tei/dev/MapLibreParticleLab`,`particle/main@9d6c9e9684e4c71cc2fb3f08bfb27e1638be4188`。原 worktree 唯一,子模块正常;开工已有 v1 开发记录追加及 v1/v2 未跟踪任务书、v1 Step/报告,完整保留。正式 iOS 仍为 `6.29.0-particle.27`,不含新接口。 + +用户指定外部执行器 `GPT-6.1 Sol High`,当前工具无独立模型/档位元数据,不宣称已核实;没有切模型或启动子代理。 + +v2 明确:仅支持可证明无损重放的子集,其余 E_FILTER_UNSUPPORTED;Style 成员关系由 Host 校验;Native 只检查有效 rawLayer,正常 pending/detach 可用;空值通过 NSNull 精确恢复 Filter()。 + +## 范围与禁止事项 + +源码仅 `platform/darwin/src/MLNVectorStyleLayer.h/.mm`;文档只新增本 v2 Step/报告、末尾追加开发记录。禁止改共享转换器、C++ Core、生成器、Build 配置、Android、Particle、业务与发行记录;禁止 Git stage/commit/push/tag、分支/worktree 变更、reset/clean、依赖升级或发布。Codex 不执行长构建、Native 单测、Jest 或设备操作。 + +## 必做项与当前结果 + +- [x] 重新读取规范、总则、路线、v1/v2 任务和 v1 报告;核对实际源码及 Git/主机基线。 +- [x] 在公共头声明 `rawStyleFilterJSONObjectWithError:` 与 `setRawStyleFilterJSONObject:error:`,并提供稳定 NSError 域和五个代码。 +- [x] 在指定 `.mm` 局部实现 Foundation 独立快照、C++ Value 归一/比较、真实桥接及 Core 反向解析;不经 NSPredicate/NSExpression。 +- [x] 预检 NUL、UTF-8、特殊/超范围数值及大小限制;深度 64、节点 4096、UTF-8 字符串/键合计 256 KiB。 +- [x] 明确支持的操作符候选、受限 legacy 域及结构变化拒绝;同时核对实际 Core 表达式,不依赖 Filter 指针相等。 +- [x] 先验证输入及原状态再写入;写后核对,异常/不符时恢复原 Filter 并独立核实,无法确认则要求 Host 废弃 Context。 +- [x] 文档明确主线程、有效 rawLayer 与 Host Style 成员关系的边界;没有另建 Context/generation/revision。 +- [x] 原 predicate/description 实现保持;不修改其他 Layer 属性或移除重建 Layer。 +- [x] 报告交付简单等值、两个属性范围、locale、NUL 拒绝、空 Filter 及 common 组合样例与源码依据。 +- [x] 新增中文 v2 报告并末尾追加开发记录;静态空白、范围和保护检查。 +- [ ] RDC 独立 ObjC++ 编译、iOS Metal SDK 构建与公开 Headers/调用链接验证。 +- [ ] RDC 实际运行样例、输入边界、回滚注入、线程/生命周期及 Host 成员关系联调。 +- [ ] 后续另行授权发行与 NavSea Host 集成。 + +## 检查、阻塞与下一步 + +只执行精确源码/文档/Git 静态检查,没有编译、codegen、测试或设备结果。两份 v1 文档、两份任务书、共享转换器及 V27 发行文件保持;开发记录保留开工前全部字节,仅追加。具体检查与保护证据见 v2 完成报告末尾。 + +v1 的完整 JSON 与 Native detach 契约阻塞已按用户裁决收敛;当前没有已知必须扩围的源码阻塞,但实现尚未编译,不能确认可构建。存在保守拒绝域、Core 初次序列化开销、回滚通知无法撤销与旧 SDK 不含新符号等限制;这些均已在 API 文档及报告说明。 + +下一步切换 RDC 独立审计,并在同工作区运行 `bazel build -c opt --//:renderer=metal //platform/ios:MapLibre.dynamic`;真实失败返回 Codex 修复。成功后另起发行/Host 集成,不在本轮自动推进。 + +完成报告:`docs/报告/STEP-120_iOS原始Filter无损Native接口_Codex完成报告_v2.md`。状态维持等待 RDC 验证,不宣布本 Step 或原业务总目标最终完成。 diff --git a/docs/报告/STEP-120_iOS原始Filter无损Native接口_Codex完成报告_v1.md b/docs/报告/STEP-120_iOS原始Filter无损Native接口_Codex完成报告_v1.md new file mode 100644 index 000000000..c03a630b6 --- /dev/null +++ b/docs/报告/STEP-120_iOS原始Filter无损Native接口_Codex完成报告_v1.md @@ -0,0 +1,147 @@ +# STEP-120 iOS 原始 Filter 无损 Native 接口 Codex 完成报告 v1 + +日期:2026-10-21(JST)。结论:**BLOCKED(未实现公共接口)**。 + +本轮完成前置阅读、实际源码调查与交接条件整理。当前“仅改两个文件、直接复用指定转换器、所有原始 Filter 无损、Native 拒绝 detach Layer”四项要求存在契约阻塞;没有修改 SDK,也没有将调查或空白检查写成接口实现、编译或运行 PASS。NavSea 仍为联调唯一业务主责,D4-13 未解除阻塞。 + +## 1. 环境、模型与 Git 基线 + +- 实际主机:`Mac.lan`,Darwin `24.6.0`,macOS `15.7.7 (24G720)`,`x86_64`。 +- 仓库:`/Users/tei/dev/MapLibreParticleLab`;分支:`particle/main`;开工 HEAD:`9d6c9e9684e4c71cc2fb3f08bfb27e1638be4188`。 +- `git worktree list --porcelain` 仅列出原仓库;`git submodule status` 全部为正常空格前缀,没有未初始化、偏离提交或冲突标记。本轮没有修改子模块。 +- 开工 tracked 工作区和暂存区无差异,但不是完全干净:预先存在未跟踪任务书 `docs/40_任务/STEP-120_iOS原始Filter无损Native接口_Codex任务_v1.md`。保留该文件,未 stage。 +- 用户指定外部 CLI 配置为 `GPT-6.1 Sol High`(`gpt-6.1-sol / high`)。当前工具未提供执行器模型/推理参数元数据,实际配置无法独立核验;不把用户指定值写成已核实的运行身份。没有切换模型、启动其他 Codex 实例或子代理。 +- 只读发行文档:Android/RN `13.6.1-particle.27`;iOS Metal/SwiftPM `6.29.0-particle.27`;发行构建源码 `970e3ad606e59873a32e9389e65ef257e1c259bb`;正式 Tag `maplibre-particle/rn/13.6.1-particle.27`。这些是既有发行记录,不包含本 Step 的新接口。 +- 已完整读取 `AGENTS.md`、`PARTICLE_BASELINE.md`、`docs/00_项目规范/` 全部文档、项目总则 README 指定的四份现行总则、当前任务书、V27 README,并回读相关 Darwin/C++ 实现与上一 Step 生命周期修复报告。 +- 历史 Linux 路径、Ubuntu 环境及旧版本坐标与当前 Mac/V27 事实不同;遵循当前任务,不修改总则、不创建分支。当前任务指定的报告目录与 Git 禁止项优先于历史流程。 + +## 2. 阻塞证据与边界 + +### B1:指定复用的 JSON 桥接并非对所有合法 JSON 字符串无损 + +实际源码: + +- `platform/darwin/src/MLNStyleValue.mm:16`:字符串使用 `@(value.c_str())`;第 34 行字典键同样使用 `c_str()`。两处都采用以 NUL 结尾的字符串语义。 +- `platform/darwin/src/MLNConversion.h:105` 起:`toString()` 使用 `std::string([value UTF8String])`,没有传入 UTF-8 字节长度;字典成员枚举的键也使用无长度构造。 +- `src/mln/style/rapidjson_conversion.hpp` 的字符串及键转换使用 `GetStringLength()`,因此从 Style JSON 进入 Core 的字符串可以保留嵌入 NUL。 + +可复核的源码级反例:合法 JSON Filter `["==","name","a\u0000b"]` 可在 Core 保留三字节字符串。`Filter::serialize()` 保留其值,但指定 getter 桥接会将该字符串变成 `@"a"`;setter 直接使用当前 Foundation 转换器也会截断同类输入。这是源码推导,**没有执行 iOS 实验**。 + +只增加两个直接包装方法会公开有损接口。预检后拒绝嵌入 NUL 可以避免静默损坏,却仍无法提供任务要求的所有未修改原始 Filter 的精确快照,属于缩小契约,不能自行采用。 + +若继续坚持通用无损约束,需确认以下实现路径之一:授权修复既有辅助文件 `MLNStyleValue.mm`、`MLNConversion.h`;或明确允许在新接口内实现独立的长度感知转换,调整“直接使用真实既有辅助函数”的要求。前者超出当前两个源码文件范围,并需要验证共享转换器对旧接口的影响;本轮两者均未实施。 + +此外,Foundation 转换器的数值 `toValue()` 使用 `doubleValue`,而 Core 的 legacy Filter 序列化可保留 `uint64_t/int64_t`。例如 legacy 数值 `9007199254740993` 存在 setter 精度丢失风险。后续必须验证并明确精确支持或失败拒绝,不能将 Foundation 输出持有整数等同于双向无损。 + +### B2:rawLayer 存活不等于仍属于当前 Style + +- `platform/darwin/src/MLNStyleLayer.mm:15` 起仅保存私有 `_pendingLayer` 与 `_weakLayer`;`rawLayer` 返回 `_weakLayer.get()`。 +- 同文件 `removeFromStyle:` 在成功移除时把 Core Layer 所有权转回 `_pendingLayer`;正常移除后 `rawLayer` 仍有效,可重新加入 Style。 +- `MLNStyleLayer_Private.h` 的 `MLNAssertStyleLayerIsValid()` 只拒绝空 `rawLayer`,未提供 Style 成员关系检查。公共及私有现有 Layer 接口也没有 owning Style / attached 查询。 + +因此,这个宏可以处理因销毁而失效的 Layer,却不能满足“Native 拒绝所有正常 detach Layer”的严格解释。仅在两个文件内添加新的生命周期标记不能替代权威 Style 查询;本轮不建立另一套 Host Context、generation、revision 或生命周期跟踪状态。 + +已通过异步澄清说明两种语义:沿用 SDK 有效 Layer 语义并由 Host 校验成员关系;或坚持 Native 拒绝 detach、在当前范围下报 BLOCKED。落盘时尚无答复,不把未答复当作放宽安全条件的授权。即使选择前者,B1 仍需解决。 + +### 已确认可以复用的事实 + +- `mln::style::Layer` 已有 `getFilter()` / `setFilter()`;`mln::style::Filter` 已有 `serialize()`。 +- `MLNStyleValue_Private.h` 声明真实 `MLNJSONObjectFromMBGLValue()`;`MLNConversion.h` 提供 `makeConvertible(id)`;`mln/style/conversion/filter.hpp` 提供 `Converter`。现有 `NSPredicate+MLNAdditions.mm` 是直接转换调用的实际参考。 +- `Filter{}` 序列化为 `NullValue`,既有 Foundation 桥接将其变成 `NSNull`。但 Core 当前转换 `null` 的 legacy 分支生成恒真表达式,不能直接把这一步当作原空 Filter 的精确恢复;新 setter 应对空值明确设置 `Filter{}`。 +- `platform/darwin/bazel/files.bzl` 已将 `MLNVectorStyleLayer.h` 列入 `MLN_DARWIN_OBJC_HEADERS`,`.mm` 已登记源码;`platform/darwin/BUILD.bazel` 的 `darwin_objc_hdrs` 被 `platform/ios/BUILD.bazel` 的 `public_hdrs` 引用并传入 `MapLibre.dynamic`。无需为公开声明修改生成器或构建配置。 + +## 3. 实际修改与 ABI 状态 + +仅有以下项目文档变更: + +| 文件 | 处理 | +| --- | --- | +| `docs/报告/STEP-120_iOS原始Filter无损Native接口_Codex完成报告_v1.md` | 新增本报告;创建前不存在同题版本。 | +| `docs/50_Step记录/STEP-120_iOS原始Filter无损Native接口_v1.md` | 新增阻塞状态与交接清单;创建前不存在同题版本。 | +| `粒子开发记录.md` | 仅末尾追加本 Step 事实。 | + +`MLNVectorStyleLayer.h/.mm`、全部辅助源码、C++ Core、具体 Layer 生成器、Android、Particle、RenderCore、发行文档及 SOURCE_LOCK 均未修改。没有新增 ObjC selector、导出符号、实例变量或 C++ ABI 变更;当前 SDK **没有** `rawStyleFilterJSONObject` / `setRawStyleFilterJSONObject:error:`。不应让 Host 直接调用尚不存在的方法。 + +阻塞解除后的接口草案仍为以下两个 selector,**不是本轮已交付 API**: + +```objc +- (nullable id)rawStyleFilterJSONObject; +- (BOOL)setRawStyleFilterJSONObject:(nullable id)json + error:(NSError * _Nullable * _Nullable)error; +``` + +新接口应说明线程与现有 Style 修改入口一致;getter 从当前 Core Filter 取值,不经 NSPredicate 或初始化 styleJSON;setter 先检查 JSON 类型、有限数值、深度及循环容器,再解析临时 Filter,只有成功后才能调用 `setFilter()`;失败不修改原层。`nil` / `NSNull` 的清空语义及错误域/异常应在实现时冻结。不得更改原 `predicate` 的签名或旧行为,也不得通过移除重建 Layer 实现恢复。 + +## 4. 至少三组安全样例与 RDC Test Gate + +以下是交接的源码级断言条件,**均未编译、未运行**,没有新增或修改测试文件。可在授权后使用现有 Darwin `MLNStyleLayerTests` / `MLNPredicateTests` 框架组织测试;当前任务禁止扩大测试文件范围,本轮将样例保存在报告中。 + +### 样例 A:两个不同属性的 all,加 locale + +```json +["all", + ["<=", ["string", ["get", "name"]], "m", + ["collator", {"case-sensitive": false, "diacritic-sensitive": true, "locale": "de"}]], + [">=", ["string", ["get", "category"]], "a", + ["collator", {"case-sensitive": false, "diacritic-sensitive": true, "locale": "de"}]]] +``` + +从当前运行时 Layer 读取 `before`,确认含第二个属性 `category`、两个条件及两个 `locale=de`。设置独立覆盖 Filter 后恢复 `before`,断言读取对象与 `before` 结构相等,Core `serialize()` 的对应树一致。不要只断言 NSPredicate 相等。另以数值 `all` 的 `minDepth <= 10` 与 `maxDepth >= 2` 验证不同属性不会折叠成同一属性的 BETWEEN。 + +### 样例 B:一般等号 + +```json +["==", "kind", "buoy"] +``` + +对 legacy 形式及表达式形式 `["==",["get","kind"],"buoy"]` 分别执行读取、覆盖、恢复;以当前 Core 序列化结果为比较基准。外部通过其他有效 SDK 入口修改 Filter 后,新 getter 必须反映当前状态,不能回读初始化 JSON。全过程记录 Layer 身份、ID、Source、Paint/Layout 与层序,确认仅 Filter 变化。 + +### 样例 C:原为空 + +构造当前 Core `Filter{}`,getter 应返回 `NSNull`;覆盖后以该快照恢复,断言 Core `bool(getFilter()) == false`,`serialize()` 仍为 NullValue,而非恒真表达式。若接口最终允许 `nil`,同样验证其清空行为。`NSNull` 也应在合法嵌套 JSON 位置保留,不得吞掉数组成员。 + +### 必需反例与扩展条件 + +| 条件 | 必须观测的结果 | +| --- | --- | +| 当前 Filter 含 `a\u0000b`;对象键含嵌入 NUL | 阻塞解除后应精确保留字符及键长度;不能静默变成 `a`。 | +| legacy 整数 `9007199254740993` | 精确往返,或在变更前明确拒绝不支持的输入;不能静默取整后返回成功。 | +| 超过冻结深度上限、循环数组/字典、非字符串键、NSDate/NSData、NaN/Infinity、Core 解析错误 | 明确失败,设置前后 Core Filter 和其余 Layer 状态完全一致。深度上限须在实现时明确,当前未冻结数值。 | +| 销毁 Style 后 rawLayer 失效、正常移除仍存活、同名 Layer 替换 | 分别按最终冻结的失效/成员关系契约拒绝;不得用旧指针或只比较 Layer ID 伪造有效性。 | +| Raster/Background Layer 或错误的内部 raw 类型 | 不得走 Vector Filter 设置;明确拒绝/异常。 | +| Core 序列化得到布尔常量 | 验证 getter 快照能否被 setter 精确恢复;现有 Core 顶层 primitive 转换行为需要专门处理,不能假定直接转换总是对称。 | + +## 5. 已执行与未执行检查 + +已执行:仓库规范和任务阅读、实际主机检查、Git branch/HEAD/status/worktree/submodule 只读检查、实际调用签名与公开 Headers 构建链静态审查、三个必需样例的源码推导、保护文件 SHA-256 记录、创建前版本检查。写文档前 `git diff --check` 返回 exit=0;文档落盘后的检查结果见末尾交付记录。 + +没有执行:ObjC/C++ `-fsyntax-only`、Bazel/Xcode/CMake/Ninja/Gradle 编译、链接、codegen、XCTest/GTest/Jest、打包、安装、ADB、模拟器/真机运行、网络发行核验。没有编译/测试失败日志,因为没有启动这些命令。若干只读探查最初使用旧 `mbgl` 路径或不存在的候选辅助头,随后按真实 `mln` 路径核对;这不是构建结果。 + +没有执行任何 stage/commit/push/tag、新建/切换 branch/worktree、reset/clean、依赖升级或发布。没有新增制品,因此没有本 Step 的 XCFramework/APK/AAR 大小、SHA-256 或设备验收证据。没有远端写入,也未查询远端 HEAD。 + +保护的预存任务书 SHA-256:`8231df05be824e414e186471e7ca37139545dcb1390658ae2a2734fb2182c547`。保护文件的落盘前哈希保存在本机会话临时记录 `/tmp/step120_readonly_baseline_hashes.json`,该文件不是仓库交付物;最终检查还应直接与 Git HEAD 对比,以免依赖临时文件存续。 + +## 6. 独立验收命令与后续证据 + +先解决 B1/B2、重新明确允许范围,再实现接口并交 RDC 独立审计。当前阻塞阶段不应仅运行旧 SDK 构建后宣称新接口通过。以下是从当前正式 Pipeline 读取的准确 **iOS 单项构建命令,Codex 未执行**: + +```bash +cd /Users/tei/dev/MapLibreParticleLab +bazel build -c opt --//:renderer=metal //platform/ios:MapLibre.dynamic +``` + +不要为此调用 `release.sh ... build`:该脚本还会执行 Android 构建、Maven Local 发布等本任务未授权步骤。不得以现有 V27 产物替代新实现的编译证据。 + +RDC 构建后应独立读取当次生成的 `bazel-out/darwin_x86_64-opt/bin/platform/ios/MapLibre.dynamic.xcframework.zip`,确认 `ios-arm64/MapLibre.framework/Headers/MLNVectorStyleLayer.h` 中两个公开 selector 均存在。后续提供实际 ObjC 调用编译/链接证据及本报告各样例的 XCTest/运行结果;单纯 ZIP 内出现头文件不足以证明实现可调用。 + +独立验收需保留:执行器实际模型/档位证据;实现后的 HEAD 与精确 diff;完整命令、exit code、耗时、日志路径;当次 XCFramework SHA-256;导出头文件及可调用性;样例的 Core 序列化前后树、Foundation 值/类型与字符串长度;失败输入的原层未变证据;失效/detach/同名替换行为;Paint/Layout/Source/Z-order 未变证据。新发行与 NavSea Host 集成另起授权步骤,遵守 GSRL 与跨项目边界。 + +当前未完成:公共 API 实现、输入防护、源码语法编译、iOS SDK 精确构建、实际无损与生命周期验证。当前 ABI 保持原状不等于新 API ABI 验证通过。结论维持 **BLOCKED**,不标记 iOS 可打包、发版、真机 PASS,也不代表原始多源 Provider 项目完成。 + +## 7. 落盘后的静态交付记录 + +- `git diff --check`:exit=0,无输出;`git diff --cached --exit-code`:exit=0,暂存区未变。Git diff 不包含未跟踪文档内容,另以 Python 检查两份新文档和开发记录的 UTF-8、末尾换行及行尾空白,全部通过。 +- 当前 tracked 差异只有开发记录,增加 7 行、1513 字节;逐字节确认以 Git HEAD 中的完整旧开发记录为前缀,属于末尾追加。 +- 两个指定 SDK 文件、`MLNStyleValue.mm`、`MLNConversion.h` 与 Git HEAD 字节完全一致;这些源码、V27 README/SOURCE_LOCK 及预存 STEP-120 任务书的 SHA-256 均与落盘前记录一致。 +- `git status --short` 仅列开发记录、两份本轮新增文档及原有未跟踪任务书;没有其他项目修改。branch/HEAD 保持 `particle/main@9d6c9e9684e4c71cc2fb3f08bfb27e1638be4188`。 +- 本节是静态文档/范围检查记录,不是语法编译、iOS 构建或测试 PASS。没有解除 B1/B2,也没有新增接口。 diff --git a/docs/报告/STEP-120_iOS原始Filter无损Native接口_Codex完成报告_v2.md b/docs/报告/STEP-120_iOS原始Filter无损Native接口_Codex完成报告_v2.md new file mode 100644 index 000000000..581870092 --- /dev/null +++ b/docs/报告/STEP-120_iOS原始Filter无损Native接口_Codex完成报告_v2.md @@ -0,0 +1,219 @@ +# STEP-120 iOS 原始 Filter 无损 Native 接口 Codex 完成报告 v2 + +日期:2026-10-21(JST)。实现交付状态:**IMPLEMENTED_WAITING_RDC_IOS_BUILD**。 + +本轮按 v2 裁决,在限定两个 SDK 文件中增加“无损或拒绝”的通用 ObjC 公共接口。完成的是源码实现和静态审查,**没有执行 ObjC++ 语法编译、iOS 构建或 Native 单测,尚不能确认编译、链接及运行通过**。v1 报告保留原文;其两个契约阻塞由本轮用户裁决收敛,不据此声称完整 JSON 支持。 + +NavSea 仍为唯一业务主责。本轮没有实现或迁入 Provider、CM95、Mapping、四档、Pickup、Danger、Planning 逻辑;原始多源 Provider 总目标仍开放。 + +## 1. 环境、模型与 Git 基线 + +- 实际主机:`Mac.lan`,Darwin `24.6.0`,macOS `15.7.7 (24G720)`,`x86_64`;仓库 `/Users/tei/dev/MapLibreParticleLab`。 +- 开工分支/HEAD:`particle/main@9d6c9e9684e4c71cc2fb3f08bfb27e1638be4188`。`git worktree list --porcelain` 只有原工作区;子模块状态均为空格前缀,无未初始化、偏离提交或冲突标记。 +- 开工不是干净工作区:开发记录已有 v1 的末尾追加;未跟踪文件包括 v1/v2 任务书、v1 Step 和 v1 报告。它们均保留;本轮不覆盖旧文档、不暂存。 +- 用户指定外部执行器必须为 `GPT-6.1 Sol High`(`gpt-6.1-sol / high`);工具没有可独立核实的模型/推理参数元数据。因此仅记录用户指定配置,实际执行器配置仍待外部证据,不宣称已核实。未自行切换模型、启动另一实例或子代理。 +- 已重新完整读取 AGENTS、PARTICLE_BASELINE、项目规范全部文档、总则 README 及四份现行总则、当前路线、v1/v2 任务书、v1 报告、V27 README 和相关 Darwin/Core 源码。早期 Linux 路径/系统/版本与实际 Mac/V27 不同,按本轮任务执行,未编辑总则。 +- 现有正式坐标仍为 Android/RN `13.6.1-particle.27`、iOS `6.29.0-particle.27`;发行构建源码为 `970e3ad606e59873a32e9389e65ef257e1c259bb`。本轮新接口没有进入该既有发行包。 + +## 2. 实际修改文件与接口 + +| 文件 | 实际修改 | +| --- | --- | +| `platform/darwin/src/MLNVectorStyleLayer.h` | 增加两个公共 selector、错误域/枚举及中文契约说明。 | +| `platform/darwin/src/MLNVectorStyleLayer.mm` | 增加局部预检、独立快照、Core 解析、结构/表达式核对与 Filter 写入后回滚验证。 | +| `docs/50_Step记录/STEP-120_iOS原始Filter无损Native接口_v2.md` | 新增本次实现/待验收清单。 | +| `docs/报告/STEP-120_iOS原始Filter无损Native接口_Codex完成报告_v2.md` | 新增本报告,不覆盖 v1。 | +| `粒子开发记录.md` | 在开工时已有 v1 内容之后仅末尾追加。 | + +公开接口: + +```objc +- (nullable id)rawStyleFilterJSONObjectWithError:(NSError * _Nullable * _Nullable)error; +- (BOOL)setRawStyleFilterJSONObject:(id)json error:(NSError * _Nullable * _Nullable)error; +``` + +getter 成功返回数组或 NSNull;失败返回 nil。setter 接收数组或 NSNull;nil 不是清空指令。两者成功都会将提供的 error 指针置 nil,可以不传 error 指针。采用带 error 的 getter,使不支持的快照和真正的空 Filter 可明确区分。 + +错误域为 `MLNStyleFilterErrorDomain`;NSError.code 使用以下公开枚举,localizedDescription 以对应标识开头,Host 应以域/数值分支而非解析描述文字。 + +| code | 公开枚举 | 标识及意义 | +| --- | --- | --- | +| 1 | `MLNStyleFilterErrorUnsupported` | `E_FILTER_UNSUPPORTED`:不支持、超限、不可逆或写入前验证异常;没有调用目标写入。 | +| 2 | `MLNStyleFilterErrorInvalidLayer` | `E_FILTER_INVALID_LAYER`:rawLayer 不存在、类型不符或写入前已失效/被替换。 | +| 3 | `MLNStyleFilterErrorWrongThread` | `E_FILTER_WRONG_THREAD`:未在主线程调用。 | +| 4 | `MLNStyleFilterErrorMutationFailed` | `E_FILTER_MUTATION_FAILED`:写入或后验核对失败,原 Filter 已恢复且核实;Host 应重新获取状态。 | +| 5 | `MLNStyleFilterErrorRollbackFailed` | `E_FILTER_ROLLBACK_FAILED`:无法确认恢复,Host 必须废弃当前操作 Context。 | + +## 3. 源码安全不变量与静态证明 + +### 3.1 预检与表示等价 + +`NormalizeFilterValue` 直接遍历 C++ `mln::Value`,只接受 JSON 值。`SnapshotFilterJSON` 独立读取 Foundation JSON 为 C++ 值,使用带长度 UTF-8 字节,不调用全局转换器来决定输入是否已经有损。对象键和值、数组成员和顺序、布尔与数值类型均保留;对象键顺序不参与 JSON 等价比较。 + +有限、范围内的整数和浮点数归一为 double 后进行 C++ 树比较,避免整数/浮点存储类型不同导致误拒绝。整数界限为 `±9007199254740991`,整数转 double 在此范围精确;更大的整数/浮点数即使恰好可表示也保守拒绝。NSNumber 仅接受标准整数/float/double/布尔编码,CFBoolean 与普通数值区分,NSDecimalNumber 全拒绝;不做十进制舍入或字符串数值解析。 + +字符串、键在调用真实 `MLNJSONObjectFromMBGLValue` 之前,必须证明 UTF-8 有效且不含 NUL。桥接后再次经独立读取器生成 C++ 树并比较,防止转换产生截断、类型改变或键合并。**未修改** `MLNStyleValue.mm`、`MLNConversion.h` 等全局转换器。 + +### 3.2 getter 的重放证明 + +1. 在主线程检查当前 rawLayer 非空且 Core LayerTypeInfo 为 Geometry/Source::Required,复制当前 Filter;不读取 NSPredicate 或初始化 styleJSON。 +2. 先对表达式树做深度/节点预检,再从当前 Filter 的 `serialize()` 得到值树并检查资源/类型限制。 +3. 只将受支持的数值存储类型精确归一,调用真实 Foundation 桥接,再独立读取并比较值树。 +4. 使用真实 `conversion::convert(makeConvertible(json), error)` 反向解析;要求重放后的 `Filter.serialize()` 与原值树相等。 +5. 同时检查表达式存在性、表达式序列化树和 Core `Expression::operator==`。不以 `Filter::operator==` 的 shared_ptr 身份代替语义检查。 +6. 全部成立才返回快照;不支持或异常返回 NSError,getter 没有任何 Filter 写入。 + +这同时检查 legacy 保留的 JSON 和实际执行的表达式树;仅仅“legacy 原文还在”不足以通过。 + +### 3.3 setter 的写入前准入与后验处理 + +1. 验证 rawLayer/线程,保存原 Filter 副本及原 Layer 弱指针;独立读取调用方 JSON。调用方不得并发修改容器或 Style。 +2. 从受检 C++ 值建立桥接快照,Core 只解析这份独立对象;要求输入树与解析后 `serialize()` 相等。 +3. 对新 Filter 运行 getter 的整条重放证明,再对原 Filter 做相同证明。**原状态不支持快照时也拒绝覆盖**,不把危险旧状态先覆盖再考虑恢复。 +4. 写入前重新核对弱指针/当前 rawLayer 身份及原 Filter;验证期间改变则拒绝。只有这些检查结束后才调用 `setFilter(next)`。 +5. 写入后核对实际 Filter 的值树及表达式;成功才返回 YES。发生异常或不等价则调用 `setFilter(previous)` 恢复保存的真实 Filter,随后独立核对恢复结果。 +6. observer 可在写入/恢复后抛异常,所以恢复后的核对独立执行。弱指针失效时不继续解引用旧地址,返回无法确认恢复的错误。 + +`setFilter` 只有目标写入和回滚两处;没有移除/重建 Layer、改变 Paint/Layout/Source/层序或重载 Style。写入尝试可能触发既有 Core observer/渲染失效;回滚不撤销已产生的通知,Host 不能把错误当作无事件发生,也不能在无法恢复时继续复用原 Context。 + +空 Filter 特例在解析前直接构造 `Filter()`,不把 NSNull 经 legacy converter 变成恒真表达式。Root NSNull 可成功读写;合法嵌套 NSNull 保持原成员位置。 + +## 4. 精确准入与拒绝域 + +| 项目 | 本次约束 | +| --- | --- | +| JSON 深度 | 根为 1,最大 64;对象键也按其子节点深度计数。循环容器会触及深度/节点限制而拒绝。 | +| 节点数 | 单次值树最多 4096,包含容器、值和对象键;表达式树另受 64 层/4096 节点限制。 | +| 字符串大小 | 一棵值树的字符串及键 UTF-8 字节总量最多 262144;不是 HTTP 大小或进程内存限额。 | +| 字符 | 字符串/键任意位置含 U+0000、非法 UTF-8/无法无损编码均拒绝,包含 literal 数据。 | +| 数值 | 拒绝 NaN、Infinity、负零、绝对值大于 2^53-1、NSDecimalNumber、非标准 NSNumber 编码。布尔和数字不合并。 | +| Foundation 类型 | 支持 NSArray、NSString、NSNumber、NSNull、仅字符串键的 NSDictionary;其他类型拒绝。顶层只准数组或 NSNull。 | +| 操作符候选 | `all/any/none/!`,`==/!=//>=`,`get/has/!has/in/!in/literal`,`string/number/boolean` 断言和 `collator`。候选仍必须过 Core 解析及后验比较。 | +| legacy 子集 | 仅明确形状的纯 legacy 组合:all/any/none 的子项仍为 legacy;比较为属性字符串加单个标量;in/!in 为属性字符串加标量列表;has/!has 恰有属性参数。混合表达式、数组/对象操作数或额外参数拒绝。 | +| Core 变化 | setter 输入增加隐式断言、literal 标量展开、常量折叠、缺省 collator 参数补全、丢失未知键或其他结构变化均拒绝;不靠猜测证明等价。 | +| 其他复杂表达式 | match/case/let/within/distance 等不在本次候选集,即使 SDK 其他入口支持也拒绝。未来扩展另行授权。 | +| 常量 Filter | 顶层布尔快照当前拒绝;NSNull 不与恒真布尔合并。 | +| 当前状态 | 原 Filter 不能通过 getter 全路径证明时,setter 同样拒绝覆盖,包括清空操作。 | + +Core 自己的 `serialize()` 必须先产生实际值树,之后才能检查该树的大小;本次限制约束准入值和局部遍历,不承诺 Core 内部首次序列化的绝对内存/耗时上界。对已存在表达式先做遍历上限检查。未增加缓存、并发保障或任意自定义 Foundation 子类的副作用隔离;调用方应传普通 JSON 容器并在主线程串行操作。 + +当前运行时 getter 获得的快照已经过往返验证,可直接作为恢复候选。Host 新造的 setter 输入应使用 Core 可往返形式,例如数值范围显式使用 `number` 断言、collator 显式带两个布尔项;不要把“合法 Style JSON”理解为本次 API 必须接受的全集。 + +## 5. Style 成员关系与 ABI 边界 + +SDK **只操作当前有效 rawLayer**。正常 `removeLayer:` 后仍由 ObjC pendingLayer 拥有的对象、尚未加入 Style 的对象,可以使用本次接口;销毁后的空 rawLayer 拒绝。Host 必须用当前 Style 的 Layer ID 查询结果核对对象身份,并检查自身 Token/代次/Context 与外部修改。Native 没有创建另一套 Context、generation 或 revision;弱指针仅用于本次调用后验核对和回滚。 + +Raster/Background ObjC 对象不继承本接口,Host 应先做 MLNVectorStyleLayer 类型检查。内部错误地给 Vector peer 绑定非 Geometry 或不需 Source 的 rawLayer 时返回 InvalidLayer,不对其写 Filter。 + +本轮新增两个 ObjC selector、一个导出 NSErrorDomain 和枚举,没有新增实例变量、修改 C++ ABI、改变既有方法签名或实现。旧 predicate/description 方法从 `setPredicate:` 起的完整实现后缀与 HEAD 逐字节相同。**二进制兼容性仅有静态依据,尚未编译/链接验证**;旧 V27 二进制不具备新接口,不能使用新头文件链接旧包并宣称可用。 + +`MLNVectorStyleLayer.h/.mm` 已在 `platform/darwin/bazel/files.bzl` 登记;公共头经 Darwin `darwin_objc_hdrs` 传入 iOS `public_hdrs` 与 `MapLibre.dynamic`;iOS umbrella `Mapbox.h` 已 import 此头。未修改生成器、Build 配置或发行记录。 + +## 6. 安全样例及静态采集 + +以下 JSON/断言条件随本报告交付;只核对了生产源码路径,**未编译、未运行 Native 样例,不记测试 PASS**。不在允许范围外新增测试源码,也没有 Jest。 + +### A:简单等值(表达式与受限 legacy) + +```json +["==", ["get", "kind"], "buoy"] +``` + +另测 legacy `["==","kind","buoy"]`。两者分别从当前 Core Filter 取快照、设置其他受支持 Filter、恢复原快照;记录 C++ `Filter.serialize()`、表达式树及 Foundation 值。要求恢复前后的树相等,不以两种 Filter 互相相等替代各自恢复。 + +静态依据:Core comparison 对 `==` 保留 get 与字符串操作数;legacy 原文和值树由 Core 保留,反向生成的实际 legacy 表达式也接受独立相等检查。 + +### B:范围 all 使用两个不同属性 + +```json +["all", + ["<=", ["number", ["get", "minDepth"]], 10], + [">=", ["number", ["get", "maxDepth"]], 2]] +``` + +静态依据:显式类型断言避免 setter 再由 Core 增加 number 包装;两个条件和 `maxDepth` 均在 Value 比较中逐项保留。Core 重放及表达式相等也必须通过,SDK 不走原 NSPredicate 的 BETWEEN 折叠路径。RDC 应另外给原始无显式断言形式做 setter 负例,确认结构变化时返回 Unsupported 且无写入;运行时 getter 对已由 Core 生成断言的 Filter 则从当前序列化读取。 + +### C:两个属性及 locale 参数 + +```json +["all", + ["<=", ["string", ["get", "name"]], "m", + ["collator", {"case-sensitive": false, "diacritic-sensitive": true, "locale": "de"}]], + [">=", ["string", ["get", "category"]], "a", + ["collator", {"case-sensitive": false, "diacritic-sensitive": true, "locale": "de"}]]] +``` + +静态依据:Core `CollatorExpression::serialize()` 明确保留两个布尔项及 locale;本次独立 JSON 读取保留所有对象成员;getter/setter 需要两棵条件树及实际表达式同时相等。RDC 必须断言 `category` 和两个 `locale=de` 均保留,并验证省略布尔项的 setter 输入被保守拒绝。 + +### D:NUL 明确拒绝 + +```json +["==", "name", "a\u0000b"] +``` + +另测 literal 对象键 `a\u0000b`。getter fixture 应从 Core 构造含完整三字节字符串的 Filter,避免经有损辅助函数先构造错误测试基线。setter 通过 NSJSONSerialization 解析上述 JSON 得到含 NUL 的 NSString。两条路径都应返回 Unsupported;setter 比较完整原 Filter/原 Impl 与 observer 计数,确认预检拒绝、没有 setFilter 调用。静态依据为 `FilterBudget::string()` 在桥接之前的 `find('\0')` 检查。 + +### E:空 Filter 与 common 组合 + +Core 原 `Filter()` 的 getter 应返回 NSNull,设置有效 Filter 后用 NSNull 恢复,要求 `bool(getFilter()) == false`,serialize 为 NullValue。另测 `any`、`!`、`has` 与 `["in",["get","kind"],["literal",["buoy","beacon"]]]`,确认其可往返形式可用,literal 内 NSNull 不丢位置。 + +RDC 可在现有 Darwin XCTest 框架加入下列调用形状,完整 fixture 与断言仍需由独立验收实现,当前没有已登记的新 Native 测试: + +```objc +NSError *error = nil; +id saved = [layer rawStyleFilterJSONObjectWithError:&error]; +XCTAssertNotNil(saved); +XCTAssertNil(error); +XCTAssertTrue([layer setRawStyleFilterJSONObject:overrideJSON error:&error]); +XCTAssertNil(error); +XCTAssertTrue([layer setRawStyleFilterJSONObject:saved error:&error]); +XCTAssertNil(error); +XCTAssertEqualObjects(saved, [layer rawStyleFilterJSONObjectWithError:&error]); +``` + +还需独立验证:边界深度/节点/字节上限及超限;循环容器、NaN/Infinity/-0、大整数/NSDecimalNumber、非字符串键、NSDate/NSData;未知操作符、短 `["in"]`、混合 legacy/表达式、Core 解析错误;失效 Layer、非 Vector、后台线程;正常 detach 仍有效且由 Host 阻止错误成员操作;外部 Filter 更新被 getter 读取;返回对象修改不影响 rawLayer;Paint/Layout/Source/层序/Layer 身份不变。 + +回滚测试必须通过测试 observer 或等效受控钩子使 `onLayerChanged` 一次性改写 Filter/抛异常,验证 code=4 的完整恢复;再使恢复无法核实或 Layer 失效,验证 code=5。测试应设置递归保护,不让注入本身无限递归;保存实际前后树及回调计数。当前这些运行证据均缺失。 + +## 7. 已执行检查、未执行项与差异 + +已执行的轻量检查: + +- 机器/版本及 Git branch、HEAD、status、worktree、submodule 只读检查;当前源码/规则重读。 +- 静态核对真实 Value、Filter、Expression、Layer/WeakPtr、Foundation 转换签名;Bazel C++20/异常支持与公开头路径只读审查。 +- `git diff --check` 以及显式两个 SDK 路径和开发记录的 scoped 空白检查:均 exit=0、无输出;不等同编译。 +- Python 源文件核对:旧 `setPredicate:` 至文件末尾字节完全保持;只含两处 `->setFilter(`;保护任务书、v1 文档、共享转换器及 V27 记录哈希均未变化。这些是源码/保护检查,不是 Native 行为测试。 + +没有执行 ObjC++/C++ `-fsyntax-only`、Bazel/Xcode/CMake/Ninja/Gradle 构建、链接、codegen、XCTest/GTest/Jest、打包、安装、ADB、模拟器/真机或性能测试。没有新构建/测试失败日志;不能用静态检查声称它们通过。只读查找中的不存在候选路径已按实际 `mln`/`src` 路径核正,不是编译错误。 + +未 stage/commit/push/tag;未新建/切换分支或 worktree;未 reset/clean、升级依赖、修改其他仓库或发行。没有执行远端操作,故无远端 HEAD 核验。本轮没有产物大小、SHA-256、设备日志或实际运行证据。 + +与 v2 授权范围差异:无源码扩围;getter 命名采用 `...WithError:` 以返回 NSError,符合本轮无损或拒绝语义。Android 未修改/构建;任务开头双平台授权没有覆盖同一任务中明确的两文件范围与禁止长构建规定。 + +## 8. RDC 精确命令与独立证据 + +独立审计本报告与当前 diff 后,在同一实际工作区执行以下 **iOS 单项构建,Codex 未执行**: + +```bash +cd /Users/tei/dev/MapLibreParticleLab +git diff --check +bazel build -c opt --//:renderer=metal //platform/ios:MapLibre.dynamic +``` + +这是现有正式 Pipeline 的真实 iOS target;不要为单项验证调用整套 release.sh,以免执行 Android、Maven Local 或发布步骤。编译失败保留现场,回传首个真实 error、target、必要上下文及完整日志路径,不改宏或放宽 `-Werror` 制造通过。 + +构建后检查当次 `bazel-out/darwin_x86_64-opt/bin/platform/ios/MapLibre.dynamic.xcframework.zip`:在 `ios-arm64/MapLibre.framework/Headers/MLNVectorStyleLayer.h` 中确认两个 selector、错误域和枚举;再进行实际 ObjC 调用的编译/链接与上述 XCTest。当前正式动态 target 为真机 arm64,不把旧包或其他平台产物当作新接口证据。 + +需采集:外部执行器实际模型/档位;不变 HEAD 与本轮精确 diff;真实编译 command/exit/耗时/完整日志;新产物 SHA-256 和 Headers;实际调用编译/链接;各样例的 C++ 原树/表达式与重放结果、Foundation 类型/UTF-8 长度、error domain/code;每种预检拒绝前后状态未变;真实回滚两类错误;Style/Layer/Host 身份与失效边界证据;非 Filter 属性及层序不变。 + +未完成/无法验证项:真实 ObjC++ 编译、链接、iOS Metal SDK 构建、所有 Native 样例执行、回滚异常注入、线程/生命周期和 ABI 消费者验证、真机/Host 联调。若以上成功,另起正式发行与 NavSea Host 集成步骤,遵守 GSRL。本轮不标记 iOS 可打包、发版或真机 PASS。 + +## 9. 最终静态交付记录 + +- 文档落盘后 `git diff --check`、`git diff --cached --exit-code` 均 exit=0、无输出;前者不覆盖 untracked 文档,因此另逐文件检查本轮五个文件的 UTF-8、末尾换行及无行尾空白,全部通过。 +- tracked diff 只有两个 SDK 文件及开发记录:头文件新增 54 行、实现新增 374 行;开发记录相对 HEAD 包含既有 v1 和本轮追加,共新增 15 行。本轮相对续行开工时的完整记录仅末尾增加 2037 字节,已验证原字节全量保留。 +- SHA-256 保护检查确认两份任务书、v1 Step/报告、两个全局转换器及 V27 README/SOURCE_LOCK 与本轮开工时一致。临时保护记录位于 `/tmp/step120_v2_baseline.json`,原开发记录快照位于 `/tmp/step120_v2_development_log_before`;它们是本地自检材料,不是构建产物或仓库交付文件。 +- Python 仅解析报告中四组 JSON 样例,确认 JSON 语法、第二属性、locale 和实际解码的 NUL;不代表 Core 接受或 Native 测试通过。 +- 源码交付 SHA-256:`MLNVectorStyleLayer.h` 为 `7c3e09d3582db6bd170bc091d05fe99a02ebc7f400eff60451758245134d9d6f`;`.mm` 为 `0fb201c86c058381007a405c9933406e25f3fa8d15204b22f46d57bda479326d`。 +- 最终 branch/HEAD 保持 `particle/main@9d6c9e9684e4c71cc2fb3f08bfb27e1638be4188`;暂存区无差异,没有其他文件修改。子模块开工状态正常,本轮未修改子模块或工作区结构。 +- 结论仍为 **IMPLEMENTED_WAITING_RDC_IOS_BUILD**;本节所有通过项都仅是静态源码、文档及范围检查。 diff --git a/docs/报告/STEP-120_iOS原始Filter无损接口_RDC编译验收报告_v1.md b/docs/报告/STEP-120_iOS原始Filter无损接口_RDC编译验收报告_v1.md new file mode 100644 index 000000000..216e4fc55 --- /dev/null +++ b/docs/报告/STEP-120_iOS原始Filter无损接口_RDC编译验收报告_v1.md @@ -0,0 +1,23 @@ +# STEP-120 iOS 原始 Filter 无损接口 RDC 独立编译验收报告 v1 + +日期:2026-10-21 JST。联调业务唯一主责 NavSea;专业支持仓 MapLibreParticleLab。本报告只是 Native 编译证据,不是 .28 正式发行、Runtime 或设备验收。 + +## 实际基线 +- Mac /Users/tei/dev/MapLibreParticleLab,分支 particle/main,初始 HEAD 9d6c9e9684e4c71cc2fb3f08bfb27e1638be4188;未创建/切换分支、Worktree,未触碰无关项目。 +- Codex GPT-6.1 Sol High 已按 STEP-120 v2 修改 platform/darwin/src/MLNVectorStyleLayer.h 与 .mm;中文报告 docs/报告/STEP-120_iOS原始Filter无损Native接口_Codex完成报告_v2.md。 +- 当前新增两个 ObjC API:rawStyleFilterJSONObjectWithError:、setRawStyleFilterJSONObject:error:。保留旧 predicate 逻辑、原 Style Layer 的业务语义和原 .27 正式发行文件。 + +## RDC 真实编译与校验 +- 命令:bazel build -c opt --//:renderer=metal //platform/ios:MapLibre.dynamic。 +- 日志:/private/tmp/JOINT-PBF-001-STEP120-RDC-ios-metal-build-v1.log。 +- 结果:INFO: Build completed successfully, 708 total actions;1176.689s;只有 /opt/homebrew/lib 搜索路径 warning,不影响链接。 +- 产物:bazel-out/darwin_x86_64-opt/bin/platform/ios/MapLibre.dynamic.xcframework.zip。 +- 大小 4,187,287 bytes,SHA256 577d0784f1e436c04b3cf0bd0213ce38fdf14612898367ec6e7c73156b82cd74。 +- unzip -tq PASS;产物仅 ios-arm64 真机 Slice;公开头 MLNVectorStyleLayer.h 包含上述两个 API。 +- git diff --check PASS;尚未执行设备调用或 Native Filter 回滚的独立运行测试。 + +## 后续及边界 +- “无损或拒绝”不等于所有 CM95 Filter 自动支持;Host 必须核对 Layer 实例、Style 代次、revision、外部修改。 +- .28 GSRL 必须基于干净的冻结源码提交,随后由现有 release.sh 流水线完成 Android/RN/iOS 统一构建、原子发布、公网回读、Source Lock/Manifest、Tag/Push;不得覆盖旧 .27。 +- NavSea 必须等待正式 SwiftPM 6.29.0-particle.28,然后再完成 MLRN iOS Bridge 的查询、应用、恢复三 API 及编译。Android 现有 .27 Kotlin Bridge 已构建通过但 .28 必须回归。 +- 当前状态:STEP120_RDC_IOS_METAL_OPT_BUILD_PASS / XCFRAMEWORK_HEADER_EXPORT_PASS / DEVICE_RUNTIME_PENDING / V28_RELEASE_PENDING。 diff --git a/platform/darwin/src/MLNVectorStyleLayer.h b/platform/darwin/src/MLNVectorStyleLayer.h index 13585bbec..d37441563 100644 --- a/platform/darwin/src/MLNVectorStyleLayer.h +++ b/platform/darwin/src/MLNVectorStyleLayer.h @@ -5,6 +5,23 @@ NS_ASSUME_NONNULL_BEGIN +/** 原始 Filter 接口的错误域。 */ +FOUNDATION_EXTERN MLN_EXPORT NSErrorDomain const MLNStyleFilterErrorDomain; + +/** 原始 Filter 接口的稳定错误代码;不影响既有 predicate 接口。 */ +typedef NS_ENUM(NSInteger, MLNStyleFilterErrorCode) { + /** E_FILTER_UNSUPPORTED:输入超限、不支持或无法证明无损重放;未写入。 */ + MLNStyleFilterErrorUnsupported = 1, + /** E_FILTER_INVALID_LAYER:rawLayer 已失效或不是支持的 Vector Layer。 */ + MLNStyleFilterErrorInvalidLayer = 2, + /** E_FILTER_WRONG_THREAD:必须在主线程调用。 */ + MLNStyleFilterErrorWrongThread = 3, + /** E_FILTER_MUTATION_FAILED:写入/后验检查失败,已验证原 Filter 恢复。 */ + MLNStyleFilterErrorMutationFailed = 4, + /** E_FILTER_ROLLBACK_FAILED:无法确认恢复;Host 必须废弃当前操作 Context。 */ + MLNStyleFilterErrorRollbackFailed = 5, +}; + /** ``MLNVectorStyleLayer`` is an abstract superclass for style layers whose content is defined by an ``MLNShapeSource`` or ``MLNShapeSource`` object. @@ -53,6 +70,43 @@ MLN_EXPORT */ @property (nonatomic, nullable) NSPredicate *predicate; +/** + 读取当前有效 rawLayer 的 Filter JSON 快照;不经过 NSPredicate 或初始化 Style JSON。 + 空 Filter 返回 NSNull;失败返回 nil,并通过 error 报告原因。 + + 仅返回经现有 Foundation/Core 转换器往返验证的子集:JSON 最大深度 64(根为 1), + 最多 4096 个节点(含对象键),字符串和键的 UTF-8 总量最多 256 KiB。 + 字符串/键含 U+0000 或非法 UTF-8、非有限数值、负零、绝对值大于 2^53-1 的数值、 + 非 JSON Core 值以及不可重放的 Filter 均报 MLNStyleFilterErrorUnsupported。 + 结构比较保留数组顺序、对象键和值、布尔/数值区别;仅精确可表示的数值存储类型可归一。 + + 常用 all/any/!/比较/get/has/in/literal、类型断言和 collator 可以进入验证; + 其他操作符、混合 legacy/表达式 Filter 或发生折叠/默认值补全而无法结构往返者可能拒绝。 + 返回对象与 Layer 状态分离,调用方不得在调用期间并发修改输入或 Style。 + + 必须在主线程调用。本接口不证明 Layer 属于当前 Style:未加入或正常移除后仍存活的 + Layer 可以使用。Host 必须另行核验当前 Style 按 ID 查得的对象身份、Token/代次等; + rawLayer 已销毁则失败。该接口不管理 Host Context 或 revision。 + */ +- (nullable id)rawStyleFilterJSONObjectWithError:(NSError * _Nullable * _Nullable)error; + +/** + 只设置当前 rawLayer 的 Filter。输入为 Filter 数组或 NSNull(精确恢复空 Filter); + nil 不作为清空指令。NSDecimalNumber 及不支持的 Foundation 类型也会被拒绝。 + + 在写入前对输入建立独立快照、验证 Core 解析/序列化不丢失结构,并证明新旧 Filter + 可以经上述 getter 的同一路径重放。任何准入失败都返回 NO,原 Layer 不变。 + 输入必须已是 Core 可往返的形式;显式 literal 标量、常量折叠或省略 collator 默认项 + 等即使语义可能等价,也可被保守拒绝。具体限制与 rawStyleFilterJSONObjectWithError: 一致。 + + 写入后重新检查实际 Filter;若不符或写入抛出异常则尝试恢复原 Filter。 + MLNStyleFilterErrorMutationFailed 表示恢复已核实,但宿主仍应重新获取状态; + MLNStyleFilterErrorRollbackFailed 表示状态无法确认,宿主必须废弃当前操作 Context。 + 成功仅代表本次 Filter 操作,不能替代 Host 的 Style 成员关系/外部修改检测。 + 不修改 Paint/Layout/Source/层序,也不移除 Layer 或重载 Style。 + */ +- (BOOL)setRawStyleFilterJSONObject:(id)json error:(NSError * _Nullable * _Nullable)error; + @end NS_ASSUME_NONNULL_END diff --git a/platform/darwin/src/MLNVectorStyleLayer.mm b/platform/darwin/src/MLNVectorStyleLayer.mm index edc0bea5e..6e594ef70 100644 --- a/platform/darwin/src/MLNVectorStyleLayer.mm +++ b/platform/darwin/src/MLNVectorStyleLayer.mm @@ -1,8 +1,382 @@ #import "MLNVectorStyleLayer.h" #import "MLNStyleLayer_Private.h" +#import + +#include + +#include +#include + +NSErrorDomain const MLNStyleFilterErrorDomain = @"MLNStyleFilterErrorDomain"; + +namespace { +using Filter = mln::style::Filter; +using Value = mln::Value; +using Array = std::vector; +using Object = std::unordered_map; + +constexpr std::size_t MLNFilterMaxDepth = 64; +constexpr std::size_t MLNFilterMaxNodes = 4096; +constexpr std::size_t MLNFilterMaxStringBytes = 256 * 1024; +constexpr int64_t MLNFilterMaxInteger = 9007199254740991LL; + +// 对象键也占一个节点;循环 Foundation 容器在触及深度或节点上限时拒绝。 +struct FilterBudget { + std::size_t nodes = 0; + std::size_t stringBytes = 0; + + bool enter(std::size_t depth) { + return depth <= MLNFilterMaxDepth && ++nodes <= MLNFilterMaxNodes; + } + bool string(const std::string &text) { + if (text.size() > MLNFilterMaxStringBytes - stringBytes || + text.find('\0') != std::string::npos) return false; + if (![[NSString alloc] initWithBytes:text.data() + length:text.size() + encoding:NSUTF8StringEncoding]) return false; + stringBytes += text.size(); + return true; + } +}; + +std::optional FilterNumber(double number) { + if (!std::isfinite(number) || std::abs(number) > MLNFilterMaxInteger || + (number == 0 && std::signbit(number))) return std::nullopt; + return Value(number); +} + +// 只归一精确可表示的数值;不使用 JSON 文本比较、NSPredicate 或 Filter 的指针相等。 +std::optional NormalizeFilterValue(const Value &value, FilterBudget &budget, + std::size_t depth = 1) { + if (!budget.enter(depth)) return std::nullopt; + return value.match( + [](mln::NullValue) -> std::optional { return Value(mln::NullValue()); }, + [](bool v) -> std::optional { return Value(v); }, + [](int64_t v) -> std::optional { + if (v < -MLNFilterMaxInteger || v > MLNFilterMaxInteger) return std::nullopt; + return FilterNumber(static_cast(v)); + }, + [](uint64_t v) -> std::optional { + if (v > static_cast(MLNFilterMaxInteger)) return std::nullopt; + return FilterNumber(static_cast(v)); + }, + [](float v) { return FilterNumber(v); }, [](double v) { return FilterNumber(v); }, + [&](const std::string &v) -> std::optional { + return budget.string(v) ? std::optional(Value(v)) : std::nullopt; + }, + [&](const Array &items) -> std::optional { + if (items.size() > MLNFilterMaxNodes - budget.nodes) return std::nullopt; + Array result; + for (const auto &item : items) { + auto child = NormalizeFilterValue(item, budget, depth + 1); + if (!child) return std::nullopt; + result.push_back(std::move(*child)); + } + return Value(std::move(result)); + }, + [&](const Object &items) -> std::optional { + if (items.size() > (MLNFilterMaxNodes - budget.nodes) / 2) return std::nullopt; + Object result; + for (const auto &item : items) { + if (!budget.enter(depth + 1) || !budget.string(item.first)) return std::nullopt; + auto child = NormalizeFilterValue(item.second, budget, depth + 1); + if (!child) return std::nullopt; + result.emplace(item.first, std::move(*child)); + } + return Value(std::move(result)); + }, + [](const auto &) -> std::optional { return std::nullopt; }); +} + +std::optional NormalizeFilterValue(const Value &value) { + FilterBudget budget; + return NormalizeFilterValue(value, budget); +} + +std::optional SnapshotFilterJSON(id json, FilterBudget &budget, std::size_t depth = 1) { + if (!json || !budget.enter(depth)) return std::nullopt; + if (json == [NSNull null]) return Value(mln::NullValue()); + if ([json isKindOfClass:[NSString class]]) { + NSString *text = json; + if (text.length > MLNFilterMaxStringBytes - budget.stringBytes) return std::nullopt; + NSData *data = [text dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:NO]; + if (!data || data.length > MLNFilterMaxStringBytes - budget.stringBytes) return std::nullopt; + std::string value(data.length ? static_cast(data.bytes) : "", data.length); + return budget.string(value) ? std::optional(Value(std::move(value))) : std::nullopt; + } + if ([json isKindOfClass:[NSNumber class]]) { + if ([json isKindOfClass:[NSDecimalNumber class]]) return std::nullopt; + if (CFGetTypeID((__bridge CFTypeRef)json) == CFBooleanGetTypeID()) + return Value([json boolValue] != NO); + const char *type = [json objCType]; + if (!type || !type[0] || type[1] || !std::strchr("cCsSiIlLqQfdB", type[0])) + return std::nullopt; + return FilterNumber([json doubleValue]); + } + if ([json isKindOfClass:[NSArray class]]) { + NSArray *items = json; + if (items.count > MLNFilterMaxNodes - budget.nodes) return std::nullopt; + Array result; + for (id item in items) { + auto child = SnapshotFilterJSON(item, budget, depth + 1); + if (!child) return std::nullopt; + result.push_back(std::move(*child)); + } + return Value(std::move(result)); + } + if ([json isKindOfClass:[NSDictionary class]]) { + NSDictionary *items = json; + if (items.count > (MLNFilterMaxNodes - budget.nodes) / 2) return std::nullopt; + Object result; + for (id key in items) { + if (![key isKindOfClass:[NSString class]]) return std::nullopt; + auto name = SnapshotFilterJSON(key, budget, depth + 1); + auto child = name ? SnapshotFilterJSON(items[key], budget, depth + 1) : std::nullopt; + if (!name || !child) return std::nullopt; + if (!result.emplace(name->get(), std::move(*child)).second) return std::nullopt; + } + return Value(std::move(result)); + } + return std::nullopt; +} + +std::optional SnapshotFilterJSON(id json) { + FilterBudget budget; + return SnapshotFilterJSON(json, budget); +} + +// 有限操作符准入;literal 的载荷是 JSON 数据,不按表达式解释。 +// 特别在 Core 访问 ["in"] 的缺失参数前拒绝非法形状。 +bool FilterSyntax(const Value &value) { + if (!value.is()) return !value.is(); + const auto &items = value.get(); + if (items.empty() || !items[0].is()) return false; + const auto &op = items[0].get(); + const auto count = items.size(); + if (op == "literal") return count == 2; + if (op == "collator") { + if (count != 2 || !items[1].is()) return false; + for (const auto &item : items[1].get()) if (!FilterSyntax(item.second)) return false; + return true; + } + bool valid = (op == "all" || op == "any" || op == "none") ? count >= 1 + : op == "!" ? count == 2 + : (op == "==" || op == "!=" || op == "<" || op == "<=" || op == ">" || op == ">=") + ? (count == 3 || count == 4) + : (op == "get" || op == "has") ? (count == 2 || count == 3) + : op == "!has" ? count == 2 + : (op == "in" || op == "!in") ? count >= 3 + : (op == "string" || op == "number" || op == "boolean") ? count >= 2 + : false; + if (!valid) return false; + for (std::size_t i = 1; i < count; ++i) if (!FilterSyntax(items[i])) return false; + return true; +} + +// 仅接受具有明确参数形状的 legacy Filter,拒绝 Core 宽松分支可能忽略的混合表达式。 +bool LegacyFilterSyntax(const Value &value) { + if (!value.is()) return false; + const auto &items = value.get(); + if (items.empty() || !items[0].is()) return false; + const auto &op = items[0].get(); + if (op == "all" || op == "any" || op == "none") { + if (items.size() < 2) return false; + for (std::size_t i = 1; i < items.size(); ++i) if (!LegacyFilterSyntax(items[i])) return false; + return true; + } + if (items.size() < 2 || !items[1].is()) return false; + if (op == "has" || op == "!has") return items.size() == 2; + bool valid = (op == "in" || op == "!in") ? items.size() >= 3 + : (op == "==" || op == "!=" || op == "<" || op == "<=" || op == ">" || op == ">=") + && items.size() == 3; + if (!valid) return false; + for (std::size_t i = 2; i < items.size(); ++i) + if (items[i].is() || items[i].is()) return false; + return true; +} + +bool FilterExpressionFits(const Filter &filter) { + if (!filter.expression) return true; + if (!*filter.expression) return false; + std::size_t nodes = 0; + const auto visit = [&](const auto &self, const mln::style::expression::Expression &expression, + std::size_t depth) -> bool { + if (depth > MLNFilterMaxDepth || ++nodes > MLNFilterMaxNodes) return false; + bool valid = true; + expression.eachChild([&](const auto &child) { if (valid) valid = self(self, child, depth + 1); }); + return valid; + }; + return visit(visit, **filter.expression, 1); +} + +bool SameFilter(const Filter &actual, const Filter &expected, const Value &expectedJSON) { + if (!FilterExpressionFits(actual) || !FilterExpressionFits(expected)) return false; + auto serialized = NormalizeFilterValue(actual.serialize()); + if (!serialized || *serialized != expectedJSON || + actual.expression.has_value() != expected.expression.has_value()) return false; + if (!actual.expression) return true; + auto actualExpression = NormalizeFilterValue((**actual.expression).serialize()); + auto expectedExpression = NormalizeFilterValue((**expected.expression).serialize()); + return actualExpression && expectedExpression && *actualExpression == *expectedExpression && + **actual.expression == **expected.expression; +} + +std::optional ParseFilterJSON(const Value &expected, id json) { + if (expected.is()) return Filter(); + if (!expected.is() || !FilterSyntax(expected)) return std::nullopt; + mln::style::conversion::Error error; + auto parsed = mln::style::conversion::convert( + mln::style::conversion::makeConvertible(json), error); + if (!parsed || !FilterExpressionFits(*parsed)) return std::nullopt; + auto serialized = NormalizeFilterValue(parsed->serialize()); + if (!serialized || *serialized != expected || !parsed->expression) return std::nullopt; + auto expression = NormalizeFilterValue((**parsed->expression).serialize()); + if (!expression || (*expression != expected && !LegacyFilterSyntax(expected))) return std::nullopt; + return parsed; +} + +// 输出真实桥接对象前,先检查 C++ 树,再核对桥接值和 Core 重放的结构与表达式语义。 +id CheckedFilterJSON(const Filter &filter, Value &expected) { + if (!FilterExpressionFits(filter)) return nil; + auto normalized = NormalizeFilterValue(filter.serialize()); + if (!normalized) return nil; + id json = MLNJSONObjectFromMBGLValue(*normalized); + auto bridged = SnapshotFilterJSON(json); + if (!bridged || *bridged != *normalized) return nil; + auto replayed = ParseFilterJSON(*normalized, json); + if (!replayed || !SameFilter(filter, *replayed, *normalized)) return nil; + expected = std::move(*normalized); + return json; +} + +NSError *FilterError(MLNStyleFilterErrorCode code, NSString *detail) { + NSString *identifier = @"E_FILTER_UNSUPPORTED"; + switch (code) { + case MLNStyleFilterErrorUnsupported: identifier = @"E_FILTER_UNSUPPORTED"; break; + case MLNStyleFilterErrorInvalidLayer: identifier = @"E_FILTER_INVALID_LAYER"; break; + case MLNStyleFilterErrorWrongThread: identifier = @"E_FILTER_WRONG_THREAD"; break; + case MLNStyleFilterErrorMutationFailed: identifier = @"E_FILTER_MUTATION_FAILED"; break; + case MLNStyleFilterErrorRollbackFailed: identifier = @"E_FILTER_ROLLBACK_FAILED"; break; + } + return [NSError errorWithDomain:MLNStyleFilterErrorDomain code:code + userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"%@: %@", identifier, detail]}]; +} + +mln::style::Layer *FilterLayer(MLNVectorStyleLayer *receiver, NSError **error) { + if (![NSThread isMainThread]) { + if (error) *error = FilterError(MLNStyleFilterErrorWrongThread, @"原始 Filter 接口必须在主线程调用。"); + return nullptr; + } + auto *layer = receiver.rawLayer; + if (!layer || layer->getTypeInfo()->tileKind != mln::style::LayerTypeInfo::TileKind::Geometry || + layer->getTypeInfo()->source != mln::style::LayerTypeInfo::Source::Required) { + if (error) *error = FilterError(MLNStyleFilterErrorInvalidLayer, @"rawLayer 已失效或不是 Vector Layer。"); + return nullptr; + } + return layer; +} + +// 弱指针仅保护后验检查/回滚,不表示 Style 成员关系,也不管理 Host Context。 +NSError *ApplyFilter(MLNVectorStyleLayer *receiver, const Filter &next, const Value &nextJSON, + const Filter &previous, const Value &previousJSON, + const mapbox::base::WeakPtr &weakLayer) { + const auto current = [&]() { + auto *layer = weakLayer.get(); + return layer && receiver.rawLayer == layer ? layer : nullptr; + }; + bool attempted = false; + try { + @try { + auto *layer = current(); + if (!layer) return FilterError(MLNStyleFilterErrorInvalidLayer, @"写入前 rawLayer 已失效或被替换;未写入。"); + if (!SameFilter(layer->getFilter(), previous, previousJSON)) + return FilterError(MLNStyleFilterErrorUnsupported, @"验证期间原 Filter 已改变;未写入。"); + attempted = true; + layer->setFilter(next); + if (current() && SameFilter(current()->getFilter(), next, nextJSON)) return nil; + } @catch (NSException *exception) { + (void)exception; + // 写入或 observer 抛出异常后,仍进入下面的原状态恢复和独立核对。 + } + } catch (...) { + } + if (!attempted) + return FilterError(MLNStyleFilterErrorUnsupported, @"写入前验证异常;未写入。"); + bool restored = false; + try { + @try { + if (auto *layer = current()) layer->setFilter(previous); + } @catch (NSException *exception) { + (void)exception; + } + } catch (...) { + } + // observer 可以在原 Filter 已恢复后抛出异常,恢复核对必须独立执行。 + try { + @try { + restored = current() && SameFilter(current()->getFilter(), previous, previousJSON); + } @catch (NSException *exception) { + (void)exception; + } + } catch (...) { + } + return restored ? FilterError(MLNStyleFilterErrorMutationFailed, @"写入或后验检查失败;原 Filter 已恢复并核实。") + : FilterError(MLNStyleFilterErrorRollbackFailed, @"无法确认原 Filter 恢复;Host 必须废弃当前 Context。"); +} +} // namespace + @implementation MLNVectorStyleLayer +- (id)rawStyleFilterJSONObjectWithError:(NSError **)error { + if (error) *error = nil; + auto *layer = FilterLayer(self, error); + if (!layer) return nil; + try { + @try { + Value expected; + const Filter filter = layer->getFilter(); + if (id json = CheckedFilterJSON(filter, expected)) return json; + } @catch (NSException *exception) { + (void)exception; + } + } catch (...) { + } + if (error) *error = FilterError(MLNStyleFilterErrorUnsupported, @"当前 Filter 超限、不受支持或无法证明无损重放。"); + return nil; +} + +- (BOOL)setRawStyleFilterJSONObject:(id)json error:(NSError **)error { + if (error) *error = nil; + auto *layer = FilterLayer(self, error); + if (!layer) return NO; + auto weakLayer = layer->makeWeakPtr(); + try { + @try { + const Filter previous = layer->getFilter(); + auto input = SnapshotFilterJSON(json); + if (input) { + // 解析独立的桥接快照,避免转换器读取调用方的可变容器。 + id snapshot = MLNJSONObjectFromMBGLValue(*input); + auto bridged = SnapshotFilterJSON(snapshot); + auto next = bridged && *bridged == *input ? ParseFilterJSON(*input, snapshot) : std::nullopt; + Value nextJSON, previousJSON; + if (next && CheckedFilterJSON(*next, nextJSON) && nextJSON == *input && + CheckedFilterJSON(previous, previousJSON)) { + NSError *failure = ApplyFilter(self, *next, nextJSON, previous, previousJSON, weakLayer); + if (error) *error = failure; + return failure == nil; + } + } + } @catch (NSException *exception) { + (void)exception; + } + } catch (...) { + } + if (error) *error = FilterError(MLNStyleFilterErrorUnsupported, @"输入或原 Filter 超限、不受支持或无法证明无损重放;未写入。"); + return NO; +} + - (void)setPredicate:(NSPredicate *)predicate { [NSException raise:MLNAbstractClassException format:@"MLNVectorStyleLayer is an abstract class"]; } diff --git a/粒子开发记录.md b/粒子开发记录.md index 42f21e7e0..9fb6d41a2 100644 --- a/粒子开发记录.md +++ b/粒子开发记录.md @@ -1219,3 +1219,18 @@ Android V3-08 单次综合验收已完成并关闭。Native `13.4.1-particle.2` - 新增 docs/50_Step记录/STEP-119_SymbolRetained生命周期修复_v1.md 与 docs/报告/STEP-119_SymbolRetained生命周期修复_Codex完成报告_v1.md(未覆盖旧版),报告含准确 RDC 命令、源码锁前置条件、FZ-G1 正负及动态生命周期/视觉/释放证据、风险与停止门槛;本记录仅末尾追加。立即交 RDC 独立验证,整体 STEP119 未关闭。 - 2026-10-13 v26 正式发行 CLOSED / GSRL PASS:Android/RN 13.6.1-particle.26、iOS Metal 6.29.0-particle.26,冻结实际构建源码 fce326a9009d3fc21eb91c5df362f0769163d07e;统一 Android 四 ABI/iOS arm64 真机 Metal/RN 包 Build exit0、Publish exit0、Finalize exit0。六项正式制品 NAS 原子发布及公网二次下载 SHA/size 6/6 PASS;Source Lock release/maplibre-particle/13.6.1-particle.26/SOURCE_LOCK.json,Tag maplibre-particle/rn/13.6.1-particle.26 指向 e3cd9eba3cd02149d431e0061b8d32192d876f78,远端 particle/main Finalize 后同 HEAD。V26 发行二进制 NavSea/FZ-G1 及 iOS 实机未测;STEP119 13 项 C++ GTest 因 Mac libuv 前置依赖未执行,未虚报功能/性能正式全通过。报告 docs/报告/V26_统一正式发行_GSRL_RDC完成报告_v1.md,独立验收 JSON=/tmp/maplibre_v26_independent_receipt_20261013.json。 + +### 2026-10-21 STEP-120 iOS 原始 Filter 无损接口调查(BLOCKED) + +- Mac.lan / macOS 15.7.7 / x86_64,particle/main@9d6c9e9684e4c71cc2fb3f08bfb27e1638be4188,原 worktree 与子模块正常;预存未跟踪 STEP-120 任务书保留。用户指定 GPT-6.1 Sol High,当前未取得执行器实际模型/档位元数据,不宣称独立核实;未切模型或启动子代理。 +- 完整读取当前规则、任务、V27 iOS 6.29.0-particle.27 发行记录及 Darwin/Core 源码。指定既有 JSON 桥接对字符串/键用无长度转换,会截断合法嵌入 NUL;反向整数转 double 也有精度风险。严格无损不能仅靠两个直接包装方法保证,未擅改辅助文件或缩小契约。 +- 正常 removeLayer 后私有 pendingLayer 仍拥有有效 rawLayer,现有失效宏不能证明已拒绝 detach;已发出 Host 成员关系/Native 拒绝语义澄清,未答复不当授权。SDK 未建立另一套 Context/生命周期状态,也未修改任何源码;公共 API 尚未实现,D4-13 仍 BLOCKED。 +- 新增 docs/50_Step记录/STEP-120_iOS原始Filter无损Native接口_v1.md 与 docs/报告/STEP-120_iOS原始Filter无损Native接口_Codex完成报告_v1.md,含 all/第二属性/locale、一般等号、原为空安全样例及准确 RDC 单项构建命令。仅静态审查及空白/范围/保护检查,未编译、codegen、单测、设备、发布或 Git 写操作。先明确契约及范围再实现,由 RDC 独立验收;NavSea 为业务唯一主责。 + +### 2026-10-21 STEP-120 v2 iOS Filter 无损或拒绝接口实现 + +- 状态 IMPLEMENTED_WAITING_RDC_IOS_BUILD;Mac.lan / macOS 15.7.7 / x86_64,particle/main@9d6c9e9684e4c71cc2fb3f08bfb27e1638be4188 保持。用户指定 GPT-6.1 Sol High,工具无执行器实际参数独立证据;未切模型、启动子代理或其他实例。 +- 按 v2 授权收敛 v1 阻塞,仅改 MLNVectorStyleLayer.h/.mm:公开带 NSError 的 rawStyleFilterJSONObjectWithError: 与 setRawStyleFilterJSONObject:error:,五类错误区分 Unsupported、InvalidLayer、WrongThread、已核实恢复和无法核实恢复。主线程只操作有效 rawLayer,正常 pending/detach 成员关系交 Host 校验,不建另一套 Context。 +- 局部预检和独立 JSON 快照限制深度64、节点4096、字符串/键 UTF-8 合计256KiB,拒绝NUL、特殊数值/NSDecimalNumber、超出±(2^53-1)、未知/不可重放结构;只归一精确数值,保持布尔、数组、对象键和值。经真实桥接及 Core convert/serialize、实际 Expression 比较后才能读出或写入,不走 NSPredicate/NSExpression、不改共享转换器。 +- setter 先证明新旧 Filter 可重放并复核 Layer 身份,再写入;后验不符/异常恢复原 Filter 并独立核实,无法核实要求 Host 废弃 Context。NSNull 精确还原 Filter();不改变 Paint/Layout/Source/层序,原 predicate/description 字节保持。受限 legacy、隐式断言/常量折叠/省略collator默认项等拒绝边界已写 API Doc 和报告。 +- 新增 v2 Step 和 docs/报告/STEP-120_iOS原始Filter无损Native接口_Codex完成报告_v2.md,保留 v1 与任务书,提供等值、两属性范围、locale、NUL拒绝和空Filter样例、真实 RDC 单项 Bazel 命令。仅静态源码、空白及保护检查,未语法编译、Bazel/Xcode/Gradle/codegen/Native单测/Jest、设备或发行;没有 stage/commit/push/tag 或分支/worktree 改动。RDC 编译/链接/运行及 ABI/Host 验收待执行,NavSea 原业务总目标仍开放。