Skip to content

Conversation

justinmc
Copy link
Contributor

@justinmc justinmc commented Mar 23, 2023

This PR changes the character boundary behavior of obscured fields to be based on code points instead of code units.

So it used to be possible to traverse and delete obscured text inside of code points (and breaking a code point like that would cause a crash):

output2

But now moving the cursor and deleting is based on code points:

output1

Native behavior

Native iOS deletes part of the emoji, native Mac deletes the whole emoji. See #122381 (comment). So it's unclear what the desired behavior should actually be.

Resources

Fixes #122381

I thought this might not fix the case where a broken emoji is directly pasted into the field, but it seems to work by trying this: �‍👩‍👦‍👦

CC @LongCatIsLooong

@justinmc justinmc requested a review from Renzo-Olivares March 23, 2023 23:46
@flutter-dashboard flutter-dashboard bot added a: text input Entering text in a text field or keyboard related problems framework flutter/packages/flutter repository. See also f: labels. labels Mar 23, 2023
@justinmc justinmc self-assigned this Mar 23, 2023
@Renzo-Olivares
Copy link
Contributor

Renzo-Olivares commented Mar 27, 2023

It makes sense to me that we should remove the entire emoji when pressing delete since I'm not sure what a user would expect if only a portion of it is deleted in a password field. I do wonder why _CodeUnitBoundary was used vs CharacterBoundary in the case on an obscure field maybe there are some cases where it is needed. Looking at the failing tests it looks like this causes some unexpected changes.

        const String testCluster = '👨‍👩‍👦👨‍👩‍👦👨‍👩‍👦'; // 8 * 3
        const LogicalKeyboardKey trigger = LogicalKeyboardKey.delete;
        testWidgets('cross-cluster obscured text', (WidgetTester tester) async {
          controller.text = testCluster;
          controller.selection = const TextSelection(
            baseOffset: 1,
            extentOffset: 9,
          );

          await tester.pumpWidget(buildEditableText(obscured: true));
          await sendKeyCombination(tester, const SingleActivator(trigger));

          expect(
            controller.text,
            '👨‍👩‍👦👨‍👩‍👦',
          );

          expect(
            controller.selection,
            const TextSelection.collapsed(offset: 1),
          );
        }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));
      });

Though looking at this test, it looks like we already expect that delete deletes the entire grapheme cluster.

@LongCatIsLooong
Copy link
Contributor

LongCatIsLooong commented Mar 28, 2023

This is a bit concerning that the whole emoji gets deleted, other people would easily be able to tell there are zwjs in the password. I think the problem isn't with "complex" characters, the crash is likely caused by surrogate pairs being broken up and the crash happens because it isn't valid encoding. Maybe try turning CodeUnitBoundary into CodePointBoundary (in retrospect it definitely doesn't make sense to introduce CodeUnitBoundary).
Also when obfuscating the password I think each codepoint should be turned into a *, not each code unit ( I think that's what we are doing right now?)

@flutter-dashboard
Copy link

This pull request executed golden file tests, but it has not been updated in a while (20+ days). Test results from Gold expire after as many days, so this pull request will need to be updated with a fresh commit in order to get results from Gold.

For more guidance, visit Writing a golden file test for package:flutter.

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

@goderbauer
Copy link
Member

(triage) @justinmc Do you still have plans for this PR?

@justinmc
Copy link
Contributor Author

Taking a look to see if I can turn this PR around, I'll make a decision on it by EOD today.

@justinmc
Copy link
Contributor Author

I forgot about this PR, but I played around with it today and I think I have a solution. Will put this in draft mode and open it for review soon.

@justinmc justinmc marked this pull request as draft May 10, 2023 23:58
@justinmc justinmc marked this pull request as ready for review May 11, 2023 18:57
@justinmc justinmc changed the title Treat complex characters as a single character in obscure mode Fix the breaking of multi-code-unit characters in obscure mode May 11, 2023
@justinmc justinmc requested a review from LongCatIsLooong May 11, 2023 20:02
@github-actions github-actions bot added a: internationalization Supporting other languages or locales. (aka i18n) d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos documentation engine flutter/engine related. See also e: labels. f: integration_test The flutter/packages/integration_test plugin platform-ios iOS applications specifically labels May 15, 2023
@justinmc justinmc removed the request for review from christopherfujino May 16, 2023 18:40
if (position < 0) {
return null;
}
if (position == 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like when position == 0 it's always going to return 0 even when the text is empty?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I guess we should be returning null when the text is empty? I've pushed a commit to do that for both leading/trailing.

return position;
}

return _breaksSurrogatePair(position)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If position points to a high surrogate the method should return position?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. If position points to a high surrogate then _breaksSurrogatePair will return false, and position will be returned. This is correct because position is always a code point boundary when it points to a high surrogate.

Let me know if I have anything wrong there.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see _breaksSurrogatePair also checks for position - 1. It's probably not necessary since the program would have crashed before getting here if the string isn't valid UTF16. That would simplify the logic in this method too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems possible to get here with a malformed string, at least in some cases. I added a malformed string test that seems to pass on linux, let's see if it passes on other platforms. Also, I can paste a malformed string (👨‍👩‍�) into the app on Linux without a crash.

Maybe we should be checking for this kind of thing somewhere.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh interesting. Do you mind adding a test case for that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/// * https://en.wikipedia.org/wiki/UTF-16#Code_points_from_U+010000_to_U+10FFFF
/// * [isLowSurrogate], which checks the same thing for low (second)
/// surrogates.
static bool isHighSurrogate(int value) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe assert value is in the correct range?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tried this on your suggestion, but then realized that there were a million test failures. I think these methods check lots of code units that are not necessarily surrogates.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry I meant valid UTF16 code range 0x0 – 0xFFFFF

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh agreed, I'll do that!

return position;
}

return _breaksSurrogatePair(position)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see _breaksSurrogatePair also checks for position - 1. It's probably not necessary since the program would have crashed before getting here if the string isn't valid UTF16. That would simplify the logic in this method too.

if (position < 0) {
return 0;
}
if (position == _text.length - 1) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By defninition, if the last code unit is a low surrogate then getTrailingTextBoundaryAt(_text.length - 1) should return null?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it was right as-is, but let me know if I'm misunderstanding. An example would be the string '👨', a valid surrogate pair, where the last code unit is a low surrogate. If position is in the center of that surrogate pair, then the trailing boundary is at the end of the string, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method should return the next start index of a code point AFTER position. There's nothing after the last code unit so null should be returned?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We talked offline and decided that it is possible to return _text.length here. I've updated the docs to explicitly state this in 754a3b0.

);

await tester.tap(find.byType(EditableText));
await tester.enterText(find.byType(EditableText), '👨‍👩‍�');
Copy link
Contributor

@LongCatIsLooong LongCatIsLooong May 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you add a comment to explain what code unit this is? I can't really copy that since it's rendered as 0xFFFD and it's copied as such.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also what happens if there's a sequence of high surrogates or low surrogates?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. In cacb4fc I've replaced these crazy characters with their \u equivalents and left comments about where they come from. I also added two tests, one with a series of high surrogates and one with a series of low surrogates.

@justinmc justinmc added the autosubmit Merge PR when tree becomes green via auto submit App label May 23, 2023
@auto-submit auto-submit bot merged commit 4341ed4 into flutter:master May 23, 2023
@justinmc justinmc deleted the obscure-emojis branch May 23, 2023 20:22
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request May 24, 2023
auto-submit bot pushed a commit to flutter/packages that referenced this pull request May 24, 2023
flutter/flutter@f86c529...216605b

2023-05-24 engine-flutter-autoroll@skia.org Roll Flutter Engine from aba9dc4640c2 to eebcf36cd38f (3 revisions) (flutter/flutter#127458)
2023-05-24 engine-flutter-autoroll@skia.org Roll Flutter Engine from 76afb431a740 to aba9dc4640c2 (1 revision) (flutter/flutter#127453)
2023-05-24 engine-flutter-autoroll@skia.org Roll Flutter Engine from fc6df9512aa2 to 76afb431a740 (3 revisions) (flutter/flutter#127445)
2023-05-23 49699333+dependabot[bot]@users.noreply.github.com Bump actions/labeler from 6b107e7a7ee5e054e0bcce60de5181d21e2f00fb to 2713f7303c96cb1e69627957ec16eea0fd7f94a4 (flutter/flutter#127438)
2023-05-23 engine-flutter-autoroll@skia.org Roll Flutter Engine from 3535e0d301dd to fc6df9512aa2 (1 revision) (flutter/flutter#127440)
2023-05-23 47866232+chunhtai@users.noreply.github.com Migrates android semanitcs integration to integration test (flutter/flutter#127128)
2023-05-23 engine-flutter-autoroll@skia.org Roll Flutter Engine from 23b04314d5d2 to 3535e0d301dd (3 revisions) (flutter/flutter#127428)
2023-05-23 engine-flutter-autoroll@skia.org Roll Flutter Engine from ccf50f344d5d to 23b04314d5d2 (2 revisions) (flutter/flutter#127421)
2023-05-23 ian@hixie.ch Give channel descriptions in `flutter channel`, use branch instead of upstream for channel name (flutter/flutter#126936)
2023-05-23 72562119+tgucio@users.noreply.github.com Avoid catching errors in TextPainter tests (flutter/flutter#127307)
2023-05-23 christopherfujino@gmail.com [flutter_tools] delete entitlements files after copying to macos build dir (flutter/flutter#127417)
2023-05-23 engine-flutter-autoroll@skia.org Roll Flutter Engine from a6e37bde65fd to ccf50f344d5d (7 revisions) (flutter/flutter#127416)
2023-05-23 113669907+NikolajHarderNota@users.noreply.github.com modal bottom sheet barrier label (flutter/flutter#126431)
2023-05-23 jmccandless@google.com Fix the breaking of multi-code-unit characters in obscure mode (flutter/flutter#123366)
2023-05-23 engine-flutter-autoroll@skia.org Roll Flutter Engine from 168b0bf3f70d to a6e37bde65fd (1 revision) (flutter/flutter#127397)
2023-05-23 vashworth@google.com Revert "Log all output of ios-deploy" (flutter/flutter#127405)
2023-05-23 engine-flutter-autoroll@skia.org Roll Packages from 83959fb to d449a17 (6 revisions) (flutter/flutter#127394)
2023-05-23 vashworth@google.com Log all output of ios-deploy (flutter/flutter#127222)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-packages
Please CC bmparr@google.com,rmistry@google.com,stuartmorgan@google.com on the revert to ensure that a human
is aware of the problem.

To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://bugs.chromium.org/p/skia/issues/entry?template=Autoroller+Bug

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
CaseyHillers pushed a commit to CaseyHillers/flutter that referenced this pull request May 24, 2023
…er#123366)

This PR changes the character boundary behavior of obscured fields to be based on code points instead of code units.

So it used to be possible to traverse and delete obscured text inside of code points (and breaking a code point like that would cause a crash):

![output2](https://github.com/flutter/flutter/assets/389558/674c89a4-c47d-4cdc-a402-4cadb5d2f73b)

But now moving the cursor and deleting is based on code points:

![output1](https://github.com/flutter/flutter/assets/389558/e46301f7-b5af-48d2-812a-0ad649f1383b)

### Native behavior

Native iOS deletes part of the emoji, native Mac deletes the whole emoji.  See flutter#122381 (comment).  So it's unclear what the desired behavior should actually be.

### Resources

Fixes flutter#122381

I thought this might not fix the case where a broken emoji is directly pasted into the field, but it seems to work by trying this:  �������

CC @LongCatIsLooong
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 16, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 17, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 17, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 17, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a: internationalization Supporting other languages or locales. (aka i18n) a: text input Entering text in a text field or keyboard related problems autosubmit Merge PR when tree becomes green via auto submit App d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos engine flutter/engine related. See also e: labels. f: integration_test The flutter/packages/integration_test plugin framework flutter/packages/flutter repository. See also f: labels. platform-ios iOS applications specifically
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[macOS][desktop]: Deleting emojis from textfield with obscured text causes crash
4 participants