#![cfg_attr(docsrs, feature(doc_cfg))]
use std::fs::File;
use std::io::{BufRead, BufReader, Read};
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use flate2::read::GzDecoder;
use regex::Regex;
use error::*;
#[cfg(feature = "capi")]
#[cfg_attr(docsrs, doc(cfg(feature = "capi")))]
pub mod capi;
pub mod error;
pub struct Config {
path: PathBuf,
}
impl Config {
pub fn new<P: Into<PathBuf>>(path: P) -> Self {
Self { path: path.into() }
}
pub fn reader(&self) -> Result<Box<dyn BufRead>, GettingConfigReaderError> {
let config_file =
File::open(self.path()).map_err(GettingConfigReaderError::FailedToOpenFile)?;
if self.is_gzip()? {
Ok(Box::new(BufReader::new(GzDecoder::new(config_file))))
} else {
Ok(Box::new(BufReader::new(config_file)))
}
}
pub fn path(&self) -> &PathBuf {
&self.path
}
pub fn is_gzip(&self) -> Result<bool, IsGzipError> {
let file = File::open(self.path()).map_err(IsGzipError::FailedToOpenFile)?;
let mut reader = BufReader::new(file);
const GZIP_MAGIC: [u8; 2] = [0x1F, 0x8B];
let mut magic = [0u8; GZIP_MAGIC.len()];
let n = reader
.read(&mut magic)
.map_err(IsGzipError::FailedToReadFileMagic)?;
Ok(n == GZIP_MAGIC.len() && magic == GZIP_MAGIC)
}
}
pub fn locate_config<P: AsRef<Path>>(
default_path: Option<P>,
) -> Result<Option<Config>, LocateConfigError> {
if let Some(path) = default_path {
if path.as_ref().exists() {
return Ok(Some(Config {
path: path.as_ref().to_path_buf(),
}));
} else {
return Ok(None);
}
}
let proc_path = PathBuf::from("/proc/config.gz");
if proc_path.exists() {
return Ok(Some(Config { path: proc_path }));
}
let uname_r = get_linux_kernel_version()?;
let boot_path = PathBuf::from(&format!("/boot/config-{uname_r}"));
if boot_path.exists() {
return Ok(Some(Config { path: boot_path }));
}
Ok(None)
}
pub fn require_config<P: AsRef<Path>>(
default_path: Option<P>,
) -> Result<Config, RequireConfigError> {
locate_config(default_path)?.ok_or(RequireConfigError::NotFound)
}
pub fn find_line(
entry_name: &str,
config_reader: impl BufRead,
) -> Result<String, error::FindLineError> {
if is_config_entry_name_valid(entry_name) {
let name = entry_name.trim();
let regex_is_not_set = Regex::new(&format!(r"^# {} is not set", regex::escape(name)))?;
let regex_is_set = Regex::new(&format!(r"^{}=.*$", regex::escape(name)))?;
for line in config_reader.lines() {
let line = line?;
let line = line.trim();
if regex_is_not_set.find(line).is_some() || regex_is_set.find(line).is_some() {
return Ok(line.to_string());
}
}
}
Err(FindLineError::EntryIsMissing(entry_name.to_string()))
}
pub fn find_value(
entry_name: &str,
config_reader: impl BufRead,
) -> Result<String, error::FindValueError> {
let line = find_line(entry_name, config_reader)?;
if line.starts_with("#") {
return Ok(line);
}
let split_line = line.split("=").collect::<Vec<&str>>();
if split_line.len() != 2 {
return Err(FindValueError::FailedToParseLine(line));
}
Ok(split_line
.get(1)
.expect("vec should have exactly 2 items")
.to_string())
}
fn is_config_entry_name_valid(name: &str) -> bool {
static VALID_CONFIG_ENTRY_NAME_REGEX: LazyLock<Regex> = std::sync::LazyLock::new(|| {
Regex::new(r"^CONFIG_[A-Z0-9_]+$").expect("hardcoded regex should be valid")
});
VALID_CONFIG_ENTRY_NAME_REGEX.is_match(name)
}
fn get_linux_kernel_version() -> Result<String, GetKernelVersionError> {
let uname = nix::sys::utsname::uname().map_err(GetKernelVersionError::UnameError)?;
Ok(uname
.release()
.to_str()
.ok_or(GetKernelVersionError::ReleaseMissingFromUname)?
.to_owned())
}