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

@@ -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)));