From a03c1c266c12e6e67b92c42f091042d2f6b9d9f0 Mon Sep 17 00:00:00 2001 From: Lucas Chaim Date: Thu, 5 Oct 2023 13:43:58 -0300 Subject: [PATCH] Add url decode command (#10611) Implemented URL decoding as a url subcommand, created corresponding unit tests. The logic, examples and descriptions were based on the existing `url encode` command. Resolves #10563 # Description Added a new `url decode` command to compliment the existing `url encode`, as proposed by myself in #10563. It takes a string, list of strings or cell path and produces the corresponding decoded strings. ![image](https://github.com/nushell/nushell/assets/4030336/815a34e9-7ceb-4d09-9d74-e700ba513b17) # User-Facing Changes New url subcommand `url decode`, as described above. # Tests + Formatting I've added unit tests for the new subcommand and ensured all actions outlined below showed no issues. - [x] `cargo fmt --all -- --check` - [x] `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` - [x] `cargo test --workspace` - [x] `cargo run -- -c "use std testing; testing run-tests --path crates/nu-std"` --- crates/nu-command/src/default_context.rs | 1 + crates/nu-command/src/network/url/decode.rs | 122 ++++++++++++++++++ crates/nu-command/src/network/url/mod.rs | 2 + .../nu-command/tests/commands/url/decode.rs | 19 +++ crates/nu-command/tests/commands/url/mod.rs | 1 + 5 files changed, 145 insertions(+) create mode 100644 crates/nu-command/src/network/url/decode.rs create mode 100644 crates/nu-command/tests/commands/url/decode.rs diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 4d89d28ff..af9927087 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -357,6 +357,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { HttpOptions, Url, UrlBuildQuery, + UrlDecode, UrlEncode, UrlJoin, UrlParse, diff --git a/crates/nu-command/src/network/url/decode.rs b/crates/nu-command/src/network/url/decode.rs new file mode 100644 index 000000000..468447262 --- /dev/null +++ b/crates/nu-command/src/network/url/decode.rs @@ -0,0 +1,122 @@ +use nu_cmd_base::input_handler::{operate, CellPathOnlyArgs}; +use nu_engine::CallExt; +use nu_protocol::ast::{Call, CellPath}; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::Category; +use nu_protocol::{Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value}; +use percent_encoding::percent_decode_str; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "url decode" + } + + fn signature(&self) -> Signature { + Signature::build("url decode") + .input_output_types(vec![ + (Type::String, Type::String), + ( + Type::List(Box::new(Type::String)), + Type::List(Box::new(Type::String)), + ), + (Type::Table(vec![]), Type::Table(vec![])), + (Type::Record(vec![]), Type::Record(vec![])), + ]) + .allow_variants_without_examples(true) + .rest( + "rest", + SyntaxShape::CellPath, + "For a data structure input, url decode strings at the given cell paths", + ) + .category(Category::Strings) + } + + fn usage(&self) -> &str { + "Converts a percent-encoded web safe string to a string." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["string", "text", "convert"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let cell_paths: Vec = call.rest(engine_state, stack, 0)?; + let args = CellPathOnlyArgs::from(cell_paths); + operate(action, args, input, call.head, engine_state.ctrlc.clone()) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Decode a url with escape characters", + example: "'https://example.com/foo%20bar' | url decode", + result: Some(Value::test_string("https://example.com/foo bar")), + }, + Example { + description: "Decode multiple urls with escape characters in list", + example: "['https://example.com/foo%20bar' 'https://example.com/a%3Eb' '%E4%B8%AD%E6%96%87%E5%AD%97/eng/12%2034'] | url decode", + result: Some(Value::list( + vec![ + Value::test_string("https://example.com/foo bar"), + Value::test_string("https://example.com/a>b"), + Value::test_string("中文字/eng/12 34"), + ], + Span::test_data(), + )), + }, + ] + } +} + +fn action(input: &Value, _arg: &CellPathOnlyArgs, head: Span) -> Value { + let input_span = input.span(); + match input { + Value::String { val, .. } => { + let val = percent_decode_str(val).decode_utf8(); + match val { + Ok(val) => Value::string(val, head), + Err(e) => Value::error( + ShellError::GenericError( + "Failed to decode string".into(), + e.to_string(), + Some(input_span), + None, + Vec::new(), + ), + head, + ), + } + } + Value::Error { .. } => input.clone(), + _ => Value::error( + ShellError::OnlySupportsThisInputType { + exp_input_type: "string".into(), + wrong_type: input.get_type().to_string(), + dst_span: head, + src_span: input.span(), + }, + head, + ), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/network/url/mod.rs b/crates/nu-command/src/network/url/mod.rs index 1b6cc7fd0..7cdb5ad4e 100644 --- a/crates/nu-command/src/network/url/mod.rs +++ b/crates/nu-command/src/network/url/mod.rs @@ -1,4 +1,5 @@ mod build_query; +mod decode; mod encode; mod join; mod parse; @@ -8,6 +9,7 @@ use url::{self}; pub use self::parse::SubCommand as UrlParse; pub use build_query::SubCommand as UrlBuildQuery; +pub use decode::SubCommand as UrlDecode; pub use encode::SubCommand as UrlEncode; pub use join::SubCommand as UrlJoin; pub use url_::Url; diff --git a/crates/nu-command/tests/commands/url/decode.rs b/crates/nu-command/tests/commands/url/decode.rs new file mode 100644 index 000000000..34b826a1d --- /dev/null +++ b/crates/nu-command/tests/commands/url/decode.rs @@ -0,0 +1,19 @@ +use nu_test_support::nu; + +#[test] +fn url_decode_simple() { + let actual = nu!(r#"'a%20b' | url decode"#); + assert_eq!(actual.out, "a b"); +} + +#[test] +fn url_decode_special_characters() { + let actual = nu!(r#"'%21%40%23%24%25%C2%A8%26%2A%2D%2B%3B%2C%7B%7D%5B%5D%28%29' | url decode"#); + assert_eq!(actual.out, r#"!@#$%¨&*-+;,{}[]()"#); +} + +#[test] +fn url_decode_error_invalid_utf8() { + let actual = nu!(r#"'%99' | url decode"#); + assert!(actual.err.contains("invalid utf-8 sequence")); +} diff --git a/crates/nu-command/tests/commands/url/mod.rs b/crates/nu-command/tests/commands/url/mod.rs index 6c825d470..10c5b3ad3 100644 --- a/crates/nu-command/tests/commands/url/mod.rs +++ b/crates/nu-command/tests/commands/url/mod.rs @@ -1,2 +1,3 @@ +mod decode; mod join; mod parse;