##// END OF EJS Templates
hg-core: add basic config module...
Raphaël Gomès -
r46803:95d6f31e default
parent child Browse files
Show More
@@ -0,0 +1,14 b''
1 // config.rs
2 //
3 // Copyright 2020
4 // Valentin Gatien-Baron,
5 // Raphaël Gomès <rgomes@octobus.net>
6 //
7 // This software may be used and distributed according to the terms of the
8 // GNU General Public License version 2 or any later version.
9
10 //! Mercurial config parsing and interfaces.
11
12 mod config;
13 mod layer;
14 pub use config::Config;
@@ -0,0 +1,197 b''
1 // config.rs
2 //
3 // Copyright 2020
4 // Valentin Gatien-Baron,
5 // Raphaël Gomès <rgomes@octobus.net>
6 //
7 // This software may be used and distributed according to the terms of the
8 // GNU General Public License version 2 or any later version.
9
10 use super::layer;
11 use crate::config::layer::{ConfigError, ConfigLayer, ConfigValue};
12 use std::path::PathBuf;
13
14 use crate::operations::find_root;
15 use crate::utils::files::read_whole_file;
16
17 /// Holds the config values for the current repository
18 /// TODO update this docstring once we support more sources
19 pub struct Config {
20 layers: Vec<layer::ConfigLayer>,
21 }
22
23 impl std::fmt::Debug for Config {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 for (index, layer) in self.layers.iter().rev().enumerate() {
26 write!(
27 f,
28 "==== Layer {} (trusted: {}) ====\n{:?}",
29 index, layer.trusted, layer
30 )?;
31 }
32 Ok(())
33 }
34 }
35
36 pub enum ConfigSource {
37 /// Absolute path to a config file
38 AbsPath(PathBuf),
39 /// Already parsed (from the CLI, env, Python resources, etc.)
40 Parsed(layer::ConfigLayer),
41 }
42
43 pub fn parse_bool(v: &[u8]) -> Option<bool> {
44 match v.to_ascii_lowercase().as_slice() {
45 b"1" | b"yes" | b"true" | b"on" | b"always" => Some(true),
46 b"0" | b"no" | b"false" | b"off" | b"never" => Some(false),
47 _ => None,
48 }
49 }
50
51 impl Config {
52 /// Loads in order, which means that the precedence is the same
53 /// as the order of `sources`.
54 pub fn load_from_explicit_sources(
55 sources: Vec<ConfigSource>,
56 ) -> Result<Self, ConfigError> {
57 let mut layers = vec![];
58
59 for source in sources.into_iter() {
60 match source {
61 ConfigSource::Parsed(c) => layers.push(c),
62 ConfigSource::AbsPath(c) => {
63 // TODO check if it should be trusted
64 // mercurial/ui.py:427
65 let data = match read_whole_file(&c) {
66 Err(_) => continue, // same as the python code
67 Ok(data) => data,
68 };
69 layers.extend(ConfigLayer::parse(&c, &data)?)
70 }
71 }
72 }
73
74 Ok(Config { layers })
75 }
76
77 /// Loads the local config. In a future version, this will also load the
78 /// `$HOME/.hgrc` and more to mirror the Python implementation.
79 pub fn load() -> Result<Self, ConfigError> {
80 let root = find_root().unwrap();
81 Ok(Self::load_from_explicit_sources(vec![
82 ConfigSource::AbsPath(root.join(".hg/hgrc")),
83 ])?)
84 }
85
86 /// Returns an `Err` if the first value found is not a valid boolean.
87 /// Otherwise, returns an `Ok(option)`, where `option` is the boolean if
88 /// found, or `None`.
89 pub fn get_option(
90 &self,
91 section: &[u8],
92 item: &[u8],
93 ) -> Result<Option<bool>, ConfigError> {
94 match self.get_inner(&section, &item) {
95 Some((layer, v)) => match parse_bool(&v.bytes) {
96 Some(b) => Ok(Some(b)),
97 None => Err(ConfigError::Parse {
98 origin: layer.origin.to_owned(),
99 line: v.line,
100 bytes: v.bytes.to_owned(),
101 }),
102 },
103 None => Ok(None),
104 }
105 }
106
107 /// Returns the corresponding boolean in the config. Returns `Ok(false)`
108 /// if the value is not found, an `Err` if it's not a valid boolean.
109 pub fn get_bool(
110 &self,
111 section: &[u8],
112 item: &[u8],
113 ) -> Result<bool, ConfigError> {
114 Ok(self.get_option(section, item)?.unwrap_or(false))
115 }
116
117 /// Returns the raw value bytes of the first one found, or `None`.
118 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&[u8]> {
119 self.get_inner(section, item)
120 .map(|(_, value)| value.bytes.as_ref())
121 }
122
123 /// Returns the layer and the value of the first one found, or `None`.
124 fn get_inner(
125 &self,
126 section: &[u8],
127 item: &[u8],
128 ) -> Option<(&ConfigLayer, &ConfigValue)> {
129 for layer in self.layers.iter().rev() {
130 if !layer.trusted {
131 continue;
132 }
133 if let Some(v) = layer.get(&section, &item) {
134 return Some((&layer, v));
135 }
136 }
137 None
138 }
139
140 /// Get raw values bytes from all layers (even untrusted ones) in order
141 /// of precedence.
142 #[cfg(test)]
143 fn get_all(&self, section: &[u8], item: &[u8]) -> Vec<&[u8]> {
144 let mut res = vec![];
145 for layer in self.layers.iter().rev() {
146 if let Some(v) = layer.get(&section, &item) {
147 res.push(v.bytes.as_ref());
148 }
149 }
150 res
151 }
152 }
153
154 #[cfg(test)]
155 mod tests {
156 use super::*;
157 use pretty_assertions::assert_eq;
158 use std::fs::File;
159 use std::io::Write;
160
161 #[test]
162 fn test_include_layer_ordering() {
163 let tmpdir = tempfile::tempdir().unwrap();
164 let tmpdir_path = tmpdir.path();
165 let mut included_file =
166 File::create(&tmpdir_path.join("included.rc")).unwrap();
167
168 included_file.write_all(b"[section]\nitem=value1").unwrap();
169 let base_config_path = tmpdir_path.join("base.rc");
170 let mut config_file = File::create(&base_config_path).unwrap();
171 let data =
172 b"[section]\nitem=value0\n%include included.rc\nitem=value2";
173 config_file.write_all(data).unwrap();
174
175 let sources = vec![ConfigSource::AbsPath(base_config_path)];
176 let config = Config::load_from_explicit_sources(sources)
177 .expect("expected valid config");
178
179 dbg!(&config);
180
181 let (_, value) = config.get_inner(b"section", b"item").unwrap();
182 assert_eq!(
183 value,
184 &ConfigValue {
185 bytes: b"value2".to_vec(),
186 line: Some(4)
187 }
188 );
189
190 let value = config.get(b"section", b"item").unwrap();
191 assert_eq!(value, b"value2",);
192 assert_eq!(
193 config.get_all(b"section", b"item"),
194 [b"value2", b"value1", b"value0"]
195 );
196 }
197 }
@@ -0,0 +1,268 b''
1 // layer.rs
2 //
3 // Copyright 2020
4 // Valentin Gatien-Baron,
5 // Raphaël Gomès <rgomes@octobus.net>
6 //
7 // This software may be used and distributed according to the terms of the
8 // GNU General Public License version 2 or any later version.
9
10 use crate::utils::files::{
11 get_bytes_from_path, get_path_from_bytes, read_whole_file,
12 };
13 use format_bytes::format_bytes;
14 use lazy_static::lazy_static;
15 use regex::bytes::Regex;
16 use std::collections::HashMap;
17 use std::io;
18 use std::path::{Path, PathBuf};
19
20 lazy_static! {
21 static ref SECTION_RE: Regex = make_regex(r"^\[([^\[]+)\]");
22 static ref ITEM_RE: Regex = make_regex(r"^([^=\s][^=]*?)\s*=\s*((.*\S)?)");
23 /// Continuation whitespace
24 static ref CONT_RE: Regex = make_regex(r"^\s+(\S|\S.*\S)\s*$");
25 static ref EMPTY_RE: Regex = make_regex(r"^(;|#|\s*$)");
26 static ref COMMENT_RE: Regex = make_regex(r"^(;|#)");
27 /// A directive that allows for removing previous entries
28 static ref UNSET_RE: Regex = make_regex(r"^%unset\s+(\S+)");
29 /// A directive that allows for including other config files
30 static ref INCLUDE_RE: Regex = make_regex(r"^%include\s+(\S|\S.*\S)\s*$");
31 }
32
33 /// All config values separated by layers of precedence.
34 /// Each config source may be split in multiple layers if `%include` directives
35 /// are used.
36 /// TODO detail the general precedence
37 #[derive(Clone)]
38 pub struct ConfigLayer {
39 /// Mapping of the sections to their items
40 sections: HashMap<Vec<u8>, ConfigItem>,
41 /// All sections (and their items/values) in a layer share the same origin
42 pub origin: ConfigOrigin,
43 /// Whether this layer comes from a trusted user or group
44 pub trusted: bool,
45 }
46
47 impl ConfigLayer {
48 pub fn new(origin: ConfigOrigin) -> Self {
49 ConfigLayer {
50 sections: HashMap::new(),
51 trusted: true, // TODO check
52 origin,
53 }
54 }
55
56 /// Add an entry to the config, overwriting the old one if already present.
57 pub fn add(
58 &mut self,
59 section: Vec<u8>,
60 item: Vec<u8>,
61 value: Vec<u8>,
62 line: Option<usize>,
63 ) {
64 self.sections
65 .entry(section)
66 .or_insert_with(|| HashMap::new())
67 .insert(item, ConfigValue { bytes: value, line });
68 }
69
70 /// Returns the config value in `<section>.<item>` if it exists
71 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&ConfigValue> {
72 Some(self.sections.get(section)?.get(item)?)
73 }
74
75 pub fn is_empty(&self) -> bool {
76 self.sections.is_empty()
77 }
78
79 /// Returns a `Vec` of layers in order of precedence (so, in read order),
80 /// recursively parsing the `%include` directives if any.
81 pub fn parse(src: &Path, data: &[u8]) -> Result<Vec<Self>, ConfigError> {
82 let mut layers = vec![];
83
84 // Discard byte order mark if any
85 let data = if data.starts_with(b"\xef\xbb\xbf") {
86 &data[3..]
87 } else {
88 data
89 };
90
91 // TODO check if it's trusted
92 let mut current_layer = Self::new(ConfigOrigin::File(src.to_owned()));
93
94 let mut lines_iter =
95 data.split(|b| *b == b'\n').enumerate().peekable();
96 let mut section = b"".to_vec();
97
98 while let Some((index, bytes)) = lines_iter.next() {
99 if let Some(m) = INCLUDE_RE.captures(&bytes) {
100 let filename_bytes = &m[1];
101 let filename_to_include = get_path_from_bytes(&filename_bytes);
102 match read_include(&src, &filename_to_include) {
103 (include_src, Ok(data)) => {
104 layers.push(current_layer);
105 layers.extend(Self::parse(&include_src, &data)?);
106 current_layer =
107 Self::new(ConfigOrigin::File(src.to_owned()));
108 }
109 (_, Err(e)) => {
110 return Err(ConfigError::IncludeError {
111 path: filename_to_include.to_owned(),
112 io_error: e,
113 })
114 }
115 }
116 } else if let Some(_) = EMPTY_RE.captures(&bytes) {
117 } else if let Some(m) = SECTION_RE.captures(&bytes) {
118 section = m[1].to_vec();
119 } else if let Some(m) = ITEM_RE.captures(&bytes) {
120 let item = m[1].to_vec();
121 let mut value = m[2].to_vec();
122 loop {
123 match lines_iter.peek() {
124 None => break,
125 Some((_, v)) => {
126 if let Some(_) = COMMENT_RE.captures(&v) {
127 } else if let Some(_) = CONT_RE.captures(&v) {
128 value.extend(b"\n");
129 value.extend(&m[1]);
130 } else {
131 break;
132 }
133 }
134 };
135 lines_iter.next();
136 }
137 current_layer.add(
138 section.clone(),
139 item,
140 value,
141 Some(index + 1),
142 );
143 } else if let Some(m) = UNSET_RE.captures(&bytes) {
144 if let Some(map) = current_layer.sections.get_mut(&section) {
145 map.remove(&m[1]);
146 }
147 } else {
148 return Err(ConfigError::Parse {
149 origin: ConfigOrigin::File(src.to_owned()),
150 line: Some(index + 1),
151 bytes: bytes.to_owned(),
152 });
153 }
154 }
155 if !current_layer.is_empty() {
156 layers.push(current_layer);
157 }
158 Ok(layers)
159 }
160 }
161
162 impl std::fmt::Debug for ConfigLayer {
163 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164 let mut sections: Vec<_> = self.sections.iter().collect();
165 sections.sort_by(|e0, e1| e0.0.cmp(e1.0));
166
167 for (section, items) in sections.into_iter() {
168 let mut items: Vec<_> = items.into_iter().collect();
169 items.sort_by(|e0, e1| e0.0.cmp(e1.0));
170
171 for (item, config_entry) in items {
172 writeln!(
173 f,
174 "{}",
175 String::from_utf8_lossy(&format_bytes!(
176 b"{}.{}={} # {}",
177 section,
178 item,
179 &config_entry.bytes,
180 &self.origin.to_bytes(),
181 ))
182 )?
183 }
184 }
185 Ok(())
186 }
187 }
188
189 /// Mapping of section item to value.
190 /// In the following:
191 /// ```text
192 /// [ui]
193 /// paginate=no
194 /// ```
195 /// "paginate" is the section item and "no" the value.
196 pub type ConfigItem = HashMap<Vec<u8>, ConfigValue>;
197
198 #[derive(Clone, Debug, PartialEq)]
199 pub struct ConfigValue {
200 /// The raw bytes of the value (be it from the CLI, env or from a file)
201 pub bytes: Vec<u8>,
202 /// Only present if the value comes from a file, 1-indexed.
203 pub line: Option<usize>,
204 }
205
206 #[derive(Clone, Debug)]
207 pub enum ConfigOrigin {
208 /// The value comes from a configuration file
209 File(PathBuf),
210 /// The value comes from the environment like `$PAGER` or `$EDITOR`
211 Environment(Vec<u8>),
212 /* TODO cli
213 * TODO defaults (configitems.py)
214 * TODO extensions
215 * TODO Python resources?
216 * Others? */
217 }
218
219 impl ConfigOrigin {
220 /// TODO use some kind of dedicated trait?
221 pub fn to_bytes(&self) -> Vec<u8> {
222 match self {
223 ConfigOrigin::File(p) => get_bytes_from_path(p),
224 ConfigOrigin::Environment(e) => e.to_owned(),
225 }
226 }
227 }
228
229 #[derive(Debug)]
230 pub enum ConfigError {
231 Parse {
232 origin: ConfigOrigin,
233 line: Option<usize>,
234 bytes: Vec<u8>,
235 },
236 /// Failed to include a sub config file
237 IncludeError {
238 path: PathBuf,
239 io_error: std::io::Error,
240 },
241 /// Any IO error that isn't expected
242 IO(std::io::Error),
243 }
244
245 impl From<std::io::Error> for ConfigError {
246 fn from(e: std::io::Error) -> Self {
247 Self::IO(e)
248 }
249 }
250
251 fn make_regex(pattern: &'static str) -> Regex {
252 Regex::new(pattern).expect("expected a valid regex")
253 }
254
255 /// Includes are relative to the file they're defined in, unless they're
256 /// absolute.
257 fn read_include(
258 old_src: &Path,
259 new_src: &Path,
260 ) -> (PathBuf, io::Result<Vec<u8>>) {
261 if new_src.is_absolute() {
262 (new_src.to_path_buf(), read_whole_file(&new_src))
263 } else {
264 let dir = old_src.parent().unwrap();
265 let new_src = dir.join(&new_src);
266 (new_src.to_owned(), read_whole_file(&new_src))
267 }
268 }
@@ -26,6 +26,7 b' pub mod matchers;'
26 pub mod repo;
26 pub mod repo;
27 pub mod revlog;
27 pub mod revlog;
28 pub use revlog::*;
28 pub use revlog::*;
29 pub mod config;
29 pub mod operations;
30 pub mod operations;
30 pub mod utils;
31 pub mod utils;
31
32
@@ -18,6 +18,7 b' use lazy_static::lazy_static;'
18 use same_file::is_same_file;
18 use same_file::is_same_file;
19 use std::borrow::{Cow, ToOwned};
19 use std::borrow::{Cow, ToOwned};
20 use std::fs::Metadata;
20 use std::fs::Metadata;
21 use std::io::Read;
21 use std::iter::FusedIterator;
22 use std::iter::FusedIterator;
22 use std::ops::Deref;
23 use std::ops::Deref;
23 use std::path::{Path, PathBuf};
24 use std::path::{Path, PathBuf};
@@ -308,6 +309,17 b' pub fn relativize_path(path: &HgPath, cw'
308 }
309 }
309 }
310 }
310
311
312 /// Reads a file in one big chunk instead of doing multiple reads
313 pub fn read_whole_file(filepath: &Path) -> std::io::Result<Vec<u8>> {
314 let mut file = std::fs::File::open(filepath)?;
315 let size = file.metadata()?.len();
316
317 let mut res = vec![0; size as usize];
318 file.read_exact(&mut res)?;
319
320 Ok(res)
321 }
322
311 #[cfg(test)]
323 #[cfg(test)]
312 mod tests {
324 mod tests {
313 use super::*;
325 use super::*;
General Comments 0
You need to be logged in to leave comments. Login now