diff --git a/README.md b/README.md index 59f8186..2bf7ce3 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ **Collective memory, encrypted.** -[![tests: 416](https://img.shields.io/badge/tests-416-brightgreen?style=flat)](test/) +[![tests: 419](https://img.shields.io/badge/tests-419-brightgreen?style=flat)](test/) ![lints: 8](https://img.shields.io/badge/lints-8-blue?style=flat) [![license: MIT](https://img.shields.io/badge/license-MIT-blue?style=flat)](LICENSE) diff --git a/lib/obfuscate.sh b/lib/obfuscate.sh index 391abfe..1bc3eb5 100644 --- a/lib/obfuscate.sh +++ b/lib/obfuscate.sh @@ -83,6 +83,8 @@ build_obfuscation_plan() { if ! awk -F '\t' -v OFS='\t' -v candidates="$candidates" ' FILENAME != candidates { if ($1 != "") { + if (($1 in id_names) && id_names[$1] != $2) duplicate_ids[$1] = 1 + id_names[$1] = $2 ids[$1] = 1 if (!($2 in names)) names[$2] = $1 } @@ -97,7 +99,11 @@ build_obfuscation_plan() { if (base ~ /^[a-f0-9]{8}$/) { print "invalid", "-", relpath } else if (relpath in names) { - print "known", names[relpath], relpath + if (names[relpath] in duplicate_ids) { + print "collision", names[relpath], relpath + } else { + print "known", names[relpath], relpath + } } else { print "new", "-", relpath } @@ -114,6 +120,16 @@ build_obfuscation_plan() { refuse_if_hex_basename "$relpath" return 1 fi + if [ "$kind" = "collision" ]; then + rm -rf "$workspace" + echo "Error: manifest ID '$id' maps to multiple readable paths" >&2 + return 1 + fi + if [ "$kind" = "known" ] && { [ -e "$notes_dir/$id" ] || [ -L "$notes_dir/$id" ]; }; then + rm -rf "$workspace" + echo "Error: refusing to overwrite existing obfuscated path: $id" >&2 + return 1 + fi done < "$plan_file" rm -rf "$workspace" diff --git a/test/obfuscate.bats b/test/obfuscate.bats index fabdeb4..786ebec 100644 --- a/test/obfuscate.bats +++ b/test/obfuscate.bats @@ -174,6 +174,57 @@ setup() { [ -f "$NOTES_CALLER_PWD/notes/$alpha_id" ] } +@test "obfuscate refuses to overwrite an existing known-ID destination" { + notes obfuscate + local alpha_id + alpha_id=$(grep $'\talpha\.md$' "$NOTES_CALLER_PWD/notes/.manifest" | cut -f1) + git -C "$NOTES_CALLER_PWD" commit -q --no-verify -m "obfuscated" + + # Simulate an interrupted or stale state containing both representations. + printf 'dirty readable content\n' > "$NOTES_CALLER_PWD/notes/alpha.md" + local obfuscated_before + obfuscated_before=$(cat "$NOTES_CALLER_PWD/notes/$alpha_id") + + run notes obfuscate alpha.md + + [ "$status" -ne 0 ] + [[ "$output" == *"refusing to overwrite existing obfuscated path: $alpha_id"* ]] + [ "$(cat "$NOTES_CALLER_PWD/notes/$alpha_id")" = "$obfuscated_before" ] + [ "$(cat "$NOTES_CALLER_PWD/notes/alpha.md")" = "dirty readable content" ] +} + +@test "obfuscate refuses to replace a dangling known-ID symlink" { + notes obfuscate + local alpha_id + alpha_id=$(grep $'\talpha\.md$' "$NOTES_CALLER_PWD/notes/.manifest" | cut -f1) + rm "$NOTES_CALLER_PWD/notes/$alpha_id" + printf 'readable content\n' > "$NOTES_CALLER_PWD/notes/alpha.md" + ln -s missing-target "$NOTES_CALLER_PWD/notes/$alpha_id" + + run notes obfuscate alpha.md + + [ "$status" -ne 0 ] + [[ "$output" == *"refusing to overwrite existing obfuscated path: $alpha_id"* ]] + [ -L "$NOTES_CALLER_PWD/notes/$alpha_id" ] + [ "$(readlink "$NOTES_CALLER_PWD/notes/$alpha_id")" = "missing-target" ] + [ "$(cat "$NOTES_CALLER_PWD/notes/alpha.md")" = "readable content" ] +} + +@test "obfuscate refuses a manifest ID shared by planned readable paths" { + notes obfuscate + local alpha_id + alpha_id=$(grep $'\talpha\.md$' "$NOTES_CALLER_PWD/notes/.manifest" | cut -f1) + notes deobfuscate + printf '%s\tbeta.md\n' "$alpha_id" >> "$NOTES_CALLER_PWD/notes/.manifest" + + run notes obfuscate alpha.md beta.md + + [ "$status" -ne 0 ] + [[ "$output" == *"manifest ID '$alpha_id' maps to multiple readable paths"* ]] + [ -f "$NOTES_CALLER_PWD/notes/alpha.md" ] + [ -f "$NOTES_CALLER_PWD/notes/beta.md" ] +} + @test "full obfuscate batches Fold-scale known-entry classification and staging" { local count=535 i=1 id name mock_bin command real_command call_log rm -rf "$NOTES_CALLER_PWD/notes"