use anyhow::anyhow;
use regex::{Captures, Regex};
use crate::backend::{Backend, DataRead};
use crate::defs::{ConfgetError, Config, FileData, SectionData};
#[derive(Debug)]
#[non_exhaustive]
#[allow(clippy::module_name_repetitions)]
pub struct IniREBackend<'cfg> {
pub config: &'cfg Config,
filename: &'cfg str,
}
#[derive(Debug)]
struct State {
re_comment: Regex,
re_section: Regex,
re_variable: Regex,
filename: String,
first_section: Option<String>,
section: String,
cont: Option<(String, String)>,
found: bool,
}
impl State {
fn feed_line(self, line: &str, res: &mut FileData) -> Result<Self, ConfgetError> {
if let Some((name, value)) = self.cont {
if let Some(stripped) = line.strip_suffix('\\') {
Ok(Self {
cont: Some((name, format!("{}{}", value, stripped))),
..self
})
} else {
res.get_mut(&self.section)
.ok_or_else(|| {
ConfgetError::Internal(format!(
"Internal error: no data for section {}",
self.section
))
})?
.insert(name, format!("{}{}", value, line.trim_end()));
Ok(Self { cont: None, ..self })
}
} else if self.re_comment.is_match(line) {
Ok(self)
} else {
fn extr<'data>(
caps: &'data Captures<'_>,
name: &str,
) -> Result<&'data str, ConfgetError> {
Ok(caps
.name(name)
.ok_or_else(|| {
ConfgetError::Internal(format!(
"Internal error: no '{}' in {:?}",
name, caps
))
})?
.as_str())
}
if let Some(caps) = self.re_section.captures(line) {
let name = extr(&caps, "name")?;
res.entry(name.to_owned()).or_insert_with(SectionData::new);
Ok(Self {
first_section: if self.first_section.is_none() && !self.found {
Some(name.to_owned())
} else {
self.first_section
},
section: name.to_owned(),
found: true,
..self
})
} else {
let caps = if let Some(caps) = self.re_variable.captures(line) {
caps
} else {
return Err(ConfgetError::FileFormat(
self.filename,
anyhow!(format!("Unexpected line: '{}'", line.escape_debug())),
));
};
let name = extr(&caps, "name")?;
let value = extr(&caps, "value")?;
let cont = caps.name("cont").is_some();
if cont {
Ok(Self {
cont: Some((name.to_owned(), format!("{}{}", value, extr(&caps, "ws")?))),
found: true,
..self
})
} else {
res.get_mut(&self.section)
.ok_or_else(|| {
ConfgetError::Internal(format!(
"Internal error: no data for section {}",
self.section
))
})?
.insert(name.to_owned(), value.to_owned());
Ok(Self {
found: true,
..self
})
}
}
}
}
}
static RE_COMMENT: &str = r"(?x) ^ \s* (?: [\#;] .* )? $ ";
static RE_SECTION: &str = r"(?x)
^ \s*
\[ \s*
(?P<name> [^\]]+? )
\s* \]
\s* $ ";
static RE_VARIABLE: &str = r"(?x)
^ \s*
(?P<name> [^\s=]+ )
\s* = \s*
(?P<value> .*? )
(?P<ws> \s* )
(?P<cont> [\\] )?
$ ";
impl<'cfg> Backend<'cfg> for IniREBackend<'cfg> {
#[inline]
fn from_config(config: &'cfg Config) -> Result<Self, ConfgetError> {
let filename: &str = config
.filename
.as_ref()
.ok_or_else(|| ConfgetError::Config("No filename supplied".to_owned()))?;
Ok(Self { config, filename })
}
#[inline]
fn read_file(&self) -> Result<DataRead, ConfgetError> {
let mut res = FileData::new();
res.insert(String::new(), SectionData::new());
let init_state = State {
re_comment: Regex::new(RE_COMMENT).map_err(|err| {
ConfgetError::Internal(format!(
"Could not compile the '{}' regular expression: {}",
RE_COMMENT, err
))
})?,
re_section: Regex::new(RE_SECTION).map_err(|err| {
ConfgetError::Internal(format!(
"Could not compile the '{}' regular expression: {}",
RE_SECTION, err
))
})?,
re_variable: Regex::new(RE_VARIABLE).map_err(|err| {
ConfgetError::Internal(format!(
"Could not compile the '{}' regular expression: {}",
RE_VARIABLE, err
))
})?,
filename: self.filename.to_owned(),
first_section: self
.config
.section_specified
.then(|| self.config.section.clone()),
section: String::new(),
cont: None,
found: false,
};
let final_state = super::get_file_lines(self.filename, &self.config.encoding)?
.iter()
.try_fold(init_state, |state, line| state.feed_line(line, &mut res))?;
if final_state.cont.is_some() {
return Err(ConfgetError::FileFormat(
self.filename.to_owned(),
anyhow!("Line continuation on the last line"),
));
}
Ok((
res,
final_state
.first_section
.unwrap_or_else(|| self.config.section.clone()),
))
}
}