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}