spotify / ios-sdk

Spotify SDK for iOS
https://developer.spotify.com/documentation/ios/
661 stars 188 forks source link

SDK crashes eventually in `-[SPTAppRemoteMessageStreamWriter writeAvailableDataToStream:]` #396

Open Eskils opened 1 year ago

Eskils commented 1 year ago

Error thrown: NSConcreteMutableData replaceBytesInRange:withBytes:length:]: range {0, 73} exceeds data length 0

Workaround solution until a fix is available:

-> Attempts to replace replaceBytesInRange:withBytes:length: with setData:

class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.

        swizzleWriteAvailableDataToStream()

        return true
    }

    // ...

    private func swizzleWriteAvailableDataToStream() {
        // The modified implementation is approximatley equal to the Spotify SDKs -[SPTAppRemoteMessageStreamWriter writeAvailableDataToStream:]
        // except instead of -[NSMutableData replaceBytesInRange:withBytes:length:], setData is called on the messageBuffer.
        let exchangedImplementation: @convention(c) (NSObject, Selector, OutputStream) -> Void = { messageStreamWriter, selector, stream in
            guard let messageBuffer = messageStreamWriter.value(forKey: "_messageBuffer") as? NSMutableData else {
                return
            }

            while stream.hasSpaceAvailable {
                if messageBuffer.isEmpty {
                    break
                }

                let buffer = messageBuffer.bytes.assumingMemoryBound(to: UInt8.self)
                let result = stream.write(buffer, maxLength: messageBuffer.length)

                if result > 0 {
                    messageBuffer.setData(Data())
                }
            }
        }

        // Make a reference to the class and selector for which to exchange the implementation.
        guard let messageStreamWriterClass = NSClassFromString("SPTAppRemoteMessageStreamWriter") else {
            return
        }
        let writeAvailableDataToStreamSelector = NSSelectorFromString("writeAvailableDataToStream:")
        let writeAvailableDataToStreamSelectorExchanged = NSSelectorFromString("writeAvailableDataToStreamExchanged:")

        // This string describes the types taken by the method. `@` is an instance (id), `:` is a selector. For a full list of types, see: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100
        let types = "@:@"

        let exchangedFunctionPointer = unsafeBitCast(exchangedImplementation, to: UnsafeRawPointer.self)
        let exchangedFunctionImplementation = IMP(exchangedFunctionPointer)

        // Add the modified implementation under a different selector to the class.
        class_addMethod(messageStreamWriterClass, writeAvailableDataToStreamSelectorExchanged, exchangedFunctionImplementation, types)

        guard
            let originalMethodPointer = class_getInstanceMethod(messageStreamWriterClass, writeAvailableDataToStreamSelector),
            let targetMethodPointer = class_getInstanceMethod(messageStreamWriterClass, writeAvailableDataToStreamSelectorExchanged)
        else {
            return
        }

        // Exchange (Swizzle) the two methods. The selector in the SDK will point to the modified implementation,
        // while the other selector (for the modified method) will point to the SDK implementation.
        method_exchangeImplementations(originalMethodPointer, targetMethodPointer)
    }
}