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