1 /*
2  * JBoss, Home of Professional Open Source.
3  * Copyright 2014 Red Hat, Inc., and individual contributors
4  * as indicated by the @author tags.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  *  Unless required by applicable law or agreed to in writing, software
13  *  distributed under the License is distributed on an "AS IS" BASIS,
14  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  *  See the License for the specific language governing permissions and
16  *  limitations under the License.
17  */

18
19 package io.undertow.util;
20
21 import java.io.IOException;
22 import java.io.ObjectInputStream;
23 import java.io.OutputStream;
24 import java.io.Serializable;
25 import java.lang.reflect.Field;
26 import java.nio.ByteBuffer;
27
28 import static java.lang.Integer.signum;
29 import static java.lang.System.arraycopy;
30 import static java.util.Arrays.copyOfRange;
31
32 import io.undertow.UndertowMessages;
33
34 /**
35  * An HTTP case-insensitive Latin-1 string.
36  *
37  * @author <a href="mailto:david.lloyd@redhat.com">David M. Lloyd</a>
38  */

39 public final class HttpString implements Comparable<HttpString>, Serializable {
40     private static final long serialVersionUID = 1L;
41
42     private final byte[] bytes;
43     private final transient int hashCode;
44     /**
45      * And integer that is only set for well known header to make
46      * comparison fast
47      */

48     private final int orderInt;
49     private transient String string;
50
51     private static final Field hashCodeField;
52
53     static {
54         try {
55             hashCodeField = HttpString.class.getDeclaredField("hashCode");
56             hashCodeField.setAccessible(true);
57         } catch (NoSuchFieldException e) {
58             throw new NoSuchFieldError(e.getMessage());
59         }
60     }
61
62     /**
63      * Empty HttpString instance.
64      */

65     public static final HttpString EMPTY = new HttpString("");
66
67     /**
68      * Construct a new instance.
69      *
70      * @param bytes the byte array to copy
71      */

72     public HttpString(final byte[] bytes) {
73         this(bytes.clone(), null);
74     }
75
76     /**
77      * Construct a new instance.
78      *
79      * @param bytes  the byte array to copy
80      * @param offset the offset into the array to start copying
81      * @param length the number of bytes to copy
82      */

83     public HttpString(final byte[] bytes, int offset, int length) {
84         this(copyOfRange(bytes, offset, offset + length), null);
85     }
86
87     /**
88      * Construct a new instance by reading the remaining bytes from a buffer.
89      *
90      * @param buffer the buffer to read
91      */

92     public HttpString(final ByteBuffer buffer) {
93         this(take(buffer), null);
94     }
95
96     /**
97      * Construct a new instance from a {@code String}.  The {@code String} will be used
98      * as the cached {@code toString()} value for this {@code HttpString}.
99      *
100      * @param string the source string
101      */

102     public HttpString(final String string) {
103         this(string, 0);
104     }
105
106     HttpString(final String string, int orderInt) {
107         this.orderInt = orderInt;
108         final int len = string.length();
109         final byte[] bytes = new byte[len];
110         for (int i = 0; i < len; i++) {
111             char c = string.charAt(i);
112             if (c > 0xff) {
113                 throw new IllegalArgumentException("Invalid string contents " + string);
114             }
115             bytes[i] = (byte) c;
116         }
117         this.bytes = bytes;
118         this.hashCode = calcHashCode(bytes);
119         this.string = string;
120         checkForNewlines();
121     }
122
123     private void checkForNewlines() {
124         for(byte b : bytes) {
125             if(b == '\r' || b == '\n') {
126                 throw UndertowMessages.MESSAGES.newlineNotSupportedInHttpString(string);
127             }
128         }
129     }
130
131     private HttpString(final byte[] bytes, final String string) {
132         this.bytes = bytes;
133         this.hashCode = calcHashCode(bytes);
134         this.string = string;
135         this.orderInt = 0;
136         checkForNewlines();
137     }
138
139     /**
140      * Attempt to convert a {@code String} to an {@code HttpString}.  If the string cannot be converted,
141      * {@code null} is returned.
142      *
143      * @param string the string to try
144      * @return the HTTP string, or {@code nullif the string is not in a compatible encoding
145      */

146     public static HttpString tryFromString(String string) {
147         HttpString cached = Headers.fromCache(string);
148         if(cached != null) {
149             return cached;
150         }
151         final int len = string.length();
152         final byte[] bytes = new byte[len];
153         for (int i = 0; i < len; i++) {
154             char c = string.charAt(i);
155             if (c > 0xff) {
156                 return null;
157             }
158             bytes[i] = (byte) c;
159         }
160         return new HttpString(bytes, string);
161     }
162
163     /**
164      * Get the string length.
165      *
166      * @return the string length
167      */

168     public int length() {
169         return bytes.length;
170     }
171
172     /**
173      * Get the byte at an index.
174      *
175      * @return the byte at an index
176      */

177     public byte byteAt(int idx) {
178         return bytes[idx];
179     }
180
181     /**
182      * Copy {@code len} bytes from this string at offset {@code srcOffs} to the given array at the given offset.
183      *
184      * @param srcOffs the source offset
185      * @param dst     the destination
186      * @param offs    the destination offset
187      * @param len     the number of bytes to copy
188      */

189     public void copyTo(int srcOffs, byte[] dst, int offs, int len) {
190         arraycopy(bytes, srcOffs, dst, offs, len);
191     }
192
193     /**
194      * Copy {@code len} bytes from this string to the given array at the given offset.
195      *
196      * @param dst  the destination
197      * @param offs the destination offset
198      * @param len  the number of bytes
199      */

200     public void copyTo(byte[] dst, int offs, int len) {
201         copyTo(0, dst, offs, len);
202     }
203
204     /**
205      * Copy all the bytes from this string to the given array at the given offset.
206      *
207      * @param dst  the destination
208      * @param offs the destination offset
209      */

210     public void copyTo(byte[] dst, int offs) {
211         copyTo(dst, offs, bytes.length);
212     }
213
214     /**
215      * Append to a byte buffer.
216      *
217      * @param buffer the buffer to append to
218      */

219     public void appendTo(ByteBuffer buffer) {
220         buffer.put(bytes);
221     }
222
223     /**
224      * Append to an output stream.
225      *
226      * @param output the stream to write to
227      * @throws IOException if an error occurs
228      */

229     public void writeTo(OutputStream output) throws IOException {
230         output.write(bytes);
231     }
232
233     private static byte[] take(final ByteBuffer buffer) {
234         if (buffer.hasArray()) {
235             // avoid useless array clearing
236             try {
237                 return copyOfRange(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining());
238             } finally {
239                 buffer.position(buffer.limit());
240             }
241         } else {
242             final byte[] bytes = new byte[buffer.remaining()];
243             buffer.get(bytes);
244             return bytes;
245         }
246     }
247
248     /**
249      * Compare this string to another in a case-insensitive manner.
250      *
251      * @param other the other string
252      * @return -1, 0, or 1
253      */

254     public int compareTo(final HttpString other) {
255         if(orderInt != 0 && other.orderInt != 0) {
256             return signum(orderInt - other.orderInt);
257         }
258         final int len = Math.min(bytes.length, other.bytes.length);
259         int res;
260         for (int i = 0; i < len; i++) {
261             res = signum(higher(bytes[i]) - higher(other.bytes[i]));
262             if (res != 0) return res;
263         }
264         // shorter strings sort higher
265         return signum(bytes.length - other.bytes.length);
266     }
267
268     /**
269      * Get the hash code.
270      *
271      * @return the hash code
272      */

273     @Override
274     public int hashCode() {
275         return hashCode;
276     }
277
278     /**
279      * Determine if this {@code HttpString} is equal to another.
280      *
281      * @param other the other object
282      * @return {@code trueif they are equal, {@code false} otherwise
283      */

284     @Override
285     public boolean equals(final Object other) {
286         if(other == this) {
287             return true;
288         }
289         if(!(other instanceof HttpString)) {
290             return false;
291         }
292         HttpString otherString = (HttpString) other;
293         if(orderInt > 0 && otherString.orderInt > 0) {
294             //if the order int is set for both of them and different then we know they are different strings
295             return false;
296         }
297         return bytesAreEqual(bytes, otherString.bytes);
298     }
299
300     /**
301      * Determine if this {@code HttpString} is equal to another.
302      *
303      * @param other the other object
304      * @return {@code trueif they are equal, {@code false} otherwise
305      */

306     public boolean equals(final HttpString other) {
307         return other == this || other != null && bytesAreEqual(bytes, other.bytes);
308     }
309
310     private static int calcHashCode(final byte[] bytes) {
311         int hc = 17;
312         for (byte b : bytes) {
313             hc = (hc << 4) + hc + higher(b);
314         }
315         return hc;
316     }
317
318     private static int higher(byte b) {
319         return b & (b >= 'a' && b <= 'z' ? 0xDF : 0xFF);
320     }
321
322     private static boolean bytesAreEqual(final byte[] a, final byte[] b) {
323         return a.length == b.length && bytesAreEquivalent(a, b);
324     }
325
326     private static boolean bytesAreEquivalent(final byte[] a, final byte[] b) {
327         assert a.length == b.length;
328         final int len = a.length;
329         for (int i = 0; i < len; i++) {
330             if (higher(a[i]) != higher(b[i])) {
331                 return false;
332             }
333         }
334         return true;
335     }
336
337     /**
338      * Get the {@code String} representation of this {@code HttpString}.
339      *
340      * @return the string
341      */

342     @Override
343     @SuppressWarnings("deprecation")
344     public String toString() {
345         if (string == null) {
346             string = new String(bytes, 0);
347         }
348         return string;
349     }
350
351     private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
352         ois.defaultReadObject();
353         try {
354             hashCodeField.setInt(this, calcHashCode(bytes));
355         } catch (IllegalAccessException e) {
356             throw new IllegalAccessError(e.getMessage());
357         }
358     }
359
360     static int hashCodeOf(String headerName) {
361         int hc = 17;
362
363         for (int i = 0; i < headerName.length(); ++i) {
364             hc = (hc << 4) + hc + higher((byte) headerName.charAt(i));
365         }
366         return hc;
367     }
368
369     public boolean equalToString(String headerName) {
370         if(headerName.length() != bytes.length) {
371             return false;
372         }
373
374         final int len = bytes.length;
375         for (int i = 0; i < len; i++) {
376             if (higher(bytes[i]) != higher((byte)headerName.charAt(i))) {
377                 return false;
378             }
379         }
380         return true;
381     }
382 }
383