nu_command/formats/to/
msgpack.rs

1// Credit to https://github.com/hulthe/nu_plugin_msgpack for the original idea, though the
2// implementation here is unique.
3
4use std::io;
5
6use byteorder::{BigEndian, WriteBytesExt};
7use nu_engine::command_prelude::*;
8use nu_protocol::{Signals, Spanned, ast::PathMember, shell_error::io::IoError};
9use rmp::encode as mp;
10
11/// Max recursion depth
12const MAX_DEPTH: usize = 50;
13
14#[derive(Clone)]
15pub struct ToMsgpack;
16
17impl Command for ToMsgpack {
18    fn name(&self) -> &str {
19        "to msgpack"
20    }
21
22    fn signature(&self) -> Signature {
23        Signature::build(self.name())
24            .input_output_type(Type::Any, Type::Binary)
25            .switch(
26                "serialize",
27                "serialize nushell types that cannot be deserialized",
28                Some('s'),
29            )
30            .category(Category::Formats)
31    }
32
33    fn description(&self) -> &str {
34        "Convert Nu values into MessagePack."
35    }
36
37    fn extra_description(&self) -> &str {
38        r#"
39Not all values are representable as MessagePack.
40
41The datetime extension type is used for dates. Binaries are represented with
42the native MessagePack binary type. Most other types are represented in an
43analogous way to `to json`, and may not convert to the exact same type when
44deserialized with `from msgpack`.
45
46MessagePack: https://msgpack.org/
47"#
48        .trim()
49    }
50
51    fn examples(&self) -> Vec<Example> {
52        vec![
53            Example {
54                description: "Convert a list of values to MessagePack",
55                example: "[foo, 42, false] | to msgpack",
56                result: Some(Value::test_binary(b"\x93\xA3\x66\x6F\x6F\x2A\xC2")),
57            },
58            Example {
59                description: "Convert a range to a MessagePack array",
60                example: "1..10 | to msgpack",
61                result: Some(Value::test_binary(b"\x9A\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A"))
62            },
63            Example {
64                description: "Convert a table to MessagePack",
65                example: "[
66        [event_name time];
67        ['Apollo 11 Landing' 1969-07-24T16:50:35]
68        ['Nushell first commit' 2019-05-10T09:59:12-07:00]
69    ] | to msgpack",
70                result: Some(Value::test_binary(b"\x92\x82\xAA\x65\x76\x65\x6E\x74\x5F\x6E\x61\x6D\x65\xB1\x41\x70\x6F\x6C\x6C\x6F\x20\x31\x31\x20\x4C\x61\x6E\x64\x69\x6E\x67\xA4\x74\x69\x6D\x65\xC7\x0C\xFF\x00\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\x2C\xAB\x5B\x82\xAA\x65\x76\x65\x6E\x74\x5F\x6E\x61\x6D\x65\xB4\x4E\x75\x73\x68\x65\x6C\x6C\x20\x66\x69\x72\x73\x74\x20\x63\x6F\x6D\x6D\x69\x74\xA4\x74\x69\x6D\x65\xD6\xFF\x5C\xD5\xAD\xE0")),
71            },
72        ]
73    }
74
75    fn run(
76        &self,
77        engine_state: &EngineState,
78        stack: &mut Stack,
79        call: &Call,
80        input: PipelineData,
81    ) -> Result<PipelineData, ShellError> {
82        let metadata = input
83            .metadata()
84            .unwrap_or_default()
85            .with_content_type(Some("application/x-msgpack".into()));
86
87        let value_span = input.span().unwrap_or(call.head);
88        let value = input.into_value(value_span)?;
89        let mut out = vec![];
90
91        let serialize_types = call.has_flag(engine_state, stack, "serialize")?;
92
93        write_value(
94            &mut out,
95            &value,
96            0,
97            engine_state,
98            call.head,
99            serialize_types,
100        )?;
101
102        Ok(Value::binary(out, call.head).into_pipeline_data_with_metadata(Some(metadata)))
103    }
104}
105
106#[derive(Debug)]
107pub(crate) enum WriteError {
108    MaxDepth(Span),
109    Rmp(mp::ValueWriteError<io::Error>, Span),
110    Io(io::Error, Span),
111    Shell(Box<ShellError>),
112}
113
114impl From<Spanned<mp::ValueWriteError<io::Error>>> for WriteError {
115    fn from(v: Spanned<mp::ValueWriteError<io::Error>>) -> Self {
116        Self::Rmp(v.item, v.span)
117    }
118}
119
120impl From<Spanned<io::Error>> for WriteError {
121    fn from(v: Spanned<io::Error>) -> Self {
122        Self::Io(v.item, v.span)
123    }
124}
125
126impl From<Box<ShellError>> for WriteError {
127    fn from(v: Box<ShellError>) -> Self {
128        Self::Shell(v)
129    }
130}
131
132impl From<ShellError> for WriteError {
133    fn from(value: ShellError) -> Self {
134        Box::new(value).into()
135    }
136}
137
138impl From<WriteError> for ShellError {
139    fn from(value: WriteError) -> Self {
140        match value {
141            WriteError::MaxDepth(span) => ShellError::GenericError {
142                error: "MessagePack data is nested too deeply".into(),
143                msg: format!("exceeded depth limit ({MAX_DEPTH})"),
144                span: Some(span),
145                help: None,
146                inner: vec![],
147            },
148            WriteError::Rmp(err, span) => ShellError::GenericError {
149                error: "Failed to encode MessagePack data".into(),
150                msg: err.to_string(),
151                span: Some(span),
152                help: None,
153                inner: vec![],
154            },
155            WriteError::Io(err, span) => ShellError::Io(IoError::new(err, span, None)),
156            WriteError::Shell(err) => *err,
157        }
158    }
159}
160
161pub(crate) fn write_value(
162    out: &mut impl io::Write,
163    value: &Value,
164    depth: usize,
165    engine_state: &EngineState,
166    call_span: Span,
167    serialize_types: bool,
168) -> Result<(), WriteError> {
169    use mp::ValueWriteError::InvalidMarkerWrite;
170    let span = value.span();
171    // Prevent stack overflow
172    if depth >= MAX_DEPTH {
173        return Err(WriteError::MaxDepth(span));
174    }
175    match value {
176        Value::Bool { val, .. } => {
177            mp::write_bool(out, *val)
178                .map_err(InvalidMarkerWrite)
179                .err_span(span)?;
180        }
181        Value::Int { val, .. } => {
182            mp::write_sint(out, *val).err_span(span)?;
183        }
184        Value::Float { val, .. } => {
185            mp::write_f64(out, *val).err_span(span)?;
186        }
187        Value::Filesize { val, .. } => {
188            mp::write_sint(out, val.get()).err_span(span)?;
189        }
190        Value::Duration { val, .. } => {
191            mp::write_sint(out, *val).err_span(span)?;
192        }
193        Value::Date { val, .. } => {
194            if val.timestamp_subsec_nanos() == 0
195                && val.timestamp() >= 0
196                && val.timestamp() < u32::MAX as i64
197            {
198                // Timestamp extension type, 32-bit. u32 seconds since UNIX epoch only.
199                mp::write_ext_meta(out, 4, -1).err_span(span)?;
200                out.write_u32::<BigEndian>(val.timestamp() as u32)
201                    .err_span(span)?;
202            } else {
203                // Timestamp extension type, 96-bit. u32 nanoseconds and i64 seconds.
204                mp::write_ext_meta(out, 12, -1).err_span(span)?;
205                out.write_u32::<BigEndian>(val.timestamp_subsec_nanos())
206                    .err_span(span)?;
207                out.write_i64::<BigEndian>(val.timestamp()).err_span(span)?;
208            }
209        }
210        Value::Range { val, .. } => {
211            // Convert range to list
212            write_value(
213                out,
214                &Value::list(val.into_range_iter(span, Signals::empty()).collect(), span),
215                depth,
216                engine_state,
217                call_span,
218                serialize_types,
219            )?;
220        }
221        Value::String { val, .. } => {
222            mp::write_str(out, val).err_span(span)?;
223        }
224        Value::Glob { val, .. } => {
225            mp::write_str(out, val).err_span(span)?;
226        }
227        Value::Record { val, .. } => {
228            mp::write_map_len(out, convert(val.len(), span)?).err_span(span)?;
229            for (k, v) in val.iter() {
230                mp::write_str(out, k).err_span(span)?;
231                write_value(out, v, depth + 1, engine_state, call_span, serialize_types)?;
232            }
233        }
234        Value::List { vals, .. } => {
235            mp::write_array_len(out, convert(vals.len(), span)?).err_span(span)?;
236            for val in vals {
237                write_value(
238                    out,
239                    val,
240                    depth + 1,
241                    engine_state,
242                    call_span,
243                    serialize_types,
244                )?;
245            }
246        }
247        Value::Nothing { .. } => {
248            mp::write_nil(out)
249                .map_err(InvalidMarkerWrite)
250                .err_span(span)?;
251        }
252        Value::Closure { val, .. } => {
253            if serialize_types {
254                let closure_string = val
255                    .coerce_into_string(engine_state, span)
256                    .map_err(|err| WriteError::Shell(Box::new(err)))?;
257                mp::write_str(out, &closure_string).err_span(span)?;
258            } else {
259                return Err(WriteError::Shell(Box::new(ShellError::UnsupportedInput {
260                    msg: "closures are currently not deserializable (use --serialize to serialize as a string)".into(),
261                    input: "value originates from here".into(),
262                    msg_span: call_span,
263                    input_span: span,
264                })));
265            }
266        }
267        Value::Error { error, .. } => {
268            return Err(WriteError::Shell(error.clone()));
269        }
270        Value::CellPath { val, .. } => {
271            // Write as a list of strings/ints
272            mp::write_array_len(out, convert(val.members.len(), span)?).err_span(span)?;
273            for member in &val.members {
274                match member {
275                    PathMember::String { val, .. } => {
276                        mp::write_str(out, val).err_span(span)?;
277                    }
278                    PathMember::Int { val, .. } => {
279                        mp::write_uint(out, *val as u64).err_span(span)?;
280                    }
281                }
282            }
283        }
284        Value::Binary { val, .. } => {
285            mp::write_bin(out, val).err_span(span)?;
286        }
287        Value::Custom { val, .. } => {
288            write_value(
289                out,
290                &val.to_base_value(span)?,
291                depth,
292                engine_state,
293                call_span,
294                serialize_types,
295            )?;
296        }
297    }
298    Ok(())
299}
300
301fn convert<T, U>(value: T, span: Span) -> Result<U, ShellError>
302where
303    U: TryFrom<T>,
304    <U as TryFrom<T>>::Error: std::fmt::Display,
305{
306    value
307        .try_into()
308        .map_err(|err: <U as TryFrom<T>>::Error| ShellError::GenericError {
309            error: "Value not compatible with MessagePack".into(),
310            msg: err.to_string(),
311            span: Some(span),
312            help: None,
313            inner: vec![],
314        })
315}
316
317#[cfg(test)]
318mod test {
319    use nu_cmd_lang::eval_pipeline_without_terminal_expression;
320
321    use crate::{Get, Metadata};
322
323    use super::*;
324
325    #[test]
326    fn test_examples() {
327        use crate::test_examples;
328
329        test_examples(ToMsgpack {})
330    }
331
332    #[test]
333    fn test_content_type_metadata() {
334        let mut engine_state = Box::new(EngineState::new());
335        let delta = {
336            // Base functions that are needed for testing
337            // Try to keep this working set small to keep tests running as fast as possible
338            let mut working_set = StateWorkingSet::new(&engine_state);
339
340            working_set.add_decl(Box::new(ToMsgpack {}));
341            working_set.add_decl(Box::new(Metadata {}));
342            working_set.add_decl(Box::new(Get {}));
343
344            working_set.render()
345        };
346
347        engine_state
348            .merge_delta(delta)
349            .expect("Error merging delta");
350
351        let cmd = "{a: 1 b: 2} | to msgpack | metadata | get content_type";
352        let result = eval_pipeline_without_terminal_expression(
353            cmd,
354            std::env::temp_dir().as_ref(),
355            &mut engine_state,
356        );
357        assert_eq!(
358            Value::test_record(
359                record!("content_type" => Value::test_string("application/x-msgpack"))
360            ),
361            result.expect("There should be a result")
362        );
363    }
364}