From 06de8b41cfa44d5f5686163800871a684b209567 Mon Sep 17 00:00:00 2001 From: Tai Nguyen <49914814+nvt-ak@users.noreply.github.com> Date: Tue, 31 Mar 2026 23:14:39 +0700 Subject: [PATCH] fix(cmd): sanitize followup reminder UTF-8 truncation (#609) Prevent invalid UTF-8 from being persisted when auto-setting followups by sanitizing message content and truncating by rune count. Add regression tests for emoji truncation and malformed byte sequences. --- cmd/gateway_consumer.go | 11 +++++++-- cmd/gateway_consumer_followup_test.go | 34 +++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 cmd/gateway_consumer_followup_test.go diff --git a/cmd/gateway_consumer.go b/cmd/gateway_consumer.go index 3c88bdc9..cbca58e3 100644 --- a/cmd/gateway_consumer.go +++ b/cmd/gateway_consumer.go @@ -8,6 +8,7 @@ import ( "strings" "sync" "time" + "unicode/utf8" "github.com/google/uuid" @@ -212,8 +213,14 @@ func truncateForReminder(content string, maxLen int) string { // Use last non-empty line as it's typically the most relevant. lines := strings.Split(strings.TrimSpace(content), "\n") msg := lines[len(lines)-1] - if len(msg) > maxLen { - msg = msg[:maxLen] + "..." + // Ensure we only persist valid UTF-8 into PostgreSQL. + msg = strings.ToValidUTF8(msg, "") + if maxLen <= 0 { + return msg + } + if utf8.RuneCountInString(msg) > maxLen { + r := []rune(msg) + msg = string(r[:maxLen]) + "..." } return msg } diff --git a/cmd/gateway_consumer_followup_test.go b/cmd/gateway_consumer_followup_test.go new file mode 100644 index 00000000..a8624c7c --- /dev/null +++ b/cmd/gateway_consumer_followup_test.go @@ -0,0 +1,34 @@ +package cmd + +import ( + "strings" + "testing" + "unicode/utf8" +) + +func TestTruncateForReminder_TruncatesByRuneAndKeepsUTF8Valid(t *testing.T) { + input := strings.Repeat("a", 199) + "✌️" + + got := truncateForReminder(input, 200) + if !utf8.ValidString(got) { + t.Fatalf("truncateForReminder() produced invalid UTF-8: %q", got) + } + if !strings.HasSuffix(got, "...") { + t.Fatalf("truncateForReminder() = %q, want suffix ...", got) + } + if strings.Contains(got, "\uFFFD") { + t.Fatalf("truncateForReminder() introduced replacement rune: %q", got) + } +} + +func TestTruncateForReminder_StripsInvalidUTF8(t *testing.T) { + invalid := string([]byte{'a', 0xef, 0xb8, '.', 'b'}) + + got := truncateForReminder(invalid, 200) + if !utf8.ValidString(got) { + t.Fatalf("truncateForReminder() produced invalid UTF-8: %q", got) + } + if strings.ContainsRune(got, '\uFFFD') { + t.Fatalf("truncateForReminder() = %q, want no replacement rune", got) + } +}