blob: 8a5621f1e3e4197c8716b98dca0fa7bd7283fc4f [file] [log] [blame]
#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