Repository: casey/just
Pull request #2052 (2024-05-18 19:45:03)

Add percent encoding function
https://github.com/casey/just/issues/1982

This function encodes more characters than [Javascript `encodeURIComponent`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) because the Examples section of that page mentions some cases that require additional percent-encoding, and in my experience extra percent-encoding like this doesn't hurt anything.

Not happy about the `static` in light of https://github.com/casey/just/pull/2049#discussion_r1605682328 but don't know any way around it :frowning_face: 

casey: My issue with static is just that it requires navigating to the definition of the static to find out what it actually contains, not that it's static per se. I moved the static inside the function, so it's not an issue here.

Regarding which characters to actually percent encode, I think it might be useful to provide functions which exactly match `encodeURI` and `encodeURIComponent`, that way, users aren't surprised by which characters are encoded.

We could also then later add a function which percent encodes even more things, if that winds up being useful. I'd rather percent encode more things based on specific feedback and concrete use-cases.

What do you think?
casey: LGTM! I renamed it to `encode_uri_component`, so that we have the ability to add a more flexible `percent_encode` function later on, perhaps one which takes as input a string containing a custom list of characters to escape.

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 91c4be60..96b94f22 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -141,9 +141,9 @@ dependencies = [
 
 [[package]]
 name = "camino"
-version = "1.1.6"
+version = "1.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c"
+checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239"
 
 [[package]]
 name = "cc"
@@ -362,9 +362,9 @@ checksum = "bbbaaaf38131deb9ca518a274a45bfdb8771f139517b073b16c2d3d32ae5037b"
 
 [[package]]
 name = "either"
-version = "1.11.0"
+version = "1.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2"
+checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b"
 
 [[package]]
 name = "env_filter"
@@ -536,6 +536,7 @@ dependencies = [
  "libc",
  "log",
  "num_cpus",
+ "percent-encoding",
  "pretty_assertions",
  "rand",
  "regex",
@@ -569,9 +570,9 @@ checksum = "441225017b106b9f902e97947a6d31e44ebcf274b91bdbfb51e5c477fcd468e5"
 
 [[package]]
 name = "libc"
-version = "0.2.154"
+version = "0.2.155"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
+checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
 
 [[package]]
 name = "libredox"
@@ -585,9 +586,9 @@ dependencies = [
 
 [[package]]
 name = "linux-raw-sys"
-version = "0.4.13"
+version = "0.4.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
+checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
 
 [[package]]
 name = "log"
@@ -638,6 +639,12 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
 
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
 [[package]]
 name = "ppv-lite86"
 version = "0.2.17"
@@ -843,9 +850,9 @@ dependencies = [
 
 [[package]]
 name = "rustversion"
-version = "1.0.16"
+version = "1.0.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "092474d1a01ea8278f69e6a358998405fae5b8b963ddaeb2b0b04a128bf1dfb0"
+checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
 
 [[package]]
 name = "ryu"
@@ -861,22 +868,22 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
 
 [[package]]
 name = "serde"
-version = "1.0.201"
+version = "1.0.202"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c"
+checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.201"
+version = "1.0.202"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865"
+checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.63",
+ "syn 2.0.64",
 ]
 
 [[package]]
@@ -929,7 +936,7 @@ dependencies = [
  "heck 0.4.1",
  "proc-macro2",
  "quote",
- "syn 2.0.63",
+ "syn 2.0.64",
 ]
 
 [[package]]
@@ -987,7 +994,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "rustversion",
- "syn 2.0.63",
+ "syn 2.0.64",
 ]
 
 [[package]]
@@ -1003,9 +1010,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "2.0.63"
+version = "2.0.64"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704"
+checksum = "7ad3dee41f36859875573074334c200d1add8e4a87bb37113ebd31d926b7b11f"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1060,22 +1067,22 @@ dependencies = [
 
 [[package]]
 name = "thiserror"
-version = "1.0.60"
+version = "1.0.61"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18"
+checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.60"
+version = "1.0.61"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524"
+checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.63",
+ "syn 2.0.64",
 ]
 
 [[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 9c200b18..6e3defa7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -36,6 +36,7 @@ lexiclean = "0.0.1"
 libc = "0.2.0"
 log = "0.4.4"
 num_cpus = "1.15.0"
+percent-encoding = "2.3.1"
 rand = "0.8.5"
 regex = "1.10.4"
 semver = "1.0.20"
diff --git a/README.md b/README.md
index 371ab198..8eb34525 100644
--- a/README.md
+++ b/README.md
@@ -1427,6 +1427,9 @@ The process ID is: 420
 - `prepend(prefix, s)`<sup>master</sup> Prepend `prefix` to
   whitespace-separated strings in `s`. `prepend('src/', 'foo bar baz')` →
   `'src/foo src/bar src/baz'`
+- `encode_uri_component(s)`<sup>master</sup> - Percent-encode characters in `s`
+  except `[A-Za-z0-9_.!~*'()-]`, matching the behavior of the
+  [JavaScript `encodeURIComponent` function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent).
 - `quote(s)` - Replace all single quotes with `'\''` and prepend and append
   single quotes to `s`. This is sufficient to escape special characters for
   many shells, including most Bourne shell descendants.
diff --git a/src/function.rs b/src/function.rs
index f8fc9963..1b657d1c 100644
--- a/src/function.rs
+++ b/src/function.rs
@@ -35,6 +35,7 @@ pub(crate) fn get(name: &str) -> Option<Function> {
     "config_local_directory" => Nullary(|_| dir("local config", dirs::config_local_dir)),
     "data_directory" => Nullary(|_| dir("data", dirs::data_dir)),
     "data_local_directory" => Nullary(|_| dir("local data", dirs::data_local_dir)),
+    "encode_uri_component" => Unary(encode_uri_component),
     "env" => UnaryOpt(env),
     "env_var" => Unary(env_var),
     "env_var_or_default" => Binary(env_var_or_default),
@@ -204,6 +205,20 @@ fn dir(name: &'static str, f: fn() -> Option<PathBuf>) -> Result<String, String>
   }
 }
 
+fn encode_uri_component(_evaluator: &Evaluator, s: &str) -> Result<String, String> {
+  static PERCENT_ENCODE: percent_encoding::AsciiSet = percent_encoding::NON_ALPHANUMERIC
+    .remove(b'-')
+    .remove(b'_')
+    .remove(b'.')
+    .remove(b'!')
+    .remove(b'~')
+    .remove(b'*')
+    .remove(b'\'')
+    .remove(b'(')
+    .remove(b')');
+  Ok(percent_encoding::utf8_percent_encode(s, &PERCENT_ENCODE).to_string())
+}
+
 fn env_var(evaluator: &Evaluator, key: &str) -> Result<String, String> {
   use std::env::VarError::*;
 


Test patch:
diff --git a/tests/functions.rs b/tests/functions.rs
index e1341101..614761a0 100644
--- a/tests/functions.rs
+++ b/tests/functions.rs
@@ -793,3 +793,12 @@ fn canonicalize() {
     .stdout_regex(".*/justfile")
     .run();
 }
+
+#[test]
+fn encode_uri_component() {
+  Test::new()
+    .justfile("x := encode_uri_component(\"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\\\"#$%&'()*+,-./:;<=>?@[\\\\]^_`{|}~ \\t\\r\\n🌐\")")
+    .args(["--evaluate", "x"])
+    .stdout("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!%22%23%24%25%26'()*%2B%2C-.%2F%3A%3B%3C%3D%3E%3F%40%5B%5C%5D%5E_%60%7B%7C%7D~%20%09%0D%0A%F0%9F%8C%90")
+    .run();
+}