feat(ios): expose lossless-or-reject style filter API

This commit is contained in:
tei
2026-10-21 15:38:35 +09:00
parent 9d6c9e9684
commit 1aba9875f0
10 changed files with 971 additions and 0 deletions

View File

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

View File

@@ -1,8 +1,382 @@
#import "MLNVectorStyleLayer.h"
#import "MLNStyleLayer_Private.h"
#import <CoreFoundation/CoreFoundation.h>
#include <mln/style/conversion/filter.hpp>
#include <cmath>
#include <cstring>
NSErrorDomain const MLNStyleFilterErrorDomain = @"MLNStyleFilterErrorDomain";
namespace {
using Filter = mln::style::Filter;
using Value = mln::Value;
using Array = std::vector<Value>;
using Object = std::unordered_map<std::string, Value>;
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<Value> 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<Value> 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<Value> { return Value(mln::NullValue()); },
[](bool v) -> std::optional<Value> { return Value(v); },
[](int64_t v) -> std::optional<Value> {
if (v < -MLNFilterMaxInteger || v > MLNFilterMaxInteger) return std::nullopt;
return FilterNumber(static_cast<double>(v));
},
[](uint64_t v) -> std::optional<Value> {
if (v > static_cast<uint64_t>(MLNFilterMaxInteger)) return std::nullopt;
return FilterNumber(static_cast<double>(v));
},
[](float v) { return FilterNumber(v); }, [](double v) { return FilterNumber(v); },
[&](const std::string &v) -> std::optional<Value> {
return budget.string(v) ? std::optional<Value>(Value(v)) : std::nullopt;
},
[&](const Array &items) -> std::optional<Value> {
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<Value> {
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<Value> { return std::nullopt; });
}
std::optional<Value> NormalizeFilterValue(const Value &value) {
FilterBudget budget;
return NormalizeFilterValue(value, budget);
}
std::optional<Value> 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<const char *>(data.bytes) : "", data.length);
return budget.string(value) ? std::optional<Value>(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::string>(), std::move(*child)).second) return std::nullopt;
}
return Value(std::move(result));
}
return std::nullopt;
}
std::optional<Value> SnapshotFilterJSON(id json) {
FilterBudget budget;
return SnapshotFilterJSON(json, budget);
}
// 有限操作符准入;literal 的载荷是 JSON 数据,不按表达式解释。
// 特别在 Core 访问 ["in"] 的缺失参数前拒绝非法形状。
bool FilterSyntax(const Value &value) {
if (!value.is<Array>()) return !value.is<Object>();
const auto &items = value.get<Array>();
if (items.empty() || !items[0].is<std::string>()) return false;
const auto &op = items[0].get<std::string>();
const auto count = items.size();
if (op == "literal") return count == 2;
if (op == "collator") {
if (count != 2 || !items[1].is<Object>()) return false;
for (const auto &item : items[1].get<Object>()) 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<Array>()) return false;
const auto &items = value.get<Array>();
if (items.empty() || !items[0].is<std::string>()) return false;
const auto &op = items[0].get<std::string>();
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<std::string>()) 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<Array>() || items[i].is<Object>()) 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<Filter> ParseFilterJSON(const Value &expected, id json) {
if (expected.is<mln::NullValue>()) return Filter();
if (!expected.is<Array>() || !FilterSyntax(expected)) return std::nullopt;
mln::style::conversion::Error error;
auto parsed = mln::style::conversion::convert<Filter>(
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<mln::style::Layer> &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"];
}