From 297d2c1839f0627c7a5b79b6d03db150b88dc3f6 Mon Sep 17 00:00:00 2001 From: Rain Seven <128443127+RAiNY7Study@users.noreply.github.com> Date: Sun, 16 Aug 2026 17:09:08 +0800 Subject: [PATCH] fix: deduplicate cumulative IMS SMS segments (#34) --- internal/store/sms_reassembly.go | 32 ++++++++++++++- internal/store/sms_reassembly_test.go | 56 +++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/internal/store/sms_reassembly.go b/internal/store/sms_reassembly.go index 573620c..6154d41 100644 --- a/internal/store/sms_reassembly.go +++ b/internal/store/sms_reassembly.go @@ -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) + } +} diff --git a/internal/store/sms_reassembly_test.go b/internal/store/sms_reassembly_test.go index 9434050..9e75106 100644 --- a/internal/store/sms_reassembly_test.go +++ b/internal/store/sms_reassembly_test.go @@ -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 {