1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
//! Errors of the byte-oriented parser core.
//!
//! The spec fixes only whether a document is rejected (`docs/spec/11-errors.md` §11.1). The kinds
//! and messages here are the project's own; every error carries the position where it was
//! detected, and the file when that is an included file.
use crate::error::Position;
use crate::value::UclValue;
use std::fmt;
use std::path::{Path, PathBuf};
/// A parse error: what went wrong and where.
///
/// A silent stop (spec §9.4, *Missing and unusable files*) is reported as an error of kind
/// [`ErrorKind::Stopped`]; it carries the entries parsed before the stop ([`Error::partial`]).
#[derive(Debug, Clone, PartialEq)]
pub struct Error {
kind: ErrorKind,
position: Position,
file: Option<PathBuf>,
partial: Option<Box<UclValue>>,
}
impl Error {
/// An error of `kind` at `position`.
pub fn new(kind: ErrorKind, position: Position) -> Self {
Self {
kind,
position,
file: None,
partial: None,
}
}
/// What went wrong.
pub fn kind(&self) -> &ErrorKind {
&self.kind
}
/// Where the error was detected: 1-based line and column (in characters), 0-based byte offset,
/// in the input named by [`Error::file`].
pub fn position(&self) -> Position {
self.position
}
/// The canonical path of the included file (spec §9.4) the error was detected in, or `None`
/// when it was detected in the document the parser was given.
pub fn file(&self) -> Option<&Path> {
self.file.as_deref()
}
/// True for input the parser recognises but does not support: the macro `.includes`, the
/// parameter `sign=true` of the include macros (project decision: signatures are never
/// verified), and `.load` when the crate is built without its `load` feature. Such an error
/// is not a rejection of the document by the format rules.
pub fn is_unsupported(&self) -> bool {
matches!(self.kind, ErrorKind::Unsupported { .. })
}
/// True for a silent stop: [`ErrorKind::Stopped`], or [`ErrorKind::MacroStopped`] for a
/// registered macro whose handler stopped the parse (spec §13.2).
pub fn is_stopped(&self) -> bool {
matches!(
self.kind,
ErrorKind::Stopped { .. } | ErrorKind::MacroStopped { .. }
)
}
/// For a silent stop, the root value as parsed up to the macro that stopped: what libucl
/// returns as the result in that situation (spec §9.4). For a stop in one of several inputs
/// ([`crate::parse::Inputs`]), the root as parsed so far, with the inputs before.
pub fn partial(&self) -> Option<&UclValue> {
self.partial.as_deref()
}
/// [`Error::partial`], by value.
pub fn into_partial(self) -> Option<UclValue> {
self.partial.map(|v| *v)
}
/// The error with `file` recorded, unless it already names one.
pub(crate) fn in_file(mut self, file: &Path) -> Self {
if self.file.is_none() {
self.file = Some(file.to_path_buf());
}
self
}
/// The error with the partial result of a silent stop.
pub(crate) fn with_partial(mut self, root: UclValue) -> Self {
self.partial = Some(Box::new(root));
self
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} (line {}, column {}",
self.kind, self.position.line, self.position.column
)?;
match &self.file {
Some(file) => write!(f, " of {})", file.display()),
None => f.write_str(")"),
}
}
}
impl std::error::Error for Error {}
/// The kinds of parse error.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ErrorKind {
/// An object's `{` has no matching `}`.
UnterminatedObject,
/// An array's `[` has no matching `]`.
UnterminatedArray,
/// A quoted string has no closing quote.
UnterminatedString,
/// A heredoc has no terminator line.
UnterminatedHeredoc,
/// A block comment has no matching `*/`.
UnterminatedComment,
/// A `#` that is the last byte of the input, directly after whitespace where the first key
/// of an unbraced root could start (spec §2.2, *Quirk*), or after a macro (spec §9.2).
HashAtEnd,
/// A `}` or `]` that closes nothing, or the wrong kind of container.
UnmatchedClose { found: char },
/// A key could not start or continue here.
InvalidKey { found: Option<char> },
/// `""` as a key.
EmptyKey,
/// A key written in single quotes.
SingleQuotedKey,
/// Two separators between a key and its value, such as `a == b`.
DoubleSeparator,
/// A key without a value, or a value that is empty.
MissingValue,
/// A `,` or `;` where an entry or an array element must start.
UnexpectedTerminator,
/// Something other than whitespace, a comment, a terminator or a closing bracket directly
/// after a quoted string or heredoc.
MissingDelimiter { found: char },
/// A raw control character inside a double-quoted string.
ControlCharacter { byte: u8 },
/// A `\u` escape without four hex digits in a double-quoted string.
InvalidUnicodeEscape,
/// An integer outside the 64-bit signed range, or a float that overflows or underflows.
NumberOutOfRange,
/// A key or string that is not valid UTF-8 (a project divergence from libucl, spec §11.3).
InvalidUtf8,
/// A repeated key that the duplicate strategy does not accept (spec §8).
DuplicateKey { key: String },
/// More containers nested inside one another than a limit allows: open at once
/// ([`crate::parse::MAX_NESTING`], spec §11.2), in the copies `.inherit` adds (the parser's
/// [`inherit_depth_limit`](crate::parse::Parser::inherit_depth_limit), §9.7), or in the
/// entries a registered macro adds ([`crate::parse::MAX_NESTING`], §13.2).
NestingTooDeep { limit: usize },
/// A macro name that is not known.
UnknownMacro { name: String },
/// A macro while macros are disabled (`ParserFlags::DISABLE_MACRO`).
MacrosDisabled,
/// A macro's `(` has no matching `)` (spec §9.2).
UnterminatedArguments,
/// A macro value written in braces has no `}` (spec §9.2).
UnterminatedMacroValue,
/// More macro argument documents open inside one another than the limit allows
/// ([`crate::parse::MAX_ARGUMENT_DEPTH`]).
ArgumentsTooDeep { limit: usize },
/// The value of `.priority` is not a decimal integer, or it is empty and there is no
/// integer `priority` parameter (spec §9.5).
InvalidPriority,
/// `.inherit` names no key of the root object, or the root is an array (spec §9.7).
InheritSourceMissing { name: String },
/// `.inherit` names a root key whose first value is not an object (spec §9.7).
InheritSourceNotObject { name: String },
/// A file named by `.include`, `.try_include` or `.load` does not exist, or, with a search
/// path, exists in none of its directories (spec §9.4, §9.6).
FileNotFound { path: String },
/// A file named by `.include`, `.try_include` or `.load` is a directory or another kind of
/// file that is not a regular file, or cannot be read.
NotAFile { path: String },
/// An include macro names the file that holds it (spec §9.4).
IncludeSelf { path: String },
/// More input units open at once than [`crate::parse::MAX_INCLUDE_DEPTH`] allows: the
/// inputs given to the parser, the files they include, and text registered macros parse in
/// place (spec §9.4, §13).
IncludeTooDeep { limit: usize },
/// An included file, or text a registered macro parses in place, starts with `[` where a
/// bracketed root would start (spec §9.4, §13.2).
IncludeArrayRoot,
/// Input after a macro whose included file closed the braced root of the document: only
/// whitespace and `;` may follow there, up to the end of the unit (oracle runs,
/// QUESTIONS.md #47).
AfterRootClosedByInclude,
/// Nesting an included file under key `key` (`key`, `prefix`), whose first value is not an
/// object, without `target="array"` (spec §9.4).
IncludeTargetNotObject { key: String },
/// `url=true` with `://` in the path: the crate never fetches URLs (project decision).
UrlNotSupported { path: String },
/// `.load` without a `key` parameter, or with an empty one (spec §9.6).
LoadKeyMissing,
/// `.load` of a key the current object already has (spec §9.6).
LoadKeyExists { key: String },
/// A silent stop (spec §9.4, *Missing and unusable files*): `.try_include` found no usable
/// file, or `.include` a glob pattern that matches nothing. libucl ends the parse there
/// without an error message and keeps what it parsed; [`Error::partial`] holds that.
Stopped { path: String },
/// A silent stop inside a macro argument list (spec §9.2), which makes the macro fail.
StoppedInArguments { path: String },
/// Input the parser recognises but does not support.
Unsupported { feature: String },
/// The input file could not be read.
Io { message: String },
/// The document, or the document and the files that its macros read (`.include`,
/// `.try_include`, `.load`), hold more bytes than the parser's input limit
/// ([`crate::parse::Parser::set_max_input_bytes`]). `path` is the file that went over it,
/// or `None` for the document itself.
InputTooLarge { limit: u64, path: Option<String> },
/// More inputs given to one parser than it takes (spec §13.1, *How many inputs*): every
/// input counts as an open input unit for the rest of the parse, and so does an included
/// file that stopped silently (oracle runs, QUESTIONS.md #59); at most `limit`
/// ([`crate::parse::MAX_INCLUDE_DEPTH`]) may be open.
TooManyInputs { limit: usize },
/// A later input with content after the root is complete (spec §13.1, *The root*): an
/// earlier input closed a braced root or held an array root, or the first input had no
/// bytes at all.
AfterRoot,
/// An input's first entry directly after a value that ended the input before it, with no
/// line break, `;`, `,` or comment between them: the end of an input is not a separator
/// (spec §13.1, *Quirk*).
UnseparatedInput { found: char },
/// A registered macro's handler failed with this message (spec §13.2; the message is a
/// project addition, WORKLIST C8b decision 3).
MacroFailed { name: String, message: String },
/// A silent stop by a registered macro's handler (spec §13.2, *Fail*): libucl ends the
/// input at the macro without an error message and keeps what it parsed;
/// [`Error::partial`] holds that.
MacroStopped { name: String },
}
impl fmt::Display for ErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ErrorKind::UnterminatedObject => f.write_str("object is not closed with '}'"),
ErrorKind::UnterminatedArray => f.write_str("array is not closed with ']'"),
ErrorKind::UnterminatedString => f.write_str("string has no closing quote"),
ErrorKind::UnterminatedHeredoc => f.write_str("heredoc has no terminator line"),
ErrorKind::UnterminatedComment => f.write_str("block comment is not closed"),
ErrorKind::HashAtEnd => {
f.write_str("a '#' after whitespace cannot be the last byte of the input here")
}
ErrorKind::UnmatchedClose { found } => {
write!(f, "'{found}' does not close an open container")
}
ErrorKind::InvalidKey { found: Some(c) } => {
write!(f, "{} is not allowed in or after a key", describe(*c))
}
ErrorKind::InvalidKey { found: None } => f.write_str("input ends inside a key"),
ErrorKind::EmptyKey => f.write_str("a key cannot be empty"),
ErrorKind::SingleQuotedKey => f.write_str("a key cannot be single-quoted"),
ErrorKind::DoubleSeparator => f.write_str("more than one separator after a key"),
ErrorKind::MissingValue => f.write_str("key has no value"),
ErrorKind::UnexpectedTerminator => f.write_str("separator before any entry"),
ErrorKind::MissingDelimiter { found } => write!(
f,
"{} directly after a quoted value; expected a separator",
describe(*found)
),
ErrorKind::ControlCharacter { byte } => {
write!(f, "control character 0x{byte:02x} in a quoted string")
}
ErrorKind::InvalidUnicodeEscape => {
f.write_str("'\\u' must be followed by four hex digits")
}
ErrorKind::NumberOutOfRange => f.write_str("number is out of range"),
ErrorKind::InvalidUtf8 => f.write_str("text is not valid UTF-8"),
ErrorKind::DuplicateKey { key } => {
write!(f, "key '{key}' cannot take another value")
}
ErrorKind::NestingTooDeep { limit } => {
write!(f, "more than {limit} containers nested inside one another")
}
ErrorKind::UnknownMacro { name } if name.is_empty() => {
f.write_str("'.' must be followed by a macro name")
}
ErrorKind::UnknownMacro { name } => write!(f, "unknown macro '.{name}'"),
ErrorKind::MacrosDisabled => f.write_str("macros are disabled"),
ErrorKind::UnterminatedArguments => {
f.write_str("macro arguments are not closed with ')'")
}
ErrorKind::UnterminatedMacroValue => {
f.write_str("macro value in braces is not closed with '}'")
}
ErrorKind::ArgumentsTooDeep { limit } => {
write!(
f,
"more than {limit} macro argument lists inside one another"
)
}
ErrorKind::InvalidPriority => f.write_str(
".priority needs a decimal integer, as its value or its priority parameter",
),
ErrorKind::InheritSourceMissing { name } => {
write!(f, ".inherit: the root object has no key '{name}'")
}
ErrorKind::InheritSourceNotObject { name } => {
write!(f, ".inherit: the root key '{name}' is not an object")
}
ErrorKind::FileNotFound { path } => write!(f, "file '{path}' does not exist"),
ErrorKind::NotAFile { path } => {
write!(f, "'{path}' is not a regular file that can be read")
}
ErrorKind::IncludeSelf { path } => write!(f, "file '{path}' includes itself"),
ErrorKind::IncludeTooDeep { limit } => write!(
f,
"more than {limit} input units (inputs, included files, macro text) are open \
inside one another"
),
ErrorKind::IncludeArrayRoot => {
f.write_str("an included file or macro text cannot start with '['")
}
ErrorKind::AfterRootClosedByInclude => {
f.write_str("an included file closed the root object; nothing may follow the macro")
}
ErrorKind::IncludeTargetNotObject { key } => write!(
f,
"cannot include into key '{key}': its value is not an object"
),
ErrorKind::UrlNotSupported { path } => {
write!(f, "'{path}' is a URL, and URLs are never fetched")
}
ErrorKind::LoadKeyMissing => f.write_str(".load needs a non-empty key parameter"),
ErrorKind::LoadKeyExists { key } => {
write!(f, ".load: the object already has the key '{key}'")
}
ErrorKind::Stopped { path } => write!(
f,
"parsing stopped at the include of '{path}', which found no usable file; \
the entries before it are kept"
),
ErrorKind::StoppedInArguments { path } => write!(
f,
"macro arguments stopped at the include of '{path}', which found no usable file"
),
ErrorKind::Unsupported { feature } => write!(f, "{feature} is not supported"),
ErrorKind::Io { message } => write!(f, "cannot read input: {message}"),
ErrorKind::InputTooLarge { limit, path: None } => {
write!(
f,
"the document holds more than the input limit of {limit} bytes"
)
}
ErrorKind::InputTooLarge {
limit,
path: Some(path),
} => write!(
f,
"reading '{path}' goes over the input limit of {limit} bytes for the document \
and the files it reads"
),
ErrorKind::TooManyInputs { limit } => write!(
f,
"a parser takes at most {limit} inputs, fewer after included files that stopped"
),
ErrorKind::AfterRoot => {
f.write_str("the root is complete: a later input can add nothing to it")
}
ErrorKind::UnseparatedInput { found } => write!(
f,
"{} directly after the value that ended the input before; a line break, ';', \
',' or a comment must come first",
describe(*found)
),
ErrorKind::MacroFailed { name, message } => {
write!(f, "macro '.{name}' failed: {message}")
}
ErrorKind::MacroStopped { name } => write!(
f,
"parsing stopped at the macro '.{name}', whose handler failed; the entries \
before it are kept"
),
}
}
}
fn describe(c: char) -> String {
match c {
'\n' => "a line break".to_string(),
'\r' => "a carriage return".to_string(),
c if c.is_control() => format!("control character U+{:04X}", c as u32),
c => format!("'{c}'"),
}
}
/// The position of byte `offset` in `src`. Columns count UTF-8 characters.
pub(crate) fn position_at(src: &[u8], offset: usize) -> Position {
let offset = offset.min(src.len());
let before = &src[..offset];
let line_start = before
.iter()
.rposition(|&b| b == b'\n')
.map_or(0, |i| i + 1);
let line = 1 + before.iter().filter(|&&b| b == b'\n').count();
let column = 1 + before[line_start..]
.iter()
.filter(|&&b| (b & 0xC0) != 0x80)
.count();
Position {
line,
column,
offset,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn position_counts_lines_and_characters() {
let src = "a = 1\nbé = x".as_bytes();
assert_eq!(
position_at(src, 0),
Position {
line: 1,
column: 1,
offset: 0
}
);
let x = src.iter().position(|&b| b == b'x').unwrap();
let p = position_at(src, x);
assert_eq!((p.line, p.column, p.offset), (2, 6, x));
}
}