<!-- if this PR closes one or more issues, you can automatically link the PR with them by using one of the [*linking keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword), e.g. - this PR should close #xxxx - fixes #xxxx you can also mention related issues, PRs or discussions! --> Related meta-issue: #10239. # Description <!-- Thank you for improving Nushell. Please, check our [contributing guide](../CONTRIBUTING.md) and talk to the core team before making major changes. Description of your pull request goes here. **Provide examples and/or screenshots** if your changes affect the user experience. --> This PR will modify some `str`-related commands so that they can be evaluated at parse time. See the following list for those implemented by this pr. # User-Facing Changes <!-- List of all changes that impact the user experience here. This helps us keep track of breaking changes. --> Available now: - `str` subcommands - `trim` - `contains` - `distance` - `ends-with` - `expand` - `index-of` - `join` - `replace` - `reverse` - `starts-with` - `stats` - `substring` - `capitalize` - `downcase` - `upcase` - `split` subcommands - `chars` - `column` - `list` - `row` - `words` - `format` subcommands - `date` - `duration` - `filesize` - string related commands - `parse` - `detect columns` - `encode` & `decode` # Tests + Formatting <!-- Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass (on Windows make sure to [enable developer mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging)) - `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the tests for the standard library > **Note** > from `nushell` you can also use the `toolkit` as follows > ```bash > use toolkit.nu # or use an `env_change` hook to activate it automatically > toolkit check pr > ``` --> Unresolved questions: - [ ] Is there any routine of testing const expressions? I haven't found any yet. - [ ] Is const expressions required to behave just like there non-const version, like what rust promises? # After Submitting <!-- If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date. --> Unresolved questions: - [ ] Do const commands need special marks in the docs?
378 lines
13 KiB
Rust
378 lines
13 KiB
Rust
use base64::{
|
|
alphabet,
|
|
engine::{
|
|
general_purpose::{NO_PAD, PAD},
|
|
GeneralPurpose,
|
|
},
|
|
Engine,
|
|
};
|
|
use nu_cmd_base::input_handler::{operate as general_operate, CmdArgument};
|
|
use nu_protocol::{
|
|
ast::{Call, CellPath},
|
|
engine::EngineState,
|
|
PipelineData, ShellError, Span, Spanned, Value,
|
|
};
|
|
|
|
pub const CHARACTER_SET_DESC: &str = "specify the character rules for encoding the input.\n\
|
|
\tValid values are 'standard', 'standard-no-padding', 'url-safe', 'url-safe-no-padding',\
|
|
'binhex', 'bcrypt', 'crypt', 'mutf7'";
|
|
|
|
#[derive(Clone)]
|
|
pub struct Base64Config {
|
|
pub character_set: Spanned<String>,
|
|
pub action_type: ActionType,
|
|
}
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
pub enum ActionType {
|
|
Encode,
|
|
Decode,
|
|
}
|
|
|
|
struct Arguments {
|
|
cell_paths: Option<Vec<CellPath>>,
|
|
binary: bool,
|
|
encoding_config: Base64Config,
|
|
}
|
|
|
|
impl CmdArgument for Arguments {
|
|
fn take_cell_paths(&mut self) -> Option<Vec<CellPath>> {
|
|
self.cell_paths.take()
|
|
}
|
|
}
|
|
|
|
pub(super) struct Base64CommandArguments {
|
|
pub(super) character_set: Option<Spanned<String>>,
|
|
pub(super) action_type: ActionType,
|
|
pub(super) binary: bool,
|
|
}
|
|
|
|
pub fn operate(
|
|
engine_state: &EngineState,
|
|
call: &Call,
|
|
input: PipelineData,
|
|
cell_paths: Vec<CellPath>,
|
|
args: Base64CommandArguments,
|
|
) -> Result<PipelineData, ShellError> {
|
|
let head = call.head;
|
|
let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
|
|
|
|
// Default the character set to standard if the argument is not specified.
|
|
let character_set = match args.character_set {
|
|
Some(inner_tag) => inner_tag,
|
|
None => Spanned {
|
|
item: "standard".to_string(),
|
|
span: head, // actually this span is always useless, because default character_set is always valid.
|
|
},
|
|
};
|
|
|
|
let args = Arguments {
|
|
encoding_config: Base64Config {
|
|
character_set,
|
|
action_type: args.action_type,
|
|
},
|
|
binary: args.binary,
|
|
cell_paths,
|
|
};
|
|
|
|
general_operate(action, args, input, call.head, engine_state.ctrlc.clone())
|
|
}
|
|
|
|
fn action(
|
|
input: &Value,
|
|
// only used for `decode` action
|
|
args: &Arguments,
|
|
command_span: Span,
|
|
) -> Value {
|
|
let base64_config = &args.encoding_config;
|
|
let output_binary = args.binary;
|
|
|
|
let config_character_set = &base64_config.character_set;
|
|
let base64_engine: GeneralPurpose = match config_character_set.item.as_str() {
|
|
"standard" => GeneralPurpose::new(&alphabet::STANDARD, PAD),
|
|
"standard-no-padding" => GeneralPurpose::new(&alphabet::STANDARD, NO_PAD),
|
|
"url-safe" => GeneralPurpose::new(&alphabet::URL_SAFE, PAD),
|
|
"url-safe-no-padding" => GeneralPurpose::new(&alphabet::URL_SAFE, NO_PAD),
|
|
"bcrypt" => GeneralPurpose::new(&alphabet::BCRYPT, NO_PAD),
|
|
"binhex" => GeneralPurpose::new(&alphabet::BIN_HEX, NO_PAD),
|
|
"crypt" => GeneralPurpose::new(&alphabet::CRYPT, NO_PAD),
|
|
"mutf7" => GeneralPurpose::new(&alphabet::IMAP_MUTF7, NO_PAD),
|
|
not_valid => return Value::error (
|
|
ShellError::GenericError {
|
|
error: "value is not an accepted character set".into(),
|
|
msg: format!(
|
|
"{not_valid} is not a valid character-set.\nPlease use `help encode base64` to see a list of valid character sets."
|
|
),
|
|
span: Some(config_character_set.span),
|
|
help: None,
|
|
inner: vec![],
|
|
}, config_character_set.span)
|
|
};
|
|
let value_span = input.span();
|
|
match input {
|
|
// Propagate existing errors.
|
|
Value::Error { .. } => input.clone(),
|
|
Value::Binary { val, .. } => match base64_config.action_type {
|
|
ActionType::Encode => {
|
|
let mut enc_vec = vec![0; val.len() * 4 / 3 + 4];
|
|
let bytes_written = match base64_engine.encode_slice(val, &mut enc_vec) {
|
|
Ok(bytes_written) => bytes_written,
|
|
Err(e) => {
|
|
return Value::error(
|
|
ShellError::GenericError {
|
|
error: "Error encoding data".into(),
|
|
msg: e.to_string(),
|
|
span: Some(value_span),
|
|
help: None,
|
|
inner: vec![],
|
|
},
|
|
value_span,
|
|
)
|
|
}
|
|
};
|
|
enc_vec.truncate(bytes_written);
|
|
Value::string(std::str::from_utf8(&enc_vec).unwrap_or(""), command_span)
|
|
}
|
|
ActionType::Decode => Value::error(
|
|
ShellError::UnsupportedInput {
|
|
msg: "Binary data can only be encoded".to_string(),
|
|
input: "value originates from here".into(),
|
|
msg_span: command_span,
|
|
input_span: input.span(),
|
|
},
|
|
command_span,
|
|
),
|
|
},
|
|
Value::String { val, .. } => {
|
|
match base64_config.action_type {
|
|
ActionType::Encode => {
|
|
let mut enc_str = String::new();
|
|
base64_engine.encode_string(val, &mut enc_str);
|
|
Value::string(enc_str, command_span)
|
|
}
|
|
|
|
ActionType::Decode => {
|
|
// for decode, input val may contains invalid new line character, which is ok to omitted them by default.
|
|
let val = val.clone();
|
|
let val = val.replace("\r\n", "").replace('\n', "");
|
|
|
|
match base64_engine.decode(val) {
|
|
Ok(decoded_value) => {
|
|
if output_binary {
|
|
Value::binary(decoded_value, command_span)
|
|
} else {
|
|
match String::from_utf8(decoded_value) {
|
|
Ok(string_value) => Value::string(string_value, command_span),
|
|
Err(e) => Value::error(
|
|
ShellError::GenericError {
|
|
error: "base64 payload isn't a valid utf-8 sequence"
|
|
.into(),
|
|
msg: e.to_string(),
|
|
span: Some(value_span),
|
|
help: Some(
|
|
"consider using the `--binary` flag".to_owned(),
|
|
),
|
|
inner: vec![],
|
|
},
|
|
value_span,
|
|
),
|
|
}
|
|
}
|
|
}
|
|
Err(_) => Value::error(
|
|
ShellError::GenericError {
|
|
error: "value could not be base64 decoded".into(),
|
|
msg: format!(
|
|
"invalid base64 input for character set {}",
|
|
&config_character_set.item
|
|
),
|
|
span: Some(command_span),
|
|
help: None,
|
|
inner: vec![],
|
|
},
|
|
command_span,
|
|
),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
other => Value::error(
|
|
ShellError::TypeMismatch {
|
|
err_message: format!("string or binary, not {}", other.get_type()),
|
|
span: other.span(),
|
|
},
|
|
other.span(),
|
|
),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{action, ActionType, Arguments, Base64Config};
|
|
use nu_protocol::{Span, Spanned, Value};
|
|
|
|
#[test]
|
|
fn base64_encode_standard() {
|
|
let word = Value::test_string("Some Data Padding");
|
|
let expected = Value::test_string("U29tZSBEYXRhIFBhZGRpbmc=");
|
|
|
|
let actual = action(
|
|
&word,
|
|
&Arguments {
|
|
encoding_config: Base64Config {
|
|
character_set: Spanned {
|
|
item: "standard".to_string(),
|
|
span: Span::test_data(),
|
|
},
|
|
action_type: ActionType::Encode,
|
|
},
|
|
binary: true,
|
|
cell_paths: None,
|
|
},
|
|
Span::test_data(),
|
|
);
|
|
assert_eq!(actual, expected);
|
|
}
|
|
|
|
#[test]
|
|
fn base64_encode_standard_no_padding() {
|
|
let word = Value::test_string("Some Data Padding");
|
|
let expected = Value::test_string("U29tZSBEYXRhIFBhZGRpbmc");
|
|
|
|
let actual = action(
|
|
&word,
|
|
&Arguments {
|
|
encoding_config: Base64Config {
|
|
character_set: Spanned {
|
|
item: "standard-no-padding".to_string(),
|
|
span: Span::test_data(),
|
|
},
|
|
action_type: ActionType::Encode,
|
|
},
|
|
binary: true,
|
|
cell_paths: None,
|
|
},
|
|
Span::test_data(),
|
|
);
|
|
assert_eq!(actual, expected);
|
|
}
|
|
|
|
#[test]
|
|
fn base64_encode_url_safe() {
|
|
let word = Value::test_string("this is for url");
|
|
let expected = Value::test_string("dGhpcyBpcyBmb3IgdXJs");
|
|
|
|
let actual = action(
|
|
&word,
|
|
&Arguments {
|
|
encoding_config: Base64Config {
|
|
character_set: Spanned {
|
|
item: "url-safe".to_string(),
|
|
span: Span::test_data(),
|
|
},
|
|
action_type: ActionType::Encode,
|
|
},
|
|
binary: true,
|
|
cell_paths: None,
|
|
},
|
|
Span::test_data(),
|
|
);
|
|
assert_eq!(actual, expected);
|
|
}
|
|
|
|
#[test]
|
|
fn base64_decode_binhex() {
|
|
let word = Value::test_string(r#"B5"LD@jSCAJJG'9cG!"#);
|
|
let expected = Value::binary(b"a binhex test".as_slice(), Span::test_data());
|
|
|
|
let actual = action(
|
|
&word,
|
|
&Arguments {
|
|
encoding_config: Base64Config {
|
|
character_set: Spanned {
|
|
item: "binhex".to_string(),
|
|
span: Span::test_data(),
|
|
},
|
|
action_type: ActionType::Decode,
|
|
},
|
|
binary: true,
|
|
cell_paths: None,
|
|
},
|
|
Span::test_data(),
|
|
);
|
|
assert_eq!(actual, expected);
|
|
}
|
|
|
|
#[test]
|
|
fn base64_decode_binhex_with_new_line_input() {
|
|
let word = Value::test_string("B5\"LD@jSC\nAJJG'9cG!");
|
|
let expected = Value::binary(b"a binhex test".as_slice(), Span::test_data());
|
|
|
|
let actual = action(
|
|
&word,
|
|
&Arguments {
|
|
encoding_config: Base64Config {
|
|
character_set: Spanned {
|
|
item: "binhex".to_string(),
|
|
span: Span::test_data(),
|
|
},
|
|
action_type: ActionType::Decode,
|
|
},
|
|
binary: true,
|
|
cell_paths: None,
|
|
},
|
|
Span::test_data(),
|
|
);
|
|
assert_eq!(actual, expected);
|
|
}
|
|
|
|
#[test]
|
|
fn base64_encode_binary() {
|
|
let word = Value::binary(vec![77, 97, 110], Span::test_data());
|
|
let expected = Value::test_string("TWFu");
|
|
|
|
let actual = action(
|
|
&word,
|
|
&Arguments {
|
|
encoding_config: Base64Config {
|
|
character_set: Spanned {
|
|
item: "standard".to_string(),
|
|
span: Span::test_data(),
|
|
},
|
|
action_type: ActionType::Encode,
|
|
},
|
|
binary: true,
|
|
cell_paths: None,
|
|
},
|
|
Span::test_data(),
|
|
);
|
|
assert_eq!(actual, expected);
|
|
}
|
|
|
|
#[test]
|
|
fn base64_decode_binary_expect_error() {
|
|
let word = Value::binary(vec![77, 97, 110], Span::test_data());
|
|
|
|
let actual = action(
|
|
&word,
|
|
&Arguments {
|
|
encoding_config: Base64Config {
|
|
character_set: Spanned {
|
|
item: "standard".to_string(),
|
|
span: Span::test_data(),
|
|
},
|
|
action_type: ActionType::Decode,
|
|
},
|
|
binary: true,
|
|
cell_paths: None,
|
|
},
|
|
Span::test_data(),
|
|
);
|
|
|
|
match actual {
|
|
Value::Error { .. } => {}
|
|
_ => panic!("the result should be Value::Error"),
|
|
}
|
|
}
|
|
}
|