From 50198139929bcc011f5d4d7386a9945f2774ef18 Mon Sep 17 00:00:00 2001 From: James Murty Date: Thu, 18 Sep 2025 02:02:06 +1000 Subject: [PATCH 1/6] Fix listing of file names with special characters to avoid crash and unrecognised names #204 Tweak the file listing to fix two issues found to affect files with problematic names like `"Terrible file""")))(((][][].secret`. This change may undo performance improvements achieved in #193. - Ensure the `git check-attr filter` command does not quote file name characters by using `-z` arg and some extra trickery to keep space and newline delimiter characters for filter metadata and each file name respectively. This fixes a file named `"Terrible file""")))(((][][].secret` being listed as just `Terrible file`. - Adjust checking for `filter` and `crypt` strings to work with changed output from `check-attr` command. - Tweak `eval 'echo $filename'` portion of command to avoid script crash on files with special characters with error message like `eval: line 192: syntax error near unexpected token` --- transcrypt | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/transcrypt b/transcrypt index 8280058..5b4d398 100755 --- a/transcrypt +++ b/transcrypt @@ -178,18 +178,26 @@ _list_encrypted_files() { # regardless of core.quotePath=false as per # https://git-scm.com/docs/git-config#Documentation/git-config.txt-corequotePath git -c core.quotePath=false ls-files -z | tr '\0' '\n' | - git -c core.quotePath=false check-attr filter --stdin 2>/dev/null | + # Read unquoted filenames one at a time per newline delimiters and use + # check-attr to print filter info, with some trickery to keep filename + # unquoted (-z) with space-delimited filter status and anewline + # delimiting of each file name + while read -r filename + do + git -c core.quotePath=false check-attr filter -z 2>/dev/null -- "$filename" | tr '\0' ' ' + echo + done | { # 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" ]]; then - grep ": filter: crypt${CONTEXT_CRYPT_SUFFIX:-}$" || true + grep " filter crypt${CONTEXT_CRYPT_SUFFIX:-}$" || true else - grep ": filter: crypt${CONTEXT_CRYPT_SUFFIX:-}.*$" || true + grep " filter crypt${CONTEXT_CRYPT_SUFFIX:-}.*$" || true fi } | - sed "s|: filter: crypt${CONTEXT_CRYPT_SUFFIX:-}.*||" | - while read -r file; do eval "echo $file"; done + sed "s| filter crypt${CONTEXT_CRYPT_SUFFIX:-}.*||" | + while read -r filename; do eval 'echo $filename'; done } # Detect OpenSSL major version 3 or later which requires a compatibility From e116a1b424ecdaf0c3b4455f275351249385118d Mon Sep 17 00:00:00 2001 From: James Murty Date: Thu, 18 Sep 2025 02:06:04 +1000 Subject: [PATCH 2/6] Lint fixes --- transcrypt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/transcrypt b/transcrypt index 5b4d398..cd69874 100755 --- a/transcrypt +++ b/transcrypt @@ -182,9 +182,8 @@ _list_encrypted_files() { # check-attr to print filter info, with some trickery to keep filename # unquoted (-z) with space-delimited filter status and anewline # delimiting of each file name - while read -r filename - do - git -c core.quotePath=false check-attr filter -z 2>/dev/null -- "$filename" | tr '\0' ' ' + while read -r filename; do + git -c core.quotePath=false check-attr filter -z -- "$filename" 2>/dev/null | tr '\0' ' ' echo done | { @@ -196,7 +195,7 @@ _list_encrypted_files() { grep " filter crypt${CONTEXT_CRYPT_SUFFIX:-}.*$" || true fi } | - sed "s| filter crypt${CONTEXT_CRYPT_SUFFIX:-}.*||" | + sed "s| filter crypt${CONTEXT_CRYPT_SUFFIX:-}.*||" | while read -r filename; do eval 'echo $filename'; done } From 6aa50df95d484bb10038247f8e348b7ffd83a133 Mon Sep 17 00:00:00 2001 From: James Murty Date: Thu, 18 Sep 2025 02:13:26 +1000 Subject: [PATCH 3/6] Fix strict check for crypt filter status when listing files --- transcrypt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transcrypt b/transcrypt index cd69874..b742270 100755 --- a/transcrypt +++ b/transcrypt @@ -190,7 +190,7 @@ _list_encrypted_files() { # 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" ]]; then - grep " filter crypt${CONTEXT_CRYPT_SUFFIX:-}$" || true + grep " filter crypt${CONTEXT_CRYPT_SUFFIX:-} $" || true else grep " filter crypt${CONTEXT_CRYPT_SUFFIX:-}.*$" || true fi From 93ba18f123f3b62da0d5f2431d40b662a73c2445 Mon Sep 17 00:00:00 2001 From: James Murty Date: Sat, 27 Sep 2025 06:44:18 +0000 Subject: [PATCH 4/6] Try to improve performance in repo with many files by using awk instead of loop for `--list` --- transcrypt | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/transcrypt b/transcrypt index b742270..a7ec34b 100755 --- a/transcrypt +++ b/transcrypt @@ -177,15 +177,12 @@ _list_encrypted_files() { # backslash and control characters (eval) which are part of the output # regardless of core.quotePath=false as per # https://git-scm.com/docs/git-config#Documentation/git-config.txt-corequotePath - git -c core.quotePath=false ls-files -z | tr '\0' '\n' | - # Read unquoted filenames one at a time per newline delimiters and use - # check-attr to print filter info, with some trickery to keep filename - # unquoted (-z) with space-delimited filter status and anewline - # delimiting of each file name - while read -r filename; do - git -c core.quotePath=false check-attr filter -z -- "$filename" 2>/dev/null | tr '\0' ' ' - echo - done | + git -c core.quotePath=false ls-files -z | + git -c core.quotePath=false check-attr filter -z --stdin 2>/dev/null | + # Split null-delimited list of filename + filter entries to be one line + # per file with space delimiters in that line by replacing every third + # null character with newline and the rest with a space + awk -v RS='\x00' 'NR % 3 == 0 {printf "%s\n", $0} NR % 3 != 0 {printf "%s ", $0}' | { # Only output names of encrypted files matching the context, either # strictly (if $1 = "true") or loosely (if $1 is false or unset) From e0dedcb27599c475d0b35c359a7ae8eac1f39326 Mon Sep 17 00:00:00 2001 From: James Murty Date: Sat, 27 Sep 2025 06:53:25 +0000 Subject: [PATCH 5/6] Add unit test for --list working with encrypted files with special characters in file name --- tests/test_crypt.bats | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/test_crypt.bats b/tests/test_crypt.bats index d4b4ede..cf5875e 100755 --- a/tests/test_crypt.bats +++ b/tests/test_crypt.bats @@ -135,6 +135,40 @@ SECRET_CONTENT_ENC="U2FsdGVkX1/6ilR0PmJpAyCF7iG3+k4aBwbgVd48WaQXznsg42nXbQrlWsf/ rm "$FILENAME" } +@test "crypt: challenging file name with special characters is included in --list" { + FILENAME='"Difficult file name""")))(((][][].secret' + SECRET_CONTENT_ENC="U2FsdGVkX18wtFI6Ydnw7t7uUCKUMK3KeqExy6bC9mGNx1BjpXLbaOH3mxOtoPjn" + + # It's too hard to use `encrypt_named_file` function here due to difficult + # filename quoting, so do similar steps manually + echo "$SECRET_CONTENT" > "$FILENAME" + echo "*.secret filter=crypt diff=crypt merge=crypt" >> .gitattributes + git add .gitattributes "${FILENAME}" + run git commit -m "Encrypt file \"$FILENAME\"" + + # Working copy is decrypted + run cat "$FILENAME" + [ "$status" -eq 0 ] + [ "${lines[0]}" = "$SECRET_CONTENT" ] + + # Git internal copy is encrypted + run git show HEAD:"\"Difficult file name\"\"\")))(((][][].secret" --no-textconv + [ "$status" -eq 0 ] + [ "${lines[0]}" = "$SECRET_CONTENT_ENC" ] + + # git ls-crypt lists encrypted file + run git ls-crypt + [ "$status" -eq 0 ] + [[ "${output}" = *"$FILENAME" ]] + + # transcrypt --list lists encrypted file" + run ../transcrypt --list + [ "$status" -eq 0 ] + [[ "${output}" = *"$FILENAME" ]] + + rm "$FILENAME" +} + @test "crypt: handle very small file" { FILENAME="small file.txt" SECRET_CONTENT="sh" From 94b85993787b4646d63eb1e101687441ca07b63e Mon Sep 17 00:00:00 2001 From: James Murty Date: Sat, 27 Sep 2025 07:05:23 +0000 Subject: [PATCH 6/6] Fix --list to work with strict context checks, after latest changes --- transcrypt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transcrypt b/transcrypt index a7ec34b..3fdfe7f 100755 --- a/transcrypt +++ b/transcrypt @@ -187,7 +187,7 @@ _list_encrypted_files() { # 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" ]]; then - grep " filter crypt${CONTEXT_CRYPT_SUFFIX:-} $" || true + grep " filter crypt${CONTEXT_CRYPT_SUFFIX:-}$" || true else grep " filter crypt${CONTEXT_CRYPT_SUFFIX:-}.*$" || true fi