layer.rs
323 lines
| 10.9 KiB
| application/rls-services+xml
|
RustLexer
Raphaël Gomès
|
r46803 | // layer.rs | ||
// | ||||
// Copyright 2020 | ||||
// Valentin Gatien-Baron, | ||||
// Raphaël Gomès <rgomes@octobus.net> | ||||
// | ||||
// This software may be used and distributed according to the terms of the | ||||
// GNU General Public License version 2 or any later version. | ||||
Simon Sapin
|
r47469 | use crate::errors::HgError; | ||
Pulkit Goyal
|
r48199 | use crate::exit_codes::CONFIG_PARSE_ERROR_ABORT; | ||
Simon Sapin
|
r47210 | use crate::utils::files::{get_bytes_from_path, get_path_from_bytes}; | ||
Simon Sapin
|
r47465 | use format_bytes::{format_bytes, write_bytes, DisplayBytes}; | ||
Raphaël Gomès
|
r46803 | use lazy_static::lazy_static; | ||
use regex::bytes::Regex; | ||||
use std::collections::HashMap; | ||||
use std::path::{Path, PathBuf}; | ||||
lazy_static! { | ||||
static ref SECTION_RE: Regex = make_regex(r"^\[([^\[]+)\]"); | ||||
static ref ITEM_RE: Regex = make_regex(r"^([^=\s][^=]*?)\s*=\s*((.*\S)?)"); | ||||
/// Continuation whitespace | ||||
static ref CONT_RE: Regex = make_regex(r"^\s+(\S|\S.*\S)\s*$"); | ||||
static ref EMPTY_RE: Regex = make_regex(r"^(;|#|\s*$)"); | ||||
static ref COMMENT_RE: Regex = make_regex(r"^(;|#)"); | ||||
/// A directive that allows for removing previous entries | ||||
static ref UNSET_RE: Regex = make_regex(r"^%unset\s+(\S+)"); | ||||
/// A directive that allows for including other config files | ||||
static ref INCLUDE_RE: Regex = make_regex(r"^%include\s+(\S|\S.*\S)\s*$"); | ||||
} | ||||
/// All config values separated by layers of precedence. | ||||
/// Each config source may be split in multiple layers if `%include` directives | ||||
/// are used. | ||||
/// TODO detail the general precedence | ||||
#[derive(Clone)] | ||||
pub struct ConfigLayer { | ||||
/// Mapping of the sections to their items | ||||
sections: HashMap<Vec<u8>, ConfigItem>, | ||||
/// All sections (and their items/values) in a layer share the same origin | ||||
pub origin: ConfigOrigin, | ||||
/// Whether this layer comes from a trusted user or group | ||||
pub trusted: bool, | ||||
} | ||||
impl ConfigLayer { | ||||
pub fn new(origin: ConfigOrigin) -> Self { | ||||
ConfigLayer { | ||||
sections: HashMap::new(), | ||||
trusted: true, // TODO check | ||||
origin, | ||||
} | ||||
} | ||||
Simon Sapin
|
r47254 | /// Parse `--config` CLI arguments and return a layer if there’s any | ||
pub(crate) fn parse_cli_args( | ||||
cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>, | ||||
) -> Result<Option<Self>, ConfigError> { | ||||
fn parse_one(arg: &[u8]) -> Option<(Vec<u8>, Vec<u8>, Vec<u8>)> { | ||||
use crate::utils::SliceExt; | ||||
Simon Sapin
|
r47255 | let (section_and_item, value) = arg.split_2(b'=')?; | ||
let (section, item) = section_and_item.trim().split_2(b'.')?; | ||||
Simon Sapin
|
r47254 | Some(( | ||
section.to_owned(), | ||||
item.to_owned(), | ||||
value.trim().to_owned(), | ||||
)) | ||||
} | ||||
let mut layer = Self::new(ConfigOrigin::CommandLine); | ||||
for arg in cli_config_args { | ||||
let arg = arg.as_ref(); | ||||
if let Some((section, item, value)) = parse_one(arg) { | ||||
layer.add(section, item, value, None); | ||||
} else { | ||||
Pulkit Goyal
|
r48199 | Err(HgError::abort( | ||
format!( | ||||
"abort: malformed --config option: '{}' \ | ||||
Simon Sapin
|
r47254 | (use --config section.name=value)", | ||
Pulkit Goyal
|
r48199 | String::from_utf8_lossy(arg), | ||
), | ||||
CONFIG_PARSE_ERROR_ABORT, | ||||
))? | ||||
Simon Sapin
|
r47254 | } | ||
} | ||||
if layer.sections.is_empty() { | ||||
Ok(None) | ||||
} else { | ||||
Ok(Some(layer)) | ||||
} | ||||
} | ||||
Simon Sapin
|
r47215 | /// Returns whether this layer comes from `--config` CLI arguments | ||
pub(crate) fn is_from_command_line(&self) -> bool { | ||||
if let ConfigOrigin::CommandLine = self.origin { | ||||
true | ||||
} else { | ||||
false | ||||
} | ||||
} | ||||
Raphaël Gomès
|
r46803 | /// Add an entry to the config, overwriting the old one if already present. | ||
pub fn add( | ||||
&mut self, | ||||
section: Vec<u8>, | ||||
item: Vec<u8>, | ||||
value: Vec<u8>, | ||||
line: Option<usize>, | ||||
) { | ||||
self.sections | ||||
.entry(section) | ||||
.or_insert_with(|| HashMap::new()) | ||||
.insert(item, ConfigValue { bytes: value, line }); | ||||
} | ||||
/// Returns the config value in `<section>.<item>` if it exists | ||||
pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&ConfigValue> { | ||||
Some(self.sections.get(section)?.get(item)?) | ||||
} | ||||
Simon Sapin
|
r47467 | /// Returns the keys defined in the given section | ||
pub fn iter_keys(&self, section: &[u8]) -> impl Iterator<Item = &[u8]> { | ||||
self.sections | ||||
.get(section) | ||||
.into_iter() | ||||
.flat_map(|section| section.keys().map(|vec| &**vec)) | ||||
} | ||||
Raphaël Gomès
|
r46803 | pub fn is_empty(&self) -> bool { | ||
self.sections.is_empty() | ||||
} | ||||
/// Returns a `Vec` of layers in order of precedence (so, in read order), | ||||
/// recursively parsing the `%include` directives if any. | ||||
pub fn parse(src: &Path, data: &[u8]) -> Result<Vec<Self>, ConfigError> { | ||||
let mut layers = vec![]; | ||||
// Discard byte order mark if any | ||||
let data = if data.starts_with(b"\xef\xbb\xbf") { | ||||
&data[3..] | ||||
} else { | ||||
data | ||||
}; | ||||
// TODO check if it's trusted | ||||
let mut current_layer = Self::new(ConfigOrigin::File(src.to_owned())); | ||||
let mut lines_iter = | ||||
data.split(|b| *b == b'\n').enumerate().peekable(); | ||||
let mut section = b"".to_vec(); | ||||
while let Some((index, bytes)) = lines_iter.next() { | ||||
Simon Sapin
|
r47469 | let line = Some(index + 1); | ||
Raphaël Gomès
|
r46803 | if let Some(m) = INCLUDE_RE.captures(&bytes) { | ||
let filename_bytes = &m[1]; | ||||
Simon Sapin
|
r47476 | let filename_bytes = crate::utils::expand_vars(filename_bytes); | ||
Simon Sapin
|
r47211 | // `Path::parent` only fails for the root directory, | ||
Simon Sapin
|
r47215 | // which `src` can’t be since we’ve managed to open it as a | ||
// file. | ||||
Simon Sapin
|
r47211 | let dir = src | ||
.parent() | ||||
.expect("Path::parent fail on a file we’ve read"); | ||||
Simon Sapin
|
r47215 | // `Path::join` with an absolute argument correctly ignores the | ||
// base path | ||||
Simon Sapin
|
r47211 | let filename = dir.join(&get_path_from_bytes(&filename_bytes)); | ||
Simon Sapin
|
r47477 | match std::fs::read(&filename) { | ||
Ok(data) => { | ||||
layers.push(current_layer); | ||||
layers.extend(Self::parse(&filename, &data)?); | ||||
current_layer = | ||||
Self::new(ConfigOrigin::File(src.to_owned())); | ||||
Simon Sapin
|
r47469 | } | ||
Simon Sapin
|
r47477 | Err(error) => { | ||
if error.kind() != std::io::ErrorKind::NotFound { | ||||
return Err(ConfigParseError { | ||||
origin: ConfigOrigin::File(src.to_owned()), | ||||
line, | ||||
message: format_bytes!( | ||||
b"cannot include {} ({})", | ||||
filename_bytes, | ||||
format_bytes::Utf8(error) | ||||
), | ||||
} | ||||
.into()); | ||||
} | ||||
} | ||||
} | ||||
Raphaël Gomès
|
r46803 | } else if let Some(_) = EMPTY_RE.captures(&bytes) { | ||
} else if let Some(m) = SECTION_RE.captures(&bytes) { | ||||
section = m[1].to_vec(); | ||||
} else if let Some(m) = ITEM_RE.captures(&bytes) { | ||||
let item = m[1].to_vec(); | ||||
let mut value = m[2].to_vec(); | ||||
loop { | ||||
match lines_iter.peek() { | ||||
None => break, | ||||
Some((_, v)) => { | ||||
if let Some(_) = COMMENT_RE.captures(&v) { | ||||
} else if let Some(_) = CONT_RE.captures(&v) { | ||||
value.extend(b"\n"); | ||||
value.extend(&m[1]); | ||||
} else { | ||||
break; | ||||
} | ||||
} | ||||
}; | ||||
lines_iter.next(); | ||||
} | ||||
Simon Sapin
|
r47469 | current_layer.add(section.clone(), item, value, line); | ||
Raphaël Gomès
|
r46803 | } else if let Some(m) = UNSET_RE.captures(&bytes) { | ||
if let Some(map) = current_layer.sections.get_mut(§ion) { | ||||
map.remove(&m[1]); | ||||
} | ||||
} else { | ||||
Simon Sapin
|
r47465 | let message = if bytes.starts_with(b" ") { | ||
format_bytes!(b"unexpected leading whitespace: {}", bytes) | ||||
} else { | ||||
bytes.to_owned() | ||||
}; | ||||
Simon Sapin
|
r47176 | return Err(ConfigParseError { | ||
Raphaël Gomès
|
r46803 | origin: ConfigOrigin::File(src.to_owned()), | ||
Simon Sapin
|
r47469 | line, | ||
Simon Sapin
|
r47465 | message, | ||
Simon Sapin
|
r47176 | } | ||
.into()); | ||||
Raphaël Gomès
|
r46803 | } | ||
} | ||||
if !current_layer.is_empty() { | ||||
layers.push(current_layer); | ||||
} | ||||
Ok(layers) | ||||
} | ||||
} | ||||
Simon Sapin
|
r47249 | impl DisplayBytes for ConfigLayer { | ||
fn display_bytes( | ||||
&self, | ||||
out: &mut dyn std::io::Write, | ||||
) -> std::io::Result<()> { | ||||
Raphaël Gomès
|
r46803 | let mut sections: Vec<_> = self.sections.iter().collect(); | ||
sections.sort_by(|e0, e1| e0.0.cmp(e1.0)); | ||||
for (section, items) in sections.into_iter() { | ||||
let mut items: Vec<_> = items.into_iter().collect(); | ||||
items.sort_by(|e0, e1| e0.0.cmp(e1.0)); | ||||
for (item, config_entry) in items { | ||||
Simon Sapin
|
r47249 | write_bytes!( | ||
out, | ||||
b"{}.{}={} # {}\n", | ||||
section, | ||||
item, | ||||
&config_entry.bytes, | ||||
&self.origin, | ||||
Raphaël Gomès
|
r46803 | )? | ||
} | ||||
} | ||||
Ok(()) | ||||
} | ||||
} | ||||
/// Mapping of section item to value. | ||||
/// In the following: | ||||
/// ```text | ||||
/// [ui] | ||||
/// paginate=no | ||||
/// ``` | ||||
/// "paginate" is the section item and "no" the value. | ||||
pub type ConfigItem = HashMap<Vec<u8>, ConfigValue>; | ||||
#[derive(Clone, Debug, PartialEq)] | ||||
pub struct ConfigValue { | ||||
/// The raw bytes of the value (be it from the CLI, env or from a file) | ||||
pub bytes: Vec<u8>, | ||||
/// Only present if the value comes from a file, 1-indexed. | ||||
pub line: Option<usize>, | ||||
} | ||||
#[derive(Clone, Debug)] | ||||
pub enum ConfigOrigin { | ||||
Simon Sapin
|
r47215 | /// From a configuration file | ||
Raphaël Gomès
|
r46803 | File(PathBuf), | ||
Simon Sapin
|
r47215 | /// From a `--config` CLI argument | ||
CommandLine, | ||||
/// From environment variables like `$PAGER` or `$EDITOR` | ||||
Raphaël Gomès
|
r46803 | Environment(Vec<u8>), | ||
/* TODO cli | ||||
* TODO defaults (configitems.py) | ||||
* TODO extensions | ||||
* TODO Python resources? | ||||
* Others? */ | ||||
} | ||||
Simon Sapin
|
r47249 | impl DisplayBytes for ConfigOrigin { | ||
fn display_bytes( | ||||
&self, | ||||
out: &mut dyn std::io::Write, | ||||
) -> std::io::Result<()> { | ||||
Raphaël Gomès
|
r46803 | match self { | ||
Simon Sapin
|
r47249 | ConfigOrigin::File(p) => out.write_all(&get_bytes_from_path(p)), | ||
ConfigOrigin::CommandLine => out.write_all(b"--config"), | ||||
ConfigOrigin::Environment(e) => write_bytes!(out, b"${}", e), | ||||
Raphaël Gomès
|
r46803 | } | ||
} | ||||
} | ||||
Simon Sapin
|
r47176 | #[derive(Debug)] | ||
pub struct ConfigParseError { | ||||
pub origin: ConfigOrigin, | ||||
pub line: Option<usize>, | ||||
Simon Sapin
|
r47465 | pub message: Vec<u8>, | ||
Simon Sapin
|
r47176 | } | ||
Simon Sapin
|
r47164 | #[derive(Debug, derive_more::From)] | ||
Raphaël Gomès
|
r46803 | pub enum ConfigError { | ||
Simon Sapin
|
r47176 | Parse(ConfigParseError), | ||
Other(HgError), | ||||
Raphaël Gomès
|
r46803 | } | ||
fn make_regex(pattern: &'static str) -> Regex { | ||||
Regex::new(pattern).expect("expected a valid regex") | ||||
} | ||||