kmp_application/memory/
ref_boundary.rs1pub fn validate_ref_token(path: &str, value: &str) -> Result<(), String> {
4 let unsafe_character = value
5 .chars()
6 .any(|ch| ch.is_control() || ch.is_whitespace() || matches!(ch, '/' | '\\'));
7 let unsafe_segment = value
8 .split(':')
9 .any(|segment| segment.is_empty() || matches!(segment, "." | ".."));
10 if unsafe_character || unsafe_segment {
11 return Err(format!(
12 "invalid `{path}` `{value}`; memory refs cannot contain whitespace, control characters, path separators, or empty/dot path segments"
13 ));
14 }
15 Ok(())
16}
17
18pub fn validate_supplied_entry_ref(about: &str, path: &str, entry_ref: &str) -> Result<(), String> {
22 validate_ref_token(path, entry_ref)?;
23 let owned_prefix = format!("{about}:");
24 if !entry_ref.starts_with(&owned_prefix) {
25 return Err(format!(
26 "`{path}` `{entry_ref}` does not belong to about `{about}`; it must start with `{owned_prefix}` and cannot replace the about anchor or a node from another about. Omit {path} to generate a safe ref for a new memory"
27 ));
28 }
29 Ok(())
30}
31
32pub fn validate_supplied_evidence_ref(
35 about: &str,
36 path: &str,
37 evidence_ref: &str,
38) -> Result<(), String> {
39 validate_ref_token(path, evidence_ref)?;
40 let owned_prefix = format!("evidence:{about}:");
41 if !evidence_ref.starts_with(&owned_prefix) {
42 return Err(format!(
43 "`{path}` `{evidence_ref}` does not belong to about `{about}`; evidence ids must start with `{owned_prefix}`"
44 ));
45 }
46 Ok(())
47}
48
49pub fn validate_supplied_member_ref(
52 about: &str,
53 path: &str,
54 member_ref: &str,
55) -> Result<(), String> {
56 validate_ref_token(path, member_ref)?;
57 let entry_prefix = format!("{about}:");
58 let evidence_prefix = format!("evidence:{about}:");
59 let dimension_prefix = format!("about:{about}:dimension:");
60 if member_ref != about
61 && !member_ref.starts_with(&entry_prefix)
62 && !member_ref.starts_with(&evidence_prefix)
63 && !member_ref.starts_with(&dimension_prefix)
64 {
65 return Err(format!(
66 "`{path}` `{member_ref}` does not belong to about `{about}`"
67 ));
68 }
69 Ok(())
70}
71
72#[cfg(test)]
73mod tests {
74 use super::{
75 validate_ref_token, validate_supplied_entry_ref, validate_supplied_evidence_ref,
76 validate_supplied_member_ref,
77 };
78
79 const ABOUT: &str = "incident:alfa";
80 const HOSTILE_REFS: &[&str] = &[
81 "incident:gamma:entry:observation:foreign",
82 "incident:beta",
83 "incident:alfa:entry:x\nincident:beta:entry:y",
84 "../../incident:beta:entry:x",
85 ];
86
87 #[test]
88 fn every_owned_ref_validator_rejects_the_shared_hostile_vectors() {
89 for hostile in HOSTILE_REFS {
90 assert!(
91 validate_supplied_entry_ref(ABOUT, "entry", hostile).is_err(),
92 "entry validator accepted {hostile:?}"
93 );
94 assert!(
95 validate_supplied_evidence_ref(ABOUT, "evidence", hostile).is_err(),
96 "evidence validator accepted {hostile:?}"
97 );
98 assert!(
99 validate_supplied_member_ref(ABOUT, "member", hostile).is_err(),
100 "member validator accepted {hostile:?}"
101 );
102 }
103 }
104
105 #[test]
106 fn owned_graph_namespaces_remain_valid() {
107 validate_ref_token("about", ABOUT).expect("safe about");
108 validate_supplied_entry_ref(ABOUT, "entry", "incident:alfa:entry:decision:one")
109 .expect("owned entry");
110 validate_supplied_evidence_ref(
111 ABOUT,
112 "evidence",
113 "evidence:incident:alfa:entry:decision:one:current",
114 )
115 .expect("owned evidence");
116 for member in [
117 ABOUT,
118 "incident:alfa:entry:decision:one",
119 "evidence:incident:alfa:entry:decision:one:current",
120 "about:incident:alfa:dimension:agentic_process:run",
121 ] {
122 validate_supplied_member_ref(ABOUT, "member", member).expect("owned member");
123 }
124 }
125}