##// END OF EJS Templates
rhg: Align "malformed --config" error message with Python...
Simon Sapin -
r47461:28a54c12 default
parent child Browse files
Show More
@@ -1,292 +1,292
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, IoResultExt};
11 11 use crate::utils::files::{get_bytes_from_path, get_path_from_bytes};
12 12 use 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 "malformed --config option: \"{}\" \
77 "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 pub fn is_empty(&self) -> bool {
119 119 self.sections.is_empty()
120 120 }
121 121
122 122 /// Returns a `Vec` of layers in order of precedence (so, in read order),
123 123 /// recursively parsing the `%include` directives if any.
124 124 pub fn parse(src: &Path, data: &[u8]) -> Result<Vec<Self>, ConfigError> {
125 125 let mut layers = vec![];
126 126
127 127 // Discard byte order mark if any
128 128 let data = if data.starts_with(b"\xef\xbb\xbf") {
129 129 &data[3..]
130 130 } else {
131 131 data
132 132 };
133 133
134 134 // TODO check if it's trusted
135 135 let mut current_layer = Self::new(ConfigOrigin::File(src.to_owned()));
136 136
137 137 let mut lines_iter =
138 138 data.split(|b| *b == b'\n').enumerate().peekable();
139 139 let mut section = b"".to_vec();
140 140
141 141 while let Some((index, bytes)) = lines_iter.next() {
142 142 if let Some(m) = INCLUDE_RE.captures(&bytes) {
143 143 let filename_bytes = &m[1];
144 144 // `Path::parent` only fails for the root directory,
145 145 // which `src` can’t be since we’ve managed to open it as a
146 146 // file.
147 147 let dir = src
148 148 .parent()
149 149 .expect("Path::parent fail on a file we’ve read");
150 150 // `Path::join` with an absolute argument correctly ignores the
151 151 // base path
152 152 let filename = dir.join(&get_path_from_bytes(&filename_bytes));
153 153 let data =
154 154 std::fs::read(&filename).when_reading_file(&filename)?;
155 155 layers.push(current_layer);
156 156 layers.extend(Self::parse(&filename, &data)?);
157 157 current_layer = Self::new(ConfigOrigin::File(src.to_owned()));
158 158 } else if let Some(_) = EMPTY_RE.captures(&bytes) {
159 159 } else if let Some(m) = SECTION_RE.captures(&bytes) {
160 160 section = m[1].to_vec();
161 161 } else if let Some(m) = ITEM_RE.captures(&bytes) {
162 162 let item = m[1].to_vec();
163 163 let mut value = m[2].to_vec();
164 164 loop {
165 165 match lines_iter.peek() {
166 166 None => break,
167 167 Some((_, v)) => {
168 168 if let Some(_) = COMMENT_RE.captures(&v) {
169 169 } else if let Some(_) = CONT_RE.captures(&v) {
170 170 value.extend(b"\n");
171 171 value.extend(&m[1]);
172 172 } else {
173 173 break;
174 174 }
175 175 }
176 176 };
177 177 lines_iter.next();
178 178 }
179 179 current_layer.add(
180 180 section.clone(),
181 181 item,
182 182 value,
183 183 Some(index + 1),
184 184 );
185 185 } else if let Some(m) = UNSET_RE.captures(&bytes) {
186 186 if let Some(map) = current_layer.sections.get_mut(&section) {
187 187 map.remove(&m[1]);
188 188 }
189 189 } else {
190 190 return Err(ConfigParseError {
191 191 origin: ConfigOrigin::File(src.to_owned()),
192 192 line: Some(index + 1),
193 193 bytes: bytes.to_owned(),
194 194 }
195 195 .into());
196 196 }
197 197 }
198 198 if !current_layer.is_empty() {
199 199 layers.push(current_layer);
200 200 }
201 201 Ok(layers)
202 202 }
203 203 }
204 204
205 205 impl DisplayBytes for ConfigLayer {
206 206 fn display_bytes(
207 207 &self,
208 208 out: &mut dyn std::io::Write,
209 209 ) -> std::io::Result<()> {
210 210 let mut sections: Vec<_> = self.sections.iter().collect();
211 211 sections.sort_by(|e0, e1| e0.0.cmp(e1.0));
212 212
213 213 for (section, items) in sections.into_iter() {
214 214 let mut items: Vec<_> = items.into_iter().collect();
215 215 items.sort_by(|e0, e1| e0.0.cmp(e1.0));
216 216
217 217 for (item, config_entry) in items {
218 218 write_bytes!(
219 219 out,
220 220 b"{}.{}={} # {}\n",
221 221 section,
222 222 item,
223 223 &config_entry.bytes,
224 224 &self.origin,
225 225 )?
226 226 }
227 227 }
228 228 Ok(())
229 229 }
230 230 }
231 231
232 232 /// Mapping of section item to value.
233 233 /// In the following:
234 234 /// ```text
235 235 /// [ui]
236 236 /// paginate=no
237 237 /// ```
238 238 /// "paginate" is the section item and "no" the value.
239 239 pub type ConfigItem = HashMap<Vec<u8>, ConfigValue>;
240 240
241 241 #[derive(Clone, Debug, PartialEq)]
242 242 pub struct ConfigValue {
243 243 /// The raw bytes of the value (be it from the CLI, env or from a file)
244 244 pub bytes: Vec<u8>,
245 245 /// Only present if the value comes from a file, 1-indexed.
246 246 pub line: Option<usize>,
247 247 }
248 248
249 249 #[derive(Clone, Debug)]
250 250 pub enum ConfigOrigin {
251 251 /// From a configuration file
252 252 File(PathBuf),
253 253 /// From a `--config` CLI argument
254 254 CommandLine,
255 255 /// From environment variables like `$PAGER` or `$EDITOR`
256 256 Environment(Vec<u8>),
257 257 /* TODO cli
258 258 * TODO defaults (configitems.py)
259 259 * TODO extensions
260 260 * TODO Python resources?
261 261 * Others? */
262 262 }
263 263
264 264 impl DisplayBytes for ConfigOrigin {
265 265 fn display_bytes(
266 266 &self,
267 267 out: &mut dyn std::io::Write,
268 268 ) -> std::io::Result<()> {
269 269 match self {
270 270 ConfigOrigin::File(p) => out.write_all(&get_bytes_from_path(p)),
271 271 ConfigOrigin::CommandLine => out.write_all(b"--config"),
272 272 ConfigOrigin::Environment(e) => write_bytes!(out, b"${}", e),
273 273 }
274 274 }
275 275 }
276 276
277 277 #[derive(Debug)]
278 278 pub struct ConfigParseError {
279 279 pub origin: ConfigOrigin,
280 280 pub line: Option<usize>,
281 281 pub bytes: Vec<u8>,
282 282 }
283 283
284 284 #[derive(Debug, derive_more::From)]
285 285 pub enum ConfigError {
286 286 Parse(ConfigParseError),
287 287 Other(HgError),
288 288 }
289 289
290 290 fn make_regex(pattern: &'static str) -> Regex {
291 291 Regex::new(pattern).expect("expected a valid regex")
292 292 }
General Comments 0
You need to be logged in to leave comments. Login now