1/*
2 * Copyright (C) 2004, 2012 Apple Inc.  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
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 *    notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 *    notice, this list of conditions and the following disclaimer in the
11 *    documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "objc_class.h"
28
29#include "objc_instance.h"
30#include "WebScriptObject.h"
31
32namespace JSC {
33namespace Bindings {
34
35ObjcClass::ObjcClass(ClassStructPtr aClass)
36    : _isa(aClass)
37{
38}
39
40static CFMutableDictionaryRef classesByIsA = 0;
41
42static void _createClassesByIsAIfNecessary()
43{
44    if (!classesByIsA)
45        classesByIsA = CFDictionaryCreateMutable(NULL, 0, NULL, NULL);
46}
47
48ObjcClass* ObjcClass::classForIsA(ClassStructPtr isa)
49{
50    _createClassesByIsAIfNecessary();
51
52    ObjcClass* aClass = (ObjcClass*)CFDictionaryGetValue(classesByIsA, isa);
53    if (!aClass) {
54        aClass = new ObjcClass(isa);
55        CFDictionaryAddValue(classesByIsA, isa, aClass);
56    }
57
58    return aClass;
59}
60
61/*
62    By default, a JavaScript method name is produced by concatenating the
63    components of an ObjectiveC method name, replacing ':' with '_', and
64    escaping '_' and '$' with a leading '$', such that '_' becomes "$_" and
65    '$' becomes "$$". For example:
66
67    ObjectiveC name         Default JavaScript name
68        moveTo::                moveTo__
69        moveTo_                 moveTo$_
70        moveTo$_                moveTo$$$_
71
72    This function performs the inverse of that operation.
73
74    @result Fills 'buffer' with the ObjectiveC method name that corresponds to 'JSName'.
75*/
76typedef Vector<char, 256> JSNameConversionBuffer;
77static inline void convertJSMethodNameToObjc(const CString& jsName, JSNameConversionBuffer& buffer)
78{
79    buffer.reserveInitialCapacity(jsName.length() + 1);
80
81    const char* source = jsName.data();
82    while (true) {
83        if (*source == '$') {
84            ++source;
85            buffer.uncheckedAppend(*source);
86        } else if (*source == '_')
87            buffer.uncheckedAppend(':');
88        else
89            buffer.uncheckedAppend(*source);
90
91        if (!*source)
92            return;
93
94        ++source;
95    }
96}
97
98Method* ObjcClass::methodNamed(PropertyName propertyName, Instance*) const
99{
100    String name(propertyName.publicName());
101    if (name.isNull())
102        return 0;
103
104    if (Method* method = m_methodCache.get(name.impl()))
105        return method;
106
107    CString jsName = name.ascii();
108    JSNameConversionBuffer buffer;
109    convertJSMethodNameToObjc(jsName, buffer);
110    RetainPtr<CFStringRef> methodName = adoptCF(CFStringCreateWithCString(NULL, buffer.data(), kCFStringEncodingASCII));
111
112    Method* methodPtr = 0;
113    ClassStructPtr thisClass = _isa;
114
115    while (thisClass && !methodPtr) {
116        unsigned numMethodsInClass = 0;
117        MethodStructPtr* objcMethodList = class_copyMethodList(thisClass, &numMethodsInClass);
118        for (unsigned i = 0; i < numMethodsInClass; i++) {
119            MethodStructPtr objcMethod = objcMethodList[i];
120            SEL objcMethodSelector = method_getName(objcMethod);
121            const char* objcMethodSelectorName = sel_getName(objcMethodSelector);
122            NSString* mappedName = nil;
123
124            // See if the class wants to exclude the selector from visibility in JavaScript.
125            if ([thisClass respondsToSelector:@selector(isSelectorExcludedFromWebScript:)])
126                if ([thisClass isSelectorExcludedFromWebScript:objcMethodSelector])
127                    continue;
128
129            // See if the class want to provide a different name for the selector in JavaScript.
130            // Note that we do not do any checks to guarantee uniqueness. That's the responsiblity
131            // of the class.
132            if ([thisClass respondsToSelector:@selector(webScriptNameForSelector:)])
133                mappedName = [thisClass webScriptNameForSelector:objcMethodSelector];
134
135            if ((mappedName && [mappedName isEqual:(NSString*)methodName.get()]) || strcmp(objcMethodSelectorName, buffer.data()) == 0) {
136                OwnPtr<Method> method = adoptPtr(new ObjcMethod(thisClass, objcMethodSelector));
137                methodPtr = method.get();
138                m_methodCache.add(name.impl(), method.release());
139                break;
140            }
141        }
142        thisClass = class_getSuperclass(thisClass);
143        free(objcMethodList);
144    }
145
146    return methodPtr;
147}
148
149Field* ObjcClass::fieldNamed(PropertyName propertyName, Instance* instance) const
150{
151    String name(propertyName.publicName());
152    if (name.isNull())
153        return 0;
154
155    Field* field = m_fieldCache.get(name.impl());
156    if (field)
157        return field;
158
159    ClassStructPtr thisClass = _isa;
160
161    CString jsName = name.ascii();
162    RetainPtr<CFStringRef> fieldName = adoptCF(CFStringCreateWithCString(NULL, jsName.data(), kCFStringEncodingASCII));
163    id targetObject = (static_cast<ObjcInstance*>(instance))->getObject();
164#if PLATFORM(IOS)
165    id attributes = [targetObject respondsToSelector:@selector(attributeKeys)] ? [targetObject performSelector:@selector(attributeKeys)] : nil;
166#else
167    id attributes = [targetObject attributeKeys];
168#endif
169    if (attributes) {
170        // Class overrides attributeKeys, use that array of key names.
171        unsigned count = [attributes count];
172        for (unsigned i = 0; i < count; i++) {
173            NSString* keyName = [attributes objectAtIndex:i];
174            const char* UTF8KeyName = [keyName UTF8String]; // ObjC actually only supports ASCII names.
175
176            // See if the class wants to exclude the selector from visibility in JavaScript.
177            if ([thisClass respondsToSelector:@selector(isKeyExcludedFromWebScript:)])
178                if ([thisClass isKeyExcludedFromWebScript:UTF8KeyName])
179                    continue;
180
181            // See if the class want to provide a different name for the selector in JavaScript.
182            // Note that we do not do any checks to guarantee uniqueness. That's the responsiblity
183            // of the class.
184            NSString* mappedName = nil;
185            if ([thisClass respondsToSelector:@selector(webScriptNameForKey:)])
186                mappedName = [thisClass webScriptNameForKey:UTF8KeyName];
187
188            if ((mappedName && [mappedName isEqual:(NSString*)fieldName.get()]) || [keyName isEqual:(NSString*)fieldName.get()]) {
189                OwnPtr<Field> newField = adoptPtr(new ObjcField((CFStringRef)keyName));
190                field = newField.get();
191                m_fieldCache.add(name.impl(), newField.release());
192                break;
193            }
194        }
195    } else {
196        // Class doesn't override attributeKeys, so fall back on class runtime
197        // introspection.
198
199        while (thisClass) {
200            unsigned numFieldsInClass = 0;
201            IvarStructPtr* ivarsInClass = class_copyIvarList(thisClass, &numFieldsInClass);
202
203            for (unsigned i = 0; i < numFieldsInClass; i++) {
204                IvarStructPtr objcIVar = ivarsInClass[i];
205                const char* objcIvarName = ivar_getName(objcIVar);
206                NSString* mappedName = 0;
207
208                // See if the class wants to exclude the selector from visibility in JavaScript.
209                if ([thisClass respondsToSelector:@selector(isKeyExcludedFromWebScript:)])
210                    if ([thisClass isKeyExcludedFromWebScript:objcIvarName])
211                        continue;
212
213                // See if the class want to provide a different name for the selector in JavaScript.
214                // Note that we do not do any checks to guarantee uniqueness. That's the responsiblity
215                // of the class.
216                if ([thisClass respondsToSelector:@selector(webScriptNameForKey:)])
217                    mappedName = [thisClass webScriptNameForKey:objcIvarName];
218
219                if ((mappedName && [mappedName isEqual:(NSString*)fieldName.get()]) || strcmp(objcIvarName, jsName.data()) == 0) {
220                    OwnPtr<Field> newField = adoptPtr(new ObjcField(objcIVar));
221                    field = newField.get();
222                    m_fieldCache.add(name.impl(), newField.release());
223                    break;
224                }
225            }
226
227            thisClass = class_getSuperclass(thisClass);
228            free(ivarsInClass);
229        }
230    }
231
232    return field;
233}
234
235JSValue ObjcClass::fallbackObject(ExecState* exec, Instance* instance, PropertyName propertyName)
236{
237    ObjcInstance* objcInstance = static_cast<ObjcInstance*>(instance);
238    id targetObject = objcInstance->getObject();
239
240    if (![targetObject respondsToSelector:@selector(invokeUndefinedMethodFromWebScript:withArguments:)])
241        return jsUndefined();
242    return ObjcFallbackObjectImp::create(exec, exec->lexicalGlobalObject(), objcInstance, propertyName.publicName());
243}
244
245}
246}
247