##// END OF EJS Templates
rhg: Silently ignore missing files in config %include...
Simon Sapin -
r47477:84a3deca default
parent child Browse files
Show More
@@ -1,311 +1,319
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 10 use crate::errors::HgError;
11 11 use crate::utils::files::{get_bytes_from_path, get_path_from_bytes};
12 12 use format_bytes::{format_bytes, write_bytes, DisplayBytes};
13 13 use lazy_static::lazy_static;
14 14 use regex::bytes::Regex;
15 15 use std::collections::HashMap;
16 16 use std::path::{Path, PathBuf};
17 17
18 18 lazy_static! {
19 19 static ref SECTION_RE: Regex = make_regex(r"^\[([^\[]+)\]");
20 20 static ref ITEM_RE: Regex = make_regex(r"^([^=\s][^=]*?)\s*=\s*((.*\S)?)");
21 21 /// Continuation whitespace
22 22 static ref CONT_RE: Regex = make_regex(r"^\s+(\S|\S.*\S)\s*$");
23 23 static ref EMPTY_RE: Regex = make_regex(r"^(;|#|\s*$)");
24 24 static ref COMMENT_RE: Regex = make_regex(r"^(;|#)");
25 25 /// A directive that allows for removing previous entries
26 26 static ref UNSET_RE: Regex = make_regex(r"^%unset\s+(\S+)");
27 27 /// A directive that allows for including other config files
28 28 static ref INCLUDE_RE: Regex = make_regex(r"^%include\s+(\S|\S.*\S)\s*$");
29 29 }
30 30
31 31 /// All config values separated by layers of precedence.
32 32 /// Each config source may be split in multiple layers if `%include` directives
33 33 /// are used.
34 34 /// TODO detail the general precedence
35 35 #[derive(Clone)]
36 36 pub struct ConfigLayer {
37 37 /// Mapping of the sections to their items
38 38 sections: HashMap<Vec<u8>, ConfigItem>,
39 39 /// All sections (and their items/values) in a layer share the same origin
40 40 pub origin: ConfigOrigin,
41 41 /// Whether this layer comes from a trusted user or group
42 42 pub trusted: bool,
43 43 }
44 44
45 45 impl ConfigLayer {
46 46 pub fn new(origin: ConfigOrigin) -> Self {
47 47 ConfigLayer {
48 48 sections: HashMap::new(),
49 49 trusted: true, // TODO check
50 50 origin,
51 51 }
52 52 }
53 53
54 54 /// Parse `--config` CLI arguments and return a layer if there’s any
55 55 pub(crate) fn parse_cli_args(
56 56 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
57 57 ) -> Result<Option<Self>, ConfigError> {
58 58 fn parse_one(arg: &[u8]) -> Option<(Vec<u8>, Vec<u8>, Vec<u8>)> {
59 59 use crate::utils::SliceExt;
60 60
61 61 let (section_and_item, value) = arg.split_2(b'=')?;
62 62 let (section, item) = section_and_item.trim().split_2(b'.')?;
63 63 Some((
64 64 section.to_owned(),
65 65 item.to_owned(),
66 66 value.trim().to_owned(),
67 67 ))
68 68 }
69 69
70 70 let mut layer = Self::new(ConfigOrigin::CommandLine);
71 71 for arg in cli_config_args {
72 72 let arg = arg.as_ref();
73 73 if let Some((section, item, value)) = parse_one(arg) {
74 74 layer.add(section, item, value, None);
75 75 } else {
76 76 Err(HgError::abort(format!(
77 77 "abort: malformed --config option: '{}' \
78 78 (use --config section.name=value)",
79 79 String::from_utf8_lossy(arg),
80 80 )))?
81 81 }
82 82 }
83 83 if layer.sections.is_empty() {
84 84 Ok(None)
85 85 } else {
86 86 Ok(Some(layer))
87 87 }
88 88 }
89 89
90 90 /// Returns whether this layer comes from `--config` CLI arguments
91 91 pub(crate) fn is_from_command_line(&self) -> bool {
92 92 if let ConfigOrigin::CommandLine = self.origin {
93 93 true
94 94 } else {
95 95 false
96 96 }
97 97 }
98 98
99 99 /// Add an entry to the config, overwriting the old one if already present.
100 100 pub fn add(
101 101 &mut self,
102 102 section: Vec<u8>,
103 103 item: Vec<u8>,
104 104 value: Vec<u8>,
105 105 line: Option<usize>,
106 106 ) {
107 107 self.sections
108 108 .entry(section)
109 109 .or_insert_with(|| HashMap::new())
110 110 .insert(item, ConfigValue { bytes: value, line });
111 111 }
112 112
113 113 /// Returns the config value in `<section>.<item>` if it exists
114 114 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&ConfigValue> {
115 115 Some(self.sections.get(section)?.get(item)?)
116 116 }
117 117
118 118 /// Returns the keys defined in the given section
119 119 pub fn iter_keys(&self, section: &[u8]) -> impl Iterator<Item = &[u8]> {
120 120 self.sections
121 121 .get(section)
122 122 .into_iter()
123 123 .flat_map(|section| section.keys().map(|vec| &**vec))
124 124 }
125 125
126 126 pub fn is_empty(&self) -> bool {
127 127 self.sections.is_empty()
128 128 }
129 129
130 130 /// Returns a `Vec` of layers in order of precedence (so, in read order),
131 131 /// recursively parsing the `%include` directives if any.
132 132 pub fn parse(src: &Path, data: &[u8]) -> Result<Vec<Self>, ConfigError> {
133 133 let mut layers = vec![];
134 134
135 135 // Discard byte order mark if any
136 136 let data = if data.starts_with(b"\xef\xbb\xbf") {
137 137 &data[3..]
138 138 } else {
139 139 data
140 140 };
141 141
142 142 // TODO check if it's trusted
143 143 let mut current_layer = Self::new(ConfigOrigin::File(src.to_owned()));
144 144
145 145 let mut lines_iter =
146 146 data.split(|b| *b == b'\n').enumerate().peekable();
147 147 let mut section = b"".to_vec();
148 148
149 149 while let Some((index, bytes)) = lines_iter.next() {
150 150 let line = Some(index + 1);
151 151 if let Some(m) = INCLUDE_RE.captures(&bytes) {
152 152 let filename_bytes = &m[1];
153 153 let filename_bytes = crate::utils::expand_vars(filename_bytes);
154 154 // `Path::parent` only fails for the root directory,
155 155 // which `src` can’t be since we’ve managed to open it as a
156 156 // file.
157 157 let dir = src
158 158 .parent()
159 159 .expect("Path::parent fail on a file we’ve read");
160 160 // `Path::join` with an absolute argument correctly ignores the
161 161 // base path
162 162 let filename = dir.join(&get_path_from_bytes(&filename_bytes));
163 let data = std::fs::read(&filename).map_err(|io_error| {
164 ConfigParseError {
165 origin: ConfigOrigin::File(src.to_owned()),
166 line,
167 message: format_bytes!(
168 b"cannot include {} ({})",
169 filename_bytes,
170 format_bytes::Utf8(io_error)
171 ),
163 match std::fs::read(&filename) {
164 Ok(data) => {
165 layers.push(current_layer);
166 layers.extend(Self::parse(&filename, &data)?);
167 current_layer =
168 Self::new(ConfigOrigin::File(src.to_owned()));
172 169 }
173 })?;
174 layers.push(current_layer);
175 layers.extend(Self::parse(&filename, &data)?);
176 current_layer = Self::new(ConfigOrigin::File(src.to_owned()));
170 Err(error) => {
171 if error.kind() != std::io::ErrorKind::NotFound {
172 return Err(ConfigParseError {
173 origin: ConfigOrigin::File(src.to_owned()),
174 line,
175 message: format_bytes!(
176 b"cannot include {} ({})",
177 filename_bytes,
178 format_bytes::Utf8(error)
179 ),
180 }
181 .into());
182 }
183 }
184 }
177 185 } else if let Some(_) = EMPTY_RE.captures(&bytes) {
178 186 } else if let Some(m) = SECTION_RE.captures(&bytes) {
179 187 section = m[1].to_vec();
180 188 } else if let Some(m) = ITEM_RE.captures(&bytes) {
181 189 let item = m[1].to_vec();
182 190 let mut value = m[2].to_vec();
183 191 loop {
184 192 match lines_iter.peek() {
185 193 None => break,
186 194 Some((_, v)) => {
187 195 if let Some(_) = COMMENT_RE.captures(&v) {
188 196 } else if let Some(_) = CONT_RE.captures(&v) {
189 197 value.extend(b"\n");
190 198 value.extend(&m[1]);
191 199 } else {
192 200 break;
193 201 }
194 202 }
195 203 };
196 204 lines_iter.next();
197 205 }
198 206 current_layer.add(section.clone(), item, value, line);
199 207 } else if let Some(m) = UNSET_RE.captures(&bytes) {
200 208 if let Some(map) = current_layer.sections.get_mut(&section) {
201 209 map.remove(&m[1]);
202 210 }
203 211 } else {
204 212 let message = if bytes.starts_with(b" ") {
205 213 format_bytes!(b"unexpected leading whitespace: {}", bytes)
206 214 } else {
207 215 bytes.to_owned()
208 216 };
209 217 return Err(ConfigParseError {
210 218 origin: ConfigOrigin::File(src.to_owned()),
211 219 line,
212 220 message,
213 221 }
214 222 .into());
215 223 }
216 224 }
217 225 if !current_layer.is_empty() {
218 226 layers.push(current_layer);
219 227 }
220 228 Ok(layers)
221 229 }
222 230 }
223 231
224 232 impl DisplayBytes for ConfigLayer {
225 233 fn display_bytes(
226 234 &self,
227 235 out: &mut dyn std::io::Write,
228 236 ) -> std::io::Result<()> {
229 237 let mut sections: Vec<_> = self.sections.iter().collect();
230 238 sections.sort_by(|e0, e1| e0.0.cmp(e1.0));
231 239
232 240 for (section, items) in sections.into_iter() {
233 241 let mut items: Vec<_> = items.into_iter().collect();
234 242 items.sort_by(|e0, e1| e0.0.cmp(e1.0));
235 243
236 244 for (item, config_entry) in items {
237 245 write_bytes!(
238 246 out,
239 247 b"{}.{}={} # {}\n",
240 248 section,
241 249 item,
242 250 &config_entry.bytes,
243 251 &self.origin,
244 252 )?
245 253 }
246 254 }
247 255 Ok(())
248 256 }
249 257 }
250 258
251 259 /// Mapping of section item to value.
252 260 /// In the following:
253 261 /// ```text
254 262 /// [ui]
255 263 /// paginate=no
256 264 /// ```
257 265 /// "paginate" is the section item and "no" the value.
258 266 pub type ConfigItem = HashMap<Vec<u8>, ConfigValue>;
259 267
260 268 #[derive(Clone, Debug, PartialEq)]
261 269 pub struct ConfigValue {
262 270 /// The raw bytes of the value (be it from the CLI, env or from a file)
263 271 pub bytes: Vec<u8>,
264 272 /// Only present if the value comes from a file, 1-indexed.
265 273 pub line: Option<usize>,
266 274 }
267 275
268 276 #[derive(Clone, Debug)]
269 277 pub enum ConfigOrigin {
270 278 /// From a configuration file
271 279 File(PathBuf),
272 280 /// From a `--config` CLI argument
273 281 CommandLine,
274 282 /// From environment variables like `$PAGER` or `$EDITOR`
275 283 Environment(Vec<u8>),
276 284 /* TODO cli
277 285 * TODO defaults (configitems.py)
278 286 * TODO extensions
279 287 * TODO Python resources?
280 288 * Others? */
281 289 }
282 290
283 291 impl DisplayBytes for ConfigOrigin {
284 292 fn display_bytes(
285 293 &self,
286 294 out: &mut dyn std::io::Write,
287 295 ) -> std::io::Result<()> {
288 296 match self {
289 297 ConfigOrigin::File(p) => out.write_all(&get_bytes_from_path(p)),
290 298 ConfigOrigin::CommandLine => out.write_all(b"--config"),
291 299 ConfigOrigin::Environment(e) => write_bytes!(out, b"${}", e),
292 300 }
293 301 }
294 302 }
295 303
296 304 #[derive(Debug)]
297 305 pub struct ConfigParseError {
298 306 pub origin: ConfigOrigin,
299 307 pub line: Option<usize>,
300 308 pub message: Vec<u8>,
301 309 }
302 310
303 311 #[derive(Debug, derive_more::From)]
304 312 pub enum ConfigError {
305 313 Parse(ConfigParseError),
306 314 Other(HgError),
307 315 }
308 316
309 317 fn make_regex(pattern: &'static str) -> Regex {
310 318 Regex::new(pattern).expect("expected a valid regex")
311 319 }
General Comments 0
You need to be logged in to leave comments. Login now