nu_command/conversions/into/
binary.rs

1use nu_cmd_base::input_handler::{CmdArgument, operate};
2use nu_engine::command_prelude::*;
3
4struct Arguments {
5    cell_paths: Option<Vec<CellPath>>,
6    compact: bool,
7    little_endian: bool,
8}
9
10impl CmdArgument for Arguments {
11    fn take_cell_paths(&mut self) -> Option<Vec<CellPath>> {
12        self.cell_paths.take()
13    }
14}
15
16#[derive(Clone)]
17pub struct IntoBinary;
18
19impl Command for IntoBinary {
20    fn name(&self) -> &str {
21        "into binary"
22    }
23
24    fn signature(&self) -> Signature {
25        Signature::build("into binary")
26            .input_output_types(vec![
27                (Type::Binary, Type::Binary),
28                (Type::Int, Type::Binary),
29                (Type::Number, Type::Binary),
30                (Type::String, Type::Binary),
31                (Type::Bool, Type::Binary),
32                (Type::Filesize, Type::Binary),
33                (Type::Date, Type::Binary),
34                (Type::table(), Type::table()),
35                (Type::record(), Type::record()),
36            ])
37            .allow_variants_without_examples(true) // TODO: supply exhaustive examples
38            .switch("compact", "output without padding zeros", Some('c'))
39            .named(
40                "endian",
41                SyntaxShape::String,
42                "byte encode endian. Does not affect string, date or binary. In containers, only individual elements are affected. Available options: native(default), little, big",
43                Some('e'),
44            )
45            .rest(
46                "rest",
47                SyntaxShape::CellPath,
48                "For a data structure input, convert data at the given cell paths.",
49            )
50            .category(Category::Conversions)
51    }
52
53    fn description(&self) -> &str {
54        "Convert value to a binary primitive."
55    }
56
57    fn search_terms(&self) -> Vec<&str> {
58        vec!["convert", "bytes"]
59    }
60
61    fn run(
62        &self,
63        engine_state: &EngineState,
64        stack: &mut Stack,
65        call: &Call,
66        input: PipelineData,
67    ) -> Result<PipelineData, ShellError> {
68        into_binary(engine_state, stack, call, input)
69    }
70
71    fn examples(&self) -> Vec<Example> {
72        vec![
73            Example {
74                description: "convert string to a nushell binary primitive",
75                example: "'This is a string that is exactly 52 characters long.' | into binary",
76                result: Some(Value::binary(
77                    "This is a string that is exactly 52 characters long."
78                        .to_string()
79                        .as_bytes()
80                        .to_vec(),
81                    Span::test_data(),
82                )),
83            },
84            Example {
85                description: "convert a number to a nushell binary primitive",
86                example: "1 | into binary",
87                result: Some(Value::binary(
88                    i64::from(1).to_ne_bytes().to_vec(),
89                    Span::test_data(),
90                )),
91            },
92            Example {
93                description: "convert a number to a nushell binary primitive (big endian)",
94                example: "258 | into binary --endian big",
95                result: Some(Value::binary(
96                    i64::from(258).to_be_bytes().to_vec(),
97                    Span::test_data(),
98                )),
99            },
100            Example {
101                description: "convert a number to a nushell binary primitive (little endian)",
102                example: "258 | into binary --endian little",
103                result: Some(Value::binary(
104                    i64::from(258).to_le_bytes().to_vec(),
105                    Span::test_data(),
106                )),
107            },
108            Example {
109                description: "convert a boolean to a nushell binary primitive",
110                example: "true | into binary",
111                result: Some(Value::binary(
112                    i64::from(1).to_ne_bytes().to_vec(),
113                    Span::test_data(),
114                )),
115            },
116            Example {
117                description: "convert a filesize to a nushell binary primitive",
118                example: "ls | where name == LICENSE | get size | into binary",
119                result: None,
120            },
121            Example {
122                description: "convert a filepath to a nushell binary primitive",
123                example: "ls | where name == LICENSE | get name | path expand | into binary",
124                result: None,
125            },
126            Example {
127                description: "convert a float to a nushell binary primitive",
128                example: "1.234 | into binary",
129                result: Some(Value::binary(
130                    1.234f64.to_ne_bytes().to_vec(),
131                    Span::test_data(),
132                )),
133            },
134            Example {
135                description: "convert an int to a nushell binary primitive with compact enabled",
136                example: "10 | into binary --compact",
137                result: Some(Value::binary(vec![10], Span::test_data())),
138            },
139        ]
140    }
141}
142
143fn into_binary(
144    engine_state: &EngineState,
145    stack: &mut Stack,
146    call: &Call,
147    input: PipelineData,
148) -> Result<PipelineData, ShellError> {
149    let head = call.head;
150    let cell_paths = call.rest(engine_state, stack, 0)?;
151    let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
152
153    if let PipelineData::ByteStream(stream, metadata) = input {
154        // Just set the type - that should be good enough
155        Ok(PipelineData::byte_stream(
156            stream.with_type(ByteStreamType::Binary),
157            metadata,
158        ))
159    } else {
160        let endian = call.get_flag::<Spanned<String>>(engine_state, stack, "endian")?;
161
162        let little_endian = if let Some(endian) = endian {
163            match endian.item.as_str() {
164                "native" => cfg!(target_endian = "little"),
165                "little" => true,
166                "big" => false,
167                _ => {
168                    return Err(ShellError::TypeMismatch {
169                        err_message: "Endian must be one of native, little, big".to_string(),
170                        span: endian.span,
171                    });
172                }
173            }
174        } else {
175            cfg!(target_endian = "little")
176        };
177
178        let args = Arguments {
179            cell_paths,
180            compact: call.has_flag(engine_state, stack, "compact")?,
181            little_endian,
182        };
183        operate(action, args, input, head, engine_state.signals())
184    }
185}
186
187fn action(input: &Value, args: &Arguments, span: Span) -> Value {
188    let value = match input {
189        Value::Binary { .. } => input.clone(),
190        Value::Int { val, .. } => Value::binary(
191            if args.little_endian {
192                val.to_le_bytes()
193            } else {
194                val.to_be_bytes()
195            }
196            .to_vec(),
197            span,
198        ),
199        Value::Float { val, .. } => Value::binary(
200            if args.little_endian {
201                val.to_le_bytes()
202            } else {
203                val.to_be_bytes()
204            }
205            .to_vec(),
206            span,
207        ),
208        Value::Filesize { val, .. } => Value::binary(
209            if args.little_endian {
210                val.get().to_le_bytes()
211            } else {
212                val.get().to_be_bytes()
213            }
214            .to_vec(),
215            span,
216        ),
217        Value::String { val, .. } => Value::binary(val.as_bytes().to_vec(), span),
218        Value::Bool { val, .. } => Value::binary(
219            {
220                let as_int = i64::from(*val);
221                if args.little_endian {
222                    as_int.to_le_bytes()
223                } else {
224                    as_int.to_be_bytes()
225                }
226                .to_vec()
227            },
228            span,
229        ),
230        Value::Duration { val, .. } => Value::binary(
231            if args.little_endian {
232                val.to_le_bytes()
233            } else {
234                val.to_be_bytes()
235            }
236            .to_vec(),
237            span,
238        ),
239        Value::Date { val, .. } => {
240            Value::binary(val.format("%c").to_string().as_bytes().to_vec(), span)
241        }
242        // Propagate errors by explicitly matching them before the final case.
243        Value::Error { .. } => input.clone(),
244        other => Value::error(
245            ShellError::OnlySupportsThisInputType {
246                exp_input_type: "int, float, filesize, string, date, duration, binary, or bool"
247                    .into(),
248                wrong_type: other.get_type().to_string(),
249                dst_span: span,
250                src_span: other.span(),
251            },
252            span,
253        ),
254    };
255
256    if args.compact {
257        let val_span = value.span();
258        if let Value::Binary { val, .. } = value {
259            let val = if args.little_endian {
260                match val.iter().rposition(|&x| x != 0) {
261                    Some(idx) => &val[..idx + 1],
262
263                    // all 0s should just return a single 0 byte
264                    None => &[0],
265                }
266            } else {
267                match val.iter().position(|&x| x != 0) {
268                    Some(idx) => &val[idx..],
269                    None => &[0],
270                }
271            };
272
273            Value::binary(val.to_vec(), val_span)
274        } else {
275            value
276        }
277    } else {
278        value
279    }
280}
281
282#[cfg(test)]
283mod test {
284    use rstest::rstest;
285
286    use super::*;
287
288    #[test]
289    fn test_examples() {
290        use crate::test_examples;
291
292        test_examples(IntoBinary {})
293    }
294
295    #[rstest]
296    #[case(vec![10], vec![10], vec![10])]
297    #[case(vec![10, 0, 0], vec![10], vec![10, 0, 0])]
298    #[case(vec![0, 0, 10], vec![0, 0, 10], vec![10])]
299    #[case(vec![0, 10, 0, 0], vec![0, 10], vec![10, 0, 0])]
300    fn test_compact(#[case] input: Vec<u8>, #[case] little: Vec<u8>, #[case] big: Vec<u8>) {
301        let s = Value::test_binary(input);
302        let actual = action(
303            &s,
304            &Arguments {
305                cell_paths: None,
306                compact: true,
307                little_endian: cfg!(target_endian = "little"),
308            },
309            Span::test_data(),
310        );
311        if cfg!(target_endian = "little") {
312            assert_eq!(actual, Value::test_binary(little));
313        } else {
314            assert_eq!(actual, Value::test_binary(big));
315        }
316    }
317}