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