-
-
Notifications
You must be signed in to change notification settings - Fork 368
refactor: Migrate SentryMsgPackSerializer from Objective-C to Swift #6143
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a121c88
6eb1e43
3021956
810f132
b73f131
8d45ba3
9b91dac
7726ae1
2307fe1
5181e9e
568d287
157de83
e7abc0a
0442fd3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
@testable import Sentry | ||
|
||
private class ErrorInputStream: InputStream { | ||
override var hasBytesAvailable: Bool { | ||
return true | ||
} | ||
|
||
override func read(_ buffer: UnsafeMutablePointer<UInt8>, maxLength len: Int) -> Int { | ||
return -1 // Simulate read error | ||
} | ||
|
||
override func open() { | ||
// No-op | ||
} | ||
|
||
override func close() { | ||
// No-op | ||
} | ||
} | ||
|
||
public class TestStreamableObject: NSObject, SentryStreamable { | ||
|
||
private let shouldReturnNilInputStream: Bool | ||
private let streamSizeValue: UInt? | ||
private let shouldReturnErrorStream: Bool | ||
|
||
public init(streamSize: UInt?, shouldReturnNilInputStream: Bool, shouldReturnErrorStream: Bool = false) { | ||
self.streamSizeValue = streamSize | ||
self.shouldReturnNilInputStream = shouldReturnNilInputStream | ||
self.shouldReturnErrorStream = shouldReturnErrorStream | ||
super.init() | ||
} | ||
|
||
public func asInputStream() -> InputStream? { | ||
if shouldReturnNilInputStream { | ||
return nil | ||
} | ||
if shouldReturnErrorStream { | ||
return ErrorInputStream() | ||
} | ||
return InputStream(data: Data()) | ||
} | ||
|
||
public func streamSize() -> UInt? { | ||
return streamSizeValue | ||
} | ||
|
||
// MARK: - Convenience factory methods for common test scenarios | ||
|
||
public static func objectWithNilInputStream() -> TestStreamableObject { | ||
return TestStreamableObject(streamSize: 10, shouldReturnNilInputStream: true) | ||
} | ||
|
||
public static func objectWithZeroSize() -> TestStreamableObject { | ||
return TestStreamableObject(streamSize: 0, shouldReturnNilInputStream: false) | ||
} | ||
|
||
public static func objectWithNegativeSize() -> TestStreamableObject { | ||
return TestStreamableObject(streamSize: nil, shouldReturnNilInputStream: false) | ||
} | ||
|
||
public static func objectWithErrorStream() -> TestStreamableObject { | ||
return TestStreamableObject(streamSize: 10, shouldReturnNilInputStream: false, shouldReturnErrorStream: true) | ||
} | ||
|
||
public static func objectWithZeroBytesRead() -> TestStreamableObject { | ||
return TestStreamableObject(streamSize: 10, shouldReturnNilInputStream: false, shouldReturnErrorStream: false) | ||
} | ||
|
||
public static func objectWithLargeSize() -> TestStreamableObject { | ||
// Return size larger than UInt32.max to test truncation | ||
return TestStreamableObject( | ||
streamSize: UInt.max, | ||
shouldReturnNilInputStream: false, | ||
shouldReturnErrorStream: false | ||
) | ||
} | ||
} |
This file was deleted.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
extension Data: SentryStreamable { | ||
func asInputStream() -> InputStream? { | ||
return InputStream(data: self) | ||
} | ||
|
||
func streamSize() -> UInt? { | ||
return UInt(self.count) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
/** | ||
* This is a partial implementation of the MessagePack format. | ||
* We only need to concatenate a list of NSData into an envelope item. | ||
*/ | ||
class SentryMsgPackSerializer { | ||
@objc | ||
static func serializeDictionary(toMessagePack dictionary: [String: Any], intoFile fileURL: URL) -> Bool { | ||
do { | ||
try serializeToFile(dictionary: dictionary, fileURL: fileURL) | ||
return true | ||
} catch { | ||
SentrySDKLog.error("Failed to serialize dictionary to MessagePack - Error: \(error)") | ||
// Clean up partial file on error | ||
do { | ||
try FileManager.default.removeItem(at: fileURL) | ||
} catch { | ||
// Ignore cleanup errors - file might not exist | ||
} | ||
return false | ||
} | ||
} | ||
|
||
// swiftlint:disable:next function_body_length cyclomatic_complexity | ||
private static func serializeToFile(dictionary: [String: Any], fileURL: URL) throws { | ||
guard let outputStream = OutputStream(url: fileURL, append: false) else { | ||
throw SentryMsgPackSerializerError.outputError("Failed to create output stream for file: \(fileURL)") | ||
} | ||
outputStream.open() | ||
defer { | ||
outputStream.close() | ||
} | ||
|
||
// Check if stream opened successfully | ||
if outputStream.streamError != nil { | ||
throw SentryMsgPackSerializerError.outputError("Failed to open output stream for file: \(fileURL)") | ||
} | ||
|
||
let mapHeader = UInt8(truncatingIfNeeded: 0x80 | dictionary.count) // Map up to 15 elements | ||
_ = outputStream.write([mapHeader], maxLength: 1) | ||
|
||
for (key, anyValue) in dictionary { | ||
philprime marked this conversation as resolved.
Show resolved
Hide resolved
|
||
guard let value = anyValue as? SentryStreamable else { | ||
throw SentryMsgPackSerializerError.invalidValue("Value does not conform to SentryStreamable: \(anyValue)") | ||
} | ||
guard let keyData = key.data(using: .utf8) else { | ||
throw SentryMsgPackSerializerError.invalidInput("Could not encode key as UTF-8: \(key)") | ||
} | ||
|
||
let str8Header: UInt8 = 0xD9 // String up to 255 characters | ||
let keyLength = UInt8(truncatingIfNeeded: keyData.count) // Truncates if > 255, matching Objective-C behavior | ||
_ = outputStream.write([str8Header], maxLength: 1) | ||
_ = outputStream.write([keyLength], maxLength: 1) | ||
|
||
philprime marked this conversation as resolved.
Show resolved
Hide resolved
|
||
keyData.withUnsafeBytes { bytes in | ||
guard let bufferAddress = bytes.bindMemory(to: UInt8.self).baseAddress else { | ||
return | ||
} | ||
_ = outputStream.write(bufferAddress, maxLength: keyData.count) | ||
} | ||
|
||
guard let dataLength = value.streamSize(), dataLength > 0 else { | ||
// MsgPack is being used strictly for session replay. | ||
// An item with a length of 0 will not be useful. | ||
// If we plan to use MsgPack for something else, | ||
// this needs to be re-evaluated. | ||
throw SentryMsgPackSerializerError.emptyData("Data for MessagePack dictionary has no content - Input: \(value)") | ||
} | ||
philprime marked this conversation as resolved.
Show resolved
Hide resolved
philprime marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
let valueLength = UInt32(truncatingIfNeeded: dataLength) | ||
// We will always use the 4 bytes data length for simplicity. | ||
// Worst case we're losing 3 bytes. | ||
let bin32Header: UInt8 = 0xC6 | ||
philprime marked this conversation as resolved.
Show resolved
Hide resolved
|
||
_ = outputStream.write([bin32Header], maxLength: 1) | ||
|
||
// Write UInt32 as big endian bytes | ||
let lengthBytes = [ | ||
UInt8((valueLength >> 24) & 0xFF), | ||
UInt8((valueLength >> 16) & 0xFF), | ||
UInt8((valueLength >> 8) & 0xFF), | ||
UInt8(valueLength & 0xFF) | ||
] | ||
_ = outputStream.write(lengthBytes, maxLength: 4) | ||
|
||
guard let inputStream = value.asInputStream() else { | ||
throw SentryMsgPackSerializerError.streamError("Could not get input stream - Input: \(value)") | ||
} | ||
|
||
inputStream.open() | ||
defer { inputStream.close() } | ||
|
||
var buffer = [UInt8](repeating: 0, count: 1_024) | ||
var bytesRead: Int | ||
|
||
while inputStream.hasBytesAvailable { | ||
bytesRead = inputStream.read(&buffer, maxLength: buffer.count) | ||
if bytesRead > 0 { | ||
_ = outputStream.write(buffer, maxLength: bytesRead) | ||
philprime marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} else if bytesRead < 0 { | ||
throw SentryMsgPackSerializerError.streamError("Error reading bytes from input stream - Input: \(value) - Bytes read: \(bytesRead)") | ||
} | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
enum SentryMsgPackSerializerError: Error { | ||
case dictionaryTooLarge | ||
case invalidValue(String) | ||
case invalidInput(String) | ||
case emptyData(String) | ||
case streamError(String) | ||
case outputError(String) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
protocol SentryStreamable { | ||
func asInputStream() -> InputStream? | ||
func streamSize() -> UInt? | ||
} | ||
philprime marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
extension URL: SentryStreamable { | ||
func asInputStream() -> InputStream? { | ||
return InputStream(url: self) | ||
} | ||
|
||
func streamSize() -> UInt? { | ||
let attributes: [FileAttributeKey: Any] | ||
philprime marked this conversation as resolved.
Show resolved
Hide resolved
|
||
do { | ||
attributes = try FileManager.default.attributesOfItem(atPath: path) | ||
} catch { | ||
SentrySDKLog.error("Could not read file attributes - File: \(self) - Error: \(error)") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Logging Level Change Causes Unexpected NoiseFile attribute errors in |
||
return nil | ||
} | ||
guard let fileSize = attributes[.size] as? NSNumber else { | ||
SentrySDKLog.error("Could not read file size attribute - File: \(self)") | ||
return nil | ||
} | ||
return fileSize.uintValue | ||
} | ||
philprime marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} |
Uh oh!
There was an error while loading. Please reload this page.