I think we should be clearer with the terms and phrases that we're using. It may give people a false impression, especially as you've mentioned that this may have security implications.
To be precise: extracting a Character from a String is not O(n). It does depend on the length of the character, but not the length of the string beyond that character. The length of the string is the salient n because extraction is an operation that one performs on a string (subscripting using an index), not on a character.
For any Character, if I keep that character constant, and subscript it from a String that contains 1/2/4/8 other characters, the time to extract that character using its index does not change. In that respect it is O(1).
This highlights an important point when discussing algorithmic complexity - it pertains to scaling, not absolute performance. The issue here is that an individual Character may be surprisingly expensive to extract in absolute terms.
That means that this is not, in fact, quadratic:
In this example, if you double the number of characters in string, execution time should scale linearly (with the outer loop), even if the first character is one of these pathological cases consisting of hundreds or thousands of code-points.
I hacked together a little script to test it. The first character is a pathological case like in your example: "a" + String(repeating: "\u{302}", count: 20_000) (I shortened it to 20K scalars rather than 20M), and the rest of the string is a string of "x"s. Here are my results:
Command: swiftc -O main.swift && ./main
| Tail Length |
Time (ns, relative to TL=1) |
| 1, |
1.0 |
| 2, |
1.1209763106021202 |
| 4, |
1.450454106412358 |
| 8, |
2.119564980202834 |
| 16, |
3.3017124757373306 |
| 32, |
6.015886260665994 |
| 64, |
11.696052411165708 |
| 128, |
23.113525354604906 |
| 256, |
45.930820600690446 |
| 512, |
91.53868614042064 |
| 1024, |
182.80684011048763 |
| 2048, |
365.3415016794289 |
| 4096, |
730.4373593487122 |
When I plot these figures, they show exactly what we expect - linear execution time, from the outer loop. I eyeballed it as roughly fitting y=0.176x:
