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 java.io.PrintStream;
022import java.math.BigInteger;
023import java.util.Arrays;
024import org.jpos.iso.ISOUtil;
025import org.jpos.util.Loggeable;
026
027/**
028 * A single TLV (Tag-Length-Value) message element.
029 * @author bharavi
030 */
031public class TLVMsg implements Loggeable {
032
033    /**
034     * Value not used as tag id in accordance with ISO/IEC 7816.
035     */
036    private static final int SKIP_BYTE1     = 0x00;
037
038    /**
039     * Value not used as tag id in accordance with ISO/IEC 7816.
040     */
041    private static final int SKIP_BYTE2     = 0xFF;
042
043    private static final int EXT_TAG_MASK   = 0x1F;
044
045    /**
046     * Enforces fixed tag size.
047     * <p>
048     * Zero means that the tag size will be determined in accordance with
049     * ISO/IEC 7816.
050     */
051    private final int tagSize;
052
053    /**
054     * Enforces fixed length size.
055     * <p>
056     * Zero means that the length size will be determined in accordance with
057     * ISO/IEC 7816.
058     */
059    private final int lengthSize;
060
061    private final int tag;
062    private final byte[] value;
063
064    /**
065     * Constructs a TLV message from tag and value.
066     *
067     * @param tag id
068     * @param value tag value
069     * @deprecated In most cases, a message is created to attach it to the list.
070     * <br>
071     * It can be done by:
072     * <pre>{@code
073     *   TLVList tl = ...;
074     *   tl.append(tag, value);
075     * }</pre>
076     * If for some reason this is not possible then a message can be created:
077     * <pre>{@code
078     *   TLVList tl = TLVListBuilder.createInstance().build(); // or just new TLVList();
079     *   tl.append(tag, value);
080     *   TLVMsg tm = tl.find(tag);
081     * }</pre>
082     * The intention is to not promote the use of TLVMsg outside. Due to
083     * the lack of compatibility of various TLV types at TLVList.append(TLVMsg)
084     */
085    @Deprecated
086    public TLVMsg(int tag, byte[] value) throws IllegalArgumentException {
087        this(tag, value, 0, 0);
088    }
089
090    /**
091     * Constructs a TLV message using explicit tag and length sizes.
092     *
093     * @param tag the TLV tag identifier
094     * @param value the TLV value bytes
095     * @param tagSize fixed tag size, or {@code 0} for auto-detection
096     * @param lengthSize fixed length size, or {@code 0} for auto-detection
097     * @throws IllegalArgumentException if the supplied tag or value is invalid
098     */
099    protected TLVMsg(int tag, byte[] value, int tagSize, int lengthSize)
100            throws IllegalArgumentException {
101        this.tag = tag;
102        this.value = value;
103        this.tagSize = tagSize;
104        this.lengthSize = lengthSize;
105
106        if (tagSize == 0)
107            verifyTag(tag);
108        else
109            verifyTagLength(tag);
110
111        if (lengthSize > 0)
112            verifyValue(value);
113
114    }
115
116    private boolean isExtTagByte(int b) {
117        return (b & EXT_TAG_MASK) == EXT_TAG_MASK;
118    }
119
120    /**
121     * Verify tag identifier.
122     * <p>
123     * Tag number in accordance with ISO/IEC 7816
124     * <ol>
125     *   <li>The tag field consists of one or more consecutive bytes. It is a number.
126     *   <li>ISO/IEC 7816 uses neither ’00’ nor ‘FF’ as tag value.
127     *   <li>If the bits B5-B1 of the leading byte are not all set to 1, then
128     *       may they shall encode an integer equal to the tag number. Then the
129     *       tag field consists of a single byte.
130     *       Otherwise <i>(B5-B1 set to 1 in the leading byte)</i>, the tag
131     *       field shall continue on one or more subsequent bytes.
132     *   </li>
133     * </ol>
134     *
135     * See <a href="http://cardwerk.com/iso7816-4-annex-d-use-of-basic-encoding-rules-asn-1/#AnnexD_2">ISO 7816-4 Annex D.2: Tag field</a>
136     *
137     * @param tag tag identifier
138     * @throws IllegalArgumentException if tag identifier is zero or less or
139     * it is included in the illegal ranges.
140     */
141    private void verifyTag(int tag) throws IllegalArgumentException {
142        if (tag <= 0)
143            throw new IllegalArgumentException("Tag id must be greater than zero");
144
145        BigInteger bi = BigInteger.valueOf(tag);
146        byte[] ba = bi.toByteArray();
147
148        if (ba[0] == 0x00) {
149            // strip byte array if starts with 0x00
150            ba = Arrays.copyOfRange(ba, 1, ba.length);
151        }
152
153        int idx = 0;
154        do {
155            boolean byteFollows = (idx == 0 && isExtTagByte(ba[idx])) || (idx > 0 && (ba[idx] & 0x80) == 0x80);
156            if ((ba[idx] & 0xff) == SKIP_BYTE1) {
157                throw new IllegalArgumentException("Tag id: 0x" + Integer.toString(tag, 0x10).toUpperCase()
158                        + " cannot contain in any 0x00 byte"
159                );
160            } else if ((ba[idx] & 0xff) == SKIP_BYTE2) {
161                throw new IllegalArgumentException("Tag id: 0x" + Integer.toString(tag, 0x10).toUpperCase()
162                        + " cannot contain in any 0xff byte"
163                );
164            } else if (byteFollows && ba.length <= idx + 1) {
165                throw new IllegalArgumentException("Tag id: 0x" + Integer.toString(tag, 0x10).toUpperCase()
166                        + " shall contain subsequent byte"
167                );
168            } else if (!byteFollows && idx+1 < ba.length) {
169                throw new IllegalArgumentException("Tag id: 0x" + Integer.toString(tag, 0x10).toUpperCase()
170                        + " cannot contain subsequent byte"
171                );
172            }
173            idx++;
174        } while (idx < ba.length);
175    }
176
177    private void verifyTagLength(int tag) throws IllegalArgumentException {
178        if (tag < 0)
179            throw new IllegalArgumentException("The tag id must be greater than or equals zero");
180
181        int maxTag = 1 << (tagSize << 3);
182        maxTag -= 1;
183        if (tag > maxTag)
184            throw new IllegalArgumentException("The tag id cannot be greater that: " + maxTag);
185    }
186
187    private void verifyValue(byte[] value) throws IllegalArgumentException {
188        if (value == null)
189            return;
190
191        int maxLength = 1 << (lengthSize << 3);
192        maxLength -= 1;
193        if (value.length > maxLength)
194            throw new IllegalArgumentException("The tag value length cannot exceed: " + maxLength);
195    }
196
197    /**
198     * Returns the TLV tag identifier.
199     *
200     * @return tag
201     */
202    public int getTag() {
203        return tag;
204    }
205
206    /**
207     * Returns the TLV value bytes.
208     *
209     * @return tag value
210     */
211    public byte[] getValue() {
212        return value;
213    }
214
215    /**
216     * Returns the encoded TLV message bytes.
217     *
218     * @return tag + length + value of the TLV message
219     */
220    public byte[] getTLV() {
221        String hexTag = Integer.toHexString(tag);
222        byte[] bTag = ISOUtil.hex2byte(hexTag);
223        if (tagSize > 0)
224            bTag = fitInArray(bTag, tagSize);
225
226        byte[] bLen = getL();
227        byte[] bVal = getValue();
228        if (bVal == null)
229            //Value can be null
230            bVal = new byte[0];
231
232        int tLength = bTag.length + bLen.length + bVal.length;
233        byte[] out = new byte[tLength];
234        System.arraycopy(bTag, 0, out, 0, bTag.length);
235        System.arraycopy(bLen, 0, out, bTag.length, bLen.length);
236        System.arraycopy(bVal, 0, out, bTag.length + bLen.length,
237                bVal.length
238        );
239        return out;
240    }
241
242    private byte[] fitInArray(byte[] bytes, int length) {
243        byte[] ret = new byte[length];
244        if (bytes.length <= length)
245            // copy bytes at end of ret
246            System.arraycopy(bytes, 0, ret, ret.length - bytes.length, bytes.length);
247        else
248            // copy last tagSize bytes of bytes
249            System.arraycopy(bytes, bytes.length - ret.length, ret, 0, ret.length);
250        return ret;
251    }
252
253    private byte[] getLengthArray() {
254        int length = 0;
255        if (value != null)
256            length = value.length;
257
258        byte[] ret = BigInteger.valueOf(length).toByteArray();
259        return fitInArray(ret, lengthSize);
260    }
261
262    /**
263     * Value up to 127 can be encoded in single byte and multiple bytes are
264     * required for length bigger than 127
265     *
266     * @return encoded length
267     */
268    public byte[] getL() {
269
270        if (lengthSize > 0)
271            return getLengthArray();
272
273        if (value == null)
274            return new byte[1];
275
276        // if Length is less than 128
277        // set the 8bit as 0 indicating next 7 bits is the length
278        // of the message
279        // if length is more than 127 then, set the first bit as 1 indicating
280        // next 7 bits will indicate the length of following bytes used for
281        // length
282
283        BigInteger bi = BigInteger.valueOf(value.length);
284        /* Value to be encoded on multiple bytes */
285        byte[] rBytes = bi.toByteArray();
286        /* If value can be encoded on one byte */
287        if (value.length < 0x80)
288            return rBytes;
289
290        //we need 1 byte to indicate the length
291        //for that is used sign byte (first 8-bits equals 0),
292        //if it is not present it is added
293        if (rBytes[0] > 0)
294            rBytes = ISOUtil.concat(new byte[1], rBytes);
295        rBytes[0] = (byte) (0x80 | rBytes.length - 1);
296
297        return rBytes;
298    }
299
300    /**
301     * Returns the TLV value as a hexadecimal string.
302     *
303     * @return value
304     */
305    public String getStringValue() {
306        return ISOUtil.hexString(value);
307    }
308
309    @Override
310    public String toString(){
311        String t = Integer.toHexString(tag);
312        if (t.length() % 2 > 0)
313            t = "0" + t;
314        return String.format("[tag: 0x%s, %s]", t
315                ,value == null ? null : getStringValue()
316        );
317    }
318
319    @Override
320    public void dump(PrintStream p, String indent) {
321        p.print(indent);
322        p.print("<tag id='");
323        p.print(Integer.toHexString(getTag()));
324        p.print("' value='");
325        p.print(ISOUtil.hexString(getValue()));
326        p.println("' />");
327    }
328
329    private boolean isPrimitive (byte firstByte) {
330        return (firstByte & 0x20) == 0x00;
331    }
332
333}