├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── release.toml └── src ├── encode_sets.rs └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | == [1.0.1] - 2022-05-21 8 | === Changed 9 | 10 | * Minor optimization with encode set selection 11 | 12 | == [1.0.0] - 2022-05-21 13 | === Changed 14 | 15 | * Upgrade to Rust edition 2021 16 | * Updated dependencies 17 | * This included a *breaking change* in `percent-encoding`, which removed the 18 | canned encode sets. Therefore, the encode sets have been replaced with ones 19 | defined in the WHATWG URL spec. See https://url.spec.whatwg.org/ 20 | 21 | == [0.1.2] - 2019-04-25 22 | === Changed 23 | 24 | * Upgrade to Rust edition 2018 25 | * Updated dependencies 26 | 27 | == [0.1.1] - 2018-02-12 28 | === Changed 29 | 30 | * Updated dependencies 31 | 32 | == [0.1.0] - 2017-08-11 33 | 34 | * Initial release 35 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "atty" 7 | version = "0.2.11" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652" 10 | dependencies = [ 11 | "libc", 12 | "termion", 13 | "winapi", 14 | ] 15 | 16 | [[package]] 17 | name = "autocfg" 18 | version = "1.1.0" 19 | source = "registry+https://github.com/rust-lang/crates.io-index" 20 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 21 | 22 | [[package]] 23 | name = "bitflags" 24 | version = "1.3.2" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 27 | 28 | [[package]] 29 | name = "clap" 30 | version = "3.1.18" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" 33 | dependencies = [ 34 | "atty", 35 | "bitflags", 36 | "clap_lex", 37 | "indexmap", 38 | "strsim", 39 | "termcolor", 40 | "textwrap", 41 | ] 42 | 43 | [[package]] 44 | name = "clap_lex" 45 | version = "0.2.0" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" 48 | dependencies = [ 49 | "os_str_bytes", 50 | ] 51 | 52 | [[package]] 53 | name = "hashbrown" 54 | version = "0.11.2" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" 57 | 58 | [[package]] 59 | name = "indexmap" 60 | version = "1.8.1" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" 63 | dependencies = [ 64 | "autocfg", 65 | "hashbrown", 66 | ] 67 | 68 | [[package]] 69 | name = "libc" 70 | version = "0.2.54" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "c6785aa7dd976f5fbf3b71cfd9cd49d7f783c1ff565a858d71031c6c313aa5c6" 73 | 74 | [[package]] 75 | name = "numtoa" 76 | version = "0.1.0" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" 79 | 80 | [[package]] 81 | name = "os_str_bytes" 82 | version = "6.0.1" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "029d8d0b2f198229de29dca79676f2738ff952edf3fde542eb8bf94d8c21b435" 85 | 86 | [[package]] 87 | name = "percent-encoding" 88 | version = "2.1.0" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 91 | 92 | [[package]] 93 | name = "redox_syscall" 94 | version = "0.1.54" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "12229c14a0f65c4f1cb046a3b52047cdd9da1f4b30f8a39c5063c8bae515e252" 97 | 98 | [[package]] 99 | name = "redox_termios" 100 | version = "0.1.1" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" 103 | dependencies = [ 104 | "redox_syscall", 105 | ] 106 | 107 | [[package]] 108 | name = "strsim" 109 | version = "0.10.0" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 112 | 113 | [[package]] 114 | name = "termcolor" 115 | version = "1.1.3" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 118 | dependencies = [ 119 | "winapi-util", 120 | ] 121 | 122 | [[package]] 123 | name = "termion" 124 | version = "1.5.2" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "dde0593aeb8d47accea5392b39350015b5eccb12c0d98044d856983d89548dea" 127 | dependencies = [ 128 | "libc", 129 | "numtoa", 130 | "redox_syscall", 131 | "redox_termios", 132 | ] 133 | 134 | [[package]] 135 | name = "textwrap" 136 | version = "0.15.0" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" 139 | 140 | [[package]] 141 | name = "urlencode" 142 | version = "1.0.2-dev" 143 | dependencies = [ 144 | "clap", 145 | "percent-encoding", 146 | ] 147 | 148 | [[package]] 149 | name = "winapi" 150 | version = "0.3.7" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "f10e386af2b13e47c89e7236a7a14a086791a2b88ebad6df9bf42040195cf770" 153 | dependencies = [ 154 | "winapi-i686-pc-windows-gnu", 155 | "winapi-x86_64-pc-windows-gnu", 156 | ] 157 | 158 | [[package]] 159 | name = "winapi-i686-pc-windows-gnu" 160 | version = "0.4.0" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 163 | 164 | [[package]] 165 | name = "winapi-util" 166 | version = "0.1.5" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 169 | dependencies = [ 170 | "winapi", 171 | ] 172 | 173 | [[package]] 174 | name = "winapi-x86_64-pc-windows-gnu" 175 | version = "0.4.0" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 178 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "urlencode" 3 | version = "1.0.2-dev" 4 | authors = ["Skyler Hawthorne "] 5 | description = "A CLI utility for URL-encoding or -decoding strings" 6 | repository = "https://github.com/dead10ck/urlencode" 7 | readme = "README.md" 8 | keywords = [ "urlencode", "url", "encode", "decode", "percent" ] 9 | categories = [ "command-line-utilities", "encoding" ] 10 | license = "MIT" 11 | edition = "2021" 12 | 13 | [dependencies] 14 | percent-encoding = "2.1.0" 15 | clap = "3.1.18" 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Skyler Hawthorne 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `urlencode` 2 | 3 | `urlencode` is a CLI utility for URL-encoding or -decoding strings. 4 | 5 | ## Usage 6 | 7 | You can give it a positional argument for a single string, or you can pipe input 8 | to it from stdin. 9 | 10 | ```bash 11 | $ urlencode 'foo bar' 12 | foo%20bar 13 | 14 | $ echo -e "foo bar\nbaz quux" | urlencode 15 | foo%20bar 16 | baz%20quux 17 | ``` 18 | 19 | You can pass `-d` or `--decode` to decode the input. 20 | 21 | ```bash 22 | $ urlencode -d 'foo%20bar' 23 | foo bar 24 | 25 | $ echo -e "foo%20bar\nbaz%20quux" | urlencode -d 26 | foo bar 27 | baz quux 28 | ``` 29 | 30 | Run `urlencode --help` to see all options. 31 | 32 | ### Encode sets 33 | 34 | Since different parts of a URL have different encoding requirements, there are 35 | many encode sets to choose from. See 36 | [this documentation page](https://url.spec.whatwg.org/) 37 | for an explanation of each. They can be specified with the `-e` or `--encode-set` 38 | option: 39 | 40 | ```bash 41 | $ echo 'https://docs.rs/percent-encoding/1.0.0/percent_encoding/index.html' | urlencode -e path 42 | https:%2F%2Fdocs.rs%2Fpercent-encoding%2F1.0.0%2Fpercent_encoding%2Findex.html 43 | 44 | $ echo 'https://docs.rs/percent-encoding/1.0.0/percent_encoding/index.html' | urlencode -e userinfo 45 | https%3A%2F%2Fdocs.rs%2Fpercent-encoding%2F1.0.0%2Fpercent_encoding%2Findex.html 46 | ---- 47 | ``` 48 | 49 | ### Cargo 50 | 51 | You can install with Cargo with: 52 | 53 | ```bash 54 | cargo install urlencode 55 | ``` -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | dev-version = true 2 | dev-version-ext = "dev" 3 | push-remote = "origin" 4 | -------------------------------------------------------------------------------- /src/encode_sets.rs: -------------------------------------------------------------------------------- 1 | //! see https://url.spec.whatwg.org/#c0-control-percent-encode-set 2 | 3 | use percent_encoding::AsciiSet; 4 | pub use percent_encoding::CONTROLS; 5 | 6 | pub(crate) const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`'); 7 | pub(crate) const QUERY: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'#').add(b'<').add(b'>'); 8 | pub(crate) const SPECIAL_QUERY: &AsciiSet = &QUERY.add(b'\''); 9 | pub(crate) const PATH: &AsciiSet = &QUERY.add(b'?').add(b'`').add(b'{').add(b'}'); 10 | pub(crate) const USERINFO: &AsciiSet = &PATH 11 | .add(b'/') 12 | .add(b':') 13 | .add(b';') 14 | .add(b'=') 15 | .add(b'@') 16 | .add(b'[') 17 | .add(b'^') 18 | .add(b'|'); 19 | pub(crate) const COMPONENT: &AsciiSet = &USERINFO.add(b'$').add(b'&').add(b'+').add(b','); 20 | pub(crate) const FORM: &AsciiSet = &COMPONENT.add(b'!').add(b'\'').add(b')').add(b'~'); 21 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Borrow; 2 | use std::error::Error; 3 | use std::io; 4 | use std::io::BufRead; 5 | use std::iter; 6 | 7 | use clap::{Arg, ArgMatches, Command}; 8 | use pe::AsciiSet; 9 | use percent_encoding as pe; 10 | 11 | const VERSION: &str = env!("CARGO_PKG_VERSION"); 12 | const AUTHORS: &str = env!("CARGO_PKG_AUTHORS"); 13 | 14 | mod encode_sets; 15 | 16 | fn main() { 17 | let matches = Command::new("urlencode") 18 | .version(VERSION) 19 | .author(AUTHORS) 20 | .about( 21 | "URL-encodes or -decodes the input. If INPUT is given, it encodes or \ 22 | decodes INPUT, otherwise it takes its input fromt stdin.", 23 | ) 24 | .arg( 25 | Arg::new("decode") 26 | .short('d') 27 | .long("decode") 28 | .help("Decode the input, rather than encode."), 29 | ) 30 | .arg( 31 | Arg::new("strict-decode") 32 | .short('s') 33 | .long("strict-decode") 34 | .help( 35 | "Decode the input non-lossily. If set, the program will fail if it \ 36 | encounters a sequence that does not produce valid UTF-8.", 37 | ), 38 | ) 39 | .arg( 40 | Arg::new("encode-set") 41 | .short('e') 42 | .long("encode-set") 43 | .takes_value(true) 44 | .possible_values(&[ 45 | "control", 46 | "fragment", 47 | "query", 48 | "squery", 49 | "path", 50 | "userinfo", 51 | "component", 52 | "form", 53 | ]) 54 | .default_value("component") 55 | .help("The encode set to use when encoding.") 56 | .long_help( 57 | "The encode set to use when encoding. See \ 58 | https://url.spec.whatwg.org/ \ 59 | for more details.", 60 | ), 61 | ) 62 | .arg(Arg::new("INPUT").help("The string to encode.").index(1)) 63 | .get_matches(); 64 | 65 | if let Err(e) = run(&matches) { 66 | eprintln!("error: {}", e); 67 | std::process::exit(1); 68 | } 69 | } 70 | 71 | fn run(arg_matches: &ArgMatches) -> Result<(), Box> { 72 | let stdin = io::stdin(); 73 | let stdout = io::stdout(); 74 | let mut stdout_handle = stdout.lock(); 75 | let mut stdin_handle = stdin.lock(); 76 | let encode_set = get_encode_set(arg_matches); 77 | 78 | if arg_matches.is_present("INPUT") { 79 | let input = arg_matches.value_of("INPUT").unwrap(); 80 | return transform_line(input, &mut stdout_handle, encode_set, arg_matches); 81 | } 82 | 83 | let mut buf = String::new(); 84 | 85 | while stdin_handle.read_line(&mut buf)? > 0 { 86 | transform_line(buf.trim_end(), &mut stdout_handle, encode_set, arg_matches)?; 87 | buf.clear(); 88 | } 89 | 90 | Ok(()) 91 | } 92 | 93 | fn get_encode_set(args: &ArgMatches) -> &'static AsciiSet { 94 | match args.value_of("encode-set").unwrap() { 95 | "control" => encode_sets::CONTROLS, 96 | "fragment" => encode_sets::FRAGMENT, 97 | "query" => encode_sets::QUERY, 98 | "squery" => encode_sets::SPECIAL_QUERY, 99 | "path" => encode_sets::PATH, 100 | "userinfo" => encode_sets::USERINFO, 101 | "component" => encode_sets::COMPONENT, 102 | "form" => encode_sets::FORM, 103 | _ => panic!("Unknown encode set"), 104 | } 105 | } 106 | 107 | fn transform_line( 108 | line: &str, 109 | output: &mut W, 110 | encode_set: &'static AsciiSet, 111 | arg_matches: &ArgMatches, 112 | ) -> Result<(), Box> { 113 | let decode_mode = arg_matches.is_present("decode") || arg_matches.is_present("strict-decode"); 114 | let lossy = !arg_matches.is_present("strict-decode"); 115 | 116 | if decode_mode { 117 | decode(line.as_bytes(), output, lossy) 118 | } else { 119 | encode(line, encode_set, output)?; 120 | Ok(()) 121 | } 122 | } 123 | 124 | fn decode( 125 | line: &[u8], 126 | output: &mut W, 127 | lossy: bool, 128 | ) -> Result<(), Box> { 129 | let decoder = pe::percent_decode(line); 130 | 131 | let decoded = if lossy { 132 | decoder.decode_utf8_lossy() 133 | } else { 134 | decoder.decode_utf8()? 135 | }; 136 | 137 | let result = write_output(iter::once(decoded.borrow()), output); 138 | 139 | match result { 140 | Err(e) => Err(Box::new(e)), 141 | _ => Ok(()), 142 | } 143 | } 144 | 145 | fn encode( 146 | line: &str, 147 | encode_set: &'static AsciiSet, 148 | output: &mut W, 149 | ) -> io::Result<()> { 150 | let encoded = pe::utf8_percent_encode(line, encode_set); 151 | write_output(encoded, output) 152 | } 153 | 154 | fn write_output<'a, B, W>(strings: B, output: &mut W) -> io::Result<()> 155 | where 156 | B: IntoIterator, 157 | W: io::Write, 158 | { 159 | for string in strings { 160 | output.write_all(string.as_bytes())?; 161 | } 162 | 163 | output.write_all("\n".as_bytes())?; 164 | 165 | Ok(()) 166 | } 167 | --------------------------------------------------------------------------------