Skip to content

Conversation

philprime
Copy link
Member

@philprime philprime commented Sep 11, 2025

Summary

This PR migrates the SentryMsgPackSerializer from Objective-C to Swift while maintaining 100% behavioral compatibility.

Changes Made

Complete Migration

  • Converted SentryMsgPackSerializer.m/.h to modular Swift implementation
  • Reorganized code into separate files in Sources/Swift/Tools/MsgPack/ directory:
    • SentryMsgPackSerializer.swift - Main serialization logic
    • SentryStreamable.swift - Protocol definition
    • SentryMsgPackSerializerError.swift - Error types
    • Data+SentryStreamable.swift - Data extension
    • URL+SentryStreamable.swift - URL extension
    • NSData+SentryStreamable.swift - NSData extension
    • NSURL+SentryStreamable.swift - NSURL extension

Behavioral Compatibility

  • Maintained exact behavior matching original Objective-C implementation
  • Preserved edge cases like silent key length truncation for >255 byte keys
  • Kept error handling patterns including -1 return values for file size errors
  • Maintained logging levels (error vs debug) matching original code

Enhanced Test Coverage

  • Added comprehensive test suite with 14 test cases covering all code paths
  • Added edge case testing for large dictionaries, long keys, stream errors
  • Improved error validation for nil input streams and invalid file paths
  • AAA test pattern with proper setup/teardown and temp file cleanup

Implementation Improvements

  • Modern Swift patterns with proper error throwing instead of boolean returns
  • Type safety with explicit error types via SentryMsgPackSerializerError
  • Memory safety improvements while maintaining compatibility
  • Cleaner byte operations using modern Swift APIs

Key Technical Details

  • Protocol signature: Uses streamSize() -> Int to support -1 error values from Objective-C
  • Truncating conversion: Uses UInt8(truncatingIfNeeded:) to match Objective-C silent truncation
  • Error propagation: Swift errors are caught and converted to boolean returns for Objective-C compatibility
  • File I/O: Improved path validation through Swift's Data.write(to:) method

Testing

All existing functionality verified through comprehensive test suite:

  • 14 tests covering all code paths and edge cases
  • 100% backward compatibility with existing behavior
  • Proper error handling for all failure scenarios

Closes #6140

#skip-changelog

…nverted tests

- Updated SentryMsgPackSerializer to log errors instead of debug messages for empty data and input stream issues.
- Modified the `asInputStream` method in the SentryStreamable protocol to return nullable streams.
- Removed outdated Objective-C tests and added comprehensive Swift tests for SentryMsgPackSerializer, covering various scenarios including nil input streams and invalid file paths.
- Ensured proper cleanup of temporary files in tests.
Copy link

codecov bot commented Sep 11, 2025

Codecov Report

❌ Patch coverage is 97.04142% with 5 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@3e57e15). Learn more about missing BASE report.
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
.../Swift/Tools/MsgPack/SentryMsgPackSerializer.swift 96.666% 3 Missing ⚠️
...ces/Swift/Tools/MsgPack/URL+SentryStreamable.swift 88.235% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff            @@
##             main     #6143   +/-   ##
========================================
  Coverage        ?   86.828%           
========================================
  Files           ?       442           
  Lines           ?     37345           
  Branches        ?     17399           
========================================
  Hits            ?     32426           
  Misses          ?      4644           
  Partials        ?       275           
Files with missing lines Coverage Δ
SentryTestUtils/TestStreamableObject.swift 100.000% <100.000%> (ø)
Sources/Sentry/SentryClient.m 99.329% <ø> (ø)
...es/Swift/Tools/MsgPack/Data+SentryStreamable.swift 100.000% <100.000%> (ø)
Sources/Swift/Tools/SentryEnvelopeItem.swift 91.034% <100.000%> (ø)
...ces/Swift/Tools/MsgPack/URL+SentryStreamable.swift 88.235% <88.235%> (ø)
.../Swift/Tools/MsgPack/SentryMsgPackSerializer.swift 96.666% <96.666%> (ø)

Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 3e57e15...568d287. Read the comment docs.

Copy link
Contributor

github-actions bot commented Sep 11, 2025

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 1196.13 ms 1225.79 ms 29.66 ms
Size 23.75 KiB 992.26 KiB 968.52 KiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
6e99155 1223.96 ms 1249.25 ms 25.29 ms
0529194 1237.23 ms 1254.67 ms 17.44 ms
22b6996 1234.00 ms 1263.24 ms 29.24 ms
13bc1aa 1244.04 ms 1262.89 ms 18.85 ms
01faa71 1238.81 ms 1263.98 ms 25.17 ms
fac4ca3 1222.81 ms 1235.83 ms 13.02 ms
7eafbde 1212.16 ms 1243.64 ms 31.48 ms
f2bfecd 1234.92 ms 1250.34 ms 15.42 ms
4d264fa 1223.48 ms 1246.91 ms 23.44 ms
2a07609 1207.79 ms 1233.77 ms 25.98 ms

App size

Revision Plain With Sentry Diff
6e99155 23.75 KiB 963.18 KiB 939.43 KiB
0529194 23.74 KiB 891.02 KiB 867.28 KiB
22b6996 23.75 KiB 908.02 KiB 884.27 KiB
13bc1aa 23.75 KiB 908.40 KiB 884.65 KiB
01faa71 23.75 KiB 926.77 KiB 903.02 KiB
fac4ca3 23.75 KiB 902.01 KiB 878.27 KiB
7eafbde 23.75 KiB 927.53 KiB 903.78 KiB
f2bfecd 23.75 KiB 919.68 KiB 895.93 KiB
4d264fa 23.74 KiB 874.07 KiB 850.33 KiB
2a07609 23.75 KiB 912.78 KiB 889.03 KiB

Previous results on branch: philprime/msg-pack-serializer-null-handling

Startup times

Revision Plain With Sentry Diff
8215a0d 1206.23 ms 1237.04 ms 30.81 ms
c0e1523 1229.65 ms 1263.41 ms 33.76 ms
43dc3b5 1236.69 ms 1255.65 ms 18.95 ms

App size

Revision Plain With Sentry Diff
8215a0d 23.75 KiB 969.21 KiB 945.46 KiB
c0e1523 23.75 KiB 988.63 KiB 964.88 KiB
43dc3b5 23.75 KiB 988.55 KiB 964.80 KiB

@philprime philprime marked this pull request as draft September 11, 2025 11:32
…nal serialization tests

- Added support for error streams in TestStreamableObject.
- Introduced new test cases for serializing empty dictionaries, single elements, large dictionaries, long keys, and handling invalid paths.
- Implemented a custom ErrorInputStream to simulate read errors during serialization.
…ntation

- Deleted the Objective-C SentryMsgPackSerializer and its associated header files.
- Introduced a new Swift implementation of SentryMsgPackSerializer with improved error handling.
- Added SentryStreamable protocol and extensions for Data, NSData, NSURL, and URL to support serialization.
- Updated tests to validate the new Swift serialization logic and error handling.
@philprime philprime changed the title refactor: Add nullability-handling to SentryMsgPackSerializer with converted tests refactor: Migrate SentryMsgPackSerializer from Objective-C to Swift Sep 15, 2025
…r error propagation

- Changed keyData.withUnsafeBytes to use try for improved error handling.
- This ensures that any errors during buffer address retrieval are properly thrown.
@philprime
Copy link
Member Author

@cursor review

cursor[bot]

This comment was marked as outdated.

- Introduced TestStreamableObject to simulate various SentryStreamable behaviors, including handling nil and error streams.
- Updated SentryMsgPackSerializerTests to utilize TestStreamableObject for improved test coverage on serialization scenarios.
- Removed redundant TestStreamableObject implementation from SentryMsgPackSerializerTests to streamline code.
@philprime
Copy link
Member Author

@cursor review
@seer review

cursor[bot]

This comment was marked as outdated.

@philprime philprime marked this pull request as ready for review September 23, 2025 12:49
cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

@philprime philprime requested a review from noahsmartin October 9, 2025 11:58
@philprime
Copy link
Member Author

@sentry review

return nil
}
return fileSize.uintValue
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Inconsistent Error Handling in Streamable Protocol

The SentryStreamable.streamSize() protocol method returns UInt? instead of NSInteger. This prevents signaling errors with negative values (like -1), which existing serializer logic expects. For example, the URL extension returns nil for file read errors, but the serializer expects a negative value to indicate an issue, breaking error handling compatibility.

Additional Locations (1)

Fix in Cursor Fix in Web

// this needs to be re-evaluated.
SentrySDKLog.error("Data for MessagePack dictionary has no content - Input: \(value)")
throw SentryMsgPackSerializerError.emptyData("Empty data for MessagePack dictionary")
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Serializer Fails on Empty Streams

The SentryMsgPackSerializer incorrectly rejects valid zero-length data streams, treating them as errors. This diverges from the Objective-C implementation, breaking backward compatibility and causing SentrySDKLog.error messages for what should be non-error scenarios.

Fix in Cursor Fix in Web


let mapHeader = UInt8(0x80 | dictionary.count) // Map up to 15 elements
_ = outputStream.write([mapHeader], maxLength: 1)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The map header implementation silently truncates dictionaries with more than 15 elements by using UInt8(0x80 | dictionary.count). This produces incorrect MessagePack format for dictionaries with 16+ elements. Consider either:

  1. Using fixmap16 (0xDE) or fixmap32 (0xDF) format for larger dictionaries
  2. Validating dictionary size and throwing an error if it exceeds 15 elements
  3. Adding explicit documentation/warning about this limitation

The current silent truncation can lead to data corruption that's difficult to debug.

Suggested change
// Validate dictionary size for proper MessagePack format
if dictionary.count > 15 {
throw SentryMsgPackSerializerError.dictionaryTooLarge
}
let mapHeader = UInt8(0x80 | dictionary.count) // fixmap format for up to 15 elements

Did we get this right? 👍 / 👎 to inform future reviews.

let keyLength = UInt8(truncatingIfNeeded: keyData.count) // Truncates if > 255, matching Objective-C behavior
_ = outputStream.write([str8Header], maxLength: 1)
_ = outputStream.write([keyLength], maxLength: 1)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Key length is silently truncated to UInt8 using truncatingIfNeeded when the UTF-8 encoded key exceeds 255 bytes. This will corrupt the MessagePack output if a key is longer than 255 bytes. Consider validating the key length and throwing an error instead of silently truncating.

Note: A key with 255 UTF-8 characters might actually have more than 255 bytes if it contains multi-byte Unicode characters.

Suggested change
let str8Header: UInt8 = 0xD9 // String up to 255 bytes
if keyData.count > 255 {
throw SentryMsgPackSerializerError.invalidInput("Key exceeds 255 bytes: \(key)")
}
let keyLength = UInt8(keyData.count)

Did we get this right? 👍 / 👎 to inform future reviews.

let mapHeader = UInt8(0x80 | dictionary.count) // Map up to 15 elements
_ = outputStream.write([mapHeader], maxLength: 1)

for (key, anyValue) in dictionary {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stream write operations discard the return value which indicates the number of bytes actually written. If the write fails or writes fewer bytes than expected, this will silently corrupt the MessagePack data. Consider checking write return values and throwing errors on failure.

Suggested change
for (key, anyValue) in dictionary {
let bytesWritten = outputStream.write([mapHeader], maxLength: 1)
if bytesWritten != 1 {
throw SentryMsgPackSerializerError.outputError("Failed to write map header")
}

Did we get this right? 👍 / 👎 to inform future reviews.

Comment on lines +41 to +47
throw SentryMsgPackSerializerError.invalidInput("Could not get buffer address for key: \(key)")
}
_ = outputStream.write(bufferAddress, maxLength: keyData.count)
}

guard let dataLength = value.streamSize(), dataLength > 0 else {
// MsgPack is being used strictly for session replay.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The keyData.withUnsafeBytes closure writes to the output stream, but if the stream is in an error state or closed, this could lead to undefined behavior. Consider checking the stream status before writing.

Did we get this right? 👍 / 👎 to inform future reviews.

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Value length is truncated to UInt32 using truncatingIfNeeded when streamSize exceeds UInt32.max (4GB). This will cause a mismatch between the declared length in the MessagePack header and the actual data length, leading to corrupted data. Consider validating the size and throwing an error for excessively large values.

Suggested change
let bin32Header: UInt8 = 0xC6
if dataLength > UInt32.max {
throw SentryMsgPackSerializerError.invalidInput("Data size exceeds UInt32.max: \(dataLength)")
}
let valueLength = UInt32(dataLength)

Did we get this right? 👍 / 👎 to inform future reviews.

Comment on lines +83 to +84
if bytesRead > 0 {
_ = outputStream.write(buffer, maxLength: bytesRead)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stream write operations inside the while loop don't check the return value, which could lead to partial writes and corrupted MessagePack data. The loop should verify that all bytes are written successfully.

Suggested change
if bytesRead > 0 {
_ = outputStream.write(buffer, maxLength: bytesRead)
let bytesWritten = outputStream.write(buffer, maxLength: bytesRead)
if bytesWritten != bytesRead {
throw SentryMsgPackSerializerError.outputError("Failed to write all bytes: wrote \(bytesWritten) of \(bytesRead)")
}

Did we get this right? 👍 / 👎 to inform future reviews.

Comment on lines +287 to +292
// Maintains Objective-C behavior: allows long keys but length will be truncated to uint8_t
XCTAssertTrue(result)
let tempFile = try Data(contentsOf: tempFileURL)
XCTAssertGreaterThan(tempFile.count, 1)
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test testSerializeLongKey verifies that keys longer than 255 characters succeed with truncation, but this actually produces invalid MessagePack. The test should verify that such keys either fail with an error or are properly encoded using str16 or str32 formats.

Suggested change
// Maintains Objective-C behavior: allows long keys but length will be truncated to uint8_t
XCTAssertTrue(result)
let tempFile = try Data(contentsOf: tempFileURL)
XCTAssertGreaterThan(tempFile.count, 1)
}
// Act
let result = SentryMsgPackSerializer.serializeDictionary(toMessagePack: dictionary, intoFile: tempFileURL)
// Assert
// Should fail for keys longer than 255 bytes with current str8 implementation
XCTAssertFalse(result)

Did we get this right? 👍 / 👎 to inform future reviews.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix nullability in SentryMsgPackSerializer.m

2 participants