Skip to content

Commit 0b5a591

Browse files
committed
Fix UTF-8-safe truncation for Telegram and CLI summaries
1 parent dade2c7 commit 0b5a591

4 files changed

Lines changed: 73 additions & 5 deletions

File tree

cmd/ralphctl/main.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"bufio"
5+
"bytes"
56
"context"
67
"errors"
78
"flag"
@@ -15,6 +16,7 @@ import (
1516
"strings"
1617
"syscall"
1718
"time"
19+
"unicode/utf8"
1820

1921
"codex-ralph/internal/ralph"
2022
)
@@ -1550,13 +1552,20 @@ func compactSingleLine(raw string, maxLen int) string {
15501552
v = strings.ReplaceAll(v, "\n", " ")
15511553
v = strings.ReplaceAll(v, "\r", " ")
15521554
v = strings.Join(strings.Fields(v), " ")
1553-
if maxLen <= 0 || len(v) <= maxLen {
1555+
if !utf8.ValidString(v) {
1556+
v = string(bytes.ToValidUTF8([]byte(v), []byte("?")))
1557+
}
1558+
if maxLen <= 0 {
1559+
return v
1560+
}
1561+
runes := []rune(v)
1562+
if len(runes) <= maxLen {
15541563
return v
15551564
}
15561565
if maxLen <= 3 {
1557-
return v[:maxLen]
1566+
return string(runes[:maxLen])
15581567
}
1559-
return v[:maxLen-3] + "..."
1568+
return string(runes[:maxLen-3]) + "..."
15601569
}
15611570

15621571
func valueOrDash(raw string) string {

cmd/ralphctl/main_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"os"
55
"path/filepath"
66
"testing"
7+
"unicode/utf8"
78

89
"codex-ralph/internal/ralph"
910
)
@@ -53,6 +54,32 @@ func TestCompactSingleLine(t *testing.T) {
5354
}
5455
}
5556

57+
func TestCompactSingleLineUnicodeSafe(t *testing.T) {
58+
t.Parallel()
59+
60+
// 4-byte emoji + Korean should not be cut mid-rune.
61+
got := compactSingleLine("🔥비트코인 자동화", 5)
62+
if !utf8.ValidString(got) {
63+
t.Fatalf("output must be valid UTF-8: %q", got)
64+
}
65+
if got == "" {
66+
t.Fatalf("output should not be empty")
67+
}
68+
}
69+
70+
func TestCompactSingleLineInvalidUTF8Sanitized(t *testing.T) {
71+
t.Parallel()
72+
73+
raw := string([]byte{0xff, 0xfe, 'a', 'b', 'c'})
74+
got := compactSingleLine(raw, 10)
75+
if !utf8.ValidString(got) {
76+
t.Fatalf("output must be valid UTF-8: %q", got)
77+
}
78+
if got == "" {
79+
t.Fatalf("output should not be empty")
80+
}
81+
}
82+
5683
func TestResolveReloadTargetsFleetOnlyWhenCurrentUnmanaged(t *testing.T) {
5784
t.Parallel()
5885

internal/ralph/telegram.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"strings"
1616
"sync"
1717
"time"
18+
"unicode/utf8"
1819
)
1920

2021
const defaultTelegramBaseURL = "https://api.telegram.org"
@@ -628,8 +629,12 @@ func compactTelegramError(raw string) string {
628629
}
629630
raw = strings.ReplaceAll(raw, "\n", " ")
630631
raw = strings.Join(strings.Fields(raw), " ")
631-
if len(raw) > 300 {
632-
return raw[:297] + "..."
632+
if !utf8.ValidString(raw) {
633+
raw = string(bytes.ToValidUTF8([]byte(raw), []byte("?")))
634+
}
635+
runes := []rune(raw)
636+
if len(runes) > 300 {
637+
return string(runes[:297]) + "..."
633638
}
634639
return raw
635640
}

internal/ralph/telegram_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"strings"
1010
"testing"
1111
"time"
12+
"unicode/utf8"
1213
)
1314

1415
func TestParseTelegramChatIDs(t *testing.T) {
@@ -81,6 +82,32 @@ func TestSplitTelegramMessage(t *testing.T) {
8182
}
8283
}
8384

85+
func TestCompactTelegramErrorUnicodeSafe(t *testing.T) {
86+
t.Parallel()
87+
88+
raw := strings.Repeat("🔥비트코인 자동화 ", 40)
89+
got := compactTelegramError(raw)
90+
if !utf8.ValidString(got) {
91+
t.Fatalf("output must be valid UTF-8: %q", got)
92+
}
93+
if len([]rune(got)) > 300 {
94+
t.Fatalf("output should be capped at 300 runes: %d", len([]rune(got)))
95+
}
96+
}
97+
98+
func TestCompactTelegramErrorInvalidUTF8Sanitized(t *testing.T) {
99+
t.Parallel()
100+
101+
raw := string([]byte{0xff, 0xfe, 'a', 'b', 'c'})
102+
got := compactTelegramError(raw)
103+
if !utf8.ValidString(got) {
104+
t.Fatalf("output must be valid UTF-8: %q", got)
105+
}
106+
if !strings.Contains(got, "abc") {
107+
t.Fatalf("sanitized output should preserve readable content: %q", got)
108+
}
109+
}
110+
84111
func TestIsTelegramChatAllowed(t *testing.T) {
85112
t.Parallel()
86113

0 commit comments

Comments
 (0)