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