├── .gitattributes ├── .gitignore ├── .hooks ├── .hookah │ └── lib.sh └── pre-commit ├── Bakefile.sh ├── LICENSE ├── README.md ├── bake ├── basalt.toml ├── docs ├── algo.md ├── index.md ├── pem.md └── uri.md ├── foxxo.toml ├── pkg └── src │ ├── public │ ├── algo.sh │ ├── pem.sh │ └── uri.sh │ └── util │ └── impl.sh └── tests ├── algo.bats ├── pem.bats ├── testdata └── example.pem ├── uri.bats └── util ├── init.sh └── test_util.sh /.gitattributes: -------------------------------------------------------------------------------- 1 | # foxxo start 2 | * text=auto eol=lf 3 | bake linguist-generated 4 | # foxxo end 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .basalt/ 2 | site/ -------------------------------------------------------------------------------- /.hooks/.hookah/lib.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | # Version: 0.1.2 3 | 4 | # @name Hookah lib.sh 5 | # @brief Hookah: An elegantly minimal solution for Git hooks 6 | # @description Hookah streamlines the process of managing Git hooks. This file is a 7 | # library of functions that can easily be used by hooks written in Bash. Use it by 8 | # prepending your hook script with the following 9 | # 10 | # ```bash 11 | # #!/usr/bin/env bash 12 | # 13 | # source "${0%/*}/.hookah/lib.sh" 14 | # hookah.init 15 | # ``` 16 | # 17 | # Learn more about it [on GitHub](https://github.com/hyperupcall/hookah) 18 | 19 | if [ -z "$BASH_VERSION" ]; then 20 | printf '%s\n' "Error: lib.sh: This script is only compatible with Bash. Exiting" >&2 21 | exit 1 22 | fi 23 | 24 | # @description Initiates the environment, sets up stacktrace printing on the 'ERR' trap, 25 | # and sets the directory to the root of the Git repository 26 | # @noargs 27 | hookah.init() { 28 | set -Eeo pipefail 29 | shopt -s dotglob extglob globasciiranges globstar lastpipe shift_verbose 30 | export LANG='C' LC_CTYPE='C' LC_NUMERIC='C' LC_TIME='C' LC_COLLATE='C' \ 31 | LC_MONETARY='C' LC_MESSAGES='C' LC_PAPER='C' LC_NAME='C' LC_ADDRESS='C' \ 32 | LC_TELEPHONE='C' LC_MEASUREMENT='C' LC_IDENTIFICATION='C' LC_ALL='C' 33 | trap '__hookah_trap_err' 'ERR' 34 | 35 | while [ ! -d '.git' ] && [ "$PWD" != / ]; do 36 | if ! cd ..; then 37 | __hookah_internal_die "Failed to cd to nearest Git repository" 38 | fi 39 | done 40 | if [ "$PWD" = / ]; then 41 | __hookah_internal_die "Failed to cd to nearest Git repository" 42 | fi 43 | 44 | # Prevent any possibilities of 'stdin in is not a tty' 45 | if ! exec &2 216 | 217 | # @internal 218 | __hookah_internal_warn() { 219 | local str="${2:-"Hookah (internal)"}" 220 | 221 | if __hookah_is_color; then 222 | printf "\033[1;33m\033[1m$str \033[1m[warn]:\033[0m %s\n" "$1" 223 | else 224 | printf "$str [warn]: %s\n" "$1" 225 | fi 226 | } >&2 227 | 228 | # @internal 229 | __hookah_internal_info() { 230 | local str="${2:-"Hookah (internal)"}" 231 | 232 | if __hookah_is_color; then 233 | printf "\033[0;36m\033[1m$str \033[1m[info]:\033[0m %s\n" "$1" 234 | else 235 | printf "$str [info]: %s\n" "$1" 236 | fi 237 | } 238 | 239 | # @internal 240 | __hookah_trap_err() { 241 | local error_code=$? 242 | 243 | __hookah_internal_error "Your hook did not exit successfully (exit code $error_code)" 244 | 245 | exit $error_code 246 | } 247 | -------------------------------------------------------------------------------- /.hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source "${0%/*}/.hookah/lib.sh" 4 | hookah.init 5 | 6 | hookah.run ./bake test 7 | hookah.run ./bake docs 8 | git add ./docs/* -------------------------------------------------------------------------------- /Bakefile.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | task.test() { 4 | bats ./tests 5 | } 6 | 7 | task.docs() { 8 | for f in algo pem uri; do 9 | shdoc < "./pkg/src/public/$f.sh" > "./docs/$f.md" 10 | done 11 | } 12 | 13 | task.lint() { 14 | shfmt -w -ln bash -sr ./pkg ./Bakefile.sh 15 | } 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, Edwin Kofler 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bash-algo 2 | 3 | Common algorithms implemented in pure Bash 4 | 5 | All algorithms use _only_ builtins and create _no subshells_ 6 | 7 | ## Algorithms 8 | 9 | - [x] URL encode 10 | - [x] URL decode 11 | - [x] base32 encode 12 | - [x] base32 decode 13 | - [x] base64 encode 14 | - [x] base64 decode 15 | - [ ] ascii85 encode 16 | - [ ] ascii85 decode 17 | - [x] PEM encode 18 | - [x] PEM decode 19 | - [ ] adler32 20 | - [ ] crc32 21 | - [ ] crc64 22 | - [ ] md5 23 | - [ ] sha1 24 | - [ ] sha256 25 | 26 | ## Roadmap 27 | 28 | - Pem_encode/decode uses subshells 29 | 30 | ## Installation 31 | 32 | Use [Basalt](https://github.com/hyperupcall/basalt), a Bash package manager, to add this project as a dependency 33 | 34 | ```sh 35 | basalt add hyperupcall/bash-algo 36 | ``` 37 | -------------------------------------------------------------------------------- /bake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # @name Bake 4 | # @brief Bake: A Bash-based Make alternative 5 | # @description Bake is a dead-simple task runner used to quickly cobble together shell scripts 6 | # 7 | # In a few words, Bake lets you call the following 'print' task with './bake print' 8 | # 9 | # ```bash 10 | # #!/usr/bin/env bash 11 | # task.print() { 12 | # printf '%s\n' 'Contrived example' 13 | # } 14 | # ``` 15 | # 16 | # Learn more about it [on GitHub](https://github.com/hyperupcall/bake) 17 | 18 | __global_bake_version='1.11.2' 19 | 20 | if [ "$BAKE_INTERNAL_ONLY_VERSION" = 'yes' ]; then 21 | # This is unused, but keep it here just in case 22 | # shellcheck disable=SC2034 23 | BAKE_INTERNAL_ONLY_VERSION_SUCCESS='yes' 24 | 25 | return 0 26 | fi 27 | 28 | if [ "$0" != "${BASH_SOURCE[0]}" ] && [ "$BAKE_INTERNAL_CAN_SOURCE" != 'yes' ]; then 29 | printf '%s\n' 'Error: This file should not be sourced' >&2 30 | return 1 31 | fi 32 | 33 | # @description Prints `$1` formatted as an error and the stacktrace to standard error, 34 | # then exits with code 1 35 | # @arg $1 string Text to print 36 | bake.die() { 37 | if [ -n "$1" ]; then 38 | __bake_error "$1. Exiting" 39 | else 40 | __bake_error 'Exiting' 41 | fi 42 | __bake_print_big --show-time '<- ERROR' 43 | 44 | __bake_print_stacktrace 45 | 46 | exit 1 47 | } 48 | 49 | # @description Prints `$1` formatted as a warning to standard error 50 | # @arg $1 string Text to print 51 | bake.warn() { 52 | if __bake_is_color; then 53 | printf "\033[1;33m%s:\033[0m %s\n" 'Warn' "$1" 54 | else 55 | printf '%s: %s\n' 'Warn' "$1" 56 | fi 57 | } >&2 58 | 59 | # @description Prints `$1` formatted as information to standard output 60 | # @arg $1 string Text to print 61 | bake.info() { 62 | if __bake_is_color; then 63 | printf "\033[0;34m%s:\033[0m %s\n" 'Info' "$1" 64 | else 65 | printf '%s: %s\n' 'Info' "$1" 66 | fi 67 | } 68 | 69 | # breaking: remove in v2 70 | # @description Dies if any of the supplied variables are empty. Deprecated in favor of 'bake.assert_not_empty' 71 | # @arg $@ string Names of variables to check for emptiness 72 | # @see bake.assert_not_empty 73 | bake.assert_nonempty() { 74 | __bake_internal_warn "Function 'bake.assert_nonempty' is deprecated. Please use 'bake.assert_not_empty' instead" 75 | bake.assert_not_empty "$@" 76 | } 77 | 78 | # @description Dies if any of the supplied variables are empty 79 | # @arg $@ string Names of variables to check for emptiness 80 | bake.assert_not_empty() { 81 | local variable_name= 82 | for variable_name; do 83 | local -n ____variable="$variable_name" 84 | 85 | if [ -z "$____variable" ]; then 86 | bake.die "Failed because variable '$variable_name' is empty" 87 | fi 88 | done; unset -v variable_name 89 | } 90 | 91 | # @description Dies if a command cannot be found 92 | # @arg $1 string Command name to test for existence 93 | bake.assert_cmd() { 94 | local cmd=$1 95 | 96 | if [ -z "$cmd" ]; then 97 | bake.die "Argument must not be empty" 98 | fi 99 | 100 | if ! command -v "$cmd" &>/dev/null; then 101 | bake.die "Failed to find command '$cmd'. Please install it before continuing" 102 | fi 103 | } 104 | 105 | # @description Determine if a flag was passed as an argument 106 | # @arg $1 string Flag name to test for 107 | # @arg $@ string Rest of the arguments to search through 108 | bake.has_flag() { 109 | local flag_name="$1" 110 | 111 | if [ -z "$flag_name" ]; then 112 | bake.die "Argument must not be empty" 113 | fi 114 | if ! shift; then 115 | bake.die 'Failed to shift' 116 | fi 117 | 118 | local -a flags=("$@") 119 | if ((${#flags[@]} == 0)); then 120 | flags=("${__bake_args_userflags[@]}") 121 | fi 122 | 123 | local arg= 124 | for arg in "${flags[@]}"; do 125 | if [ "$arg" = "$flag_name" ]; then 126 | return 0 127 | fi 128 | done; unset -v arg 129 | 130 | return 1 131 | } 132 | 133 | # @description Change the behavior of Bake. See [guide.md](./docs/guide.md) for details 134 | # @arg $1 string Name of config property to change 135 | # @arg $2 string New value of config property 136 | bake.cfg() { 137 | local cfg="$1" 138 | local value="$2" 139 | 140 | # breaking: remove in v2 141 | case $cfg in 142 | stacktrace) 143 | case $value in 144 | yes) __bake_internal_warn "Passing either 'yes' or 'no' as a value for 'bake.cfg stacktrace' is deprecated. Instead, use either 'on' or 'off'"; __bake_cfg_stacktrace='on' ;; 145 | no) __bake_internal_warn "Passing either 'yes' or 'no' as a value for 'bake.cfg stacktrace' is deprecated. Instead, use either 'on' or 'off'"; __bake_cfg_stacktrace='off' ;; 146 | on|off) __bake_cfg_stacktrace=$value ;; 147 | *) __bake_internal_bigdie "Config property '$cfg' accepts only either 'on' or 'off'" ;; 148 | esac 149 | ;; 150 | big-print) 151 | case $value in 152 | yes|no|on|off) __bake_internal_warn "Passing any once-valid value to 'bake.cfg big-print' is deprecated. Instead, use function comments" ;; 153 | *) __bake_internal_bigdie "Config property '$cfg' accepts only either 'on' or 'off'" ;; 154 | esac 155 | ;; 156 | pedantic-task-cd) 157 | case $value in 158 | yes) __bake_internal_warn "Passing either 'yes' or 'no' as a value for 'bake.cfg pedantic-task-cd' is deprecated. Instead, use either 'on' or 'off'"; trap '__bake_trap_debug_fixcd' 'DEBUG' ;; 159 | no) __bake_internal_warn "Passing either 'yes' or 'no' as a value for 'bake.cfg pedantic-task-cd' is deprecated. Instead, use either 'on' or 'off'"; trap - 'DEBUG' ;; 160 | on) trap '__bake_trap_debug_fixcd' 'DEBUG' ;; 161 | off) trap - 'DEBUG' ;; 162 | *) __bake_internal_bigdie "Config property '$cfg' accepts only either 'on' or 'off'" ;; 163 | esac 164 | ;; 165 | *) 166 | __bake_internal_bigdie "No config property matched '$cfg'" 167 | ;; 168 | esac 169 | } 170 | 171 | # @description Prints stacktrace 172 | # @internal 173 | __bake_print_stacktrace() { 174 | if [ "$__bake_cfg_stacktrace" = 'on' ]; then 175 | if __bake_is_color; then 176 | printf '\033[4m%s\033[0m\n' 'Stacktrace:' 177 | else 178 | printf '%s\n' 'Stacktrace:' 179 | fi 180 | 181 | local i= 182 | for ((i=0; i<${#FUNCNAME[@]}-1; ++i)); do 183 | local __bash_source="${BASH_SOURCE[$i]}"; __bash_source=${__bash_source##*/} 184 | printf '%s\n' " in ${FUNCNAME[$i]} ($__bash_source:${BASH_LINENO[$i-1]})" 185 | done; unset -v i __bash_source 186 | fi 187 | } >&2 188 | 189 | # @description Function that is executed when the 'ERR' event is trapped 190 | # @internal 191 | __bake_trap_err() { 192 | local error_code=$? 193 | 194 | __bake_print_big --show-time '<- ERROR' 195 | __bake_internal_error "Your Bakefile did not exit successfully (exit code $error_code)" 196 | __bake_print_stacktrace 197 | 198 | exit $error_code 199 | } >&2 200 | 201 | # @description When running a task, ensure that we start in the correct directory 202 | # @internal 203 | __bake_trap_debug_fixcd_current_fn= 204 | __bake_trap_debug_fixcd() { 205 | local current_function="${FUNCNAME[1]}" 206 | 207 | if [[ $current_function != "$__bake_trap_debug_fixcd_current_fn" \ 208 | && $current_function == task.* ]]; then 209 | if ! cd -- "$BAKE_ROOT"; then 210 | __bake_internal_die "Failed to cd to \$BAKE_ROOT" 211 | fi 212 | fi 213 | 214 | __bake_trap_debug_fixcd_current_fn=$current_function 215 | } >&2 216 | 217 | # @description Ensure that the main function is not ran 218 | # @internal 219 | __bake_trap_debug_barrier() { 220 | local current_function="${FUNCNAME[1]}" 221 | 222 | if [ "$current_function" = '__bake_main' ]; then 223 | # shellcheck disable=SC2034 224 | local __version_old="$__global_bake_version" 225 | 226 | trap - DEBUG 227 | unset -v BAKE_INTERNAL_ONLY_VERSION 228 | unset -v BAKE_INTERNAL_CAN_SOURCE 229 | 230 | __bake_copy_bakescript 231 | if [ "$BAKE_FLAG_UPDATE" = 'yes' ]; then 232 | exit 0 233 | else 234 | # shellcheck disable=SC2154 235 | exec "$BAKE_ROOT/bake" "${__bake_global_backup_args[@]}" 236 | fi 237 | fi 238 | } 239 | 240 | # @description Test whether color should be outputed 241 | # @exitcode 0 if should print color 242 | # @exitcode 1 if should not print color 243 | # @internal 244 | __bake_is_color() { 245 | local fd="1" 246 | 247 | if [ ${NO_COLOR+x} ]; then 248 | return 1 249 | fi 250 | 251 | if [[ $FORCE_COLOR == @(1|2|3) ]]; then 252 | return 0 253 | elif [[ $FORCE_COLOR == '0' ]]; then 254 | return 1 255 | fi 256 | 257 | if [ "$TERM" = 'dumb' ]; then 258 | return 1 259 | fi 260 | 261 | if [ -t "$fd" ]; then 262 | return 0 263 | fi 264 | 265 | return 1 266 | } 267 | 268 | # @description Calls `__bake_internal_error` and terminates with code 1 269 | # @arg $1 string Text to print 270 | # @internal 271 | __bake_internal_die() { 272 | __bake_internal_error "$1. Exiting" 273 | exit 1 274 | } 275 | 276 | # @description Calls `__bake_internal_error` and terminates with code 1. Before 277 | # doing so, it closes with "<- ERROR" big text 278 | # @arg $1 string Text to print 279 | # @internal 280 | __bake_internal_bigdie() { 281 | __bake_print_big '<- ERROR' 282 | 283 | __bake_internal_error "$1. Exiting" 284 | exit 1 285 | } 286 | 287 | # @description Prints `$1` formatted as an internal Bake error to standard error 288 | # @arg $1 Text to print 289 | # @internal 290 | __bake_internal_error() { 291 | if __bake_is_color; then 292 | printf "\033[0;31m%s:\033[0m %s\n" "Error (bake)" "$1" 293 | else 294 | printf '%s: %s\n' 'Error (bake)' "$1" 295 | fi 296 | } >&2 297 | 298 | # @description Prints `$1` formatted as an internal Bake warning to standard error 299 | # @arg $1 Text to print 300 | # @internal 301 | __bake_internal_warn() { 302 | if __bake_is_color; then 303 | printf "\033[0;33m%s:\033[0m %s\n" "Warn (bake)" "$1" 304 | else 305 | printf '%s: %s\n' 'Warn (bake)' "$1" 306 | fi 307 | } >&2 308 | 309 | # @description Prints `$1` formatted as an error to standard error. This is not called because 310 | # I do not wish to surface a public 'bake.error' function. All errors should halt execution 311 | # @arg $1 string Text to print 312 | # @internal 313 | __bake_error() { 314 | if __bake_is_color; then 315 | printf "\033[0;31m%s:\033[0m %s\n" 'Error' "$1" 316 | else 317 | printf '%s: %s\n' 'Error' "$1" 318 | fi 319 | } >&2 320 | 321 | 322 | # @description Tests if the './bake' file should be replaced. It should only 323 | # be replaced if we're not in an interactive Git context 324 | # @internal 325 | __bake_should_replace_bakescript() { 326 | local dir="$BAKE_ROOT" 327 | while [ ! -d "$dir/.git" ] && [[ -n "$dir" ]]; do 328 | dir=${dir%/*} 329 | done 330 | 331 | if [ -d "$dir/.git" ]; then 332 | # ref: https://github.com/git/git/blob/d420dda0576340909c3faff364cfbd1485f70376/wt-status.c#L1749 333 | # ref2: https://github.com/Byron/gitoxide/blob/375051fa97d79f95fa7179b536e616c4aefd88e2/git-repository/src/repository/state.rs#L8 334 | local file= 335 | for file in {rebase-apply/applying,rebase-apply/rebasing,rebase-apply,rebase-merge/interactive,rebase-merge,CHERRY_PICK_HEAD,MERGE_HEAD,BISECT_LOG,REVERT_HEAD}; do 336 | if [ -f "$dir/.git/$file" ]; then 337 | return 1 338 | fi 339 | done; unset -v file 340 | fi 341 | 342 | return 0 343 | } 344 | 345 | # @description Copy 'bake' script to current context 346 | # @internal 347 | __bake_copy_bakescript() { 348 | # If there was an older version, and the versions are different, let the user know 349 | if [ -z ${__version_old+x} ]; then 350 | # shellcheck disable=SC2154 351 | __bake_internal_warn "Updating from version <=1.10.0 to $__version_new" 352 | else 353 | if [ -n "$__version_old" ] && [ "$__version_old" != "$__version_new" ]; then 354 | __bake_internal_warn "Updating from version $__version_old to $__version_new" 355 | fi 356 | fi 357 | 358 | # shellcheck disable=SC2154 359 | if ! cp -f "$__bake_dynamic_script" "$BAKE_ROOT/bake"; then 360 | __bake_internal_die "Failed to copy 'bakeScript.sh'" 361 | fi 362 | if ! printf '\n%s\n' '__bake_main "$@"' >> "$BAKE_ROOT/bake"; then 363 | __bake_internal_die "Failed to append to '$BAKE_ROOT/bake'" 364 | fi 365 | 366 | if ! chmod +x "$BAKE_ROOT/bake"; then 367 | __bake_internal_die "Failed to 'chmod +x' bake script" >&2 368 | fi 369 | } 370 | 371 | # @description Prepares internal variables for time setting 372 | # @internal 373 | __bake_time_prepare() { 374 | if ((BASH_VERSINFO[0] >= 5)); then 375 | __bake_global_timestart=$EPOCHSECONDS 376 | fi 377 | } 378 | 379 | # @description Determines total approximate execution time of a task 380 | # @set string REPLY 381 | # @internal 382 | __bake_time_get_total_pretty() { 383 | unset -v REPLY; REPLY= 384 | 385 | if ((BASH_VERSINFO[0] >= 5)); then 386 | local timediff=$((EPOCHSECONDS - __bake_global_timestart)) 387 | if ((timediff < 1)); then 388 | return 389 | fi 390 | 391 | local seconds=$((timediff % 60)) 392 | local minutes=$((timediff / 60 % 60)) 393 | local hours=$((timediff / 3600 % 60)) 394 | 395 | REPLY="${seconds}s" 396 | 397 | if ((minutes > 0)); then 398 | REPLY="${minutes}m $REPLY" 399 | fi 400 | 401 | if ((hours > 0)); then 402 | REPLY="${hours}h $REPLY" 403 | fi 404 | fi 405 | } 406 | 407 | # @description Parses the configuration for functions embeded in comments. This properly 408 | # parses inherited config from the 'init' function 409 | # @set string __bake_config_docstring 410 | # @set array __bake_config_watchexec_args 411 | # @set object __bake_config_map 412 | # @internal 413 | __bake_parse_task_comments() { 414 | local task_name="$1" 415 | 416 | local tmp_docstring= 417 | local -a tmp_watch_args=() 418 | local -A tmp_cfg_map=() 419 | local line= 420 | while IFS= read -r line || [ -n "$line" ]; do 421 | if [[ $line =~ ^[[:space:]]*#[[:space:]](doc|watch|config):[[:space:]]*(.*?)$ ]]; then 422 | local comment_category="${BASH_REMATCH[1]}" 423 | local comment_content="${BASH_REMATCH[2]}" 424 | 425 | if [ "$comment_category" = 'doc' ]; then 426 | tmp_docstring=$comment_content 427 | elif [ "$comment_category" = 'watch' ]; then 428 | readarray -td' ' tmp_watch_args <<< "$comment_content" 429 | tmp_watch_args[-1]=${tmp_watch_args[-1]::-1} 430 | elif [ "$comment_category" = 'config' ]; then 431 | local -a pairs=() 432 | readarray -td' ' pairs <<< "$comment_content" 433 | pairs[-1]=${pairs[-1]::-1} 434 | 435 | # shellcheck disable=SC1007 436 | local pair= key= value= 437 | for pair in "${pairs[@]}"; do 438 | IFS='=' read -r key value <<< "$pair" 439 | 440 | tmp_cfg_map[$key]=${value:-on} 441 | done; unset -v pair 442 | fi 443 | fi 444 | 445 | # function() 446 | if [[ $line =~ ^([[:space:]]*function[[:space:]]*)?(.*?)[[:space:]]*\(\)[[:space:]]*\{ ]]; then 447 | local function_name="${BASH_REMATCH[2]}" 448 | 449 | if [ "$function_name" == task."$task_name" ]; then 450 | __bake_config_docstring=$tmp_docstring 451 | 452 | __bake_config_watchexec_args+=("${tmp_watch_args[@]}") 453 | 454 | local key= 455 | for key in "${!tmp_cfg_map[@]}"; do 456 | __bake_config_map[$key]=${tmp_cfg_map[$key]} 457 | done; unset -v key 458 | 459 | break 460 | elif [ "$function_name" == 'init' ]; then 461 | __bake_config_watchexec_args+=("${tmp_watch_args[@]}") 462 | 463 | local key= 464 | for key in "${!tmp_cfg_map[@]}"; do 465 | __bake_config_map[$key]=${tmp_cfg_map[$key]} 466 | done; unset -v key 467 | fi 468 | 469 | tmp_docstring= 470 | tmp_watch_args=() 471 | tmp_cfg_map=() 472 | fi 473 | done < "$BAKE_FILE"; unset -v line 474 | } 475 | 476 | # @description Nicely prints all 'Bakefile.sh' tasks to standard output 477 | # @internal 478 | __bake_print_tasks() { 479 | local str=$'Tasks:\n' 480 | 481 | local -a task_flags=() 482 | # shellcheck disable=SC1007 483 | local line= task_docstring= 484 | while IFS= read -r line || [ -n "$line" ]; do 485 | # doc 486 | if [[ $line =~ ^[[:space:]]*#[[:space:]]doc:[[:space:]](.*?) ]]; then 487 | task_docstring=${BASH_REMATCH[1]} 488 | fi 489 | 490 | # flag 491 | if [[ $line =~ bake\.has_flag[[:space:]][\'\"]?([[:alnum:]]+) ]]; then 492 | task_flags+=("[--${BASH_REMATCH[1]}]") 493 | fi 494 | 495 | if [[ $line =~ ^([[:space:]]*function[[:space:]]*)?task\.(.*?)\(\)[[:space:]]*\{[[:space:]]*(#[[:space:]]*(.*))? ]]; then 496 | local matched_function_name="${BASH_REMATCH[2]}" 497 | local matched_comment="${BASH_REMATCH[4]}" 498 | 499 | if ((${#task_flags[@]} > 0)); then 500 | str+=" ${task_flags[*]}"$'\n' 501 | fi 502 | task_flags=() 503 | 504 | str+=" -> $matched_function_name" 505 | 506 | if [[ -n "$matched_comment" || -n "$task_docstring" ]]; then 507 | if [ -n "$matched_comment" ]; then 508 | __bake_internal_warn "Adjacent documentation comments are deprecated. Instead, write a comment above 'task.$matched_function_name()' like so: '# doc: $matched_comment'" 509 | task_docstring=$matched_comment 510 | fi 511 | 512 | if __bake_is_color; then 513 | str+=$' \033[3m'"($task_docstring)"$'\033[0m' 514 | else 515 | str+=" ($task_docstring)" 516 | fi 517 | fi 518 | 519 | str+=$'\n' 520 | task_docstring= 521 | fi 522 | done < "$BAKE_FILE"; unset -v line 523 | 524 | if [ -z "$str" ]; then 525 | if __bake_is_color; then 526 | str=$' \033[3mNo tasks\033[0m\n' 527 | else 528 | str=$' No tasks\n' 529 | fi 530 | fi 531 | 532 | printf '%s' "$str" 533 | } >&2 534 | 535 | # @description Prints text that takes up the whole terminal width 536 | # @arg $1 string Text to print 537 | # @internal 538 | __bake_print_big() { 539 | if [ "${__bake_config_map[big-print]}" = 'off' ]; then 540 | return 541 | fi 542 | 543 | if [ "$1" = '--show-time' ]; then 544 | local flag_show_time='yes' 545 | local print_text="$2" 546 | else 547 | local flag_show_time='no' 548 | local print_text="$1" 549 | fi 550 | 551 | __bake_time_get_total_pretty 552 | local time_str="${REPLY:+ ($REPLY) }" 553 | 554 | # shellcheck disable=SC1007 555 | local _stty_height= _stty_width= 556 | read -r _stty_height _stty_width < <( 557 | if stty size &>/dev/null; then 558 | stty size 559 | else 560 | # Only columns is used by Bake, so '20 was chosen arbitrarily 561 | if [ -n "$COLUMNS" ]; then 562 | printf '%s\n' "20 $COLUMNS" 563 | else 564 | printf '%s\n' '20 80' 565 | fi 566 | fi 567 | ) 568 | 569 | local separator_text= 570 | # shellcheck disable=SC2183 571 | printf -v separator_text '%*s' $((_stty_width - ${#print_text} - 1)) 572 | printf -v separator_text '%s' "${separator_text// /=}" 573 | if [[ "$flag_show_time" == 'yes' && -n "$time_str" ]]; then 574 | separator_text="${separator_text::5}${time_str}${separator_text:5+${#time_str}:${#separator_text}}" 575 | fi 576 | if __bake_is_color; then 577 | printf '\033[1m%s %s\033[0m\n' "$print_text" "$separator_text" 578 | else 579 | printf '%s %s\n' "$print_text" "$separator_text" 580 | fi 581 | } >&2 582 | 583 | # @description Parses the arguments. This also includes setting the the 'BAKE_ROOT' 584 | # and 'BAKE_FILE' variables 585 | # @set REPLY Number of times to shift 586 | # @internal 587 | __bake_parse_args() { 588 | unset -v REPLY; REPLY= 589 | local -i total_shifts=0 590 | 591 | local arg= 592 | for arg; do case $arg in 593 | -f) 594 | BAKE_FILE=$2 595 | if [ -z "$BAKE_FILE" ]; then 596 | __bake_internal_die "A value was not specified for for flag '-f'" 597 | fi 598 | ((total_shifts += 2)) 599 | if ! shift 2; then 600 | __bake_internal_die 'Failed to shift' 601 | fi 602 | 603 | if [ ! -e "$BAKE_FILE" ]; then 604 | __bake_internal_die "Specified file '$BAKE_FILE' does not exist" 605 | fi 606 | if [ ! -f "$BAKE_FILE" ]; then 607 | __bake_internal_die "Specified file '$BAKE_FILE' is not actually a file" 608 | fi 609 | ;; 610 | -h) 611 | local flag_help='yes' 612 | if ! shift; then 613 | __bake_internal_die 'Failed to shift' 614 | fi 615 | ;; 616 | -w) 617 | ((total_shifts += 1)) 618 | if ! shift; then 619 | __bake_internal_die 'Failed to shift' 620 | fi 621 | 622 | if [[ ! -v 'BAKE_INTERNAL_NO_WATCH_OVERRIDE' ]]; then 623 | BAKE_FLAG_WATCH='yes' 624 | fi 625 | ;; 626 | -u) 627 | ((total_shifts += 1)) 628 | if ! shift; then 629 | __bake_internal_die 'Failed to shift' 630 | fi 631 | 632 | BAKE_FLAG_UPDATE='yes' 633 | ;; 634 | -v) 635 | printf '%s\n' "Version: $__global_bake_version" 636 | exit 0 637 | ;; 638 | *) 639 | break 640 | ;; 641 | esac done; unset -v arg 642 | 643 | if [ -n "$BAKE_FILE" ]; then 644 | BAKE_ROOT=$( 645 | # shellcheck disable=SC1007 646 | CDPATH= cd -- "${BAKE_FILE%/*}" 647 | printf '%s\n' "$PWD" 648 | ) 649 | BAKE_FILE="$BAKE_ROOT/${BAKE_FILE##*/}" 650 | else 651 | if ! BAKE_ROOT=$( 652 | while [ ! -f './Bakefile.sh' ] && [ "$PWD" != / ]; do 653 | if ! cd ..; then 654 | exit 1 655 | fi 656 | done 657 | 658 | if [ "$PWD" = / ]; then 659 | exit 1 660 | fi 661 | 662 | printf '%s' "$PWD" 663 | ); then 664 | __bake_internal_die "Failed to find 'Bakefile.sh'" 665 | fi 666 | BAKE_FILE="$BAKE_ROOT/Bakefile.sh" 667 | fi 668 | 669 | if [ "$flag_help" = 'yes' ]; then 670 | cat <<-"EOF" 671 | Usage: bake [-h|-v] [-u|-w] [-f ] [var=value ...] [args ...] 672 | EOF 673 | __bake_print_tasks 674 | exit 675 | fi 676 | 677 | REPLY=$total_shifts 678 | } 679 | 680 | # @description Main function 681 | # @internal 682 | __bake_main() { 683 | # Environment and configuration boilerplate 684 | set -ETeo pipefail 685 | shopt -s dotglob extglob globasciiranges globstar lastpipe shift_verbose 686 | export LANG='C' LC_CTYPE='C' LC_NUMERIC='C' LC_TIME='C' LC_COLLATE='C' \ 687 | LC_MONETARY='C' LC_MESSAGES='C' LC_PAPER='C' LC_NAME='C' LC_ADDRESS='C' \ 688 | LC_TELEPHONE='C' LC_MEASUREMENT='C' LC_IDENTIFICATION='C' LC_ALL='C' 689 | trap '__bake_trap_err' 'ERR' 690 | trap ':' 'INT' # Ensure Ctrl-C ends up printing <- ERROR ==== etc. 691 | 692 | declare -ga __bake_args_original=("$@") 693 | 694 | # Parse arguments 695 | # Set `BAKE_{ROOT,FILE,FLAG_WATCH}` 696 | BAKE_ROOT=; BAKE_FILE=; BAKE_FLAG_WATCH= 697 | __bake_parse_args "$@" 698 | if ! shift $REPLY; then 699 | __bake_internal_die 'Failed to shift' 700 | fi 701 | 702 | # Set variables à la Make 703 | # shellcheck disable=SC1007 704 | local __bake_key= __bake_value= __bake_arg= 705 | for __bake_arg; do case $__bake_arg in 706 | *=*) 707 | IFS='=' read -r __bake_key __bake_value <<< "$__bake_arg" 708 | 709 | # If 'key=value' is passed, create global variable $value 710 | declare -g "$__bake_key" 711 | local -n __bake_variable="$__bake_key" 712 | __bake_variable="$__bake_value" 713 | 714 | # If 'key=value' is passed, create global variable $value_key 715 | declare -g "var_$__bake_key" 716 | local -n __bake_variable="var_$__bake_key" 717 | __bake_variable="$__bake_value" 718 | 719 | if ! shift; then 720 | __bake_internal_die 'Failed to shift' 721 | fi 722 | ;; 723 | *) 724 | break 725 | ;; 726 | esac done; unset -v __bake_arg 727 | unset -v __bake_key __bake_value 728 | unset -vn __bake_variable 729 | 730 | local __bake_task="$1" 731 | if [ -z "$__bake_task" ]; then 732 | __bake_internal_error 'No valid task supplied' 733 | __bake_print_tasks 734 | exit 1 735 | fi 736 | if ! shift; then 737 | __bake_internal_die 'Failed to shift' 738 | fi 739 | 740 | declare -ga __bake_args_userflags=("$@") 741 | 742 | declare -g __bake_config_docstring= 743 | declare -ga __bake_config_watchexec_args=() 744 | declare -gA __bake_config_map=( 745 | [stacktrace]='off' 746 | [big-print]='on' 747 | [pedantic-cd]='off' 748 | ) 749 | 750 | if [ "$BAKE_FLAG_WATCH" = 'yes' ]; then 751 | if ! command -v watchexec &>/dev/null; then 752 | __bake_internal_die "Executable not found: 'watchexec'" 753 | fi 754 | 755 | __bake_parse_task_comments "$__bake_task" 756 | 757 | # shellcheck disable=SC1007 758 | BAKE_INTERNAL_NO_WATCH_OVERRIDE= exec watchexec "${__bake_config_watchexec_args[@]}" "$BAKE_ROOT/bake" -- "${__bake_args_original[@]}" 759 | else 760 | if ! cd -- "$BAKE_ROOT"; then 761 | __bake_internal_die "Failed to cd" 762 | fi 763 | 764 | # shellcheck disable=SC2097,SC1007,SC1090 765 | __bake_task= source "$BAKE_FILE" 766 | 767 | if declare -f task."$__bake_task" >/dev/null 2>&1; then 768 | __bake_parse_task_comments "$__bake_task" 769 | 770 | __bake_print_big "-> RUNNING TASK '$__bake_task'" 771 | 772 | if declare -f init >/dev/null 2>&1; then 773 | init "$__bake_task" 774 | fi 775 | 776 | __bake_time_prepare 777 | 778 | task."$__bake_task" "${__bake_args_userflags[@]}" 779 | 780 | __bake_print_big --show-time "<- DONE" 781 | else 782 | __bake_internal_error "Task '$__bake_task' not found" 783 | __bake_print_tasks 784 | exit 1 785 | fi 786 | fi 787 | } 788 | 789 | __bake_entrypoint() { 790 | printf '%s\n' 'Not implemented.' 791 | } 792 | 793 | if [[ -v BAKE_INTERNAL_EXPERIMENTAL_SINGLEFILE ]]; then 794 | __bake_entrypoint "$@" 795 | fi 796 | 797 | __bake_main "$@" 798 | -------------------------------------------------------------------------------- /basalt.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = 'bash-algo' 3 | slug = 'algo' 4 | version = '0.2.0' 5 | authors = ['Edwin Kofler '] 6 | description = 'Common algorithms implemented in pure Bash' 7 | 8 | [run] 9 | dependencies = ['https://github.com/hyperupcall/bats-all@v4.3.0', 'https://github.com/hyperupcall/bash-core@v0.6.0'] 10 | sourceDirs = ['pkg/src/public', 'pkg/src/util'] 11 | builtinDirs = [] 12 | binDirs = [] 13 | completionDirs = [] 14 | manDirs = [] 15 | 16 | [run.shellEnvironment] 17 | 18 | [run.setOptions] 19 | errexit = 'on' 20 | pipefail = 'on' 21 | 22 | [run.shoptOptions] 23 | shift_verbose = 'on' 24 | -------------------------------------------------------------------------------- /docs/algo.md: -------------------------------------------------------------------------------- 1 | ## Index 2 | 3 | * [algo.base32_encode()](#algobase32_encode) 4 | * [algo.base32_decode()](#algobase32_decode) 5 | * [algo.base64_encode()](#algobase64_encode) 6 | * [algo.base64_decode()](#algobase64_decode) 7 | 8 | ### algo.base32_encode() 9 | 10 | Encodes an arbitrary string to a base32 sequence of characters 11 | 12 | #### Arguments 13 | 14 | * **$1** (string): input 15 | 16 | ### algo.base32_decode() 17 | 18 | Decodes a base32 sequence of characters to a string 19 | 20 | #### Arguments 21 | 22 | * **$1** (string): input 23 | 24 | ### algo.base64_encode() 25 | 26 | Encodes an arbitrary string to a base32 sequence of characters 27 | 28 | #### Arguments 29 | 30 | * **$1** (string): input 31 | 32 | ### algo.base64_decode() 33 | 34 | Decodes a base64 sequence of characters to a string 35 | 36 | #### Arguments 37 | 38 | * **$1** (string): input 39 | 40 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # bash-algo 2 | 3 | There are three parts: 4 | 5 | - FOr basen encoding/decoding, see [Algo](./algo.md) 6 | - For PEM encoding/decoding, see [PEM](./pem.md) 7 | - For url encoding/decoding, see [URI](./uri.md) 8 | -------------------------------------------------------------------------------- /docs/pem.md: -------------------------------------------------------------------------------- 1 | ## Index 2 | 3 | * [algo.pem_encode()](#algopem_encode) 4 | * [algo.pem_decode()](#algopem_decode) 5 | 6 | ### algo.pem_encode() 7 | 8 | PEM encode data 9 | 10 | #### Arguments 11 | 12 | * **$1** (string): PEM label 13 | * **$2** (string): PEM contents 14 | * **$3** (string): Output file 15 | 16 | ### algo.pem_decode() 17 | 18 | PEM decode data 19 | 20 | #### Arguments 21 | 22 | * **$1** (string): input 23 | 24 | -------------------------------------------------------------------------------- /docs/uri.md: -------------------------------------------------------------------------------- 1 | ## Index 2 | 3 | * [algo.uri_encode()](#algouri_encode) 4 | * [algo.uri_decode()](#algouri_decode) 5 | 6 | ### algo.uri_encode() 7 | 8 | URI encode a particular string 9 | 10 | #### Arguments 11 | 12 | * **$1** (string): input 13 | 14 | ### algo.uri_decode() 15 | 16 | URI decode a particular string 17 | 18 | #### Arguments 19 | 20 | * **$1** (string): input 21 | 22 | -------------------------------------------------------------------------------- /foxxo.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | ecosystem = 'basalt' 3 | form = 'unknown' 4 | for = 'anyone' 5 | status = 'experimental' 6 | 7 | [discovery] 8 | categories = [] 9 | tags = [] -------------------------------------------------------------------------------- /pkg/src/public/algo.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | # @description Encodes an arbitrary string to a base32 sequence of characters 4 | # @arg $1 string input 5 | algo.base32_encode() { 6 | unset -v REPLY; REPLY= 7 | 8 | local flag="$1" 9 | if [ -z "$flag" ]; then 10 | core.err_set 'INVALID_ARGS' || return 11 | fi 12 | 13 | local input_byte_{one,two,three,four,five}= 14 | if [ "$flag" = '--use-arg' ]; then 15 | local arg="$2" 16 | if [ -z "$arg" ]; then 17 | core.err_set 'INVALID_ARGS' || return 18 | fi 19 | 20 | # shellcheck disable=SC1007 21 | local i= b= 22 | # shellcheck disable=SC2059 23 | for ((i=0; i<${#arg}; i=i+5)); do 24 | b="${arg:$i:1}"; printf -v input_byte_one "${b:+%d}" "'$b" 25 | b="${arg:$i+1:1}"; printf -v input_byte_two "${b:+%d}" "'$b" 26 | b="${arg:$i+2:1}"; printf -v input_byte_three "${b:+%d}" "'$b" 27 | b="${arg:$i+3:1}"; printf -v input_byte_four "${b:+%d}" "'$b" 28 | b="${arg:$i+4:1}"; printf -v input_byte_five "${b:+%d}" "'$b" 29 | 30 | algo.base32_encode_impl 31 | done; unset -v i b 32 | elif [ "$flag" = '--use-stdin' ]; then 33 | local line= 34 | 35 | while IFS= read -r line; do 36 | while [ -n "$line" ]; do 37 | line=${line#"${line%%[![:space:]]*}"} 38 | input_byte_one=${line%% *} 39 | line=${line#* } 40 | 41 | if [[ $line =~ ^[[:digit:]]+$ ]]; then 42 | line= 43 | fi 44 | 45 | line=${line#"${line%%[![:space:]]*}"} 46 | input_byte_two=${line%% *} 47 | line=${line#* } 48 | 49 | if [[ $line =~ ^[[:digit:]]+$ ]]; then 50 | line= 51 | fi 52 | 53 | line=${line#"${line%%[![:space:]]*}"} 54 | input_byte_three=${line%% *} 55 | line=${line#* } 56 | 57 | if [[ $line =~ ^[[:digit:]]+$ ]]; then 58 | line= 59 | fi 60 | 61 | line=${line#"${line%%[![:space:]]*}"} 62 | input_byte_four=${line%% *} 63 | line=${line#* } 64 | 65 | if [[ $line =~ ^[[:digit:]]+$ ]]; then 66 | line= 67 | fi 68 | 69 | line=${line#"${line%%[![:space:]]*}"} 70 | input_byte_five=${line%% *} 71 | line=${line#* } 72 | 73 | if [[ $line =~ ^[[:digit:]]+$ ]]; then 74 | line= 75 | fi 76 | 77 | algo.base32_encode_impl 78 | done 79 | done < <(od -An -td1); unset -v line 80 | else 81 | core.err_set 'INVALID_ARGS' || return 82 | fi 83 | } 84 | 85 | # @description Decodes a base32 sequence of characters to a string 86 | # @arg $1 string input 87 | algo.base32_decode() { 88 | unset -v REPLY; REPLY= 89 | 90 | local flag="$1" 91 | if [ -z "$flag" ]; then 92 | core.err_set 'INVALID_ARGS' || return 93 | fi 94 | 95 | local input_byte_{one,two,three,four,five,six,seven,eight}= 96 | if [ "$flag" = '--use-arg' ]; then 97 | local arg="$2" 98 | if [ -z "$arg" ]; then 99 | core.err_set 'INVALID_ARGS' || return 100 | fi 101 | 102 | # shellcheck disable=SC1007 103 | local i= b= 104 | # shellcheck disable=SC2059 105 | for ((i=0; i<${#arg}; i=i+8)); do 106 | b="${arg:$i:1}"; printf -v input_byte_one "${b:+%c}" "$b" 107 | b="${arg:$i+1:1}"; printf -v input_byte_two "${b:+%c}" "$b" 108 | b="${arg:$i+2:1}"; printf -v input_byte_three "${b:+%c}" "$b" 109 | b="${arg:$i+3:1}"; printf -v input_byte_four "${b:+%c}" "$b" 110 | b="${arg:$i+4:1}"; printf -v input_byte_five "${b:+%c}" "$b" 111 | b="${arg:$i+5:1}"; printf -v input_byte_six "${b:+%c}" "$b" 112 | b="${arg:$i+6:1}"; printf -v input_byte_seven "${b:+%c}" "$b" 113 | b="${arg:$i+7:1}"; printf -v input_byte_eight "${b:+%c}" "$b" 114 | algo.base32_decode_impl 115 | done; unset -v i b 116 | elif [ "$flag" = '--use-stdin' ]; then 117 | while IFS=' ' read -r input_byte_{one,two,three,four,five,six,seven,eight}; do 118 | algo.base32_decode_impl 119 | done < <(od -An -td1) 120 | else 121 | core.err_set 'INVALID_ARGS' || return 122 | fi 123 | } 124 | 125 | # @description Encodes an arbitrary string to a base32 sequence of characters 126 | # @arg $1 string input 127 | algo.base64_encode() { 128 | unset -v REPLY; REPLY= 129 | 130 | local flag="$1" 131 | if [ -z "$flag" ]; then 132 | core.err_set 'INVALID_ARGS' || return 133 | fi 134 | 135 | local input_byte_{one,two,three}= 136 | if [ "$flag" = '--use-arg' ]; then 137 | local arg="$2" 138 | if [ -z "$arg" ]; then 139 | core.err_set 'INVALID_ARGS' || return 140 | fi 141 | 142 | # shellcheck disable=SC1007 143 | local i= b= 144 | # shellcheck disable=SC2059 145 | for ((i=0; i<${#arg}; i=i+3)); do 146 | b="${arg:$i:1}"; printf -v input_byte_one "${b:+%d}" "'$b" 147 | b="${arg:$i+1:1}"; printf -v input_byte_two "${b:+%d}" "'$b" 148 | b="${arg:$i+2:1}"; printf -v input_byte_three "${b:+%d}" "'$b" 149 | algo.base64_encode_impl 150 | done; unset -v i b 151 | elif [ "$flag" = '--use-stdin' ]; then 152 | while IFS=' ' read -r input_byte_{one,two,three}; do 153 | algo.base64_encode_impl 154 | done < <(od -An -td1) 155 | else 156 | core.err_set 'INVALID_ARGS' || return 157 | fi 158 | } 159 | 160 | # @description Decodes a base64 sequence of characters to a string 161 | # @arg $1 string input 162 | algo.base64_decode() { 163 | unset -v REPLY; REPLY= 164 | 165 | local flag="$1" 166 | if [ -z "$flag" ]; then 167 | core.err_set 'INVALID_ARGS' || return 168 | fi 169 | 170 | local input_byte_{one,two,three,four}= 171 | if [ "$flag" = '--use-arg' ]; then 172 | local arg="$2" 173 | if [ -z "$arg" ]; then 174 | core.err_set 'INVALID_ARGS' || return 175 | fi 176 | 177 | # shellcheck disable=SC1007 178 | local i= b= 179 | # shellcheck disable=SC2059 180 | for ((i=0; i<${#arg}; i=i+4)); do 181 | b="${arg:$i:1}"; printf -v input_byte_one "${b:+%c}" "$b" 182 | b="${arg:$i+1:1}"; printf -v input_byte_two "${b:+%c}" "$b" 183 | b="${arg:$i+2:1}"; printf -v input_byte_three "${b:+%c}" "$b" 184 | b="${arg:$i+3:1}"; printf -v input_byte_four "${b:+%c}" "$b" 185 | algo.base64_decode_impl 186 | done; unset -v i b 187 | elif [ "$flag" = '--use-stdin' ]; then 188 | while IFS=' ' read -r input_byte_{one,two,three,four}; do 189 | algo.base64_decode_impl 190 | done < <(od -An -td1) 191 | else 192 | core.err_set 'INVALID_ARGS' || return 193 | fi 194 | } 195 | 196 | # @description Encodes an arbitrary string to a base85 sequence of characters 197 | # @arg $1 string input 198 | # @internal 199 | algo.ascii85_encode() { 200 | unset -v REPLY; REPLY= 201 | local input="$1" 202 | 203 | local char_str='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-:+=^!/*?&<>()[]{}@%$#' 204 | 205 | local i= 206 | for ((i=0; i < ${#input}; i=i+1)); do 207 | : 208 | done 209 | } 210 | 211 | # @description Decodes a base85 sequence of characters to a string 212 | # @arg $1 string input 213 | # @internal 214 | algo.ascii85_decode() { 215 | unset -v REPLY; REPLY= 216 | local input="$1" 217 | } 218 | 219 | # @description Performs the md5 algorithm on an arbitrary string 220 | # @arg $1 string input 221 | # @internal 222 | algo.md5() { 223 | unset -v REPLY; REPLY= 224 | local input="$1" 225 | 226 | local m=${#input} 227 | 228 | local -a T=( 229 | $((0xD76AA476)) $((0xE8C7B756)) $((0x242070DB)) $((0xC1BDCEEE)) 230 | ) 231 | 232 | local A=$((0x01234567)) 233 | local B=$((0x89abcdef)) 234 | local C=$((0xfedcba98)) 235 | local D=$((0x76543210)) 236 | 237 | # F 238 | 239 | } 240 | -------------------------------------------------------------------------------- /pkg/src/public/pem.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | # @description PEM encode data 4 | # @arg $1 string PEM label 5 | # @arg $2 string PEM contents 6 | # @arg $3 string Output file 7 | algo.pem_encode() { 8 | local pem_label="$1" 9 | local pem_contents="$2" 10 | local file="$3" 11 | 12 | if printf -- '-----BEGIN %s-----\n' "$pem_label" > "$file"; then :; else 13 | printf '%s\n' "Error: Could not write to file (code $?)" >&2 14 | fi 15 | 16 | local line= 17 | while read -rn64 line; do 18 | if printf '%s\n' "$line" >> "$file"; then :; else 19 | printf '%s\n' "Error: Could not write to file (code $?)" >&2 20 | fi 21 | done <<< "$pem_contents" 22 | unset -v line 23 | 24 | if printf -- '-----END %s-----\n' "$pem_label" >> "$file"; then :; else 25 | printf '%s\n' "Error: Could not write to file (code $?)" >&2 26 | fi 27 | } 28 | 29 | # TODO: implement headers, etc. 30 | # @description PEM decode data 31 | # @arg $1 string input 32 | algo.pem_decode() { 33 | unset -v REPLY 34 | REPLY= 35 | local input="$1" 36 | 37 | # modes: MODE_INIT, MODE_LABEL, MODE_LABEL_AFTER 38 | local mode='MODE_INIT' 39 | local start_str_1='-----BEGIN ' 40 | local start_str_2=$'-----\n' 41 | local end_str_1='-----END ' 42 | local end_str_2='-----' 43 | 44 | local pem_label= 45 | local pem_contents= 46 | local chars= 47 | local char= 48 | while read -rN1 char; do 49 | chars+="$char" 50 | 51 | case "$mode" in 52 | MODE_INIT) 53 | if [ "$chars" = "$start_str_1" ]; then 54 | chars= 55 | mode='MODE_LABEL' 56 | fi 57 | ;; 58 | MODE_LABEL) 59 | if [ "$char" = '-' ]; then 60 | chars='-' 61 | mode='MODE_LABEL_AFTER' 62 | else 63 | pem_label+="$char" 64 | fi 65 | ;; 66 | MODE_LABEL_AFTER) 67 | if [ "$chars" = "$start_str_2" ]; then 68 | mode='MODE_CONTENT' 69 | fi 70 | ;; 71 | MODE_CONTENT) 72 | if [ "$char" = $'\n' ]; then 73 | mode='MODE_CONTENT_POSSIBLY_END' 74 | else 75 | pem_contents+="$char" 76 | fi 77 | ;; 78 | MODE_CONTENT_POSSIBLY_END) 79 | if [ "$char" = '-' ]; then 80 | chars='-' 81 | mode='MODE_CONTENT_END' 82 | else 83 | pem_contents+="$char" 84 | mode='MODE_CONTENT' 85 | fi 86 | ;; 87 | MODE_CONTENT_END) 88 | if [ "$chars" = "$end_str_1" ]; then 89 | # For now, assume the rest of the PEM is valid 90 | REPLY1="$pem_label" 91 | REPLY2="$pem_contents" 92 | return 93 | fi 94 | ;; 95 | esac 96 | done < <(printf '%s' "$1") 97 | unset -v char 98 | 99 | # TODO: bash-error 100 | case "$mode" in 101 | MODE_INIT) 102 | printf '%s\n' "Could not find start or end of BEGIN statement" 103 | ;; 104 | MODE_LABEL) 105 | printf '%s\n' "Could not find end of label" 106 | ;; 107 | MODE_LABEL_AFTER) 108 | printf '%s\n' "Could not find end of label end" 109 | ;; 110 | MODE_CONTENT) 111 | printf '%s\n' "Could not find end of PEM content" 112 | ;; 113 | MODE_CONTENT_POSSIBLY_END) 114 | printf '%s\n' "Could not find start of -----END" 115 | ;; 116 | esac >&2 117 | } 118 | -------------------------------------------------------------------------------- /pkg/src/public/uri.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | # @description URI encode a particular string 4 | # @arg $1 string input 5 | algo.uri_encode() { 6 | unset -v REPLY 7 | REPLY= 8 | local input="$1" 9 | 10 | # https://tc39.es/ecma262/multipage/global-object.html#sec-encodeuri-uri 11 | local uri_reserved=";/?:@&=+$," 12 | local uri_unescaped="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.!~*'()#" 13 | 14 | # shellcheck disable=SC1007 15 | local i= char= 16 | for ((i = 0; i < ${#input}; ++i)); do 17 | char="${input:$i:1}" 18 | 19 | case "${uri_reserved}${uri_unescaped}" in 20 | *"$char"*) 21 | REPLY+="$char" 22 | ;; 23 | *) 24 | printf -v REPLY '%s%%%02X' "$REPLY" "'$char" 25 | ;; 26 | esac 27 | done 28 | 29 | printf '%s\n' "$REPLY" 30 | } 31 | 32 | # @description URI decode a particular string 33 | # @arg $1 string input 34 | algo.uri_decode() { 35 | unset -v REPLY 36 | REPLY= 37 | local input="$1" 38 | 39 | local url_encoded="${input//+/ }" 40 | printf -v REPLY '%b' "${url_encoded//%/\\x}" 41 | } 42 | -------------------------------------------------------------------------------- /pkg/src/util/impl.sh: -------------------------------------------------------------------------------- 1 | # shellcheck shell=bash 2 | 3 | # @internal 4 | algo.base32_encode_impl() { 5 | local char_str='ABCDEFGHIJKLMNOPQRSTUVWXYZ234567' 6 | local bits_{one,two,three,four,five,six,seven,eight}= 7 | local output_byte_{one,two,three,four,five,six,seven,eight}= 8 | 9 | # Output byte one 10 | bits_one=$(( (input_byte_one >> 3) & 2#00011111 )) 11 | output_byte_one="${char_str:$bits_one:1}" 12 | 13 | # Output byte two 14 | bits_two=$(( ((input_byte_one & 2#00000111) << 2) | ((input_byte_two & 2#11000000) >> 6) )) 15 | output_byte_two="${char_str:$bits_two:1}" 16 | 17 | # Output byte three 18 | if ((input_byte_two == 0)); then 19 | output_byte_three='=' 20 | else 21 | bits_three=$(( (input_byte_two >> 1) & 2#00011111 )) 22 | output_byte_three="${char_str:$bits_three:1}" 23 | fi 24 | 25 | # Output byte four 26 | if ((input_byte_two == 0)); then 27 | output_byte_four='=' 28 | else 29 | bits_four=$(( ((input_byte_two & 2#00000001) << 4) | ((input_byte_three & 2#11110000) >> 4) )) 30 | output_byte_four="${char_str:$bits_four:1}" 31 | fi 32 | 33 | # Output byte five 34 | if ((input_byte_three == 0)); then 35 | output_byte_five='=' 36 | else 37 | bits_five=$(( ((input_byte_three & 2#00001111) << 1) | ((input_byte_four & 2#10000000) >> 7) )) 38 | output_byte_five="${char_str:$bits_five:1}" 39 | fi 40 | 41 | # Output byte six 42 | if ((input_byte_four == 0)); then 43 | output_byte_six='=' 44 | else 45 | bits_six=$(( (input_byte_four >> 2) & 2#00011111 )) 46 | output_byte_six="${char_str:$bits_six:1}" 47 | fi 48 | 49 | # Output byte seven 50 | if ((input_byte_four == 0)); then 51 | output_byte_seven='=' 52 | else 53 | bits_seven=$(( (input_byte_four & 2#00000011) << 3 | ((input_byte_five & 2#11100000) >> 5) )) 54 | output_byte_seven="${char_str:$bits_seven:1}" 55 | fi 56 | 57 | # Output byte eight 58 | if ((input_byte_five == 0)); then 59 | output_byte_eight='=' 60 | else 61 | bits_eight=$(( input_byte_five & 2#00011111 )) 62 | output_byte_eight="${char_str:$bits_eight:1}" 63 | fi 64 | 65 | REPLY+="${output_byte_one}${output_byte_two}${output_byte_three}${output_byte_four}${output_byte_five}${output_byte_six}${output_byte_seven}${output_byte_eight}" 66 | } 67 | 68 | # @internal 69 | algo.base32_decode_impl() { 70 | local char_str='ABCDEFGHIJKLMNOPQRSTUVWXYZ234567' 71 | local index_{one,two,three,four,five,six,seven,eight}= 72 | local bits_{one,two,three,four,five}= 73 | local output_byte_{one,two,three,four,five}= 74 | 75 | # Equivalent to char_str.indexOf(input) 76 | index_one="${char_str%"$input_byte_one"*}"; index_one=${#index_one} 77 | index_two="${char_str%"$input_byte_two"*}"; index_two=${#index_two} 78 | index_three="${char_str%"$input_byte_three"*}"; index_three=${#index_three} 79 | index_four="${char_str%"$input_byte_four"*}"; index_four=${#index_four} 80 | index_five="${char_str%"$input_byte_five"*}"; index_five=${#index_five} 81 | index_six="${char_str%"$input_byte_six"*}"; index_six=${#index_six} 82 | index_seven="${char_str%"$input_byte_seven"*}"; index_seven=${#index_seven} 83 | index_eight="${char_str%"$input_byte_eight"*}"; index_eight=${#index_eight} 84 | 85 | # Output byte one 86 | bits_one=$(( ((index_one & 2#00011111) << 3) | ((index_two >> 2) & 2#00000111) )) 87 | printf -v output_byte_one '%03o' "$bits_one" 88 | printf -v output_byte_one "\\$output_byte_one" 89 | 90 | # TODO `if ((index_two === 32)) ...` required? 91 | # An index of '32' means char_str.`indexOf(input)` could not find the substring 92 | # i.e. `-1` in traditional languages. This occurs when an `=` is found 93 | 94 | # Output byte two 95 | bits_two=$(( ((index_two & 2#00000011) << 6) | ((index_three & 2#00011111) << 1) | ((index_four & 2#00010000) >> 4) )) 96 | printf -v output_byte_two '%03o' "$bits_two" 97 | printf -v output_byte_two "\\$output_byte_two" 98 | 99 | # Output byte three 100 | bits_three=$(( ((index_four & 2#00001111) << 4) | ((index_five & 2#00011110) >> 1) )) 101 | printf -v output_byte_three '%03o' "$bits_three" 102 | printf -v output_byte_three "\\$output_byte_three" 103 | 104 | # Output byte four 105 | bits_four=$(( ((index_five & 2#00000001) << 7) | ((index_six & 2#00011111) << 2) | ((index_seven & 2#00011000) >> 3) )) 106 | printf -v output_byte_four '%03o' "$bits_four" 107 | printf -v output_byte_four "\\$output_byte_four" 108 | 109 | # Output byte five 110 | bits_five=$(( ((index_seven & 2#00000111) << 5) | (index_eight & 2#00011111) )) 111 | printf -v output_byte_five '%03o' "$bits_five" 112 | printf -v output_byte_five "\\$output_byte_five" 113 | 114 | REPLY+="${output_byte_one}${output_byte_two}${output_byte_three}${output_byte_four}${output_byte_five}" 115 | } 116 | 117 | # @internal 118 | algo.base64_encode_impl() { 119 | local char_str='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' 120 | local bits_{one,two,three,four}= 121 | local output_byte_{one,two,three,four}= 122 | 123 | # Output byte one 124 | bits_one=$(( (input_byte_one >> 2) & 2#00111111 )) 125 | output_byte_one="${char_str:$bits_one:1}" 126 | 127 | # Output byte two 128 | bits_two=$(( ((input_byte_one & 2#00000011) << 4) | ((input_byte_two & 2#11110000) >> 4 & 2#00001111) )) 129 | output_byte_two="${char_str:$bits_two:1}" 130 | 131 | # Output byte three 132 | if ((input_byte_two == 0)); then # TODO: required? 133 | output_byte_three='=' 134 | else 135 | bits_three=$(( (input_byte_two & 2#00001111) << 2 | input_byte_three >> 6 & 2#00000011 )) 136 | output_byte_three="${char_str:$bits_three:1}" 137 | fi 138 | 139 | # Output byte four 140 | if ((input_byte_three == 0)); then 141 | output_byte_four='=' 142 | else 143 | bits_four=$(( input_byte_three & 2#00111111 )) 144 | output_byte_four="${char_str:$bits_four:1}" 145 | fi 146 | 147 | REPLY+="${output_byte_one}${output_byte_two}${output_byte_three}${output_byte_four}" 148 | } 149 | 150 | # @internal 151 | algo.base64_decode_impl() { 152 | local char_str='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' 153 | local index_{one,two,three,four}= 154 | local bits_{one,two,three}= 155 | local output_byte_{one,two,three}= 156 | 157 | # Equivalent to char_str.indexOf(input) 158 | index_one="${char_str%"$input_byte_one"*}"; index_one=${#index_one} 159 | index_two="${char_str%"$input_byte_two"*}"; index_two=${#index_two} 160 | index_three="${char_str%"$input_byte_three"*}"; index_three=${#index_three} 161 | index_four="${char_str%"$input_byte_four"*}"; index_four=${#index_four} 162 | 163 | # Output byte one 164 | bits_one=$(( ((index_one & 2#00111111) << 2) | ((index_two >> 4) & 2#00000011) )) 165 | printf -v output_byte_one '%03o' "$bits_one" 166 | printf -v output_byte_one "\\$output_byte_one" 167 | 168 | # An index of '64' means char_str.`indexOf(input)` could not find the substring 169 | # i.e. `-1` in traditional languages. This occurs when an `=` is found 170 | 171 | # Output byte two 172 | if ((index_three == 64)); then 173 | output_byte_two= 174 | else 175 | bits_two=$(( ((index_two & 2#00001111) << 4) | ((index_three >> 2) & 2#00001111) )) 176 | printf -v output_byte_two '%03o' "$bits_two" 177 | printf -v output_byte_two "\\$output_byte_two" 178 | fi 179 | 180 | # Output byte three 181 | if ((index_four == 64)); then 182 | output_byte_three= 183 | else 184 | bits_three=$(( ((index_three & 2#00000011) << 6) | (index_four & 2#00111111) )) 185 | printf -v output_byte_three '%03o' "$bits_three" 186 | printf -v output_byte_three "\\$output_byte_three" 187 | fi 188 | 189 | REPLY+="${output_byte_one}${output_byte_two}${output_byte_three}" 190 | } 191 | -------------------------------------------------------------------------------- /tests/algo.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | load './util/init.sh' 4 | 5 | @test "base32_encode" { 6 | algo.base32_encode --use-arg 'guatemala-hondorus' 7 | assert [ "$REPLY" = 'M52WC5DFNVQWYYJNNBXW4ZDPOJ2XG===' ] 8 | 9 | test_util.base32_encode --use-arg 'A' 10 | test_util.base32_encode --use-arg 'AB' 11 | test_util.base32_encode --use-arg 'ABC' 12 | test_util.base32_encode --use-arg 'ABCD' 13 | test_util.base32_encode --use-arg 'ABCDE' 14 | test_util.base32_encode --use-arg 'WOOF' 15 | test_util.base32_encode --use-arg 'kafka38quebec' 16 | test_util.base32_encode --use-arg 'EcHo##8(0}}' 17 | test_util.base32_encode --use-stdin <(printf '%s' 'EcHo##8(0}}') < <(printf '%s' 'EcHo##8(0}}') 18 | } 19 | 20 | @test "base32_decode" { 21 | algo.base32_decode --use-arg 'M52WC5DFNVQWYYJNNBXW4ZDPOJ2XG===' 22 | assert [ "$REPLY" = 'guatemala-hondorus' ] 23 | 24 | test_util.base32_decode --use-arg 'IE======' 25 | test_util.base32_decode --use-arg 'IFBA====' 26 | test_util.base32_decode --use-arg 'IFBEG===' 27 | test_util.base32_decode --use-arg 'IFBEGRA=' 28 | test_util.base32_decode --use-arg 'IFBEGRCF' 29 | test_util.base32_decode --use-arg 'K5HU6RQ=' 30 | test_util.base32_decode --use-arg 'NNQWM23BGM4HC5LFMJSWG===' 31 | test_util.base32_decode --use-arg 'IVRUQ3ZDEM4CQMD5PU======' 32 | } 33 | 34 | @test "base64_encode" { 35 | algo.base64_encode --use-arg 'paraguay-uruguay' 36 | assert [ "$REPLY" = 'cGFyYWd1YXktdXJ1Z3VheQ==' ] 37 | 38 | test_util.base64_encode --use-arg 'A' 39 | test_util.base64_encode --use-arg 'AB' 40 | test_util.base64_encode --use-arg 'ABC' 41 | test_util.base64_encode --use-arg 'ABCD' 42 | test_util.base64_encode --use-arg 'WOOF' 43 | test_util.base64_encode --use-arg 'kafka38quebec' 44 | test_util.base64_encode --use-arg 'EcHo##8(0}}' 45 | } 46 | 47 | @test "base64_decode" { 48 | algo.base64_decode --use-arg 'cGFyYWd1YXktdXJ1Z3VheQ==' 49 | assert [ "$REPLY" = 'paraguay-uruguay' ] 50 | 51 | test_util.base64_decode --use-arg 'QQ==' 52 | test_util.base64_decode --use-arg 'QUI=' 53 | test_util.base64_decode --use-arg 'QUJD' 54 | test_util.base64_decode --use-arg 'QUJDRA==' 55 | test_util.base64_decode --use-arg 'V09PRg==' 56 | test_util.base64_decode --use-arg 'a2Fma2EzOHF1ZWJlYw==' 57 | test_util.base64_decode --use-arg 'RWNIbyMjOCgwfX0=' 58 | 59 | test_util.base64_decode --use-arg 'Z3VhdGVtYWxhLWhvbmRvcnVz' 60 | test_util.base64_decode --use-arg 'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=' 61 | test_util.base64_decode --use-arg 'YXNsZGZramFzbDtka2ZqYTtzbGRrZmphO2xzZGtqZmE7bGtkc2pmO2xha2pkZnFvcHdlcmlydWpwb2Zpc2R1amxranZoeGtjbGp2YnNmZGtsamhhYmxlcnVocW9wZWlydWhmZG9pdXNqYmh2Y2tsc2poYmRmaW9wcXdlcnVkaGZxd29waWVkZnVoYWxpc2tkamZnYmhpd3FvdWVnaHJ0b3FpdWVyaHBmZHNpb2F1aHBxb2FpdWRoZnNrbGpmaGFsc2tkamZoYXEyMzA5LXU4d3BvZWk7IGFzZGxmamhxaXV3b2pFSA==' 62 | } 63 | -------------------------------------------------------------------------------- /tests/pem.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | load './util/init.sh' 4 | 5 | declare -gr content='MIICLDCCAdKgAwIBAgIBADAKBggqhkjOPQQDAjB9MQswCQYDVQQGEwJCRTEPMA0GA1UEChMGR251VExTMSUwIwYDVQQLExxHbnVUTFMgY2VydGlmaWNhdGUgYXV0aG9yaXR5MQ8wDQYDVQQIEwZMZXV2ZW4xJTAjBgNVBAMTHEdudVRMUyBjZXJ0aWZpY2F0ZSBhdXRob3JpdHkwHhcNMTEwNTIzMjAzODIxWhcNMTIxMjIyMDc0MTUxWjB9MQswCQYDVQQGEwJCRTEPMA0GA1UEChMGR251VExTMSUwIwYDVQQLExxHbnVUTFMgY2VydGlmaWNhdGUgYXV0aG9yaXR5MQ8wDQYDVQQIEwZMZXV2ZW4xJTAjBgNVBAMTHEdudVRMUyBjZXJ0aWZpY2F0ZSBhdXRob3JpdHkwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARS2I0jiuNn14Y2sSALCX3IybqiIJUvxUpj+oNfzngvj/Niyv2394BWnW4XuQ4RTEiywK87WRcWMGgJB5kX/t2no0MwQTAPBgNVHRMBAf8EBTADAQH/MA8GA1UdDwEB/wQFAwMHBgAwHQYDVR0OBBYEFPC0gf6YEr+1KLlkQAPLzB9mTigDMAoGCCqGSM49BAMCA0gAMEUCIDGuwD1KPyG+hRf88MeyMQcqOFZD0TbVleF+UsAGQ4enAiEAl4wOuDwKQa+upc8GftXE2C//4mKANBC6It01gUaTIpo=' 6 | 7 | @test "simple decode works" { 8 | skip 9 | 10 | algo.pem_decode "$(<"$BATS_TEST_DIRNAME/mocks/example.pem")" 11 | assert [ "$REPLY1" = 'CERTIFICATE' ] 12 | assert [ "$REPLY2" = "$content" ] 13 | } 14 | 15 | @test "endocde and decode sequence works" { 16 | algo.pem_encode 'CERTIFICATE' "$content" 'file.pem' 17 | 18 | algo.pem_decode "$(