use serde_json::{json, Value};
use rust_store_core::command::{
plan_insert, plan_mutation, plan_query, plan_query_with_count, plan_remove, plan_update,
plan_update_many, Probe,
};
use rust_store_core::permission::{
can_read_schema, can_write_schema, should_inject_owner_condition, Context, UnconfiguredPolicy,
};
use rust_store_core::rbac;
use rust_store_core::schema::Registry;
const RBAC_PREFIX: &str = "ERR_PERMISSION:RBAC:";
fn post_schema() -> Value {
json!({
"name": "Post",
"collection": "posts",
"timestamps": false,
"fields": {
"title": { "type": "string" },
"body": { "type": "string" },
"secret": { "type": "string" }
},
"relations": {},
})
}
fn comment_schema() -> Value {
json!({
"name": "Comment",
"collection": "comments",
"timestamps": false,
"fields": { "body": { "type": "string" } },
"relations": {},
})
}
fn post_with_rel_schema() -> Value {
json!({
"name": "Post",
"collection": "posts",
"idPrefix": "p",
"timestamps": false,
"fields": { "title": { "type": "string" } },
"relations": {
"comments": { "model": "Comment", "type": "many", "localField": "_id", "foreignField": "postId" }
},
})
}
fn projection_of(plan: &rust_store_core::command::QueryPlan) -> Value {
let cmd = &plan.commands[0];
if let Some(p) = cmd.get("projection") {
if !p.is_null() {
return p.clone();
}
}
if let Some(pipeline) = cmd.get("pipeline").and_then(|v| v.as_array()) {
for s in pipeline {
if let Some(p) = s.get("$project") {
return p.clone();
}
}
}
Value::Null
}
fn registry_with(defns: &[Value]) -> Registry {
let mut reg = Registry::new();
for d in defns {
reg.register(d).expect("schema 注册应成功");
}
reg
}
fn ctx_of(uid: &str, roles: &[&str]) -> Context {
Context {
user_id: Some(uid.to_string()),
roles: Some(roles.iter().map(|r| r.to_string()).collect()),
..Default::default()
}
}
fn policy(v: Value) -> Value {
v
}
fn params_of(extra: Value) -> serde_json::Map<String, Value> {
extra.as_object().cloned().unwrap_or_default()
}
#[test]
fn parse_rejects_unknown_action() {
let mut reg = registry_with(&[post_schema()]);
let err = reg
.set_rbac(Some(&policy(json!({
"grants": [ { "role": "r1", "model": "Post", "actions": ["drop"] } ]
}))))
.expect_err("未知 action 应 Err");
assert!(err.contains("非法值"), "{err}");
}
#[test]
fn parse_rejects_empty_role_and_model() {
let mut reg = registry_with(&[post_schema()]);
assert!(reg
.set_rbac(Some(&policy(json!({
"grants": [ { "role": "", "model": "Post", "actions": ["read"] } ]
}))))
.is_err());
assert!(reg
.set_rbac(Some(&policy(json!({
"grants": [ { "role": "r1", "model": "", "actions": ["read"] } ]
}))))
.is_err());
}
#[test]
fn parse_rejects_operator_condition_and_non_scalar() {
let mut reg = registry_with(&[post_schema()]);
assert!(reg
.set_rbac(Some(&policy(json!({
"grants": [ { "role": "r1", "model": "Post", "actions": ["read"], "condition": { "$gt": 1 } } ]
}))))
.is_err());
assert!(reg
.set_rbac(Some(&policy(json!({
"grants": [ { "role": "r1", "model": "Post", "actions": ["read"], "condition": { "status": { "ne": "x" } } } ]
}))))
.is_err());
}
#[test]
fn parse_rejects_enforce_without_roles_and_accepts_write_shorthand() {
let mut reg = registry_with(&[post_schema()]);
assert!(reg
.set_rbac(Some(&policy(json!({ "mode": "enforce", "grants": [] }))))
.is_err(), "enforce 无受管角色应 Err");
reg.set_rbac(Some(&policy(json!({
"roles": { "r1": {} },
"grants": [ { "role": "r1", "model": "Post", "actions": ["read", "write"] } ]
}))))
.expect("合法策略应注入成功");
}
#[test]
fn overlay_without_grant_is_transparent() {
let mut reg = registry_with(&[post_schema()]);
reg.set_rbac(Some(&policy(json!({
"roles": { "editor": {} },
"grants": [ { "role": "editor", "model": "Comment", "actions": ["read"] } ]
}))))
.unwrap();
let c = ctx_of("u1", &["editor"]);
plan_query("Post{ title }", ¶ms_of(json!({})), ®, Some(&c))
.unwrap_or_else(|e| panic!("overlay 未覆盖 model 应直通: {e}"));
}
#[test]
fn overlay_grant_allows_and_denies_by_action() {
let mut reg = registry_with(&[post_schema()]);
reg.set_rbac(Some(&policy(json!({
"roles": { "viewer": {} },
"grants": [ { "role": "viewer", "model": "Post", "actions": ["read"] } ]
}))))
.unwrap();
let v = ctx_of("u1", &["viewer"]);
plan_query("Post{ title }", ¶ms_of(json!({})), ®, Some(&v))
.unwrap_or_else(|e| panic!("granted read 应放行: {e}"));
let err = plan_insert(
"Post",
®,
Some(&v),
&json!({ "_id": "p1", "title": "x" }),
0,
"p1",
None,
)
.expect_err("未授予 insert 应拒绝");
assert!(err.starts_with(RBAC_PREFIX), "{err}");
}
#[test]
fn wildcard_grant_matches_any_model() {
let mut reg = registry_with(&[post_schema(), comment_schema()]);
reg.set_rbac(Some(&policy(json!({
"roles": { "viewer": {} },
"grants": [ { "role": "viewer", "model": "*", "actions": ["read"] } ]
}))))
.unwrap();
let v = ctx_of("u1", &["viewer"]);
plan_query("Post{ title }", ¶ms_of(json!({})), ®, Some(&v))
.unwrap_or_else(|e| panic!("通配 read 应放行 Post: {e}"));
plan_query("Comment{ body }", ¶ms_of(json!({})), ®, Some(&v))
.unwrap_or_else(|e| panic!("通配 read 应放行 Comment: {e}"));
}
#[test]
fn enforce_default_denies_unconfigured_models() {
let mut reg = registry_with(&[post_schema(), comment_schema()]);
reg.set_rbac(Some(&policy(json!({
"mode": "enforce",
"roles": { "editor": {} },
"grants": [ { "role": "editor", "model": "Post", "actions": ["read"] } ]
}))))
.unwrap();
let e = ctx_of("u1", &["editor"]);
plan_query("Post{ title }", ¶ms_of(json!({})), ®, Some(&e))
.unwrap_or_else(|err| panic!("enforce 已配置 model 应放行: {err}"));
let err = plan_query("Comment{ body }", ¶ms_of(json!({})), ®, Some(&e))
.expect_err("enforce 受管角色未配置 model 应 default deny");
assert!(err.starts_with(RBAC_PREFIX), "{err}");
}
#[test]
fn enforce_unmanaged_roles_transparent() {
let mut reg = registry_with(&[post_schema()]);
reg.set_rbac(Some(&policy(json!({
"mode": "enforce",
"roles": { "editor": {} },
"grants": [ { "role": "editor", "model": "Post", "actions": ["read"] } ]
}))))
.unwrap();
let v = ctx_of("u1", &["visitor"]);
plan_query("Post{ title }", ¶ms_of(json!({})), ®, Some(&v))
.unwrap_or_else(|e| panic!("enforce 未受管角色应直通: {e}"));
}
#[test]
fn bypass_paths_are_transparent() {
let mut reg = registry_with(&[post_schema()]);
reg.set_rbac(Some(&policy(json!({
"mode": "enforce",
"roles": { "editor": {} },
"grants": []
}))))
.unwrap();
plan_query("Post{ title }", ¶ms_of(json!({})), ®, None)
.unwrap_or_else(|e| panic!("ctx=None 应直通: {e}"));
let sys = Context::system();
plan_query("Post{ title }", ¶ms_of(json!({})), ®, Some(&sys))
.unwrap_or_else(|e| panic!("internal 应直通: {e}"));
}
#[test]
fn clearing_policy_restores_passthrough() {
let mut reg = registry_with(&[post_schema()]);
reg.set_rbac(Some(&policy(json!({
"mode": "enforce",
"roles": { "editor": {} },
"grants": []
}))))
.unwrap();
let e = ctx_of("u1", &["editor"]);
assert!(plan_query("Post{ title }", ¶ms_of(json!({})), ®, Some(&e)).is_err());
reg.set_rbac(None).expect("清除应成功");
plan_query("Post{ title }", ¶ms_of(json!({})), ®, Some(&e))
.unwrap_or_else(|err| panic!("清除策略后应直通: {err}"));
}
#[test]
fn readfields_intersect_prunes_projection() {
let mut reg = registry_with(&[post_schema()]);
reg.set_rbac(Some(&policy(json!({
"roles": { "viewer": {} },
"grants": [ { "role": "viewer", "model": "Post", "actions": ["read"], "readFields": ["title", "body"] } ]
}))))
.unwrap();
let v = ctx_of("u1", &["viewer"]);
let plan = plan_query("Post{ title body secret }", ¶ms_of(json!({})), ®, Some(&v))
.expect("granted read 应放行");
let proj = projection_of(&plan);
let proj = proj.as_object().expect("应有投影");
assert!(proj.contains_key("title") && proj.contains_key("body"), "可读字段应保留");
assert!(!proj.contains_key("secret"), "readFields 之外应被裁剪: {proj:?}");
assert!(proj.contains_key("_id"), "_id 豁免应保留");
}
#[test]
fn writefields_intersect_filters_insert_data() {
let mut reg = registry_with(&[post_schema()]);
reg.set_rbac(Some(&policy(json!({
"roles": { "writer": {} },
"grants": [ { "role": "writer", "model": "Post", "actions": ["insert"], "writeFields": ["title"] } ]
}))))
.unwrap();
let w = ctx_of("u1", &["writer"]);
let plan = plan_insert(
"Post",
®,
Some(&w),
&json!({ "_id": "p1", "title": "t", "secret": "s" }),
0,
"p1",
None,
)
.expect("granted insert 应放行");
let doc = &plan["command"]["doc"];
assert_eq!(doc["title"], json!("t"), "可写字段应保留");
assert!(doc.get("secret").is_none(), "writeFields 之外应被过滤: {doc}");
}
#[test]
fn owner_only_read_injects_row_condition() {
let mut reg = registry_with(&[post_schema()]);
reg.set_rbac(Some(&policy(json!({
"roles": { "reader": {} },
"grants": [ { "role": "reader", "model": "Post", "actions": ["read"], "ownerOnly": true } ]
}))))
.unwrap();
let r = ctx_of("u1", &["reader"]);
let plan = plan_query("Post{ title }", ¶ms_of(json!({})), ®, Some(&r))
.expect("ownerOnly read 应放行规划");
let s = plan.commands[0].to_string();
assert!(s.contains("createdBy"), "应注入 createdBy 行条件: {s}");
assert!(s.contains("u1"), "行条件应绑定当前 userId: {s}");
}
#[test]
fn static_condition_merges_with_rbac_row_condition() {
let static_schema = json!({
"name": "Post",
"collection": "posts",
"timestamps": false,
"fields": { "title": { "type": "string" } },
"relations": {},
"read": ["creator"]
});
let mut reg = registry_with(&[static_schema]);
reg.set_rbac(Some(&policy(json!({
"roles": { "reader": {} },
"grants": [ { "role": "reader", "model": "Post", "actions": ["read"] } ]
}))))
.unwrap();
let r = ctx_of("u1", &["reader"]);
let plan = plan_query("Post{ title }", ¶ms_of(json!({})), ®, Some(&r))
.expect("两引擎都过应放行");
let s = plan.commands[0].to_string();
assert!(s.contains("createdBy"), "静态 creator owner 条件不得被 RBAC 放大: {s}");
}
#[test]
fn count_injects_owner_row_condition() {
let mut reg = registry_with(&[post_schema()]);
reg.set_rbac(Some(&policy(json!({
"roles": { "reader": {} },
"grants": [ { "role": "reader", "model": "Post", "actions": ["read"], "ownerOnly": true } ]
}))))
.unwrap();
let r = ctx_of("u1", &["reader"]);
let plan = plan_query_with_count("Post{ title }", ¶ms_of(json!({})), ®, Some(&r))
.expect("count 应放行规划");
let s = plan.count_command.to_string();
assert!(s.contains("createdBy"), "count 应注入行条件: {s}");
}
#[test]
fn condition_based_row_filter_in_read() {
let mut reg = registry_with(&[post_schema()]);
reg.set_rbac(Some(&policy(json!({
"roles": { "reader": {} },
"grants": [ { "role": "reader", "model": "Post", "actions": ["read"], "condition": { "status": "published" } } ]
}))))
.unwrap();
let r = ctx_of("u1", &["reader"]);
let plan = plan_query("Post{ title }", ¶ms_of(json!({})), ®, Some(&r))
.expect("condition read 应放行规划");
let s = plan.commands[0].to_string();
assert!(s.contains("published"), "静态行条件应注入: {s}");
}
#[test]
fn owner_only_update_probes_and_judges_doc() {
let mut reg = registry_with(&[post_schema()]);
reg.set_rbac(Some(&policy(json!({
"roles": { "editor": {} },
"grants": [ { "role": "editor", "model": "Post", "actions": ["update"], "ownerOnly": true } ]
}))))
.unwrap();
let e = ctx_of("u1", &["editor"]);
let cond = json!({ "_id": "p1" });
let first = plan_update(
"Post",
®,
Some(&e),
&cond,
&json!({ "title": "y" }),
&json!({}),
0,
Probe::NotProbed,
)
.expect("首入应返回探针");
let probe_cmd = first.get("needsProbe").expect("应含 needsProbe");
let proj = probe_cmd["projection"].as_object().expect("探针应有投影");
assert!(proj.contains_key("createdBy"), "探针投影应含 createdBy");
let other = plan_update(
"Post",
®,
Some(&e),
&cond,
&json!({ "title": "y" }),
&json!({}),
0,
Probe::Found(&json!({ "_id": "p1", "createdBy": "someone_else" })),
)
.expect_err("ownerOnly 更新他人文档应拒绝");
assert!(other.starts_with(RBAC_PREFIX), "{other}");
plan_update(
"Post",
®,
Some(&e),
&cond,
&json!({ "title": "y" }),
&json!({}),
0,
Probe::Found(&json!({ "_id": "p1", "createdBy": "u1" })),
)
.expect("ownerOnly 更新本人文档应放行");
}
#[test]
fn owner_only_remove_probes_like_static_creator() {
let mut reg = registry_with(&[post_schema()]);
reg.set_rbac(Some(&policy(json!({
"roles": { "editor": {} },
"grants": [ { "role": "editor", "model": "Post", "actions": ["remove"], "ownerOnly": true } ]
}))))
.unwrap();
let e = ctx_of("u1", &["editor"]);
let first = plan_remove("Post", ®, Some(&e), &json!({ "_id": "p1" }), Probe::NotProbed)
.expect("首入应返回探针");
assert!(first.get("needsProbe").is_some(), "remove ownerOnly 应走探针: {first}");
let err = plan_remove(
"Post",
®,
Some(&e),
&json!({ "_id": "p1" }),
Probe::Found(&json!({ "_id": "p1", "createdBy": "someone_else" })),
)
.expect_err("ownerOnly 删除他人文档应拒绝");
assert!(err.starts_with(RBAC_PREFIX), "{err}");
}
#[test]
fn owner_only_update_many_merges_condition() {
let mut reg = registry_with(&[post_schema()]);
reg.set_rbac(Some(&policy(json!({
"roles": { "editor": {} },
"grants": [ { "role": "editor", "model": "Post", "actions": ["update"], "ownerOnly": true } ]
}))))
.unwrap();
let e = ctx_of("u1", &["editor"]);
let plan = plan_update_many(
"Post",
®,
Some(&e),
&json!({ "status": "draft" }),
&json!({ "title": "y" }),
0,
)
.expect("updateMany 行级应合并条件");
let filter = &plan["command"]["filter"];
let ands = filter["$and"].as_array().expect("应 $and 合并: {filter}");
assert!(
ands.iter().any(|c| c == &json!({ "createdBy": "u1" })),
"应含 createdBy 条件: {filter}"
);
assert!(
ands.iter().any(|c| c == &json!({ "status": "draft" })),
"原条件应保留: {filter}"
);
}
#[test]
fn mutation_child_model_denied_by_rbac() {
let mut reg = registry_with(&[post_with_rel_schema(), comment_schema()]);
reg.set_rbac(Some(&policy(json!({
"mode": "enforce",
"roles": { "writer": {} },
"grants": [
{ "role": "writer", "model": "Post", "actions": ["insert"] },
{ "role": "writer", "model": "Comment", "actions": [] }
]
}))))
.expect_err("Comment 空动作 grant 不允许——应解析失败");
let mut reg = registry_with(&[post_with_rel_schema(), comment_schema()]);
reg.set_rbac(Some(&policy(json!({
"mode": "enforce",
"roles": { "writer": {} },
"grants": [ { "role": "writer", "model": "Post", "actions": ["insert"] } ]
}))))
.unwrap();
let w = ctx_of("u1", &["writer"]);
let err = plan_mutation(
"Post",
®,
Some(&w),
&json!({ "title": "x", "comments": [{ "body": "c" }] }),
0,
&["p_new_1".to_string(), "p_new_2".to_string()],
)
.expect_err("enforce 下 Comment 未授权应拒绝");
assert!(err.starts_with(RBAC_PREFIX), "{err}");
}
#[test]
fn decide_unit_semantics() {
let mut reg = registry_with(&[post_schema()]);
let p = json!({
"roles": { "editor": {}, "viewer": {} },
"grants": [
{ "role": "editor", "model": "Post", "actions": ["read", "write"], "writeFields": ["title"] },
{ "role": "viewer", "model": "Post", "actions": ["read"], "readFields": ["title", "body"] }
]
});
reg.set_rbac(Some(&p)).unwrap();
let policy = reg.rbac().expect("策略应已注入");
let both = ctx_of("u1", &["editor", "viewer"]);
let d = rbac::decide(policy, &[], Some(&both), "Post").expect("应有判决");
assert!(d.allowed_actions.contains("read"));
assert!(d.allowed_actions.contains("insert") && d.allowed_actions.contains("remove"));
assert!(d.read_fields.is_none(), "任一 read grant 未声明字段 = 不收紧");
let viewer = ctx_of("u2", &["viewer"]);
let d = rbac::decide(policy, &[], Some(&viewer), "Post").expect("应有判决");
assert!(d.allowed_actions.contains("read"));
assert!(!d.allowed_actions.contains("insert"));
let rf = d.read_fields.as_ref().expect("viewer 应有字段收紧");
assert!(rf.contains("title") && !rf.contains("secret"));
assert!(rbac::decide(policy, &[], Some(&viewer), "Comment").is_none());
assert!(rbac::row_condition(®, reg.get("Post").unwrap(), Some(&both), "read").is_none());
let rc = rbac::row_condition(®, reg.get("Post").unwrap(), Some(&viewer), "read");
assert!(rc.is_none());
}
#[test]
fn enforce_no_exception_default_deny() {
let mut reg = registry_with(&[post_schema()]);
reg.set_rbac(Some(&policy(json!({
"mode": "enforce",
"roles": { "editor": {}, "admin": {} },
"grants": []
}))))
.unwrap();
let a = ctx_of("u1", &["admin"]);
let err = plan_query("Post{ title }", ¶ms_of(json!({})), ®, Some(&a))
.expect_err("豁免清单空 = 无例外 default deny(admin 不再隐形放行)");
assert!(err.starts_with(RBAC_PREFIX), "{err}");
reg.set_exempt_roles(vec!["admin".to_string()]);
plan_query("Post{ title }", ¶ms_of(json!({})), ®, Some(&a))
.unwrap_or_else(|e| panic!("显式豁免后应直通: {e}"));
}
#[test]
fn exempt_roles_configurable_static() {
let read_editor_schema = json!({
"name": "Post",
"collection": "posts",
"timestamps": false,
"fields": { "title": { "type": "string" } },
"relations": {},
"read": ["editor"]
});
let mut reg = registry_with(&[read_editor_schema]);
let schema = reg.get("Post").unwrap();
let a = ctx_of("u1", &["admin"]);
assert!(
!can_read_schema(reg.role_rules(), schema, Some(&a)),
"admin 不再默认放行(原隐形豁免已清单化)"
);
reg.set_exempt_roles(vec!["admin".to_string()]);
let schema = reg.get("Post").unwrap();
assert!(can_read_schema(reg.role_rules(), schema, Some(&a)));
let creator_schema = json!({
"name": "Memo",
"collection": "memos",
"timestamps": false,
"fields": { "body": { "type": "string" } },
"relations": {},
"read": ["creator"]
});
let mut reg2 = registry_with(&[creator_schema]);
let memo = reg2.get("Memo").unwrap();
assert!(
should_inject_owner_condition(reg2.role_rules(), memo, Some(&a)),
"creator-only 下非豁免者应注入 owner 条件"
);
reg2.set_exempt_roles(vec!["admin".to_string()]);
let memo = reg2.get("Memo").unwrap();
assert!(
!should_inject_owner_condition(reg2.role_rules(), memo, Some(&a)),
"豁免者不注入 owner 条件"
);
}
#[test]
fn deny_write_roles_configurable() {
let write_open_schema = json!({
"name": "Post",
"collection": "posts",
"timestamps": false,
"fields": { "title": { "type": "string" } },
"relations": {},
"write": true
});
let mut reg = registry_with(&[write_open_schema]);
let schema = reg.get("Post").unwrap();
let g = ctx_of("u1", &["guest"]);
assert!(can_write_schema(reg.role_rules(), schema, Some(&g)), "guest 写不再必拒");
assert!(
can_read_schema(reg.role_rules(), schema, Some(&g)),
"read 未配 → Open 姿态放行"
);
reg.set_deny_write_roles(vec!["guest".to_string()]);
let schema = reg.get("Post").unwrap();
assert!(!can_write_schema(reg.role_rules(), schema, Some(&g)), "拒写清单命中应拒绝");
assert!(
can_read_schema(reg.role_rules(), schema, Some(&g)),
"拒写只影响写路径,读不受影响"
);
}
#[test]
fn unconfigured_open_allows_guest_read() {
let reg = registry_with(&[post_schema()]); let schema = reg.get("Post").unwrap();
let g = ctx_of("u1", &["guest"]);
assert!(
can_read_schema(reg.role_rules(), schema, Some(&g)),
"Open 下 guest 无白名单读放行——原「无白名单时 guest 读拒」已随清单化移除(迁移差异,§11.4)"
);
}
#[test]
fn unconfigured_closed_fails_secure() {
let mut reg = registry_with(&[post_schema()]);
reg.set_unconfigured_policy(UnconfiguredPolicy::Closed);
let schema = reg.get("Post").unwrap();
let g = ctx_of("u1", &["guest"]);
assert!(!can_read_schema(reg.role_rules(), schema, Some(&g)), "Closed 读拒");
assert!(!can_write_schema(reg.role_rules(), schema, Some(&g)), "Closed 写拒");
let sys = Context::system();
assert!(can_read_schema(reg.role_rules(), schema, Some(&sys)), "internal 不受姿态影响");
assert!(can_read_schema(reg.role_rules(), schema, None), "ctx=None 不受姿态影响");
reg.set_exempt_roles(vec!["admin".to_string()]);
let schema = reg.get("Post").unwrap();
let a = ctx_of("u1", &["admin"]);
assert!(
can_read_schema(reg.role_rules(), schema, Some(&a)),
"豁免清单命中者在 Closed 下仍放行(§11.3 一切判决环节直通)"
);
}
#[test]
fn deny_write_blocks_all_write_paths() {
let mut reg = registry_with(&[post_with_rel_schema(), comment_schema()]);
reg.set_deny_write_roles(vec!["guest".to_string()]);
let g = ctx_of("u1", &["guest"]);
const PERM: &str = "ERR_PERMISSION:";
let err = plan_mutation(
"Post",
®,
Some(&g),
&json!({ "title": "x" }),
0,
&["p_new_1".to_string()],
)
.expect_err("拒写清单命中应拒绝 insert");
assert!(err.starts_with(PERM), "{err}");
let err = plan_update_many(
"Post",
®,
Some(&g),
&json!({ "_id": "p1" }),
&json!({ "title": "y" }),
0,
)
.expect_err("拒写清单命中应拒绝 updateMany");
assert!(err.starts_with(PERM), "{err}");
let err = plan_update(
"Post",
®,
Some(&g),
&json!({ "_id": "p1" }),
&json!({ "title": "y" }),
&json!({}),
0,
Probe::NotProbed,
)
.expect_err("拒写清单命中应拒绝单条 update");
assert!(err.starts_with(PERM), "{err}");
}