questdb_confstr/
lib.rs

1/*******************************************************************************
2 *     ___                  _   ____  ____
3 *    / _ \ _   _  ___  ___| |_|  _ \| __ )
4 *   | | | | | | |/ _ \/ __| __| | | |  _ \
5 *   | |_| | |_| |  __/\__ \ |_| |_| | |_) |
6 *    \__\_\\__,_|\___||___/\__|____/|____/
7 *
8 *  Copyright (c) 2014-2019 Appsicle
9 *  Copyright (c)  2019-2025 QuestDB
10 *
11 *  Licensed under the Apache License, Version 2.0 (the "License");
12 *  you may not use this file except in compliance with the License.
13 *  You may obtain a copy of the License at
14 *
15 *  http://www.apache.org/licenses/LICENSE-2.0
16 *
17 *  Unless required by applicable law or agreed to in writing, software
18 *  distributed under the License is distributed on an "AS IS" BASIS,
19 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 *  See the License for the specific language governing permissions and
21 *  limitations under the License.
22 *
23 ******************************************************************************/
24
25#![doc = include_str!("../README.md")]
26
27use crate::peekable2::{Peekable2, Peekable2Ext};
28use std::collections::HashMap;
29use std::fmt;
30use std::fmt::{Debug, Display, Formatter};
31use std::str::CharIndices;
32
33mod peekable2;
34
35/// Parameter keys are ascii lowercase strings.
36pub type Key = String;
37
38/// Parameter values are strings.
39pub type Value = String;
40
41/// Parameters are stored in a `Vec` of `(Key, Value)` pairs.
42/// Keys are always lowercase.
43pub type Params = HashMap<Key, Value>;
44
45/// Parsed configuration string.
46///
47/// The parameters are stored in a `Vec` of `(Key, Value)` pairs.
48pub struct ConfStr {
49    service: String,
50    params: Params,
51}
52
53impl Debug for ConfStr {
54    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
55        // Params are hidden from debug output in case they contain sensitive information.
56        write!(f, "ConfStr {{ service: {:?}, .. }}", self.service)
57    }
58}
59
60impl ConfStr {
61    /// Create a new configuration string object.
62    pub fn new(service: String, params: Params) -> Self {
63        ConfStr { service, params }
64    }
65
66    /// Access the service name.
67    pub fn service(&self) -> &str {
68        &self.service
69    }
70
71    /// Access the parameters.
72    pub fn params(&self) -> &Params {
73        &self.params
74    }
75
76    /// Get a parameter.
77    /// Key should always be specified as lowercase.
78    pub fn get(&self, key: &str) -> Option<&str> {
79        self.params.get(key).map(|s| s.as_str())
80    }
81}
82
83/// Byte position in the input string where the parsing error occurred.
84pub type Position = usize;
85
86/// The type of parsing error.
87#[derive(Debug, PartialEq, Eq, Clone)]
88pub enum ErrorKind {
89    ExpectedIdentifierNot(char),
90    MustBeAlphanumeric(char),
91    ExpectedIdentifierNotEmpty,
92    BadSeparator((char, char)),
93    IncompleteKeyValue,
94    InvalidCharInValue(char),
95    DuplicateKey(String),
96}
97
98impl<'a> PartialEq<&'a ErrorKind> for ErrorKind {
99    fn eq(&self, other: &&'a ErrorKind) -> bool {
100        self == *other
101    }
102}
103
104impl PartialEq<ErrorKind> for &ErrorKind {
105    fn eq(&self, other: &ErrorKind) -> bool {
106        *self == other
107    }
108}
109
110impl Display for ErrorKind {
111    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
112        // Ensure no values are leaked in error messages.
113        match self {
114            ErrorKind::ExpectedIdentifierNot(c) => {
115                write!(
116                    f,
117                    "expected identifier to start with ascii letter, not {:?}",
118                    c
119                )
120            }
121            ErrorKind::MustBeAlphanumeric(c) => write!(f, "must be alphanumeric, not {:?}", c),
122            ErrorKind::ExpectedIdentifierNotEmpty => {
123                write!(f, "expected identifier, not an empty string")
124            }
125            ErrorKind::BadSeparator((e, c)) => {
126                write!(f, "bad separator, expected {:?} got {:?}", e, c)
127            }
128            ErrorKind::IncompleteKeyValue => {
129                write!(f, "incomplete key-value pair before end of input")
130            }
131            ErrorKind::InvalidCharInValue(c) => write!(f, "invalid char {:?} in value", c),
132            ErrorKind::DuplicateKey(s) => write!(f, "duplicate key {:?}", s),
133        }
134    }
135}
136
137/// The parsing error.
138#[derive(Debug)]
139pub struct ParsingError {
140    kind: ErrorKind,
141    position: usize,
142}
143
144impl ParsingError {
145    /// Access the byte position in the input string where the parsing error occurred.
146    pub fn position(&self) -> usize {
147        self.position
148    }
149
150    /// Access the type of parsing error.
151    pub fn kind(&self) -> &ErrorKind {
152        &self.kind
153    }
154}
155
156fn parse_err(kind: ErrorKind, position: Position) -> ParsingError {
157    ParsingError { kind, position }
158}
159
160impl Display for ParsingError {
161    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
162        write!(f, "{} at position {}", self.kind, self.position)
163    }
164}
165
166impl std::error::Error for ParsingError {}
167
168fn parse_ident(
169    iter: &mut Peekable2<CharIndices>,
170    next_pos: &mut Position,
171) -> Result<Key, ParsingError> {
172    let mut token = String::new();
173    while let Some((pos, c)) = iter.peek0() {
174        *next_pos = *pos;
175        if c.is_ascii_alphanumeric() || *c == '_' {
176            token.push(*c);
177            iter.next();
178        } else {
179            if token.is_empty() {
180                return Err(parse_err(ErrorKind::ExpectedIdentifierNot(*c), *next_pos));
181            } else if !c.is_ascii() || matches!(c, '\0'..=' ') {
182                return Err(parse_err(ErrorKind::MustBeAlphanumeric(*c), *next_pos));
183            }
184            break;
185        }
186    }
187
188    if token.is_empty() {
189        return Err(parse_err(ErrorKind::ExpectedIdentifierNotEmpty, *next_pos));
190    }
191
192    Ok(token)
193}
194
195fn parse_value(
196    iter: &mut Peekable2<CharIndices>,
197    next_pos: &mut Position,
198) -> Result<Value, ParsingError> {
199    let mut value = String::new();
200    loop {
201        let c1 = iter.peek0().cloned();
202        let c2 = iter.peek1().cloned();
203        if let Some((p, _)) = c1 {
204            *next_pos = p;
205        }
206        match (c1, c2) {
207            (Some((_, ';')), Some((_, ';'))) => {
208                let _ = iter.next();
209                let _ = iter.next();
210                value.push(';');
211            }
212            (Some((_, ';')), _) => break,
213            (Some((p, c)), _) => {
214                if matches!(c, '\u{0}'..='\u{1f}' | '\u{7f}'..='\u{9f}') {
215                    return Err(parse_err(ErrorKind::InvalidCharInValue(c), p));
216                }
217                value.push(c);
218                let _ = iter.next();
219            }
220            (None, _) => break,
221        }
222    }
223    Ok(value)
224}
225
226fn parse_double_colon(
227    iter: &mut Peekable2<CharIndices>,
228    next_pos: &mut Position,
229) -> Result<bool, ParsingError> {
230    let c1 = iter.next();
231    let c2 = iter.next();
232    match (c1, c2) {
233        (Some((_, ':')), Some((_, ':'))) => {
234            *next_pos += 2;
235            Ok(true)
236        }
237        (None, None) => Ok(false),
238        (Some((_, ':')), Some((p, c))) => Err(parse_err(ErrorKind::BadSeparator((':', c)), p)),
239        (Some((p, c)), _) => Err(parse_err(ErrorKind::BadSeparator((':', c)), p)),
240        (None, _) => unreachable!("peekable2 guarantees that the second item is always None"),
241    }
242}
243
244fn parse_params(
245    iter: &mut Peekable2<CharIndices>,
246    next_pos: &mut Position,
247    input_len: usize,
248) -> Result<Params, ParsingError> {
249    let mut params = Params::new();
250    while let Some((p, _)) = iter.peek0() {
251        *next_pos = *p;
252        let key_pos = *next_pos;
253        let key = parse_ident(iter, next_pos)?;
254        if params.contains_key(&key) {
255            return Err(parse_err(ErrorKind::DuplicateKey(key.clone()), key_pos));
256        }
257        match iter.next() {
258            Some((p, '=')) => *next_pos = p + 1,
259            Some((p, c)) => return Err(parse_err(ErrorKind::BadSeparator(('=', c)), p)),
260            None => return Err(parse_err(ErrorKind::IncompleteKeyValue, input_len)),
261        }
262        let value = parse_value(iter, next_pos)?;
263        iter.next(); // skip ';', if present.
264        params.insert(key, value);
265    }
266    Ok(params)
267}
268
269/// Parse a config string.
270///
271/// ```
272/// use questdb_confstr::parse_conf_str;
273/// # use questdb_confstr::ParsingError;
274/// let config = parse_conf_str("service::key1=value1;key2=value2;")?;
275/// assert_eq!(config.service(), "service");
276/// assert_eq!(config.get("key1"), Some("value1"));
277/// assert_eq!(config.get("key2"), Some("value2"));
278/// # Ok::<(), ParsingError>(())
279/// ```
280pub fn parse_conf_str(input: &str) -> Result<ConfStr, ParsingError> {
281    let mut iter = input.char_indices().peekable2();
282    let mut next_pos = 0;
283    let service = parse_ident(&mut iter, &mut next_pos)?;
284    let has_separator = parse_double_colon(&mut iter, &mut next_pos)?;
285    if !has_separator {
286        return Ok(ConfStr::new(service, Params::new()));
287    }
288    let params = parse_params(&mut iter, &mut next_pos, input.len())?;
289    Ok(ConfStr::new(service, params))
290}