Skip to main content

ISOUtil: Byte Manipulation Helpers

ISOUtil is the Swiss Army knife of jPOS low-level byte manipulation. It lives in org.jpos.iso.ISOUtil and is a static utility class — no instantiation, all static methods.

BCD encoding and decoding​

Binary Coded Decimal (BCD) packs two decimal digits into a single byte. The high nibble holds the first digit, the low nibble holds the second. So the string "1234" becomes 0x12 0x34.

str2bcd​

// Convert decimal string to BCD bytes
byte[] bcd = ISOUtil.str2bcd("1234", false);
// Result: [0x12, 0x34]

byte[] bcd = ISOUtil.str2bcd("123", false); // odd length, right-padded
// Result: [0x12, 0x30]

byte[] bcd = ISOUtil.str2bcd("123", true); // odd length, left-padded
// Result: [0x01, 0x23]

The padLeft flag controls which end gets the zero nibble when the digit count is odd. IFB_NUMERIC with pad=false uses right-padding; pad=true uses left-padding.

// In-place variant — fills existing array at a given offset
// (used internally by ISOStringFieldPackager to avoid extra allocations)
ISOUtil.str2bcd("9900", false, buffer, 4);

bcd2str​

// Convert BCD bytes back to a decimal string
String s = ISOUtil.bcd2str(new byte[]{0x12, 0x34}, 0, 4, false);
// Result: "1234"

String s = ISOUtil.bcd2str(new byte[]{0x01, 0x23}, 0, 3, true);
// Result: "123" (left-padded BCD, 3 significant digits)

Parameters: (byte[] b, int offset, int numDigits, boolean padLeft).

Hex encoding and decoding​

hexString​

// byte[] → hex string (uppercase)
String hex = ISOUtil.hexString(new byte[]{0x1A, 0x2B, 0x3C});
// Result: "1A2B3C"

// Subset
String hex = ISOUtil.hexString(data, 4, 8); // 8 bytes starting at offset 4

hexString is the most-used helper in log output. Every ISOBinaryField.dump() and most channel trace output uses it.

hex2byte​

// hex string → byte[]
byte[] data = ISOUtil.hex2byte("1A2B3C");
// Result: [0x1A, 0x2B, 0x3C]

// Odd-length: auto left-pads with zero
byte[] data = ISOUtil.hex2byte("ABC"); // → hex2byte("0ABC")
// Result: [0x0A, 0xBC]

Used everywhere binary data crosses a text boundary — loading keys from config files, parsing track data, preparing MAC computation inputs.

dumpString​

// byte[] → printable form; replaces non-printable bytes with '.'
String s = ISOUtil.dumpString(new byte[]{0x41, 0x00, 0x42});
// Result: "A.B"

Useful for debugging raw channel traffic without triggering encoding errors.

String padding​

// Zero-pad on the left to reach target length
String s = ISOUtil.zeropad("42", 6); // "000042"
String s = ISOUtil.zeropad(42L, 6); // "000042" (long variant)

// Zero-pad on the right
String s = ISOUtil.zeropadRight("42", 6); // "420000"

// Space-pad on the right (for character fields)
String s = ISOUtil.strpad("hi", 6); // "hi "

// Left-pad with an arbitrary character
String s = ISOUtil.padleft("hi", 6, '*'); // "****hi"

// Right-pad with an arbitrary character
String s = ISOUtil.padright("hi", 6, '*'); // "hi****"

Unpadding​

String s = ISOUtil.zeroUnPad("000042");    // "42"
String s = ISOUtil.blankUnPad("hi "); // "hi"
String s = ISOUtil.trim(" hello "); // "hello" (both sides)

trim on a String removes leading and trailing whitespace and non-printable characters:

public static String trim(String s) {
if (s == null) return null;
int start = 0, end = s.length() - 1;
while (start <= end && s.charAt(start) <= ' ') start++;
while (end >= start && s.charAt(end) <= ' ') end--;
return s.substring(start, end + 1);
}

Validation helpers​

boolean ok = ISOUtil.isNumeric("12345", 10);    // true — all decimal digits
boolean ok = ISOUtil.isNumeric("123X5", 10); // false

boolean ok = ISOUtil.isZero("000000"); // true
boolean ok = ISOUtil.isBlank(" "); // true
boolean ok = ISOUtil.isAlphaNumeric("ABC123"); // true

isNumeric(String s, int radix) is more general — pass radix=16 to validate hex strings.

BitSet ↔ byte array​

Used internally by IFB_BITMAP and IFA_BITMAP but also directly useful when manipulating processing codes and response codes as bit fields.

bitSet2byte​

// BitSet (1-based field numbers) → compact byte array
BitSet bs = new BitSet();
bs.set(2); // field 2 present
bs.set(4); // field 4 present
bs.set(11); // field 11 present

byte[] bitmap = ISOUtil.bitSet2byte(bs);
// Produces 8 bytes; bit 0 of each byte = highest-numbered field in that byte's range

The 1-based convention: bs.get(n+1) corresponds to ISO-8583 field n. Bit 0 is unused.

// Force a specific byte count (e.g. always emit 16 bytes for primary + secondary)
byte[] bitmap = ISOUtil.bitSet2byte(bs, 16);

byte2BitSet​

// byte array → BitSet (reverse of above)
BitSet bs = ISOUtil.byte2BitSet(data, 0, 64); // primary bitmap only (64 bits)
BitSet bs = ISOUtil.byte2BitSet(data, 0, 128); // primary + secondary

// Incremental: add secondary to existing BitSet
ISOUtil.byte2BitSet(existing, secondaryBytes, 64); // bits 65-128

hex2BitSet / bitSet2String​

// Parse a hex-ASCII bitmap string into a BitSet
BitSet bs = ISOUtil.hex2BitSet(new byte[]{'7','2','3','4',...}, 0, 64);

// Format a BitSet as hex string (for IFA_BITMAP)
String hex = ISOUtil.bitSet2String(bs); // "7234040000000000" etc.

XOR and byte arithmetic​

Essential for cryptographic operations (PIN block verification, MAC computation, key management).

// Byte-level XOR of two arrays (result length = minimum of the two)
byte[] result = ISOUtil.xor(key1, key2);

// String hex XOR (convenience wrapper)
String result = ISOUtil.hexor("1234ABCD", "FFFF0000");
// result = "EDCBABCD"
// Integer ↔ byte array (big-endian, minimum bytes)
byte[] b = ISOUtil.int2byte(0x0200); // [0x02, 0x00]
byte[] b = ISOUtil.int2byte(42); // [0x2A] (one byte, value fits)
int v = ISOUtil.byte2int(new byte[]{0x02, 0x00}); // 512

// Concatenate two byte arrays
byte[] full = ISOUtil.concat(header, body);

// Slice (trim) a byte array to length bytes
byte[] trimmed = ISOUtil.trim(data, 8);

Amount and display formatting​

// Format a long amount with implied decimal (cents → display)
String s = ISOUtil.formatAmount(100L, 12); // " 1.00"
String s = ISOUtil.formatAmount(12345L, 12); // " 123.45"

The length argument includes the decimal point character. Common for field 4 display in logs.

PAN masking​

protect() masks the middle digits of a PAN or track data, leaving the BIN (first 6) and the last 4 visible:

String masked = ISOUtil.protect("4111111111111111");
// Result: "411111______1111" (default mask char is '_')

String masked = ISOUtil.protect("4111111111111111", '*');
// Result: "411111******1111"

// Track 2 format (BIN=641111, expiry and service code stay masked)
String masked = ISOUtil.protect("4111111111111111=2512101000000000");
// Result: "411111______1111=________________"

Always use protect() before logging anything that might contain card data. jPOS channel and packager logging passes PAN values through protect() automatically in the standard log formatters.

EBCDIC conversion​

For networks using EBCDIC encoding (common in mainframe-connected acquirer systems):

// ASCII String → EBCDIC bytes
byte[] ebcdic = ISOUtil.asciiToEbcdic("HELLO");

// EBCDIC bytes → ASCII String
String ascii = ISOUtil.ebcdicToAscii(ebcdic);

// In-place conversion into existing buffer
ISOUtil.asciiToEbcdic("VALUE", targetBuffer, offset);

The EBCDIC charset used is IBM1047 (the standard EBCDIC code page for US English financial systems), available as ISOUtil.EBCDIC.

normalize()​

// Escape XML special characters (suitable for embedding in log output)
String safe = ISOUtil.normalize("<field>");
// Result: "&lt;field&gt;"

Called by ISOField.dump() to produce safe XML log output. You rarely need to call this directly unless building a custom log formatter.

sleep()​

// Thread sleep that wraps LockSupport.parkNanos() (more precise than Thread.sleep())
ISOUtil.sleep(1000); // 1 second
ISOUtil.sleep(0); // Thread.yield()

Used throughout jPOS wherever a precise sleep is needed (retry loops, heartbeat intervals). Avoids the InterruptedException boilerplate of Thread.sleep().