diff --git a/CHANGELOG.md b/CHANGELOG.md index ef27c77..e6e2927 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ system, you must also run the `--upgrade` command in each repository: ### Fixed - Prevent `cd` commands printing out excess details when `CDPATH` is set (#156) +- Fix handling of double-quotes in encrypted file names (#173) ### Changed diff --git a/tests/test_contexts.bats b/tests/test_contexts.bats index f0f9de7..589faa9 100755 --- a/tests/test_contexts.bats +++ b/tests/test_contexts.bats @@ -76,7 +76,7 @@ function teardown { [[ $(git config --get diff.crypt.binary) = "true" ]] [[ $(git config --get merge.renormalize) = "true" ]] - [[ "$(git config --get alias.ls-crypt)" = "!git -c core.quotePath=false ls-files | git -c core.quotePath=false check-attr --stdin filter | awk 'BEGIN { FS = \":\" }; / crypt/{ print \$1 }'" ]] + [[ "$(git config --get alias.ls-crypt)" = '!"$(git config transcrypt.crypt-dir 2>/dev/null || printf %s/crypt ""$(git rev-parse --git-dir)"")"/transcrypt --list' ]] } @test "init: show extra context details in --display" { @@ -216,7 +216,7 @@ function teardown { [ "${lines[1]}" = "$SUPER_SECRET_CONTENT_ENC" ] } -@test "contexts: git ls-crypt lists encrypted file for all contexts" { +@test "contexts: git ls-crypt lists encrypted files for all contexts" { encrypt_named_file sensitive_file "$SECRET_CONTENT" encrypt_named_file super_sensitive_file "$SECRET_CONTENT" "super-secret" @@ -226,14 +226,14 @@ function teardown { [ "${lines[1]}" = "super_sensitive_file" ] } -@test "contexts: git ls-crypt-default lists encrypted file for only 'default' context" { +@test "contexts: git ls-crypt-default lists encrypted files for all contexts" { encrypt_named_file sensitive_file "$SECRET_CONTENT" encrypt_named_file super_sensitive_file "$SECRET_CONTENT" "super-secret" run git ls-crypt-default [ "$status" -eq 0 ] [ "${lines[0]}" = "sensitive_file" ] - [ "${lines[1]}" = "" ] + [ "${lines[1]}" = "super_sensitive_file" ] } @test "contexts: git ls-crypt-super-secret lists encrypted file for only 'super-secret' context" { diff --git a/tests/test_init.bats b/tests/test_init.bats index cbae5b2..54640e6 100755 --- a/tests/test_init.bats +++ b/tests/test_init.bats @@ -49,7 +49,7 @@ SETUP_SKIP_INIT_TRANSCRYPT=1 [ "$(git config --get merge.renormalize)" = "true" ] [ "$(git config --get merge.crypt.name)" = "Merge transcrypt secret files" ] - [ "$(git config --get alias.ls-crypt)" = "!git -c core.quotePath=false ls-files | git -c core.quotePath=false check-attr --stdin filter | awk 'BEGIN { FS = \":\" }; / crypt/{ print \$1 }'" ] + [ "$(git config --get alias.ls-crypt)" = '!"$(git config transcrypt.crypt-dir 2>/dev/null || printf %s/crypt ""$(git rev-parse --git-dir)"")"/transcrypt --list' ] } @test "init: show details for --display" { diff --git a/transcrypt b/transcrypt index 92aae3f..4ff9863 100755 --- a/transcrypt +++ b/transcrypt @@ -161,6 +161,34 @@ derive_context_config_group() { fi } +# Internal function that returns a list of filenames for encrypted files in the +# repo, where the filenames are verbatim and not quoted in any way even if they +# contain unusual characters like double-quotes, backslash and control +# characters. We must avoid quoting of filenames to support names containing +# double quotes. #173 +_list_encrypted_files() { + local strict_context=${1:-} + + IFS=$'\n' + # List files with -z option to disable quoting of filenames, then + # immediately convert NUL-delimited filenames to be newline-delimited to be + # compatibility with bash variables + for file in $(git ls-files -z | tr '\0' '\n'); do + # Check for the suffix ': filter: crypt' that identifies encrypted file + local check + check=$(git check-attr filter "$file" 2>/dev/null) + + # Only output names of encrypted files matching the context, either + # strictly (if $1 = "true") or loosely (if $1 is false or unset) + if [[ "$strict_context" == "true" ]] && + [[ "$check" == *": filter: crypt${CONTEXT_CRYPT_SUFFIX:-}" ]]; then + echo "$file" + elif [[ "$check" == *": filter: crypt${CONTEXT_CRYPT_SUFFIX:-}"* ]]; then + echo "$file" + fi + done +} + # Detect OpenSSL major version 3 or later which requires a compatibility # work-around to include the prefix 'Salted__' and salt value when encrypting. # @@ -305,7 +333,7 @@ git_pre_commit() { tmp=$(mktemp) IFS=$'\n' slow_mode_if_failed() { - for secret_file in $(git -c core.quotePath=false ls-files | git -c core.quotePath=false check-attr --stdin filter | awk 'BEGIN { FS = ":" }; /crypt$/{ print $1 }'); do + for secret_file in $(_list_encrypted_files); do # Skip symlinks, they contain the linked target file path not plaintext if [[ -L $secret_file ]]; then continue @@ -353,7 +381,7 @@ git_pre_commit() { if [[ "${BASH_VERSINFO[0]}" -ge 4 ]] && [[ "${BASH_VERSINFO[1]}" -ge 4 ]]; then num_procs=$(nproc) num_jobs="\j" - for secret_file in $(git -c core.quotePath=false ls-files | git -c core.quotePath=false check-attr --stdin filter | awk 'BEGIN { FS = ":" }; /crypt$/{ print $1 }'); do + for secret_file in $(_list_encrypted_files); do while ((${num_jobs@P} >= num_procs)); do wait -n done @@ -659,15 +687,15 @@ save_configuration() { git config merge.crypt.name 'Merge transcrypt secret files' # add git alias for listing ALL encrypted files regardless of context - git config alias.ls-crypt "!git -c core.quotePath=false ls-files | git -c core.quotePath=false check-attr --stdin filter | awk 'BEGIN { FS = \":\" }; / crypt/{ print \$1 }'" + git config alias.ls-crypt "!$transcrypt_path --list" # add a git alias for listing encrypted files in specific context, including 'default' if [[ "$CONTEXT" = 'default' ]]; then # List files with gitattribute 'filter=crypt' - git config alias.ls-crypt-default "!git -c core.quotePath=false ls-files | git -c core.quotePath=false check-attr --stdin filter | awk 'BEGIN { FS = \":\" }; / crypt$/{ print \$1 }'" + git config alias.ls-crypt-default "!$transcrypt_path --list" else # List files with gitattribute 'filter=crypt-' - git config "alias.ls-crypt-${CONTEXT}" "!git -c core.quotePath=false ls-files | git -c core.quotePath=false check-attr --stdin filter | awk 'BEGIN { FS = \":\" }; / crypt-${CONTEXT}$/{ print \$1 }'" + git config "alias.ls-crypt-${CONTEXT}" "!$transcrypt_path --context=${CONTEXT} --list" fi } @@ -810,6 +838,15 @@ uninstall_transcrypt() { remove_cached_plaintext fi + # touch all encrypted files to prevent stale stat info + local encrypted_files + encrypted_files=$(git ls-crypt) + if [[ $encrypted_files ]] && [[ $IS_BARE == 'false' ]]; then + cd "$REPO" >/dev/null || die 1 'could not change into the "%s" directory' "$REPO" + # shellcheck disable=SC2086 + touch $encrypted_files + fi + # remove helper scripts # Keep obsolete clean,smudge,textconv,merge refs here to remove them on upgrade for script in {transcrypt,clean,smudge,textconv,merge}; do @@ -831,15 +868,6 @@ uninstall_transcrypt() { fi [[ -f "$pre_commit_hook_installed" ]] && rm "$pre_commit_hook_installed" - # touch all encrypted files to prevent stale stat info - local encrypted_files - encrypted_files=$(git ls-crypt) - if [[ $encrypted_files ]] && [[ $IS_BARE == 'false' ]]; then - cd "$REPO" >/dev/null || die 1 'could not change into the "%s" directory' "$REPO" - # shellcheck disable=SC2086 - touch $encrypted_files - fi - # remove context settings: cipher & password config, ls-crypt alias variant, # crypt filter/diff/merge attributes. We do it here instead of `clean_gitconfig` # to avoid interfering with flushing of credentials @@ -964,7 +992,7 @@ upgrade_transcrypt() { list_files() { if [[ $IS_BARE == 'false' ]]; then cd "$REPO" >/dev/null || die 1 'could not change into the "%s" directory' "$REPO" - git -c core.quotePath=false ls-files | git -c core.quotePath=false check-attr --stdin filter | awk 'BEGIN { FS = ":" }; /crypt/{ print $1 }' + _list_encrypted_files true fi } @@ -973,13 +1001,12 @@ show_raw_file() { if [[ -f $show_file ]]; then # ensure the file is currently being tracked local escaped_file=${show_file//\//\\\/} - if git -c core.quotePath=false ls-files --others -- "$show_file" | awk "/${escaped_file}/{ exit 1 }"; then - file_paths=$(git -c core.quotePath=false ls-tree --name-only --full-name HEAD "$show_file") - else + file_paths=$(_list_encrypted_files | grep "$escaped_file") + if [[ -z "$file_paths" ]]; then die 1 'the file "%s" is not currently being tracked by git' "$show_file" fi elif [[ $show_file == '*' ]]; then - file_paths=$(git ls-crypt) + file_paths=$(_list_encrypted_files) else die 1 'the file "%s" does not exist' "$show_file" fi