use configparser crate

This parser can correctly read globs from configuration keys which allows
their use not just in theory in the server logic but in the config file too.
This commit is contained in:
Johann150 2021-02-12 16:51:42 +01:00
parent fdca530591
commit 197e4592b9
No known key found for this signature in database
GPG key ID: 9EE6577A2A06F8F1
5 changed files with 126 additions and 147 deletions

View file

@ -1,8 +1,8 @@
use configparser::ini::Ini;
use glob::{glob_with, MatchOptions};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use yaml_rust::YamlLoader;
static SIDECAR_FILENAME: &str = ".meta";
@ -78,7 +78,7 @@ impl FileOptions {
};
db.push(SIDECAR_FILENAME);
let should_read = if let Ok(metadata) = db.as_path().metadata() {
let should_read = if let Ok(metadata) = db.metadata() {
if !metadata.is_file() {
// it exists, but it is a directory
false
@ -109,122 +109,108 @@ impl FileOptions {
fn read_database(&mut self, db: &PathBuf) {
log::trace!("reading database {:?}", db);
if let Ok(contents) = std::fs::read_to_string(db) {
self.databases_read
.insert(db.to_path_buf(), SystemTime::now());
let mut ini = Ini::new_cs();
ini.set_default_section("mime");
let map = ini
.load(db.to_str().expect("config path not UTF-8"))
.and_then(|mut sections| {
sections
.remove("mime")
.ok_or_else(|| "no \"mime\" or default section".to_string())
});
self.databases_read
.insert(db.to_path_buf(), SystemTime::now());
let files = match map {
Ok(section) => section,
Err(err) => {
log::error!("invalid config file {:?}: {}", db, err);
return;
}
};
let docs = match YamlLoader::load_from_str(&contents) {
Ok(docs) => docs,
Err(e) => {
log::error!("Invalid YAML document in {:?}: {}", db, e);
for (rel_path, header) in files {
// treat unassigned keys as if they had an empty value
let header = header.unwrap_or_default();
// generate workspace-relative path
let mut path = db.clone();
path.pop();
path.push(rel_path);
// parse the preset
let preset = if header.is_empty() || header.starts_with(';') {
PresetMeta::Parameters(header.to_string())
} else if matches!(header.chars().next(), Some('1'..='6')) {
if header.len() < 3
|| !header.chars().nth(1).unwrap().is_ascii_digit()
|| !header.chars().nth(2).unwrap().is_whitespace()
{
log::error!("Line for {:?} starts like a full header line, but it is incorrect; ignoring it.", path);
return;
}
let separator = header.chars().nth(2).unwrap();
if separator != ' ' {
// the Gemini specification says that the third
// character has to be a space, so correct any
// other whitespace to it (e.g. tabs)
log::warn!("Full Header line for {:?} has an invalid character, treating {:?} as a space.", path, separator);
}
let status = header
.chars()
.take(2)
.collect::<String>()
.parse::<u8>()
// unwrap since we alread checked it's a number
.unwrap();
// not taking a slice here because the separator
// might be a whitespace wider than a byte
let meta = header.chars().skip(3).collect::<String>();
PresetMeta::FullHeader(status, meta)
} else {
// must be a MIME type, but without status code
PresetMeta::FullMime(header.to_string())
};
if let Some(files) = docs.get(0).and_then(|hash| hash.as_hash()) {
for (rel_path, header) in files {
// from YAML to Rust types
let rel_path = if let Some(rel_path) = rel_path.as_str() {
rel_path
} else {
log::error!(
"Expected string filename, but got {:?} in {:?}",
rel_path,
db
);
let glob_options = MatchOptions {
case_sensitive: true,
// so there is a difference between "*" and "**".
require_literal_separator: true,
// security measure because entries for .hidden files
// would result in them being exposed.
require_literal_leading_dot: !crate::ARGS.serve_secret,
};
// process filename as glob
let paths = if let Some(path) = path.to_str() {
match glob_with(path, glob_options) {
Ok(paths) => paths.collect::<Vec<_>>(),
Err(err) => {
log::error!("incorrect glob pattern in {:?}: {}", path, err);
continue;
};
let header = if let Some(header) = header.as_str() {
header
} else {
log::error!("Expected string contents, but got {:?} in {:?}", header, db);
continue;
};
// generate workspace-relative path
let mut path = db.clone();
path.pop();
path.push(rel_path);
// parse the preset
let preset = if header.is_empty() || header.starts_with(';') {
PresetMeta::Parameters(header.to_string())
} else if matches!(header.chars().next(), Some('1'..='6')) {
if header.len() < 3
|| !header.chars().nth(1).unwrap().is_ascii_digit()
|| !header.chars().nth(2).unwrap().is_whitespace()
{
log::error!("Line for {:?} starts like a full header line, but it is incorrect; ignoring it.", path);
return;
}
let separator = header.chars().nth(2).unwrap();
if separator != ' ' {
// the Gemini specification says that the third
// character has to be a space, so correct any
// other whitespace to it (e.g. tabs)
log::warn!("Full Header line for {:?} has an invalid character, treating {:?} as a space.", path, separator);
}
let status = header
.chars()
.take(2)
.collect::<String>()
.parse::<u8>()
// unwrap since we alread checked it's a number
.unwrap();
// not taking a slice here because the separator
// might be a whitespace wider than a byte
let meta = header.chars().skip(3).collect::<String>();
PresetMeta::FullHeader(status, meta)
} else {
// must be a MIME type, but without status code
PresetMeta::FullMime(header.to_string())
};
let glob_options = MatchOptions {
case_sensitive: true,
// so there is a difference between "*" and "**".
require_literal_separator: true,
// security measure because entries for .hidden files
// would result in them being exposed.
require_literal_leading_dot: !crate::ARGS.serve_secret,
};
// process filename as glob
let paths = if let Some(path) = path.to_str() {
match glob_with(path, glob_options) {
Ok(paths) => paths.collect::<Vec<_>>(),
Err(err) => {
log::error!("incorrect glob pattern: {}", err);
continue;
}
}
} else {
log::error!("path is not UTF-8: {:?}", path);
continue;
};
if paths.is_empty() {
// probably an entry for a nonexistent file, glob only works for existing files
self.file_meta.insert(path, preset);
} else {
for glob_result in paths {
match glob_result {
Ok(path) if path.is_dir() => { /* ignore */ }
Ok(path) => {
self.file_meta.insert(path, preset.clone());
}
Err(err) => {
log::warn!("could not process glob path: {}", err);
continue;
}
};
}
}
}
} else {
log::error!("no YAML document {:?}", db);
log::error!("path is not UTF-8: {:?}", path);
continue;
};
if paths.is_empty() {
// probably an entry for a nonexistent file, glob only works for existing files
self.file_meta.insert(path, preset);
} else {
for glob_result in paths {
match glob_result {
Ok(path) if path.is_dir() => { /* ignore */ }
Ok(path) => {
self.file_meta.insert(path, preset.clone());
}
Err(err) => {
log::warn!("could not process glob path: {}", err);
continue;
}
};
}
}
} else {
log::error!("could not read configuration file {:?}", db);
}
}