##// END OF EJS Templates
rust: Add a `ConfigValueParseError` variant to common errors...
Simon Sapin -
r47340:bc08c233 default
parent child Browse files
Show More
@@ -1,15 +1,15
1 // config.rs
1 // config.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 //! Mercurial config parsing and interfaces.
10 //! Mercurial config parsing and interfaces.
11
11
12 mod config;
12 mod config;
13 mod layer;
13 mod layer;
14 pub use config::Config;
14 pub use config::{Config, ConfigValueParseError};
15 pub use layer::{ConfigError, ConfigParseError};
15 pub use layer::{ConfigError, ConfigParseError};
@@ -1,421 +1,437
1 // config.rs
1 // config.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 super::layer;
10 use super::layer;
11 use crate::config::layer::{
11 use crate::config::layer::{
12 ConfigError, ConfigLayer, ConfigParseError, ConfigValue,
12 ConfigError, ConfigLayer, ConfigOrigin, ConfigValue,
13 };
13 };
14 use crate::utils::files::get_bytes_from_os_str;
14 use crate::utils::files::get_bytes_from_os_str;
15 use format_bytes::{write_bytes, DisplayBytes};
15 use format_bytes::{write_bytes, DisplayBytes};
16 use std::env;
16 use std::env;
17 use std::path::{Path, PathBuf};
17 use std::path::{Path, PathBuf};
18 use std::str;
18 use std::str;
19
19
20 use crate::errors::{HgResultExt, IoResultExt};
20 use crate::errors::{HgResultExt, IoResultExt};
21
21
22 /// Holds the config values for the current repository
22 /// Holds the config values for the current repository
23 /// TODO update this docstring once we support more sources
23 /// TODO update this docstring once we support more sources
24 pub struct Config {
24 pub struct Config {
25 layers: Vec<layer::ConfigLayer>,
25 layers: Vec<layer::ConfigLayer>,
26 }
26 }
27
27
28 impl DisplayBytes for Config {
28 impl DisplayBytes for Config {
29 fn display_bytes(
29 fn display_bytes(
30 &self,
30 &self,
31 out: &mut dyn std::io::Write,
31 out: &mut dyn std::io::Write,
32 ) -> std::io::Result<()> {
32 ) -> std::io::Result<()> {
33 for (index, layer) in self.layers.iter().rev().enumerate() {
33 for (index, layer) in self.layers.iter().rev().enumerate() {
34 write_bytes!(
34 write_bytes!(
35 out,
35 out,
36 b"==== Layer {} (trusted: {}) ====\n{}",
36 b"==== Layer {} (trusted: {}) ====\n{}",
37 index,
37 index,
38 if layer.trusted {
38 if layer.trusted {
39 &b"yes"[..]
39 &b"yes"[..]
40 } else {
40 } else {
41 &b"no"[..]
41 &b"no"[..]
42 },
42 },
43 layer
43 layer
44 )?;
44 )?;
45 }
45 }
46 Ok(())
46 Ok(())
47 }
47 }
48 }
48 }
49
49
50 pub enum ConfigSource {
50 pub enum ConfigSource {
51 /// Absolute path to a config file
51 /// Absolute path to a config file
52 AbsPath(PathBuf),
52 AbsPath(PathBuf),
53 /// Already parsed (from the CLI, env, Python resources, etc.)
53 /// Already parsed (from the CLI, env, Python resources, etc.)
54 Parsed(layer::ConfigLayer),
54 Parsed(layer::ConfigLayer),
55 }
55 }
56
56
57 #[derive(Debug)]
58 pub struct ConfigValueParseError {
59 pub origin: ConfigOrigin,
60 pub line: Option<usize>,
61 pub section: Vec<u8>,
62 pub item: Vec<u8>,
63 pub value: Vec<u8>,
64 pub expected_type: &'static str,
65 }
66
57 pub fn parse_bool(v: &[u8]) -> Option<bool> {
67 pub fn parse_bool(v: &[u8]) -> Option<bool> {
58 match v.to_ascii_lowercase().as_slice() {
68 match v.to_ascii_lowercase().as_slice() {
59 b"1" | b"yes" | b"true" | b"on" | b"always" => Some(true),
69 b"1" | b"yes" | b"true" | b"on" | b"always" => Some(true),
60 b"0" | b"no" | b"false" | b"off" | b"never" => Some(false),
70 b"0" | b"no" | b"false" | b"off" | b"never" => Some(false),
61 _ => None,
71 _ => None,
62 }
72 }
63 }
73 }
64
74
65 pub fn parse_byte_size(value: &[u8]) -> Option<u64> {
75 pub fn parse_byte_size(value: &[u8]) -> Option<u64> {
66 let value = str::from_utf8(value).ok()?.to_ascii_lowercase();
76 let value = str::from_utf8(value).ok()?.to_ascii_lowercase();
67 const UNITS: &[(&str, u64)] = &[
77 const UNITS: &[(&str, u64)] = &[
68 ("g", 1 << 30),
78 ("g", 1 << 30),
69 ("gb", 1 << 30),
79 ("gb", 1 << 30),
70 ("m", 1 << 20),
80 ("m", 1 << 20),
71 ("mb", 1 << 20),
81 ("mb", 1 << 20),
72 ("k", 1 << 10),
82 ("k", 1 << 10),
73 ("kb", 1 << 10),
83 ("kb", 1 << 10),
74 ("b", 1 << 0), // Needs to be last
84 ("b", 1 << 0), // Needs to be last
75 ];
85 ];
76 for &(unit, multiplier) in UNITS {
86 for &(unit, multiplier) in UNITS {
77 // TODO: use `value.strip_suffix(unit)` when we require Rust 1.45+
87 // TODO: use `value.strip_suffix(unit)` when we require Rust 1.45+
78 if value.ends_with(unit) {
88 if value.ends_with(unit) {
79 let value_before_unit = &value[..value.len() - unit.len()];
89 let value_before_unit = &value[..value.len() - unit.len()];
80 let float: f64 = value_before_unit.trim().parse().ok()?;
90 let float: f64 = value_before_unit.trim().parse().ok()?;
81 if float >= 0.0 {
91 if float >= 0.0 {
82 return Some((float * multiplier as f64).round() as u64);
92 return Some((float * multiplier as f64).round() as u64);
83 } else {
93 } else {
84 return None;
94 return None;
85 }
95 }
86 }
96 }
87 }
97 }
88 value.parse().ok()
98 value.parse().ok()
89 }
99 }
90
100
91 impl Config {
101 impl Config {
92 /// Load system and user configuration from various files.
102 /// Load system and user configuration from various files.
93 ///
103 ///
94 /// This is also affected by some environment variables.
104 /// This is also affected by some environment variables.
95 pub fn load(
105 pub fn load(
96 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
106 cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
97 ) -> Result<Self, ConfigError> {
107 ) -> Result<Self, ConfigError> {
98 let mut config = Self { layers: Vec::new() };
108 let mut config = Self { layers: Vec::new() };
99 let opt_rc_path = env::var_os("HGRCPATH");
109 let opt_rc_path = env::var_os("HGRCPATH");
100 // HGRCPATH replaces system config
110 // HGRCPATH replaces system config
101 if opt_rc_path.is_none() {
111 if opt_rc_path.is_none() {
102 config.add_system_config()?
112 config.add_system_config()?
103 }
113 }
104 config.add_for_environment_variable("EDITOR", b"ui", b"editor");
114 config.add_for_environment_variable("EDITOR", b"ui", b"editor");
105 config.add_for_environment_variable("VISUAL", b"ui", b"editor");
115 config.add_for_environment_variable("VISUAL", b"ui", b"editor");
106 config.add_for_environment_variable("PAGER", b"pager", b"pager");
116 config.add_for_environment_variable("PAGER", b"pager", b"pager");
107 // HGRCPATH replaces user config
117 // HGRCPATH replaces user config
108 if opt_rc_path.is_none() {
118 if opt_rc_path.is_none() {
109 config.add_user_config()?
119 config.add_user_config()?
110 }
120 }
111 if let Some(rc_path) = &opt_rc_path {
121 if let Some(rc_path) = &opt_rc_path {
112 for path in env::split_paths(rc_path) {
122 for path in env::split_paths(rc_path) {
113 if !path.as_os_str().is_empty() {
123 if !path.as_os_str().is_empty() {
114 if path.is_dir() {
124 if path.is_dir() {
115 config.add_trusted_dir(&path)?
125 config.add_trusted_dir(&path)?
116 } else {
126 } else {
117 config.add_trusted_file(&path)?
127 config.add_trusted_file(&path)?
118 }
128 }
119 }
129 }
120 }
130 }
121 }
131 }
122 if let Some(layer) = ConfigLayer::parse_cli_args(cli_config_args)? {
132 if let Some(layer) = ConfigLayer::parse_cli_args(cli_config_args)? {
123 config.layers.push(layer)
133 config.layers.push(layer)
124 }
134 }
125 Ok(config)
135 Ok(config)
126 }
136 }
127
137
128 fn add_trusted_dir(&mut self, path: &Path) -> Result<(), ConfigError> {
138 fn add_trusted_dir(&mut self, path: &Path) -> Result<(), ConfigError> {
129 if let Some(entries) = std::fs::read_dir(path)
139 if let Some(entries) = std::fs::read_dir(path)
130 .for_file(path)
140 .for_file(path)
131 .io_not_found_as_none()?
141 .io_not_found_as_none()?
132 {
142 {
133 for entry in entries {
143 for entry in entries {
134 let file_path = entry.for_file(path)?.path();
144 let file_path = entry.for_file(path)?.path();
135 if file_path.extension() == Some(std::ffi::OsStr::new("rc")) {
145 if file_path.extension() == Some(std::ffi::OsStr::new("rc")) {
136 self.add_trusted_file(&file_path)?
146 self.add_trusted_file(&file_path)?
137 }
147 }
138 }
148 }
139 }
149 }
140 Ok(())
150 Ok(())
141 }
151 }
142
152
143 fn add_trusted_file(&mut self, path: &Path) -> Result<(), ConfigError> {
153 fn add_trusted_file(&mut self, path: &Path) -> Result<(), ConfigError> {
144 if let Some(data) =
154 if let Some(data) =
145 std::fs::read(path).for_file(path).io_not_found_as_none()?
155 std::fs::read(path).for_file(path).io_not_found_as_none()?
146 {
156 {
147 self.layers.extend(ConfigLayer::parse(path, &data)?)
157 self.layers.extend(ConfigLayer::parse(path, &data)?)
148 }
158 }
149 Ok(())
159 Ok(())
150 }
160 }
151
161
152 fn add_for_environment_variable(
162 fn add_for_environment_variable(
153 &mut self,
163 &mut self,
154 var: &str,
164 var: &str,
155 section: &[u8],
165 section: &[u8],
156 key: &[u8],
166 key: &[u8],
157 ) {
167 ) {
158 if let Some(value) = env::var_os(var) {
168 if let Some(value) = env::var_os(var) {
159 let origin = layer::ConfigOrigin::Environment(var.into());
169 let origin = layer::ConfigOrigin::Environment(var.into());
160 let mut layer = ConfigLayer::new(origin);
170 let mut layer = ConfigLayer::new(origin);
161 layer.add(
171 layer.add(
162 section.to_owned(),
172 section.to_owned(),
163 key.to_owned(),
173 key.to_owned(),
164 get_bytes_from_os_str(value),
174 get_bytes_from_os_str(value),
165 None,
175 None,
166 );
176 );
167 self.layers.push(layer)
177 self.layers.push(layer)
168 }
178 }
169 }
179 }
170
180
171 #[cfg(unix)] // TODO: other platforms
181 #[cfg(unix)] // TODO: other platforms
172 fn add_system_config(&mut self) -> Result<(), ConfigError> {
182 fn add_system_config(&mut self) -> Result<(), ConfigError> {
173 let mut add_for_prefix = |prefix: &Path| -> Result<(), ConfigError> {
183 let mut add_for_prefix = |prefix: &Path| -> Result<(), ConfigError> {
174 let etc = prefix.join("etc").join("mercurial");
184 let etc = prefix.join("etc").join("mercurial");
175 self.add_trusted_file(&etc.join("hgrc"))?;
185 self.add_trusted_file(&etc.join("hgrc"))?;
176 self.add_trusted_dir(&etc.join("hgrc.d"))
186 self.add_trusted_dir(&etc.join("hgrc.d"))
177 };
187 };
178 let root = Path::new("/");
188 let root = Path::new("/");
179 // TODO: use `std::env::args_os().next().unwrap()` a.k.a. argv[0]
189 // TODO: use `std::env::args_os().next().unwrap()` a.k.a. argv[0]
180 // instead? TODO: can this be a relative path?
190 // instead? TODO: can this be a relative path?
181 let hg = crate::utils::current_exe()?;
191 let hg = crate::utils::current_exe()?;
182 // TODO: this order (per-installation then per-system) matches
192 // TODO: this order (per-installation then per-system) matches
183 // `systemrcpath()` in `mercurial/scmposix.py`, but
193 // `systemrcpath()` in `mercurial/scmposix.py`, but
184 // `mercurial/helptext/config.txt` suggests it should be reversed
194 // `mercurial/helptext/config.txt` suggests it should be reversed
185 if let Some(installation_prefix) = hg.parent().and_then(Path::parent) {
195 if let Some(installation_prefix) = hg.parent().and_then(Path::parent) {
186 if installation_prefix != root {
196 if installation_prefix != root {
187 add_for_prefix(&installation_prefix)?
197 add_for_prefix(&installation_prefix)?
188 }
198 }
189 }
199 }
190 add_for_prefix(root)?;
200 add_for_prefix(root)?;
191 Ok(())
201 Ok(())
192 }
202 }
193
203
194 #[cfg(unix)] // TODO: other plateforms
204 #[cfg(unix)] // TODO: other plateforms
195 fn add_user_config(&mut self) -> Result<(), ConfigError> {
205 fn add_user_config(&mut self) -> Result<(), ConfigError> {
196 let opt_home = home::home_dir();
206 let opt_home = home::home_dir();
197 if let Some(home) = &opt_home {
207 if let Some(home) = &opt_home {
198 self.add_trusted_file(&home.join(".hgrc"))?
208 self.add_trusted_file(&home.join(".hgrc"))?
199 }
209 }
200 let darwin = cfg!(any(target_os = "macos", target_os = "ios"));
210 let darwin = cfg!(any(target_os = "macos", target_os = "ios"));
201 if !darwin {
211 if !darwin {
202 if let Some(config_home) = env::var_os("XDG_CONFIG_HOME")
212 if let Some(config_home) = env::var_os("XDG_CONFIG_HOME")
203 .map(PathBuf::from)
213 .map(PathBuf::from)
204 .or_else(|| opt_home.map(|home| home.join(".config")))
214 .or_else(|| opt_home.map(|home| home.join(".config")))
205 {
215 {
206 self.add_trusted_file(&config_home.join("hg").join("hgrc"))?
216 self.add_trusted_file(&config_home.join("hg").join("hgrc"))?
207 }
217 }
208 }
218 }
209 Ok(())
219 Ok(())
210 }
220 }
211
221
212 /// Loads in order, which means that the precedence is the same
222 /// Loads in order, which means that the precedence is the same
213 /// as the order of `sources`.
223 /// as the order of `sources`.
214 pub fn load_from_explicit_sources(
224 pub fn load_from_explicit_sources(
215 sources: Vec<ConfigSource>,
225 sources: Vec<ConfigSource>,
216 ) -> Result<Self, ConfigError> {
226 ) -> Result<Self, ConfigError> {
217 let mut layers = vec![];
227 let mut layers = vec![];
218
228
219 for source in sources.into_iter() {
229 for source in sources.into_iter() {
220 match source {
230 match source {
221 ConfigSource::Parsed(c) => layers.push(c),
231 ConfigSource::Parsed(c) => layers.push(c),
222 ConfigSource::AbsPath(c) => {
232 ConfigSource::AbsPath(c) => {
223 // TODO check if it should be trusted
233 // TODO check if it should be trusted
224 // mercurial/ui.py:427
234 // mercurial/ui.py:427
225 let data = match std::fs::read(&c) {
235 let data = match std::fs::read(&c) {
226 Err(_) => continue, // same as the python code
236 Err(_) => continue, // same as the python code
227 Ok(data) => data,
237 Ok(data) => data,
228 };
238 };
229 layers.extend(ConfigLayer::parse(&c, &data)?)
239 layers.extend(ConfigLayer::parse(&c, &data)?)
230 }
240 }
231 }
241 }
232 }
242 }
233
243
234 Ok(Config { layers })
244 Ok(Config { layers })
235 }
245 }
236
246
237 /// Loads the per-repository config into a new `Config` which is combined
247 /// Loads the per-repository config into a new `Config` which is combined
238 /// with `self`.
248 /// with `self`.
239 pub(crate) fn combine_with_repo(
249 pub(crate) fn combine_with_repo(
240 &self,
250 &self,
241 repo_config_files: &[PathBuf],
251 repo_config_files: &[PathBuf],
242 ) -> Result<Self, ConfigError> {
252 ) -> Result<Self, ConfigError> {
243 let (cli_layers, other_layers) = self
253 let (cli_layers, other_layers) = self
244 .layers
254 .layers
245 .iter()
255 .iter()
246 .cloned()
256 .cloned()
247 .partition(ConfigLayer::is_from_command_line);
257 .partition(ConfigLayer::is_from_command_line);
248
258
249 let mut repo_config = Self {
259 let mut repo_config = Self {
250 layers: other_layers,
260 layers: other_layers,
251 };
261 };
252 for path in repo_config_files {
262 for path in repo_config_files {
253 // TODO: check if this file should be trusted:
263 // TODO: check if this file should be trusted:
254 // `mercurial/ui.py:427`
264 // `mercurial/ui.py:427`
255 repo_config.add_trusted_file(path)?;
265 repo_config.add_trusted_file(path)?;
256 }
266 }
257 repo_config.layers.extend(cli_layers);
267 repo_config.layers.extend(cli_layers);
258 Ok(repo_config)
268 Ok(repo_config)
259 }
269 }
260
270
261 fn get_parse<'config, T: 'config>(
271 fn get_parse<'config, T: 'config>(
262 &'config self,
272 &'config self,
263 section: &[u8],
273 section: &[u8],
264 item: &[u8],
274 item: &[u8],
275 expected_type: &'static str,
265 parse: impl Fn(&'config [u8]) -> Option<T>,
276 parse: impl Fn(&'config [u8]) -> Option<T>,
266 ) -> Result<Option<T>, ConfigParseError> {
277 ) -> Result<Option<T>, ConfigValueParseError> {
267 match self.get_inner(&section, &item) {
278 match self.get_inner(&section, &item) {
268 Some((layer, v)) => match parse(&v.bytes) {
279 Some((layer, v)) => match parse(&v.bytes) {
269 Some(b) => Ok(Some(b)),
280 Some(b) => Ok(Some(b)),
270 None => Err(ConfigParseError {
281 None => Err(ConfigValueParseError {
271 origin: layer.origin.to_owned(),
282 origin: layer.origin.to_owned(),
272 line: v.line,
283 line: v.line,
273 bytes: v.bytes.to_owned(),
284 value: v.bytes.to_owned(),
285 section: section.to_owned(),
286 item: item.to_owned(),
287 expected_type,
274 }),
288 }),
275 },
289 },
276 None => Ok(None),
290 None => Ok(None),
277 }
291 }
278 }
292 }
279
293
280 /// Returns an `Err` if the first value found is not a valid UTF-8 string.
294 /// Returns an `Err` if the first value found is not a valid UTF-8 string.
281 /// Otherwise, returns an `Ok(value)` if found, or `None`.
295 /// Otherwise, returns an `Ok(value)` if found, or `None`.
282 pub fn get_str(
296 pub fn get_str(
283 &self,
297 &self,
284 section: &[u8],
298 section: &[u8],
285 item: &[u8],
299 item: &[u8],
286 ) -> Result<Option<&str>, ConfigParseError> {
300 ) -> Result<Option<&str>, ConfigValueParseError> {
287 self.get_parse(section, item, |value| str::from_utf8(value).ok())
301 self.get_parse(section, item, "ASCII or UTF-8 string", |value| {
302 str::from_utf8(value).ok()
303 })
288 }
304 }
289
305
290 /// Returns an `Err` if the first value found is not a valid unsigned
306 /// Returns an `Err` if the first value found is not a valid unsigned
291 /// integer. Otherwise, returns an `Ok(value)` if found, or `None`.
307 /// integer. Otherwise, returns an `Ok(value)` if found, or `None`.
292 pub fn get_u32(
308 pub fn get_u32(
293 &self,
309 &self,
294 section: &[u8],
310 section: &[u8],
295 item: &[u8],
311 item: &[u8],
296 ) -> Result<Option<u32>, ConfigParseError> {
312 ) -> Result<Option<u32>, ConfigValueParseError> {
297 self.get_parse(section, item, |value| {
313 self.get_parse(section, item, "valid integer", |value| {
298 str::from_utf8(value).ok()?.parse().ok()
314 str::from_utf8(value).ok()?.parse().ok()
299 })
315 })
300 }
316 }
301
317
302 /// Returns an `Err` if the first value found is not a valid file size
318 /// Returns an `Err` if the first value found is not a valid file size
303 /// value such as `30` (default unit is bytes), `7 MB`, or `42.5 kb`.
319 /// value such as `30` (default unit is bytes), `7 MB`, or `42.5 kb`.
304 /// Otherwise, returns an `Ok(value_in_bytes)` if found, or `None`.
320 /// Otherwise, returns an `Ok(value_in_bytes)` if found, or `None`.
305 pub fn get_byte_size(
321 pub fn get_byte_size(
306 &self,
322 &self,
307 section: &[u8],
323 section: &[u8],
308 item: &[u8],
324 item: &[u8],
309 ) -> Result<Option<u64>, ConfigParseError> {
325 ) -> Result<Option<u64>, ConfigValueParseError> {
310 self.get_parse(section, item, parse_byte_size)
326 self.get_parse(section, item, "byte quantity", parse_byte_size)
311 }
327 }
312
328
313 /// Returns an `Err` if the first value found is not a valid boolean.
329 /// Returns an `Err` if the first value found is not a valid boolean.
314 /// Otherwise, returns an `Ok(option)`, where `option` is the boolean if
330 /// Otherwise, returns an `Ok(option)`, where `option` is the boolean if
315 /// found, or `None`.
331 /// found, or `None`.
316 pub fn get_option(
332 pub fn get_option(
317 &self,
333 &self,
318 section: &[u8],
334 section: &[u8],
319 item: &[u8],
335 item: &[u8],
320 ) -> Result<Option<bool>, ConfigParseError> {
336 ) -> Result<Option<bool>, ConfigValueParseError> {
321 self.get_parse(section, item, parse_bool)
337 self.get_parse(section, item, "boolean", parse_bool)
322 }
338 }
323
339
324 /// Returns the corresponding boolean in the config. Returns `Ok(false)`
340 /// Returns the corresponding boolean in the config. Returns `Ok(false)`
325 /// if the value is not found, an `Err` if it's not a valid boolean.
341 /// if the value is not found, an `Err` if it's not a valid boolean.
326 pub fn get_bool(
342 pub fn get_bool(
327 &self,
343 &self,
328 section: &[u8],
344 section: &[u8],
329 item: &[u8],
345 item: &[u8],
330 ) -> Result<bool, ConfigError> {
346 ) -> Result<bool, ConfigValueParseError> {
331 Ok(self.get_option(section, item)?.unwrap_or(false))
347 Ok(self.get_option(section, item)?.unwrap_or(false))
332 }
348 }
333
349
334 /// Returns the raw value bytes of the first one found, or `None`.
350 /// Returns the raw value bytes of the first one found, or `None`.
335 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&[u8]> {
351 pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&[u8]> {
336 self.get_inner(section, item)
352 self.get_inner(section, item)
337 .map(|(_, value)| value.bytes.as_ref())
353 .map(|(_, value)| value.bytes.as_ref())
338 }
354 }
339
355
340 /// Returns the layer and the value of the first one found, or `None`.
356 /// Returns the layer and the value of the first one found, or `None`.
341 fn get_inner(
357 fn get_inner(
342 &self,
358 &self,
343 section: &[u8],
359 section: &[u8],
344 item: &[u8],
360 item: &[u8],
345 ) -> Option<(&ConfigLayer, &ConfigValue)> {
361 ) -> Option<(&ConfigLayer, &ConfigValue)> {
346 for layer in self.layers.iter().rev() {
362 for layer in self.layers.iter().rev() {
347 if !layer.trusted {
363 if !layer.trusted {
348 continue;
364 continue;
349 }
365 }
350 if let Some(v) = layer.get(&section, &item) {
366 if let Some(v) = layer.get(&section, &item) {
351 return Some((&layer, v));
367 return Some((&layer, v));
352 }
368 }
353 }
369 }
354 None
370 None
355 }
371 }
356
372
357 /// Get raw values bytes from all layers (even untrusted ones) in order
373 /// Get raw values bytes from all layers (even untrusted ones) in order
358 /// of precedence.
374 /// of precedence.
359 #[cfg(test)]
375 #[cfg(test)]
360 fn get_all(&self, section: &[u8], item: &[u8]) -> Vec<&[u8]> {
376 fn get_all(&self, section: &[u8], item: &[u8]) -> Vec<&[u8]> {
361 let mut res = vec![];
377 let mut res = vec![];
362 for layer in self.layers.iter().rev() {
378 for layer in self.layers.iter().rev() {
363 if let Some(v) = layer.get(&section, &item) {
379 if let Some(v) = layer.get(&section, &item) {
364 res.push(v.bytes.as_ref());
380 res.push(v.bytes.as_ref());
365 }
381 }
366 }
382 }
367 res
383 res
368 }
384 }
369 }
385 }
370
386
371 #[cfg(test)]
387 #[cfg(test)]
372 mod tests {
388 mod tests {
373 use super::*;
389 use super::*;
374 use pretty_assertions::assert_eq;
390 use pretty_assertions::assert_eq;
375 use std::fs::File;
391 use std::fs::File;
376 use std::io::Write;
392 use std::io::Write;
377
393
378 #[test]
394 #[test]
379 fn test_include_layer_ordering() {
395 fn test_include_layer_ordering() {
380 let tmpdir = tempfile::tempdir().unwrap();
396 let tmpdir = tempfile::tempdir().unwrap();
381 let tmpdir_path = tmpdir.path();
397 let tmpdir_path = tmpdir.path();
382 let mut included_file =
398 let mut included_file =
383 File::create(&tmpdir_path.join("included.rc")).unwrap();
399 File::create(&tmpdir_path.join("included.rc")).unwrap();
384
400
385 included_file.write_all(b"[section]\nitem=value1").unwrap();
401 included_file.write_all(b"[section]\nitem=value1").unwrap();
386 let base_config_path = tmpdir_path.join("base.rc");
402 let base_config_path = tmpdir_path.join("base.rc");
387 let mut config_file = File::create(&base_config_path).unwrap();
403 let mut config_file = File::create(&base_config_path).unwrap();
388 let data =
404 let data =
389 b"[section]\nitem=value0\n%include included.rc\nitem=value2\n\
405 b"[section]\nitem=value0\n%include included.rc\nitem=value2\n\
390 [section2]\ncount = 4\nsize = 1.5 KB\nnot-count = 1.5\nnot-size = 1 ub";
406 [section2]\ncount = 4\nsize = 1.5 KB\nnot-count = 1.5\nnot-size = 1 ub";
391 config_file.write_all(data).unwrap();
407 config_file.write_all(data).unwrap();
392
408
393 let sources = vec![ConfigSource::AbsPath(base_config_path)];
409 let sources = vec![ConfigSource::AbsPath(base_config_path)];
394 let config = Config::load_from_explicit_sources(sources)
410 let config = Config::load_from_explicit_sources(sources)
395 .expect("expected valid config");
411 .expect("expected valid config");
396
412
397 let (_, value) = config.get_inner(b"section", b"item").unwrap();
413 let (_, value) = config.get_inner(b"section", b"item").unwrap();
398 assert_eq!(
414 assert_eq!(
399 value,
415 value,
400 &ConfigValue {
416 &ConfigValue {
401 bytes: b"value2".to_vec(),
417 bytes: b"value2".to_vec(),
402 line: Some(4)
418 line: Some(4)
403 }
419 }
404 );
420 );
405
421
406 let value = config.get(b"section", b"item").unwrap();
422 let value = config.get(b"section", b"item").unwrap();
407 assert_eq!(value, b"value2",);
423 assert_eq!(value, b"value2",);
408 assert_eq!(
424 assert_eq!(
409 config.get_all(b"section", b"item"),
425 config.get_all(b"section", b"item"),
410 [b"value2", b"value1", b"value0"]
426 [b"value2", b"value1", b"value0"]
411 );
427 );
412
428
413 assert_eq!(config.get_u32(b"section2", b"count").unwrap(), Some(4));
429 assert_eq!(config.get_u32(b"section2", b"count").unwrap(), Some(4));
414 assert_eq!(
430 assert_eq!(
415 config.get_byte_size(b"section2", b"size").unwrap(),
431 config.get_byte_size(b"section2", b"size").unwrap(),
416 Some(1024 + 512)
432 Some(1024 + 512)
417 );
433 );
418 assert!(config.get_u32(b"section2", b"not-count").is_err());
434 assert!(config.get_u32(b"section2", b"not-count").is_err());
419 assert!(config.get_byte_size(b"section2", b"not-size").is_err());
435 assert!(config.get_byte_size(b"section2", b"not-size").is_err());
420 }
436 }
421 }
437 }
@@ -1,133 +1,161
1 use crate::config::ConfigValueParseError;
1 use std::fmt;
2 use std::fmt;
2
3
3 /// Common error cases that can happen in many different APIs
4 /// Common error cases that can happen in many different APIs
4 #[derive(Debug)]
5 #[derive(Debug, derive_more::From)]
5 pub enum HgError {
6 pub enum HgError {
6 IoError {
7 IoError {
7 error: std::io::Error,
8 error: std::io::Error,
8 context: IoErrorContext,
9 context: IoErrorContext,
9 },
10 },
10
11
11 /// A file under `.hg/` normally only written by Mercurial is not in the
12 /// A file under `.hg/` normally only written by Mercurial is not in the
12 /// expected format. This indicates a bug in Mercurial, filesystem
13 /// expected format. This indicates a bug in Mercurial, filesystem
13 /// corruption, or hardware failure.
14 /// corruption, or hardware failure.
14 ///
15 ///
15 /// The given string is a short explanation for users, not intended to be
16 /// The given string is a short explanation for users, not intended to be
16 /// machine-readable.
17 /// machine-readable.
17 CorruptedRepository(String),
18 CorruptedRepository(String),
18
19
19 /// The respository or requested operation involves a feature not
20 /// The respository or requested operation involves a feature not
20 /// supported by the Rust implementation. Falling back to the Python
21 /// supported by the Rust implementation. Falling back to the Python
21 /// implementation may or may not work.
22 /// implementation may or may not work.
22 ///
23 ///
23 /// The given string is a short explanation for users, not intended to be
24 /// The given string is a short explanation for users, not intended to be
24 /// machine-readable.
25 /// machine-readable.
25 UnsupportedFeature(String),
26 UnsupportedFeature(String),
26
27
27 /// Operation cannot proceed for some other reason.
28 /// Operation cannot proceed for some other reason.
28 ///
29 ///
29 /// The given string is a short explanation for users, not intended to be
30 /// The given string is a short explanation for users, not intended to be
30 /// machine-readable.
31 /// machine-readable.
31 Abort(String),
32 Abort(String),
33
34 /// A configuration value is not in the expected syntax.
35 ///
36 /// These errors can happen in many places in the code because values are
37 /// parsed lazily as the file-level parser does not know the expected type
38 /// and syntax of each value.
39 #[from]
40 ConfigValueParseError(ConfigValueParseError),
32 }
41 }
33
42
34 /// Details about where an I/O error happened
43 /// Details about where an I/O error happened
35 #[derive(Debug, derive_more::From)]
44 #[derive(Debug, derive_more::From)]
36 pub enum IoErrorContext {
45 pub enum IoErrorContext {
37 /// A filesystem operation for the given file
46 /// A filesystem operation for the given file
38 #[from]
47 #[from]
39 File(std::path::PathBuf),
48 File(std::path::PathBuf),
40 /// `std::env::current_dir`
49 /// `std::env::current_dir`
41 CurrentDir,
50 CurrentDir,
42 /// `std::env::current_exe`
51 /// `std::env::current_exe`
43 CurrentExe,
52 CurrentExe,
44 }
53 }
45
54
46 impl HgError {
55 impl HgError {
47 pub fn corrupted(explanation: impl Into<String>) -> Self {
56 pub fn corrupted(explanation: impl Into<String>) -> Self {
48 // TODO: capture a backtrace here and keep it in the error value
57 // TODO: capture a backtrace here and keep it in the error value
49 // to aid debugging?
58 // to aid debugging?
50 // https://doc.rust-lang.org/std/backtrace/struct.Backtrace.html
59 // https://doc.rust-lang.org/std/backtrace/struct.Backtrace.html
51 HgError::CorruptedRepository(explanation.into())
60 HgError::CorruptedRepository(explanation.into())
52 }
61 }
53
62
54 pub fn unsupported(explanation: impl Into<String>) -> Self {
63 pub fn unsupported(explanation: impl Into<String>) -> Self {
55 HgError::UnsupportedFeature(explanation.into())
64 HgError::UnsupportedFeature(explanation.into())
56 }
65 }
57 pub fn abort(explanation: impl Into<String>) -> Self {
66 pub fn abort(explanation: impl Into<String>) -> Self {
58 HgError::Abort(explanation.into())
67 HgError::Abort(explanation.into())
59 }
68 }
60 }
69 }
61
70
62 // TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly?
71 // TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly?
63 impl fmt::Display for HgError {
72 impl fmt::Display for HgError {
64 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
73 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
65 match self {
74 match self {
75 HgError::Abort(explanation) => write!(f, "{}", explanation),
66 HgError::IoError { error, context } => {
76 HgError::IoError { error, context } => {
67 write!(f, "{}: {}", error, context)
77 write!(f, "{}: {}", error, context)
68 }
78 }
69 HgError::CorruptedRepository(explanation) => {
79 HgError::CorruptedRepository(explanation) => {
70 write!(f, "corrupted repository: {}", explanation)
80 write!(f, "corrupted repository: {}", explanation)
71 }
81 }
72 HgError::UnsupportedFeature(explanation) => {
82 HgError::UnsupportedFeature(explanation) => {
73 write!(f, "unsupported feature: {}", explanation)
83 write!(f, "unsupported feature: {}", explanation)
74 }
84 }
75 HgError::Abort(explanation) => explanation.fmt(f),
85 HgError::ConfigValueParseError(ConfigValueParseError {
86 origin: _,
87 line: _,
88 section,
89 item,
90 value,
91 expected_type,
92 }) => {
93 // TODO: add origin and line number information, here and in
94 // corresponding python code
95 write!(
96 f,
97 "config error: {}.{} is not a {} ('{}')",
98 String::from_utf8_lossy(section),
99 String::from_utf8_lossy(item),
100 expected_type,
101 String::from_utf8_lossy(value)
102 )
103 }
76 }
104 }
77 }
105 }
78 }
106 }
79
107
80 // TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly?
108 // TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly?
81 impl fmt::Display for IoErrorContext {
109 impl fmt::Display for IoErrorContext {
82 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
110 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
83 match self {
111 match self {
84 IoErrorContext::File(path) => path.display().fmt(f),
112 IoErrorContext::File(path) => path.display().fmt(f),
85 IoErrorContext::CurrentDir => f.write_str("current directory"),
113 IoErrorContext::CurrentDir => f.write_str("current directory"),
86 IoErrorContext::CurrentExe => f.write_str("current executable"),
114 IoErrorContext::CurrentExe => f.write_str("current executable"),
87 }
115 }
88 }
116 }
89 }
117 }
90
118
91 pub trait IoResultExt<T> {
119 pub trait IoResultExt<T> {
92 /// Annotate a possible I/O error as related to a file at the given path.
120 /// Annotate a possible I/O error as related to a file at the given path.
93 ///
121 ///
94 /// This allows printing something like “File not found: example.txt”
122 /// This allows printing something like “File not found: example.txt”
95 /// instead of just “File not found”.
123 /// instead of just “File not found”.
96 ///
124 ///
97 /// Converts a `Result` with `std::io::Error` into one with `HgError`.
125 /// Converts a `Result` with `std::io::Error` into one with `HgError`.
98 fn for_file(self, path: &std::path::Path) -> Result<T, HgError>;
126 fn for_file(self, path: &std::path::Path) -> Result<T, HgError>;
99 }
127 }
100
128
101 impl<T> IoResultExt<T> for std::io::Result<T> {
129 impl<T> IoResultExt<T> for std::io::Result<T> {
102 fn for_file(self, path: &std::path::Path) -> Result<T, HgError> {
130 fn for_file(self, path: &std::path::Path) -> Result<T, HgError> {
103 self.map_err(|error| HgError::IoError {
131 self.map_err(|error| HgError::IoError {
104 error,
132 error,
105 context: IoErrorContext::File(path.to_owned()),
133 context: IoErrorContext::File(path.to_owned()),
106 })
134 })
107 }
135 }
108 }
136 }
109
137
110 pub trait HgResultExt<T> {
138 pub trait HgResultExt<T> {
111 /// Handle missing files separately from other I/O error cases.
139 /// Handle missing files separately from other I/O error cases.
112 ///
140 ///
113 /// Wraps the `Ok` type in an `Option`:
141 /// Wraps the `Ok` type in an `Option`:
114 ///
142 ///
115 /// * `Ok(x)` becomes `Ok(Some(x))`
143 /// * `Ok(x)` becomes `Ok(Some(x))`
116 /// * An I/O "not found" error becomes `Ok(None)`
144 /// * An I/O "not found" error becomes `Ok(None)`
117 /// * Other errors are unchanged
145 /// * Other errors are unchanged
118 fn io_not_found_as_none(self) -> Result<Option<T>, HgError>;
146 fn io_not_found_as_none(self) -> Result<Option<T>, HgError>;
119 }
147 }
120
148
121 impl<T> HgResultExt<T> for Result<T, HgError> {
149 impl<T> HgResultExt<T> for Result<T, HgError> {
122 fn io_not_found_as_none(self) -> Result<Option<T>, HgError> {
150 fn io_not_found_as_none(self) -> Result<Option<T>, HgError> {
123 match self {
151 match self {
124 Ok(x) => Ok(Some(x)),
152 Ok(x) => Ok(Some(x)),
125 Err(HgError::IoError { error, .. })
153 Err(HgError::IoError { error, .. })
126 if error.kind() == std::io::ErrorKind::NotFound =>
154 if error.kind() == std::io::ErrorKind::NotFound =>
127 {
155 {
128 Ok(None)
156 Ok(None)
129 }
157 }
130 Err(other_error) => Err(other_error),
158 Err(other_error) => Err(other_error),
131 }
159 }
132 }
160 }
133 }
161 }
General Comments 0
You need to be logged in to leave comments. Login now