1#![allow(clippy::missing_safety_doc)]
36#![expect(
37 clippy::undocumented_unsafe_blocks,
38 reason = "module-wide FFI safety contract documented in the # Safety preamble above"
39)]
40
41use std::collections::{BTreeMap, BTreeSet};
42use std::ffi::c_char;
43use std::os::raw::c_int;
44
45use serde_json::{json, Value};
46
47use super::NetError;
48use crate::adapter::net::behavior::{
49 ClauseTrace, EvalContext, PredicateDebugReport, PredicateWire, Tag,
50};
51
52fn clause_trace_to_wire(t: &ClauseTrace) -> Value {
59 json!({
60 "label": t.label,
61 "result": t.result,
62 "children": t.children.iter().map(clause_trace_to_wire).collect::<Vec<_>>(),
63 })
64}
65
66fn report_to_wire(report: &PredicateDebugReport) -> Value {
70 let stats: Vec<Value> = report
71 .clause_stats
72 .values()
73 .map(|s| {
74 json!({
75 "label": s.label,
76 "evaluated": s.evaluated,
77 "matched": s.matched,
78 })
79 })
80 .collect();
81 json!({
82 "total_candidates": report.total_candidates,
83 "matched": report.matched,
84 "clause_stats": stats,
85 })
86}
87
88fn parse_tag_array(tags_json_str: &str) -> Option<Vec<Tag>> {
99 let strings: Vec<String> = serde_json::from_str(tags_json_str).ok()?;
100 strings
101 .iter()
102 .map(|s| Tag::parse(s))
103 .collect::<Result<_, _>>()
104 .ok()
105}
106
107fn parse_metadata(metadata_json_str: &str) -> Option<BTreeMap<String, String>> {
109 serde_json::from_str(metadata_json_str).ok()
110}
111
112#[unsafe(no_mangle)]
147pub unsafe extern "C" fn net_predicate_evaluate_with_trace(
148 predicate_json: *const c_char,
149 tags_json: *const c_char,
150 metadata_json: *const c_char,
151 out_result: *mut c_int,
152 out_trace_json: *mut *mut c_char,
153 out_trace_len: *mut usize,
154) -> c_int {
155 if predicate_json.is_null()
156 || tags_json.is_null()
157 || metadata_json.is_null()
158 || out_result.is_null()
159 || out_trace_json.is_null()
160 || out_trace_len.is_null()
161 {
162 return NetError::NullPointer.into();
163 }
164
165 let pred_s = match unsafe { super::mesh::c_str_to_string(predicate_json) } {
166 Some(s) => s,
167 None => return NetError::InvalidUtf8.into(),
168 };
169 let tags_s = match unsafe { super::mesh::c_str_to_string(tags_json) } {
170 Some(s) => s,
171 None => return NetError::InvalidUtf8.into(),
172 };
173 let meta_s = match unsafe { super::mesh::c_str_to_string(metadata_json) } {
174 Some(s) => s,
175 None => return NetError::InvalidUtf8.into(),
176 };
177
178 let wire: PredicateWire = match serde_json::from_str(&pred_s) {
179 Ok(w) => w,
180 Err(_) => return NetError::InvalidJson.into(),
181 };
182 let predicate = match wire.into_predicate() {
183 Ok(p) => p,
184 Err(_) => return NetError::InvalidJson.into(),
185 };
186 let Some(tags) = parse_tag_array(&tags_s) else {
187 return NetError::InvalidJson.into();
188 };
189 let Some(metadata) = parse_metadata(&meta_s) else {
190 return NetError::InvalidJson.into();
191 };
192
193 let ctx = EvalContext::new(&tags, &metadata);
194 let (result, trace) = predicate.evaluate_with_trace(&ctx);
195
196 unsafe {
197 *out_result = if result { 1 } else { 0 };
198 }
199 let payload = clause_trace_to_wire(&trace);
200 super::mesh::write_string_out(
201 serde_json::to_string(&payload).unwrap_or_else(|_| "{}".to_string()),
202 out_trace_json,
203 out_trace_len,
204 )
205}
206
207#[unsafe(no_mangle)]
250pub unsafe extern "C" fn net_predicate_aggregate_debug_report(
251 predicate_json: *const c_char,
252 contexts_json: *const c_char,
253 out_report_json: *mut *mut c_char,
254 out_report_len: *mut usize,
255) -> c_int {
256 if predicate_json.is_null()
257 || contexts_json.is_null()
258 || out_report_json.is_null()
259 || out_report_len.is_null()
260 {
261 return NetError::NullPointer.into();
262 }
263
264 let pred_s = match unsafe { super::mesh::c_str_to_string(predicate_json) } {
265 Some(s) => s,
266 None => return NetError::InvalidUtf8.into(),
267 };
268 let ctx_s = match unsafe { super::mesh::c_str_to_string(contexts_json) } {
269 Some(s) => s,
270 None => return NetError::InvalidUtf8.into(),
271 };
272
273 let wire: PredicateWire = match serde_json::from_str(&pred_s) {
274 Ok(w) => w,
275 Err(_) => return NetError::InvalidJson.into(),
276 };
277 let predicate = match wire.into_predicate() {
278 Ok(p) => p,
279 Err(_) => return NetError::InvalidJson.into(),
280 };
281
282 #[derive(serde::Deserialize)]
287 struct CtxJson {
288 tags: Vec<String>,
289 metadata: BTreeMap<String, String>,
290 }
291 let raw_contexts: Vec<CtxJson> = match serde_json::from_str(&ctx_s) {
292 Ok(v) => v,
293 Err(_) => return NetError::InvalidJson.into(),
294 };
295 let mut owned: Vec<(Vec<Tag>, BTreeMap<String, String>)> =
296 Vec::with_capacity(raw_contexts.len());
297 for c in raw_contexts {
298 let tags: Result<Vec<Tag>, _> = c.tags.iter().map(|s| Tag::parse(s)).collect();
299 let Ok(tags) = tags else {
300 return NetError::InvalidJson.into();
301 };
302 owned.push((tags, c.metadata));
303 }
304
305 let report = PredicateDebugReport::from_evaluations(
306 &predicate,
307 owned
308 .iter()
309 .map(|(tags, meta)| EvalContext::new(tags, meta)),
310 );
311
312 let payload = report_to_wire(&report);
313 super::mesh::write_string_out(
314 serde_json::to_string(&payload).unwrap_or_else(|_| "{}".to_string()),
315 out_report_json,
316 out_report_len,
317 )
318}
319
320fn strip_label<'a>(label: &'a str, prefix: &str, suffix: &str) -> Option<&'a str> {
345 label
346 .strip_prefix(prefix)
347 .and_then(|rest| rest.strip_suffix(suffix))
348}
349
350fn find_redactable_key_split(
359 inner: &str,
360 separator: &str,
361 keys: &BTreeSet<String>,
362) -> Option<usize> {
363 let mut search_start = 0usize;
364 while let Some(rel) = inner[search_start..].find(separator) {
365 let abs = search_start + rel;
366 if keys.contains(&inner[..abs]) {
367 return Some(abs);
368 }
369 search_start = abs + separator.len();
370 if search_start > inner.len() {
371 break;
372 }
373 }
374 None
375}
376
377fn redact_label(label: &str, keys: &BTreeSet<String>) -> String {
381 if let Some(inner) = strip_label(label, "MetadataEquals(", ")") {
383 if let Some(eq_idx) = find_redactable_key_split(inner, "=", keys) {
384 let key = &inner[..eq_idx];
385 return format!("MetadataEquals({key}=<redacted>)");
386 }
387 return label.to_string();
388 }
389 if let Some(inner) = strip_label(label, "MetadataMatches(", ")") {
391 let needle = " contains \"";
392 if let Some(at) = find_redactable_key_split(inner, needle, keys) {
393 if inner.ends_with('"') {
395 let key = &inner[..at];
396 return format!("MetadataMatches({key} contains \"<redacted>\")");
397 }
398 }
399 return label.to_string();
400 }
401 if let Some(inner) = strip_label(label, "MetadataNumericAtLeast(", ")") {
403 let needle = " >= ";
404 if let Some(at) = find_redactable_key_split(inner, needle, keys) {
405 let key = &inner[..at];
406 return format!("MetadataNumericAtLeast({key} >= <redacted>)");
407 }
408 return label.to_string();
409 }
410 label.to_string()
413}
414
415#[unsafe(no_mangle)]
447pub unsafe extern "C" fn net_predicate_redact_metadata_keys(
448 report_json: *const c_char,
449 keys_json: *const c_char,
450 out_redacted_json: *mut *mut c_char,
451 out_redacted_len: *mut usize,
452) -> c_int {
453 if report_json.is_null()
454 || keys_json.is_null()
455 || out_redacted_json.is_null()
456 || out_redacted_len.is_null()
457 {
458 return NetError::NullPointer.into();
459 }
460
461 let report_s = match unsafe { super::mesh::c_str_to_string(report_json) } {
462 Some(s) => s,
463 None => return NetError::InvalidUtf8.into(),
464 };
465 let keys_s = match unsafe { super::mesh::c_str_to_string(keys_json) } {
466 Some(s) => s,
467 None => return NetError::InvalidUtf8.into(),
468 };
469
470 let report: Value = match serde_json::from_str(&report_s) {
471 Ok(v) => v,
472 Err(_) => return NetError::InvalidJson.into(),
473 };
474 let keys_vec: Vec<String> = match serde_json::from_str(&keys_s) {
475 Ok(v) => v,
476 Err(_) => return NetError::InvalidJson.into(),
477 };
478 let keys: BTreeSet<String> = keys_vec.into_iter().collect();
479
480 let stats = match report.get("clause_stats").and_then(|s| s.as_array()) {
482 Some(s) => s,
483 None => return NetError::InvalidJson.into(),
484 };
485 let mut merged: BTreeMap<String, (u64, u64)> = BTreeMap::new();
486 for entry in stats {
487 let label = match entry.get("label").and_then(|l| l.as_str()) {
488 Some(l) => l.to_string(),
489 None => return NetError::InvalidJson.into(),
490 };
491 let evaluated = entry.get("evaluated").and_then(|n| n.as_u64()).unwrap_or(0);
492 let matched = entry.get("matched").and_then(|n| n.as_u64()).unwrap_or(0);
493 let new_label = redact_label(&label, &keys);
494 let slot = merged.entry(new_label).or_insert((0, 0));
495 slot.0 += evaluated;
496 slot.1 += matched;
497 }
498 let new_stats: Vec<Value> = merged
499 .into_iter()
500 .map(|(label, (evaluated, matched))| {
501 json!({
502 "label": label,
503 "evaluated": evaluated,
504 "matched": matched,
505 })
506 })
507 .collect();
508
509 let total = report
511 .get("total_candidates")
512 .and_then(|n| n.as_u64())
513 .unwrap_or(0);
514 let matched = report.get("matched").and_then(|n| n.as_u64()).unwrap_or(0);
515
516 let payload = json!({
517 "total_candidates": total,
518 "matched": matched,
519 "clause_stats": new_stats,
520 });
521 super::mesh::write_string_out(
522 serde_json::to_string(&payload).unwrap_or_else(|_| "{}".to_string()),
523 out_redacted_json,
524 out_redacted_len,
525 )
526}
527
528fn redact_trace_value(node: &Value, keys: &BTreeSet<String>) -> Value {
544 let label = node
545 .get("label")
546 .and_then(|l| l.as_str())
547 .unwrap_or_default();
548 let result = node.get("result").cloned().unwrap_or(Value::Null);
549 let children: Vec<Value> = node
550 .get("children")
551 .and_then(|c| c.as_array())
552 .map(|arr| arr.iter().map(|c| redact_trace_value(c, keys)).collect())
553 .unwrap_or_default();
554 json!({
555 "label": redact_label(label, keys),
556 "result": result,
557 "children": children,
558 })
559}
560
561#[unsafe(no_mangle)]
589pub unsafe extern "C" fn net_predicate_redact_trace_metadata_keys(
590 trace_json: *const c_char,
591 keys_json: *const c_char,
592 out_redacted_json: *mut *mut c_char,
593 out_redacted_len: *mut usize,
594) -> c_int {
595 if trace_json.is_null()
596 || keys_json.is_null()
597 || out_redacted_json.is_null()
598 || out_redacted_len.is_null()
599 {
600 return NetError::NullPointer.into();
601 }
602
603 let trace_s = match unsafe { super::mesh::c_str_to_string(trace_json) } {
604 Some(s) => s,
605 None => return NetError::InvalidUtf8.into(),
606 };
607 let keys_s = match unsafe { super::mesh::c_str_to_string(keys_json) } {
608 Some(s) => s,
609 None => return NetError::InvalidUtf8.into(),
610 };
611
612 let trace: Value = match serde_json::from_str(&trace_s) {
613 Ok(v) => v,
614 Err(_) => return NetError::InvalidJson.into(),
615 };
616 let keys_vec: Vec<String> = match serde_json::from_str(&keys_s) {
617 Ok(v) => v,
618 Err(_) => return NetError::InvalidJson.into(),
619 };
620 let keys: BTreeSet<String> = keys_vec.into_iter().collect();
621
622 let redacted = redact_trace_value(&trace, &keys);
623 super::mesh::write_string_out(
624 serde_json::to_string(&redacted).unwrap_or_else(|_| "{}".to_string()),
625 out_redacted_json,
626 out_redacted_len,
627 )
628}
629
630#[cfg(test)]
631mod tests {
632 use super::*;
633 use std::ffi::{CStr, CString};
634
635 fn read_and_free(ptr: *mut c_char) -> String {
637 assert!(!ptr.is_null());
638 let s = unsafe { CStr::from_ptr(ptr) }.to_str().unwrap().to_string();
639 unsafe {
640 let _ = CString::from_raw(ptr);
641 }
642 s
643 }
644
645 #[test]
649 fn evaluate_with_trace_records_full_tree() {
650 let pred = CString::new(
651 r#"{"nodes":[
652 {"kind":"exists","key":{"axis":"hardware","key":"gpu"}},
653 {"kind":"metadata_equals","key":"region","value":"us-east"},
654 {"kind":"and","children":[0,1]}
655 ],"root_idx":2}"#,
656 )
657 .unwrap();
658 let tags = CString::new(r#"["hardware.gpu"]"#).unwrap();
659 let meta = CString::new(r#"{"region":"us-east"}"#).unwrap();
660
661 let mut result: c_int = -1;
662 let mut out_ptr: *mut c_char = std::ptr::null_mut();
663 let mut out_len: usize = 0;
664 let rc = unsafe {
665 net_predicate_evaluate_with_trace(
666 pred.as_ptr(),
667 tags.as_ptr(),
668 meta.as_ptr(),
669 &mut result,
670 &mut out_ptr,
671 &mut out_len,
672 )
673 };
674 assert_eq!(rc, 0);
675 assert_eq!(result, 1);
676
677 let trace_json = read_and_free(out_ptr);
678 let v: Value = serde_json::from_str(&trace_json).unwrap();
679 assert!(v["label"].as_str().unwrap().starts_with("And"));
680 assert_eq!(v["result"], true);
681 let children = v["children"].as_array().unwrap();
682 assert_eq!(children.len(), 2);
683 assert!(children.iter().all(|c| c["result"] == true));
685 }
686
687 #[test]
691 fn aggregate_debug_report_rolls_up_per_clause_stats() {
692 let pred = CString::new(
693 r#"{"nodes":[
694 {"kind":"metadata_equals","key":"region","value":"us-east"}
695 ],"root_idx":0}"#,
696 )
697 .unwrap();
698 let contexts = CString::new(
699 r#"[
700 {"tags":[],"metadata":{"region":"us-east"}},
701 {"tags":[],"metadata":{"region":"us-west"}},
702 {"tags":[],"metadata":{"region":"us-east"}}
703 ]"#,
704 )
705 .unwrap();
706
707 let mut out_ptr: *mut c_char = std::ptr::null_mut();
708 let mut out_len: usize = 0;
709 let rc = unsafe {
710 net_predicate_aggregate_debug_report(
711 pred.as_ptr(),
712 contexts.as_ptr(),
713 &mut out_ptr,
714 &mut out_len,
715 )
716 };
717 assert_eq!(rc, 0);
718
719 let report_json = read_and_free(out_ptr);
720 let v: Value = serde_json::from_str(&report_json).unwrap();
721 assert_eq!(v["total_candidates"], 3);
722 assert_eq!(v["matched"], 2);
723 let stats = v["clause_stats"].as_array().unwrap();
724 assert_eq!(stats.len(), 1);
725 assert_eq!(stats[0]["evaluated"], 3);
726 assert_eq!(stats[0]["matched"], 2);
727 }
728
729 #[test]
733 fn redact_metadata_keys_rewrites_targeted_labels() {
734 let report = CString::new(
735 r#"{
736 "total_candidates": 10,
737 "matched": 4,
738 "clause_stats": [
739 {"label": "MetadataEquals(api_key=sk-secret-1)", "evaluated": 10, "matched": 4},
740 {"label": "MetadataEquals(region=us-east)", "evaluated": 10, "matched": 7},
741 {"label": "Exists(hardware.gpu)", "evaluated": 10, "matched": 8}
742 ]
743 }"#,
744 )
745 .unwrap();
746 let keys = CString::new(r#"["api_key"]"#).unwrap();
747
748 let mut out_ptr: *mut c_char = std::ptr::null_mut();
749 let mut out_len: usize = 0;
750 let rc = unsafe {
751 net_predicate_redact_metadata_keys(
752 report.as_ptr(),
753 keys.as_ptr(),
754 &mut out_ptr,
755 &mut out_len,
756 )
757 };
758 assert_eq!(rc, 0);
759
760 let redacted = read_and_free(out_ptr);
761 let v: Value = serde_json::from_str(&redacted).unwrap();
762 assert_eq!(v["total_candidates"], 10);
763 assert_eq!(v["matched"], 4);
764 let stats = v["clause_stats"].as_array().unwrap();
765 let labels: Vec<&str> = stats.iter().map(|s| s["label"].as_str().unwrap()).collect();
766 assert!(labels.contains(&"MetadataEquals(api_key=<redacted>)"));
767 assert!(labels.contains(&"MetadataEquals(region=us-east)"));
768 assert!(labels.contains(&"Exists(hardware.gpu)"));
769 }
770
771 #[test]
774 fn redact_metadata_keys_is_idempotent() {
775 let report = CString::new(
776 r#"{
777 "total_candidates": 5,
778 "matched": 2,
779 "clause_stats": [
780 {"label": "MetadataEquals(secret=foo)", "evaluated": 5, "matched": 2}
781 ]
782 }"#,
783 )
784 .unwrap();
785 let keys = CString::new(r#"["secret"]"#).unwrap();
786
787 let mut out1: *mut c_char = std::ptr::null_mut();
789 let mut len1: usize = 0;
790 unsafe {
791 net_predicate_redact_metadata_keys(report.as_ptr(), keys.as_ptr(), &mut out1, &mut len1)
792 };
793 let pass1 = read_and_free(out1);
794
795 let pass1_cs = CString::new(pass1.clone()).unwrap();
797 let mut out2: *mut c_char = std::ptr::null_mut();
798 let mut len2: usize = 0;
799 unsafe {
800 net_predicate_redact_metadata_keys(
801 pass1_cs.as_ptr(),
802 keys.as_ptr(),
803 &mut out2,
804 &mut len2,
805 )
806 };
807 let pass2 = read_and_free(out2);
808
809 assert_eq!(pass1, pass2, "redaction must be idempotent");
810 }
811
812 #[test]
819 fn redact_label_handles_keys_containing_separator() {
820 let mut keys = BTreeSet::new();
821 keys.insert("weird=key".to_string());
822
823 let label = "MetadataEquals(weird=key=sk-secret)";
826 let redacted = redact_label(label, &keys);
827 assert_eq!(redacted, "MetadataEquals(weird=key=<redacted>)");
828 assert!(
829 !redacted.contains("sk-secret"),
830 "secret leaked through label-parser heuristic: {redacted}"
831 );
832
833 let mut keys = BTreeSet::new();
835 keys.insert("a >= b".to_string());
836 let label = "MetadataNumericAtLeast(a >= b >= 42)";
837 let redacted = redact_label(label, &keys);
838 assert_eq!(redacted, "MetadataNumericAtLeast(a >= b >= <redacted>)");
839
840 let label = "MetadataEquals(region=us-east)";
842 let redacted = redact_label(label, &keys);
843 assert_eq!(redacted, label);
844 }
845
846 #[test]
853 fn redact_trace_metadata_keys_rewrites_recursively() {
854 let trace = CString::new(
858 r#"{
859 "label": "And(2)",
860 "result": true,
861 "children": [
862 {"label": "MetadataEquals(api_key=sk-secret-1)", "result": true, "children": []},
863 {"label": "Exists(hardware.gpu)", "result": true, "children": []}
864 ]
865 }"#,
866 )
867 .unwrap();
868 let keys = CString::new(r#"["api_key"]"#).unwrap();
869
870 let mut out_ptr: *mut c_char = std::ptr::null_mut();
871 let mut out_len: usize = 0;
872 let rc = unsafe {
873 net_predicate_redact_trace_metadata_keys(
874 trace.as_ptr(),
875 keys.as_ptr(),
876 &mut out_ptr,
877 &mut out_len,
878 )
879 };
880 assert_eq!(rc, 0);
881
882 let redacted = read_and_free(out_ptr);
883 let v: Value = serde_json::from_str(&redacted).unwrap();
884 assert_eq!(v["label"], "And(2)");
885 assert_eq!(v["result"], true);
886 let children = v["children"].as_array().unwrap();
887 assert_eq!(children.len(), 2);
888 assert_eq!(
889 children[0]["label"], "MetadataEquals(api_key=<redacted>)",
890 "targeted leaf must be redacted"
891 );
892 assert_eq!(
893 children[1]["label"], "Exists(hardware.gpu)",
894 "non-metadata leaf must pass through"
895 );
896 assert!(
898 !redacted.contains("sk-secret-1"),
899 "secret value still present in redacted trace: {redacted}"
900 );
901 }
902
903 #[test]
905 fn redact_trace_metadata_keys_is_idempotent() {
906 let trace = CString::new(
907 r#"{
908 "label": "MetadataEquals(secret=foo)",
909 "result": false,
910 "children": []
911 }"#,
912 )
913 .unwrap();
914 let keys = CString::new(r#"["secret"]"#).unwrap();
915
916 let mut out1: *mut c_char = std::ptr::null_mut();
917 let mut len1: usize = 0;
918 unsafe {
919 net_predicate_redact_trace_metadata_keys(
920 trace.as_ptr(),
921 keys.as_ptr(),
922 &mut out1,
923 &mut len1,
924 )
925 };
926 let pass1 = read_and_free(out1);
927 let pass1_cs = CString::new(pass1.clone()).unwrap();
928
929 let mut out2: *mut c_char = std::ptr::null_mut();
930 let mut len2: usize = 0;
931 unsafe {
932 net_predicate_redact_trace_metadata_keys(
933 pass1_cs.as_ptr(),
934 keys.as_ptr(),
935 &mut out2,
936 &mut len2,
937 )
938 };
939 let pass2 = read_and_free(out2);
940 assert_eq!(pass1, pass2);
941 }
942
943 #[test]
945 fn null_inputs_return_null_pointer_across_all_three() {
946 let pred = CString::new(r#"{"nodes":[],"root_idx":0}"#).unwrap();
947 let tags = CString::new(r#"[]"#).unwrap();
948 let meta = CString::new(r#"{}"#).unwrap();
949 let ctxs = CString::new(r#"[]"#).unwrap();
950 let report =
951 CString::new(r#"{"total_candidates":0,"matched":0,"clause_stats":[]}"#).unwrap();
952 let keys = CString::new(r#"[]"#).unwrap();
953
954 let mut result: c_int = 0;
955 let mut out_ptr: *mut c_char = std::ptr::null_mut();
956 let mut out_len: usize = 0;
957
958 assert!(
960 unsafe {
961 net_predicate_evaluate_with_trace(
962 std::ptr::null(),
963 tags.as_ptr(),
964 meta.as_ptr(),
965 &mut result,
966 &mut out_ptr,
967 &mut out_len,
968 )
969 } < 0
970 );
971
972 assert!(
974 unsafe {
975 net_predicate_aggregate_debug_report(
976 pred.as_ptr(),
977 std::ptr::null(),
978 &mut out_ptr,
979 &mut out_len,
980 )
981 } < 0
982 );
983
984 assert!(
986 unsafe {
987 net_predicate_redact_metadata_keys(
988 report.as_ptr(),
989 std::ptr::null(),
990 &mut out_ptr,
991 &mut out_len,
992 )
993 } < 0
994 );
995 assert!(
997 unsafe {
998 net_predicate_redact_metadata_keys(
999 std::ptr::null(),
1000 keys.as_ptr(),
1001 &mut out_ptr,
1002 &mut out_len,
1003 )
1004 } < 0
1005 );
1006 let _ = ctxs;
1008 }
1009}