nu_data/
config.rs

1mod conf;
2mod config_trust;
3mod local_config;
4mod nuconfig;
5pub mod path;
6
7pub use conf::Conf;
8pub use config_trust::is_file_trusted;
9pub use config_trust::read_trusted;
10pub use config_trust::Trusted;
11pub use local_config::loadable_cfg_exists_in_dir;
12pub use local_config::LocalConfigDiff;
13pub use nuconfig::NuConfig;
14
15use indexmap::IndexMap;
16use log::trace;
17use nu_errors::{CoerceInto, ShellError};
18use nu_protocol::{
19    Dictionary, Primitive, ShellTypeName, TaggedDictBuilder, UnspannedPathMember, UntaggedValue,
20    Value,
21};
22use nu_source::{SpannedItem, Tag, TaggedItem};
23use std::env::var;
24use std::fs::{self, OpenOptions};
25use std::io;
26use std::path::{Path, PathBuf};
27
28pub fn convert_toml_value_to_nu_value(v: &toml::Value, tag: impl Into<Tag>) -> Value {
29    let tag = tag.into();
30
31    match v {
32        toml::Value::Boolean(b) => UntaggedValue::boolean(*b).into_value(tag),
33        toml::Value::Integer(n) => UntaggedValue::int(*n).into_value(tag),
34        toml::Value::Float(n) => UntaggedValue::decimal_from_float(*n, tag.span).into_value(tag),
35        toml::Value::String(s) => {
36            UntaggedValue::Primitive(Primitive::String(String::from(s))).into_value(tag)
37        }
38        toml::Value::Array(a) => UntaggedValue::Table(
39            a.iter()
40                .map(|x| convert_toml_value_to_nu_value(x, &tag))
41                .collect(),
42        )
43        .into_value(tag),
44        toml::Value::Datetime(dt) => {
45            UntaggedValue::Primitive(Primitive::String(dt.to_string())).into_value(tag)
46        }
47        toml::Value::Table(t) => {
48            let mut collected = TaggedDictBuilder::new(&tag);
49
50            for (k, v) in t {
51                collected.insert_value(k.clone(), convert_toml_value_to_nu_value(v, &tag));
52            }
53
54            collected.into_value()
55        }
56    }
57}
58
59fn collect_values(input: &[Value]) -> Result<Vec<toml::Value>, ShellError> {
60    let mut out = vec![];
61
62    for value in input {
63        out.push(helper(value)?);
64    }
65
66    Ok(out)
67}
68// Helper method to recursively convert nu_protocol::Value -> toml::Value
69// This shouldn't be called at the top-level
70fn helper(v: &Value) -> Result<toml::Value, ShellError> {
71    use bigdecimal::ToPrimitive;
72
73    Ok(match &v.value {
74        UntaggedValue::Primitive(Primitive::Boolean(b)) => toml::Value::Boolean(*b),
75        UntaggedValue::Primitive(Primitive::Filesize(b)) => {
76            if let Some(value) = b.to_i64() {
77                toml::Value::Integer(value)
78            } else {
79                return Err(ShellError::labeled_error(
80                    "Value too large to convert to toml value",
81                    "value too large",
82                    v.tag.span,
83                ));
84            }
85        }
86        UntaggedValue::Primitive(Primitive::Duration(i)) => toml::Value::String(i.to_string()),
87        UntaggedValue::Primitive(Primitive::Date(d)) => toml::Value::String(d.to_string()),
88        UntaggedValue::Primitive(Primitive::EndOfStream) => {
89            toml::Value::String("<End of Stream>".to_string())
90        }
91        UntaggedValue::Primitive(Primitive::BeginningOfStream) => {
92            toml::Value::String("<Beginning of Stream>".to_string())
93        }
94        UntaggedValue::Primitive(Primitive::Decimal(f)) => {
95            toml::Value::Float(f.tagged(&v.tag).coerce_into("converting to TOML float")?)
96        }
97        UntaggedValue::Primitive(Primitive::Int(i)) => toml::Value::Integer(*i),
98        UntaggedValue::Primitive(Primitive::BigInt(i)) => {
99            toml::Value::Integer(i.tagged(&v.tag).coerce_into("converting to TOML integer")?)
100        }
101        UntaggedValue::Primitive(Primitive::Nothing) => {
102            toml::Value::String("<Nothing>".to_string())
103        }
104        UntaggedValue::Primitive(Primitive::GlobPattern(s)) => toml::Value::String(s.clone()),
105        UntaggedValue::Primitive(Primitive::String(s)) => toml::Value::String(s.clone()),
106        UntaggedValue::Primitive(Primitive::FilePath(s)) => {
107            toml::Value::String(s.display().to_string())
108        }
109        UntaggedValue::Primitive(Primitive::ColumnPath(path)) => toml::Value::Array(
110            path.iter()
111                .map(|x| match &x.unspanned {
112                    UnspannedPathMember::String(string) => Ok(toml::Value::String(string.clone())),
113                    UnspannedPathMember::Int(int) => Ok(toml::Value::Integer(*int)),
114                })
115                .collect::<Result<Vec<toml::Value>, ShellError>>()?,
116        ),
117        UntaggedValue::Table(l) => toml::Value::Array(collect_values(l)?),
118        UntaggedValue::Error(e) => return Err(e.clone()),
119        UntaggedValue::Block(_) => toml::Value::String("<Block>".to_string()),
120        #[cfg(feature = "dataframe")]
121        UntaggedValue::DataFrame(_) | UntaggedValue::FrameStruct(_) => {
122            toml::Value::String("<DataFrame>".to_string())
123        }
124        UntaggedValue::Primitive(Primitive::Range(_)) => toml::Value::String("<Range>".to_string()),
125        UntaggedValue::Primitive(Primitive::Binary(b)) => {
126            toml::Value::Array(b.iter().map(|x| toml::Value::Integer(*x as i64)).collect())
127        }
128        UntaggedValue::Row(o) => {
129            let mut m = toml::map::Map::new();
130            for (k, v) in &o.entries {
131                m.insert(k.clone(), helper(v)?);
132            }
133            toml::Value::Table(m)
134        }
135    })
136}
137
138/// Converts a nu_protocol::Value into a toml::Value
139/// Will return a Shell Error, if the Nu Value is not a valid top-level TOML Value
140pub fn value_to_toml_value(v: &Value) -> Result<toml::Value, ShellError> {
141    match &v.value {
142        UntaggedValue::Row(o) => {
143            let mut m = toml::map::Map::new();
144            for (k, v) in &o.entries {
145                m.insert(k.clone(), helper(v)?);
146            }
147            Ok(toml::Value::Table(m))
148        }
149        UntaggedValue::Primitive(Primitive::String(s)) => {
150            // Attempt to de-serialize the String
151            toml::de::from_str(s).map_err(|_| {
152                ShellError::labeled_error(
153                    format!("{:?} unable to de-serialize string to TOML", s),
154                    "invalid TOML",
155                    v.tag(),
156                )
157            })
158        }
159        _ => Err(ShellError::labeled_error(
160            format!("{:?} is not a valid top-level TOML", v.value),
161            "invalid TOML",
162            v.tag(),
163        )),
164    }
165}
166
167pub fn config_path() -> Result<PathBuf, ShellError> {
168    use directories_next::ProjectDirs;
169
170    let dir = ProjectDirs::from("org", "nushell", "nu")
171        .ok_or_else(|| ShellError::untagged_runtime_error("Couldn't find project directory"))?;
172    let path = var("NU_CONFIG_DIR").map_or(ProjectDirs::config_dir(&dir).to_owned(), |path| {
173        PathBuf::from(path)
174    });
175    std::fs::create_dir_all(&path).map_err(|err| {
176        ShellError::untagged_runtime_error(&format!("Couldn't create {} path:\n{}", "config", err))
177    })?;
178
179    Ok(path)
180}
181
182pub fn default_path() -> Result<PathBuf, ShellError> {
183    default_path_for(&None)
184}
185
186pub fn default_path_for(file: &Option<PathBuf>) -> Result<PathBuf, ShellError> {
187    let mut filename = config_path()?;
188    let file: &Path = file.as_deref().unwrap_or_else(|| "config.toml".as_ref());
189    filename.push(file);
190
191    Ok(filename)
192}
193
194pub fn user_data() -> Result<PathBuf, ShellError> {
195    use directories_next::ProjectDirs;
196
197    let dir = ProjectDirs::from("org", "nushell", "nu")
198        .ok_or_else(|| ShellError::untagged_runtime_error("Couldn't find project directory"))?;
199    let path = ProjectDirs::data_local_dir(&dir).to_owned();
200    std::fs::create_dir_all(&path).map_err(|err| {
201        ShellError::untagged_runtime_error(&format!(
202            "Couldn't create {} path:\n{}",
203            "user data", err
204        ))
205    })?;
206
207    Ok(path)
208}
209
210#[derive(Debug, Clone)]
211pub enum Status {
212    LastModified(std::time::SystemTime),
213    Unavailable,
214}
215
216impl Default for Status {
217    fn default() -> Self {
218        Status::Unavailable
219    }
220}
221
222pub fn last_modified(at: &Option<PathBuf>) -> Result<Status, Box<dyn std::error::Error>> {
223    let filename = default_path()?;
224
225    let filename = match at {
226        None => filename,
227        Some(ref file) => file.clone(),
228    };
229
230    if let Ok(time) = filename.metadata()?.modified() {
231        return Ok(Status::LastModified(time));
232    }
233
234    Ok(Status::Unavailable)
235}
236
237pub fn read(
238    tag: impl Into<Tag>,
239    at: &Option<PathBuf>,
240) -> Result<IndexMap<String, Value>, ShellError> {
241    let filename = default_path()?;
242
243    let filename = match at {
244        None => filename,
245        Some(ref file) => file.clone(),
246    };
247
248    if !filename.exists() && touch(&filename).is_err() {
249        // If we can't create configs, let's just return an empty indexmap instead as we may be in
250        // a readonly environment
251        return Ok(IndexMap::new());
252    }
253
254    trace!("config file = {}", filename.display());
255
256    let tag = tag.into();
257    let contents = fs::read_to_string(filename)
258        .map(|v| v.tagged(&tag))
259        .map_err(|err| {
260            ShellError::labeled_error(
261                &format!("Couldn't read config file:\n{}", err),
262                "file name",
263                &tag,
264            )
265        })?;
266
267    let parsed: toml::Value = toml::from_str(&contents).map_err(|err| {
268        ShellError::labeled_error(
269            &format!("Couldn't parse config file:\n{}", err),
270            "file name",
271            &tag,
272        )
273    })?;
274
275    let value = convert_toml_value_to_nu_value(&parsed, tag);
276    let tag = value.tag();
277    match value.value {
278        UntaggedValue::Row(Dictionary { entries }) => Ok(entries),
279        other => Err(ShellError::type_error(
280            "Dictionary",
281            other.type_name().spanned(tag.span),
282        )),
283    }
284}
285
286pub fn config(tag: impl Into<Tag>) -> Result<IndexMap<String, Value>, ShellError> {
287    read(tag, &None)
288}
289
290pub fn write(config: &IndexMap<String, Value>, at: &Option<PathBuf>) -> Result<(), ShellError> {
291    let filename = default_path()?;
292
293    let filename = match at {
294        None => filename,
295        Some(ref file) => file.clone(),
296    };
297
298    let contents = value_to_toml_value(
299        &UntaggedValue::Row(Dictionary::new(config.clone())).into_untagged_value(),
300    )?;
301
302    let contents = toml::to_string(&contents)?;
303
304    fs::write(&filename, &contents)?;
305
306    Ok(())
307}
308
309// A simple implementation of `% touch path` (ignores existing files)
310fn touch(path: &Path) -> io::Result<()> {
311    match OpenOptions::new().create(true).write(true).open(path) {
312        Ok(_) => Ok(()),
313        Err(e) => Err(e),
314    }
315}
316
317pub fn cfg_path_to_scope_tag(cfg_path: &Path) -> String {
318    cfg_path.to_string_lossy().to_string()
319}