Files
pad/internal/attachments/filename_source_test.go
T
xarmian c1fa878889 fix(attachments,mcp-audit): record where a stored filename or tool name came from, instead of sentinel strings a caller can choose (BUG-2819) (#1537)
* fix(attachments,mcp-audit): record where a stored filename or tool name came from, instead of sentinel strings a caller can choose (BUG-2819)

attachments.filename and mcp_audit_log.tool_name each held either the
caller's value or a server substitution with no marker, and the caller could
send the substitution itself ("upload.bin", "(unknown)"). New columns,
CHECK-constrained on both dialects and defined once in Go:

- attachments.filename_source: caller | normalised | substituted | derived |
  unknown, decided by NormalizeFilenameWithSource at the upload and import
  doors (raw vs stored), derived for thumbnails and transforms, carried by
  clones, and carried through bundle export/import as the bundle's claim.
- mcp_audit_log.tool_name_source: caller | sanitised | synthesised | unknown,
  decided at each branch of the audit parser.

Existing rows default to unknown, never caller. The (sanitised)/(unknown)
namespace reservation stays as defence in depth. filename_source reaches
pad_attachment list on both transports, so ToolSurfaceVersion 0.53
(additive).

Claude-Session: https://claude.ai/code/session_01A3rXV7GDLAhBe9mL4V6z2m

* fix(attachments): the bidi filename backfill keeps filename_source true when it rewrites a name (BUG-2819 review round 1)

The startup backfill rewrites a stored filename that carries a Bidi_Control
character, and left filename_source untouched, so a row recorded as the
caller's own name would keep that claim over a server-altered name. A
rewritten 'caller' row now reads 'normalised'; every other value is still
true after the rewrite. The 0.53 entry now says the key is always present.

Claude-Session: https://claude.ai/code/session_01A3rXV7GDLAhBe9mL4V6z2m
2026-09-25 05:13:05 -04:00

54 lines
2.0 KiB
Go

package attachments
import "testing"
// BUG-2819: the stored name cannot say where it came from, so the normaliser
// reports which branch decided it. Every case also checks that the name is
// NormalizeFilename's, byte for byte, so provenance can never change what is
// stored.
func TestNormalizeFilenameWithSource(t *testing.T) {
cases := []struct {
raw string
wantName string
wantSource FilenameSource
}{
{"report.pdf", "report.pdf", FilenameFromCaller},
// The case the field exists for: a caller who really named a file
// what the server would substitute.
{"upload.bin", "upload.bin", FilenameFromCaller},
{"upload.png", "upload.png", FilenameFromCaller},
{"dir/report.pdf", "report.pdf", FilenameNormalised},
{`dir\report.pdf`, "report.pdf", FilenameNormalised},
{"report.pdf.", "report.pdf", FilenameNormalised},
{"re\x07port.pdf", "report.pdf", FilenameNormalised},
{"nul.txt", "_nul.txt", FilenameNormalised},
{"", "upload.bin", FilenameSubstituted},
{".", "upload.bin", FilenameSubstituted},
{"..", "upload.bin", FilenameSubstituted},
{"\xff\xfe.png", "upload.png", FilenameSubstituted},
{"bad\x00name", "upload", FilenameSubstituted},
}
for _, tc := range cases {
name, source := NormalizeFilenameWithSource(tc.raw)
if name != tc.wantName || source != tc.wantSource {
t.Errorf("NormalizeFilenameWithSource(%q) = (%q, %q), want (%q, %q)", tc.raw, name, source, tc.wantName, tc.wantSource)
}
if plain := NormalizeFilename(tc.raw); plain != name {
t.Errorf("NormalizeFilename(%q) = %q, but WithSource returned %q", tc.raw, plain, name)
}
}
}
func TestValidFilenameSource(t *testing.T) {
for _, v := range []string{"caller", "normalised", "substituted", "derived", "unknown"} {
if !ValidFilenameSource(v) {
t.Errorf("ValidFilenameSource(%q) = false, want true", v)
}
}
for _, v := range []string{"", "Caller", "server", "normalized"} {
if ValidFilenameSource(v) {
t.Errorf("ValidFilenameSource(%q) = true, want false", v)
}
}
}