diff --git a/Cargo.lock b/Cargo.lock index 89e5127356..902210ffbc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -605,7 +605,7 @@ dependencies = [ "encoding_rs", "log", "once_cell", - "quick-xml", + "quick-xml 0.31.0", "serde", "zip", ] @@ -3094,7 +3094,7 @@ dependencies = [ "pretty_assertions", "print-positions", "procfs", - "quick-xml", + "quick-xml 0.31.0", "quickcheck", "quickcheck_macros", "rand", @@ -3476,12 +3476,14 @@ dependencies = [ name = "nu_plugin_formats" version = "0.96.2" dependencies = [ + "chrono", "eml-parser", "ical", "indexmap", "nu-plugin", "nu-plugin-test-support", "nu-protocol", + "plist", "rust-ini", ] @@ -4148,6 +4150,19 @@ dependencies = [ "winapi", ] +[[package]] +name = "plist" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" +dependencies = [ + "base64 0.22.1", + "indexmap", + "quick-xml 0.32.0", + "serde", + "time", +] + [[package]] name = "polars" version = "0.41.2" @@ -4816,6 +4831,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" +dependencies = [ + "memchr", +] + [[package]] name = "quickcheck" version = "1.0.3" @@ -6872,7 +6896,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b3a62929287001986fb58c789dce9b67604a397c15c611ad9f747300b6c283" dependencies = [ "proc-macro2", - "quick-xml", + "quick-xml 0.31.0", "quote", ] diff --git a/crates/nu_plugin_formats/Cargo.toml b/crates/nu_plugin_formats/Cargo.toml index dbf1b43657..f6ad84758f 100644 --- a/crates/nu_plugin_formats/Cargo.toml +++ b/crates/nu_plugin_formats/Cargo.toml @@ -16,6 +16,8 @@ indexmap = { workspace = true } eml-parser = "0.1" ical = "0.11" rust-ini = "0.21.0" +plist = "1.7" +chrono = "0.4" [dev-dependencies] -nu-plugin-test-support = { path = "../nu-plugin-test-support", version = "0.96.2" } \ No newline at end of file +nu-plugin-test-support = { path = "../nu-plugin-test-support", version = "0.96.2" } diff --git a/crates/nu_plugin_formats/src/from/eml.rs b/crates/nu_plugin_formats/src/from/eml.rs index 2630e3b1c2..2b4c33a814 100644 --- a/crates/nu_plugin_formats/src/from/eml.rs +++ b/crates/nu_plugin_formats/src/from/eml.rs @@ -1,4 +1,4 @@ -use crate::FromCmds; +use crate::FormatCmdsPlugin; use eml_parser::eml::*; use eml_parser::EmlParser; use indexmap::IndexMap; @@ -12,7 +12,7 @@ const DEFAULT_BODY_PREVIEW: usize = 50; pub struct FromEml; impl SimplePluginCommand for FromEml { - type Plugin = FromCmds; + type Plugin = FormatCmdsPlugin; fn name(&self) -> &str { "from eml" @@ -40,7 +40,7 @@ impl SimplePluginCommand for FromEml { fn run( &self, - _plugin: &FromCmds, + _plugin: &FormatCmdsPlugin, _engine: &EngineInterface, call: &EvaluatedCall, input: &Value, @@ -176,5 +176,5 @@ fn from_eml(input: &Value, body_preview: usize, head: Span) -> Result Result<(), nu_protocol::ShellError> { use nu_plugin_test_support::PluginTest; - PluginTest::new("formats", crate::FromCmds.into())?.test_command_examples(&FromEml) + PluginTest::new("formats", crate::FormatCmdsPlugin.into())?.test_command_examples(&FromEml) } diff --git a/crates/nu_plugin_formats/src/from/ics.rs b/crates/nu_plugin_formats/src/from/ics.rs index 099b3431fe..bcd7311f8a 100644 --- a/crates/nu_plugin_formats/src/from/ics.rs +++ b/crates/nu_plugin_formats/src/from/ics.rs @@ -1,4 +1,4 @@ -use crate::FromCmds; +use crate::FormatCmdsPlugin; use ical::{parser::ical::component::*, property::Property}; use indexmap::IndexMap; @@ -11,7 +11,7 @@ use std::io::BufReader; pub struct FromIcs; impl SimplePluginCommand for FromIcs { - type Plugin = FromCmds; + type Plugin = FormatCmdsPlugin; fn name(&self) -> &str { "from ics" @@ -33,7 +33,7 @@ impl SimplePluginCommand for FromIcs { fn run( &self, - _plugin: &FromCmds, + _plugin: &FormatCmdsPlugin, _engine: &EngineInterface, call: &EvaluatedCall, input: &Value, @@ -274,5 +274,5 @@ fn params_to_value(params: Vec<(String, Vec)>, span: Span) -> Value { fn test_examples() -> Result<(), nu_protocol::ShellError> { use nu_plugin_test_support::PluginTest; - PluginTest::new("formats", crate::FromCmds.into())?.test_command_examples(&FromIcs) + PluginTest::new("formats", crate::FormatCmdsPlugin.into())?.test_command_examples(&FromIcs) } diff --git a/crates/nu_plugin_formats/src/from/ini.rs b/crates/nu_plugin_formats/src/from/ini.rs index cf37ffc3d7..bb44ce1398 100644 --- a/crates/nu_plugin_formats/src/from/ini.rs +++ b/crates/nu_plugin_formats/src/from/ini.rs @@ -1,4 +1,4 @@ -use crate::FromCmds; +use crate::FormatCmdsPlugin; use nu_plugin::{EngineInterface, EvaluatedCall, SimplePluginCommand}; use nu_protocol::{ @@ -8,7 +8,7 @@ use nu_protocol::{ pub struct FromIni; impl SimplePluginCommand for FromIni { - type Plugin = FromCmds; + type Plugin = FormatCmdsPlugin; fn name(&self) -> &str { "from ini" @@ -30,7 +30,7 @@ impl SimplePluginCommand for FromIni { fn run( &self, - _plugin: &FromCmds, + _plugin: &FormatCmdsPlugin, _engine: &EngineInterface, call: &EvaluatedCall, input: &Value, @@ -101,5 +101,5 @@ b=2' | from ini", fn test_examples() -> Result<(), nu_protocol::ShellError> { use nu_plugin_test_support::PluginTest; - PluginTest::new("formats", crate::FromCmds.into())?.test_command_examples(&FromIni) + PluginTest::new("formats", crate::FormatCmdsPlugin.into())?.test_command_examples(&FromIni) } diff --git a/crates/nu_plugin_formats/src/from/mod.rs b/crates/nu_plugin_formats/src/from/mod.rs index 4b4976f391..232dde7d6d 100644 --- a/crates/nu_plugin_formats/src/from/mod.rs +++ b/crates/nu_plugin_formats/src/from/mod.rs @@ -1,4 +1,5 @@ -pub mod eml; -pub mod ics; -pub mod ini; -pub mod vcf; +pub(crate) mod eml; +pub(crate) mod ics; +pub(crate) mod ini; +pub(crate) mod plist; +pub(crate) mod vcf; diff --git a/crates/nu_plugin_formats/src/from/plist.rs b/crates/nu_plugin_formats/src/from/plist.rs new file mode 100644 index 0000000000..9894879955 --- /dev/null +++ b/crates/nu_plugin_formats/src/from/plist.rs @@ -0,0 +1,240 @@ +use std::time::SystemTime; + +use chrono::{DateTime, FixedOffset, Offset, Utc}; +use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand, SimplePluginCommand}; +use nu_protocol::{ + record, Category, Example, LabeledError, Record, Signature, Span, Value as NuValue, +}; +use plist::{Date as PlistDate, Dictionary, Value as PlistValue}; + +use crate::FormatCmdsPlugin; + +pub struct FromPlist; + +impl SimplePluginCommand for FromPlist { + type Plugin = FormatCmdsPlugin; + + fn name(&self) -> &str { + "from plist" + } + + fn usage(&self) -> &str { + "Convert plist to Nushell values" + } + + fn examples(&self) -> Vec { + vec![Example { + example: r#"' + + + + a + 3 + +' | from plist"#, + description: "Convert a table into a plist file", + result: Some(NuValue::test_record(record!( "a" => NuValue::test_int(3)))), + }] + } + + fn signature(&self) -> Signature { + Signature::build(PluginCommand::name(self)).category(Category::Formats) + } + + fn run( + &self, + _plugin: &FormatCmdsPlugin, + _engine: &EngineInterface, + call: &EvaluatedCall, + input: &NuValue, + ) -> Result { + match input { + NuValue::String { val, .. } => { + let plist = plist::from_bytes(val.as_bytes()) + .map_err(|e| build_label_error(format!("{}", e), input.span()))?; + let converted = convert_plist_value(&plist, call.head)?; + Ok(converted) + } + NuValue::Binary { val, .. } => { + let plist = plist::from_bytes(val) + .map_err(|e| build_label_error(format!("{}", e), input.span()))?; + let converted = convert_plist_value(&plist, call.head)?; + Ok(converted) + } + _ => Err(build_label_error( + format!("Invalid input, must be string not: {:?}", input), + call.head, + )), + } + } +} + +fn build_label_error(msg: impl Into, span: Span) -> LabeledError { + LabeledError::new("Could not load plist").with_label(msg, span) +} + +fn convert_plist_value(plist_val: &PlistValue, span: Span) -> Result { + match plist_val { + PlistValue::String(s) => Ok(NuValue::string(s.to_owned(), span)), + PlistValue::Boolean(b) => Ok(NuValue::bool(*b, span)), + PlistValue::Real(r) => Ok(NuValue::float(*r, span)), + PlistValue::Date(d) => Ok(NuValue::date(convert_date(d), span)), + PlistValue::Integer(i) => { + let signed = i + .as_signed() + .ok_or_else(|| build_label_error(format!("Cannot convert {i} to i64"), span))?; + Ok(NuValue::int(signed, span)) + } + PlistValue::Uid(uid) => Ok(NuValue::float(uid.get() as f64, span)), + PlistValue::Data(data) => Ok(NuValue::binary(data.to_owned(), span)), + PlistValue::Array(arr) => Ok(NuValue::list(convert_array(arr, span)?, span)), + PlistValue::Dictionary(dict) => Ok(convert_dict(dict, span)?), + _ => Ok(NuValue::nothing(span)), + } +} + +fn convert_dict(dict: &Dictionary, span: Span) -> Result { + let cols: Vec = dict.keys().cloned().collect(); + let vals: Result, LabeledError> = dict + .values() + .map(|v| convert_plist_value(v, span)) + .collect(); + Ok(NuValue::record( + Record::from_raw_cols_vals(cols, vals?, span, span)?, + span, + )) +} + +fn convert_array(plist_array: &[PlistValue], span: Span) -> Result, LabeledError> { + plist_array + .iter() + .map(|v| convert_plist_value(v, span)) + .collect() +} + +pub fn convert_date(plist_date: &PlistDate) -> DateTime { + // In the docs the plist date object is listed as a utc timestamp, so this + // conversion should be fine + let plist_sys_time: SystemTime = plist_date.to_owned().into(); + let utc_date: DateTime = plist_sys_time.into(); + let utc_offset = utc_date.offset().fix(); + utc_date.with_timezone(&utc_offset) +} + +#[cfg(test)] +mod test { + use super::*; + use chrono::Datelike; + use plist::Uid; + use std::time::SystemTime; + + use nu_plugin_test_support::PluginTest; + use nu_protocol::ShellError; + + #[test] + fn test_convert_string() { + let plist_val = PlistValue::String("hello".to_owned()); + let result = convert_plist_value(&plist_val, Span::test_data()); + assert_eq!( + result, + Ok(NuValue::string("hello".to_owned(), Span::test_data())) + ); + } + + #[test] + fn test_convert_boolean() { + let plist_val = PlistValue::Boolean(true); + let result = convert_plist_value(&plist_val, Span::test_data()); + assert_eq!(result, Ok(NuValue::bool(true, Span::test_data()))); + } + + #[test] + fn test_convert_real() { + let plist_val = PlistValue::Real(3.14); + let result = convert_plist_value(&plist_val, Span::test_data()); + assert_eq!(result, Ok(NuValue::float(3.14, Span::test_data()))); + } + + #[test] + fn test_convert_integer() { + let plist_val = PlistValue::Integer(42.into()); + let result = convert_plist_value(&plist_val, Span::test_data()); + assert_eq!(result, Ok(NuValue::int(42, Span::test_data()))); + } + + #[test] + fn test_convert_uid() { + let v = 12345678_u64; + let uid = Uid::new(v); + let plist_val = PlistValue::Uid(uid); + let result = convert_plist_value(&plist_val, Span::test_data()); + assert_eq!(result, Ok(NuValue::float(v as f64, Span::test_data()))); + } + + #[test] + fn test_convert_data() { + let data = vec![0x41, 0x42, 0x43]; + let plist_val = PlistValue::Data(data.clone()); + let result = convert_plist_value(&plist_val, Span::test_data()); + assert_eq!(result, Ok(NuValue::binary(data, Span::test_data()))); + } + + #[test] + fn test_convert_date() { + let epoch = SystemTime::UNIX_EPOCH; + let plist_date = epoch.into(); + + let datetime = convert_date(&plist_date); + assert_eq!(1970, datetime.year()); + assert_eq!(1, datetime.month()); + assert_eq!(1, datetime.day()); + } + + #[test] + fn test_convert_dict() { + let mut dict = Dictionary::new(); + dict.insert("a".to_string(), PlistValue::String("c".to_string())); + dict.insert("b".to_string(), PlistValue::String("d".to_string())); + let nu_dict = convert_dict(&dict, Span::test_data()).unwrap(); + assert_eq!( + nu_dict, + NuValue::record( + Record::from_raw_cols_vals( + vec!["a".to_string(), "b".to_string()], + vec![ + NuValue::string("c".to_string(), Span::test_data()), + NuValue::string("d".to_string(), Span::test_data()) + ], + Span::test_data(), + Span::test_data(), + ) + .expect("failed to create record"), + Span::test_data(), + ) + ); + } + + #[test] + fn test_convert_array() { + let mut arr = Vec::new(); + arr.push(PlistValue::String("a".to_string())); + arr.push(PlistValue::String("b".to_string())); + let nu_arr = convert_array(&arr, Span::test_data()).unwrap(); + assert_eq!( + nu_arr, + vec![ + NuValue::string("a".to_string(), Span::test_data()), + NuValue::string("b".to_string(), Span::test_data()) + ] + ); + } + + #[test] + fn test_examples() -> Result<(), ShellError> { + let plugin = FormatCmdsPlugin {}; + let cmd = FromPlist {}; + + let mut plugin_test = PluginTest::new("polars", plugin.into())?; + plugin_test.test_command_examples(&cmd) + } +} diff --git a/crates/nu_plugin_formats/src/from/vcf.rs b/crates/nu_plugin_formats/src/from/vcf.rs index 4de20154d7..a751a774d3 100644 --- a/crates/nu_plugin_formats/src/from/vcf.rs +++ b/crates/nu_plugin_formats/src/from/vcf.rs @@ -1,4 +1,4 @@ -use crate::FromCmds; +use crate::FormatCmdsPlugin; use ical::{parser::vcard::component::*, property::Property}; use indexmap::IndexMap; @@ -10,7 +10,7 @@ use nu_protocol::{ pub struct FromVcf; impl SimplePluginCommand for FromVcf { - type Plugin = FromCmds; + type Plugin = FormatCmdsPlugin; fn name(&self) -> &str { "from vcf" @@ -32,7 +32,7 @@ impl SimplePluginCommand for FromVcf { fn run( &self, - _plugin: &FromCmds, + _plugin: &FormatCmdsPlugin, _engine: &EngineInterface, call: &EvaluatedCall, input: &Value, @@ -164,5 +164,5 @@ fn params_to_value(params: Vec<(String, Vec)>, span: Span) -> Value { fn test_examples() -> Result<(), nu_protocol::ShellError> { use nu_plugin_test_support::PluginTest; - PluginTest::new("formats", crate::FromCmds.into())?.test_command_examples(&FromVcf) + PluginTest::new("formats", crate::FormatCmdsPlugin.into())?.test_command_examples(&FromVcf) } diff --git a/crates/nu_plugin_formats/src/lib.rs b/crates/nu_plugin_formats/src/lib.rs index 2ae24a4971..ce95fcf8c9 100644 --- a/crates/nu_plugin_formats/src/lib.rs +++ b/crates/nu_plugin_formats/src/lib.rs @@ -1,15 +1,18 @@ mod from; +mod to; use nu_plugin::{Plugin, PluginCommand}; -pub use from::eml::FromEml; -pub use from::ics::FromIcs; -pub use from::ini::FromIni; -pub use from::vcf::FromVcf; +use from::eml::FromEml; +use from::ics::FromIcs; +use from::ini::FromIni; +use from::plist::FromPlist; +use from::vcf::FromVcf; +use to::plist::IntoPlist; -pub struct FromCmds; +pub struct FormatCmdsPlugin; -impl Plugin for FromCmds { +impl Plugin for FormatCmdsPlugin { fn version(&self) -> String { env!("CARGO_PKG_VERSION").into() } @@ -20,6 +23,8 @@ impl Plugin for FromCmds { Box::new(FromIcs), Box::new(FromIni), Box::new(FromVcf), + Box::new(FromPlist), + Box::new(IntoPlist), ] } } diff --git a/crates/nu_plugin_formats/src/main.rs b/crates/nu_plugin_formats/src/main.rs index e6c7179781..f36ba3364a 100644 --- a/crates/nu_plugin_formats/src/main.rs +++ b/crates/nu_plugin_formats/src/main.rs @@ -1,6 +1,6 @@ use nu_plugin::{serve_plugin, MsgPackSerializer}; -use nu_plugin_formats::FromCmds; +use nu_plugin_formats::FormatCmdsPlugin; fn main() { - serve_plugin(&FromCmds, MsgPackSerializer {}) + serve_plugin(&FormatCmdsPlugin, MsgPackSerializer {}) } diff --git a/crates/nu_plugin_formats/src/to/mod.rs b/crates/nu_plugin_formats/src/to/mod.rs new file mode 100644 index 0000000000..2f804de5d6 --- /dev/null +++ b/crates/nu_plugin_formats/src/to/mod.rs @@ -0,0 +1 @@ +pub(crate) mod plist; diff --git a/crates/nu_plugin_formats/src/to/plist.rs b/crates/nu_plugin_formats/src/to/plist.rs new file mode 100644 index 0000000000..94368c184a --- /dev/null +++ b/crates/nu_plugin_formats/src/to/plist.rs @@ -0,0 +1,113 @@ +use std::time::SystemTime; + +use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand, SimplePluginCommand}; +use nu_protocol::{Category, Example, LabeledError, Record, Signature, Span, Value as NuValue}; +use plist::{Integer, Value as PlistValue}; + +use crate::FormatCmdsPlugin; + +pub(crate) struct IntoPlist; + +impl SimplePluginCommand for IntoPlist { + type Plugin = FormatCmdsPlugin; + + fn name(&self) -> &str { + "to plist" + } + + fn usage(&self) -> &str { + "Convert Nu values into plist" + } + + fn examples(&self) -> Vec { + vec![Example { + example: "{ a: 3 } | to plist", + description: "Convert a table into a plist file", + result: None, + }] + } + + fn signature(&self) -> Signature { + Signature::build(PluginCommand::name(self)) + .switch("binary", "Output plist in binary format", Some('b')) + .category(Category::Formats) + } + + fn run( + &self, + _plugin: &FormatCmdsPlugin, + _engine: &EngineInterface, + call: &EvaluatedCall, + input: &NuValue, + ) -> Result { + let plist_val = convert_nu_value(input)?; + let mut out = Vec::new(); + if call.has_flag("binary")? { + plist::to_writer_binary(&mut out, &plist_val) + .map_err(|e| build_label_error(format!("{}", e), input.span()))?; + Ok(NuValue::binary(out, input.span())) + } else { + plist::to_writer_xml(&mut out, &plist_val) + .map_err(|e| build_label_error(format!("{}", e), input.span()))?; + Ok(NuValue::string( + String::from_utf8(out) + .map_err(|e| build_label_error(format!("{}", e), input.span()))?, + input.span(), + )) + } + } +} + +fn build_label_error(msg: String, span: Span) -> LabeledError { + LabeledError::new("Cannot convert plist").with_label(msg, span) +} + +fn convert_nu_value(nu_val: &NuValue) -> Result { + let span = Span::test_data(); + match nu_val { + NuValue::String { val, .. } => Ok(PlistValue::String(val.to_owned())), + NuValue::Bool { val, .. } => Ok(PlistValue::Boolean(*val)), + NuValue::Float { val, .. } => Ok(PlistValue::Real(*val)), + NuValue::Int { val, .. } => Ok(PlistValue::Integer(Into::::into(*val))), + NuValue::Binary { val, .. } => Ok(PlistValue::Data(val.to_owned())), + NuValue::Record { val, .. } => convert_nu_dict(val), + NuValue::List { vals, .. } => Ok(PlistValue::Array( + vals.iter() + .map(convert_nu_value) + .collect::>()?, + )), + NuValue::Date { val, .. } => Ok(PlistValue::Date(SystemTime::from(val.to_owned()).into())), + NuValue::Filesize { val, .. } => Ok(PlistValue::Integer(Into::::into(*val))), + _ => Err(build_label_error( + format!("{:?} is not convertible", nu_val), + span, + )), + } +} + +fn convert_nu_dict(record: &Record) -> Result { + Ok(PlistValue::Dictionary( + record + .iter() + .map(|(k, v)| convert_nu_value(v).map(|v| (k.to_owned(), v))) + .collect::>()?, + )) +} + +#[cfg(test)] +mod test { + + use nu_plugin_test_support::PluginTest; + use nu_protocol::ShellError; + + use super::*; + + #[test] + fn test_examples() -> Result<(), ShellError> { + let plugin = FormatCmdsPlugin {}; + let cmd = IntoPlist {}; + + let mut plugin_test = PluginTest::new("polars", plugin.into())?; + plugin_test.test_command_examples(&cmd) + } +}