From 84c6bf0534f78f6a2924b1de78da058b4a80df17 Mon Sep 17 00:00:00 2001 From: Canaan Etai Date: Wed, 3 Jun 2026 15:29:35 +0100 Subject: [PATCH] =?UTF-8?q?feat(iso8583):=20freeze-prep=20=E2=80=94=20lock?= =?UTF-8?q?=20down=20Bitmap,=20rename=20PutTag=E2=86=92SetTag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit API freeze-prep ahead of v1: enforce the always-derived bitmap invariant through the type system, and align tag-write naming with the field API. - Unexport the engine-internal Bitmap mutators Set/Clear/SetWord so application code cannot mutate a bitmap directly (the wire bitmap is always derived from the present-field set at marshal time). The read surface is unchanged. - Add immutable constructors iso8583.BitmapFromWords([3]uint64) (wire/word-level, used by BitmapCodec implementations) and iso8583.BitmapFor(des ...int) (by present DE, for codec authors and tests). - Update the internal engine, the fieldcodec bitmap codec, and tests to the new constructors. - Rename Message.PutTag -> SetTag for consistency with Set/SetS/SetP; update the two callers (message.go, fieldcodec/tlv). Breaking (pre-1.0). gofmt/go vet clean; full suite passes under go test -race. --- CHANGELOG.md | 25 +++++++++++++++++++++++++ fieldcodec/bitmap.go | 10 +++++----- fieldcodec/fieldcodec_test.go | 8 ++------ fieldcodec/tlv/bertlv.go | 2 +- iso8583/bitmap.go | 29 +++++++++++++++++++++-------- iso8583/bitmap_test.go | 18 +++++++++--------- iso8583/codec.go | 2 +- iso8583/codecs_test.go | 16 ++++++++-------- iso8583/message.go | 16 ++++++++-------- 9 files changed, 80 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66f01fc..c41c96e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,31 @@ the [versioning policy](https://teqpace-services.github.io/isopace/versioning/). ## [Unreleased] +API freeze-prep ahead of v1: tighten the public surface so the always-derived +bitmap invariant is enforced by the type system, and align tag-write naming with +the rest of the field API. + +### Added + +- **`iso8583.BitmapFromWords([3]uint64)` and `iso8583.BitmapFor(des ...int)`** — + immutable `Bitmap` constructors. `BitmapFromWords` is the wire/word-level path + for `BitmapCodec` implementations; `BitmapFor` builds a bitmap from present DE + numbers for codec authors and tests. + +### Changed + +- **`iso8583.Message.PutTag` renamed to `SetTag`**, for consistency with the + `Set` / `SetS` / `SetP` family. **Breaking** for direct callers. + +### Removed + +- **Unexported the engine-internal `Bitmap` mutators** `Set`, `Clear`, and + `SetWord`. The wire bitmap is always derived from the present-field set at + marshal time, so application code never mutates a bitmap directly; this makes + that invariant impossible to violate through the public API. **Breaking** — + construct bitmaps with `BitmapFromWords` / `BitmapFor` instead. The read + methods (`IsSet`, `Word`, `Count`, `Width`, `Range`, `String`) are unchanged. + ## [0.3.0] - 2026-06-02 The acquirer-profiles release: two more ISO 8583:1987 switch profiles — diff --git a/fieldcodec/bitmap.go b/fieldcodec/bitmap.go index 1fe0c70..5a634a4 100644 --- a/fieldcodec/bitmap.go +++ b/fieldcodec/bitmap.go @@ -58,25 +58,25 @@ type bitmapCodec struct{ repr wordRepr } func (c bitmapCodec) Name() string { return c.repr.name() } func (c bitmapCodec) ReadBitmap(src []byte, off, maxLevels int) (iso8583.Bitmap, int, error) { - var bm iso8583.Bitmap + var words [3]uint64 wl := c.repr.wireLen() for level := 0; level < 3; level++ { // Overflow-safe bounds check (off can be large/adversarial): never form // off+wl, which could wrap negative and pass a naive comparison. if off < 0 || off > len(src) || len(src)-off < wl { - return bm, 0, ErrShortBitmap + return iso8583.Bitmap{}, 0, ErrShortBitmap } w, ok := c.repr.get(src[off : off+wl]) if !ok { - return bm, 0, ErrBadHex + return iso8583.Bitmap{}, 0, ErrBadHex } - bm.SetWord(level, w) + words[level] = w off += wl if w&contMSB == 0 || level+1 >= maxLevels { break } } - return bm, off, nil + return iso8583.BitmapFromWords(words), off, nil } func (c bitmapCodec) WriteBitmap(dst []byte, bm iso8583.Bitmap, maxLevels int) ([]byte, error) { diff --git a/fieldcodec/fieldcodec_test.go b/fieldcodec/fieldcodec_test.go index 5f19ff9..fd8355e 100644 --- a/fieldcodec/fieldcodec_test.go +++ b/fieldcodec/fieldcodec_test.go @@ -161,10 +161,7 @@ func TestAmountCodecs(t *testing.T) { } func TestBitmapCodecs(t *testing.T) { - var bm iso8583.Bitmap - bm.Set(2) - bm.Set(3) - bm.Set(70) // secondary + bm := iso8583.BitmapFor(2, 3, 70) // 70 is in the secondary bitmap for _, bc := range []iso8583.BitmapCodec{fieldcodec.BitmapBinary, fieldcodec.BitmapHex, fieldcodec.BitmapEBCDIC} { wire, err := bc.WriteBitmap(nil, bm, 2) @@ -181,8 +178,7 @@ func TestBitmapCodecs(t *testing.T) { } // Primary-only stays one level. - var p iso8583.Bitmap - p.Set(2) + p := iso8583.BitmapFor(2) wire, _ := fieldcodec.BitmapBinary.WriteBitmap(nil, p, 2) if len(wire) != 8 { t.Errorf("primary-only binary bitmap = %d bytes want 8", len(wire)) diff --git a/fieldcodec/tlv/bertlv.go b/fieldcodec/tlv/bertlv.go index 1411489..d6c070c 100644 --- a/fieldcodec/tlv/bertlv.go +++ b/fieldcodec/tlv/bertlv.go @@ -73,7 +73,7 @@ func (berTLV) DecodeBody(body []byte, _ int, def *iso8583.FieldDef) (iso8583.Val } else { tv = iso8583.BytesValue(val) } - child.PutTag(tag, tv) + child.SetTag(tag, tv) } return iso8583.CompositeValue(body, child), nil } diff --git a/iso8583/bitmap.go b/iso8583/bitmap.go index ff40627..090386a 100644 --- a/iso8583/bitmap.go +++ b/iso8583/bitmap.go @@ -53,9 +53,9 @@ func (b Bitmap) IsSet(de int) bool { return b.words[w]&mask != 0 } -// Set marks de present. Intended for engine use; application code never calls -// it directly (presence is derived from set fields at marshal time). -func (b *Bitmap) Set(de int) { +// set marks de present. Engine-internal: presence is derived from the set of +// present fields at marshal time, never set by application code. +func (b *Bitmap) set(de int) { if de < 1 || de > maxDEIndex { return } @@ -63,8 +63,8 @@ func (b *Bitmap) Set(de int) { b.words[w] |= mask } -// Clear marks de absent. -func (b *Bitmap) Clear(de int) { +// clear marks de absent (engine-internal). +func (b *Bitmap) clear(de int) { if de < 1 || de > maxDEIndex { return } @@ -76,9 +76,22 @@ func (b *Bitmap) Clear(de int) { // codecs that emit the raw wire form. func (b Bitmap) Word(i int) uint64 { return b.words[i] } -// SetWord installs the i-th 64-bit word; for codecs reconstructing a Bitmap -// from wire bytes. -func (b *Bitmap) SetWord(i int, v uint64) { b.words[i] = v } +// BitmapFromWords builds a Bitmap from its raw 64-bit words (index 0 = primary, +// 1 = secondary, 2 = tertiary). It is the construction path for BitmapCodec +// implementations that parse the wire form. +func BitmapFromWords(words [3]uint64) Bitmap { return Bitmap{words: words} } + +// BitmapFor builds a Bitmap with the given data elements marked present. It is a +// convenience constructor for codec authors and tests; for a live Message the +// engine derives the wire bitmap from the present-field set at marshal time, so +// application code never builds a Bitmap by hand. +func BitmapFor(des ...int) Bitmap { + var b Bitmap + for _, de := range des { + b.set(de) + } + return b +} // Count returns the number of present data elements, excluding the // continuation bits. diff --git a/iso8583/bitmap_test.go b/iso8583/bitmap_test.go index 9f5d040..0622ef4 100644 --- a/iso8583/bitmap_test.go +++ b/iso8583/bitmap_test.go @@ -20,7 +20,7 @@ import ( func TestBitmapSetIsSetClear(t *testing.T) { var b Bitmap for _, de := range []int{2, 3, 64, 65, 128, 129, 192} { - b.Set(de) + b.set(de) } for _, de := range []int{2, 3, 64, 65, 128, 129, 192} { if !b.IsSet(de) { @@ -30,7 +30,7 @@ func TestBitmapSetIsSetClear(t *testing.T) { if b.IsSet(4) { t.Errorf("DE 4 should not be set") } - b.Clear(65) + b.clear(65) if b.IsSet(65) { t.Errorf("DE 65 should be cleared") } @@ -43,10 +43,10 @@ func TestBitmapSetIsSetClear(t *testing.T) { func TestBitmapRangeAscendingExcludesContinuation(t *testing.T) { var b Bitmap // Set continuation bits plus real fields. - b.Set(1) // continuation - b.Set(65) // continuation + b.set(1) // continuation + b.set(65) // continuation for _, de := range []int{2, 11, 70, 129} { - b.Set(de) + b.set(de) } var got []int b.Range(func(de int) bool { @@ -64,15 +64,15 @@ func TestBitmapRangeAscendingExcludesContinuation(t *testing.T) { func TestBitmapWidth(t *testing.T) { var b Bitmap - b.Set(2) + b.set(2) if b.Width() != 64 { t.Errorf("primary-only width = %d want 64", b.Width()) } - b.Set(1) // secondary continuation + b.set(1) // secondary continuation if b.Width() != 128 { t.Errorf("secondary width = %d want 128", b.Width()) } - b.Set(65) // tertiary continuation + b.set(65) // tertiary continuation if b.Width() != 192 { t.Errorf("tertiary width = %d want 192", b.Width()) } @@ -81,7 +81,7 @@ func TestBitmapWidth(t *testing.T) { func TestBitmapRangeEarlyStop(t *testing.T) { var b Bitmap for _, de := range []int{2, 3, 4, 5} { - b.Set(de) + b.set(de) } count := 0 b.Range(func(de int) bool { diff --git a/iso8583/codec.go b/iso8583/codec.go index 53638e9..c60b442 100644 --- a/iso8583/codec.go +++ b/iso8583/codec.go @@ -130,7 +130,7 @@ func (c *Codec) Marshal(m *Message, dst []byte) ([]byte, error) { var bm Bitmap for de := 1; de < len(m.slots); de++ { if m.slots[de].present { - bm.Set(de) + bm.set(de) } } if s.bitmap.Codec == nil { diff --git a/iso8583/codecs_test.go b/iso8583/codecs_test.go index 03f1319..2e08a18 100644 --- a/iso8583/codecs_test.go +++ b/iso8583/codecs_test.go @@ -146,29 +146,29 @@ const ( ) func (binBitmap) ReadBitmap(src []byte, off, maxLevels int) (Bitmap, int, error) { - var bm Bitmap + var words [3]uint64 if off+8 > len(src) { - return bm, 0, errTruncated + return Bitmap{}, 0, errTruncated } w0 := binary.BigEndian.Uint64(src[off:]) - bm.SetWord(0, w0) + words[0] = w0 next := off + 8 if w0&maskDE1 != 0 && maxLevels >= 2 { if next+8 > len(src) { - return bm, 0, errTruncated + return Bitmap{}, 0, errTruncated } w1 := binary.BigEndian.Uint64(src[next:]) - bm.SetWord(1, w1) + words[1] = w1 next += 8 if w1&maskDE65 != 0 && maxLevels >= 3 { if next+8 > len(src) { - return bm, 0, errTruncated + return Bitmap{}, 0, errTruncated } - bm.SetWord(2, binary.BigEndian.Uint64(src[next:])) + words[2] = binary.BigEndian.Uint64(src[next:]) next += 8 } } - return bm, next, nil + return BitmapFromWords(words), next, nil } func (binBitmap) WriteBitmap(dst []byte, bm Bitmap, maxLevels int) ([]byte, error) { diff --git a/iso8583/message.go b/iso8583/message.go index 6232ed5..5ebfd76 100644 --- a/iso8583/message.go +++ b/iso8583/message.go @@ -181,9 +181,9 @@ func NewTLV(sub *Schema) *Message { return &Message{schema: sub, tags: make(map[string]tagSlot), owned: true} } -// PutTag stores a decoded canonical value under a BER-TLV tag (canonicalised to +// SetTag stores a decoded canonical value under a BER-TLV tag (canonicalised to // uppercase), preserving first-seen order for deterministic re-encoding. -func (m *Message) PutTag(tag string, v Value) { +func (m *Message) SetTag(tag string, v Value) { key := strings.ToUpper(tag) if m.tags == nil { m.tags = make(map[string]tagSlot) @@ -303,8 +303,8 @@ func (m *Message) Set(de int, v any) error { s.fromSrc = false m.owned = true if de >= 1 { - m.bm.Set(de) - m.dirty.Set(de) + m.bm.set(de) + m.dirty.set(de) } return nil } @@ -335,7 +335,7 @@ func (m *Message) SetP(p FieldPath, v any) error { if tagDef != nil { val.codec = tagDef.Codec } - m.PutTag(tag, val) + m.SetTag(tag, val) m.owned = true return nil } @@ -392,8 +392,8 @@ func (m *Message) ensureChild(de int, def *FieldDef) (*Message, error) { s.fromSrc = false m.owned = true if de >= 1 { - m.bm.Set(de) - m.dirty.Set(de) + m.bm.set(de) + m.dirty.set(de) } return child, nil } @@ -418,7 +418,7 @@ func (m *Message) Unset(de int) { } *s = slot{} if de >= 1 { - m.bm.Clear(de) + m.bm.clear(de) } m.owned = true }