document_svg/document/
turtle.rs1use std::path::Path;
8
9use crate::convert::{ConvertOptions, PageConsumer, read_limited_file};
10use crate::document::html::{HtmlBlock, render_blocks_to_pages_with_warnings};
11use crate::error::{Error, Result};
12use crate::table::{TableAlign, TableData};
13
14const MAX_TURTLE_INPUT_BYTES: u64 = 64 * 1024 * 1024;
15const MAX_TURTLE_LINES: usize = 1_000_000;
16const MAX_TURTLE_LINE_BYTES: usize = 1024 * 1024;
17const MAX_TURTLE_TRIPLES: usize = 200_000;
18const MAX_TURTLE_CELL_CHARS: usize = 512;
19const MAX_TURTLE_TEXT_BYTES: usize = 64 * 1024 * 1024;
20type ParsedRdf = (Vec<String>, Vec<Vec<String>>, bool, Vec<String>);
21
22pub(crate) fn looks_like_prefix(bytes: &[u8]) -> bool {
23 let text = String::from_utf8_lossy(bytes);
24 text.lines().take(64).any(|line| {
25 let trimmed = line.trim_start();
26 trimmed.starts_with("@prefix")
27 || trimmed.starts_with("@base")
28 || trimmed.starts_with("PREFIX ")
29 || trimmed.starts_with("BASE ")
30 })
31}
32
33pub(crate) fn convert(
34 path: &Path,
35 options: &ConvertOptions,
36 sink: &mut dyn PageConsumer,
37) -> Result<Vec<String>> {
38 let bytes = read_limited_file(
39 path,
40 options.max_input_bytes.min(MAX_TURTLE_INPUT_BYTES),
41 "RDF input",
42 )?;
43 let text = std::str::from_utf8(&bytes)
44 .map_err(|error| Error::InvalidInput(format!("RDF input is not UTF-8: {error}")))?;
45 let (prefixes, mut rows, has_graph, mut warnings) = parse_statements(text)?;
46 if rows.is_empty() {
47 return Err(Error::InvalidInput(
48 "RDF input contains no renderable statements".into(),
49 ));
50 }
51 if !prefixes.is_empty() {
52 warnings.push(format!(
53 "RDF prefix declarations are displayed without namespace expansion ({})",
54 prefixes.len()
55 ));
56 }
57 let mut headers = vec!["Subject".into(), "Predicate".into(), "Object".into()];
58 if has_graph {
59 headers.push("Graph".into());
60 for row in &mut rows {
61 row.resize(4, String::new());
62 }
63 }
64 let table = TableData {
65 headers,
66 rows,
67 alignments: vec![TableAlign::Left; if has_graph { 4 } else { 3 }],
68 raw_source: String::new(),
69 };
70 let blocks = vec![
71 HtmlBlock::Heading {
72 level: 1,
73 text: "RDF statements".into(),
74 },
75 HtmlBlock::Table(table),
76 ];
77 render_blocks_to_pages_with_warnings(&blocks, sink, options, &warnings)?;
78 Ok(dedup_warnings(warnings))
79}
80
81fn parse_statements(text: &str) -> Result<ParsedRdf> {
82 let mut prefixes = Vec::new();
83 let mut rows = Vec::new();
84 let mut warnings = Vec::new();
85 let mut has_graph = false;
86 let mut subject = None::<String>;
87 let mut predicate = None::<String>;
88 let mut text_bytes = 0usize;
89 for (line_number, line) in text.lines().enumerate() {
90 if line_number >= MAX_TURTLE_LINES {
91 return Err(Error::LimitExceeded(format!(
92 "RDF input exceeds {MAX_TURTLE_LINES} lines"
93 )));
94 }
95 if line.len() > MAX_TURTLE_LINE_BYTES {
96 return Err(Error::LimitExceeded(format!(
97 "RDF line exceeds {MAX_TURTLE_LINE_BYTES} bytes"
98 )));
99 }
100 text_bytes = text_bytes.saturating_add(line.len());
101 if text_bytes > MAX_TURTLE_TEXT_BYTES {
102 return Err(Error::LimitExceeded(format!(
103 "RDF text exceeds {MAX_TURTLE_TEXT_BYTES} bytes"
104 )));
105 }
106 let trimmed = line.trim();
107 if trimmed.is_empty() || trimmed.starts_with('#') {
108 continue;
109 }
110 if trimmed.starts_with("@prefix")
111 || trimmed.starts_with("@base")
112 || trimmed.starts_with("PREFIX ")
113 || trimmed.starts_with("BASE ")
114 {
115 prefixes.push(truncate(trimmed));
116 continue;
117 }
118 let mut tokens = tokenize(trimmed);
119 if subject.is_none()
120 && !trimmed.contains(';')
121 && !trimmed.contains(',')
122 && tokens.len() >= 4
123 {
124 if tokens.last().is_some_and(|token| token == ".") {
125 tokens.pop();
126 }
127 if tokens.len() >= 4 {
128 if rows.len() >= MAX_TURTLE_TRIPLES {
129 return Err(Error::LimitExceeded(format!(
130 "RDF exceeds {MAX_TURTLE_TRIPLES} statements"
131 )));
132 }
133 has_graph = true;
134 rows.push(
135 tokens
136 .into_iter()
137 .take(4)
138 .map(|value| truncate(&value))
139 .collect(),
140 );
141 continue;
142 }
143 }
144 let mut delimiter = None::<char>;
145 for token in tokens {
146 let (term, token_delimiter) = split_delimiter(&token);
147 if term.is_empty() && token_delimiter.is_some() {
148 if token_delimiter == Some('.') {
149 subject = None;
150 predicate = None;
151 }
152 delimiter = token_delimiter;
153 continue;
154 }
155 if delimiter.is_none() && subject.is_none() {
156 subject = Some(term);
157 delimiter = token_delimiter;
158 continue;
159 }
160 if delimiter == Some('.') {
161 subject = Some(term);
162 delimiter = token_delimiter;
163 continue;
164 }
165 if predicate.is_none() {
166 predicate = Some(term);
167 delimiter = token_delimiter;
168 continue;
169 }
170 let Some(subject_value) = subject.clone() else {
171 continue;
172 };
173 let Some(predicate_value) = predicate.clone() else {
174 continue;
175 };
176 if rows.len() >= MAX_TURTLE_TRIPLES {
177 return Err(Error::LimitExceeded(format!(
178 "RDF exceeds {MAX_TURTLE_TRIPLES} statements"
179 )));
180 }
181 rows.push(vec![
182 truncate(&subject_value),
183 truncate(&predicate_value),
184 truncate(&term),
185 ]);
186 match token_delimiter.or(delimiter) {
187 Some('.') => {
188 subject = None;
189 predicate = None;
190 delimiter = None;
191 }
192 Some(';') => {
193 predicate = None;
194 delimiter = None;
195 }
196 Some(',') => {
197 delimiter = None;
198 }
199 _ => {
200 delimiter = None;
201 }
202 }
203 }
204 if let Some('.') = delimiter {
205 subject = None;
206 predicate = None;
207 }
208 if predicate.is_some() && subject.is_some() && !trimmed.ends_with('.') {
209 warnings.push(format!(
210 "RDF line {} continues a statement without a terminator",
211 line_number + 1
212 ));
213 }
214 }
215 if subject.is_some() || predicate.is_some() {
216 warnings.push("RDF input ended with an incomplete statement".into());
217 }
218 Ok((prefixes, rows, has_graph, warnings))
219}
220
221fn split_delimiter(token: &str) -> (String, Option<char>) {
222 let Some(last) = token.chars().last() else {
223 return (String::new(), None);
224 };
225 if matches!(last, '.' | ';' | ',') && !token.ends_with("...") {
226 let mut term = token.to_owned();
227 term.pop();
228 return (term, Some(last));
229 }
230 (token.to_owned(), None)
231}
232
233fn tokenize(line: &str) -> Vec<String> {
234 let mut tokens = Vec::new();
235 let mut current = String::new();
236 let mut quote = None::<char>;
237 let mut angle = false;
238 for character in line.chars() {
239 if let Some(delimiter) = quote {
240 current.push(character);
241 if character == delimiter {
242 quote = None;
243 }
244 } else if angle {
245 current.push(character);
246 if character == '>' {
247 angle = false;
248 }
249 } else if character == '"' || character == '\'' {
250 quote = Some(character);
251 current.push(character);
252 } else if character == '<' {
253 angle = true;
254 current.push(character);
255 } else if character.is_whitespace() {
256 if !current.is_empty() {
257 tokens.push(std::mem::take(&mut current));
258 }
259 } else {
260 current.push(character);
261 }
262 }
263 if !current.is_empty() {
264 tokens.push(current);
265 }
266 tokens
267}
268
269fn truncate(value: &str) -> String {
270 value.chars().take(MAX_TURTLE_CELL_CHARS).collect()
271}
272
273fn dedup_warnings(mut warnings: Vec<String>) -> Vec<String> {
274 warnings.sort();
275 warnings.dedup();
276 warnings
277}