[CP-beta] Fix crash when NSAttributedString is passed to insertText on macOS (#177401)

This pull request is created by [automatic cherry pick workflow](https://github.com/flutter/flutter/blob/main/docs/releases/Flutter-Cherrypick-Process.md#automatically-creates-a-cherry-pick-request)
Please fill in the form below, and a flutter domain expert will evaluate this cherry pick request.

### Issue Link:
What is the link to the issue this cherry-pick is addressing?

There does not appear to be a dedicated issue. The issue description can be found in the PR description:
https://github.com/flutter/flutter/pull/176329

The PR fixes a macOS text input crash caused by down-casting the string argument from `Any` to a `NSString`, from [this API](https://developer.apple.com/documentation/appkit/nstextinputclient/inserttext(_:replacementrange:)#parameters). The documentation says it can either be a `NSString` or `NSAttributedString`

### Changelog Description:
Explain this cherry pick in one line that is accessible to most Flutter developers. See [best practices](https://github.com/flutter/flutter/blob/main/docs/releases/Hotfix-Documentation-Best-Practices.md) for examples

> Fixes a crash on macOS when the input method (or other services) inserts a `NSAttributedString` instead of `NSString` into a text field.

### Impact Description:
What is the impact (ex. visual jank on Samsung phones, app crash, cannot ship an iOS app)? Does it impact development (ex. flutter doctor crashes when Android Studio is installed), or the shipping production app (the app crashes on launch)

App crashes on macOS when interacting with certain IMEs or other system services, if they decided to use `NSAttributedString` instead of `NSString`. It affects production apps.

### Workaround:
Is there a workaround for this issue?

None. Users or app developers have little control over what the IME / services decide to do. The problematic code path in the engine directly interacts with system services so app developer can't workaround that.

### Risk:
What is the risk level of this cherry-pick?

### Test Coverage:
Are you confident that your fix is well-tested by automated tests?

### Validation Steps:
What are the steps to validate that this fix works?

The PR description does not have repro steps. @p1318k could you add the steps to trigger the bug here?
This commit is contained in:
flutteractionsbot 2025-10-27 15:55:52 -07:00 committed by GitHub
parent 082bd44a7c
commit d7ec7e8064
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 79 additions and 1 deletions

View File

@ -781,7 +781,10 @@ static char markerKey;
flutter::TextRange replacedRange(-1, -1);
std::string textBeforeChange = _activeModel->GetText().c_str();
std::string utf8String = [string UTF8String];
// Input string may be NSString or NSAttributedString.
BOOL isAttributedString = [string isKindOfClass:[NSAttributedString class]];
const NSString* rawString = isAttributedString ? [string string] : string;
std::string utf8String = rawString ? [rawString UTF8String] : "";
_activeModel->AddText(utf8String);
if (_activeModel->composing()) {
replacedRange = composingBeforeChange;

View File

@ -2485,4 +2485,79 @@ TEST(FlutterTextInputPluginTest, InsertTextWithCollapsedSelectionInsideComposing
[[FlutterInputPluginTestObjc alloc] testInsertTextWithCollapsedSelectionInsideComposing]);
}
TEST(FlutterTextInputPluginTest, InsertTextHandlesNSAttributedString) {
id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
[engineMock binaryMessenger])
.andReturn(binaryMessengerMock);
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
nibName:@""
bundle:nil];
FlutterTextInputPluginTestDelegate* delegate =
[[FlutterTextInputPluginTestDelegate alloc] initWithBinaryMessenger:binaryMessengerMock
viewController:viewController];
FlutterTextInputPlugin* plugin = [[FlutterTextInputPlugin alloc] initWithDelegate:delegate];
NSDictionary* setClientConfig = @{
@"viewId" : @(kViewId),
@"inputAction" : @"action",
@"inputType" : @{@"name" : @"inputName"},
};
[plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
arguments:@[ @(1), setClientConfig ]]
result:^(id){
}];
// Test with NSAttributedString
NSAttributedString* attributedString =
[[NSAttributedString alloc] initWithString:@"attributed text"];
[plugin insertText:attributedString replacementRange:NSMakeRange(NSNotFound, 0)];
NSDictionary* editingState = [plugin editingState];
EXPECT_STREQ([editingState[@"text"] UTF8String], "attributed text");
EXPECT_EQ([editingState[@"selectionBase"] intValue], 15);
EXPECT_EQ([editingState[@"selectionExtent"] intValue], 15);
}
TEST(FlutterTextInputPluginTest, InsertTextHandlesEmptyAttributedString) {
id engineMock = flutter::testing::CreateMockFlutterEngine(@"");
id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
OCMStub( // NOLINT(google-objc-avoid-throwing-exception)
[engineMock binaryMessenger])
.andReturn(binaryMessengerMock);
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock
nibName:@""
bundle:nil];
FlutterTextInputPluginTestDelegate* delegate =
[[FlutterTextInputPluginTestDelegate alloc] initWithBinaryMessenger:binaryMessengerMock
viewController:viewController];
FlutterTextInputPlugin* plugin = [[FlutterTextInputPlugin alloc] initWithDelegate:delegate];
NSDictionary* setClientConfig = @{
@"viewId" : @(kViewId),
@"inputAction" : @"action",
@"inputType" : @{@"name" : @"inputName"},
};
[plugin handleMethodCall:[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
arguments:@[ @(1), setClientConfig ]]
result:^(id){
}];
// Test with empty NSAttributedString
NSAttributedString* emptyAttributedString = [[NSAttributedString alloc] initWithString:@""];
[plugin insertText:emptyAttributedString replacementRange:NSMakeRange(NSNotFound, 0)];
NSDictionary* editingState = [plugin editingState];
EXPECT_STREQ([editingState[@"text"] UTF8String], "");
EXPECT_EQ([editingState[@"selectionBase"] intValue], 0);
EXPECT_EQ([editingState[@"selectionExtent"] intValue], 0);
}
} // namespace flutter::testing