#!/bin/sh
-# Copyright 2018 Paul Hänsch
+# Copyright 2018, 2019, 2021 Paul Hänsch
#
# This is a file format helper, part of CGIlite.
#
# You should have received a copy of the GNU Affero General Public License
# along with CGIlite. If not, see <http://www.gnu.org/licenses/>.
-# ksh and zsh workaround
-case "${0##*/}" in
- zsh) setopt -o OCTAL_ZEROES 2>&- ;; # zsh, mostly ignored by other shells
- ksh) set -o posix ;; # ksh, will crash most other shells
-esac
+[ -n "$include_storage" ] && return 0
+include_storage="$0"
-STORE(){
- # Store fields from a HTTP GET or POST string in a file
- # usage: STORE "${QUERY_STRING}"
- # or: STORE "$(head -n $HTTP_CONTENT_LENGTH)"
- # backslashes and newline characters will be escaped in field values,
- # the escape symbol is a backslash
- # hexadecimal character descriptions (%00 - %FF) will be expanded
- # the + character will be converted to [space]
- # one line in the output corresponds to exactly one field, so you can
- # use grep to filter which fields of a query should be stored
- printf "$(
- printf '%s' "$@" \
- | sed -r ':X; $bY; N; bX; :Y;
- s;\+; ;g; s;(\\|5[Cc]);\\\\\\\\;g; s;(\n|%0[Aa]);\\\\n;g; s;(^|&)([^=]+)=;\1\2:;g; s;&;\n;g;
- s;^\n+;;; s;$;\\n;; s;\n+;\n;g;
- # Hexadecimal { %00 - %FF } will be transformed to octal { \000 - \377 } for posix printf
- s;%[0123].;&\\0;g; s;%[4567].;&\\1;g; s;%[89AB].;&\\2;g; s;%[CDEF].;&\\3;g;
- s;%[048C][0-7]\\.;&0;g; s;%[048C][89A-F]\\.;&1;g; s;%[159D][0-7]\\.;&2;g; s;%[159D][89A-F]\\.;&3;g;
- s;%[26AE][0-7]\\.;&4;g; s;%[26AE][89A-F]\\.;&5;g; s;%[37BF][0-7]\\.;&6;g; s;%[37BF][89A-F]\\.;&7;g;
- s;%.[08](\\..);\10;g; s;%.[19](\\..);\11;g; s;%.[2A](\\..);\12;g; s;%.[3B](\\..);\13;g;
- s;%.[4C](\\..);\14;g; s;%.[5D](\\..);\15;g; s;%.[6E](\\..);\16;g; s;%.[7F](\\..);\17;g;
- '
- )"
+CR="\r"
+BR='
+'
+
+LOCK(){
+ local lock="${1}.lock" timeout="${2-20}" block
+
+ if [ \! -w "${lock%/*}" ] || [ -e "$lock" -a \! -f "$lock" ]; then
+ debug "Impossible to get lock: $lock"
+ return 1
+ fi
+
+ while [ $timeout -gt 0 ]; do
+ printf '%i\n' $$ >>"${lock}"
+ read block <"$lock"
+ if [ "$block" = $$ ]; then
+ return 0
+ elif ! { ps -eo pid |grep -qwF "$block"; }; then
+ debug "Trying to override stale lock: $lock"
+ if LOCK "$lock" 1; then
+ rm -- "$lock"
+ RELEASE "$lock"
+ fi
+ else
+ timeout=$((timeout - 1))
+ [ $timeout -gt 0 ] && sleep 1
+ fi
+ done
+
+ debug "Timeout while trying to get lock: $lock"
+ return 1
+}
+
+RELEASE(){
+ local lock="${1}.lock" block
+
+ read block <"$lock"
+ if [ "$block" = $$ ]; then
+ rm -- "$lock"
+ return 0
+ else
+ debug "Refusing to release foreign lock: $lock"
+ return 1
+ fi
+}
+
+STRING(){
+ local in out=''
+ [ $# -gt 0 ] && in="$*" || in="$(cat)"
+ while [ "$in" ]; do case $in in
+ \\*) out="${out}\\\\"; in="${in#\\}" ;;
+ "$BR"*) out="${out}\\n"; in="${in#${BR}}" ;;
+ "$CR"*) out="${out}\\r"; in="${in#${CR}}" ;;
+ " "*) out="${out}\\t"; in="${in# }" ;;
+ +*) out="${out}\\+"; in="${in#+}" ;;
+ " "*) out="${out}+"; in="${in# }" ;;
+ *) out="${out}${in%%[\\${CR}${BR} + ]*}"; in="${in#"${in%%[\\${BR}${CR} + ]*}"}" ;;
+ esac; done
+ printf '%s' "${out:-\\}"
+}
+
+UNSTRING(){
+ local in out=''
+ [ $# -gt 0 ] && in="$*" || in="$(cat)"
+ while [ "$in" ]; do case $in in
+ \\\\*) out="${out}\\"; in="${in#\\\\}" ;;
+ \\n*) out="${out}${BR}"; in="${in#\\n}" ;;
+ \\r*) out="${out}${CR}"; in="${in#\\r}" ;;
+ \\t*) out="${out} "; in="${in#\\t}" ;;
+ \\+*) out="${out}+"; in="${in#\\+}" ;;
+ +*) out="${out} "; in="${in#+}" ;;
+ \\*) in="${in#\\}" ;;
+ *) out="${out}${in%%[\\+]*}"; in="${in#"${in%%[\\+]*}"}" ;;
+ esac; done
+ printf '%s\n' "$out"
}
-LOAD(){
- # read a file written by STORE and assign all fields to variables
- # usage: eval "$(LOAD <file)"
- #
- # fields will be assigned to shell variables of the same name, but with the following constraints:
- # * small letters will be converted to capitals
- # * all non-alphanumeric characters will be converted to underscore (_)
- # * an underscore will be prepended
- # as a consequence variable names are guaranteed to match the regex /_[A-Z0-9_]+/
- #
- # field values may contain any character, proper escaping for safe use in eval is taken care of
- # Your shell may or may not purge certain binary characters from variable values during processing
+DBM() {
+ local file="$1" cmd="$2"
+ local k v key value
+ shift 2;
- sed -r 'h; s;^[^:]+:;;
- s;'\'';'\''\\'\'\'';g;
- s;^.*$;'\''&'\'';;
- s;\\n;\n;g;
- x; s;^([^:]+):.*$;\1;
- y;abcdefghijklmnopqrstuvwxyz;ABCDEFGHIJKLMNOPQRSTUVWXYZ;
- s;[^A-Z0-9_];_;g; s;^.*$;_&=;;
- G; s;^([A-Z0-9_]+=)\n;\1;;
- '
+ case "$cmd" in
+ check|contains)
+ key="$(STRING "$1")"
+ while read -r k v; do if [ "$k" = "$key" ]; then
+ return 0
+ fi; done <"$file" 2>&-
+ return 1
+ ;;
+ get)
+ key="$(STRING "$1")"
+ while read -r k v; do if [ "$k" = "$key" ]; then
+ UNSTRING "$v"
+ return 0
+ fi; done <"$file" 2>&-
+ return 1
+ ;;
+ set|store)
+ key="$(STRING "$1")" value="$(STRING "$2")"
+ LOCK "$file" || return 1
+ { while read -r k v; do
+ [ "$k" = "$key" ] || printf '%s\t%s\n' "$k" "$v"
+ done <"$file" 2>&-
+ printf '%s\t%s\n' "$key" "$value"
+ } >"${file}.$$.tmp"
+ mv "${file}.$$.tmp" "${file}"
+ RELEASE "$file"
+ return 0
+ ;;
+ add|insert)
+ k="$1" key="$(STRING "$1")" value="$(STRING "$2")"
+ LOCK "$file" || return 1
+ if DBM "$file" check "$k"; then
+ RELEASE "$file"
+ return 1
+ else
+ printf '%s\t%s\n' "$key" "$value" >>"${file}"
+ RELEASE "$file"
+ return 0
+ fi
+ ;;
+ update|replace)
+ k="$1" key="$(STRING "$1")" value="$(STRING "$2")"
+ LOCK "$file" || return 1
+ if ! DBM "$file" check "$k"; then
+ RELEASE "$file"
+ return 1
+ fi
+ { while read -r k v; do
+ [ "$k" = "$key" ] \
+ && printf '%s\t%s\n' "$key" "$value" \
+ || printf '%s\t%s\n' "$k" "$v"
+ done <"$file" 2>&-
+ } >"${file}.$$.tmp"
+ mv "${file}.$$.tmp" "${file}"
+ RELEASE "$file"
+ return 0
+ ;;
+ append)
+ key="$(STRING "$1")" value="$(STRING "$2")"
+ LOCK "$file" || return 1
+ if ! DBM "$file" check "$1"; then
+ RELEASE "$file"
+ return 1
+ fi
+ { while read -r k v; do
+ [ "$k" = "$key" ] \
+ && printf '%s\t%s\n' "$key" "$v$value" \
+ || printf '%s\t%s\n' "$k" "$v"
+ done <"$file" 2>&-
+ } >"${file}.$$.tmp"
+ mv "${file}.$$.tmp" "${file}"
+ RELEASE "$file"
+ return 0
+ ;;
+ delete|remove)
+ key="$(STRING "$1")"
+ LOCK "$file" || return 1
+ { while read -r k v; do
+ [ "$k" = "$key" ] || printf '%s\t%s\n' "$k" "$v"
+ done <"$file" 2>&-
+ } >"${file}.$$.tmp"
+ mv "${file}.$$.tmp" "${file}"
+ RELEASE "$file"
+ return 0
+ ;;
+ esac
}