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