fix: close STEP-078 V4 bridge guard audit
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
OpenAI Codex
2026-09-13 17:14:50 +08:00
parent 73779b89bc
commit 75a974e775
15 changed files with 178 additions and 9 deletions

View File

@@ -230,6 +230,7 @@ list(APPEND INCLUDE_FILES
${PROJECT_SOURCE_DIR}/include/mbgl/particle/field/field_runtime.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/particle/field/field_binding.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/particle/ingress/qbin_v2_decoder.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/particle/ingress/encoded_field_transport.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/particle/field/local_particle_field_feed.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/actor/actor_ref.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/actor/actor.hpp

View File

@@ -0,0 +1,38 @@
# STEP-078 — QBIN2 V4 Android / JNI / RN 桥接实现 v3
状态:完成
日期:2026-09-13
## 目标与基线
- 基线:`73779b89bcce1094eb344f4727d6d23904f667f3`。
- 仅关闭 V4 bridge 的 QBIN transport 三层 64 MiB guard 与受限 ID 的 UTF-8 byte-length guard;不进入 STEP-079。
- 任务开始前 STEP-062 两处 dirty、历史 untracked、`.codex-build-logs/` 与 Test Host generated/android/.expo/tgz 均未清理、改写、暂存或提交。
## 完成项
- [x] Java `WeatherRuntime.submitEncodedField(...)` 在 map/JNI 前拒绝空及超过 64 MiB QBIN,并以 UTF-8 1..64 bytes 检查 fieldKey、fieldID 和 contract JSON 中 profileID。
- [x] JNI 在读取 `jbyteArray` 长度后、复制到 `std::vector` 前重复检查 1..64 MiB;无效 transport 直接返回既有 `RejectedDecode`,不产生 oversized vector。
- [x] Kotlin local-file 既有 1..64 MiB read guard 保持;Kotlin fieldKey、fieldID、profileID 改为 `toByteArray(Charsets.UTF_8).size` 的 1..64 bytes 检查。
- [x] RN/TS 改用 `TextEncoder` UTF-8 bytes,fieldKey、runtime fieldID 与 profileID 在 33 个 CJK 字符(99 bytes)时 fail-fast。
- [x] 新增 provider-neutral C++ transport-size helper/test、JDK-only Java guard test 与 TS directed test;未改变 QBIN decoder 的 64 MiB file / 32 MiB decoded-raw 语义、Core 十种 submit result、binding 或 forecast state/speed。
## 修改范围
- RN:`FieldRuntime.ts` 与 private `EncodedFieldGuards.ts`、其 directed test。
- Kotlin/Java:`WeatherRuntimeBridgeModule.kt`、`WeatherRuntime.java` 与 JDK-only V4 guard/helper test。
- JNI:`native_map_view.cpp`;Core only 增加 transport-size header 和 Linux unit test。
- CMake/test registration、本 Step、本报告、`粒子开发记录.md`。
## 验证
- [x] 在 `particle-codex` tmux session,经全局 `build-output-throttle`:package `tsc --noEmit` PASS、TS UTF-8 directed test PASS、TurboModule schema combine/codegen PASS。
- [x] JDK-only Java guard test PASS:64 MiB accepted,0 与 64 MiB+1 rejected,33 CJK 字符 rejected。
- [x] `mbgl-test-runner` 实际 rebuild PASS;`ParticleFieldProfileContractJson.*`、`Source.RasterArrayV4HostApi*`、`ParticleFieldBinding.*`、`ParticleQbinV2Decoder.*:ParticleEncodedFieldIngress.*`、`ParticleEncodedFieldTransport.*`、`ParticleFieldRuntime.*:ParticleFieldProfileValidator.*`、`Source.ParticleFieldDataAdapter*:Source.RasterArrayTimeline*:Source.HostField*` 合并过滤 PASS。
- [x] Java declaration ↔ C++ JNI registration V4 name-set 静态核对、Kotlin/Java UTF-8 guard 静态核对、`git diff --check`:PASS。
## 未完成、阻塞与下一步
- Android Kotlin/Java/JNI 真实编译、Gradle、APK、ADB、设备验证按任务约束未执行;需要后续一次性 Android 验收。iOS/Metal 与 Jest 同样未执行。
- 无实现阻塞。本 Step 在 scoped commit/push 后立即停止,不进入 STEP-079。

View File

@@ -0,0 +1,22 @@
# STEP-078 QBIN2 V4 Android / JNI / RN 桥接实现 Codex 完成报告 v3
## 结果
已按 `73779b89bcce1094eb344f4727d6d23904f667f3` 基线关闭审计发现的两个 production guard 缺口,未重构已通过的 V4 bridge,未进入 STEP-079。
QBIN transport 现在在 Kotlin local-file、Java facade、JNI copy 三处均为 1..64 MiB;JNI 的检查在 `std::vector` 构造之前,直接 JNI caller 的空/超限数组 fail-closed。该 transport guard 是独立 helper,未混入 QBIN decoder:decoder 自有的 64 MiB 文件与 32 MiB decoded raw limits 不变。
RN/TS 以 `TextEncoder`、Kotlin 以 `toByteArray(Charsets.UTF_8)`、Java 以 `StandardCharsets.UTF_8` 执行相同的 1..64 UTF-8 bytes guard,覆盖 fieldKey、runtime fieldID、profileID。profileID 的 lowercase ASCII structural contract 仍由 Core parser/validator 最终裁决;未统一或放宽其它 ID 字符集。contractJson 仍为既有 256 KiB UTF-8 上限。
## 验证结果
- `particle-codex` tmux session 内所有 noisy build/test 均经 `build-output-throttle`:package TypeScript、TS 多字节 fail-fast directed test、TurboModule combine/codegen、Linux `mbgl-test-runner` rebuild 与规定 V4/V3 filter 均 PASS。
- 新 C++ `ParticleEncodedFieldTransport` test 证明 0/64 MiB+1 拒绝、64 MiB 接受,且 helper 在 JNI vector copy 之前调用。
- 新 JDK-only Java guard test 证明 Java facade 采用的 helper 接受 64 MiB、拒绝 0 和 64 MiB+1,并拒绝 33 CJK 字符(99 UTF-8 bytes)。Kotlin 与 Java source 的 UTF-8 guard 已定向静态核对。
- Java JNI declaration 与 C++ registration 的 V4 name-set 静态核对 PASS;`git diff --check` PASS。
## 未完成与边界
Android Kotlin/Java/JNI 的真实编译、Gradle、APK、安装、ADB 和实体机验证均按任务要求未执行,留待后续一次性 Android 验收;iOS/Metal 与 Jest 也未执行。没有读取真实服务器 QBIN,未改 Renderer/GPU/shader、Test Host、NavSea 或 STEP-073~076 冻结 Core 语义。
任务开始前的 STEP-062 dirty、历史 untracked、`.codex-build-logs/`、Test Host generated/android/.expo 和 tgz 未纳入本次提交。scoped commit、push result、remote branch 与 Final HEAD 记录于本报告的 Git 收尾更新及 Step 记录;完成后停止,不进入 STEP-079。

View File

@@ -0,0 +1,14 @@
#pragma once
#include <cstddef>
namespace mbgl::particle::ingress {
// Transport-only bound for JNI byte-array copying; decoder limits remain independent.
constexpr std::size_t MaxEncodedFieldTransportBytes = 64u * 1024u * 1024u;
constexpr bool isValidEncodedFieldTransportSize(const std::size_t size) {
return size >= 1u && size <= MaxEncodedFieldTransportBytes;
}
} // namespace mbgl::particle::ingress

View File

@@ -106,7 +106,7 @@ class WeatherRuntimeBridgeModule(private val context: ReactApplicationContext) :
}
/** V4 transports only a bounded local raw QBIN file plus small primitive control-plane values. */
@ReactMethod override fun submitEncodedField(reactTag: Double, fieldKey: String, fieldID: String, generation: Double, dataVersion: Double, contractJson: String, qbinFileUri: String, promise: Promise) {
if (!validEncodedId(fieldKey) || !validEncodedId(fieldID) || !validSafeInteger(generation, true) || !validSafeInteger(dataVersion, false) || contractJson.isEmpty() || contractJson.toByteArray(Charsets.UTF_8).size > MAX_CONTRACT_UTF8_BYTES) return reject(promise, "INVALID_ARGUMENT", "invalid V4 encoded field control-plane argument")
if (!validEncodedId(fieldKey) || !validEncodedId(fieldID) || !validEncodedContractJson(contractJson) || !validSafeInteger(generation, true) || !validSafeInteger(dataVersion, false)) return reject(promise, "INVALID_ARGUMENT", "invalid V4 encoded field control-plane argument")
val payload = try { readLocalPayload(qbinFileUri) } catch (e: Exception) { return reject(promise, "PAYLOAD_FILE_UNREADABLE", e.message ?: "QBIN file is unreadable") }
if (payload.isEmpty()) return reject(promise, "PAYLOAD_FILE_UNREADABLE", "QBIN file is empty")
withRuntime(reactTag, promise) { runtime -> try { promise.resolve(runtime.submitEncodedField(fieldKey, fieldID, generation.toLong(), dataVersion.toLong(), contractJson, payload).name) } catch (e: IllegalStateException) { reject(promise, if (e.message == "SOURCE_MISSING") "SOURCE_MISSING" else "RUNTIME_DISPOSED", e.message ?: "V4 runtime unavailable") } catch (e: IllegalArgumentException) { reject(promise, "INVALID_ARGUMENT", e.message ?: "invalid encoded field") } }
@@ -294,7 +294,8 @@ class WeatherRuntimeBridgeModule(private val context: ReactApplicationContext) :
}
private fun presence(exists: Boolean) = if (exists) "present" else "missing"
private fun validEncodedId(value: String) = value.isNotEmpty() && value.length <= 64
private fun validEncodedId(value: String) = value.toByteArray(Charsets.UTF_8).size in 1..MAX_ENCODED_ID_UTF8_BYTES
private fun validEncodedContractJson(value: String) = value.isNotEmpty() && value.toByteArray(Charsets.UTF_8).size <= MAX_CONTRACT_UTF8_BYTES && try { validEncodedId(org.json.JSONObject(value).optString("profileID", "")) } catch (_: org.json.JSONException) { false }
private fun validUInt16(value: Double, positive: Boolean) = value.isFinite() && value == kotlin.math.floor(value) && value >= if (positive) 1 else 0 && value <= 0xffff
private fun inactiveSelector(semanticID: Double, groupID: Double) = semanticID == 0.0 && groupID == 0.0
private fun validSafeInteger(value: Double, positive: Boolean) = value.isFinite() && value <= MAX_SAFE_INTEGER && value >= if (positive) 1 else 0 && value == kotlin.math.floor(value)
@@ -309,6 +310,7 @@ class WeatherRuntimeBridgeModule(private val context: ReactApplicationContext) :
const val NAME = "WeatherRuntimeBridge"
private const val MAX_METADATA_UTF8_BYTES = 1_048_576
private const val MAX_CONTRACT_UTF8_BYTES = 256 * 1024
private const val MAX_ENCODED_ID_UTF8_BYTES = 64
private const val MAX_SAFE_INTEGER = 9_007_199_254_740_991.0
private const val MAX_PAYLOAD_BYTES = 64L * 1024L * 1024L
private const val DEFAULT_READ_BUFFER_BYTES = 16 * 1024

View File

@@ -0,0 +1,9 @@
const MAX_ENCODED_ID_UTF8_BYTES = 64;
/** Transport guard only; Core retains its own field/profile structural rules. */
export function hasValidEncodedIdUtf8Length(value: unknown): value is string {
return typeof value === "string" && (() => {
const bytes = new TextEncoder().encode(value).length;
return bytes >= 1 && bytes <= MAX_ENCODED_ID_UTF8_BYTES;
})();
}

View File

@@ -1,10 +1,10 @@
import { getNativeFieldRuntimeBridge } from "./NativeWeatherRuntimeBridge";
import { hasValidEncodedIdUtf8Length } from "./EncodedFieldGuards";
import type { ColorChannel, CommandResult, EncodedColorBindingInput, EncodedFieldRuntimeMetadata, EncodedFieldSubmitResult, EncodedParticleBindingInput, FieldColorStop, FieldPayloadRefs, FieldSubmitResult, FieldValueSource, PlaybackState, ResolvedFieldProfileContractInput, RuntimeSnapshot, TargetFps } from "./types";
const MAX_SAFE = Number.MAX_SAFE_INTEGER;
const MAX_ID_LENGTH = 64;
const MAX_CONTRACT_BYTES = 256 * 1024;
const encodedString = (value: string, name: string) => { if (typeof value !== "string" || value.length === 0 || value.length > MAX_ID_LENGTH) throw new Error(`${name} must be a non-empty string of at most ${MAX_ID_LENGTH} characters`); };
const encodedString = (value: string, name: string) => { if (!hasValidEncodedIdUtf8Length(value)) throw new Error(`${name} must be a non-empty UTF-8 string of at most 64 bytes`); };
const uint = (value: number, name: string, maximum = MAX_SAFE, positive = false) => { if (!Number.isSafeInteger(value) || value < (positive ? 1 : 0) || value > maximum) throw new Error(`${name} must be a ${positive ? "positive" : "non-negative"} safe integer`); };
function encodedContractJson(contract: ResolvedFieldProfileContractInput): string { encodedString(contract.profileID, "profileID"); uint(contract.contractSchemaVersion, "contractSchemaVersion", 0xffff, true); if (contract.contractSchemaVersion !== 1) throw new Error("unsupported contractSchemaVersion"); uint(contract.profileVersion, "profileVersion", 0xffffffff, true); if (!Array.isArray(contract.requiredPlanes) || contract.requiredPlanes.length === 0 || contract.requiredPlanes.length > 64) throw new Error("requiredPlanes must contain 1..64 entries"); for (const p of contract.requiredPlanes) { uint(p.semanticID, "semanticID", 0xffff, true); uint(p.unitID, "unitID", 0xffff); uint(p.groupID, "groupID", 0xffff); if (!p.requiredFlag || !["SCALAR", "VELOCITY_COMPONENT", "DIRECTION_ANGLE", "VALIDITY"].includes(p.valueClass) || !["NONE", "U_EAST", "V_NORTH", "W_UP"].includes(p.component)) throw new Error("invalid required plane contract"); } const json = JSON.stringify(contract); if (new TextEncoder().encode(json).length > MAX_CONTRACT_BYTES) throw new Error("contractJson exceeds 256 KiB"); return json; }

View File

@@ -0,0 +1,8 @@
import { hasValidEncodedIdUtf8Length } from "../src/EncodedFieldGuards";
const expect = (value: boolean): void => { if (!value) throw new Error("encoded UTF-8 ID guard failed"); };
expect(hasValidEncodedIdUtf8Length("a".repeat(64)));
expect(!hasValidEncodedIdUtf8Length(""));
// 33 CJK code points are within a 64-character UI limit but occupy 99 UTF-8 bytes.
expect(!hasValidEncodedIdUtf8Length("风".repeat(33)));

View File

@@ -37,6 +37,7 @@
#include <mbgl/style/filter.hpp>
#include <mbgl/renderer/query.hpp>
#include <mbgl/particle/field/field_profile_contract_json.hpp>
#include <mbgl/particle/ingress/encoded_field_transport.hpp>
// Java -> C++ conversion
#include "style/android_conversion.hpp"
@@ -1425,7 +1426,7 @@ jni::jboolean NativeMapView::controlRasterArraySourceTimeline(JNIEnv& env,
jni::jint NativeMapView::submitRasterArraySourceEncodedField(JNIEnv& env, const jni::String& sourceId, const jni::String& fieldKey, const jni::String& fieldId, jni::jlong generation, jni::jlong dataVersion, const jni::String& contractJson, const jni::Array<jbyte>& qbin) {
const auto id=jni::Make<std::string>(env,sourceId); auto* source=dynamic_cast<mbgl::style::RasterArraySource*>(map->getStyle().getSource(id));
if(!source) return 0; // bridge-layer SOURCE_MISSING sentinel, never a Core result
if(generation<=0 || dataVersion<0 || !qbin) return static_cast<jni::jint>(mbgl::style::RasterArrayEncodedFieldSubmitResult::RejectedDecode);
if(generation<=0 || dataVersion<0 || !qbin || !mbgl::particle::ingress::isValidEncodedFieldTransportSize(qbin.Length(env))) return static_cast<jni::jint>(mbgl::style::RasterArrayEncodedFieldSubmitResult::RejectedDecode);
const auto contract=mbgl::particle::parseResolvedFieldProfileContractJson(jni::Make<std::string>(env,contractJson));
if(!contract) return static_cast<jni::jint>(mbgl::style::RasterArrayEncodedFieldSubmitResult::RejectedContract);
std::vector<uint8_t> bytes(qbin.Length(env)); if(!bytes.empty()) jni::GetArrayRegion(env,*qbin,0,bytes.size(),reinterpret_cast<jbyte*>(bytes.data()));

View File

@@ -7,6 +7,8 @@ import android.os.Handler;
import android.os.Looper;
import java.util.List;
import org.json.JSONException;
import org.json.JSONObject;
/**
* Main-thread, low-frequency Host facade for one raster-array weather runtime.
@@ -86,20 +88,28 @@ public final class WeatherRuntime {
/** Isolated V4 ingress: it deliberately does not mutate V3 state/cadence/timeline. */
public EncodedFieldSubmitResult submitEncodedField(@NonNull String fieldKey, @NonNull String fieldId, long generation, long dataVersion, @NonNull String contractJson, @NonNull byte[] qbin) {
requireAttached(); requireEnabled();
if (fieldKey.isEmpty() || fieldId.isEmpty() || generation <= 0 || dataVersion < 0 || contractJson.isEmpty() || qbin.length == 0) throw new IllegalArgumentException("invalid encoded field argument");
if (!WeatherRuntimeV4Guards.hasValidEncodedIdUtf8Length(fieldKey) || !WeatherRuntimeV4Guards.hasValidEncodedIdUtf8Length(fieldId) || !WeatherRuntimeV4Guards.hasValidEncodedContractUtf8Length(contractJson) || !hasValidEncodedContractProfileId(contractJson) || generation <= 0 || dataVersion < 0 || !WeatherRuntimeV4Guards.hasValidEncodedQbin(qbin)) throw new IllegalArgumentException("invalid encoded field argument");
int code = map.submitRasterArraySourceEncodedField(config.sourceId, fieldKey, fieldId, generation, dataVersion, contractJson, qbin);
if (code == 0) throw new IllegalStateException("SOURCE_MISSING");
return EncodedFieldSubmitResult.fromCode(code);
}
public CommandResult clearEncodedField(@NonNull String fieldKey) { requireAttached(); return fieldKey.isEmpty() ? CommandResult.REJECTED : encodedCommand(map.clearRasterArraySourceEncodedField(config.sourceId, fieldKey)); }
private static boolean hasValidEncodedContractProfileId(String contractJson) {
if (contractJson == null) return false;
try {
return WeatherRuntimeV4Guards.hasValidEncodedIdUtf8Length(new JSONObject(contractJson).optString("profileID", ""));
} catch (JSONException exception) {
return false;
}
}
public CommandResult clearEncodedField(@NonNull String fieldKey) { requireAttached(); return !WeatherRuntimeV4Guards.hasValidEncodedIdUtf8Length(fieldKey) ? CommandResult.REJECTED : encodedCommand(map.clearRasterArraySourceEncodedField(config.sourceId, fieldKey)); }
public CommandResult clearAllEncodedFields() { requireAttached(); return encodedCommand(map.clearAllRasterArraySourceEncodedFields(config.sourceId)); }
/** V4 binding intent; unit and all mode selectors reach Core unchanged. */
public CommandResult setEncodedColorBinding(@NonNull String fieldKey, int semanticID, int groupID, int valueClass, int expectedUnitID) {
requireAttached(); return encodedCommand(map.setRasterArraySourceEncodedColorBinding(config.sourceId, fieldKey, semanticID, groupID, valueClass, expectedUnitID));
requireAttached(); return !WeatherRuntimeV4Guards.hasValidEncodedIdUtf8Length(fieldKey) ? CommandResult.REJECTED : encodedCommand(map.setRasterArraySourceEncodedColorBinding(config.sourceId, fieldKey, semanticID, groupID, valueClass, expectedUnitID));
}
/** ABI reserves all selector slots: inactive slots must be exactly (0, 0). */
public CommandResult setEncodedParticleBinding(@NonNull String fieldKey, int mode, int uSemanticID, int uGroupID, int vSemanticID, int vGroupID, int directionSemanticID, int directionGroupID, int expectedUnitID) {
requireAttached(); return encodedCommand(map.setRasterArraySourceEncodedParticleBinding(config.sourceId, fieldKey, mode, uSemanticID, uGroupID, vSemanticID, vGroupID, directionSemanticID, directionGroupID, expectedUnitID));
requireAttached(); return !WeatherRuntimeV4Guards.hasValidEncodedIdUtf8Length(fieldKey) ? CommandResult.REJECTED : encodedCommand(map.setRasterArraySourceEncodedParticleBinding(config.sourceId, fieldKey, mode, uSemanticID, uGroupID, vSemanticID, vGroupID, directionSemanticID, directionGroupID, expectedUnitID));
}
public CommandResult setEncodedForecastTime(long timestamp) { requireAttached(); return timestamp < 0 ? CommandResult.REJECTED : encodedCommand(map.setRasterArraySourceEncodedForecastTime(config.sourceId, timestamp)); }
/** State and speed intentionally remain independent V4 Core commands. */

View File

@@ -0,0 +1,27 @@
package org.maplibre.android.maps;
import java.nio.charset.StandardCharsets;
/** Small transport guards shared by Java facade checks; they do not define QBIN decoding. */
final class WeatherRuntimeV4Guards {
static final int MAX_ENCODED_ID_UTF8_BYTES = 64;
static final int MAX_CONTRACT_UTF8_BYTES = 256 * 1024;
static final int MAX_ENCODED_QBIN_BYTES = 64 * 1024 * 1024;
private WeatherRuntimeV4Guards() {}
static boolean hasValidEncodedIdUtf8Length(String value) {
if (value == null) return false;
final int length = value.getBytes(StandardCharsets.UTF_8).length;
return length >= 1 && length <= MAX_ENCODED_ID_UTF8_BYTES;
}
static boolean hasValidEncodedQbin(byte[] qbin) {
return qbin != null && qbin.length >= 1 && qbin.length <= MAX_ENCODED_QBIN_BYTES;
}
static boolean hasValidEncodedContractUtf8Length(String contractJson) {
return contractJson != null && !contractJson.isEmpty()
&& contractJson.getBytes(StandardCharsets.UTF_8).length <= MAX_CONTRACT_UTF8_BYTES;
}
}

View File

@@ -0,0 +1,17 @@
package org.maplibre.android.maps;
/** JDK-only directed guard test; Android facade compilation remains a later Android validation. */
public final class WeatherRuntimeV4GuardsTest {
public static void main(String[] args) {
require(WeatherRuntimeV4Guards.hasValidEncodedIdUtf8Length(repeat("a", 64)));
require(!WeatherRuntimeV4Guards.hasValidEncodedIdUtf8Length(repeat("风", 33)));
require(WeatherRuntimeV4Guards.hasValidEncodedQbin(new byte[64 * 1024 * 1024]));
require(!WeatherRuntimeV4Guards.hasValidEncodedQbin(new byte[0]));
require(!WeatherRuntimeV4Guards.hasValidEncodedQbin(new byte[64 * 1024 * 1024 + 1]));
require(WeatherRuntimeV4Guards.hasValidEncodedContractUtf8Length("{}"));
require(!WeatherRuntimeV4Guards.hasValidEncodedContractUtf8Length("a".repeat(256 * 1024 + 1)));
}
private static String repeat(String value, int count) { return value.repeat(count); }
private static void require(boolean value) { if (!value) throw new AssertionError("V4 transport guard failed"); }
}

View File

@@ -22,6 +22,7 @@ add_library(
${PROJECT_SOURCE_DIR}/test/particle/field_binding.test.cpp
${PROJECT_SOURCE_DIR}/test/particle/field_profile_contract_json.test.cpp
${PROJECT_SOURCE_DIR}/test/particle/qbin_v2_decoder.test.cpp
${PROJECT_SOURCE_DIR}/test/particle/encoded_field_transport.test.cpp
${PROJECT_SOURCE_DIR}/test/plugin/plugin.test.cpp
${PROJECT_SOURCE_DIR}/test/renderer/image_manager.test.cpp
${PROJECT_SOURCE_DIR}/test/renderer/pattern_atlas.test.cpp

View File

@@ -0,0 +1,13 @@
#include <mbgl/particle/ingress/encoded_field_transport.hpp>
#include <gtest/gtest.h>
namespace mbgl::particle::ingress {
TEST(ParticleEncodedFieldTransport, RejectsEmptyAndOversizedBeforeCopy) {
EXPECT_FALSE(isValidEncodedFieldTransportSize(0));
EXPECT_TRUE(isValidEncodedFieldTransportSize(MaxEncodedFieldTransportBytes));
EXPECT_FALSE(isValidEncodedFieldTransportSize(MaxEncodedFieldTransportBytes + 1u));
}
} // namespace mbgl::particle::ingress

View File

@@ -779,3 +779,9 @@ Android V3-08 单次综合验收已完成并关闭。Native `13.4.1-particle.2`
- frozen contract schema 改为 Core 同名 `contractSchemaVersion` / `requiredFlag` 等字段,并抽取为可 Linux 测试的 provider-neutral C++ parser;新增合法与所有指定 fail-closed case 单测。
- Color binding 端到端传递 expected unit;Particle binding 固化 mode-aware U/V/direction slots,inactive slot 仅 `(0,0)`;forecast state/speed 为完全独立 Core command。
- Linux 限流 runner build、parser/Source V4/Binding/QBIN/Runtime/V3 Source 回归、TypeScript、TurboModule codegen、JNI declaration-registration 静态核对和 `git diff --check` 均 PASS。Android Gradle/APK/ADB/JNI 真实编译、Jest、iOS/Metal 未执行,等待后续一次性 Android 验收;未进入 STEP-079。
### 2026-09-13 STEP-078 v3 production guard 审计收口
- QBIN transport 的 1..64 MiB 上限现由 Kotlin local-file、Java `WeatherRuntime` facade、JNI copy 三层执行;JNI 在 `std::vector` 分配前 fail-closed。Decoder 的 64 MiB file / 32 MiB decoded-raw 业务限制不变。
- fieldKey、runtime fieldID、profileID 统一按 UTF-8 byte length 1..64 验证:RN `TextEncoder`、Kotlin `toByteArray(Charsets.UTF_8)`、Java `StandardCharsets.UTF_8`。多字节 33 CJK(99 bytes)输入在 RN/Java guard tests 中拒绝。
- `particle-codex` 内所有限流验证均 PASS:package tsc、TS directed test、TurboModule schema、JDK-only Java guard、Linux runner rebuild、V4/V3 指定 filter、JNI name-set 与 `git diff --check`。Android Gradle/APK/ADB/JNI 真实编译、设备、Jest、iOS/Metal 仍未运行;未改 Renderer/GPU/shader/Test Host/NavSea,不进入 STEP-079。