fix: deduplicate cumulative IMS SMS segments (#34)

This commit is contained in:
Rain Seven
2026-08-16 17:09:08 +08:00
committed by GitHub
parent 7ba30132f9
commit 297d2c1839
2 changed files with 86 additions and 2 deletions
+30 -2
View File
@@ -3,6 +3,7 @@ package store
import (
"encoding/json"
"fmt"
"maps"
"sort"
"strconv"
"strings"
@@ -94,9 +95,15 @@ func mergeConcatSegment(
}
}
}
prior, alreadyHad := parts[sequence]
changed = !alreadyHad || prior != segmentBody
// Some IMS stacks hand us a cumulative segment: sequence 2 contains the
// already-decoded text of sequence 1 followed by its own payload. Keep a
// snapshot so normalizing that representation remains idempotent on a later
// redelivery of the same segment.
previousParts := maps.Clone(parts)
normalizeCumulativeConcatParts(previousParts)
parts[sequence] = segmentBody
normalizeCumulativeConcatParts(parts)
changed = !maps.Equal(previousParts, parts)
sequences := make([]int, 0, len(parts))
for n := range parts {
@@ -130,3 +137,24 @@ func mergeConcatSegment(
}
return joined.String(), json.RawMessage(encoded), changed, nil
}
// normalizeCumulativeConcatParts converts cumulative IMS segment bodies back
// into ordinary per-segment bodies. It only removes an exact, non-empty prefix
// assembled from every preceding sequence starting at 1, and only when the
// current value also contains additional text. That deliberately leaves equal
// repeated segments and incomplete/out-of-order prefixes untouched.
func normalizeCumulativeConcatParts(parts map[int]string) {
var prefix strings.Builder
for sequence := 1; ; sequence++ {
text, ok := parts[sequence]
if !ok {
return
}
assembled := prefix.String()
if assembled != "" && len(text) > len(assembled) && strings.HasPrefix(text, assembled) {
text = strings.TrimPrefix(text, assembled)
parts[sequence] = text
}
prefix.WriteString(text)
}
}
+56
View File
@@ -101,6 +101,62 @@ func TestMergeConcatSegmentRedeliveryIsIdempotent(t *testing.T) {
}
}
func TestMergeConcatSegmentNormalizesCumulativeIMSPart(t *testing.T) {
first := strings.Repeat("安全提醒", 17)
want := first + "请通过官方渠道核实。"
_, extra, _, err := mergeConcatSegment(nil, first, concatExtra(t, 8, 2, 1))
if err != nil {
t.Fatal(err)
}
body, normalized, changed, err := mergeConcatSegment(extra, want, concatExtra(t, 8, 2, 2))
if err != nil || !changed {
t.Fatalf("cumulative segment: body=%q changed=%v err=%v", body, changed, err)
}
if body != want {
t.Fatalf("body = %q, want cumulative text once %q", body, want)
}
// Redelivering the cumulative wire representation must compare equal to the
// normalized stored representation and must not churn the durable row id.
body, _, changed, err = mergeConcatSegment(normalized, want, concatExtra(t, 8, 2, 2))
if err != nil {
t.Fatal(err)
}
if changed || body != want {
t.Fatalf("redelivery: body=%q changed=%v, want %q/false", body, changed, want)
}
}
func TestMergeConcatSegmentNormalizesCumulativeIMSPartOutOfOrder(t *testing.T) {
first := strings.Repeat("甲", 67)
want := first + "尾段"
_, extra, _, err := mergeConcatSegment(nil, want, concatExtra(t, 12, 2, 2))
if err != nil {
t.Fatal(err)
}
body, _, changed, err := mergeConcatSegment(extra, first, concatExtra(t, 12, 2, 1))
if err != nil || !changed {
t.Fatalf("out-of-order segment: body=%q changed=%v err=%v", body, changed, err)
}
if body != want {
t.Fatalf("body = %q, want cumulative text once %q", body, want)
}
}
func TestMergeConcatSegmentKeepsEqualRepeatedPart(t *testing.T) {
_, extra, _, err := mergeConcatSegment(nil, "重复", concatExtra(t, 13, 2, 1))
if err != nil {
t.Fatal(err)
}
body, _, _, err := mergeConcatSegment(extra, "重复", concatExtra(t, 13, 2, 2))
if err != nil {
t.Fatal(err)
}
if body != "重复重复" {
t.Fatalf("body = %q, want intentional equal segments preserved", body)
}
}
func TestMergeConcatSegmentWithoutHeaderPassesThrough(t *testing.T) {
extra, err := json.Marshal(map[string]any{"encoding": "gsm7"})
if err != nil {