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
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,35 @@ 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.
- **Docs:** corrected the `CoralPay` / `Zone` profile descriptions to state their
actual provenance — clean-room layouts composed from public ISO 8583:1987 field
semantics and the acquirers' published field tables — replacing earlier wording
that implied derivation from a jPOS packager definition. No code change.

### 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 —
Expand Down
10 changes: 5 additions & 5 deletions fieldcodec/bitmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
8 changes: 2 additions & 6 deletions fieldcodec/fieldcodec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion fieldcodec/tlv/bertlv.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
29 changes: 21 additions & 8 deletions iso8583/bitmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,18 @@ 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
}
w, mask := wordBit(de)
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
}
Expand All @@ -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.
Expand Down
18 changes: 9 additions & 9 deletions iso8583/bitmap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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")
}
Expand All @@ -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 {
Expand All @@ -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())
}
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion iso8583/codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
16 changes: 8 additions & 8 deletions iso8583/codecs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
16 changes: 8 additions & 8 deletions iso8583/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down
Loading