use crate::prelude::*;
impl FromStr for ExpireLine {
type Err = AE;
fn from_str(ls: &str) -> AR<Self> {
(||{
let mut words = ls.split(':');
let mut next = || words.next().ok_or_else(|| anyhow!("too few fields"));
let el = ExpireLine {
pattern: next()?.into(),
flag: next()?.into(),
min: next()?.parse().context("min")?,
def: next()?.parse().context("default")?,
max: next()?.parse().context("max")?,
};
if words.next().is_some() {
return Err(anyhow!("too many fields"));
}
Ok(el)
})().context("active entry")
}
}
#[derive(Debug, Clone)]
struct Setting {
value: String,
lno: usize,
}
type Settings = HashMap<String, Setting>;
pub struct SettingsConverter<'s> {
filename: &'s str,
settings: &'s Settings,
unused: HashSet<&'s str>,
}
pub type SettingsUnusedReported = HashSet<String>;
impl<'s> SettingsConverter<'s> {
fn new(filename: &'s str, settings: &'s Settings) -> Self {
let unused = settings.keys().map(|s| &**s).collect();
SettingsConverter {
filename,
settings,
unused,
}
}
fn check_all_used(mut self, dedup: &mut SettingsUnusedReported) -> AR<()> {
for s in mem::take(&mut self.unused) {
if dedup.insert(s.to_owned()) {
let lno = &self.settings[s].lno;
eprintln!("dynexp2: warning: {}:{}: unknown setting {:?}",
self.filename, lno, s);
}
}
Ok(())
}
pub fn obtain1<T>(&mut self, kw: &str, def: Option<&dyn Fn() -> T>) -> AR<T>
where T: FromStr + Clone, Result<T, T::Err>: anyhow::Context<T, T::Err>,
{
self.unused.remove(kw);
let value = self.settings.get(kw)
.map(|setting| {
setting.value.parse()
.with_context(|| format!("{}:{}", self.filename, setting.lno))
})
.transpose()?
.or_else(|| def.map(|def| def()))
.ok_or_else(|| anyhow!("missing setting {}", kw))?;
Ok(value)
}
}
impl ActiveData {
pub fn new(params: Parameters, line: ExpireLine) -> AR<Self> {
let maxdays = cmp::min(
params.maxdays,
line.max,
).try_into().map_err(|NeverError| anyhow!(
"need expiry limit, but both maxdays (in parameters) and max (in expire line) are never"
))?;
Ok(ActiveData {
was: line.def,
maxdays,
params,
line,
})
}
}
impl Loaded {
pub fn read(filename: &str) -> AR<Self> {
let f = File::open(filename)
.with_context(|| filename.to_string())
.context("open")?;
Loaded::read_any(Box::new(f) as _, filename)
}
pub fn overwrite(&self, filename: &str) -> AR<()> {
let tmp = format!("{}.tmp", filename);
(||{
let f = File::create(&tmp).context("create")?;
let mut f = BufWriter::new(f);
write!(f, "{}", self).context("write")?;
f.flush().context("flush")?;
AOk(())
})()
.with_context(|| tmp.to_string())
.context("temporary output file")?;
fs::rename(tmp, filename)
.with_context(|| filename.to_string())
.context("install new output file")?;
Ok(())
}
pub fn read_any(f: Box<dyn io::Read>, filename: &str) -> AR<Self> {
let f = BufReader::new(f);
let mut settings = HashMap::new();
let mut enable = false;
let mut spools = BTreeMap::new();
let mut lines = vec![];
let mut dedup_unused = SettingsUnusedReported::default();
for (lno, line_raw) in f.lines().enumerate() {
let lno = lno + 1;
let line_raw = line_raw
.with_context(|| filename.to_string())
.context("read")?;
(||{
let line = line_raw.trim();
if ! line.starts_with('#') && ! line.is_empty() && enable {
let mut settings = SettingsConverter::new(filename, &settings);
let params = Parameters::from_settings(&mut settings)?;
let spool_spec = SpoolSpec::from_settings(&mut settings)?;
settings.check_all_used(&mut dedup_unused)?;
params.check()?;
let spool = spools.entry(spool_spec)
.or_insert_with(|| SpoolEntries { entries: vec![] });
let line: ExpireLine = line.parse()?;
let ae = Rc::new(RefCell::new(ActiveData::new(params, line)?));
spool.entries.push(ae.clone());
lines.push(FileLine::Active(ae));
return AOk(());
}
if let Some(mut words) = (||{
let mut words = line.trim().split_ascii_whitespace();
let mut expect_word = |w: &str| {
if words.next()? != w { return None }
Some(())
};
expect_word("#")?;
expect_word("::")?;
expect_word("dynexpire")?;
Some(words)
})() {
let kw = words.next()
.ok_or_else(|| anyhow!("keyword missing in setting"))?;
match kw {
"on" => enable = true,
"off" => enable = false,
"dffield" => { let _: Option<&str> = words.next(); },
_ => {
let value = words.next()
.ok_or_else(|| anyhow!("value missing in setting"))?
.into();
settings.insert(kw.into(), Setting { value, lno });
},
}
if words.next().is_some() {
return Err(anyhow!("extraneous words in setting"))?;
}
}
lines.push(FileLine::Inert(line_raw));
Ok(())
})()
.with_context(|| format!("{}:{}", &filename, lno))
.context("parse error")?;
}
Ok(Loaded {
lines,
spools,
})
}
}
impl Display for Loaded {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for l in &self.lines {
match l {
FileLine::Inert(s) => write!(f, "{}", s)?,
FileLine::Active(ae) => write!(f, "{}", &ae.borrow().line)?,
}
writeln!(f)?;
}
Ok(())
}
}
impl Display for ExpireLine {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}:{}", &self.pattern, &self.flag)?;
for v in [
&self.min as &dyn Display,
&self.def as _,
&self.max as _,
] {
write!(f, ":{}", v)?;
}
Ok(())
}
}