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 &lt; 0 || index &gt;= 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}