mirror of
https://github.com/gnustep/libs-base.git
synced 2025-04-22 16:33:29 +00:00
1189 lines
30 KiB
Objective-C
1189 lines
30 KiB
Objective-C
/** Implementation of class NSJSONSerialization
|
|
Copyright (C) 2011-2021 Free Software Foundation, Inc.
|
|
|
|
NSJSONSerialization.m. This file provides an implementation of the JSON
|
|
reading and writing APIs introduced with OS X 10.7.
|
|
|
|
The parser is implemented as a simple recursive parser. The JSON is
|
|
unambiguous, so this requires no read-ahead or backtracking. The source of
|
|
data for the parse can be either a static JSON string or some JSON data.
|
|
|
|
By: David Chisnall <github@theravensnest.org>
|
|
Date: Jul 2011
|
|
|
|
This file is part of the GNUstep Library.
|
|
|
|
This library is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU Lesser General Public
|
|
License as published by the Free Software Foundation; either
|
|
version 2 of the License, or (at your option) any later version.
|
|
|
|
This library is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
Lesser General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Lesser General Public
|
|
License along with this library; if not, write to the Free
|
|
Software Foundation, Inc., 31 Milk Street #960789 Boston, MA 02196 USA.
|
|
*/
|
|
|
|
#import "common.h"
|
|
#import "Foundation/NSArray.h"
|
|
#import "Foundation/NSByteOrder.h"
|
|
#import "Foundation/NSCharacterSet.h"
|
|
#import "Foundation/NSData.h"
|
|
#import "Foundation/NSDictionary.h"
|
|
#import "Foundation/NSError.h"
|
|
#import "Foundation/NSJSONSerialization.h"
|
|
#import "Foundation/NSNull.h"
|
|
#import "Foundation/NSStream.h"
|
|
#import "Foundation/NSString.h"
|
|
#import "Foundation/NSValue.h"
|
|
#import "GSFastEnumeration.h"
|
|
|
|
/* Boolean constants.
|
|
*/
|
|
static id boolN;
|
|
static id boolY;
|
|
|
|
/**
|
|
* The number of (unicode) characters to fetch from the source at once.
|
|
*/
|
|
#define BUFFER_SIZE 64
|
|
|
|
/**
|
|
* Structure for storing the internal state of the parser. An instance of this
|
|
* is allocated on the stack, and a copy of it passed down to each parse
|
|
* function.
|
|
*/
|
|
typedef struct ParserStateStruct
|
|
{
|
|
/**
|
|
* The data source. This is either an NSString or an NSStream, depending on
|
|
* the source.
|
|
*/
|
|
id source;
|
|
/**
|
|
* The length of the byte order mark in the source. 0 if there is no BOM.
|
|
*/
|
|
int BOMLength;
|
|
/**
|
|
* The string encoding used in the source.
|
|
*/
|
|
NSStringEncoding enc;
|
|
/**
|
|
* Function used to pull the next BUFFER_SIZE characters from the string.
|
|
*/
|
|
void (*updateBuffer)(struct ParserStateStruct*);
|
|
/**
|
|
* Buffer used to store the next data from the input stream.
|
|
*/
|
|
unichar buffer[BUFFER_SIZE];
|
|
/**
|
|
* The index of the parser within the buffer.
|
|
*/
|
|
NSUInteger bufferIndex;
|
|
/**
|
|
* The number of bytes stored within the buffer.
|
|
*/
|
|
NSUInteger bufferLength;
|
|
/**
|
|
* The index of the parser within the source.
|
|
*/
|
|
NSInteger sourceIndex;
|
|
/**
|
|
* Should the parser construct mutable string objects?
|
|
*/
|
|
BOOL mutableStrings;
|
|
/**
|
|
* Should the parser construct mutable containers?
|
|
*/
|
|
BOOL mutableContainers;
|
|
/**
|
|
* Error value, if this parser is currently in an error state, nil otherwise.
|
|
*/
|
|
NSError *error;
|
|
} ParserState;
|
|
|
|
/**
|
|
* Pulls the next group of characters from a string source.
|
|
*/
|
|
static inline void
|
|
updateStringBuffer(ParserState* state)
|
|
{
|
|
NSRange r = {state->sourceIndex, BUFFER_SIZE};
|
|
NSUInteger end = [state->source length];
|
|
|
|
if (end - state->sourceIndex < BUFFER_SIZE)
|
|
{
|
|
r.length = end - state->sourceIndex;
|
|
}
|
|
[state->source getCharacters: state->buffer range: r];
|
|
state->sourceIndex = r.location;
|
|
state->bufferIndex = 0;
|
|
state->bufferLength = r.length;
|
|
if (r.length == 0)
|
|
{
|
|
state->buffer[0] = 0;
|
|
}
|
|
}
|
|
|
|
static inline void
|
|
updateStreamBuffer(ParserState* state)
|
|
{
|
|
NSInputStream *stream = state->source;
|
|
uint8_t *buffer;
|
|
NSUInteger length;
|
|
NSString *str;
|
|
|
|
// Discard anything that we've already consumed
|
|
while (state->sourceIndex > 0)
|
|
{
|
|
uint8_t discard[128];
|
|
NSUInteger toRead = 128;
|
|
NSInteger amountRead;
|
|
|
|
if (state->sourceIndex < 128)
|
|
{
|
|
toRead = state->sourceIndex;
|
|
}
|
|
amountRead = [stream read: discard maxLength: toRead];
|
|
/* If something goes wrong with the stream, return the stream
|
|
* error as our error.
|
|
*/
|
|
if (amountRead == 0)
|
|
{
|
|
state->error = [stream streamError];
|
|
state->bufferIndex = 0;
|
|
state->bufferLength = 0;
|
|
state->buffer[0] = 0;
|
|
}
|
|
state->sourceIndex -= amountRead;
|
|
}
|
|
|
|
/* Get the temporary buffer. We need to read from here so that we can read
|
|
* characters from the stream without advancing the stream position.
|
|
* If the stream doesn't do buffering, then we need to get data one character
|
|
* at a time.
|
|
*/
|
|
if (![stream getBuffer: &buffer length: &length])
|
|
{
|
|
uint8_t bytes[7] = { 0 };
|
|
|
|
switch (state->enc)
|
|
{
|
|
case NSUTF8StringEncoding:
|
|
{
|
|
// Read one UTF8 character from the stream
|
|
NSUInteger i = 0;
|
|
NSInteger n;
|
|
|
|
n = [stream read: &bytes[0] maxLength: 1];
|
|
if (n != 1)
|
|
{
|
|
state->error = [stream streamError];
|
|
if (state->error == nil)
|
|
{
|
|
state->error
|
|
= [NSError errorWithDomain: NSCocoaErrorDomain
|
|
code: 0
|
|
userInfo: nil];
|
|
}
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
if ((bytes[0] & 0xC0) == 0xC0)
|
|
{
|
|
for (i = 1; i <= 5; i++)
|
|
if ((bytes[0] & (0x40 >> i)) == 0)
|
|
break;
|
|
}
|
|
}
|
|
if (0 == i)
|
|
{
|
|
state->buffer[0] = bytes[0];
|
|
}
|
|
else
|
|
{
|
|
n = [stream read: &bytes[1] maxLength: i];
|
|
if (n == i)
|
|
{
|
|
str = [[NSString alloc] initWithUTF8String: (char*)bytes];
|
|
[str getCharacters: state->buffer
|
|
range: NSMakeRange(0,1)];
|
|
[str release];
|
|
}
|
|
else
|
|
{
|
|
state->error = [stream streamError];
|
|
if (state->error == nil)
|
|
{
|
|
state->error
|
|
= [NSError errorWithDomain: NSCocoaErrorDomain
|
|
code: 0
|
|
userInfo: nil];
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case NSUTF32LittleEndianStringEncoding:
|
|
{
|
|
[stream read: bytes maxLength: 4];
|
|
state->buffer[0] = (unichar)NSSwapLittleIntToHost
|
|
(*(unsigned int*)(void*)bytes);
|
|
break;
|
|
}
|
|
case NSUTF32BigEndianStringEncoding:
|
|
{
|
|
[stream read: bytes maxLength: 4];
|
|
state->buffer[0] = (unichar)NSSwapBigIntToHost
|
|
(*(unsigned int*)(void*)bytes);
|
|
break;
|
|
}
|
|
case NSUTF16LittleEndianStringEncoding:
|
|
{
|
|
[stream read: bytes maxLength: 2];
|
|
state->buffer[0] = (unichar)NSSwapLittleShortToHost
|
|
(*(unsigned short*)(void*)bytes);
|
|
break;
|
|
}
|
|
case NSUTF16BigEndianStringEncoding:
|
|
{
|
|
[stream read: bytes maxLength: 4];
|
|
state->buffer[0] = (unichar)NSSwapBigShortToHost
|
|
(*(unsigned short*)(void*)bytes);
|
|
break;
|
|
}
|
|
default:
|
|
GS_UNREACHABLE();
|
|
}
|
|
// Set the source index to -1 so it will be 0 when we've finished with it
|
|
state->sourceIndex = -1;
|
|
state->bufferIndex = 0;
|
|
state->bufferLength = 1;
|
|
return;
|
|
}
|
|
// Use an NSString to do the character set conversion. We could do this more
|
|
// efficiently. We could also reuse the string.
|
|
str = [[NSString alloc] initWithBytesNoCopy: buffer
|
|
length: length
|
|
encoding: state->enc
|
|
freeWhenDone: NO];
|
|
// Just use the string buffer fetch function to actually get the data
|
|
state->source = str;
|
|
updateStringBuffer(state);
|
|
RELEASE(str);
|
|
state->source = stream;
|
|
}
|
|
|
|
/**
|
|
* Returns the current character.
|
|
*/
|
|
static inline unichar
|
|
currentChar(ParserState *state)
|
|
{
|
|
if (state->bufferIndex >= state->bufferLength)
|
|
{
|
|
state->updateBuffer(state);
|
|
}
|
|
return state->buffer[state->bufferIndex];
|
|
}
|
|
|
|
/**
|
|
* Consumes a character.
|
|
*/
|
|
static inline unichar
|
|
consumeChar(ParserState *state)
|
|
{
|
|
state->sourceIndex++;
|
|
state->bufferIndex++;
|
|
if (state->bufferIndex >= state->bufferLength)
|
|
{
|
|
state->updateBuffer(state);
|
|
}
|
|
return currentChar(state);
|
|
}
|
|
|
|
/**
|
|
* Consumes all whitespace characters and returns the first non-space
|
|
* character. Returns 0 if we're past the end of the input.
|
|
*/
|
|
static inline unichar
|
|
consumeSpace(ParserState *state)
|
|
{
|
|
while (isspace(currentChar(state)))
|
|
{
|
|
consumeChar(state);
|
|
}
|
|
return currentChar(state);
|
|
}
|
|
|
|
/**
|
|
* Sets an error state.
|
|
*/
|
|
static void
|
|
parseError(ParserState *state)
|
|
{
|
|
/* TODO: Work out what stuff should go in this and probably add them to
|
|
* parameters for this function.
|
|
*/
|
|
NSDictionary *userInfo = [[NSDictionary alloc] initWithObjectsAndKeys:
|
|
_(@"JSON Parse error"), NSLocalizedDescriptionKey,
|
|
_(([NSString stringWithFormat: @"Unexpected character %c at index %"PRIdPTR,
|
|
(char)currentChar(state), state->sourceIndex])),
|
|
NSLocalizedFailureReasonErrorKey,
|
|
nil];
|
|
state->error = [NSError errorWithDomain: NSCocoaErrorDomain
|
|
code: 0
|
|
userInfo: userInfo];
|
|
RELEASE(userInfo);
|
|
}
|
|
|
|
|
|
NS_RETURNS_RETAINED static id parseValue(ParserState *state);
|
|
|
|
/**
|
|
* Parse a string, as defined by RFC4627, section 2.5
|
|
*/
|
|
NS_RETURNS_RETAINED static NSString*
|
|
parseString(ParserState *state)
|
|
{
|
|
NSMutableString *val = nil;
|
|
unichar buffer[BUFFER_SIZE];
|
|
int bufferIndex = 0;
|
|
unichar next;
|
|
|
|
if (state->error)
|
|
{
|
|
return nil;
|
|
}
|
|
|
|
if (currentChar(state) != '"')
|
|
{
|
|
parseError(state);
|
|
return nil;
|
|
}
|
|
|
|
next = consumeChar(state);
|
|
while ((next != 0) && (next != '"'))
|
|
{
|
|
// Unexpected end of stream
|
|
if (next == '\\')
|
|
{
|
|
next = consumeChar(state);
|
|
switch (next)
|
|
{
|
|
// Simple escapes, just ignore the leading '
|
|
case '"':
|
|
case '\\':
|
|
case '/':
|
|
break;
|
|
// Map to the unicode values specified in RFC4627
|
|
case 'b': next = 0x0008; break;
|
|
case 'f': next = 0x000c; break;
|
|
case 'n': next = 0x000a; break;
|
|
case 'r': next = 0x000d; break;
|
|
case 't': next = 0x0009; break;
|
|
// decode a unicode value from 4 hex digits
|
|
case 'u':
|
|
{
|
|
char hex[5] = {0};
|
|
unsigned i;
|
|
for (i = 0 ; i < 4 ; i++)
|
|
{
|
|
next = consumeChar(state);
|
|
if (!isxdigit(next))
|
|
{
|
|
[val release];
|
|
parseError(state);
|
|
return nil;
|
|
}
|
|
hex[i] = next;
|
|
}
|
|
// Parse 4 hex digits and a NULL terminator into a 16-bit
|
|
// unicode character ID.
|
|
next = (unichar)strtol(hex, 0, 16);
|
|
}
|
|
}
|
|
}
|
|
buffer[bufferIndex++] = next;
|
|
if (bufferIndex >= BUFFER_SIZE)
|
|
{
|
|
NSMutableString *str;
|
|
int len = bufferIndex;
|
|
|
|
bufferIndex = 0;
|
|
if (next < 0xd800 || next > 0xdbff)
|
|
{
|
|
str = [[NSMutableString alloc] initWithCharacters: buffer
|
|
length: len];
|
|
}
|
|
else
|
|
{
|
|
/* The most recent unicode character is the first half of a
|
|
* surrogate pair, so we need to defer it to the next chunk
|
|
* to make sure the whole unicode character is in the same
|
|
* string (otherwise we would have an invalid string).
|
|
*/
|
|
len--;
|
|
str = [[NSMutableString alloc] initWithCharacters: buffer
|
|
length: len];
|
|
buffer[bufferIndex++] = next;
|
|
}
|
|
if (nil == val)
|
|
{
|
|
val = str;
|
|
}
|
|
else
|
|
{
|
|
[val appendString: str];
|
|
RELEASE(str);
|
|
}
|
|
}
|
|
next = consumeChar(state);
|
|
}
|
|
|
|
if (currentChar(state) != '"')
|
|
{
|
|
[val release];
|
|
parseError(state);
|
|
return nil;
|
|
}
|
|
|
|
if (bufferIndex > 0)
|
|
{
|
|
NSMutableString *str;
|
|
|
|
str = [[NSMutableString alloc] initWithCharacters: buffer
|
|
length: bufferIndex];
|
|
if (nil == val)
|
|
{
|
|
val = str;
|
|
}
|
|
else
|
|
{
|
|
[val appendString: str];
|
|
[str release];
|
|
}
|
|
}
|
|
else if (nil == val)
|
|
{
|
|
val = [NSMutableString new];
|
|
}
|
|
// Consume the trailing "
|
|
consumeChar(state);
|
|
if (!state->mutableStrings)
|
|
{
|
|
if (NO == [val makeImmutable])
|
|
{
|
|
NSMutableString *m = val;
|
|
|
|
val = [m copy];
|
|
RELEASE(m);
|
|
}
|
|
}
|
|
return val;
|
|
}
|
|
|
|
/**
|
|
* Parses a number, as defined by section 2.4 of the JSON specification.
|
|
*/
|
|
NS_RETURNS_RETAINED static NSNumber*
|
|
parseNumber(ParserState *state)
|
|
{
|
|
unichar c = currentChar(state);
|
|
char numberBuffer[128];
|
|
char *number = numberBuffer;
|
|
int bufferSize = 128;
|
|
int parsedSize = 0;
|
|
double num;
|
|
|
|
// Define a macro to add a character to the buffer, because we'll need to do
|
|
// it a lot. This resizes the buffer if required.
|
|
#define BUFFER(x) do {\
|
|
if (parsedSize == bufferSize)\
|
|
{\
|
|
bufferSize *= 2;\
|
|
if (number == numberBuffer)\
|
|
{\
|
|
number = malloc(bufferSize);\
|
|
memcpy(number, numberBuffer, sizeof(numberBuffer));\
|
|
}\
|
|
else\
|
|
{\
|
|
number = realloc(number, bufferSize);\
|
|
}\
|
|
}\
|
|
number[parsedSize++] = (char)x; } while (0)
|
|
// JSON numbers must start with a - or a digit
|
|
if (!(c == '-' || isdigit(c)))
|
|
{
|
|
parseError(state);
|
|
return nil;
|
|
}
|
|
// digit or -
|
|
BUFFER(c);
|
|
// Read as many digits as we see
|
|
while (isdigit(c = consumeChar(state)))
|
|
{
|
|
BUFFER(c);
|
|
}
|
|
// Parse the fractional component, if there is one
|
|
if ('.' == c)
|
|
{
|
|
BUFFER(c);
|
|
while (isdigit(c = consumeChar(state)))
|
|
{
|
|
BUFFER(c);
|
|
}
|
|
}
|
|
// parse the exponent if there is one
|
|
if ('e' == tolower(c))
|
|
{
|
|
BUFFER(c);
|
|
c = consumeChar(state);
|
|
// The exponent must be a valid number
|
|
if (!(c == '-' || c == '+' || isdigit(c)))
|
|
{
|
|
if (number != numberBuffer)
|
|
{
|
|
free(number);
|
|
}
|
|
parseError(state);
|
|
return nil;
|
|
}
|
|
BUFFER(c);
|
|
while (isdigit(c = consumeChar(state)))
|
|
{
|
|
BUFFER(c);
|
|
}
|
|
}
|
|
// Add a null terminator on the buffer.
|
|
BUFFER(0);
|
|
num = strtod(number, 0);
|
|
if (number != numberBuffer)
|
|
{
|
|
free(number);
|
|
}
|
|
return [[NSNumber alloc] initWithDouble: num];
|
|
#undef BUFFER
|
|
}
|
|
/**
|
|
* Parse an array, as described by section 2.3 of RFC 4627.
|
|
*/
|
|
NS_RETURNS_RETAINED static NSArray*
|
|
parseArray(ParserState *state)
|
|
{
|
|
unichar c = consumeSpace(state);
|
|
NSMutableArray *array;
|
|
|
|
if (c != '[')
|
|
{
|
|
parseError(state);
|
|
return nil;
|
|
}
|
|
// Eat the [
|
|
consumeChar(state);
|
|
array = [NSMutableArray new];
|
|
c = consumeSpace(state);
|
|
while (c != ']')
|
|
{
|
|
// If this fails, it will already set the error, so we don't have to.
|
|
id obj = parseValue(state);
|
|
if (nil == obj)
|
|
{
|
|
[array release];
|
|
return nil;
|
|
}
|
|
[array addObject: obj];
|
|
[obj release];
|
|
c = consumeSpace(state);
|
|
if (c == ',')
|
|
{
|
|
consumeChar(state);
|
|
c = consumeSpace(state);
|
|
}
|
|
}
|
|
// Eat the trailing ]
|
|
consumeChar(state);
|
|
if (!state->mutableContainers)
|
|
{
|
|
if (NO == [array makeImmutable])
|
|
{
|
|
id a = array;
|
|
|
|
array = [array copy];
|
|
RELEASE(a);
|
|
}
|
|
}
|
|
return array;
|
|
}
|
|
|
|
NS_RETURNS_RETAINED static NSDictionary*
|
|
parseObject(ParserState *state)
|
|
{
|
|
unichar c = consumeSpace(state);
|
|
NSMutableDictionary *dict;
|
|
|
|
if (c != '{')
|
|
{
|
|
parseError(state);
|
|
return nil;
|
|
}
|
|
// Eat the {
|
|
consumeChar(state);
|
|
dict = [NSMutableDictionary new];
|
|
c = consumeSpace(state);
|
|
while (c != '}')
|
|
{
|
|
id key = parseString(state);
|
|
id obj;
|
|
|
|
if (nil == key)
|
|
{
|
|
[dict release];
|
|
return nil;
|
|
}
|
|
c = consumeSpace(state);
|
|
if (':' != c)
|
|
{
|
|
[key release];
|
|
[dict release];
|
|
parseError(state);
|
|
return nil;
|
|
}
|
|
// Eat the :
|
|
consumeChar(state);
|
|
obj = parseValue(state);
|
|
if (nil == obj)
|
|
{
|
|
[key release];
|
|
[dict release];
|
|
return nil;
|
|
}
|
|
[dict setObject: obj forKey: key];
|
|
[key release];
|
|
[obj release];
|
|
c = consumeSpace(state);
|
|
if (c == ',')
|
|
{
|
|
consumeChar(state);
|
|
}
|
|
c = consumeSpace(state);
|
|
}
|
|
// Eat the trailing }
|
|
consumeChar(state);
|
|
if (!state->mutableContainers)
|
|
{
|
|
if (NO == [dict makeImmutable])
|
|
{
|
|
id d = dict;
|
|
|
|
dict = [dict copy];
|
|
RELEASE(d);
|
|
}
|
|
}
|
|
return dict;
|
|
}
|
|
|
|
/**
|
|
* Parses a JSON value, as defined by RFC4627, section 2.1.
|
|
*/
|
|
NS_RETURNS_RETAINED static id
|
|
parseValue(ParserState *state)
|
|
{
|
|
unichar c;
|
|
|
|
if (state->error) { return nil; };
|
|
c = consumeSpace(state);
|
|
/* 2.1: A JSON value MUST be an object, array, number, or string,
|
|
* or one of the following three literal names:
|
|
* false null true
|
|
*/
|
|
switch (c)
|
|
{
|
|
case (unichar)'"':
|
|
return parseString(state);
|
|
case (unichar)'[':
|
|
return parseArray(state);
|
|
case (unichar)'{':
|
|
return parseObject(state);
|
|
case (unichar)'-':
|
|
case (unichar)'0' ... (unichar)'9':
|
|
return parseNumber(state);
|
|
// Literal null
|
|
case 'n':
|
|
{
|
|
if ((consumeChar(state) == 'u')
|
|
&& (consumeChar(state) == 'l')
|
|
&& (consumeChar(state) == 'l'))
|
|
{
|
|
consumeChar(state);
|
|
return [[NSNull null] retain];
|
|
}
|
|
break;
|
|
}
|
|
// literal
|
|
case 't':
|
|
{
|
|
if ((consumeChar(state) == 'r')
|
|
&& (consumeChar(state) == 'u')
|
|
&& (consumeChar(state) == 'e'))
|
|
{
|
|
consumeChar(state);
|
|
return [boolY retain];
|
|
}
|
|
break;
|
|
}
|
|
case 'f':
|
|
{
|
|
if ((consumeChar(state) == 'a')
|
|
&& (consumeChar(state) == 'l')
|
|
&& (consumeChar(state) == 's')
|
|
&& (consumeChar(state) == 'e'))
|
|
{
|
|
consumeChar(state);
|
|
return [boolN retain];
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
parseError(state);
|
|
return nil;
|
|
}
|
|
|
|
/**
|
|
* We have to autodetect the string encoding. We know that it is some
|
|
* unicode encoding, which may or may not contain a BOM. If it contains a
|
|
* BOM, then we need to skip that. If it doesn't, then we need to work out
|
|
* the encoding from the position of the NULLs. The first two characters are
|
|
* guaranteed to be ASCII in a JSON stream, so we can work out the encoding
|
|
* from the pattern of NULLs.
|
|
*/
|
|
static void
|
|
getEncoding(const uint8_t BOM[4], ParserState *state)
|
|
{
|
|
NSStringEncoding enc = NSUTF8StringEncoding;
|
|
int BOMLength = 0;
|
|
|
|
if ((BOM[0] == 0xEF) && (BOM[1] == 0xBB) && (BOM[2] == 0xBF))
|
|
{
|
|
BOMLength = 3;
|
|
}
|
|
else if ((BOM[0] == 0xFE) && (BOM[1] == 0xFF))
|
|
{
|
|
BOMLength = 2;
|
|
enc = NSUTF16BigEndianStringEncoding;
|
|
}
|
|
else if ((BOM[0] == 0xFF) && (BOM[1] == 0xFE))
|
|
{
|
|
if ((BOM[2] == 0) && (BOM[3] == 0))
|
|
{
|
|
BOMLength = 4;
|
|
enc = NSUTF32LittleEndianStringEncoding;
|
|
}
|
|
else
|
|
{
|
|
BOMLength = 2;
|
|
enc = NSUTF16LittleEndianStringEncoding;
|
|
}
|
|
}
|
|
else if ((BOM[0] == 0)
|
|
&& (BOM[1] == 0)
|
|
&& (BOM[2] == 0xFE)
|
|
&& (BOM[3] == 0xFF))
|
|
{
|
|
BOMLength = 4;
|
|
enc = NSUTF32BigEndianStringEncoding;
|
|
}
|
|
else if (BOM[0] == 0)
|
|
{
|
|
// TODO: Throw an error if this doesn't match one of the patterns
|
|
// described in section 3 of RFC4627
|
|
if (BOM[1] == 0)
|
|
{
|
|
enc = NSUTF32BigEndianStringEncoding;
|
|
}
|
|
else
|
|
{
|
|
enc = NSUTF16BigEndianStringEncoding;
|
|
}
|
|
}
|
|
else if (BOM[1] == 0)
|
|
{
|
|
if (BOM[2] == 0)
|
|
{
|
|
enc = NSUTF32LittleEndianStringEncoding;
|
|
}
|
|
else
|
|
{
|
|
enc = NSUTF16LittleEndianStringEncoding;
|
|
}
|
|
}
|
|
state->enc = enc;
|
|
state->BOMLength = BOMLength;
|
|
}
|
|
|
|
/**
|
|
* Classes that are permitted to be written.
|
|
*/
|
|
static Class NSArrayClass;
|
|
static Class NSDictionaryClass;
|
|
static Class NSNullClass;
|
|
static Class NSNumberClass;
|
|
static Class NSStringClass;
|
|
|
|
static NSMutableCharacterSet *escapeSet;
|
|
|
|
static inline void
|
|
writeTabs(NSMutableString *output, NSInteger tabs)
|
|
{
|
|
NSInteger i;
|
|
|
|
for (i = 0 ; i < tabs ; i++)
|
|
{
|
|
[output appendString: @"\t"];
|
|
}
|
|
}
|
|
|
|
static inline void
|
|
writeNewline(NSMutableString *output, NSInteger tabs)
|
|
{
|
|
if (tabs >= 0)
|
|
{
|
|
[output appendString: @"\n"];
|
|
}
|
|
}
|
|
|
|
static BOOL
|
|
writeObject(id obj, NSMutableString *output, NSInteger tabs)
|
|
{
|
|
if ([obj isKindOfClass: NSArrayClass])
|
|
{
|
|
BOOL writeComma = NO;
|
|
[output appendString: @"["];
|
|
FOR_IN(id, o, obj)
|
|
if (writeComma)
|
|
{
|
|
[output appendString: @","];
|
|
}
|
|
writeComma = YES;
|
|
writeNewline(output, tabs);
|
|
writeTabs(output, tabs);
|
|
writeObject(o, output, tabs + 1);
|
|
END_FOR_IN(obj)
|
|
writeNewline(output, tabs);
|
|
writeTabs(output, tabs);
|
|
[output appendString: @"]"];
|
|
}
|
|
else if ([obj isKindOfClass: NSDictionaryClass])
|
|
{
|
|
BOOL writeComma = NO;
|
|
[output appendString: @"{"];
|
|
FOR_IN(id, o, obj)
|
|
// Keys in dictionaries must be strings
|
|
if (![o isKindOfClass: NSStringClass]) { return NO; }
|
|
if (writeComma)
|
|
{
|
|
[output appendString: @","];
|
|
}
|
|
writeComma = YES;
|
|
writeNewline(output, tabs);
|
|
writeTabs(output, tabs);
|
|
writeObject(o, output, tabs + 1);
|
|
[output appendString: @": "];
|
|
writeObject([obj objectForKey: o], output, tabs + 1);
|
|
END_FOR_IN(obj)
|
|
writeNewline(output, tabs);
|
|
writeTabs(output, tabs);
|
|
[output appendString: @"}"];
|
|
}
|
|
else if ([obj isKindOfClass: NSStringClass])
|
|
{
|
|
NSString *str = (NSString*)obj;
|
|
unsigned length = [str length];
|
|
|
|
if (length == 0)
|
|
{
|
|
[output appendString: @"\"\""];
|
|
}
|
|
else
|
|
{
|
|
unsigned size = 2;
|
|
unichar *from;
|
|
unsigned i = 0;
|
|
unichar *to;
|
|
unsigned j = 0;
|
|
|
|
from = NSZoneMalloc (NSDefaultMallocZone(), sizeof(unichar) * length);
|
|
[str getCharacters: from];
|
|
|
|
for (i = 0; i < length; i++)
|
|
{
|
|
unichar c = from[i];
|
|
|
|
if (c == '"' || c == '\\' || c == '\b'
|
|
|| c == '\f' || c == '\n' || c == '\r' || c == '\t')
|
|
{
|
|
size += 2;
|
|
}
|
|
else if (c < 0x20 || c > 0x7f)
|
|
{
|
|
size += 6;
|
|
}
|
|
else
|
|
{
|
|
size++;
|
|
}
|
|
}
|
|
|
|
to = NSZoneMalloc (NSDefaultMallocZone(), sizeof(unichar) * size);
|
|
to[j++] = '"';
|
|
for (i = 0; i < length; i++)
|
|
{
|
|
unichar c = from[i];
|
|
|
|
if (c == '"' || c == '\\' || c == '\b'
|
|
|| c == '\f' || c == '\n' || c == '\r' || c == '\t')
|
|
{
|
|
to[j++] = '\\';
|
|
switch (c)
|
|
{
|
|
case '\\': to[j++] = '\\'; break;
|
|
case '\b': to[j++] = 'b'; break;
|
|
case '\f': to[j++] = 'f'; break;
|
|
case '\n': to[j++] = 'n'; break;
|
|
case '\r': to[j++] = 'r'; break;
|
|
case '\t': to[j++] = 't'; break;
|
|
default: to[j++] = '"'; break;
|
|
}
|
|
}
|
|
else if (c < 0x20 || c > 0x7f)
|
|
{
|
|
char buf[5];
|
|
|
|
to[j++] = '\\';
|
|
to[j++] = 'u';
|
|
sprintf(buf, "%04x", c);
|
|
to[j++] = buf[0];
|
|
to[j++] = buf[1];
|
|
to[j++] = buf[2];
|
|
to[j++] = buf[3];
|
|
}
|
|
else
|
|
{
|
|
to[j++] = c;
|
|
}
|
|
}
|
|
to[j] = '"';
|
|
str = [[NSStringClass alloc] initWithCharacters: to length: size];
|
|
NSZoneFree (NSDefaultMallocZone (), to);
|
|
NSZoneFree (NSDefaultMallocZone (), from);
|
|
[output appendString: str];
|
|
[str release];
|
|
}
|
|
}
|
|
else if (obj == boolN)
|
|
{
|
|
[output appendString: @"false"];
|
|
}
|
|
else if (obj == boolY)
|
|
{
|
|
[output appendString: @"true"];
|
|
}
|
|
else if ([obj isKindOfClass: NSNumberClass])
|
|
{
|
|
const char *t = [obj objCType];
|
|
|
|
if (strchr("csilq", *t) != 0)
|
|
{
|
|
long long i = [(NSNumber*)obj longLongValue];
|
|
|
|
[output appendFormat: @"%lld", i];
|
|
}
|
|
else if (strchr("CSILQ", *t) != 0)
|
|
{
|
|
unsigned long long u = [(NSNumber *)obj unsignedLongLongValue];
|
|
[output appendFormat: @"%llu", u];
|
|
}
|
|
else
|
|
{
|
|
[output appendFormat: @"%.17g", [(NSNumber*)obj doubleValue]];
|
|
}
|
|
}
|
|
else if ([obj isKindOfClass: NSNullClass])
|
|
{
|
|
[output appendString: @"null"];
|
|
}
|
|
else
|
|
{
|
|
return NO;
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
@implementation NSJSONSerialization
|
|
+ (void) initialize
|
|
{
|
|
static BOOL beenHere = NO;
|
|
|
|
if (NO == beenHere)
|
|
{
|
|
NSNullClass = [NSNull class];
|
|
NSArrayClass = [NSArray class];
|
|
NSStringClass = [NSString class];
|
|
NSDictionaryClass = [NSDictionary class];
|
|
NSNumberClass = [NSNumber class];
|
|
escapeSet = [NSMutableCharacterSet new];
|
|
[[NSObject leakAt: &escapeSet] release];
|
|
[escapeSet addCharactersInString: @"\"\\"];
|
|
boolN = [[NSNumber alloc] initWithBool: NO];
|
|
[[NSObject leakAt: &boolN] release];
|
|
boolY = [[NSNumber alloc] initWithBool: YES];
|
|
[[NSObject leakAt: &boolY] release];
|
|
beenHere = YES;
|
|
}
|
|
}
|
|
|
|
+ (NSData*) dataWithJSONObject: (id)obj
|
|
options: (NSJSONWritingOptions)opt
|
|
error: (NSError **)error
|
|
{
|
|
/* Temporary string: allocate more space than we are likely to use so we just
|
|
* quickly claim a page and then give it back later
|
|
*/
|
|
NSMutableString *str = [[NSMutableString alloc] initWithCapacity: 4096];
|
|
NSData *data = nil;
|
|
NSUInteger tabs;
|
|
|
|
tabs = ((opt & NSJSONWritingPrettyPrinted) == NSJSONWritingPrettyPrinted) ?
|
|
0 : NSIntegerMin;
|
|
if (writeObject(obj, str, tabs))
|
|
{
|
|
data = [str dataUsingEncoding: NSUTF8StringEncoding];
|
|
if (NULL != error)
|
|
{
|
|
*error = nil;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (NULL != error)
|
|
{
|
|
NSDictionary *userInfo = [[NSDictionary alloc] initWithObjectsAndKeys:
|
|
_(@"JSON writing error"), NSLocalizedDescriptionKey,
|
|
nil];
|
|
*error = [NSError errorWithDomain: NSCocoaErrorDomain
|
|
code: 0
|
|
userInfo: userInfo];
|
|
RELEASE(userInfo);
|
|
}
|
|
}
|
|
[str release];
|
|
return data;
|
|
}
|
|
|
|
+ (BOOL) isValidJSONObject: (id)obj
|
|
{
|
|
return writeObject(obj, nil, NSIntegerMin);
|
|
}
|
|
|
|
+ (id) JSONObjectWithData: (NSData *)data
|
|
options: (NSJSONReadingOptions)opt
|
|
error: (NSError **)error
|
|
{
|
|
uint8_t BOM[4];
|
|
ParserState p = { 0 };
|
|
id obj;
|
|
|
|
[data getBytes: BOM length: 4];
|
|
getEncoding(BOM, &p);
|
|
p.source = [[NSString alloc] initWithData: data encoding: p.enc];
|
|
p.updateBuffer = updateStringBuffer;
|
|
p.mutableContainers
|
|
= (opt & NSJSONReadingMutableContainers) == NSJSONReadingMutableContainers;
|
|
p.mutableStrings
|
|
= (opt & NSJSONReadingMutableLeaves) == NSJSONReadingMutableLeaves;
|
|
obj = parseValue(&p);
|
|
[p.source release];
|
|
if (NULL != error)
|
|
{
|
|
*error = p.error;
|
|
}
|
|
return [obj autorelease];
|
|
}
|
|
|
|
+ (id) JSONObjectWithStream: (NSInputStream *)stream
|
|
options: (NSJSONReadingOptions)opt
|
|
error: (NSError **)error
|
|
{
|
|
uint8_t BOM[4];
|
|
ParserState p = { 0 };
|
|
id obj;
|
|
|
|
// TODO: Handle failure here!
|
|
[stream read: (uint8_t*)BOM maxLength: 4];
|
|
getEncoding(BOM, &p);
|
|
p.mutableContainers
|
|
= (opt & NSJSONReadingMutableContainers) == NSJSONReadingMutableContainers;
|
|
p.mutableStrings
|
|
= (opt & NSJSONReadingMutableLeaves) == NSJSONReadingMutableLeaves;
|
|
if (p.BOMLength < 4)
|
|
{
|
|
p.source = [[NSString alloc] initWithBytesNoCopy: &BOM[p.BOMLength]
|
|
length: 4 - p.BOMLength
|
|
encoding: p.enc
|
|
freeWhenDone: NO];
|
|
updateStringBuffer(&p);
|
|
RELEASE(p.source);
|
|
/* Negative source index because we are before the
|
|
* current point in the buffer
|
|
*/
|
|
p.sourceIndex = p.BOMLength - 4;
|
|
}
|
|
p.source = stream;
|
|
p.updateBuffer = updateStreamBuffer;
|
|
obj = parseValue(&p);
|
|
// Consume any data in the stream that we've failed to read
|
|
updateStreamBuffer(&p);
|
|
if (NULL != error)
|
|
{
|
|
*error = p.error;
|
|
}
|
|
return [obj autorelease];
|
|
}
|
|
|
|
+ (NSInteger) writeJSONObject: (id)obj
|
|
toStream: (NSOutputStream *)stream
|
|
options: (NSJSONWritingOptions)opt
|
|
error: (NSError **)error
|
|
{
|
|
NSData *data = [self dataWithJSONObject: obj options: opt error: error];
|
|
|
|
if (nil != data)
|
|
{
|
|
const char *bytes = [data bytes];
|
|
NSUInteger toWrite = [data length];
|
|
|
|
while (toWrite > 0)
|
|
{
|
|
NSInteger wrote = [stream write: (uint8_t*)bytes maxLength: toWrite];
|
|
bytes += wrote;
|
|
toWrite -= wrote;
|
|
if (0 == wrote)
|
|
{
|
|
if (NULL != error)
|
|
{
|
|
*error = [stream streamError];
|
|
}
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
return [data length];
|
|
}
|
|
@end
|