001/* 002 * jPOS Project [http://jpos.org] 003 * Copyright (C) 2000-2026 jPOS Software SRL 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU Affero General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU Affero General Public License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with this program. If not, see <http://www.gnu.org/licenses/>. 017 */ 018 019package org.jpos.tlv; 020 021import org.jpos.iso.ISOUtil; 022import org.jpos.util.Loggeable; 023 024import java.io.PrintStream; 025import java.io.Serializable; 026import java.math.BigInteger; 027import java.nio.ByteBuffer; 028import java.util.ArrayList; 029import java.util.Collections; 030import java.util.Enumeration; 031import java.util.List; 032import java.util.Objects; 033 034/** 035 * An ordered list of TLV (Tag-Length-Value) records. 036 * @author bharavi 037 */ 038 039public class TLVList implements Serializable, Loggeable { 040 041 private static final long serialVersionUID = 6962311407331957465L; 042 043 /** 044 * Value not used as tag id in accordance with ISO/IEC 7816. 045 */ 046 private static final int SKIP_BYTE1 = 0x00; 047 048 /** 049 * Value not used as tag id in accordance with ISO/IEC 7816. 050 */ 051 private static final int SKIP_BYTE2 = 0xFF; 052 053 private static final int EXT_TAG_MASK = 0x1F; 054 055 private static final int LEN_SIZE_MASK = 0x7F; 056 private static final int EXT_LEN_MASK = 0x80; 057 058 /** Ordered collection of decoded TLV elements. */ 059 private final List<TLVMsg> tags = new ArrayList<>(); 060 061 /** 062 * Enforces fixed tag size. 063 * <p> 064 * Zero means that the tag size will be determined in accordance with 065 * ISO/IEC 7816. 066 */ 067 private int tagSize = 0; 068 069 /** 070 * Enforces fixed length size. 071 * <p> 072 * Zero means that the length size will be determined in accordance with 073 * ISO/IEC 7816. 074 */ 075 private int lengthSize = 0; 076 077 /** Cached tag identifier used by linear search helpers. */ 078 private int tagToFind = -1; 079 /** Cached index of the most recent occurrence found by a search helper. */ 080 private int indexLastOccurrence = -1; 081 082 /** Builder for configured {@link TLVList} instances. */ 083 public static class TLVListBuilder { 084 /** Default constructor; configure tag/length sizes via the fluent setters. */ 085 public TLVListBuilder() {} 086 087 private int tagSize = 0; 088 private int lengthSize = 0; 089 090 /** 091 * Creates instance of TLV engine builder. 092 * 093 * @return instance of TLV builder. 094 */ 095 public static TLVListBuilder createInstance() { 096 return new TLVListBuilder(); 097 } 098 099 /** 100 * Forces a fixed size of tag. 101 * <p> 102 * It disables tag size autodetection according with ISO/IEC 7816-4 103 * BER-TLV. 104 * 105 * @param tagSize The size of tag in bytes 106 * @return TLVList builder with fixed tag size 107 */ 108 public TLVListBuilder fixedTagSize(int tagSize) { 109 if (tagSize <= 0) 110 throw new IllegalArgumentException("The fixed tag size must be greater than zero"); 111 112 this.tagSize = tagSize; 113 return this; 114 } 115 116 /** 117 * Forces a fixed size of length. 118 * <p> 119 * It disables length size autodetection according with ISO/IEC 7816-4 120 * BER-TLV. 121 * 122 * @param lengthSize The size of length in bytes <i>(1 - 4)</i> 123 * @return TLVList builder with fixed length size 124 */ 125 public TLVListBuilder fixedLengthSize(int lengthSize) { 126 if (lengthSize <= 0) 127 throw new IllegalArgumentException("The fixed length size must be greater than zero"); 128 129 if (lengthSize > 4) 130 throw new IllegalArgumentException("The fixed length size must be greater than zero"); 131 132 this.lengthSize = lengthSize; 133 return this; 134 } 135 136 /** 137 * Build TLV engine. 138 * 139 * @return configured TLV engine 140 */ 141 public TLVList build() { 142 TLVList tl = new TLVList(); 143 tl.tagSize = tagSize; 144 tl.lengthSize = lengthSize; 145 return tl; 146 } 147 148 } 149 150 /** 151 * Creates instance of TLV engine. 152 * <p> 153 * It is a shorter form of: 154 * <pre>{@code 155 * TLVListBuilder.createInstance().build(); 156 * }</pre> 157 * 158 */ 159 public TLVList() { 160 super(); 161 } 162 163 /** 164 * Unpacks a TLV-encoded message. 165 * 166 * @param buf raw message 167 * @throws IllegalArgumentException if the buffer contains an invalid TLV structure 168 */ 169 public void unpack(byte[] buf) throws IllegalArgumentException { 170 unpack(buf, 0); 171 } 172 173 /** 174 * Returns the decoded tags in insertion order. 175 * 176 * @return a list of tags 177 */ 178 public List<TLVMsg> getTags() { 179 return tags; 180 } 181 182 /** 183 * Returns the decoded tags as an enumeration. 184 * 185 * @return an enumeration of the list of tags 186 */ 187 public Enumeration<TLVMsg> elements() { 188 return Collections.enumeration(tags); 189 } 190 191 /** 192 * Unpacks a TLV-encoded message starting at the provided offset. 193 * 194 * @param buf raw message 195 * @param offset the offset 196 * @throws IndexOutOfBoundsException if {@code offset} exceeds {@code buf.length} 197 * @throws IllegalArgumentException if the buffer contains an invalid TLV structure 198 */ 199 public void unpack(byte[] buf, int offset) throws IllegalArgumentException 200 , IndexOutOfBoundsException { 201 ByteBuffer buffer = ByteBuffer.wrap(buf, offset, buf.length - offset); 202 TLVMsg currentNode; 203 while (buffer.hasRemaining()) { 204 currentNode = getTLVMsg(buffer); // null is returned if no tag found (trailing padding) 205 if (currentNode != null) 206 append(currentNode); 207 } 208 } 209 210 /** 211 * Append TLVMsg to the TLV list. 212 * 213 * @param tlv the TLV message 214 * @throws NullPointerException if {@code tlv} is {@code null} 215 */ 216 public void append(TLVMsg tlv) throws NullPointerException { 217 Objects.requireNonNull(tlv, "TLV message cannot be null"); 218 219 tags.add(tlv); 220 } 221 222 /** 223 * Append TLVMsg to the TLVList. 224 * 225 * @param tag tag id 226 * @param value tag value 227 * @return the TLV list instance 228 * @throws IllegalArgumentException when contains tag with illegal id 229 */ 230 public TLVList append(int tag, byte[] value) throws IllegalArgumentException { 231 append(createTLVMsg(tag, value)); 232 return this; 233 } 234 235 /** 236 * Append TLVMsg to the TLVList. 237 * 238 * @param tag id 239 * @param value in hexadecimal character representation 240 * @return the TLV list instance 241 * @throws IllegalArgumentException when contains tag with illegal id 242 */ 243 public TLVList append(int tag, String value) throws IllegalArgumentException { 244 append(createTLVMsg(tag, ISOUtil.hex2byte(value))); 245 return this; 246 } 247 248 /** 249 * delete the specified TLV from the list using a Zero based index 250 * @param index number 251 */ 252 public void deleteByIndex(int index) { 253 tags.remove(index); 254 } 255 256 /** 257 * Delete the specified TLV from the list by tag value 258 * @param tag id 259 */ 260 public void deleteByTag(int tag) { 261 List<TLVMsg> t = new ArrayList<>(); 262 for (TLVMsg tlv2 : tags) { 263 if (tlv2.getTag() == tag) 264 t.add(tlv2); 265 } 266 tags.removeAll(t); 267 } 268 269 /** 270 * Searches the list for a specified tag and returns a TLV object. 271 * 272 * @param tag id 273 * @return TLV message 274 */ 275 public TLVMsg find(int tag) { 276 tagToFind = tag; 277 for (TLVMsg tlv : tags) { 278 if (tlv.getTag() == tag) { 279 indexLastOccurrence = tags.indexOf(tlv); 280 return tlv; 281 } 282 } 283 indexLastOccurrence = -1; 284 return null; 285 } 286 287 /** 288 * Searches the list for a specified tag and returns a zero based index for 289 * that tag. 290 * 291 * @param tag tag identifier 292 * @return index for a given {@code tag} 293 */ 294 public int findIndex(int tag) { 295 tagToFind = tag; 296 for (TLVMsg tlv : tags) { 297 if (tlv.getTag() == tag) { 298 indexLastOccurrence = tags.indexOf(tlv); 299 return indexLastOccurrence; 300 } 301 } 302 indexLastOccurrence = -1; 303 return -1; 304 } 305 306 /** 307 * Return the next TLVMsg of same TAG value. 308 * 309 * @return TLV message or {@code null} if not found. 310 * @throws IllegalStateException when the search has not been initiated 311 */ 312 public TLVMsg findNextTLV() throws IllegalStateException { 313 if (tagToFind < 0) 314 throw new IllegalStateException( 315 "The initialization of the searched tag is required" 316 ); 317 for ( int i=indexLastOccurrence + 1 ; i < tags.size(); i++) { 318 if (tags.get(i).getTag() == tagToFind) { 319 indexLastOccurrence = i; 320 return tags.get(i); 321 } 322 } 323 return null; 324 } 325 326 /** 327 * Returns a {@code TLVMsg} instance stored within the {@code TLVList} at 328 * the given {@code index}. 329 * 330 * @param index zero based index of TLV message 331 * @return TLV message instance 332 * @throws IndexOutOfBoundsException if the index is out of range 333 * (index < 0 || index >= size()) 334 */ 335 public TLVMsg index(int index) throws IndexOutOfBoundsException { 336 return tags.get(index); 337 } 338 339 /** 340 * Pack the TLV message (BER-TLV Encoding). 341 * 342 * @return the packed message 343 */ 344 public byte[] pack() { 345 ByteBuffer buffer = ByteBuffer.allocate(516); 346 for (TLVMsg tlv : tags) 347 buffer.put(tlv.getTLV()); 348 byte[] b = new byte[buffer.position()]; 349 buffer.flip(); 350 buffer.get(b); 351 return b; 352 } 353 354 private boolean isExtTagByte(int b) { 355 return (b & EXT_TAG_MASK) == EXT_TAG_MASK; 356 } 357 358 /** 359 * Read next TLV Message from stream and return it. 360 * 361 * @param buffer the buffer 362 * @return TLVMsg 363 * @throws IllegalArgumentException 364 */ 365 private TLVMsg getTLVMsg(ByteBuffer buffer) throws IllegalArgumentException { 366 int tag = getTAG(buffer); // tag id 0x00 if tag not found 367 if (tagSize == 0 && tag == SKIP_BYTE1) 368 return null; 369 370 // Get Length if buffer remains! 371 if (!buffer.hasRemaining()) 372 throw new IllegalArgumentException(String.format("BAD TLV FORMAT: tag (%x)" 373 + " without length or value",tag) 374 ); 375 int length = getValueLength(buffer); 376 if (length > buffer.remaining()) 377 throw new IllegalArgumentException(String.format("BAD TLV FORMAT: tag (%x)" 378 + " length (%d) exceeds available data", tag, length) 379 ); 380 byte[] arrValue = new byte[length]; 381 buffer.get(arrValue); 382 383 return createTLVMsg(tag, arrValue); 384 } 385 386 /** 387 * Create TLV message instance. 388 * 389 * @apiNote The protected scope is intended to not promote the use of TLVMsg 390 * outside. 391 * 392 * @param tag tag identifier 393 * @param value the value of tag 394 * @return TLV message instance 395 * @throws IllegalArgumentException when contains tag with illegal id 396 */ 397 protected TLVMsg createTLVMsg(int tag, byte[] value) throws IllegalArgumentException { 398 return new TLVMsg(tag, value, tagSize, lengthSize); 399 } 400 401 /** 402 * Skip padding bytes of TLV message. 403 * <p> 404 * ISO/IEC 7816 uses neither ’00’ nor ‘FF’ as tag value. 405 * 406 * @param buffer sequence of TLV data bytes 407 */ 408 private void skipBytes(ByteBuffer buffer) { 409 buffer.mark(); 410 int b; 411 do { 412 if (!buffer.hasRemaining()) 413 break; 414 415 buffer.mark(); 416 b = buffer.get() & 0xff; 417 } while (b == SKIP_BYTE1 || b == SKIP_BYTE2); 418 buffer.reset(); 419 } 420 421 private int readTagID(ByteBuffer buffer) throws IllegalArgumentException { 422 // Get first byte of Tag Identifier 423 int b = buffer.get() & 0xff; 424 int tag = b; 425 if (isExtTagByte(b)) { 426 // Get rest of Tag identifier 427 do { 428 tag <<= 8; 429 if (buffer.remaining() < 1) 430 throw new IllegalArgumentException("BAD TLV FORMAT: encoded tag id is too short"); 431 432 b = buffer.get() & 0xff; 433 tag |= b; 434 } while ((b & EXT_LEN_MASK) == EXT_LEN_MASK); 435 } 436 return tag; 437 } 438 439 /** 440 * Return the next Tag identifier. 441 * 442 * @param buffer contains TLV data 443 * @return tag identifier 444 * @throws IllegalArgumentException 445 */ 446 private int getTAG(ByteBuffer buffer) throws IllegalArgumentException { 447 if (tagSize > 0) 448 return bytesToInt(readBytes(buffer, tagSize)); 449 450 skipBytes(buffer); 451 return readTagID(buffer); 452 } 453 454 /** 455 * Read length bytes and return the int value 456 * @param buffer buffer 457 * @return value length 458 * @throws IllegalArgumentException if the encoded length cannot be parsed 459 */ 460 protected int getValueLength(ByteBuffer buffer) throws IllegalArgumentException { 461 if (lengthSize > 0) { 462 byte[] bb = readBytes(buffer, lengthSize); 463 return bytesToInt(bb); 464 } 465 466 byte b = buffer.get(); 467 int count = b & LEN_SIZE_MASK; 468 // check first byte for more bytes to follow 469 if ((b & EXT_LEN_MASK) == 0 || count == 0) 470 return count; 471 472 //fetch rest of bytes 473 byte[] bb = readBytes(buffer, count); 474 return bytesToInt(bb); 475 } 476 477 private int bytesToInt(byte[] bb){ 478 //adjust buffer if first bit is turn on 479 //important for BigInteger reprsentation 480 if ((bb[0] & 0x80) > 0) 481 bb = ISOUtil.concat(new byte[1], bb); 482 483 return new BigInteger(bb).intValue(); 484 } 485 486 private byte[] readBytes(ByteBuffer buffer, int length) throws IllegalArgumentException { 487 if (length > buffer.remaining()) 488 throw new IllegalArgumentException( 489 String.format("BAD TLV FORMAT: (%d) remaining bytes are not" 490 + " enough to get tag id of length (%d)" 491 , buffer.remaining(), length 492 ) 493 ); 494 byte[] bb = new byte[length]; 495 buffer.get(bb); 496 return bb; 497 } 498 499 /** 500 * searches the list for a specified tag and returns a hex String 501 * @param tag id 502 * @return hexString 503 */ 504 public String getString(int tag) { 505 TLVMsg msg = find(tag); 506 if (msg == null) 507 return null; 508 509 return msg.getStringValue(); 510 } 511 512 /** 513 * searches the list for a specified tag and returns it raw 514 * @param tag id 515 * @return byte[] 516 */ 517 public byte[] getValue(int tag) { 518 TLVMsg msg = find(tag); 519 if (msg == null) 520 return null; 521 522 return msg.getValue(); 523 } 524 525 /** 526 * Indicates if TLV measege with passed {@code tag} is on list. 527 * 528 * @param tag tag identifier 529 * @return {@code true} if tag contains on list, {@code false} otherwise 530 */ 531 public boolean hasTag(int tag) { 532 return findIndex(tag) > -1; 533 } 534 535 @Override 536 public void dump(PrintStream p, String indent) { 537 String inner = indent + " "; 538 p.println(indent + "<tlvlist>"); 539 for (TLVMsg msg : getTags()) 540 msg.dump(p, inner); 541 p.println(indent + "</tlvlist>"); 542 } 543 544}