kean / Formatting

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

String.Index is not valid after mutating its associated string

zfoltin opened this issue · comments

Great little util, thanks for it! ❤️

I've just run into this issue with longer Cyrillic text (and a simple <b>...</b> in there):
https://forums.swift.org/t/string-index-is-not-valid-after-mutating-its-associated-string/30851

It's an issue as text gets mutated between didStartElement and didEndElement calls.

So instead of storing String.Index, I'm recording the Int position in Element (text.count) and creating the NSRange based on that in didEndElement.

     private struct Element {
         let name: String
-        let startIndex: String.Index
+        let startPosition: Int
         let attributes: [NSAttributedString.Key: Any]
     }
...

     func parser(_ parser: XMLParser,
                 didStartElement elementName: String,
                 namespaceURI: String?,
                 qualifiedName qName: String?,
                 attributes attributeDict: [String: String] = [:]) {
         var attributes = style.getAttributes(for: elementName) ?? [:]
         if elementName == "a", let url = attributeDict["href"].map(URL.init(string:)) {
             attributes[.link] = url
         }
-        let element = Element(name: elementName, startIndex: text.endIndex, attributes: attributes)
+        let element = Element(name: elementName, startPosition: text.count, attributes: attributes)
         elements.append(element)
     }
 
     func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
         guard let element = elements.popLast() else {
             return assertionFailure("No opening tag for \(elementName)")
         }
         guard element.name == elementName else {
             return assertionFailure("Closing tag mismatch. Expected: \(element.name), got: \(elementName)")
         }
-        let range = NSRange(element.startIndex..<text.endIndex, in: text)
+        let range = NSRange(location: element.startPosition, length: text.count - element.startPosition)
         attributes.append((range, element.attributes))
     }

Hope this helps

Thanks, can you share the example text so that I could add a unit test for this?

I haven't seen this issue yet, my assumption was that String doesn't invalidate its indices after appending. If it does, that's going to be a problem. Yeah, switching to offset-based indexing will fix it.

This piece of string caused a crash: "Если вы забыли свой пароль на новом устройстве, вам также будет отправлен <b>6-значный код проверки</b>"
While a similar string worked: "Вы получите временный <b>4-значный пароль</b> через SMS, который вам нужно будет изменить" ...so not quite sure if it's language related or just my environment somehow.

I opened a PR #2.

Looking good 👍 Strings, especially the indexing, in Swift is 🤯 (or more like 💩). Add some compiler optimisation to the mix and we end up with weird stuff like this. I didn't expect indexes to get invalidated when a string "grows" either.