Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 2 additions & 9 deletions internal/protocol/profile_pics.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,7 @@ func EncodeProfilePicsBundle(b ProfilePicsBundle) []byte {
}
size := profilePicsHeaderFixed + relayCount + 2 /*entry count*/
for _, e := range b.Entries {
n := len(e.Username)
if n > 255 {
n = 255
}
size += 1 + n + profilePicEntryFixed
size += 1 + len(truncateUTF8Bytes(e.Username, 255)) + profilePicEntryFixed
}
buf := make([]byte, size)
off := 0
Expand All @@ -109,10 +105,7 @@ func EncodeProfilePicsBundle(b ProfilePicsBundle) []byte {
binary.BigEndian.PutUint16(buf[off:], uint16(len(b.Entries)))
off += 2
for _, e := range b.Entries {
nb := []byte(e.Username)
if len(nb) > 255 {
nb = nb[:255]
}
nb := truncateUTF8Bytes(e.Username, 255)
buf[off] = byte(len(nb))
off++
copy(buf[off:], nb)
Expand Down
30 changes: 29 additions & 1 deletion internal/protocol/profile_pics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import (
"hash/crc32"
"strings"
"testing"
"unicode/utf8"
)

func TestEncodeDecodeProfilePicsBundleRoundTrip(t *testing.T) {
// Three avatars concatenated into a fake bundle, with offsets/CRCs
// computed for real so VerifyEntry doesn't trip on them.
a := []byte("aaaaaaaaaa") // 10 bytes
a := []byte("aaaaaaaaaa") // 10 bytes
b := []byte("bbbbbbbbbbbbbb") // 14 bytes
c := []byte("ccccc") // 5 bytes
bundle := append(append(append([]byte{}, a...), b...), c...)
Expand Down Expand Up @@ -123,6 +124,33 @@ func TestProfilePicsTruncatesLongUsername(t *testing.T) {
}
}

func TestProfilePicsTruncatesLongUTF8Username(t *testing.T) {
long := strings.Repeat("u", 254) + "نام"
in := ProfilePicsBundle{
Header: ProfilePicsBundleHeader{Relays: []bool{true}},
Entries: []ProfilePicEntry{
{Username: long, Offset: 0, Size: 100, CRC: 1, MIME: 0},
},
}
got, err := DecodeProfilePicsBundle(EncodeProfilePicsBundle(in))
if err != nil {
t.Fatalf("decode: %v", err)
}
if len(got.Entries) != 1 {
t.Fatalf("entries = %d, want 1", len(got.Entries))
}
username := got.Entries[0].Username
if !utf8.ValidString(username) {
t.Fatalf("truncated username is invalid UTF-8: %q", username)
}
if len(username) > 255 {
t.Fatalf("truncated username is %d bytes, want <= 255", len(username))
}
if username != strings.Repeat("u", 254) {
t.Fatalf("truncated username = %q", username)
}
}

func TestProfilePicsTruncatedDataReturnsError(t *testing.T) {
in := ProfilePicsBundle{
Header: ProfilePicsBundleHeader{Relays: []bool{true}},
Expand Down
50 changes: 28 additions & 22 deletions internal/protocol/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,27 @@ func ContentHashOf(msgs []Message) uint32 {
return crc32.ChecksumIEEE(data)
}

func truncateUTF8Bytes(s string, max int) []byte {
if max <= 0 {
return nil
}
if len(s) <= max {
return []byte(s)
}
end := 0
for i, r := range s {
_, size := utf8.DecodeRuneInString(s[i:])
if r == utf8.RuneError && size == 0 {
break
}
if i+size > max {
break
}
end = i + size
}
return []byte(s[:end])
}

// SerializeMetadata encodes metadata into bytes for channel 0 blocks.
// Format: marker(3) + timestamp(4) + nextFetch(4) + flags(1) + channelCount(2) + per-channel data
// Per-channel: nameLen(1) + name + blocks(2) + lastMsgID(4) + contentHash(4) + chatType(1) + flags(1)
Expand All @@ -147,7 +168,7 @@ func SerializeMetadata(m *Metadata) []byte {
// 3 marker + 4 timestamp + 4 nextFetch + 1 flags + 2 channel count + per-channel data
size := MarkerSize + 4 + 4 + 1 + 2
for _, ch := range m.Channels {
size += 1 + len(ch.Name) + 2 + 4 + 4 + 1 + 1
size += 1 + len(truncateUTF8Bytes(ch.Name, 255)) + 2 + 4 + 4 + 1 + 1
}
buf := make([]byte, size)
off := 0
Expand Down Expand Up @@ -175,10 +196,7 @@ func SerializeMetadata(m *Metadata) []byte {
off += 2

for _, ch := range m.Channels {
nameBytes := []byte(ch.Name)
if len(nameBytes) > 255 {
nameBytes = nameBytes[:255]
}
nameBytes := truncateUTF8Bytes(ch.Name, 255)
buf[off] = byte(len(nameBytes))
off++
copy(buf[off:], nameBytes)
Expand Down Expand Up @@ -437,28 +455,16 @@ func CompressMessages(data []byte) []byte {
func EncodeTitlesData(titles map[string]string) []byte {
size := 2
for name, title := range titles {
n := name
if len(n) > 255 {
n = n[:255]
}
t := title
if len([]byte(t)) > 255 {
t = string([]byte(t)[:255])
}
size += 1 + len(n) + 1 + len([]byte(t))
nb := truncateUTF8Bytes(name, 255)
tb := truncateUTF8Bytes(title, 255)
size += 1 + len(nb) + 1 + len(tb)
}
buf := make([]byte, size)
binary.BigEndian.PutUint16(buf, uint16(len(titles)))
off := 2
for name, title := range titles {
nb := []byte(name)
if len(nb) > 255 {
nb = nb[:255]
}
tb := []byte(title)
if len(tb) > 255 {
tb = tb[:255]
}
nb := truncateUTF8Bytes(name, 255)
tb := truncateUTF8Bytes(title, 255)
buf[off] = byte(len(nb))
off++
copy(buf[off:], nb)
Expand Down
44 changes: 44 additions & 0 deletions internal/protocol/protocol_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package protocol
import (
"bytes"
"testing"
"unicode/utf8"
)

func TestSerializeParseMetadata(t *testing.T) {
Expand Down Expand Up @@ -35,6 +36,29 @@ func TestSerializeParseMetadata(t *testing.T) {
}
}

func TestSerializeMetadataTruncatesLongUTF8Names(t *testing.T) {
name := string(bytes.Repeat([]byte("a"), 254)) + "خبر"
parsed, err := ParseMetadata(SerializeMetadata(&Metadata{
Channels: []ChannelInfo{{Name: name, Blocks: 1}},
}))
if err != nil {
t.Fatalf("ParseMetadata: %v", err)
}
if len(parsed.Channels) != 1 {
t.Fatalf("channels = %d, want 1", len(parsed.Channels))
}
got := parsed.Channels[0].Name
if !utf8.ValidString(got) {
t.Fatalf("truncated name is invalid UTF-8: %q", got)
}
if len(got) > 255 {
t.Fatalf("truncated name is %d bytes, want <= 255", len(got))
}
if got != string(bytes.Repeat([]byte("a"), 254)) {
t.Fatalf("truncated name = %q", got)
}
}

func TestMetadataFlags(t *testing.T) {
for _, tc := range []struct {
tg, chat bool
Expand All @@ -50,6 +74,26 @@ func TestMetadataFlags(t *testing.T) {
}
}

func TestEncodeTitlesDataTruncatesLongUTF8Values(t *testing.T) {
longTitle := string(bytes.Repeat([]byte("b"), 254)) + "世界"
got, err := DecodeTitlesData(EncodeTitlesData(map[string]string{
"news": longTitle,
}))
if err != nil {
t.Fatalf("DecodeTitlesData: %v", err)
}
title := got["news"]
if !utf8.ValidString(title) {
t.Fatalf("truncated title is invalid UTF-8: %q", title)
}
if len(title) > 255 {
t.Fatalf("truncated title is %d bytes, want <= 255", len(title))
}
if title != string(bytes.Repeat([]byte("b"), 254)) {
t.Fatalf("truncated title = %q", title)
}
}

func TestSerializeParseMessages(t *testing.T) {
original := []Message{
{ID: 100, Timestamp: 1700000000, Text: "Hello world"},
Expand Down