chrony_confile/error.rs
1//! Error types for parsing, validation, and value construction.
2//!
3//! This module defines the three error types used throughout the crate:
4//! - [`ParseError`] -- I/O errors, parse errors, and include-level exceeded errors
5//! - [`DirectiveError`] -- Errors specific to a single directive (invalid option, value, etc.)
6//! - [`ValueError`] -- Errors from constructing out-of-range or invalid values
7//!
8//! Both [`ParseError`] and [`DirectiveError`] implement [`std::error::Error`] (via `thiserror`),
9//! making them compatible with `?` and any error-reporting crate.
10
11use std::{io, path::PathBuf};
12use crate::span::Span;
13
14/// Represents errors that can occur during parsing of chrony configuration files.
15///
16/// This includes I/O errors when reading included files, directive-level parse errors,
17/// and configuration-level errors such as exceeding the maximum include depth or
18/// encountering lines exceeding the maximum length.
19///
20/// [`ParseError`] implements [`std::error::Error`] via `thiserror` and supports
21/// conversion from [`std::io::Error`].
22///
23/// # Examples
24///
25/// ```rust
26/// use chrony_confile::ChronyConfig;
27///
28/// // Malformed input produces a ParseError
29/// let result = ChronyConfig::parse("::invalid directive::");
30/// assert!(result.is_err());
31/// ```
32#[derive(Debug, thiserror::Error)]
33pub enum ParseError {
34 #[error("I/O error: {0}")]
35 Io(#[from] io::Error),
36
37 /// A directive-level parse error with an optional file name.
38 #[error("{inner}")]
39 Parse {
40 file: Option<PathBuf>,
41 #[source]
42 inner: DirectiveError,
43 },
44
45 /// Maximum include nesting depth was exceeded during config expansion.
46 #[error("Maximum include level reached at {file}:{line}")]
47 IncludeLevelExceeded { file: PathBuf, line: usize },
48
49 /// A line exceeded the maximum allowed length (2048 bytes).
50 #[error("Line too long ({len} bytes) at {}:{line}", file.as_deref().map_or("?", |p| p.to_str().unwrap_or("?")))]
51 LineTooLong {
52 file: Option<PathBuf>,
53 line: usize,
54 len: usize,
55 },
56}
57
58impl PartialEq for ParseError {
59 fn eq(&self, other: &Self) -> bool {
60 match (self, other) {
61 (Self::Io(a), Self::Io(b)) => a.kind() == b.kind(),
62 (
63 Self::Parse {
64 file: f1,
65 inner: i1,
66 },
67 Self::Parse {
68 file: f2,
69 inner: i2,
70 },
71 ) => f1 == f2 && i1 == i2,
72 (
73 Self::IncludeLevelExceeded {
74 file: f1,
75 line: l1,
76 },
77 Self::IncludeLevelExceeded {
78 file: f2,
79 line: l2,
80 },
81 ) => f1 == f2 && l1 == l2,
82 (
83 Self::LineTooLong {
84 file: f1,
85 line: l1,
86 len: n1,
87 },
88 Self::LineTooLong {
89 file: f2,
90 line: l2,
91 len: n2,
92 },
93 ) => f1 == f2 && l1 == l2 && n1 == n2,
94 _ => false,
95 }
96 }
97}
98
99/// Errors specific to a single directive during parsing.
100///
101/// Covers invalid directive names, invalid options, invalid or out-of-range values,
102/// and incorrect argument counts.
103///
104/// These errors are typically wrapped in [`ParseError::Parse`] when returned from
105/// the top-level parsing API.
106#[derive(Debug, thiserror::Error, PartialEq)]
107pub enum DirectiveError {
108 /// The directive name is not a recognised chrony directive.
109 #[error("Invalid directive `{name}` at {span}")]
110 InvalidDirective { name: String, span: Span },
111
112 /// The option is not valid for the given directive.
113 /// The option is not valid for the given directive.
114 #[error("Invalid option `{option}` in `{directive}` at {span}")]
115 InvalidOption {
116 directive: String,
117 option: String,
118 span: Span,
119 },
120
121 /// The argument value could not be parsed as the expected type.
122 #[error("Invalid value in `{directive}`: expected {expected}, got `{got}` at {span}")]
123 InvalidValue {
124 directive: String,
125 expected: &'static str,
126 got: String,
127 span: Span,
128 },
129
130 /// A numeric value fell outside the valid range.
131 #[error("Value `{value}` out of range (min {min}, max {max}) in `{directive}` at {span}")]
132 ValueOutOfRange {
133 directive: String,
134 value: String,
135 min: String,
136 max: String,
137 span: Span,
138 },
139
140 /// Not enough arguments were provided for the directive.
141 #[error("Missing arguments in `{directive}`: expected {expected}, got {got} at {span}")]
142 MissingArgument {
143 directive: String,
144 expected: usize,
145 got: usize,
146 span: Span,
147 },
148
149 /// More arguments were provided than the directive expects.
150 #[error("Too many arguments in `{directive}`: expected {expected}, got {got} at {span}")]
151 TooManyArguments {
152 directive: String,
153 expected: usize,
154 got: usize,
155 span: Span,
156 },
157}
158
159/// Errors from constructing bounded integer values.
160///
161/// Returned by types like [`PollInterval`](crate::values::PollInterval),
162/// [`UdpPort`](crate::values::UdpPort), and [`Stratum`](crate::values::Stratum) when
163/// a value is out of range or has an invalid format.
164///
165/// # Examples
166///
167/// ```rust
168/// use chrony_confile::values::PollInterval;
169///
170/// // Values outside the valid range produce a ValueError
171/// let err = PollInterval::new(100).unwrap_err();
172/// assert!(err.to_string().contains("out of range"));
173/// ```
174#[derive(Debug, thiserror::Error, PartialEq)]
175pub enum ValueError {
176 /// The value is outside the valid range of the bounded integer type.
177 #[error("Value {value} out of range ({min}..{max})")]
178 OutOfRange { value: i64, min: i64, max: i64 },
179
180 /// The string could not be parsed as the expected format.
181 #[error("Invalid format `{value}`: expected {expected}")]
182 InvalidFormat { value: String, expected: &'static str },
183
184 /// The keyword did not match any of the valid options.
185 #[error("Invalid keyword `{value}`, expected one of: {}", valid.join(", "))]
186 InvalidKeyword {
187 value: String,
188 valid: &'static [&'static str],
189 },
190}