JSValue.mm   [plain text]

/*
* Copyright (C) 2013-2018 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

#include "config.h"

#import "APICast.h"
#import "DateInstance.h"
#import "Error.h"
#import "Exception.h"
#import "JavaScriptCore.h"
#import "JSContextInternal.h"
#import "JSObjectRefPrivate.h"
#import "JSVirtualMachineInternal.h"
#import "JSValueInternal.h"
#import "JSValuePrivate.h"
#import "JSWrapperMap.h"
#import "ObjcRuntimeExtras.h"
#import "JSCInlines.h"
#import "JSCJSValue.h"
#import "Strong.h"
#import "StrongInlines.h"
#import <wtf/Expected.h>
#import <wtf/HashMap.h>
#import <wtf/HashSet.h>
#import <wtf/Lock.h>
#import <wtf/ObjCRuntimeExtras.h>
#import <wtf/Vector.h>
#import <wtf/text/WTFString.h>
#import <wtf/text/StringHash.h>

#if ENABLE(REMOTE_INSPECTOR)
#import "CallFrame.h"
#import "JSGlobalObject.h"
#import "JSGlobalObjectInspectorController.h"
#endif

#if JSC_OBJC_API_ENABLED

NSString * const JSPropertyDescriptorWritableKey = @"writable";
NSString * const JSPropertyDescriptorEnumerableKey = @"enumerable";
NSString * const JSPropertyDescriptorConfigurableKey = @"configurable";
NSString * const JSPropertyDescriptorValueKey = @"value";
NSString * const JSPropertyDescriptorGetKey = @"get";
NSString * const JSPropertyDescriptorSetKey = @"set";

@implementation JSValue {
JSValueRef m_value;
}

- (void)dealloc
{
JSValueUnprotect([_context JSGlobalContextRef], m_value);
[_context release];
_context = nil;
[super dealloc];
}

- (NSString *)description
{
if (id wrapped = tryUnwrapObjcObject([_context JSGlobalContextRef], m_value))
return [wrapped description];
return [self toString];
}

- (JSValueRef)JSValueRef
{
return m_value;
}

+ (JSValue *)valueWithObject:(id)value inContext:(JSContext *)context
{
return [JSValue valueWithJSValueRef:objectToValue(context, value) inContext:context];
}

+ (JSValue *)valueWithBool:(BOOL)value inContext:(JSContext *)context
{
return [JSValue valueWithJSValueRef:JSValueMakeBoolean([context JSGlobalContextRef], value) inContext:context];
}

+ (JSValue *)valueWithDouble:(double)value inContext:(JSContext *)context
{
return [JSValue valueWithJSValueRef:JSValueMakeNumber([context JSGlobalContextRef], value) inContext:context];
}

+ (JSValue *)valueWithInt32:(int32_t)value inContext:(JSContext *)context
{
return [JSValue valueWithJSValueRef:JSValueMakeNumber([context JSGlobalContextRef], value) inContext:context];
}

+ (JSValue *)valueWithUInt32:(uint32_t)value inContext:(JSContext *)context
{
return [JSValue valueWithJSValueRef:JSValueMakeNumber([context JSGlobalContextRef], value) inContext:context];
}

+ (JSValue *)valueWithNewObjectInContext:(JSContext *)context
{
return [JSValue valueWithJSValueRef:JSObjectMake([context JSGlobalContextRef], 0, 0) inContext:context];
}

+ (JSValue *)valueWithNewArrayInContext:(JSContext *)context
{
return [JSValue valueWithJSValueRef:JSObjectMakeArray([context JSGlobalContextRef], 0, NULL, 0) inContext:context];
}

+ (JSValue *)valueWithNewRegularExpressionFromPattern:(NSString *)pattern flags:(NSString *)flags inContext:(JSContext *)context
{
auto patternString = OpaqueJSString::tryCreate(pattern);
auto flagsString = OpaqueJSString::tryCreate(flags);
JSValueRef arguments[2] = { JSValueMakeString([context JSGlobalContextRef], patternString.get()), JSValueMakeString([context JSGlobalContextRef], flagsString.get()) };
return [JSValue valueWithJSValueRef:JSObjectMakeRegExp([context JSGlobalContextRef], 2, arguments, 0) inContext:context];
}

+ (JSValue *)valueWithNewErrorFromMessage:(NSString *)message inContext:(JSContext *)context
{
auto string = OpaqueJSString::tryCreate(message);
JSValueRef argument = JSValueMakeString([context JSGlobalContextRef], string.get());
return [JSValue valueWithJSValueRef:JSObjectMakeError([context JSGlobalContextRef], 1, &argument, 0) inContext:context];
}

+ (JSValue *)valueWithNullInContext:(JSContext *)context
{
return [JSValue valueWithJSValueRef:JSValueMakeNull([context JSGlobalContextRef]) inContext:context];
}

+ (JSValue *)valueWithUndefinedInContext:(JSContext *)context
{
return [JSValue valueWithJSValueRef:JSValueMakeUndefined([context JSGlobalContextRef]) inContext:context];
}

+ (JSValue *)valueWithNewSymbolFromDescription:(NSString *)description inContext:(JSContext *)context
{
auto string = OpaqueJSString::tryCreate(description);
return [JSValue valueWithJSValueRef:JSValueMakeSymbol([context JSGlobalContextRef], string.get()) inContext:context];
}

+ (JSValue *)valueWithNewPromiseInContext:(JSContext *)context fromExecutor:(void (^)(JSValue *, JSValue *))executor
{
JSObjectRef resolve;
JSObjectRef reject;
JSValueRef exception = nullptr;
JSObjectRef promise = JSObjectMakeDeferredPromise([context JSGlobalContextRef], &resolve, &reject, &exception);
if (exception) {
[context notifyException:exception];
return [JSValue valueWithUndefinedInContext:context];
}

JSValue *result = [JSValue valueWithJSValueRef:promise inContext:context];
JSValue *rejection = [JSValue valueWithJSValueRef:reject inContext:context];
CallbackData callbackData;
const size_t argumentCount = 2;
JSValueRef arguments[argumentCount];
arguments[0] = resolve;
arguments[1] = reject;

[context beginCallbackWithData:&callbackData calleeValue:nullptr thisValue:promise argumentCount:argumentCount arguments:arguments];
executor([JSValue valueWithJSValueRef:resolve inContext:context], rejection);
if (context.exception)
[rejection callWithArguments:@[context.exception]];
[context endCallbackWithData:&callbackData];

return result;
}

+ (JSValue *)valueWithNewPromiseResolvedWithResult:(id)result inContext:(JSContext *)context
{
return [JSValue valueWithNewPromiseInContext:context fromExecutor:^(JSValue *resolve, JSValue *) {
[resolve callWithArguments:@[result]];
}];
}

+ (JSValue *)valueWithNewPromiseRejectedWithReason:(id)reason inContext:(JSContext *)context
{
return [JSValue valueWithNewPromiseInContext:context fromExecutor:^(JSValue *, JSValue *reject) {
[reject callWithArguments:@[reason]];
}];
}

- (id)toObject
{
return valueToObject(_context, m_value);
}

- (id)toObjectOfClass:(Class)expectedClass
{
id result = [self toObject];
return [result isKindOfClass:expectedClass] ? result : nil;
}

- (BOOL)toBool
{
return JSValueToBoolean([_context JSGlobalContextRef], m_value);
}

- (double)toDouble
{
JSValueRef exception = 0;
double result = JSValueToNumber([_context JSGlobalContextRef], m_value, &exception);
if (exception) {
[_context notifyException:exception];
return std::numeric_limits<double>::quiet_NaN();
}

return result;
}

- (int32_t)toInt32
{
return JSC::toInt32([self toDouble]);
}

- (uint32_t)toUInt32
{
return JSC::toUInt32([self toDouble]);
}

- (NSNumber *)toNumber
{
JSValueRef exception = 0;
id result = valueToNumber([_context JSGlobalContextRef], m_value, &exception);
if (exception)
[_context notifyException:exception];
return result;
}

- (NSString *)toString
{
JSValueRef exception = 0;
id result = valueToString([_context JSGlobalContextRef], m_value, &exception);
if (exception)
[_context notifyException:exception];
return result;
}

- (NSDate *)toDate
{
JSValueRef exception = 0;
id result = valueToDate([_context JSGlobalContextRef], m_value, &exception);
if (exception)
[_context notifyException:exception];
return result;
}

- (NSArray *)toArray
{
JSValueRef exception = 0;
id result = valueToArray([_context JSGlobalContextRef], m_value, &exception);
if (exception)
[_context notifyException:exception];
return result;
}

- (NSDictionary *)toDictionary
{
JSValueRef exception = 0;
id result = valueToDictionary([_context JSGlobalContextRef], m_value, &exception);
if (exception)
[_context notifyException:exception];
return result;
}

template<typename Result, typename NSStringFunction, typename JSValueFunction, typename... Types>
inline Expected<Result, JSValueRef> performPropertyOperation(NSStringFunction stringFunction, JSValueFunction jsFunction, JSValue* value, id propertyKey, Types... arguments)
{
JSContext* context = [value context];
JSValueRef exception = nullptr;
JSObjectRef object = JSValueToObject([context JSGlobalContextRef], [value JSValueRef], &exception);
if (exception)
return Unexpected<JSValueRef>(exception);

Result result;
// If it's a NSString already, reduce indirection and just pass the NSString.
if ([propertyKey isKindOfClass:[NSString class]]) {
auto name = OpaqueJSString::tryCreate((NSString *)propertyKey);
result = stringFunction([context JSGlobalContextRef], object, name.get(), arguments..., &exception);
} else
result = jsFunction([context JSGlobalContextRef], object, [[JSValue valueWithObject:propertyKey inContext:context] JSValueRef], arguments..., &exception);
return Expected<Result, JSValueRef>(result);
}

- (JSValue *)valueForProperty:(id)key
{
auto result = performPropertyOperation<JSValueRef>(JSObjectGetProperty, JSObjectGetPropertyForKey, self, key);
if (!result)
return [_context valueFromNotifyException:result.error()];

return [JSValue valueWithJSValueRef:result.value() inContext:_context];
}

- (void)setValue:(id)value forProperty:(JSValueProperty)key
{
// We need Unit business because void can't be assigned to in performPropertyOperation and I don't want to duplicate the code...
using Unit = std::tuple<>;
auto stringSetProperty = [] (auto... args) -> Unit {
JSObjectSetProperty(args...);
return { };
};

auto jsValueSetProperty = [] (auto... args) -> Unit {
JSObjectSetPropertyForKey(args...);
return { };
};

auto result = performPropertyOperation<Unit>(stringSetProperty, jsValueSetProperty, self, key, objectToValue(_context, value), kJSPropertyAttributeNone);
if (!result) {
[_context notifyException:result.error()];
return;
}
}

- (BOOL)deleteProperty:(JSValueProperty)key
{
Expected<BOOL, JSValueRef> result = performPropertyOperation<BOOL>(JSObjectDeleteProperty, JSObjectDeletePropertyForKey, self, key);
if (!result)
return [_context boolFromNotifyException:result.error()];
return result.value();
}

- (BOOL)hasProperty:(JSValueProperty)key
{
// The C-api doesn't return an exception value for the string version of has property.
auto stringHasProperty = [] (JSContextRef ctx, JSObjectRef object, JSStringRef propertyName, JSValueRef*) -> BOOL {
return JSObjectHasProperty(ctx, object, propertyName);
};

Expected<BOOL, JSValueRef> result = performPropertyOperation<BOOL>(stringHasProperty, JSObjectHasPropertyForKey, self, key);
if (!result)
return [_context boolFromNotifyException:result.error()];
return result.value();
}

- (void)defineProperty:(JSValueProperty)key descriptor:(id)descriptor
{
[[_context globalObject][@"Object"] invokeMethod:@"defineProperty" withArguments:@[ self, key, descriptor ]];
}

- (JSValue *)valueAtIndex:(NSUInteger)index
{
// Properties that are higher than an unsigned value can hold are converted to a double then inserted as a normal property.
// Indices that are bigger than the max allowed index size (UINT_MAX - 1) will be handled internally in get().
if (index != (unsigned)index)
return [self valueForProperty:[[JSValue valueWithDouble:index inContext:_context] toString]];

JSValueRef exception = 0;
JSObjectRef object = JSValueToObject([_context JSGlobalContextRef], m_value, &exception);
if (exception)
return [_context valueFromNotifyException:exception];

JSValueRef result = JSObjectGetPropertyAtIndex([_context JSGlobalContextRef], object, (unsigned)index, &exception);
if (exception)
return [_context valueFromNotifyException:exception];

return [JSValue valueWithJSValueRef:result inContext:_context];
}

- (void)setValue:(id)value atIndex:(NSUInteger)index
{
// Properties that are higher than an unsigned value can hold are converted to a double, then inserted as a normal property.
// Indices that are bigger than the max allowed index size (UINT_MAX - 1) will be handled internally in putByIndex().
if (index != (unsigned)index)
return [self setValue:value forProperty:[[JSValue valueWithDouble:index inContext:_context] toString]];

JSValueRef exception = 0;
JSObjectRef object = JSValueToObject([_context JSGlobalContextRef], m_value, &exception);
if (exception) {
[_context notifyException:exception];
return;
}

JSObjectSetPropertyAtIndex([_context JSGlobalContextRef], object, (unsigned)index, objectToValue(_context, value), &exception);
if (exception) {
[_context notifyException:exception];
return;
}
}

- (BOOL)isUndefined
{
return JSValueIsUndefined([_context JSGlobalContextRef], m_value);
}

- (BOOL)isNull
{
return JSValueIsNull([_context JSGlobalContextRef], m_value);
}

- (BOOL)isBoolean
{
return JSValueIsBoolean([_context JSGlobalContextRef], m_value);
}

- (BOOL)isNumber
{
return JSValueIsNumber([_context JSGlobalContextRef], m_value);
}

- (BOOL)isString
{
return JSValueIsString([_context JSGlobalContextRef], m_value);
}

- (BOOL)isObject
{
return JSValueIsObject([_context JSGlobalContextRef], m_value);
}

- (BOOL)isSymbol
{
return JSValueIsSymbol([_context JSGlobalContextRef], m_value);
}

- (BOOL)isArray
{
return JSValueIsArray([_context JSGlobalContextRef], m_value);
}

- (BOOL)isDate
{
return JSValueIsDate([_context JSGlobalContextRef], m_value);
}

- (BOOL)isEqualToObject:(id)value
{
return JSValueIsStrictEqual([_context JSGlobalContextRef], m_value, objectToValue(_context, value));
}

- (BOOL)isEqualWithTypeCoercionToObject:(id)value
{
JSValueRef exception = 0;
BOOL result = JSValueIsEqual([_context JSGlobalContextRef], m_value, objectToValue(_context, value), &exception);
if (exception)
return [_context boolFromNotifyException:exception];

return result;
}

- (BOOL)isInstanceOf:(id)value
{
JSValueRef exception = 0;
JSObjectRef constructor = JSValueToObject([_context JSGlobalContextRef], objectToValue(_context, value), &exception);
if (exception)
return [_context boolFromNotifyException:exception];

BOOL result = JSValueIsInstanceOfConstructor([_context JSGlobalContextRef], m_value, constructor, &exception);
if (exception)
return [_context boolFromNotifyException:exception];

return result;
}

- (JSValue *)callWithArguments:(NSArray *)argumentArray
{
NSUInteger argumentCount = [argumentArray count];
JSValueRef arguments[argumentCount];
for (unsigned i = 0; i < argumentCount; ++i)
arguments[i] = objectToValue(_context, [argumentArray objectAtIndex:i]);

JSValueRef exception = 0;
JSObjectRef object = JSValueToObject([_context JSGlobalContextRef], m_value, &exception);
if (exception)
return [_context valueFromNotifyException:exception];

JSValueRef result = JSObjectCallAsFunction([_context JSGlobalContextRef], object, 0, argumentCount, arguments, &exception);
if (exception)
return [_context valueFromNotifyException:exception];

return [JSValue valueWithJSValueRef:result inContext:_context];
}

- (JSValue *)constructWithArguments:(NSArray *)argumentArray
{
NSUInteger argumentCount = [argumentArray count];
JSValueRef arguments[argumentCount];
for (unsigned i = 0; i < argumentCount; ++i)
arguments[i] = objectToValue(_context, [argumentArray objectAtIndex:i]);

JSValueRef exception = 0;
JSObjectRef object = JSValueToObject([_context JSGlobalContextRef], m_value, &exception);
if (exception)
return [_context valueFromNotifyException:exception];

JSObjectRef result = JSObjectCallAsConstructor([_context JSGlobalContextRef], object, argumentCount, arguments, &exception);
if (exception)
return [_context valueFromNotifyException:exception];

return [JSValue valueWithJSValueRef:result inContext:_context];
}

- (JSValue *)invokeMethod:(NSString *)method withArguments:(NSArray *)arguments
{
NSUInteger argumentCount = [arguments count];
JSValueRef argumentArray[argumentCount];
for (unsigned i = 0; i < argumentCount; ++i)
argumentArray[i] = objectToValue(_context, [arguments objectAtIndex:i]);

JSValueRef exception = 0;
JSObjectRef thisObject = JSValueToObject([_context JSGlobalContextRef], m_value, &exception);
if (exception)
return [_context valueFromNotifyException:exception];

auto name = OpaqueJSString::tryCreate(method);
JSValueRef function = JSObjectGetProperty([_context JSGlobalContextRef], thisObject, name.get(), &exception);
if (exception)
return [_context valueFromNotifyException:exception];

JSObjectRef object = JSValueToObject([_context JSGlobalContextRef], function, &exception);
if (exception)
return [_context valueFromNotifyException:exception];

JSValueRef result = JSObjectCallAsFunction([_context JSGlobalContextRef], object, thisObject, argumentCount, argumentArray, &exception);
if (exception)
return [_context valueFromNotifyException:exception];

return [JSValue valueWithJSValueRef:result inContext:_context];
}

@end

@implementation JSValue(StructSupport)

- (CGPoint)toPoint
{
return (CGPoint){
static_cast<CGFloat>([self[@"x"] toDouble]),
static_cast<CGFloat>([self[@"y"] toDouble])
};
}

- (NSRange)toRange
{
return (NSRange){
[[self[@"location"] toNumber] unsignedIntegerValue],
[[self[@"length"] toNumber] unsignedIntegerValue]
};
}

- (CGRect)toRect
{
return (CGRect){
[self toPoint],
[self toSize]
};
}

- (CGSize)toSize
{
return (CGSize){
static_cast<CGFloat>([self[@"width"] toDouble]),
static_cast<CGFloat>([self[@"height"] toDouble])
};
}

+ (JSValue *)valueWithPoint:(CGPoint)point inContext:(JSContext *)context
{
return [JSValue valueWithObject:@{
@"x":@(point.x),
@"y":@(point.y)
} inContext:context];
}

+ (JSValue *)valueWithRange:(NSRange)range inContext:(JSContext *)context
{
return [JSValue valueWithObject:@{
@"location":@(range.location),
@"length":@(range.length)
} inContext:context];
}

+ (JSValue *)valueWithRect:(CGRect)rect inContext:(JSContext *)context
{
return [JSValue valueWithObject:@{
@"x":@(rect.origin.x),
@"y":@(rect.origin.y),
@"width":@(rect.size.width),
@"height":@(rect.size.height)
} inContext:context];
}

+ (JSValue *)valueWithSize:(CGSize)size inContext:(JSContext *)context
{
return [JSValue valueWithObject:@{
@"width":@(size.width),
@"height":@(size.height)
} inContext:context];
}

@end

@implementation JSValue(SubscriptSupport)

- (JSValue *)objectForKeyedSubscript:(id)key
{
return [self valueForProperty:key];
}

- (JSValue *)objectAtIndexedSubscript:(NSUInteger)index
{
return [self valueAtIndex:index];
}

- (void)setObject:(id)object forKeyedSubscript:(id)key
{
[self setValue:object forProperty:key];
}

- (void)setObject:(id)object atIndexedSubscript:(NSUInteger)index
{
[self setValue:object atIndex:index];
}

@end

inline bool isDate(JSC::VM& vm, JSObjectRef object, JSGlobalContextRef context)
{
JSC::JSLockHolder locker(toJS(context));
return toJS(object)->inherits<JSC::DateInstance>(vm);
}

inline bool isArray(JSC::VM& vm, JSObjectRef object, JSGlobalContextRef context)
{
JSC::JSLockHolder locker(toJS(context));
return toJS(object)->inherits<JSC::JSArray>(vm);
}

@implementation JSValue(Internal)

enum ConversionType {
ContainerNone,
ContainerArray,
ContainerDictionary
};

class JSContainerConvertor {
public:
struct Task {
JSValueRef js;
id objc;
ConversionType type;
};

JSContainerConvertor(JSGlobalContextRef context)
: m_context(context)
{
}

id convert(JSValueRef property);
void add(Task);
Task take();
bool isWorkListEmpty() const { return !m_worklist.size(); }

private:
JSGlobalContextRef m_context;
HashMap<JSValueRef, __unsafe_unretained id> m_objectMap;
Vector<Task> m_worklist;
Vector<JSC::Strong<JSC::Unknown>> m_jsValues;
};

inline id JSContainerConvertor::convert(JSValueRef value)
{
auto iter = m_objectMap.find(value);
if (iter != m_objectMap.end())
return iter->value;

Task result = valueToObjectWithoutCopy(m_context, value);
if (result.js)
add(result);
return result.objc;
}

void JSContainerConvertor::add(Task task)
{
JSC::JSGlobalObject* globalObject = toJS(m_context);
m_jsValues.append(JSC::Strong<JSC::Unknown>(globalObject->vm(), toJSForGC(globalObject, task.js)));
m_objectMap.add(task.js, task.objc);
if (task.type != ContainerNone)
m_worklist.append(task);
}

JSContainerConvertor::Task JSContainerConvertor::take()
{
ASSERT(!isWorkListEmpty());
Task last = m_worklist.last();
m_worklist.removeLast();
return last;
}

#if ENABLE(REMOTE_INSPECTOR)
static void reportExceptionToInspector(JSGlobalContextRef context, JSC::JSValue exceptionValue)
{
JSC::JSGlobalObject* globalObject = toJS(context);
JSC::VM& vm = globalObject->vm();
JSC::Exception* exception = JSC::Exception::create(vm, exceptionValue);
globalObject->inspectorController().reportAPIException(globalObject, exception);
}
#endif

static JSContainerConvertor::Task valueToObjectWithoutCopy(JSGlobalContextRef context, JSValueRef value)
{
JSC::JSGlobalObject* globalObject = toJS(context);
JSC::VM& vm = globalObject->vm();

if (!JSValueIsObject(context, value)) {
id primitive;
if (JSValueIsBoolean(context, value))
primitive = JSValueToBoolean(context, value) ? @YES : @NO;
else if (JSValueIsNumber(context, value)) {
// Normalize the number, so it will unique correctly in the hash map -
// it's nicer not to leak this internal implementation detail!
value = JSValueMakeNumber(context, JSValueToNumber(context, value, 0));
primitive = [NSNumber numberWithDouble:JSValueToNumber(context, value, 0)];
} else if (JSValueIsString(context, value)) {
// Would be nice to unique strings, too.
auto jsstring = adoptRef(JSValueToStringCopy(context, value, 0));
primitive = CFBridgingRelease(JSStringCopyCFString(kCFAllocatorDefault, jsstring.get()));
} else if (JSValueIsNull(context, value))
primitive = [NSNull null];
else {
ASSERT(JSValueIsUndefined(context, value));
primitive = nil;
}
return { value, primitive, ContainerNone };
}

JSObjectRef object = JSValueToObject(context, value, 0);

if (id wrapped = tryUnwrapObjcObject(context, object))
return { object, wrapped, ContainerNone };

if (isDate(vm, object, context))
return { object, [NSDate dateWithTimeIntervalSince1970:JSValueToNumber(context, object, 0) / 1000.0], ContainerNone };

if (isArray(vm, object, context))
return { object, [NSMutableArray array], ContainerArray };

return { object, [NSMutableDictionary dictionary], ContainerDictionary };
}

static id containerValueToObject(JSGlobalContextRef context, JSContainerConvertor::Task task)
{
ASSERT(task.type != ContainerNone);
JSC::JSLockHolder locker(toJS(context));
JSContainerConvertor convertor(context);
convertor.add(task);
ASSERT(!convertor.isWorkListEmpty());

do {
JSContainerConvertor::Task current = convertor.take();
ASSERT(JSValueIsObject(context, current.js));
JSObjectRef js = JSValueToObject(context, current.js, 0);

if (current.type == ContainerArray) {
ASSERT([current.objc isKindOfClass:[NSMutableArray class]]);
NSMutableArray *array = (NSMutableArray *)current.objc;

auto lengthString = OpaqueJSString::tryCreate("length"_s);
unsigned length = JSC::toUInt32(JSValueToNumber(context, JSObjectGetProperty(context, js, lengthString.get(), 0), 0));

for (unsigned i = 0; i < length; ++i) {
id objc = convertor.convert(JSObjectGetPropertyAtIndex(context, js, i, 0));
[array addObject:objc ? objc : [NSNull null]];
}
} else {
ASSERT([current.objc isKindOfClass:[NSMutableDictionary class]]);
NSMutableDictionary *dictionary = (NSMutableDictionary *)current.objc;

JSC::JSLockHolder locker(toJS(context));

JSPropertyNameArrayRef propertyNameArray = JSObjectCopyPropertyNames(context, js);
size_t length = JSPropertyNameArrayGetCount(propertyNameArray);

for (size_t i = 0; i < length; ++i) {
JSStringRef propertyName = JSPropertyNameArrayGetNameAtIndex(propertyNameArray, i);
if (id objc = convertor.convert(JSObjectGetProperty(context, js, propertyName, 0)))
dictionary[(__bridge NSString *)adoptCF(JSStringCopyCFString(kCFAllocatorDefault, propertyName)).get()] = objc;
}

JSPropertyNameArrayRelease(propertyNameArray);
}

} while (!convertor.isWorkListEmpty());

return task.objc;
}

id valueToObject(JSContext *context, JSValueRef value)
{
JSContainerConvertor::Task result = valueToObjectWithoutCopy([context JSGlobalContextRef], value);
if (result.type == ContainerNone)
return result.objc;
return containerValueToObject([context JSGlobalContextRef], result);
}

id valueToNumber(JSGlobalContextRef context, JSValueRef value, JSValueRef* exception)
{
ASSERT(!*exception);
if (id wrapped = tryUnwrapObjcObject(context, value)) {
if ([wrapped isKindOfClass:[NSNumber class]])
return wrapped;
}

if (JSValueIsBoolean(context, value))
return JSValueToBoolean(context, value) ? @YES : @NO;

double result = JSValueToNumber(context, value, exception);
return [NSNumber numberWithDouble:*exception ? std::numeric_limits<double>::quiet_NaN() : result];
}

id valueToString(JSGlobalContextRef context, JSValueRef value, JSValueRef* exception)
{
ASSERT(!*exception);
if (id wrapped = tryUnwrapObjcObject(context, value)) {
if ([wrapped isKindOfClass:[NSString class]])
return wrapped;
}

auto jsstring = adoptRef(JSValueToStringCopy(context, value, exception));
if (*exception) {
ASSERT(!jsstring);
return nil;
}

return CFBridgingRelease(JSStringCopyCFString(kCFAllocatorDefault, jsstring.get()));
}

id valueToDate(JSGlobalContextRef context, JSValueRef value, JSValueRef* exception)
{
ASSERT(!*exception);
if (id wrapped = tryUnwrapObjcObject(context, value)) {
if ([wrapped isKindOfClass:[NSDate class]])
return wrapped;
}

double result = JSValueToNumber(context, value, exception) / 1000.0;
return *exception ? nil : [NSDate dateWithTimeIntervalSince1970:result];
}

id valueToArray(JSGlobalContextRef context, JSValueRef value, JSValueRef* exception)
{
ASSERT(!*exception);
if (id wrapped = tryUnwrapObjcObject(context, value)) {
if ([wrapped isKindOfClass:[NSArray class]])
return wrapped;
}

if (JSValueIsObject(context, value))
return containerValueToObject(context, { value, [NSMutableArray array], ContainerArray});

JSC::JSLockHolder locker(toJS(context));
if (!(JSValueIsNull(context, value) || JSValueIsUndefined(context, value))) {
JSC::JSObject* exceptionObject = JSC::createTypeError(toJS(context), "Cannot convert primitive to NSArray"_s);
*exception = toRef(exceptionObject);
#if ENABLE(REMOTE_INSPECTOR)
reportExceptionToInspector(context, exceptionObject);
#endif
}
return nil;
}

id valueToDictionary(JSGlobalContextRef context, JSValueRef value, JSValueRef* exception)
{
ASSERT(!*exception);
if (id wrapped = tryUnwrapObjcObject(context, value)) {
if ([wrapped isKindOfClass:[NSDictionary class]])
return wrapped;
}

if (JSValueIsObject(context, value))
return containerValueToObject(context, { value, [NSMutableDictionary dictionary], ContainerDictionary});

JSC::JSLockHolder locker(toJS(context));
if (!(JSValueIsNull(context, value) || JSValueIsUndefined(context, value))) {
JSC::JSObject* exceptionObject = JSC::createTypeError(toJS(context), "Cannot convert primitive to NSDictionary"_s);
*exception = toRef(exceptionObject);
#if ENABLE(REMOTE_INSPECTOR)
reportExceptionToInspector(context, exceptionObject);
#endif
}
return nil;
}

class ObjcContainerConvertor {
public:
struct Task {
id objc;
JSValueRef js;
ConversionType type;
};

ObjcContainerConvertor(JSContext *context)
: m_context(context)
{
}

JSValueRef convert(id object);
void add(Task);
Task take();
bool isWorkListEmpty() const { return !m_worklist.size(); }

private:
JSContext *m_context;
HashMap<__unsafe_unretained id, JSValueRef> m_objectMap;
Vector<Task> m_worklist;
Vector<JSC::Strong<JSC::Unknown>> m_jsValues;
};

JSValueRef ObjcContainerConvertor::convert(id object)
{
ASSERT(object);

auto it = m_objectMap.find(object);
if (it != m_objectMap.end())
return it->value;

ObjcContainerConvertor::Task task = objectToValueWithoutCopy(m_context, object);
add(task);
return task.js;
}

void ObjcContainerConvertor::add(ObjcContainerConvertor::Task task)
{
JSC::JSGlobalObject* globalObject = toJS(m_context.JSGlobalContextRef);
m_jsValues.append(JSC::Strong<JSC::Unknown>(globalObject->vm(), toJSForGC(globalObject, task.js)));
m_objectMap.add(task.objc, task.js);
if (task.type != ContainerNone)
m_worklist.append(task);
}

ObjcContainerConvertor::Task ObjcContainerConvertor::take()
{
ASSERT(!isWorkListEmpty());
Task last = m_worklist.last();
m_worklist.removeLast();
return last;
}

inline bool isNSBoolean(id object)
{
ASSERT([@YES class] == [@NO class]);
ASSERT([@YES class] != [NSNumber class]);
ASSERT([[@YES class] isSubclassOfClass:[NSNumber class]]);
return [object isKindOfClass:[@YES class]];
}

static ObjcContainerConvertor::Task objectToValueWithoutCopy(JSContext *context, id object)
{
JSGlobalContextRef contextRef = [context JSGlobalContextRef];

if (!object)
return { object, JSValueMakeUndefined(contextRef), ContainerNone };

if (!class_conformsToProtocol(object_getClass(object), getJSExportProtocol())) {
if ([object isKindOfClass:[NSArray class]])
return { object, JSObjectMakeArray(contextRef, 0, NULL, 0), ContainerArray };

if ([object isKindOfClass:[NSDictionary class]])
return { object, JSObjectMake(contextRef, 0, 0), ContainerDictionary };

if ([object isKindOfClass:[NSNull class]])
return { object, JSValueMakeNull(contextRef), ContainerNone };

if ([object isKindOfClass:[JSValue class]])
return { object, ((JSValue *)object)->m_value, ContainerNone };

if ([object isKindOfClass:[NSString class]]) {
auto string = OpaqueJSString::tryCreate((NSString *)object);
return { object, JSValueMakeString(contextRef, string.get()), ContainerNone };
}

if ([object isKindOfClass:[NSNumber class]]) {
if (isNSBoolean(object))
return { object, JSValueMakeBoolean(contextRef, [object boolValue]), ContainerNone };
return { object, JSValueMakeNumber(contextRef, [object doubleValue]), ContainerNone };
}

if ([object isKindOfClass:[NSDate class]]) {
JSValueRef argument = JSValueMakeNumber(contextRef, [object timeIntervalSince1970] * 1000.0);
JSObjectRef result = JSObjectMakeDate(contextRef, 1, &argument, 0);
return { object, result, ContainerNone };
}

if ([object isKindOfClass:[JSManagedValue class]]) {
JSValue *value = [static_cast<JSManagedValue *>(object) value];
if (!value)
return { object, JSValueMakeUndefined(contextRef), ContainerNone };
return { object, value->m_value, ContainerNone };
}
}

return { object, valueInternalValue([context wrapperForObjCObject:object]), ContainerNone };
}

JSValueRef objectToValue(JSContext *context, id object)
{
JSGlobalContextRef contextRef = [context JSGlobalContextRef];

ObjcContainerConvertor::Task task = objectToValueWithoutCopy(context, object);
if (task.type == ContainerNone)
return task.js;

JSC::JSLockHolder locker(toJS(contextRef));
ObjcContainerConvertor convertor(context);
convertor.add(task);
ASSERT(!convertor.isWorkListEmpty());

do {
ObjcContainerConvertor::Task current = convertor.take();
ASSERT(JSValueIsObject(contextRef, current.js));
JSObjectRef js = JSValueToObject(contextRef, current.js, 0);

if (current.type == ContainerArray) {
ASSERT([current.objc isKindOfClass:[NSArray class]]);
NSArray *array = (NSArray *)current.objc;
NSUInteger count = [array count];
for (NSUInteger index = 0; index < count; ++index)
JSObjectSetPropertyAtIndex(contextRef, js, index, convertor.convert([array objectAtIndex:index]), 0);
} else {
ASSERT(current.type == ContainerDictionary);
ASSERT([current.objc isKindOfClass:[NSDictionary class]]);
NSDictionary *dictionary = (NSDictionary *)current.objc;
for (id key in [dictionary keyEnumerator]) {
if ([key isKindOfClass:[NSString class]]) {
auto propertyName = OpaqueJSString::tryCreate((NSString *)key);
JSObjectSetProperty(contextRef, js, propertyName.get(), convertor.convert([dictionary objectForKey:key]), 0, 0);
}
}
}
} while (!convertor.isWorkListEmpty());

return task.js;
}

JSValueRef valueInternalValue(JSValue * value)
{
return value->m_value;
}

+ (JSValue *)valueWithJSValueRef:(JSValueRef)value inContext:(JSContext *)context
{
return [context wrapperForJSObject:value];
}

- (JSValue *)init
{
return nil;
}

- (JSValue *)initWithValue:(JSValueRef)value inContext:(JSContext *)context
{
if (!value || !context)
return nil;

self = [super init];
if (!self)
return nil;

_context = [context retain];
m_value = value;
JSValueProtect([_context JSGlobalContextRef], m_value);
return self;
}

struct StructTagHandler {
SEL typeToValueSEL;
SEL valueToTypeSEL;
};
typedef HashMap<String, StructTagHandler> StructHandlers;

static StructHandlers* createStructHandlerMap()
{
StructHandlers* structHandlers = new StructHandlers();

size_t valueWithXinContextLength = strlen("valueWithX:inContext:");
size_t toXLength = strlen("toX");

// Step 1: find all valueWith<Foo>:inContext: class methods in JSValue.
forEachMethodInClass(object_getClass([JSValue class]), ^(Method method){
SEL selector = method_getName(method);
const char* name = sel_getName(selector);
size_t nameLength = strlen(name);
// Check for valueWith<Foo>:context:
if (nameLength < valueWithXinContextLength || memcmp(name, "valueWith", 9) || memcmp(name + nameLength - 11, ":inContext:", 11))
return;
// Check for [ id, SEL, <type>, <contextType> ]
if (method_getNumberOfArguments(method) != 4)
return;
char idType[3];
// Check 2nd argument type is "@"
{
auto secondType = adoptSystem<char[]>(method_copyArgumentType(method, 3));
if (strcmp(secondType.get(), "@") != 0)
return;
}
// Check result type is also "@"
method_getReturnType(method, idType, 3);
if (strcmp(idType, "@") != 0)
return;
{
auto type = adoptSystem<char[]>(method_copyArgumentType(method, 2));
structHandlers->add(StringImpl::create(type.get()), (StructTagHandler) { selector, 0 });
}
});

// Step 2: find all to<Foo> instance methods in JSValue.
forEachMethodInClass([JSValue class], ^(Method method){
SEL selector = method_getName(method);
const char* name = sel_getName(selector);
size_t nameLength = strlen(name);
// Check for to<Foo>
if (nameLength < toXLength || memcmp(name, "to", 2))
return;
// Check for [ id, SEL ]
if (method_getNumberOfArguments(method) != 2)
return;
// Try to find a matching valueWith<Foo>:context: method.
auto type = adoptSystem<char[]>(method_copyReturnType(method));
StructHandlers::iterator iter = structHandlers->find(type.get());
if (iter == structHandlers->end())
return;
StructTagHandler& handler = iter->value;

// check that strlen(<foo>) == strlen(<Foo>)
const char* valueWithName = sel_getName(handler.typeToValueSEL);
size_t valueWithLength = strlen(valueWithName);
if (valueWithLength - valueWithXinContextLength != nameLength - toXLength)
return;
// Check that <Foo> == <Foo>
if (memcmp(valueWithName + 9, name + 2, nameLength - toXLength - 1))
return;
handler.valueToTypeSEL = selector;
});

// Step 3: clean up - remove entries where we found prospective valueWith<Foo>:inContext: conversions, but no matching to<Foo> methods.
typedef HashSet<String> RemoveSet;
RemoveSet removeSet;
for (StructHandlers::iterator iter = structHandlers->begin(); iter != structHandlers->end(); ++iter) {
StructTagHandler& handler = iter->value;
if (!handler.valueToTypeSEL)
removeSet.add(iter->key);
}

for (RemoveSet::iterator iter = removeSet.begin(); iter != removeSet.end(); ++iter)
structHandlers->remove(*iter);

return structHandlers;
}

static StructTagHandler* handerForStructTag(const char* encodedType)
{
static Lock handerForStructTagLock;
LockHolder lockHolder(&handerForStructTagLock);

static StructHandlers* structHandlers = createStructHandlerMap();

StructHandlers::iterator iter = structHandlers->find(encodedType);
if (iter == structHandlers->end())
return 0;
return &iter->value;
}

+ (SEL)selectorForStructToValue:(const char *)structTag
{
StructTagHandler* handler = handerForStructTag(structTag);
return handler ? handler->typeToValueSEL : nil;
}

+ (SEL)selectorForValueToStruct:(const char *)structTag
{
StructTagHandler* handler = handerForStructTag(structTag);
return handler ? handler->valueToTypeSEL : nil;
}

NSInvocation *typeToValueInvocationFor(const char* encodedType)
{
SEL selector = [JSValue selectorForStructToValue:encodedType];
if (!selector)
return 0;

const char* methodTypes = method_getTypeEncoding(class_getClassMethod([JSValue class], selector));
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[NSMethodSignature signatureWithObjCTypes:methodTypes]];
[invocation setSelector:selector];
return invocation;
}

NSInvocation *valueToTypeInvocationFor(const char* encodedType)
{
SEL selector = [JSValue selectorForValueToStruct:encodedType];
if (!selector)
return 0;

const char* methodTypes = method_getTypeEncoding(class_getInstanceMethod([JSValue class], selector));
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[NSMethodSignature signatureWithObjCTypes:methodTypes]];
[invocation setSelector:selector];
return invocation;
}

@end

#endif