confget/backend/
ini_re.rs1use anyhow::anyhow;
8use regex::{Captures, Regex};
9
10use crate::backend::{Backend, DataRead};
11use crate::defs::{ConfgetError, Config, FileData, SectionData};
12
13#[derive(Debug)]
15#[non_exhaustive]
16#[allow(clippy::module_name_repetitions)]
17pub struct IniREBackend<'cfg> {
18 config: &'cfg Config,
20
21 filename: &'cfg str,
23}
24
25#[derive(Debug)]
27struct State {
28 re_comment: Regex,
30 re_section: Regex,
32 re_variable: Regex,
34 filename: String,
36 first_section: Option<String>,
38 section: String,
40 cont: Option<(String, String)>,
42 found: bool,
46}
47
48impl State {
49 fn feed_line(self, line: &str, res: &mut FileData) -> Result<Self, ConfgetError> {
51 if let Some((name, value)) = self.cont {
52 if let Some(stripped) = line.strip_suffix('\\') {
53 Ok(Self {
54 cont: Some((name, format!("{value}{stripped}"))),
55 ..self
56 })
57 } else {
58 res.get_mut(&self.section)
59 .ok_or_else(|| {
60 ConfgetError::Internal(format!(
61 "Internal error: no data for section {section}",
62 section = self.section
63 ))
64 })?
65 .insert(name, format!("{value}{trimmed}", trimmed = line.trim_end()));
66 Ok(Self { cont: None, ..self })
67 }
68 } else if self.re_comment.is_match(line) {
69 Ok(self)
70 } else {
71 fn extr<'data>(
73 caps: &'data Captures<'_>,
74 name: &str,
75 ) -> Result<&'data str, ConfgetError> {
76 Ok(caps
77 .name(name)
78 .ok_or_else(|| {
79 ConfgetError::Internal(format!("Internal error: no '{name}' in {caps:?}"))
80 })?
81 .as_str())
82 }
83
84 if let Some(caps) = self.re_section.captures(line) {
85 let name = extr(&caps, "name")?;
86 res.entry(name.to_owned()).or_insert_with(SectionData::new);
87 Ok(Self {
88 first_section: if self.first_section.is_none() && !self.found {
89 Some(name.to_owned())
90 } else {
91 self.first_section
92 },
93 section: name.to_owned(),
94 found: true,
95 ..self
96 })
97 } else {
98 let caps = if let Some(caps) = self.re_variable.captures(line) {
99 caps
100 } else {
101 return Err(ConfgetError::FileFormat(
102 self.filename,
103 anyhow!(format!(
104 "Unexpected line: '{escaped}'",
105 escaped = line.escape_debug()
106 )),
107 ));
108 };
109 let name = extr(&caps, "name")?;
110 let value = extr(&caps, "value")?;
111 let cont = caps.name("cont").is_some();
112 if cont {
113 Ok(Self {
114 cont: Some((
115 name.to_owned(),
116 format!("{value}{ws}", ws = extr(&caps, "ws")?),
117 )),
118 found: true,
119 ..self
120 })
121 } else {
122 res.get_mut(&self.section)
123 .ok_or_else(|| {
124 ConfgetError::Internal(format!(
125 "Internal error: no data for section {section}",
126 section = self.section
127 ))
128 })?
129 .insert(name.to_owned(), value.to_owned());
130 Ok(Self {
131 found: true,
132 ..self
133 })
134 }
135 }
136 }
137 }
138}
139
140static RE_COMMENT: &str = r"(?x) ^ \s* (?: [\#;] .* )? $ ";
142
143static RE_SECTION: &str = r"(?x)
145 ^ \s*
146 \[ \s*
147 (?P<name> [^\]]+? )
148 \s* \]
149 \s* $ ";
150
151static RE_VARIABLE: &str = r"(?x)
153 ^ \s*
154 (?P<name> [^\s=]+ )
155 \s* = \s*
156 (?P<value> .*? )
157 (?P<ws> \s* )
158 (?P<cont> [\\] )?
159 $ ";
160
161impl<'cfg> Backend<'cfg> for IniREBackend<'cfg> {
162 #[inline]
168 fn from_config(config: &'cfg Config) -> Result<Self, ConfgetError> {
169 let filename: &str = config
170 .filename
171 .as_ref()
172 .ok_or_else(|| ConfgetError::Config("No filename supplied".to_owned()))?;
173 Ok(Self { config, filename })
174 }
175
176 #[inline]
185 fn read_file(&self) -> Result<DataRead, ConfgetError> {
186 let mut res = FileData::new();
187 res.insert(String::new(), SectionData::new());
188
189 let init_state = State {
190 re_comment: Regex::new(RE_COMMENT).map_err(|err| {
191 ConfgetError::Internal(format!(
192 "Could not compile the '{RE_COMMENT}' regular expression: {err}"
193 ))
194 })?,
195 re_section: Regex::new(RE_SECTION).map_err(|err| {
196 ConfgetError::Internal(format!(
197 "Could not compile the '{RE_SECTION}' regular expression: {err}"
198 ))
199 })?,
200 re_variable: Regex::new(RE_VARIABLE).map_err(|err| {
201 ConfgetError::Internal(format!(
202 "Could not compile the '{RE_VARIABLE}' regular expression: {err}"
203 ))
204 })?,
205 filename: self.filename.to_owned(),
206 first_section: self
207 .config
208 .section_specified
209 .then(|| self.config.section.clone()),
210 section: String::new(),
211 cont: None,
212 found: false,
213 };
214
215 let final_state = super::get_file_lines(self.filename, &self.config.encoding)?
216 .iter()
217 .try_fold(init_state, |state, line| state.feed_line(line, &mut res))?;
218 if final_state.cont.is_some() {
219 return Err(ConfgetError::FileFormat(
220 self.filename.to_owned(),
221 anyhow!("Line continuation on the last line"),
222 ));
223 }
224 Ok((
225 res,
226 final_state
227 .first_section
228 .unwrap_or_else(|| self.config.section.clone()),
229 ))
230 }
231}