1mod csv;
11mod json;
12mod schema;
13mod text;
14
15pub use schema::suggest;
16
17use crate::error::{ErrorKind, RuntimeError};
18use crate::report::thousands;
19use crate::value::Value;
20use probl_sema::ir::{DataFormat, Input, Program, TypeSpec};
21use rustc_hash::FxHashMap;
22use sha2::{Digest, Sha256};
23use std::io::{ErrorKind as IoErrorKind, Read};
24use std::sync::Arc;
25use std::sync::atomic::{AtomicBool, Ordering};
26
27pub trait Resolver {
31 fn resolve(&mut self, path: &str) -> Result<String, String>;
35 fn open(&mut self, identity: &str) -> Result<Box<dyn Read + Send>, String>;
37}
38
39#[derive(Clone, Debug)]
41pub struct InputLimits {
42 pub max_integer_bits: u64,
44 pub max_integer_bytes: u64,
46 pub max_bytes: u64,
48 pub max_values: u64,
50 pub max_collection: usize,
52 pub max_depth: usize,
54}
55
56impl Default for InputLimits {
57 fn default() -> InputLimits {
58 InputLimits {
59 max_integer_bits: probl_number::MAX_INTEGER_BITS,
60 max_integer_bytes: 256 * 1024 * 1024,
61 max_bytes: 64 * 1024 * 1024,
62 max_values: 10_000_000,
63 max_collection: 10_000_000,
64 max_depth: 64,
65 }
66 }
67}
68
69#[derive(Debug, Default)]
74pub struct Snapshots {
75 bytes: FxHashMap<String, Arc<[u8]>>,
76}
77
78impl Snapshots {
79 pub fn clear(&mut self) {
81 self.bytes.clear();
82 }
83}
84
85#[derive(Debug)]
89pub struct Inputs {
90 max_integer_bits: u64,
91 max_string_bytes: usize,
92 manifest: Vec<Input>,
93 values: Vec<Value>,
94 sources: Vec<SourceInfo>,
95}
96
97impl Inputs {
98 pub(crate) fn max_string_bytes(&self) -> usize {
99 self.max_string_bytes
100 }
101 pub(crate) fn max_integer_bits(&self) -> u64 {
102 self.max_integer_bits
103 }
104
105 pub fn values(&self) -> &[Value] {
107 &self.values
108 }
109
110 pub fn sources(&self) -> &[SourceInfo] {
112 &self.sources
113 }
114
115 pub fn fit(&self, program: &Program) -> bool {
117 self.manifest == program.inputs
118 }
119}
120
121#[derive(Clone, Debug, PartialEq)]
123pub struct SourceInfo {
124 pub identity: String,
125 pub bytes: u64,
126 pub sha256: String,
129}
130
131pub fn load(
134 program: &Program,
135 resolver: &mut dyn Resolver,
136 snapshots: &mut Snapshots,
137 limits: &InputLimits,
138 cancel: Option<&AtomicBool>,
139) -> Result<Inputs, RuntimeError> {
140 let mut cx = Cx {
141 program,
142 budget: Budget {
143 values_left: limits.max_values,
144 integer_bytes_left: limits.max_integer_bytes,
145 max_integer_bits_seen: 0,
146 max_string_bytes_seen: 0,
147 limits,
148 cancel,
149 ticks: 0,
150 },
151 names: FxHashMap::default(),
152 depth: 0,
153 path: Vec::new(),
154 problem: None,
155 };
156 let mut read: Vec<(String, Arc<[u8]>)> = Vec::new();
158 let mut total: u64 = 0;
159 let mut values = Vec::with_capacity(program.inputs.len());
160 for input in &program.inputs {
161 let identity = resolver
162 .resolve(&input.path)
163 .map_err(|why| failure(input, Problem::new(format!("can't read it: {why}"))))?;
164 let bytes = match read.iter().find(|(id, _)| *id == identity) {
165 Some((_, bytes)) => bytes.clone(),
166 None => {
167 let bytes = match snapshots.bytes.get(&identity) {
168 Some(bytes) => bytes.clone(),
169 None => {
170 let reader = resolver
171 .open(&identity)
172 .map_err(|why| failure(input, Problem::new(format!("can't read it: {why}"))))?;
173 let left = limits.max_bytes.saturating_sub(total);
174 let bytes: Arc<[u8]> = read_bounded(reader, left, limits.max_bytes, cancel)
175 .map_err(|p| failure(input, p))?
176 .into();
177 snapshots.bytes.insert(identity.clone(), bytes.clone());
178 bytes
179 }
180 };
181 total += bytes.len() as u64;
182 if total > limits.max_bytes {
183 return Err(failure(input, too_many_bytes(limits.max_bytes)));
184 }
185 read.push((identity, bytes.clone()));
186 bytes
187 }
188 };
189 let value = decode(&bytes, input, &mut cx).map_err(|p| failure(input, p))?;
190 values.push(value);
191 }
192 let sources = read
193 .iter()
194 .map(|(identity, bytes)| SourceInfo {
195 identity: identity.clone(),
196 bytes: bytes.len() as u64,
197 sha256: Sha256::digest(bytes).iter().map(|b| format!("{b:02x}")).collect(),
198 })
199 .collect();
200 Ok(Inputs {
201 max_integer_bits: cx.budget.max_integer_bits_seen,
202 max_string_bytes: cx.budget.max_string_bytes_seen,
203 manifest: program.inputs.clone(),
204 values,
205 sources,
206 })
207}
208
209fn read_bounded(
212 mut reader: Box<dyn Read + Send>,
213 left: u64,
214 max: u64,
215 cancel: Option<&AtomicBool>,
216) -> Result<Vec<u8>, Problem> {
217 let mut bytes = Vec::new();
218 let mut chunk = vec![0u8; 64 * 1024];
219 loop {
220 if cancel.is_some_and(|c| c.load(Ordering::Relaxed)) {
221 return Err(Problem::limit("reading it was cancelled"));
222 }
223 let n = match reader.read(&mut chunk) {
224 Ok(0) => return Ok(bytes),
225 Ok(n) => n,
226 Err(e) if e.kind() == IoErrorKind::Interrupted => continue,
227 Err(_) if cancel.is_some_and(|c| c.load(Ordering::Relaxed)) => {
228 return Err(Problem::limit("reading it was cancelled"));
229 }
230 Err(e) => return Err(Problem::new(format!("reading it failed: {e}"))),
231 };
232 if (bytes.len() + n) as u64 > left {
233 return Err(too_many_bytes(max));
234 }
235 bytes.extend_from_slice(&chunk[..n]);
236 }
237}
238
239fn too_many_bytes(max: u64) -> Problem {
240 Problem::limit(format!(
241 "the data is more than {} MiB, the limit for all the data together",
242 max.div_ceil(1024 * 1024)
243 ))
244 .help("raise the limit with `--max-input`")
245}
246
247fn decode(bytes: &[u8], input: &Input, cx: &mut Cx) -> Result<Value, Problem> {
249 let bytes = bytes.strip_prefix(b"\xEF\xBB\xBF").unwrap_or(bytes);
250 cx.depth = 0;
251 cx.path.clear();
252 cx.problem = None;
253 match input.format {
254 DataFormat::Csv => csv::read(bytes, &input.ty, cx),
255 DataFormat::Json => json::read(bytes, &input.ty, cx),
256 DataFormat::Lines => lines(bytes, &input.ty, cx),
257 }
258}
259
260fn lines(bytes: &[u8], ty: &TypeSpec, cx: &mut Cx) -> Result<Value, Problem> {
262 let TypeSpec::List(item) = ty else {
263 return Err(Problem::new("lines read as a list"));
264 };
265 let text = std::str::from_utf8(bytes).map_err(|e| {
266 let line = bytes[..e.valid_up_to()].iter().filter(|&&b| b == b'\n').count() + 1;
267 Problem::new("isn't text: it isn't valid UTF-8").at(format!("line {line}"))
268 })?;
269 let mut items = Vec::new();
270 for (i, line) in text.lines().enumerate() {
271 if line.trim().is_empty() {
272 continue;
273 }
274 cx.budget.collection(items.len() + 1)?;
275 cx.budget.value()?;
276 let v = text::plain(line, item, cx.program, &mut cx.budget).map_err(|p| p.at(format!("line {}", i + 1)))?;
277 items.push(v);
278 }
279 Ok(Value::list(items))
280}
281
282pub(crate) struct Cx<'a> {
284 program: &'a Program,
285 budget: Budget<'a>,
286 names: FxHashMap<String, Arc<str>>,
288 depth: usize,
290 path: Vec<PathPart>,
292 problem: Option<Problem>,
295}
296
297impl Cx<'_> {
298 fn name(&mut self, name: &str) -> Arc<str> {
299 if let Some(n) = self.names.get(name) {
300 return n.clone();
301 }
302 let n: Arc<str> = Arc::from(name);
303 self.names.insert(name.to_string(), n.clone());
304 n
305 }
306}
307
308enum PathPart {
309 Key(String),
310 Index(usize),
311}
312
313struct Budget<'a> {
315 values_left: u64,
316 integer_bytes_left: u64,
317 max_integer_bits_seen: u64,
318 max_string_bytes_seen: usize,
319 limits: &'a InputLimits,
320 cancel: Option<&'a AtomicBool>,
321 ticks: u32,
322}
323
324impl Budget<'_> {
325 fn integer(&mut self, bits: u64) -> Result<(), Problem> {
326 let limit = self.limits.max_integer_bits.min(probl_number::MAX_INTEGER_BITS);
327 if bits > limit {
328 return Err(Problem::limit(format!(
329 "integer size exceeds the limit of {limit} bits"
330 )));
331 }
332 self.max_integer_bits_seen = self.max_integer_bits_seen.max(bits);
333 if bits > 63 {
334 let bytes = bits.div_ceil(64) * 8 + 48;
335 self.integer_bytes_left = self
336 .integer_bytes_left
337 .checked_sub(bytes)
338 .ok_or_else(|| Problem::limit("the data used up its large integer memory allowance"))?;
339 }
340 Ok(())
341 }
342
343 fn value(&mut self) -> Result<(), Problem> {
345 if self.values_left == 0 {
346 return Err(Problem::limit(format!(
347 "the data has more than {} values, the limit for all the data together",
348 thousands(self.limits.max_values.min(i64::MAX as u64) as i64)
349 )));
350 }
351 self.values_left -= 1;
352 self.ticks = self.ticks.wrapping_add(1);
353 if self.ticks % 4096 == 0 && self.cancel.is_some_and(|c| c.load(Ordering::Relaxed)) {
354 return Err(Problem::limit("reading it was cancelled"));
355 }
356 Ok(())
357 }
358
359 fn collection(&self, n: usize) -> Result<(), Problem> {
361 if n > self.limits.max_collection {
362 return Err(Problem::limit(format!(
363 "a list, map or bag has more than {} elements",
364 thousands(self.limits.max_collection as i64)
365 )));
366 }
367 Ok(())
368 }
369}
370
371#[derive(Clone, Debug)]
373pub(crate) struct Problem {
374 message: String,
375 help: Option<String>,
376 notes: Vec<String>,
377 at: Option<String>,
379 limit: bool,
380}
381
382impl Problem {
383 fn new(message: impl Into<String>) -> Problem {
384 Problem {
385 message: message.into(),
386 help: None,
387 notes: Vec::new(),
388 at: None,
389 limit: false,
390 }
391 }
392
393 fn limit(message: impl Into<String>) -> Problem {
394 Problem {
395 limit: true,
396 ..Problem::new(message)
397 }
398 }
399
400 fn help(mut self, help: impl Into<String>) -> Problem {
401 self.help = Some(help.into());
402 self
403 }
404
405 fn note(mut self, note: impl Into<String>) -> Problem {
406 self.notes.push(note.into());
407 self
408 }
409
410 fn at(mut self, at: impl Into<String>) -> Problem {
412 if self.at.is_none() {
413 self.at = Some(at.into());
414 }
415 self
416 }
417}
418
419fn failure(input: &Input, problem: Problem) -> RuntimeError {
421 let place = match &problem.at {
422 Some(at) => format!("{}, {at}", source_name(input)),
423 None => source_name(input),
424 };
425 let mut error = RuntimeError::new(input.span, format!("{place}: {}", problem.message));
426 error.notes = problem.notes;
427 error.help = problem.help;
428 if problem.limit {
429 error.kind = ErrorKind::Limit;
430 }
431 error
432}
433
434fn source_name(input: &Input) -> String {
435 match input.path.as_str() {
436 "-" => "standard input".to_string(),
437 path => path.to_string(),
438 }
439}
440
441fn quoted(text: &str) -> String {
443 const MAX: usize = 40;
444 match text.char_indices().nth(MAX) {
445 Some((cut, _)) => format!("`{}…`", &text[..cut]),
446 None => format!("`{text}`"),
447 }
448}
449
450#[cfg(test)]
451mod tests;