strixonomy_swrl/
turtle.rs1use crate::model::SwrlRule;
10use crate::{Result, SwrlError};
11
12const ONTOCORE_SWRL_PRED: &str = "http://ontocode.dev/ns#swrlRule";
13
14pub fn parse_swrl_rule_json(text: &str) -> Result<SwrlRule> {
15 Ok(serde_json::from_str(text)?)
16}
17
18fn escape_turtle_string(value: &str) -> String {
19 let mut out = String::with_capacity(value.len());
20 for ch in value.chars() {
21 match ch {
22 '\\' => out.push_str("\\\\"),
23 '"' => out.push_str("\\\""),
24 '\n' => out.push_str("\\n"),
25 '\r' => out.push_str("\\r"),
26 '\t' => out.push_str("\\t"),
27 c => out.push(c),
28 }
29 }
30 out
31}
32
33fn unescape_turtle_string(value: &str) -> String {
34 let mut out = String::with_capacity(value.len());
35 let mut chars = value.chars().peekable();
36 while let Some(ch) = chars.next() {
37 if ch == '\\' {
38 match chars.next() {
39 Some('n') => out.push('\n'),
40 Some('r') => out.push('\r'),
41 Some('t') => out.push('\t'),
42 Some('"') => out.push('"'),
43 Some('\\') => out.push('\\'),
44 Some(other) => {
45 out.push('\\');
46 out.push(other);
47 }
48 None => out.push('\\'),
49 }
50 } else {
51 out.push(ch);
52 }
53 }
54 out
55}
56
57fn compact_json(rule: &SwrlRule) -> Result<String> {
58 Ok(serde_json::to_string(rule)?)
59}
60
61pub fn rule_to_turtle_fragment(ontology_iri: &str, rule: &SwrlRule) -> Result<String> {
63 let json = compact_json(rule)?;
64 let escaped = escape_turtle_string(&json);
65 Ok(format!(
66 "<{ontology_iri}> <{ONTOCORE_SWRL_PRED}> \"{escaped}\"^^<http://www.w3.org/2001/XMLSchema#string> .\n"
67 ))
68}
69
70pub fn rules_from_turtle_document(text: &str) -> Vec<SwrlRule> {
72 let mut rules = Vec::new();
73 let marker = ONTOCORE_SWRL_PRED;
74 for line in text.lines() {
75 if !line.contains(marker) {
76 continue;
77 }
78 if let Some(start) = line.find('"') {
80 let rest = &line[start + 1..];
81 let mut end = None;
82 let mut escaped = false;
83 for (i, ch) in rest.char_indices() {
84 if escaped {
85 escaped = false;
86 continue;
87 }
88 if ch == '\\' {
89 escaped = true;
90 continue;
91 }
92 if ch == '"' {
93 end = Some(i);
94 break;
95 }
96 }
97 if let Some(end) = end {
98 let raw = &rest[..end];
99 let unescaped = unescape_turtle_string(raw);
100 if let Ok(rule) = serde_json::from_str::<SwrlRule>(&unescaped) {
101 rules.push(rule);
102 }
103 }
104 }
105 }
106 rules
107}
108
109pub fn rewrite_swrl_iris_in_turtle(text: &str, from_iri: &str, to_iri: &str) -> (String, usize) {
113 if from_iri == to_iri || !text.contains(ONTOCORE_SWRL_PRED) {
114 return (text.to_string(), 0);
115 }
116 let mut result = text.to_string();
117 let mut remapped = 0usize;
118 let marker = ONTOCORE_SWRL_PRED;
119 let mut occ: Vec<(usize, usize, String)> = Vec::new();
121 let mut search_from = 0usize;
122 while let Some(rel) = result[search_from..].find(marker) {
123 let marker_pos = search_from + rel;
124 let after = &result[marker_pos..];
125 let Some(quote_rel) = after.find('"') else {
126 break;
127 };
128 let json_start = marker_pos + quote_rel + 1;
129 let rest = &result[json_start..];
130 let mut end = None;
131 let mut escaped = false;
132 for (i, ch) in rest.char_indices() {
133 if escaped {
134 escaped = false;
135 continue;
136 }
137 if ch == '\\' {
138 escaped = true;
139 continue;
140 }
141 if ch == '"' {
142 end = Some(i);
143 break;
144 }
145 }
146 let Some(json_len) = end else {
147 break;
148 };
149 let raw = &rest[..json_len];
150 let unescaped = unescape_turtle_string(raw);
151 if let Ok(mut rule) = serde_json::from_str::<SwrlRule>(&unescaped) {
152 if rule.remap_iri(from_iri, to_iri) {
153 if let Ok(new_json) = serde_json::to_string(&rule) {
154 occ.push((json_start, json_start + json_len, escape_turtle_string(&new_json)));
155 remapped += 1;
156 }
157 }
158 }
159 search_from = json_start + json_len + 1;
160 }
161 for (start, end, new_raw) in occ.into_iter().rev() {
162 result.replace_range(start..end, &new_raw);
163 }
164 (result, remapped)
165}
166
167#[allow(dead_code)]
168pub fn err_msg(msg: impl Into<String>) -> SwrlError {
169 SwrlError::Message(msg.into())
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use crate::model::{SwrlAtom, SwrlIArg, SwrlRule};
176
177 #[test]
178 fn round_trip_preserves_newlines_in_json() {
179 let rule = SwrlRule {
180 id: Some("r1".into()),
181 body: vec![SwrlAtom::Class {
182 class: "http://ex#A".into(),
183 arg: SwrlIArg::Variable("x".into()),
184 }],
185 head: vec![SwrlAtom::Class {
186 class: "http://ex#B".into(),
187 arg: SwrlIArg::Variable("x".into()),
188 }],
189 enabled: true,
190 };
191 let frag = rule_to_turtle_fragment("http://ex#ont", &rule).expect("frag");
192 assert!(!frag.contains('\n') || frag.ends_with('\n'));
193 let inner = frag.lines().next().unwrap();
195 assert!(!inner.trim_end_matches(" .").contains("\n"));
196 let parsed = rules_from_turtle_document(&frag);
197 assert_eq!(parsed.len(), 1);
198 assert_eq!(parsed[0].id.as_deref(), Some("r1"));
199 }
200
201 #[test]
202 fn injection_payload_with_quotes_round_trips() {
203 let json = r#"{"id":"q","body":[{"kind":"class","class":"http://ex#A","arg":{"variable":"x"}}],"head":[{"kind":"class","class":"http://ex#B","arg":{"variable":"x"}}],"enabled":true}"#;
204 let rule: SwrlRule = serde_json::from_str(json).unwrap();
205 let frag = rule_to_turtle_fragment("http://ex#ont", &rule).unwrap();
206 assert_eq!(rules_from_turtle_document(&frag).len(), 1);
207 }
208}