#import #include #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 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 StringRefLite(const char (&staticStr)[N]) : data(staticStr), length(N) {} StringRefLite(swift::TwoWordPair::Return rawValue) : data(swift::TwoWordPair(rawValue).first), length(swift::TwoWordPair(rawValue).second){} NS_RETURNS_RETAINED NSString *newNSStringNoCopy() const { return [[NSString alloc] initWithBytesNoCopy:const_cast(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 .? Note: the module name could // start with "_T". if (strchr(className, '.')) return 0; // Is it a generic class? if (theClass->getDescription()->GenericParams.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