Bitmaps
The bitmap is ISO-8583's field-presence indicator. Instead of encoding field tags in the wire stream (as TLV formats do), ISO-8583 uses a compact bit array: bit N set means field N is present in the message body that follows.
The three-tier bitmap
| Tier | Bit range | Size | Trigger |
|---|---|---|---|
| Primary | 1–64 | 8 bytes | Always present |
| Secondary | 65–128 | 8 bytes | Present when bit 1 of primary is set |
| Tertiary | 129–192 | 8 bytes | Present when bit 65 of secondary is set |
Bit 1 and bit 65 are therefore flag bits. Setting bit 1 means "read 8 more bytes for the secondary bitmap". Setting bit 65 means "read 8 more bytes for the tertiary bitmap". The vast majority of production messages use only the primary + secondary (16 bytes total).
Primary bitmap (8 bytes):
Bit: 1 2 3 4 5 6 7 8 | 9 10 11 ...
↑
Bit 1 set = secondary bitmap follows
Secondary bitmap (8 bytes), read when bit 1 is set:
Bit: 65 66 67 68 ... | ...
↑
Bit 65 set = tertiary bitmap follows
How jPOS represents the bitmap
ISOBitMap and field -1
The bitmap is stored inside ISOMsg.fields under key Integer(-1):
ISOBitMap bmap = new ISOBitMap(-1, bitSet);
msg.set(bmap); // stores at key -1
The −1 key ensures the bitmap is invisible to application code iterating data fields (1–192) but always accessible to the packager.
java.util.BitSet indexing
BitSet uses 0-based indices. jPOS stores bits 1-based (to match ISO-8583 field numbers), so bit 0 of the BitSet is always unused. This is documented with the +1 offsets in ISOUtil.bitSet2byte() and byte2BitSet().
// In ISOUtil.bitSet2byte():
for (int i = 0; i < len; i++)
if (b.get(i + 1)) // +1: skip unused bit 0
d[i >> 3] |= 0x80 >> i % 8;
So BitSet.get(2) is true when ISO field 2 (PAN) is present.
recalcBitMap()
ISOMsg.recalcBitMap() rebuilds the ISOBitMap from the currently set fields:
public void recalcBitMap() throws ISOException {
if (!dirty) return;
int mf = Math.min(getMaxField(), 192);
BitSet bmap = new BitSet(mf + 62 >> 6 << 6); // rounds up to 64-bit boundary
for (int i = 1; i <= mf; i++)
if (fields.get(i) != null)
bmap.set(i); // 1-based
set(new ISOBitMap(-1, bmap));
dirty = false;
}
ISOBasePackager.pack() calls recalcBitMap() (via the message's pack() which delegates to the packager) before encoding. You never need to call it manually.
The dirty flag is set whenever ISOMsg.set() or unset() is called, so recalcBitMap() only re-computes when the field map has actually changed.
ISOBitMapPackager hierarchy
ISOBitMapPackager is a special abstract subclass of ISOFieldPackager. Packager code uses instanceof ISOBitMapPackager to detect that field 1 is a bitmap and activate the full ISO-8583 packing logic, rather than treating it as a plain data field.
// In ISOBasePackager:
protected boolean emitBitMap() {
return fld[1] instanceof ISOBitMapPackager;
}
IFB_BITMAP — binary bitmap
public class IFB_BITMAP extends ISOBitMapPackager {
// pack: convert BitSet → raw bytes
public byte[] pack(ISOComponent c) throws ISOException {
BitSet b = (BitSet) c.getValue();
int len = getLength() >= 8
? b.length() + 62 >> 6 << 3 // ← minimum bytes needed (8-byte aligned)
: getLength();
return ISOUtil.bitSet2byte(b, len);
}
// unpack from byte[]: read 8 bytes, expand if bit 1 or bit 65 set
public int unpack(ISOComponent c, byte[] b, int offset) throws ISOException {
BitSet bmap = ISOUtil.byte2BitSet(b, offset, getLength() << 3);
c.setValue(bmap);
int len = bmap.get(1) ? 128 : 64;
if (getLength() > 16 && bmap.get(1) && bmap.get(65)) len = 192;
return Math.min(getLength(), len >> 3);
}
// unpack from stream: adaptive read, no need to know message length
public void unpack(ISOComponent c, InputStream in) throws IOException, ISOException {
BitSet bmap = ISOUtil.byte2BitSet(new BitSet(64), readBytes(in, 8), 0);
if (getLength() > 8 && bmap.get(1))
ISOUtil.byte2BitSet(bmap, readBytes(in, 8), 64);
if (getLength() > 16 && bmap.get(65))
ISOUtil.byte2BitSet(bmap, readBytes(in, 8), 128);
c.setValue(bmap);
}
}
The stream variant is the right one for channel I/O. It reads exactly as many bytes as the bitmap presence bits indicate — no look-ahead, no message framing required.
The pack size formula
b.length() + 62 >> 6 << 3
This rounds up b.length() (the highest set bit, 1-based) to the next multiple of 64, then converts bits to bytes (divide by 8):
| Highest set bit | b.length() | Formula result | Bytes emitted |
|---|---|---|---|
| ≤ 64 | ≤ 64 | 8 | 8 bytes (primary only) |
| ≤ 128 | ≤ 128 | 16 | 16 bytes (primary + secondary) |
| ≤ 192 | ≤ 192 | 24 | 24 bytes (primary + secondary + tertiary) |
The +62 compensates for the 1-based BitSet offset before the rounding logic.
IFA_BITMAP — ASCII hex bitmap
Used in ASCII message formats where the bitmap must be human-readable. A binary 8-byte bitmap becomes a 16-character hex string on the wire:
Binary: 0x72 0x34 0x04 ...
ASCII: "7234040..." (2 hex chars per byte)
Configured with length="16" for primary, "32" for primary+secondary.
Configuring bitmap size
In a GenericPackager XML:
<!-- Binary bitmap: 16 bytes = primary + secondary -->
<isofield id="1" length="16" name="Bitmap" class="org.jpos.iso.IFB_BITMAP"/>
<!-- ASCII hex bitmap: 32 chars = primary + secondary -->
<isofield id="1" length="32" name="Bitmap" class="org.jpos.iso.IFA_BITMAP"/>
The length attribute sets getLength(). For IFB_BITMAP, getLength() is in bytes. For IFA_BITMAP, it's in hex characters (2× the byte count). The packager uses getLength() as the upper bound when reading, and the actual set bits determine how many bytes are emitted during pack.
Bitmap in the log
ISOMsg.dump() shows the bitmap field (key -1) when printing the full message. In normal Q2 log output you see each field by number but the bitmap is implied. If you enable it explicitly in the dump condition, you get:
<isomsg>
<field id="0" value="0200"/>
<!-- field id="-1" type="bitmap" value="{2, 3, 4, 7, 11, 41, 42}" -->
<field id="2" value="4111111111111111"/>
<field id="3" value="000000"/>
<field id="4" value="000000001000"/>
...
</isomsg>
The {2, 3, 4, 7, 11, 41, 42} is Java BitSet.toString() output — the set bit indices, 1-based.