UI changes.
[pithos-ios] / Classes / SBJsonParser.m
1 /*
2  Copyright (C) 2009 Stig Brautaset. All rights reserved.
3  
4  Redistribution and use in source and binary forms, with or without
5  modification, are permitted provided that the following conditions are met:
6  
7  * Redistributions of source code must retain the above copyright notice, this
8    list of conditions and the following disclaimer.
9  
10  * Redistributions in binary form must reproduce the above copyright notice,
11    this list of conditions and the following disclaimer in the documentation
12    and/or other materials provided with the distribution.
13  
14  * Neither the name of the author nor the names of its contributors may be used
15    to endorse or promote products derived from this software without specific
16    prior written permission.
17  
18  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
22  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28  */
29
30 #import "SBJsonParser.h"
31
32 @interface SBJsonParser ()
33
34 - (BOOL)scanValue:(NSObject **)o;
35
36 - (BOOL)scanRestOfArray:(NSMutableArray **)o;
37 - (BOOL)scanRestOfDictionary:(NSMutableDictionary **)o;
38 - (BOOL)scanRestOfNull:(NSNull **)o;
39 - (BOOL)scanRestOfFalse:(NSNumber **)o;
40 - (BOOL)scanRestOfTrue:(NSNumber **)o;
41 - (BOOL)scanRestOfString:(NSMutableString **)o;
42
43 // Cannot manage without looking at the first digit
44 - (BOOL)scanNumber:(NSNumber **)o;
45
46 - (BOOL)scanHexQuad:(unichar *)x;
47 - (BOOL)scanUnicodeChar:(unichar *)x;
48
49 - (BOOL)scanIsAtEnd;
50
51 @end
52
53 #define skipWhitespace(c) while (isspace(*c)) c++
54 #define skipDigits(c) while (isdigit(*c)) c++
55
56
57 @implementation SBJsonParser
58
59 static char ctrl[0x22];
60
61
62 + (void)initialize {
63     ctrl[0] = '\"';
64     ctrl[1] = '\\';
65     for (int i = 1; i < 0x20; i++)
66         ctrl[i+1] = i;
67     ctrl[0x21] = 0;    
68 }
69
70 /**
71  @deprecated This exists in order to provide fragment support in older APIs in one more version.
72  It should be removed in the next major version.
73  */
74 - (id)fragmentWithString:(id)repr {
75     [self clearErrorTrace];
76     
77     if (!repr) {
78         [self addErrorWithCode:EINPUT description:@"Input was 'nil'"];
79         return nil;
80     }
81     
82     depth = 0;
83     c = [repr UTF8String];
84     
85     id o;
86     if (![self scanValue:&o]) {
87         return nil;
88     }
89     
90     // We found some valid JSON. But did it also contain something else?
91     if (![self scanIsAtEnd]) {
92         [self addErrorWithCode:ETRAILGARBAGE description:@"Garbage after JSON"];
93         return nil;
94     }
95         
96     NSAssert1(o, @"Should have a valid object from %@", repr);
97     return o;    
98 }
99
100 - (id)objectWithString:(NSString *)repr {
101
102     id o = [self fragmentWithString:repr];
103     if (!o)
104         return nil;
105     
106     // Check that the object we've found is a valid JSON container.
107     if (![o isKindOfClass:[NSDictionary class]] && ![o isKindOfClass:[NSArray class]]) {
108         [self addErrorWithCode:EFRAGMENT description:@"Valid fragment, but not JSON"];
109         return nil;
110     }
111
112     return o;
113 }
114
115 /*
116  In contrast to the public methods, it is an error to omit the error parameter here.
117  */
118 - (BOOL)scanValue:(NSObject **)o
119 {
120     skipWhitespace(c);
121     
122     switch (*c++) {
123         case '{':
124             return [self scanRestOfDictionary:(NSMutableDictionary **)o];
125             break;
126         case '[':
127             return [self scanRestOfArray:(NSMutableArray **)o];
128             break;
129         case '"':
130             return [self scanRestOfString:(NSMutableString **)o];
131             break;
132         case 'f':
133             return [self scanRestOfFalse:(NSNumber **)o];
134             break;
135         case 't':
136             return [self scanRestOfTrue:(NSNumber **)o];
137             break;
138         case 'n':
139             return [self scanRestOfNull:(NSNull **)o];
140             break;
141         case '-':
142         case '0'...'9':
143             c--; // cannot verify number correctly without the first character
144             return [self scanNumber:(NSNumber **)o];
145             break;
146         case '+':
147             [self addErrorWithCode:EPARSENUM description: @"Leading + disallowed in number"];
148             return NO;
149             break;
150         case 0x0:
151             [self addErrorWithCode:EEOF description:@"Unexpected end of string"];
152             return NO;
153             break;
154         default:
155             [self addErrorWithCode:EPARSE description: @"Unrecognised leading character"];
156             return NO;
157             break;
158     }
159     
160     NSAssert(0, @"Should never get here");
161     return NO;
162 }
163
164 - (BOOL)scanRestOfTrue:(NSNumber **)o
165 {
166     if (!strncmp(c, "rue", 3)) {
167         c += 3;
168         *o = [NSNumber numberWithBool:YES];
169         return YES;
170     }
171     [self addErrorWithCode:EPARSE description:@"Expected 'true'"];
172     return NO;
173 }
174
175 - (BOOL)scanRestOfFalse:(NSNumber **)o
176 {
177     if (!strncmp(c, "alse", 4)) {
178         c += 4;
179         *o = [NSNumber numberWithBool:NO];
180         return YES;
181     }
182     [self addErrorWithCode:EPARSE description: @"Expected 'false'"];
183     return NO;
184 }
185
186 - (BOOL)scanRestOfNull:(NSNull **)o {
187     if (!strncmp(c, "ull", 3)) {
188         c += 3;
189         *o = [NSNull null];
190         return YES;
191     }
192     [self addErrorWithCode:EPARSE description: @"Expected 'null'"];
193     return NO;
194 }
195
196 - (BOOL)scanRestOfArray:(NSMutableArray **)o {
197     if (maxDepth && ++depth > maxDepth) {
198         [self addErrorWithCode:EDEPTH description: @"Nested too deep"];
199         return NO;
200     }
201     
202     *o = [NSMutableArray arrayWithCapacity:8];
203     
204     for (; *c ;) {
205         id v;
206         
207         skipWhitespace(c);
208         if (*c == ']' && c++) {
209             depth--;
210             return YES;
211         }
212         
213         if (![self scanValue:&v]) {
214             [self addErrorWithCode:EPARSE description:@"Expected value while parsing array"];
215             return NO;
216         }
217         
218         [*o addObject:v];
219         
220         skipWhitespace(c);
221         if (*c == ',' && c++) {
222             skipWhitespace(c);
223             if (*c == ']') {
224                 [self addErrorWithCode:ETRAILCOMMA description: @"Trailing comma disallowed in array"];
225                 return NO;
226             }
227         }        
228     }
229     
230     [self addErrorWithCode:EEOF description: @"End of input while parsing array"];
231     return NO;
232 }
233
234 - (BOOL)scanRestOfDictionary:(NSMutableDictionary **)o 
235 {
236     if (maxDepth && ++depth > maxDepth) {
237         [self addErrorWithCode:EDEPTH description: @"Nested too deep"];
238         return NO;
239     }
240     
241     *o = [NSMutableDictionary dictionaryWithCapacity:7];
242     
243     for (; *c ;) {
244         id k, v;
245         
246         skipWhitespace(c);
247         if (*c == '}' && c++) {
248             depth--;
249             return YES;
250         }    
251         
252         if (!(*c == '\"' && c++ && [self scanRestOfString:&k])) {
253             [self addErrorWithCode:EPARSE description: @"Object key string expected"];
254             return NO;
255         }
256         
257         skipWhitespace(c);
258         if (*c != ':') {
259             [self addErrorWithCode:EPARSE description: @"Expected ':' separating key and value"];
260             return NO;
261         }
262         
263         c++;
264         if (![self scanValue:&v]) {
265             NSString *string = [NSString stringWithFormat:@"Object value expected for key: %@", k];
266             [self addErrorWithCode:EPARSE description: string];
267             return NO;
268         }
269         
270         [*o setObject:v forKey:k];
271         
272         skipWhitespace(c);
273         if (*c == ',' && c++) {
274             skipWhitespace(c);
275             if (*c == '}') {
276                 [self addErrorWithCode:ETRAILCOMMA description: @"Trailing comma disallowed in object"];
277                 return NO;
278             }
279         }        
280     }
281     
282     [self addErrorWithCode:EEOF description: @"End of input while parsing object"];
283     return NO;
284 }
285
286 - (BOOL)scanRestOfString:(NSMutableString **)o 
287 {
288     *o = [NSMutableString stringWithCapacity:16];
289     do {
290         // First see if there's a portion we can grab in one go. 
291         // Doing this caused a massive speedup on the long string.
292         size_t len = strcspn(c, ctrl);
293         if (len) {
294             // check for 
295             id t = [[NSString alloc] initWithBytesNoCopy:(char*)c
296                                                   length:len
297                                                 encoding:NSUTF8StringEncoding
298                                             freeWhenDone:NO];
299             if (t) {
300                 [*o appendString:t];
301                 [t release];
302                 c += len;
303             }
304         }
305         
306         if (*c == '"') {
307             c++;
308             return YES;
309             
310         } else if (*c == '\\') {
311             unichar uc = *++c;
312             switch (uc) {
313                 case '\\':
314                 case '/':
315                 case '"':
316                     break;
317                     
318                 case 'b':   uc = '\b';  break;
319                 case 'n':   uc = '\n';  break;
320                 case 'r':   uc = '\r';  break;
321                 case 't':   uc = '\t';  break;
322                 case 'f':   uc = '\f';  break;                    
323                     
324                 case 'u':
325                     c++;
326                     if (![self scanUnicodeChar:&uc]) {
327                         [self addErrorWithCode:EUNICODE description: @"Broken unicode character"];
328                         return NO;
329                     }
330                     c--; // hack.
331                     break;
332                 default:
333                     [self addErrorWithCode:EESCAPE description: [NSString stringWithFormat:@"Illegal escape sequence '0x%x'", uc]];
334                     return NO;
335                     break;
336             }
337             CFStringAppendCharacters((CFMutableStringRef)*o, &uc, 1);
338             c++;
339             
340         } else if (*c < 0x20) {
341             [self addErrorWithCode:ECTRL description: [NSString stringWithFormat:@"Unescaped control character '0x%x'", *c]];
342             return NO;
343             
344         } else {
345             NSLog(@"should not be able to get here");
346         }
347     } while (*c);
348     
349     [self addErrorWithCode:EEOF description:@"Unexpected EOF while parsing string"];
350     return NO;
351 }
352
353 - (BOOL)scanUnicodeChar:(unichar *)x
354 {
355     unichar hi, lo;
356     
357     if (![self scanHexQuad:&hi]) {
358         [self addErrorWithCode:EUNICODE description: @"Missing hex quad"];
359         return NO;        
360     }
361     
362     if (hi >= 0xd800) {     // high surrogate char?
363         if (hi < 0xdc00) {  // yes - expect a low char
364             
365             if (!(*c == '\\' && ++c && *c == 'u' && ++c && [self scanHexQuad:&lo])) {
366                 [self addErrorWithCode:EUNICODE description: @"Missing low character in surrogate pair"];
367                 return NO;
368             }
369             
370             if (lo < 0xdc00 || lo >= 0xdfff) {
371                 [self addErrorWithCode:EUNICODE description:@"Invalid low surrogate char"];
372                 return NO;
373             }
374             
375             hi = (hi - 0xd800) * 0x400 + (lo - 0xdc00) + 0x10000;
376             
377         } else if (hi < 0xe000) {
378             [self addErrorWithCode:EUNICODE description:@"Invalid high character in surrogate pair"];
379             return NO;
380         }
381     }
382     
383     *x = hi;
384     return YES;
385 }
386
387 - (BOOL)scanHexQuad:(unichar *)x
388 {
389     *x = 0;
390     for (int i = 0; i < 4; i++) {
391         unichar uc = *c;
392         c++;
393         int d = (uc >= '0' && uc <= '9')
394         ? uc - '0' : (uc >= 'a' && uc <= 'f')
395         ? (uc - 'a' + 10) : (uc >= 'A' && uc <= 'F')
396         ? (uc - 'A' + 10) : -1;
397         if (d == -1) {
398             [self addErrorWithCode:EUNICODE description:@"Missing hex digit in quad"];
399             return NO;
400         }
401         *x *= 16;
402         *x += d;
403     }
404     return YES;
405 }
406
407 - (BOOL)scanNumber:(NSNumber **)o
408 {
409     const char *ns = c;
410     
411     // The logic to test for validity of the number formatting is relicensed
412     // from JSON::XS with permission from its author Marc Lehmann.
413     // (Available at the CPAN: http://search.cpan.org/dist/JSON-XS/ .)
414     
415     if ('-' == *c)
416         c++;
417     
418     if ('0' == *c && c++) {        
419         if (isdigit(*c)) {
420             [self addErrorWithCode:EPARSENUM description: @"Leading 0 disallowed in number"];
421             return NO;
422         }
423         
424     } else if (!isdigit(*c) && c != ns) {
425         [self addErrorWithCode:EPARSENUM description: @"No digits after initial minus"];
426         return NO;
427         
428     } else {
429         skipDigits(c);
430     }
431     
432     // Fractional part
433     if ('.' == *c && c++) {
434         
435         if (!isdigit(*c)) {
436             [self addErrorWithCode:EPARSENUM description: @"No digits after decimal point"];
437             return NO;
438         }        
439         skipDigits(c);
440     }
441     
442     // Exponential part
443     if ('e' == *c || 'E' == *c) {
444         c++;
445         
446         if ('-' == *c || '+' == *c)
447             c++;
448         
449         if (!isdigit(*c)) {
450             [self addErrorWithCode:EPARSENUM description: @"No digits after exponent"];
451             return NO;
452         }
453         skipDigits(c);
454     }
455     
456     id str = [[NSString alloc] initWithBytesNoCopy:(char*)ns
457                                             length:c - ns
458                                           encoding:NSUTF8StringEncoding
459                                       freeWhenDone:NO];
460     [str autorelease];
461     if (str && (*o = [NSDecimalNumber decimalNumberWithString:str]))
462         return YES;
463     
464     [self addErrorWithCode:EPARSENUM description: @"Failed creating decimal instance"];
465     return NO;
466 }
467
468 - (BOOL)scanIsAtEnd
469 {
470     skipWhitespace(c);
471     return !*c;
472 }
473
474
475 @end