1/*
2 *  Licensed to the Apache Software Foundation (ASF) under one or more
3 *  contributor license agreements.  See the NOTICE file distributed with
4 *  this work for additional information regarding copyright ownership.
5 *  The ASF licenses this file to You under the Apache License, Version 2.0
6 *  (the "License"); you may not use this file except in compliance with
7 *  the License.  You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *  Unless required by applicable law or agreed to in writing, software
12 *  distributed under the License is distributed on an "AS IS" BASIS,
13 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *  See the License for the specific language governing permissions and
15 *  limitations under the License.
16 */
17
18/**
19* @author Alexander V. Esin
20* @version $Revision$
21*/
22
23package org.apache.harmony.security.x501;
24
25import java.io.IOException;
26import org.apache.harmony.security.asn1.ASN1StringType;
27import org.apache.harmony.security.asn1.DerInputStream;
28
29/**
30 * X.501 Attribute Value
31 */
32public final class AttributeValue {
33
34    public final boolean wasEncoded;
35
36    public String escapedString;
37
38    private String hexString;
39
40    private int tag = -1;
41
42    public byte[] encoded;
43
44    public byte[] bytes; //FIXME remove??? bytes to be encoded
45
46    public boolean hasQE; // raw string contains '"' or '\'
47
48    public String rawString;
49
50    public AttributeValue(String parsedString, boolean hasQorE) {
51        wasEncoded = false;
52
53        this.hasQE = hasQorE;
54
55        this.rawString = parsedString;
56        this.escapedString = makeEscaped(rawString);
57    }
58
59    public AttributeValue(String hexString, byte[] encoded) {
60        wasEncoded = true;
61
62        this.hexString = hexString;
63        this.encoded = encoded;
64
65        try {
66            DerInputStream in = new DerInputStream(encoded);
67
68            tag = in.tag;
69
70            if (DirectoryString.ASN1.checkTag(tag)) {
71                // has string representation
72                this.rawString = (String) DirectoryString.ASN1.decode(in);
73                this.escapedString = makeEscaped(rawString);
74            } else {
75                this.rawString = hexString;
76                this.escapedString = hexString;
77            }
78        } catch (IOException e) {
79            IllegalArgumentException iae = new IllegalArgumentException(); //FIXME message
80            iae.initCause(e);
81            throw iae;
82        }
83    }
84
85    public AttributeValue(String rawString, byte[] encoded, int tag) {
86        wasEncoded = true;
87
88        this.encoded = encoded;
89        this.tag = tag;
90
91        if (rawString == null) {
92            this.rawString = getHexString();
93            this.escapedString = hexString;
94        } else {
95            this.rawString = rawString;
96            this.escapedString = makeEscaped(rawString);
97        }
98    }
99
100    /**
101     * Checks if the string is PrintableString (see X.680)
102     */
103    private static boolean isPrintableString(String str) {
104        for (int i = 0; i< str.length(); ++i) {
105            char ch = str.charAt(i);
106            if (!(ch == 0x20
107            || ch >= 0x27 && ch<= 0x29 // '()
108            || ch >= 0x2B && ch<= 0x3A // +,-./0-9:
109            || ch == '='
110            || ch == '?'
111            || ch >= 'A' && ch<= 'Z'
112            || ch >= 'a' && ch<= 'z')) {
113                return false;
114            }
115        }
116        return true;
117    }
118
119    public int getTag() {
120        if (tag == -1) {
121            tag = isPrintableString(rawString)
122                    ? ASN1StringType.PRINTABLESTRING.id
123                    : ASN1StringType.UTF8STRING.id;
124        }
125        return tag;
126    }
127
128    public String getHexString() {
129        if (hexString == null) {
130            if (!wasEncoded) {
131                //FIXME optimize me: what about reusable OutputStream???
132                encoded = isPrintableString(rawString)
133                        ? ASN1StringType.PRINTABLESTRING.encode(rawString)
134                        : ASN1StringType.UTF8STRING.encode(rawString);
135            }
136
137            StringBuilder buf = new StringBuilder(encoded.length * 2 + 1);
138            buf.append('#');
139
140            for (int i = 0, c; i < encoded.length; i++) {
141                c = (encoded[i] >> 4) & 0x0F;
142                if (c < 10) {
143                    buf.append((char) (c + 48));
144                } else {
145                    buf.append((char) (c + 87));
146                }
147
148                c = encoded[i] & 0x0F;
149                if (c < 10) {
150                    buf.append((char) (c + 48));
151                } else {
152                    buf.append((char) (c + 87));
153                }
154            }
155            hexString = buf.toString();
156        }
157        return hexString;
158    }
159
160    public void appendQEString(StringBuilder sb) {
161        sb.append('"');
162        if (hasQE) {
163            char c;
164            for (int i = 0; i < rawString.length(); i++) {
165                c = rawString.charAt(i);
166                if (c == '"' || c == '\\') {
167                    sb.append('\\');
168                }
169                sb.append(c);
170            }
171        } else {
172            sb.append(rawString);
173        }
174        sb.append('"');
175    }
176
177    /**
178     * Escapes:
179     * 1) chars ",", "+", """, "\", "<", ">", ";" (RFC 2253)
180     * 2) chars "#", "=" (required by RFC 1779)
181     * 3) a space char at the beginning or end
182     * 4) according to the requirement to be RFC 1779 compatible:
183     *    '#' char is escaped in any position
184     */
185    private String makeEscaped(String name) {
186        int length = name.length();
187        if (length == 0) {
188            return name;
189        }
190        StringBuilder buf = new StringBuilder(length * 2);
191
192        for (int index = 0; index < length; index++) {
193            char ch = name.charAt(index);
194            switch (ch) {
195            case ' ':
196                if (index == 0 || index == (length - 1)) {
197                    // escape first or last space
198                    buf.append('\\');
199                }
200                buf.append(' ');
201                break;
202
203            case '"':
204            case '\\':
205                hasQE = true;
206                buf.append('\\');
207                buf.append(ch);
208                break;
209
210            case ',':
211            case '+':
212            case '<':
213            case '>':
214            case ';':
215            case '#': // required by RFC 1779
216            case '=': // required by RFC 1779
217                buf.append('\\');
218                buf.append(ch);
219                break;
220
221            default:
222                buf.append(ch);
223                break;
224            }
225        }
226
227        return buf.toString();
228    }
229
230    public String makeCanonical() {
231        int length = rawString.length();
232        if (length == 0) {
233            return rawString;
234        }
235        StringBuilder buf = new StringBuilder(length * 2);
236
237        int index = 0;
238        if (rawString.charAt(0) == '#') {
239            buf.append('\\');
240            buf.append('#');
241            index++;
242        }
243
244        int bufLength;
245        for (; index < length; index++) {
246            char ch = rawString.charAt(index);
247
248            switch (ch) {
249            case ' ':
250                bufLength = buf.length();
251                if (bufLength == 0 || buf.charAt(bufLength - 1) == ' ') {
252                    break;
253                }
254                buf.append(' ');
255                break;
256
257            case '"':
258            case '\\':
259            case ',':
260            case '+':
261            case '<':
262            case '>':
263            case ';':
264                buf.append('\\');
265
266            default:
267                buf.append(ch);
268            }
269        }
270
271        //remove trailing spaces
272        for (bufLength = buf.length() - 1; bufLength > -1
273                && buf.charAt(bufLength) == ' '; bufLength--) {
274        }
275        buf.setLength(bufLength + 1);
276
277        return buf.toString();
278    }
279}
280