#[cfg(unix)]
use std::os::unix::ffi::OsStrExt;
use std::{
error::Error,
fmt,
fs::File,
io,
path::{Path, PathBuf},
};
use shared::ext::BytesExt;
const STEM: &str = "ocean";
const STEM_LEN: usize = 5;
mod ext {
pub const TOML: &str = "toml";
pub const JSON: &str = "json";
pub const YAML: &str = "yaml";
pub const YML: &str = "yml";
pub const MIN_LEN: usize = 3;
pub const MAX_LEN: usize = 4;
}
#[derive(Debug)]
pub struct ConfigFile {
pub path: PathBuf,
pub fmt: ConfigFileFmt,
pub handle: Option<File>,
}
impl ConfigFile {
pub fn find(path: &Path) -> Result<Self, NotFound<'_>> {
const MIN_LEN: usize = STEM_LEN + 1 + ext::MIN_LEN;
const MAX_LEN: usize = STEM_LEN + 1 + ext::MAX_LEN;
macro_rules! convert_result {
($result:expr) => {
$result.map_err(|err| {
let reason = NotFoundReason::Io(err);
NotFound { reason, path }
})
};
}
for entry in convert_result!(path.read_dir())? {
let entry = convert_result!(entry)?;
let name = entry.file_name();
match name.len() {
MIN_LEN..=MAX_LEN => {},
_ => continue,
}
#[cfg(unix)]
let bytes = name.as_bytes();
#[cfg(not(unix))]
let bytes = if let Some(s) = name.to_str() {
s.as_bytes()
} else {
continue;
};
let stem = unsafe { bytes.get_unchecked(..STEM_LEN) };
if !stem.matches_special_lowercase(STEM) {
continue;
}
if unsafe { *bytes.get_unchecked(STEM_LEN) } != b'.' {
continue;
}
let ext = unsafe { bytes.get_unchecked((STEM_LEN + 1)..) };
if let Some(fmt) = ConfigFileFmt::from_bytes(ext) {
return Ok(ConfigFile { path: path.join(name), fmt, handle: None });
} else {
continue;
};
}
Err(NotFound { reason: NotFoundReason::NoMatch, path })
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ConfigFileFmt {
Toml,
Json,
Yaml,
}
impl ConfigFileFmt {
pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
macro_rules! handle_ext {
($($fmt:ident => $($ext:expr),+;)+) => {
$(if $(bytes.matches_special_lowercase($ext))||+ {
Some(ConfigFileFmt::$fmt)
} else)+ {
None
}
};
}
handle_ext! {
Toml => ext::TOML;
Json => ext::JSON;
Yaml => ext::YAML, ext::YML;
}
}
pub fn from_path(path: &Path) -> Option<Self> {
let ext = path.extension()?;
match ext.len() {
ext::MIN_LEN..=ext::MAX_LEN => {
#[cfg(unix)]
let ext = ext.as_bytes();
#[cfg(not(unix))]
let ext = ext.to_str()?.as_bytes();
ConfigFileFmt::from_bytes(ext)
},
_ => None,
}
}
}
#[derive(Debug)]
pub enum NotFoundReason {
Io(io::Error),
NoMatch,
}
impl From<io::Error> for NotFoundReason {
fn from(error: io::Error) -> Self {
NotFoundReason::Io(error)
}
}
#[derive(Debug)]
pub struct NotFound<'a> {
pub reason: NotFoundReason,
pub path: &'a Path,
}
impl Error for NotFound<'_> {}
impl fmt::Display for NotFound<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self.reason {
NotFoundReason::Io(err) => write!(
f,
"{} for {:?}",
err,
self.path,
),
NotFoundReason::NoMatch => write!(
f,
"No TOML, JSON, or YAML file named \"ocean\" found in \"{}\"",
self.path.display()
),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use self::ConfigFileFmt::*;
static PAIRS: &[(ConfigFileFmt, &[&str])] = &[
(Toml, &[ext::TOML]),
(Json, &[ext::JSON]),
(Yaml, &[ext::YAML, ext::YML]),
];
#[test]
fn find_cfg_file() {
let dir = tempfile::tempdir().unwrap();
match ConfigFile::find(dir.path()) {
Ok(file) => panic!("Found unexpected config {:?}", file),
Err(err) => match err.reason {
NotFoundReason::NoMatch => {},
NotFoundReason::Io(err) => panic!("{}", err),
}
}
for &(fmt, exts) in PAIRS {
for &ext in exts {
let cfg_name = format!("{}.{}", STEM, ext);
let upper = cfg_name.to_uppercase();
let lower = cfg_name.to_lowercase();
for cfg_name in &[lower, upper] {
let cfg_path = dir.path().join(&cfg_name);
std::fs::File::create(&cfg_path).unwrap();
let cfg_file = ConfigFile::find(dir.path()).unwrap();
assert_eq!(cfg_file.path, cfg_path);
assert_eq!(cfg_file.fmt, fmt);
std::fs::remove_file(cfg_path).unwrap();
}
}
}
#[cfg(unix)]
{
use std::os::unix::fs::DirBuilderExt;
let no_read = dir.path().join("no_read");
std::fs::DirBuilder::new()
.mode(0) .create(&no_read)
.unwrap();
let exp_err = std::io::ErrorKind::PermissionDenied;
match ConfigFile::find(&no_read) {
Ok(file) => panic!("Found unexpected config {:?}", file),
Err(err) => match err.reason {
NotFoundReason::NoMatch => panic!("Should emit IO error"),
NotFoundReason::Io(err) => assert_eq!(err.kind(), exp_err),
}
}
}
}
#[test]
fn fmt_from_path() {
let prefixes: &[_] = &[
"",
"/",
"./",
"/xyz/",
];
for &(fmt, exts) in PAIRS {
for ext in exts {
for prefix in prefixes {
let path = PathBuf::from(format!(
"{}{}.{}",
prefix,
STEM,
ext,
));
assert_eq!(ConfigFileFmt::from_path(&path).unwrap(), fmt);
}
}
}
}
}