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
18package com.android.exchange.utility;
19
20import java.io.ByteArrayOutputStream;
21import java.net.URISyntaxException;
22import java.nio.charset.Charset;
23import java.nio.charset.Charsets;
24
25// Note: This class copied verbatim from libcore.net
26
27/**
28 * Encodes and decodes {@code application/x-www-form-urlencoded} content.
29 * Subclasses define exactly which characters are legal.
30 *
31 * <p>By default, UTF-8 is used to encode escaped characters. A single input
32 * character like "\u0080" may be encoded to multiple octets like %C2%80.
33 */
34public abstract class UriCodec {
35
36    /**
37     * Returns true if {@code c} does not need to be escaped.
38     */
39    protected abstract boolean isRetained(char c);
40
41    /**
42     * Throws if {@code s} is invalid according to this encoder.
43     */
44    public final String validate(String uri, int start, int end, String name)
45            throws URISyntaxException {
46        for (int i = start; i < end; ) {
47            char ch = uri.charAt(i);
48            if ((ch >= 'a' && ch <= 'z')
49                    || (ch >= 'A' && ch <= 'Z')
50                    || (ch >= '0' && ch <= '9')
51                    || isRetained(ch)) {
52                i++;
53            } else if (ch == '%') {
54                if (i + 2 >= end) {
55                    throw new URISyntaxException(uri, "Incomplete % sequence in " + name, i);
56                }
57                int d1 = hexToInt(uri.charAt(i + 1));
58                int d2 = hexToInt(uri.charAt(i + 2));
59                if (d1 == -1 || d2 == -1) {
60                    throw new URISyntaxException(uri, "Invalid % sequence: "
61                            + uri.substring(i, i + 3) + " in " + name, i);
62                }
63                i += 3;
64            } else {
65                throw new URISyntaxException(uri, "Illegal character in " + name, i);
66            }
67        }
68        return uri.substring(start, end);
69    }
70
71    /**
72     * Throws if {@code s} contains characters that are not letters, digits or
73     * in {@code legal}.
74     */
75    public static void validateSimple(String s, String legal)
76            throws URISyntaxException {
77        for (int i = 0; i < s.length(); i++) {
78            char ch = s.charAt(i);
79            if (!((ch >= 'a' && ch <= 'z')
80                    || (ch >= 'A' && ch <= 'Z')
81                    || (ch >= '0' && ch <= '9')
82                    || legal.indexOf(ch) > -1)) {
83                throw new URISyntaxException(s, "Illegal character", i);
84            }
85        }
86    }
87
88    /**
89     * Encodes {@code s} and appends the result to {@code builder}.
90     *
91     * @param isPartiallyEncoded true to fix input that has already been
92     *     partially or fully encoded. For example, input of "hello%20world" is
93     *     unchanged with isPartiallyEncoded=true but would be double-escaped to
94     *     "hello%2520world" otherwise.
95     */
96    private void appendEncoded(StringBuilder builder, String s, Charset charset,
97            boolean isPartiallyEncoded) {
98        if (s == null) {
99            throw new NullPointerException();
100        }
101
102        int escapeStart = -1;
103        for (int i = 0; i < s.length(); i++) {
104            char c = s.charAt(i);
105            if ((c >= 'a' && c <= 'z')
106                    || (c >= 'A' && c <= 'Z')
107                    || (c >= '0' && c <= '9')
108                    || isRetained(c)
109                    || (c == '%' && isPartiallyEncoded)) {
110                if (escapeStart != -1) {
111                    appendHex(builder, s.substring(escapeStart, i), charset);
112                    escapeStart = -1;
113                }
114                if (c == '%' && isPartiallyEncoded) {
115                    // this is an encoded 3-character sequence like "%20"
116                    builder.append(s, i, i + 3);
117                    i += 2;
118                } else if (c == ' ') {
119                    builder.append('+');
120                } else {
121                    builder.append(c);
122                }
123            } else if (escapeStart == -1) {
124                escapeStart = i;
125            }
126        }
127        if (escapeStart != -1) {
128            appendHex(builder, s.substring(escapeStart, s.length()), charset);
129        }
130    }
131
132    public final String encode(String s, Charset charset) {
133        // Guess a bit larger for encoded form
134        StringBuilder builder = new StringBuilder(s.length() + 16);
135        appendEncoded(builder, s, charset, false);
136        return builder.toString();
137    }
138
139    public final void appendEncoded(StringBuilder builder, String s) {
140        appendEncoded(builder, s, Charsets.UTF_8, false);
141    }
142
143    public final void appendPartiallyEncoded(StringBuilder builder, String s) {
144        appendEncoded(builder, s, Charsets.UTF_8, true);
145    }
146
147    /**
148     * @param convertPlus true to convert '+' to ' '.
149     */
150    public static String decode(String s, boolean convertPlus, Charset charset) {
151        if (s.indexOf('%') == -1 && (!convertPlus || s.indexOf('+') == -1)) {
152            return s;
153        }
154
155        StringBuilder result = new StringBuilder(s.length());
156        ByteArrayOutputStream out = new ByteArrayOutputStream();
157        for (int i = 0; i < s.length();) {
158            char c = s.charAt(i);
159            if (c == '%') {
160                do {
161                    if (i + 2 >= s.length()) {
162                        throw new IllegalArgumentException("Incomplete % sequence at: " + i);
163                    }
164                    int d1 = hexToInt(s.charAt(i + 1));
165                    int d2 = hexToInt(s.charAt(i + 2));
166                    if (d1 == -1 || d2 == -1) {
167                        throw new IllegalArgumentException("Invalid % sequence " +
168                                s.substring(i, i + 3) + " at " + i);
169                    }
170                    out.write((byte) ((d1 << 4) + d2));
171                    i += 3;
172                } while (i < s.length() && s.charAt(i) == '%');
173                result.append(new String(out.toByteArray(), charset));
174                out.reset();
175            } else {
176                if (convertPlus && c == '+') {
177                    c = ' ';
178                }
179                result.append(c);
180                i++;
181            }
182        }
183        return result.toString();
184    }
185
186    /**
187     * Like {@link Character#digit}, but without support for non-ASCII
188     * characters.
189     */
190    private static int hexToInt(char c) {
191        if ('0' <= c && c <= '9') {
192            return c - '0';
193        } else if ('a' <= c && c <= 'f') {
194            return 10 + (c - 'a');
195        } else if ('A' <= c && c <= 'F') {
196            return 10 + (c - 'A');
197        } else {
198            return -1;
199        }
200    }
201
202    public static String decode(String s) {
203        return decode(s, false, Charsets.UTF_8);
204    }
205
206    private static void appendHex(StringBuilder builder, String s, Charset charset) {
207        for (byte b : s.getBytes(charset)) {
208            appendHex(builder, b);
209        }
210    }
211
212    private static void appendHex(StringBuilder sb, byte b) {
213        sb.append('%');
214        sb.append(Byte.toHexString(b, true));
215    }
216}
217