1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
/*
* SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
* SPDX-License-Identifier: BSD-2-Clause
*/
//! Abstract definitions for data parsing backends.
use std::borrow::ToOwned;
use std::ffi::{CStr, CString};
use std::fs;
use std::io::{self, Read};
use anyhow::{anyhow, Context};
use encoding::label as enc_label;
use encoding::DecoderTrap;
use once_cell::sync::Lazy;
use crate::defs::{ConfgetError, Config, FileData};
#[cfg(feature = "ini-nom")]
pub mod ini_nom;
#[cfg(feature = "ini-regex")]
pub mod ini_re;
/// The variables read from the input data and the first section name.
///
/// The [`Backend::read_file`] method returns two values: a mapping of
/// section names to variable => value mappings, and the name of the first
/// section encountered in the input data. The latter is particularly
/// useful for the "ini" backend where `confget` will behave differently
/// depending on the [`Config::section_override`][`crate::defs::Config::section_override`]
/// setting.
pub type DataRead = (FileData, String);
/// A backend that implements parsing a specific type of configuration data.
///
/// A `Backend` object may be constructed manually using [`Backend::from_config`],
/// but it may be preferable to use the [`get_backend`][`crate::get_backend`] or
/// [`read_ini_file`][`crate::read_ini_file`] functions instead.
pub trait Backend<'cfg> {
/// Initialize a backend object, performing config checks if necessary.
///
/// # Errors
///
/// See the documentation of the individual backends.
fn from_config(config: &'cfg Config) -> Result<Self, ConfgetError>
where
Self: Sized;
/// Obtain and parse the input data in a backend-specific way.
///
/// # Errors
///
/// See the documentation of the individual backends.
fn read_file(&self) -> Result<DataRead, ConfgetError>;
}
/// The character encoding of the `LC_CTYPE` locale category at the time
/// the INI-style backend is initialized.
/// This is used as the default character encoding for the input file's data
/// unless it is overridden by the configuration.
static DEFAULT_ENCODING: Lazy<Result<String, String>> = Lazy::new(|| {
let langinfo_cstr = {
let empty_str = CString::from_vec_with_nul(vec![0])
.map_err(|err| format!("Could not build an empty C-style string: {err}"))?;
// SAFETY: `once_cell::sync` should prevent data races.
if unsafe { libc::setlocale(libc::LC_CTYPE, empty_str.as_c_str().as_ptr()) }.is_null() {
return Err("setlocale() failed".to_owned());
}
// SAFETY: `once_cell::sync` should prevent data races.
let langinfo_ptr = unsafe { libc::nl_langinfo(libc::CODESET) };
if langinfo_ptr.is_null() {
return Err("nl_langinfo(CODESET) returned a null pointer".to_owned());
}
// SAFETY: we just validated it.
unsafe { CStr::from_ptr(langinfo_ptr) }
};
match langinfo_cstr.to_str() {
Ok(enc_ref) => {
if enc_ref.is_empty() {
Err("nl_langinfo(CODESET) returned an empty string".to_owned())
} else {
Ok(enc_ref.to_owned())
}
}
Err(err) => Err(format!(
"nl_langinfo(CODESET) returned a non-UTF-8 string: {err}"
)),
}
});
/// Read all the input lines, either from the standard input or from a file.
///
/// # Errors
/// I/O or decoding errors reading the input file (or stream).
fn get_file_lines(filename: &str, encoding: &str) -> Result<Vec<String>, ConfgetError> {
let encoding_name = if encoding.is_empty() {
DEFAULT_ENCODING
.as_ref()
.map_err(|err| ConfgetError::Internal((*err).clone()))?
} else {
encoding
};
let enc_ref = enc_label::encoding_from_whatwg_label(encoding_name)
.with_context(|| {
format!(
"Unsupported encoding '{escaped}'",
escaped = encoding_name.escape_debug()
)
})
.map_err(ConfgetError::ReadData)?;
let raw = if filename == "-" {
let mut buf = vec![];
io::stdin()
.lock()
.read_to_end(&mut buf)
.context("Could not read from the standard input")
.map_err(ConfgetError::ReadData)?;
buf
} else {
fs::read(filename)
.with_context(|| {
format!(
"Could not read from the '{escaped}' file",
escaped = filename.escape_debug()
)
})
.map_err(ConfgetError::ReadData)?
};
let contents = enc_ref.decode(&raw, DecoderTrap::Strict).map_err(|err| {
ConfgetError::ReadData(anyhow!(
"Could not decode the {} input bytes using the '{}' encoding: {}",
raw.len(),
encoding_name,
err
))
})?;
Ok(contents.lines().map(ToOwned::to_owned).collect())
}