String processing bug?

I have a dispatching method which is auto-generated by SwiftProtobuf:

/// Parses message by concrete data by method name and it's data.
/// Throws `HandleMethodError.unknownMethod` for methods not handled by this service.
public func parseMessage(from method: String, with data: Data) throws -> Message {
    switch method {
    case "transfer_signaling.TransferSignaling.CreateUpload":
      return try TransferSignaling_CreateUploadRequest(serializedData: data)
    case "transfer_signaling.TransferSignaling.ResumeUpload":
      return try TransferSignaling_ResumeUploadRequest(serializedData: data)
    case "transfer_signaling.TransferSignaling.CancelUpload":
      return try TransferSignaling_CancelUploadRequest(serializedData: data)
    case "transfer_signaling.TransferSignaling.TriggerUploadAction":
      return try TransferSignaling_UploadActionTriggerRequest(serializedData: data)
    case "transfer_signaling.TransferSignaling.CreateDownload":
      return try TransferSignaling_CreateDownloadRequest(serializedData: data)
    case "transfer_signaling.TransferSignaling.StartDownload":
      return try TransferSignaling_StartDownloadRequest(serializedData: data)
    case "transfer_signaling.TransferSignaling.ResumeDownload":
      return try TransferSignaling_ResumeDownloadRequest(serializedData: data)
    case "transfer_signaling.TransferSignaling.CancelDownload":
      return try TransferSignaling_CancelDownloadRequest(serializedData: data)
    case "transfer_signaling.TransferSignaling.DeleteFiles":
      return try TransferSignaling_DeleteFilesRequest(serializedData: data)
    case "transfer_signaling.TransferSignaling.reportDownloadProgress":
      return try TransferSignaling_TransferProgressReport(serializedData: data)
    case "transfer_signaling.TransferSignaling.reportDownloadEnded":
      return try TransferSignaling_DownloadEndedReport(serializedData: data)
    case "transfer_signaling.TransferSignaling.registerEventOnDownloadTrigger":
      return try Common_Empty(serializedData: data)
    case "transfer_signaling.TransferSignaling.registerEventOnCancelTrigger":
      return try Common_Empty(serializedData: data)
    case "transfer_signaling.TransferSignaling.registerEventOnUploadProgressReport":
      return try Common_Empty(serializedData: data)
    default:
      throw HandleMethodError.unknownMethod
    }
}

When invoked with a string of "transfer_signaling.TransferSignaling.CreateDownload" it consistently hits the default case.

I am using Xcode 13.2.1, and I am making use of Swift Concurrency. I have tried different optimization levels. Other strings (method invocations) work.

I have also tried the following handwritten implementation and it works no better:

/// Parses message by concrete data by method name and it's data.
/// Throws `HandleMethodError.unknownMethod` for methods not handled by this service.
public func parseMessage(from method: String, with data: Data) throws -> Message {
    let map: [String: () throws -> Message] = [
      "transfer_signaling.TransferSignaling.CreateUpload": {try TransferSignaling_CreateUploadRequest(serializedData: data)},
      "transfer_signaling.TransferSignaling.ResumeUpload": {try TransferSignaling_ResumeUploadRequest(serializedData: data)},
      "transfer_signaling.TransferSignaling.CancelUpload": {try TransferSignaling_CancelUploadRequest(serializedData: data)},
      "transfer_signaling.TransferSignaling.TriggerUploadAction": {try TransferSignaling_UploadActionTriggerRequest(serializedData: data)},
      "transfer_signaling.TransferSignaling.CreateDownload": {try TransferSignaling_CreateDownloadRequest(serializedData: data)},
      "transfer_signaling.TransferSignaling.StartDownload": {try TransferSignaling_StartDownloadRequest(serializedData: data)},
      "transfer_signaling.TransferSignaling.ResumeDownload": {try TransferSignaling_ResumeDownloadRequest(serializedData: data)},
      "transfer_signaling.TransferSignaling.CancelDownload": {try TransferSignaling_CancelDownloadRequest(serializedData: data)},
      "transfer_signaling.TransferSignaling.DeleteFiles": {try TransferSignaling_DeleteFilesRequest(serializedData: data)},
      "transfer_signaling.TransferSignaling.reportDownloadProgress": {try TransferSignaling_TransferProgressReport(serializedData: data)},
      "transfer_signaling.TransferSignaling.reportDownloadEnded": {try TransferSignaling_DownloadEndedReport(serializedData: data)},
      "transfer_signaling.TransferSignaling.registerEventOnDownloadTrigger": {try Common_Empty(serializedData: data)},
      "transfer_signaling.TransferSignaling.registerEventOnCancelTrigger": {try Common_Empty(serializedData: data)},
      "transfer_signaling.TransferSignaling.registerEventOnUploadProgressReport": {try Common_Empty(serializedData: data)},
    ]

    guard let block = map[method] else {
        throw HandleMethodError.unknownMethod
    }

    return try block()
}

The string is not found in the map. This exact same code works in other projects, and in fact works with an earlier commit of the same project.

One thing that might be worth ruling out: have you inspected the code points of the string you're invoking this method with to ensure there aren't any invisible characters, surprising substitutions, or something like a NUL byte which might cause the strings to print equivalently, but not be truly equal? (Same goes for the string literal you're trying to match against, potentially.)

Then you may switch to the latest commit where it works and bring changes from the next commit (where it doesn't work), perhaps bring those changes one by one or in small groups, until it stops working - that will reveal the culprit. It sounds like memory trashing of some kind. Address sanitizer & Co might also help.

Edit: it might be easier to go in the opposite direction: switch to the latest commit where the app works correctly, apply changes from the next commit into the working set; ensure the app doesn't work correctly; then as the changes are in the working set it is easy to revert them one by one (or in small groups) until the app is working again.

Have you set a breakpoint and verified that the string really is equal to "transfer_signaling.TransferSignaling.CreateDownload"?

1 Like

Yes. The string is a protobuf method invocation, and all the strings are generated by code.

All the strings are code-generated, and the code in question has not been changed in months. I actually see a difference in behavior between my current code and HEAD. The broken code isn't actually committed yet.

More information:

(lldb) po map[method]
nil

(lldb) po map["transfer_signaling.TransferSignaling.CreateDownload"]
▿ Optional<() throws -> Message>
  - some : (Function)

(lldb) po method == "transfer_signaling.TransferSignaling.CreateDownload"
false

When I print the base64 encoding of the utf8 bytes, the strings are actually different:

"dHJhbnNmZXJfc2lnbmFsaW5nLlRyYW5zZmVyU2lnbmFsaW5nLmNyZWF0ZURvd25sb2Fk"
"dHJhbnNmZXJfc2lnbmFsaW5nLlRyYW5zZmVyU2lnbmFsaW5nLkNyZWF0ZURvd25sb2Fk"

Turns out to be a PEBKAC of sorts. The string is subtly different, and I don't know why. In any case, the bug is entirely in my code. Phew!

LmN encodes .c while LkN encodes .C—but it’s doubtful lowercase and uppercase C would look identical?

When you're scanning a 50 character string by eye, it's easy to miss that one change in the middle. That's the PEBKAC part. The part that isn't my fault is that another dev committed the change that changed the case, but did not announce the compatibility break to anyone.

Live and learn.