Skip to main content

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