Files
swift-mirror/stdlib/public/Darwin/Foundation/CheckClass.mm
Saleem Abdulrasool 41d9c2cc59 stdlib: restructure for OS family layout of SDK overlay
The SDK directory is now confusing as the Windows target also has a SDK
overlay.  In order to make this more uniform, move the SDK directory to
Darwin which covers the fact that this covers the XNU family of OSes.
The Windows directory contains the SDK overlay for the Windows target.
2018-12-06 11:32:05 -08:00

292 lines
9.9 KiB
Plaintext

#import <Foundation/Foundation.h>
#include <objc/runtime.h>
#include "swift/Runtime/HeapObject.h"
#include "swift/Runtime/Metadata.h"
@interface NSKeyedUnarchiver (SwiftAdditions)
+ (int)_swift_checkClassAndWarnForKeyedArchiving:(Class)cls
operation:(int)operation
NS_SWIFT_NAME(_swift_checkClassAndWarnForKeyedArchiving(_:operation:));
@end
static bool isASCIIIdentifierChar(char c) {
if (c >= 'a' && c <= 'z') return true;
if (c >= 'A' && c <= 'Z') return true;
if (c >= '0' && c <= '9') return true;
if (c == '_') return true;
if (c == '$') return true;
return false;
}
template <typename T, size_t N>
static constexpr size_t arrayLength(T (&)[N]) { return N; }
static void logIfFirstOccurrence(Class objcClass, void (^log)(void)) {
static auto queue = dispatch_queue_create(
"SwiftFoundation._checkClassAndWarnForKeyedArchivingQueue",
DISPATCH_QUEUE_SERIAL);
static NSHashTable *seenClasses = nil;
dispatch_sync(queue, ^{
// Will be NO when seenClasses is still nil.
if ([seenClasses containsObject:objcClass])
return;
if (!seenClasses) {
NSPointerFunctionsOptions options = 0;
options |= NSPointerFunctionsOpaqueMemory;
options |= NSPointerFunctionsObjectPointerPersonality;
seenClasses = [[NSHashTable alloc] initWithOptions:options capacity:16];
}
[seenClasses addObject:objcClass];
// Synchronize logging so that multiple lines aren't interleaved.
log();
});
}
namespace {
class StringRefLite {
StringRefLite(const char *data, size_t len) : data(data), length(len) {}
public:
const char *data;
size_t length;
StringRefLite() : data(nullptr), length(0) {}
template <size_t N>
StringRefLite(const char (&staticStr)[N]) : data(staticStr), length(N) {}
StringRefLite(swift::TypeNamePair rawValue)
: data(rawValue.data),
length(rawValue.length){}
NS_RETURNS_RETAINED
NSString *newNSStringNoCopy() const {
return [[NSString alloc] initWithBytesNoCopy:const_cast<char *>(data)
length:length
encoding:NSUTF8StringEncoding
freeWhenDone:NO];
}
const char &operator[](size_t offset) const {
assert(offset < length);
return data[offset];
}
StringRefLite slice(size_t from, size_t to) const {
assert(from <= to);
assert(to <= length);
return {data + from, to - from};
}
const char *begin() const {
return data;
}
const char *end() const {
return data + length;
}
};
}
/// Assume that a non-generic demangled class name always ends in ".MyClass"
/// or ".(MyClass plus extra info)".
static StringRefLite findBaseName(StringRefLite demangledName) {
size_t end = demangledName.length;
size_t parenCount = 0;
for (size_t i = end; i != 0; --i) {
switch (demangledName[i - 1]) {
case '.':
if (parenCount == 0) {
if (i != end && demangledName[i] == '(')
++i;
return demangledName.slice(i, end);
}
break;
case ')':
parenCount += 1;
break;
case '(':
if (parenCount > 0)
parenCount -= 1;
break;
case ' ':
end = i - 1;
break;
default:
break;
}
}
return {};
}
@implementation NSKeyedUnarchiver (SwiftAdditions)
/// Checks if class \p objcClass is good for archiving.
///
/// If not, a runtime warning is printed.
///
/// \param operation Specifies the archiving operation. Valid operations are:
/// 0: archiving
/// 1: unarchiving
/// \return Returns the status
/// 0: not a problem class (either non-Swift or has an explicit name)
/// 1: a Swift generic class
/// 2: a Swift non-generic class where adding @objc is valid
/// Future versions of this API will return nonzero values for additional cases
/// that mean the class shouldn't be archived.
+ (int)_swift_checkClassAndWarnForKeyedArchiving:(Class)objcClass
operation:(int)operation {
using namespace swift;
const ClassMetadata *theClass = (ClassMetadata *)objcClass;
// Is it a (real) swift class?
if (!theClass->isTypeMetadata() || theClass->isArtificialSubclass())
return 0;
// Does the class already have a custom name?
if (theClass->getFlags() & ClassFlags::HasCustomObjCName)
return 0;
// Is it a mangled name?
const char *className = class_getName(objcClass);
if (!(className[0] == '_' && className[1] == 'T'))
return 0;
// Is it a name in the form <module>.<class>? Note: the module name could
// start with "_T".
if (strchr(className, '.'))
return 0;
// Is it a generic class?
if (theClass->getDescription()->isGeneric()) {
logIfFirstOccurrence(objcClass, ^{
// Use actual NSStrings to force UTF-8.
StringRefLite demangledName = swift_getTypeName(theClass,
/*qualified*/true);
NSString *demangledString = demangledName.newNSStringNoCopy();
NSString *mangledString = NSStringFromClass(objcClass);
NSString *primaryMessage;
switch (operation) {
case 1:
primaryMessage = [[NSString alloc] initWithFormat:
@"Attempting to unarchive generic Swift class '%@' with mangled "
"runtime name '%@'. Runtime names for generic classes are "
"unstable and may change in the future, leading to "
"non-decodable data.", demangledString, mangledString];
break;
default:
primaryMessage = [[NSString alloc] initWithFormat:
@"Attempting to archive generic Swift class '%@' with mangled "
"runtime name '%@'. Runtime names for generic classes are "
"unstable and may change in the future, leading to "
"non-decodable data.", demangledString, mangledString];
break;
}
NSString *generatedNote = [[NSString alloc] initWithFormat:
@"To avoid this failure, create a concrete subclass and register "
"it with NSKeyedUnarchiver.setClass(_:forClassName:) instead, "
"using the name \"%@\".", mangledString];
const char *staticNote =
"If you need to produce archives compatible with older versions "
"of your program, use NSKeyedArchiver.setClassName(_:for:) as well.";
NSLog(@"%@", primaryMessage);
NSLog(@"%@", generatedNote);
NSLog(@"%s", staticNote);
RuntimeErrorDetails::Note notes[] = {
{ [generatedNote UTF8String], /*numFixIts*/0, /*fixIts*/nullptr },
{ staticNote, /*numFixIts*/0, /*fixIts*/nullptr },
};
RuntimeErrorDetails errorInfo = {};
errorInfo.version = RuntimeErrorDetails::currentVersion;
errorInfo.errorType = "nskeyedarchiver-incompatible-class";
errorInfo.notes = notes;
errorInfo.numNotes = arrayLength(notes);
_swift_reportToDebugger(RuntimeErrorFlagNone, [primaryMessage UTF8String],
&errorInfo);
[primaryMessage release];
[generatedNote release];
[demangledString release];
});
return 1;
}
// It's a swift class with a (compiler generated) mangled name, which should
// be written into an NSArchive.
logIfFirstOccurrence(objcClass, ^{
// Use actual NSStrings to force UTF-8.
StringRefLite demangledName = swift_getTypeName(theClass,/*qualified*/true);
NSString *demangledString = demangledName.newNSStringNoCopy();
NSString *mangledString = NSStringFromClass(objcClass);
NSString *primaryMessage;
switch (operation) {
case 1:
primaryMessage = [[NSString alloc] initWithFormat:
@"Attempting to unarchive Swift class '%@' with mangled runtime "
"name '%@'. The runtime name for this class is unstable and may "
"change in the future, leading to non-decodable data.",
demangledString, mangledString];
break;
default:
primaryMessage = [[NSString alloc] initWithFormat:
@"Attempting to archive Swift class '%@' with mangled runtime "
"name '%@'. The runtime name for this class is unstable and may "
"change in the future, leading to non-decodable data.",
demangledString, mangledString];
break;
}
NSString *firstNote = [[NSString alloc] initWithFormat:
@"You can use the 'objc' attribute to ensure that the name will not "
"change: \"@objc(%@)\"", mangledString];
StringRefLite baseName = findBaseName(demangledName);
// Offer a more generic message if the base name we found doesn't look like
// an ASCII identifier. This avoids printing names like "ABCモデル".
if (baseName.length == 0 ||
!std::all_of(baseName.begin(), baseName.end(), isASCIIIdentifierChar)) {
baseName = "MyModel";
}
NSString *secondNote = [[NSString alloc] initWithFormat:
@"If there are no existing archives containing this class, you should "
"choose a unique, prefixed name instead: \"@objc(ABC%1$.*2$s)\"",
baseName.data, (int)baseName.length];
NSLog(@"%@", primaryMessage);
NSLog(@"%@", firstNote);
NSLog(@"%@", secondNote);
// FIXME: We could suggest these as fix-its if we had source locations for
// the class.
RuntimeErrorDetails::Note notes[] = {
{ [firstNote UTF8String], /*numFixIts*/0, /*fixIts*/nullptr },
{ [secondNote UTF8String], /*numFixIts*/0, /*fixIts*/nullptr },
};
RuntimeErrorDetails errorInfo = {};
errorInfo.version = RuntimeErrorDetails::currentVersion;
errorInfo.errorType = "nskeyedarchiver-incompatible-class";
errorInfo.notes = notes;
errorInfo.numNotes = arrayLength(notes);
_swift_reportToDebugger(RuntimeErrorFlagNone, [primaryMessage UTF8String],
&errorInfo);
[primaryMessage release];
[firstNote release];
[secondNote release];
[demangledString release];
});
return 2;
}
@end