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