##// END OF EJS Templates
rust: fix unsound `OwningDirstateMap`...
rust: fix unsound `OwningDirstateMap` As per the previous patch, `OwningDirstateMap` is unsound. Self-referential structs are difficult to implement correctly in Rust since the compiler is free to move structs around as much as it wants to. They are also very rarely needed in practice, so the state-of-the-art on how they should be done within the Rust rules is still a bit new. The crate `ouroboros` is an attempt at providing a safe way (in the Rust sense) of declaring self-referential structs. It is getting a lot attention and was improved very quickly when soundness issues were found in the past: rather than relying on our own (limited) review circle, we might as well use the de-facto common crate to fix this problem. This will give us a much better chance of finding issues should any new ones be discovered as well as the benefit of fewer `unsafe` APIs of our own. I was starting to think about how I would present a safe API to the old struct but soon realized that the callback-based approach was already done in `ouroboros`, along with a lot more care towards refusing incorrect structs. In short: we don't return a mutable reference to the `DirstateMap` anymore, we expect users of its API to pass a `FnOnce` that takes the map as an argument. This allows our `OwningDirstateMap` to control the input and output lifetimes of the code that modifies it to prevent such issues. Changing to `ouroboros` meant changing every API with it, but it is relatively low churn in the end. It correctly identified the example buggy modification of `copy_map_insert` outlined in the previous patch as violating the borrow rules. Differential Revision: https://phab.mercurial-scm.org/D12429

File last commit:

r49586:29eb80d1 default
r49857:c9f44fc9 stable
Show More
config.rs
559 lines | 18.2 KiB | application/rls-services+xml | RustLexer
Raphaël Gomès
hg-core: add basic config module...
r46803 // config.rs
//
// Copyright 2020
// Valentin Gatien-Baron,
// Raphaël Gomès <rgomes@octobus.net>
//
// This software may be used and distributed according to the terms of the
// GNU General Public License version 2 or any later version.
use super::layer;
Simon Sapin
rust: Move config value parsing functions to a new module...
r47344 use super::values;
Simon Sapin
rust: use HgError in ConfigError...
r47176 use crate::config::layer::{
Simon Sapin
rust: Add a `ConfigValueParseError` variant to common errors...
r47340 ConfigError, ConfigLayer, ConfigOrigin, ConfigValue,
Simon Sapin
rust: use HgError in ConfigError...
r47176 };
Simon Sapin
rust: Introduce a get_bytes_from_os_str utility function...
r47338 use crate::utils::files::get_bytes_from_os_str;
Simon Sapin
rust: Use the DisplayBytes trait in config printing...
r47249 use format_bytes::{write_bytes, DisplayBytes};
Simon Sapin
rhg: Fall back to Python if unsupported extensions are enabled...
r47467 use std::collections::HashSet;
Simon Sapin
rust: Parse system and user configuration...
r47212 use std::env;
Simon Sapin
rhg: Add more conversions between error types...
r47555 use std::fmt;
Simon Sapin
rust: Parse system and user configuration...
r47212 use std::path::{Path, PathBuf};
Simon Sapin
rust: Add config parsing support for more value types...
r47339 use std::str;
Raphaël Gomès
hg-core: add basic config module...
r46803
Simon Sapin
rust: Parse system and user configuration...
r47212 use crate::errors::{HgResultExt, IoResultExt};
Raphaël Gomès
hg-core: add basic config module...
r46803
/// Holds the config values for the current repository
/// TODO update this docstring once we support more sources
Simon Sapin
rhg: Add support for the HGRCSKIPREPO environment variable...
r47475 #[derive(Clone)]
Raphaël Gomès
hg-core: add basic config module...
r46803 pub struct Config {
layers: Vec<layer::ConfigLayer>,
}
Simon Sapin
rust: Use the DisplayBytes trait in config printing...
r47249 impl DisplayBytes for Config {
fn display_bytes(
&self,
out: &mut dyn std::io::Write,
) -> std::io::Result<()> {
Raphaël Gomès
hg-core: add basic config module...
r46803 for (index, layer) in self.layers.iter().rev().enumerate() {
Simon Sapin
rust: Use the DisplayBytes trait in config printing...
r47249 write_bytes!(
out,
b"==== Layer {} (trusted: {}) ====\n{}",
index,
if layer.trusted {
&b"yes"[..]
} else {
&b"no"[..]
},
layer
Raphaël Gomès
hg-core: add basic config module...
r46803 )?;
}
Ok(())
}
}
pub enum ConfigSource {
/// Absolute path to a config file
AbsPath(PathBuf),
/// Already parsed (from the CLI, env, Python resources, etc.)
Parsed(layer::ConfigLayer),
}
Simon Sapin
rust: Add a `ConfigValueParseError` variant to common errors...
r47340 #[derive(Debug)]
pub struct ConfigValueParseError {
pub origin: ConfigOrigin,
pub line: Option<usize>,
pub section: Vec<u8>,
pub item: Vec<u8>,
pub value: Vec<u8>,
pub expected_type: &'static str,
}
Simon Sapin
rhg: Add more conversions between error types...
r47555 impl fmt::Display for ConfigValueParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// TODO: add origin and line number information, here and in
// corresponding python code
write!(
f,
"config error: {}.{} is not a {} ('{}')",
String::from_utf8_lossy(&self.section),
String::from_utf8_lossy(&self.item),
self.expected_type,
String::from_utf8_lossy(&self.value)
)
}
}
Raphaël Gomès
hg-core: add basic config module...
r46803 impl Config {
Simon Sapin
rhg: Pass a &Config to Ui::new...
r49581 /// The configuration to use when printing configuration-loading errors
pub fn empty() -> Self {
Self { layers: Vec::new() }
}
Simon Sapin
rust: Parse system and user configuration...
r47212 /// Load system and user configuration from various files.
///
/// This is also affected by some environment variables.
Pulkit Goyal
rhg: split non_repo_config and `--config` loading in different functions...
r48198 pub fn load_non_repo() -> Result<Self, ConfigError> {
Simon Sapin
rust: Parse system and user configuration...
r47212 let mut config = Self { layers: Vec::new() };
let opt_rc_path = env::var_os("HGRCPATH");
// HGRCPATH replaces system config
if opt_rc_path.is_none() {
config.add_system_config()?
}
Simon Sapin
rhg: Add environment variables for fallback configuration...
r47456
Simon Sapin
rust: Parse system and user configuration...
r47212 config.add_for_environment_variable("EDITOR", b"ui", b"editor");
config.add_for_environment_variable("VISUAL", b"ui", b"editor");
config.add_for_environment_variable("PAGER", b"pager", b"pager");
Simon Sapin
rhg: Add environment variables for fallback configuration...
r47456
// These are set by `run-tests.py --rhg` to enable fallback for the
// entire test suite. Alternatives would be setting configuration
// through `$HGRCPATH` but some tests override that, or changing the
// `hg` shell alias to include `--config` but that disrupts tests that
// print command lines and check expected output.
config.add_for_environment_variable(
"RHG_ON_UNSUPPORTED",
b"rhg",
b"on-unsupported",
);
config.add_for_environment_variable(
"RHG_FALLBACK_EXECUTABLE",
b"rhg",
b"fallback-executable",
);
Simon Sapin
rust: Parse system and user configuration...
r47212 // HGRCPATH replaces user config
if opt_rc_path.is_none() {
config.add_user_config()?
}
if let Some(rc_path) = &opt_rc_path {
for path in env::split_paths(rc_path) {
if !path.as_os_str().is_empty() {
if path.is_dir() {
config.add_trusted_dir(&path)?
} else {
config.add_trusted_file(&path)?
}
}
}
}
Pulkit Goyal
rhg: split non_repo_config and `--config` loading in different functions...
r48198 Ok(config)
}
Simon Sapin
rhg: Add parsing for the --color global CLI argument...
r49583 pub fn load_cli_args(
Pulkit Goyal
rhg: split non_repo_config and `--config` loading in different functions...
r48198 &mut self,
cli_config_args: impl IntoIterator<Item = impl AsRef<[u8]>>,
Simon Sapin
rhg: Add parsing for the --color global CLI argument...
r49583 color_arg: Option<Vec<u8>>,
Pulkit Goyal
rhg: split non_repo_config and `--config` loading in different functions...
r48198 ) -> Result<(), ConfigError> {
Simon Sapin
rhg: Add support for --config CLI arguments...
r47254 if let Some(layer) = ConfigLayer::parse_cli_args(cli_config_args)? {
Pulkit Goyal
rhg: split non_repo_config and `--config` loading in different functions...
r48198 self.layers.push(layer)
Simon Sapin
rhg: Add support for --config CLI arguments...
r47254 }
Simon Sapin
rhg: Add parsing for the --color global CLI argument...
r49583 if let Some(arg) = color_arg {
let mut layer = ConfigLayer::new(ConfigOrigin::CommandLineColor);
layer.add(b"ui"[..].into(), b"color"[..].into(), arg, None);
self.layers.push(layer)
}
Pulkit Goyal
rhg: split non_repo_config and `--config` loading in different functions...
r48198 Ok(())
Simon Sapin
rust: Parse system and user configuration...
r47212 }
fn add_trusted_dir(&mut self, path: &Path) -> Result<(), ConfigError> {
if let Some(entries) = std::fs::read_dir(path)
Simon Sapin
rust: Add a log file rotation utility...
r47341 .when_reading_file(path)
Simon Sapin
rust: Parse system and user configuration...
r47212 .io_not_found_as_none()?
{
Simon Sapin
rhg: Sort config files when adding a directory...
r47466 let mut file_paths = entries
.map(|result| {
result.when_reading_file(path).map(|entry| entry.path())
})
.collect::<Result<Vec<_>, _>>()?;
file_paths.sort();
for file_path in &file_paths {
Simon Sapin
rust: Parse system and user configuration...
r47212 if file_path.extension() == Some(std::ffi::OsStr::new("rc")) {
self.add_trusted_file(&file_path)?
}
}
}
Ok(())
}
fn add_trusted_file(&mut self, path: &Path) -> Result<(), ConfigError> {
Simon Sapin
rust: Add a log file rotation utility...
r47341 if let Some(data) = std::fs::read(path)
.when_reading_file(path)
.io_not_found_as_none()?
Simon Sapin
rust: Parse system and user configuration...
r47212 {
self.layers.extend(ConfigLayer::parse(path, &data)?)
}
Ok(())
}
fn add_for_environment_variable(
&mut self,
var: &str,
section: &[u8],
key: &[u8],
) {
if let Some(value) = env::var_os(var) {
let origin = layer::ConfigOrigin::Environment(var.into());
let mut layer = ConfigLayer::new(origin);
layer.add(
section.to_owned(),
key.to_owned(),
Simon Sapin
rust: Introduce a get_bytes_from_os_str utility function...
r47338 get_bytes_from_os_str(value),
Simon Sapin
rust: Parse system and user configuration...
r47212 None,
);
self.layers.push(layer)
}
}
#[cfg(unix)] // TODO: other platforms
fn add_system_config(&mut self) -> Result<(), ConfigError> {
let mut add_for_prefix = |prefix: &Path| -> Result<(), ConfigError> {
let etc = prefix.join("etc").join("mercurial");
self.add_trusted_file(&etc.join("hgrc"))?;
self.add_trusted_dir(&etc.join("hgrc.d"))
};
let root = Path::new("/");
// TODO: use `std::env::args_os().next().unwrap()` a.k.a. argv[0]
// instead? TODO: can this be a relative path?
let hg = crate::utils::current_exe()?;
// TODO: this order (per-installation then per-system) matches
// `systemrcpath()` in `mercurial/scmposix.py`, but
// `mercurial/helptext/config.txt` suggests it should be reversed
if let Some(installation_prefix) = hg.parent().and_then(Path::parent) {
if installation_prefix != root {
add_for_prefix(&installation_prefix)?
}
}
add_for_prefix(root)?;
Ok(())
}
#[cfg(unix)] // TODO: other plateforms
fn add_user_config(&mut self) -> Result<(), ConfigError> {
let opt_home = home::home_dir();
if let Some(home) = &opt_home {
self.add_trusted_file(&home.join(".hgrc"))?
}
let darwin = cfg!(any(target_os = "macos", target_os = "ios"));
if !darwin {
if let Some(config_home) = env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.or_else(|| opt_home.map(|home| home.join(".config")))
{
self.add_trusted_file(&config_home.join("hg").join("hgrc"))?
}
}
Ok(())
}
Raphaël Gomès
hg-core: add basic config module...
r46803 /// Loads in order, which means that the precedence is the same
/// as the order of `sources`.
pub fn load_from_explicit_sources(
sources: Vec<ConfigSource>,
) -> Result<Self, ConfigError> {
let mut layers = vec![];
for source in sources.into_iter() {
match source {
ConfigSource::Parsed(c) => layers.push(c),
ConfigSource::AbsPath(c) => {
// TODO check if it should be trusted
// mercurial/ui.py:427
Simon Sapin
rust: replace read_whole_file with std::fs::read...
r47210 let data = match std::fs::read(&c) {
Raphaël Gomès
hg-core: add basic config module...
r46803 Err(_) => continue, // same as the python code
Ok(data) => data,
};
layers.extend(ConfigLayer::parse(&c, &data)?)
}
}
}
Ok(Config { layers })
}
Simon Sapin
rhg: Parse per-repository configuration...
r47215 /// Loads the per-repository config into a new `Config` which is combined
/// with `self`.
pub(crate) fn combine_with_repo(
&self,
repo_config_files: &[PathBuf],
) -> Result<Self, ConfigError> {
let (cli_layers, other_layers) = self
.layers
.iter()
.cloned()
.partition(ConfigLayer::is_from_command_line);
let mut repo_config = Self {
layers: other_layers,
};
for path in repo_config_files {
// TODO: check if this file should be trusted:
// `mercurial/ui.py:427`
repo_config.add_trusted_file(path)?;
}
repo_config.layers.extend(cli_layers);
Ok(repo_config)
Raphaël Gomès
hg-core: add basic config module...
r46803 }
Simon Sapin
rust: Add config parsing support for more value types...
r47339 fn get_parse<'config, T: 'config>(
&'config self,
Raphaël Gomès
hg-core: add basic config module...
r46803 section: &[u8],
item: &[u8],
Simon Sapin
rust: Add a `ConfigValueParseError` variant to common errors...
r47340 expected_type: &'static str,
Simon Sapin
rust: Add config parsing support for more value types...
r47339 parse: impl Fn(&'config [u8]) -> Option<T>,
Simon Sapin
rust: Add a `ConfigValueParseError` variant to common errors...
r47340 ) -> Result<Option<T>, ConfigValueParseError> {
Raphaël Gomès
hg-core: add basic config module...
r46803 match self.get_inner(&section, &item) {
Simon Sapin
rust: Add config parsing support for more value types...
r47339 Some((layer, v)) => match parse(&v.bytes) {
Raphaël Gomès
hg-core: add basic config module...
r46803 Some(b) => Ok(Some(b)),
Simon Sapin
rust: Add a `ConfigValueParseError` variant to common errors...
r47340 None => Err(ConfigValueParseError {
Raphaël Gomès
hg-core: add basic config module...
r46803 origin: layer.origin.to_owned(),
line: v.line,
Simon Sapin
rust: Add a `ConfigValueParseError` variant to common errors...
r47340 value: v.bytes.to_owned(),
section: section.to_owned(),
item: item.to_owned(),
expected_type,
Raphaël Gomès
hg-core: add basic config module...
r46803 }),
},
None => Ok(None),
}
}
Simon Sapin
rust: Add config parsing support for more value types...
r47339 /// Returns an `Err` if the first value found is not a valid UTF-8 string.
/// Otherwise, returns an `Ok(value)` if found, or `None`.
pub fn get_str(
&self,
section: &[u8],
item: &[u8],
Simon Sapin
rust: Add a `ConfigValueParseError` variant to common errors...
r47340 ) -> Result<Option<&str>, ConfigValueParseError> {
self.get_parse(section, item, "ASCII or UTF-8 string", |value| {
str::from_utf8(value).ok()
})
Simon Sapin
rust: Add config parsing support for more value types...
r47339 }
/// Returns an `Err` if the first value found is not a valid unsigned
/// integer. Otherwise, returns an `Ok(value)` if found, or `None`.
pub fn get_u32(
&self,
section: &[u8],
item: &[u8],
Simon Sapin
rust: Add a `ConfigValueParseError` variant to common errors...
r47340 ) -> Result<Option<u32>, ConfigValueParseError> {
self.get_parse(section, item, "valid integer", |value| {
Simon Sapin
rust: Add config parsing support for more value types...
r47339 str::from_utf8(value).ok()?.parse().ok()
})
}
/// Returns an `Err` if the first value found is not a valid file size
/// value such as `30` (default unit is bytes), `7 MB`, or `42.5 kb`.
/// Otherwise, returns an `Ok(value_in_bytes)` if found, or `None`.
pub fn get_byte_size(
&self,
section: &[u8],
item: &[u8],
Simon Sapin
rust: Add a `ConfigValueParseError` variant to common errors...
r47340 ) -> Result<Option<u64>, ConfigValueParseError> {
Simon Sapin
rust: Move config value parsing functions to a new module...
r47344 self.get_parse(section, item, "byte quantity", values::parse_byte_size)
Simon Sapin
rust: Add config parsing support for more value types...
r47339 }
/// Returns an `Err` if the first value found is not a valid boolean.
/// Otherwise, returns an `Ok(option)`, where `option` is the boolean if
/// found, or `None`.
pub fn get_option(
&self,
section: &[u8],
item: &[u8],
Simon Sapin
rust: Add a `ConfigValueParseError` variant to common errors...
r47340 ) -> Result<Option<bool>, ConfigValueParseError> {
Simon Sapin
rust: Move config value parsing functions to a new module...
r47344 self.get_parse(section, item, "boolean", values::parse_bool)
Simon Sapin
rust: Add config parsing support for more value types...
r47339 }
Raphaël Gomès
hg-core: add basic config module...
r46803 /// Returns the corresponding boolean in the config. Returns `Ok(false)`
/// if the value is not found, an `Err` if it's not a valid boolean.
pub fn get_bool(
&self,
section: &[u8],
item: &[u8],
Simon Sapin
rust: Add a `ConfigValueParseError` variant to common errors...
r47340 ) -> Result<bool, ConfigValueParseError> {
Raphaël Gomès
hg-core: add basic config module...
r46803 Ok(self.get_option(section, item)?.unwrap_or(false))
}
Raphaël Gomès
rust: add function to check if an extension is enabled...
r49242 /// Returns `true` if the extension is enabled, `false` otherwise
pub fn is_extension_enabled(&self, extension: &[u8]) -> bool {
let value = self.get(b"extensions", extension);
match value {
Some(c) => !c.starts_with(b"!"),
None => false,
}
}
Simon Sapin
rhg: Port Python’s `ui.configlist` as `Config::get_list`...
r48762 /// If there is an `item` value in `section`, parse and return a list of
/// byte strings.
pub fn get_list(
&self,
section: &[u8],
item: &[u8],
) -> Option<Vec<Vec<u8>>> {
self.get(section, item).map(values::parse_list)
}
Raphaël Gomès
hg-core: add basic config module...
r46803 /// Returns the raw value bytes of the first one found, or `None`.
pub fn get(&self, section: &[u8], item: &[u8]) -> Option<&[u8]> {
self.get_inner(section, item)
.map(|(_, value)| value.bytes.as_ref())
}
Simon Sapin
rhg: Add support for colored output...
r49584 /// Returns the raw value bytes of the first one found, or `None`.
pub fn get_with_origin(
&self,
section: &[u8],
item: &[u8],
) -> Option<(&[u8], &ConfigOrigin)> {
self.get_inner(section, item)
.map(|(layer, value)| (value.bytes.as_ref(), &layer.origin))
}
Raphaël Gomès
hg-core: add basic config module...
r46803 /// Returns the layer and the value of the first one found, or `None`.
fn get_inner(
&self,
section: &[u8],
item: &[u8],
) -> Option<(&ConfigLayer, &ConfigValue)> {
for layer in self.layers.iter().rev() {
if !layer.trusted {
continue;
}
if let Some(v) = layer.get(&section, &item) {
return Some((&layer, v));
}
}
None
}
Simon Sapin
rhg: Fall back to Python if unsupported extensions are enabled...
r47467 /// Return all keys defined for the given section
pub fn get_section_keys(&self, section: &[u8]) -> HashSet<&[u8]> {
self.layers
.iter()
.flat_map(|layer| layer.iter_keys(section))
.collect()
}
Simon Sapin
rhg: [encode] and [decode] config sections are not supported...
r49162 /// Returns whether any key is defined in the given section
pub fn has_non_empty_section(&self, section: &[u8]) -> bool {
self.layers
.iter()
.any(|layer| layer.has_non_empty_section(section))
}
Simon Sapin
rhg: Add support for ui.ignore and ui.ignore.* config...
r49282 /// Yields (key, value) pairs for everything in the given section
pub fn iter_section<'a>(
&'a self,
section: &'a [u8],
) -> impl Iterator<Item = (&[u8], &[u8])> + 'a {
// TODO: Use `Iterator`’s `.peekable()` when its `peek_mut` is
// available:
// https://doc.rust-lang.org/nightly/std/iter/struct.Peekable.html#method.peek_mut
struct Peekable<I: Iterator> {
iter: I,
/// Remember a peeked value, even if it was None.
peeked: Option<Option<I::Item>>,
}
impl<I: Iterator> Peekable<I> {
fn new(iter: I) -> Self {
Self { iter, peeked: None }
}
fn next(&mut self) {
self.peeked = None
}
fn peek_mut(&mut self) -> Option<&mut I::Item> {
let iter = &mut self.iter;
self.peeked.get_or_insert_with(|| iter.next()).as_mut()
}
}
// Deduplicate keys redefined in multiple layers
let mut keys_already_seen = HashSet::new();
let mut key_is_new =
move |&(key, _value): &(&'a [u8], &'a [u8])| -> bool {
keys_already_seen.insert(key)
};
// This is similar to `flat_map` + `filter_map`, except with a single
// closure that owns `key_is_new` (and therefore the
// `keys_already_seen` set):
let mut layer_iters = Peekable::new(
self.layers
.iter()
.rev()
.map(move |layer| layer.iter_section(section)),
);
std::iter::from_fn(move || loop {
if let Some(pair) = layer_iters.peek_mut()?.find(&mut key_is_new) {
return Some(pair);
} else {
layer_iters.next();
}
})
}
Raphaël Gomès
hg-core: add basic config module...
r46803 /// Get raw values bytes from all layers (even untrusted ones) in order
/// of precedence.
#[cfg(test)]
fn get_all(&self, section: &[u8], item: &[u8]) -> Vec<&[u8]> {
let mut res = vec![];
for layer in self.layers.iter().rev() {
if let Some(v) = layer.get(&section, &item) {
res.push(v.bytes.as_ref());
}
}
res
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use std::fs::File;
use std::io::Write;
#[test]
fn test_include_layer_ordering() {
let tmpdir = tempfile::tempdir().unwrap();
let tmpdir_path = tmpdir.path();
let mut included_file =
File::create(&tmpdir_path.join("included.rc")).unwrap();
included_file.write_all(b"[section]\nitem=value1").unwrap();
let base_config_path = tmpdir_path.join("base.rc");
let mut config_file = File::create(&base_config_path).unwrap();
let data =
Simon Sapin
rust: Add config parsing support for more value types...
r47339 b"[section]\nitem=value0\n%include included.rc\nitem=value2\n\
[section2]\ncount = 4\nsize = 1.5 KB\nnot-count = 1.5\nnot-size = 1 ub";
Raphaël Gomès
hg-core: add basic config module...
r46803 config_file.write_all(data).unwrap();
let sources = vec![ConfigSource::AbsPath(base_config_path)];
let config = Config::load_from_explicit_sources(sources)
.expect("expected valid config");
let (_, value) = config.get_inner(b"section", b"item").unwrap();
assert_eq!(
value,
&ConfigValue {
bytes: b"value2".to_vec(),
line: Some(4)
}
);
let value = config.get(b"section", b"item").unwrap();
assert_eq!(value, b"value2",);
assert_eq!(
config.get_all(b"section", b"item"),
[b"value2", b"value1", b"value0"]
);
Simon Sapin
rust: Add config parsing support for more value types...
r47339
assert_eq!(config.get_u32(b"section2", b"count").unwrap(), Some(4));
assert_eq!(
config.get_byte_size(b"section2", b"size").unwrap(),
Some(1024 + 512)
);
assert!(config.get_u32(b"section2", b"not-count").is_err());
assert!(config.get_byte_size(b"section2", b"not-size").is_err());
Raphaël Gomès
hg-core: add basic config module...
r46803 }
}