1   package com.atlassian.security.auth.trustedapps;
2   
3   /*
4    * Portions Copyright (c) 2002 JSON.org Permission is hereby granted, free of charge, to any person obtaining a copy of
5    * this software and associated documentation files (the "Software"), to deal in the Software without restriction,
6    * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7    * copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following
8    * conditions: The above copyright notice and this permission notice shall be included in all copies or substantial
9    * portions of the Software. The Software shall be used for Good, not Evil. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT
10   * WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR
11   * A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
12   * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
13   * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
14   */
15  
16  import java.util.Arrays;
17  import java.util.LinkedList;
18  import java.util.List;
19  
20  /**
21   * String manipulation methods.
22   */
23  class StringUtil
24  {
25      /**
26       * Splits a comma separated list of values. Values must be quoted and can be encased in square-brackets (JSON
27       * style).
28       * 
29       * @param the
30       *            source String
31       * @return an array of String values
32       */
33      static String[] split(final String source)
34      {
35          Null.not("source", source);
36  
37          final List<String> result = new JSONList(source.trim());
38          return result.toArray(new String[result.size()]);
39      }
40  
41      static String toString(final String[] source)
42      {
43          Null.not("source", source);
44  
45          return new JSONList(source).toString();
46      }
47  
48      /**
49       * subset of JSONArray that simply takes a String and converts to a list.
50       */
51      private static class JSONList extends LinkedList<String>
52      {
53          private static final long serialVersionUID = -8317241062936626298L;
54  
55          JSONList(final String source)
56          {
57              final Tokenizer tokenizer = new Tokenizer(source);
58  
59              if (tokenizer.nextClean() != '[')
60              {
61                  tokenizer.syntaxError("String must start with square bracket");
62              }
63              switch (tokenizer.nextClean())
64              {
65                  case ']':
66                  case 0:
67                      return;
68              }
69              tokenizer.back();
70              for (;;)
71              {
72                  if (tokenizer.nextClean() == ',')
73                  {
74                      tokenizer.back();
75                      this.add(null);
76                  }
77                  else
78                  {
79                      tokenizer.back();
80                      this.add(tokenizer.nextValue());
81                  }
82                  final char nextClean = tokenizer.nextClean();
83                  switch (nextClean)
84                  {
85                      case ';':
86                      case ',':
87                          switch (tokenizer.nextClean())
88                          {
89                              case ']':
90                              case 0:
91                                  return;
92                          }
93                          tokenizer.back();
94                          break;
95                      case ']':
96                      case 0:
97                          return;
98                      default:
99                          tokenizer.syntaxError("Expected a ',' or ']' rather than: " + (int) nextClean);
100                 }
101             }
102         }
103 
104         JSONList(final String[] source)
105         {
106             super(Arrays.asList(source));
107         }
108 
109         String join(final String separator)
110         {
111             final int len = size();
112             final StringBuffer sb = new StringBuffer();
113 
114             for (int i = 0; i < len; i += 1)
115             {
116                 if (i > 0)
117                 {
118                     sb.append(separator);
119                 }
120                 sb.append(quote(get(i)));
121             }
122             return sb.toString();
123         }
124 
125         /**
126          * Produce a string in double quotes with backslash sequences in all the right places. A backslash will be
127          * inserted within </, allowing JSON text to be delivered in HTML. In JSON text, a string cannot contain a
128          * control character or an unescaped quote or backslash.
129          * 
130          * @param string
131          *            A String
132          * @return A String correctly formatted for insertion in a JSON text.
133          */
134         String quote(final String string)
135         {
136             if ((string == null) || (string.length() == 0))
137             {
138                 return "\"\"";
139             }
140 
141             char b;
142             char c = 0;
143             int i;
144             final int len = string.length();
145             final StringBuffer sb = new StringBuffer(len + 4);
146             String t;
147 
148             sb.append('"');
149             for (i = 0; i < len; i += 1)
150             {
151                 b = c;
152                 c = string.charAt(i);
153                 switch (c)
154                 {
155                     case '\\':
156                     case '"':
157                         sb.append('\\');
158                         sb.append(c);
159                         break;
160                     case '/':
161                         if (b == '<')
162                         {
163                             sb.append('\\');
164                         }
165                         sb.append(c);
166                         break;
167                     case '\b':
168                         sb.append("\\b");
169                         break;
170                     case '\t':
171                         sb.append("\\t");
172                         break;
173                     case '\n':
174                         sb.append("\\n");
175                         break;
176                     case '\f':
177                         sb.append("\\f");
178                         break;
179                     case '\r':
180                         sb.append("\\r");
181                         break;
182                     default:
183                         if ((c < ' ') || ((c >= '\u0080') && (c < '\u00a0')) || ((c >= '\u2000') && (c < '\u2100')))
184                         {
185                             t = "000" + Integer.toHexString(c);
186                             sb.append("\\u").append(t.substring(t.length() - 4));
187                         }
188                         else
189                         {
190                             sb.append(c);
191                         }
192                 }
193             }
194             sb.append('"');
195             return sb.toString();
196         }
197 
198         @Override
199         public String toString()
200         {
201             try
202             {
203                 return '[' + join(",") + ']';
204             }
205             catch (final Exception e)
206             {
207                 return "";
208             }
209         }
210 
211         /**
212          * Tokenizer takes a source string and extracts characters and tokens from it. Adapted from JSONTokener in the
213          * json.org package.
214          * <p>
215          * Kept as an inner class - or rather ghetto - so outer class can deal with Iterator conversion. Not the
216          * prettiest code in the world by a long shot.
217          */
218         private static class Tokenizer
219         {
220             /**
221              * The source string being tokenized.
222              */
223             private final String source;
224 
225             /**
226              * The index of the next character.
227              */
228             private int index = 0;
229 
230             /**
231              * Construct a JSONTokener from a string.
232              * 
233              * @param s
234              *            A source string.
235              */
236             Tokenizer(final String s)
237             {
238                 source = s;
239             }
240 
241             /**
242              * Back up one character. This provides a sort of lookahead capability, so that you can test for a digit or
243              * letter before attempting to parse the next number or identifier.
244              */
245             private void back()
246             {
247                 if (index > 0)
248                 {
249                     index -= 1;
250                 }
251             }
252 
253             /**
254              * Determine if the source string still contains characters that next() can consume.
255              * 
256              * @return true if not yet at the end of the source.
257              */
258             private boolean more()
259             {
260                 return index < source.length();
261             }
262 
263             /**
264              * Get the next character in the source string.
265              * 
266              * @return The next character, or 0 if past the end of the source string.
267              */
268             private char next()
269             {
270                 if (more())
271                 {
272                     final char c = source.charAt(index);
273                     index += 1;
274                     return c;
275                 }
276                 return 0;
277             }
278 
279             /**
280              * Get the next n characters.
281              * 
282              * @param n
283              *            The number of characters to take.
284              * @return A string of n characters.
285              * @throws JSONException
286              *             Substring bounds error if there are not n characters remaining in the source string.
287              */
288             private String next(final int n)
289             {
290                 final int i = index;
291                 final int j = i + n;
292                 if (j >= source.length())
293                 {
294                     throw new IllegalStateException("Substring bounds error");
295                 }
296                 index += n;
297                 return source.substring(i, j);
298             }
299 
300             /**
301              * Get the next char in the string, skipping whitespace and comments (slashslash, slashstar, and hash).
302              * 
303              * @return A character, or 0 if there are no more characters.
304              * @throws JSONException
305              *             in case of a syntax error
306              */
307             private char nextClean()
308             {
309                 for (;;)
310                 {
311                     char c = next();
312                     if (c == '/')
313                     {
314                         switch (next())
315                         {
316                             case '/':
317                                 do
318                                 {
319                                     c = next();
320                                 }
321                                 while ((c != '\n') && (c != '\r') && (c != 0));
322                                 break;
323                             case '*':
324                                 for (;;)
325                                 {
326                                     c = next();
327                                     if (c == 0)
328                                     {
329                                         throw new IllegalStateException("Unclosed comment");
330                                     }
331                                     if (c == '*')
332                                     {
333                                         if (next() == '/')
334                                         {
335                                             break;
336                                         }
337                                         back();
338                                     }
339                                 }
340                                 break;
341                             default:
342                                 back();
343                                 return '/';
344                         }
345                     }
346                     else if (c == '#')
347                     {
348                         do
349                         {
350                             c = next();
351                         }
352                         while ((c != '\n') && (c != '\r') && (c != 0));
353                     }
354                     else if ((c == 0) || (c > ' '))
355                     {
356                         return c;
357                     }
358                 }
359             }
360 
361             /**
362              * Return the characters up to the next close quote character. Backslash processing is done. The formal JSON
363              * format does not allow strings in single quotes, but an implementation is allowed to accept them.
364              * 
365              * @param quote
366              *            The quoting character, either <code>"</code>&nbsp;<small>(double quote)</small> or
367              *            <code>'</code>&nbsp;<small>(single quote)</small>.
368              * @return A String.
369              * @throws JSONException
370              *             Unterminated string.
371              */
372             private String nextString(final char quote)
373             {
374                 char c;
375                 final StringBuffer sb = new StringBuffer();
376                 for (;;)
377                 {
378                     c = next();
379                     switch (c)
380                     {
381                         case 0:
382                         case '\n':
383                         case '\r':
384                             throw new IllegalStateException("Unterminated string");
385                         case '\\':
386                             c = next();
387                             switch (c)
388                             {
389                                 case 'b':
390                                     sb.append('\b');
391                                     break;
392                                 case 't':
393                                     sb.append('\t');
394                                     break;
395                                 case 'n':
396                                     sb.append('\n');
397                                     break;
398                                 case 'f':
399                                     sb.append('\f');
400                                     break;
401                                 case 'r':
402                                     sb.append('\r');
403                                     break;
404                                 case 'u':
405                                     sb.append((char) Integer.parseInt(next(4), 16));
406                                     break;
407                                 case 'x':
408                                     sb.append((char) Integer.parseInt(next(2), 16));
409                                     break;
410                                 default:
411                                     sb.append(c);
412                             }
413                             break;
414                         default:
415                             if (c == quote)
416                             {
417                                 return sb.toString();
418                             }
419                             sb.append(c);
420                     }
421                 }
422             }
423 
424             /**
425              * Get the next value. The value can be a Boolean, Double, Integer, JSONArray, JSONObject, Long, or String,
426              * or the JSONObject.NULL object.
427              * 
428              * @return An object.
429              * @throws JSONException
430              *             If syntax error.
431              */
432             private String nextValue()
433             {
434                 char c = nextClean();
435                 String s;
436 
437                 switch (c)
438                 {
439                     case '"':
440                     case '\'':
441                         return nextString(c);
442                 }
443 
444                 /*
445                  * Handle unquoted text. This could be the values true, false, or null, or it can be a number. An
446                  * implementation (such as this one) is allowed to also accept non-standard forms. Accumulate characters
447                  * until we reach the end of the text or a formatting character.
448                  */
449 
450                 final StringBuffer sb = new StringBuffer();
451                 while ((c >= ' ') && (",:]}/\\\"[{;=#".indexOf(c) <= 0))
452                 {
453                     sb.append(c);
454                     c = next();
455                 }
456                 back();
457 
458                 /*
459                  * If it is true, false, or null, return the proper value.
460                  */
461 
462                 s = sb.toString().trim();
463                 if (s.equals(""))
464                 {
465                     throw new IllegalStateException("Missing value" + toString());
466                 }
467                 if (s.equalsIgnoreCase("null"))
468                 {
469                     return null;
470                 }
471 
472                 return s;
473             }
474 
475             void syntaxError(final String message)
476             {
477                 throw new IllegalStateException(message + toString());
478             }
479 
480             @Override
481             public String toString()
482             {
483                 return " at character " + index + " of " + source;
484             }
485         }
486     }
487 }