Skip to content

Commit 1dea5b4

Browse files
committed
Handle long tagged NSStrings
1 parent 25d8029 commit 1dea5b4

File tree

7 files changed

+201
-11
lines changed

7 files changed

+201
-11
lines changed

stdlib/public/core/SmallString.swift

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -346,13 +346,22 @@ extension _SmallString {
346346
//
347347
@_effects(readonly) // @opaque
348348
@usableFromInline // testable
349-
internal init(taggedCocoa cocoa: AnyObject) {
349+
internal init?(taggedCocoa cocoa: AnyObject) {
350350
self.init()
351+
var success = true
351352
self.withMutableCapacity {
352-
let len = _bridgeTagged(cocoa, intoUTF8: $0)
353-
_internalInvariant(len != nil && len! <= _SmallString.capacity,
354-
"Internal invariant violated: large tagged NSStrings")
355-
return len._unsafelyUnwrappedUnchecked
353+
/*
354+
For regular NSTaggedPointerStrings we will always succeed here, but
355+
tagged NSLocalizedStrings may not fit in a SmallString
356+
*/
357+
if let len = _bridgeTagged(cocoa, intoUTF8: $0) {
358+
return len
359+
}
360+
success = false
361+
return 0
362+
}
363+
if !success {
364+
return nil
356365
}
357366
self._invariantCheck()
358367
}

stdlib/public/core/StringBridge.swift

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -335,8 +335,9 @@ internal enum _KnownCocoaString {
335335
#if !(arch(i386) || arch(arm) || arch(arm64_32))
336336

337337
// Resiliently write a tagged _CocoaString's contents into a buffer.
338-
// TODO: move this to the Foundation overlay and reimplement it with
339-
// _NSTaggedPointerStringGetBytes
338+
// The Foundation overlay takes care of bridging tagged pointer strings before
339+
// they reach us, but this may still be called by older code, or by strings
340+
// entering our domain via the arguments to -isEqual:, etc...
340341
@_effects(releasenone) // @opaque
341342
internal func _bridgeTagged(
342343
_ cocoa: _CocoaString,
@@ -370,8 +371,11 @@ private func _withCocoaASCIIPointer<R>(
370371
if requireStableAddress {
371372
return nil // tagged pointer strings don't support _fastCStringContents
372373
}
373-
let tmp = _StringGuts(_SmallString(taggedCocoa: str))
374-
return tmp.withFastUTF8 { work($0.baseAddress._unsafelyUnwrappedUnchecked) }
374+
if let smol = _SmallString(taggedCocoa: str) {
375+
return _StringGuts(smol).withFastUTF8 {
376+
work($0.baseAddress._unsafelyUnwrappedUnchecked)
377+
}
378+
}
375379
}
376380
#endif
377381
defer { _fixLifetime(str) }
@@ -503,7 +507,11 @@ internal func _bridgeCocoaString(_ cocoaString: _CocoaString) -> _StringGuts {
503507
cocoaString, to: __SharedStringStorage.self).asString._guts
504508
#if !(arch(i386) || arch(arm) || arch(arm64_32))
505509
case .tagged:
506-
return _StringGuts(_SmallString(taggedCocoa: cocoaString))
510+
// Foundation should be taking care of tagged pointer strings before they
511+
// reach here, so the only ones reaching this point should be back deployed,
512+
// which will never have tagged pointer strings that aren't small, hence
513+
// the force unwrap here.
514+
return _StringGuts(_SmallString(taggedCocoa: cocoaString)!)
507515
#if arch(arm64)
508516
case .constantTagged:
509517
let taggedContents = getConstantTaggedCocoaContents(cocoaString)!
@@ -530,7 +538,11 @@ internal func _bridgeCocoaString(_ cocoaString: _CocoaString) -> _StringGuts {
530538

531539
#if !(arch(i386) || arch(arm) || arch(arm64_32))
532540
if _isObjCTaggedPointer(immutableCopy) {
533-
return _StringGuts(_SmallString(taggedCocoa: immutableCopy))
541+
// Copying a tagged pointer can produce a tagged pointer, but only if it's
542+
// small enough to definitely fit in a _SmallString
543+
return _StringGuts(
544+
_SmallString(taggedCocoa: immutableCopy).unsafelyUnwrapped
545+
)
534546
}
535547
#endif
536548

test/api-digester/stability-stdlib-abi-without-asserts.test

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ Protocol CodingKey has added inherited protocol Sendable
6565
Protocol CodingKey has generic signature change from <Self : Swift.CustomDebugStringConvertible, Self : Swift.CustomStringConvertible> to <Self : Swift.CustomDebugStringConvertible, Self : Swift.CustomStringConvertible, Self : Swift.Sendable>
6666
Protocol Error has added inherited protocol Sendable
6767
Protocol Error has generic signature change from to <Self : Swift.Sendable>
68+
Constructor _SmallString.init(taggedCocoa:) has mangled name changing from 'Swift._SmallString.init(taggedCocoa: Swift.AnyObject) -> Swift._SmallString' to 'Swift._SmallString.init(taggedCocoa: Swift.AnyObject) -> Swift.Optional<Swift._SmallString>'
69+
Constructor _SmallString.init(taggedCocoa:) has return type change from Swift._SmallString to Swift._SmallString?
6870
Enum Never has added a conformance to an existing protocol Identifiable
6971

7072
// These haven't actually been removed; they are simply marked unavailable.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#import <objc/objc.h>
2+
#import <objc/NSObject.h>
3+
4+
5+
@interface NSSlowTaggedLocalizedString : NSObject
6+
7+
+ (instancetype) createTestString;
8+
+ (void) setContents: (const char *)newContents;
9+
10+
@end
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#import <string.h>
2+
#import "NSSlowTaggedLocalizedString.h"
3+
#import <dlfcn.h>
4+
#import <objc/runtime.h>
5+
#import <Foundation/Foundation.h>
6+
7+
/*
8+
This horrific mess is simulating the new-in-macOS-Ventura tagged pointer strings,
9+
which can have lengths >15 characters, which can cause problems in SmallString,
10+
which used to assume that would never happen. Once CI is running on Ventura or
11+
later, this can be rewritten to use a regular NSLocalizedString.
12+
*/
13+
14+
@implementation NSSlowTaggedLocalizedString
15+
16+
+ (instancetype) createTestString {
17+
#if __LP64__
18+
static dispatch_once_t onceToken;
19+
dispatch_once(&onceToken, ^{
20+
[[[NSString alloc] init] release]; //Make sure NSString is initialized
21+
Class tagClass = objc_lookUpClass("NSTaggedPointerString");
22+
Class ourClass = [NSSlowTaggedLocalizedString class];
23+
24+
Method fastCString = class_getInstanceMethod(ourClass, @selector(_fastCStringContents:));
25+
class_replaceMethod(tagClass, @selector(_fastCStringContents:), method_getImplementation(fastCString), method_getTypeEncoding(fastCString));
26+
27+
Method length = class_getInstanceMethod(ourClass, @selector(length));
28+
class_replaceMethod(tagClass, @selector(length), method_getImplementation(length), method_getTypeEncoding(length));
29+
30+
Method charIndex = class_getInstanceMethod(ourClass, @selector(characterAtIndex:));
31+
class_replaceMethod(tagClass, @selector(characterAtIndex:), method_getImplementation(charIndex), method_getTypeEncoding(charIndex));
32+
33+
Method fastChars = class_getInstanceMethod(ourClass, @selector(_fastCharacterContents));
34+
class_replaceMethod(tagClass, @selector(_fastCharacterContents), method_getImplementation(fastChars), method_getTypeEncoding(fastChars));
35+
36+
Method retain = class_getInstanceMethod(ourClass, @selector(retain));
37+
class_replaceMethod(tagClass, @selector(retain), method_getImplementation(retain), method_getTypeEncoding(retain));
38+
39+
Method release = class_getInstanceMethod(ourClass, @selector(release));
40+
class_replaceMethod(tagClass, @selector(release), method_getImplementation(release), method_getTypeEncoding(release));
41+
42+
Method typeID = class_getInstanceMethod(ourClass, @selector(_cfTypeID));
43+
class_replaceMethod(tagClass, @selector(_cfTypeID), method_getImplementation(typeID), method_getTypeEncoding(typeID));
44+
45+
Method description = class_getInstanceMethod(ourClass, @selector(description));
46+
class_replaceMethod(tagClass, @selector(description), method_getImplementation(description), method_getTypeEncoding(description));
47+
48+
Method getBytes = class_getInstanceMethod(ourClass, @selector(getBytes:maxLength:usedLength:encoding:options:range:remainingRange:));
49+
class_replaceMethod(tagClass, @selector(getBytes:maxLength:usedLength:encoding:options:range:remainingRange:), method_getImplementation(getBytes), method_getTypeEncoding(getBytes));
50+
});
51+
return (NSSlowTaggedLocalizedString *)(void *)CFStringCreateWithCString(NULL, "a", kCFStringEncodingASCII); //make a tagged pointer string
52+
#else
53+
return nil;
54+
#endif
55+
}
56+
57+
static const char *contents = NULL;
58+
59+
+ (void) setContents: (const char *)newContents {
60+
const char *oldContents = contents;
61+
if (newContents) {
62+
contents = strdup(newContents);
63+
} else {
64+
contents = NULL;
65+
}
66+
free((void *)oldContents);
67+
}
68+
69+
- (const char *)_fastCStringContents:(BOOL)nullTerminationRequired {
70+
return contents;
71+
}
72+
73+
- (uint64_t)length {
74+
return strlen(contents);
75+
}
76+
77+
- (id)copyWithZone:(id)unused {
78+
return self;
79+
}
80+
81+
- (uint16_t)characterAtIndex:(NSUInteger)index {
82+
if (index >= [self length]) {
83+
abort();
84+
}
85+
return (uint16_t)contents[index];
86+
}
87+
88+
- (void *) _fastCharacterContents {
89+
return nil;
90+
}
91+
92+
- (id) retain { return self; }
93+
- (oneway void) release {}
94+
95+
- (uint64_t)_cfTypeID {
96+
return 7; //CFString
97+
}
98+
99+
- (id) description {
100+
return self;
101+
}
102+
103+
- (BOOL)getBytes:(void *)buffer maxLength:(uint64_t)max usedLength:(uint64_t *)used encoding:(uint64_t)encoding options:(uint64_t)options range:(NSRange)range remainingRange:(NSRange *)leftover {
104+
assert(encoding == 1 /* ASCII */ || encoding == 4 /* UTF8 */);
105+
strncpy(buffer, contents, max);
106+
if (strlen(contents) > max) {
107+
leftover->location = max;
108+
leftover->length = strlen(contents) - max;
109+
return false;
110+
}
111+
leftover->location = 0;
112+
leftover->length = 0;
113+
return true;
114+
}
115+
116+
@end
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module NSSlowTaggedLocalizedString {
2+
header "NSSlowTaggedLocalizedString.h"
3+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// RUN: mkdir -p %t
2+
// RUN: %target-clang %S/Inputs/NSSlowTaggedLocalizedString/NSSlowTaggedLocalizedString.m -fno-objc-arc -c -o %t/NSSlowTaggedLocalizedString.o
3+
// RUN: %target-build-swift -g -parse-stdlib -Xfrontend -disable-access-control -I %S/Inputs/NSSlowTaggedLocalizedString/ %t/NSSlowTaggedLocalizedString.o %s -o %t/a.out
4+
// RUN: %target-codesign %t/a.out
5+
// RUN: %target-run %t/a.out
6+
7+
// REQUIRES: executable_test
8+
// REQUIRES: objc_interop
9+
10+
import NSSlowTaggedLocalizedString
11+
import Swift
12+
13+
import StdlibUnittest
14+
15+
let longTaggedTests = TestSuite("NonContiguousTaggedStrings")
16+
var constant = "Send Message to different Team"
17+
18+
func runEqualLongTagged() {
19+
let native = constant.withUTF8 { String(decoding: $0, as: UTF8.self) }
20+
let longTagged = NSSlowTaggedLocalizedString.createTest()!
21+
constant.withCString {
22+
NSSlowTaggedLocalizedString.setContents($0)
23+
}
24+
defer {
25+
NSSlowTaggedLocalizedString.setContents(nil)
26+
}
27+
let reverseBridged = unsafeBitCast(native._guts._object.largeAddressBits, to: AnyObject.self)
28+
let eq = reverseBridged.isEqual(to: longTagged)
29+
expectEqual(eq, 1)
30+
_fixLifetime(native)
31+
}
32+
33+
longTaggedTests.test("EqualLongTagged") {
34+
runEqualLongTagged()
35+
}
36+
37+
runAllTests()
38+

0 commit comments

Comments
 (0)