|
7 | 7 | #import "SentryNSDictionarySanitize.h" |
8 | 8 | #import "SentrySwift.h" |
9 | 9 |
|
| 10 | +/** |
| 11 | + * Recursively copies a value, producing immutable collections at every level. |
| 12 | + * For @c NSDictionary / @c NSArray (including mutable subclasses) a new immutable copy is |
| 13 | + * returned with all nested containers copied as well. Other values are returned as-is. |
| 14 | + * This prevents the SDK from holding references to caller-owned mutable objects whose |
| 15 | + * concurrent mutation would crash serialization (see |
| 16 | + * https://github.com/getsentry/sentry-cocoa/issues/2601). |
| 17 | + */ |
| 18 | +static id |
| 19 | +sentry_deepCopyValue(id value) |
| 20 | +{ |
| 21 | + if ([value isKindOfClass:[NSDictionary class]]) { |
| 22 | + // Defensive copy to prevent mutation during enumeration. |
| 23 | + NSDictionary *dictionaryCopy = [(NSDictionary *)value copy]; |
| 24 | + NSMutableDictionary *result = |
| 25 | + [NSMutableDictionary dictionaryWithCapacity:dictionaryCopy.count]; |
| 26 | + for (id key in dictionaryCopy) { |
| 27 | + id v = dictionaryCopy[key]; |
| 28 | + if (v != nil) { |
| 29 | + result[key] = sentry_deepCopyValue(v); |
| 30 | + } |
| 31 | + } |
| 32 | + return [result copy]; // immutable |
| 33 | + } else if ([value isKindOfClass:[NSArray class]]) { |
| 34 | + // Defensive copy to prevent mutation during enumeration. |
| 35 | + NSArray *arrayCopy = [(NSArray *)value copy]; |
| 36 | + NSMutableArray *result = [NSMutableArray arrayWithCapacity:arrayCopy.count]; |
| 37 | + for (id item in arrayCopy) { |
| 38 | + [result addObject:sentry_deepCopyValue(item)]; |
| 39 | + } |
| 40 | + return [result copy]; // immutable |
| 41 | + } |
| 42 | + return value; |
| 43 | +} |
| 44 | + |
10 | 45 | @implementation SentryBreadcrumb |
11 | 46 |
|
| 47 | +// Explicit @synthesize so we can provide thread-safe accessors via @synchronized(self). |
| 48 | +@synthesize level = _level; |
| 49 | +@synthesize category = _category; |
| 50 | +@synthesize timestamp = _timestamp; |
| 51 | +@synthesize type = _type; |
| 52 | +@synthesize message = _message; |
| 53 | +@synthesize origin = _origin; |
| 54 | +@synthesize data = _data; |
| 55 | + |
| 56 | +#pragma mark - Thread-safe property accessors |
| 57 | + |
| 58 | +- (void)setLevel:(SentryLevel)level |
| 59 | +{ |
| 60 | + @synchronized(self) { |
| 61 | + _level = level; |
| 62 | + } |
| 63 | +} |
| 64 | + |
| 65 | +- (SentryLevel)level |
| 66 | +{ |
| 67 | + @synchronized(self) { |
| 68 | + return _level; |
| 69 | + } |
| 70 | +} |
| 71 | + |
| 72 | +- (void)setCategory:(NSString *)category |
| 73 | +{ |
| 74 | + @synchronized(self) { |
| 75 | + _category = [category copy]; |
| 76 | + } |
| 77 | +} |
| 78 | + |
| 79 | +- (NSString *)category |
| 80 | +{ |
| 81 | + @synchronized(self) { |
| 82 | + return _category; |
| 83 | + } |
| 84 | +} |
| 85 | + |
| 86 | +- (void)setTimestamp:(NSDate *)timestamp |
| 87 | +{ |
| 88 | + @synchronized(self) { |
| 89 | + _timestamp = timestamp; |
| 90 | + } |
| 91 | +} |
| 92 | + |
| 93 | +- (NSDate *)timestamp |
| 94 | +{ |
| 95 | + @synchronized(self) { |
| 96 | + return _timestamp; |
| 97 | + } |
| 98 | +} |
| 99 | + |
| 100 | +- (void)setType:(NSString *)type |
| 101 | +{ |
| 102 | + @synchronized(self) { |
| 103 | + _type = [type copy]; |
| 104 | + } |
| 105 | +} |
| 106 | + |
| 107 | +- (NSString *)type |
| 108 | +{ |
| 109 | + @synchronized(self) { |
| 110 | + return _type; |
| 111 | + } |
| 112 | +} |
| 113 | + |
| 114 | +- (void)setMessage:(NSString *)message |
| 115 | +{ |
| 116 | + @synchronized(self) { |
| 117 | + _message = [message copy]; |
| 118 | + } |
| 119 | +} |
| 120 | + |
| 121 | +- (NSString *)message |
| 122 | +{ |
| 123 | + @synchronized(self) { |
| 124 | + return _message; |
| 125 | + } |
| 126 | +} |
| 127 | + |
| 128 | +- (void)setOrigin:(NSString *)origin |
| 129 | +{ |
| 130 | + @synchronized(self) { |
| 131 | + _origin = [origin copy]; |
| 132 | + } |
| 133 | +} |
| 134 | + |
| 135 | +- (NSString *)origin |
| 136 | +{ |
| 137 | + @synchronized(self) { |
| 138 | + return _origin; |
| 139 | + } |
| 140 | +} |
| 141 | + |
| 142 | +- (void)setData:(NSDictionary<NSString *, id> *)data |
| 143 | +{ |
| 144 | + @synchronized(self) { |
| 145 | + _data = data ? sentry_deepCopyValue(data) : nil; |
| 146 | + } |
| 147 | +} |
| 148 | + |
| 149 | +- (NSDictionary<NSString *, id> *)data |
| 150 | +{ |
| 151 | + @synchronized(self) { |
| 152 | + return _data; |
| 153 | + } |
| 154 | +} |
| 155 | + |
| 156 | +#pragma mark - Initializers |
| 157 | + |
12 | 158 | - (instancetype)initWithDictionary:(NSDictionary *)dictionary |
13 | 159 | { |
14 | 160 | if (self = [super init]) { |
@@ -56,24 +202,46 @@ - (instancetype)init |
56 | 202 | return [self initWithLevel:kSentryLevelInfo category:@"default"]; |
57 | 203 | } |
58 | 204 |
|
| 205 | +#pragma mark - Serialization |
| 206 | + |
59 | 207 | - (NSDictionary<NSString *, id> *)serialize |
60 | 208 | { |
| 209 | + // Capture all properties under the lock to get a consistent snapshot. |
| 210 | + SentryLevel level; |
| 211 | + NSDate *timestamp; |
| 212 | + NSString *category; |
| 213 | + NSString *type; |
| 214 | + NSString *origin; |
| 215 | + NSString *message; |
| 216 | + NSDictionary *data; |
| 217 | + |
| 218 | + @synchronized(self) { |
| 219 | + level = _level; |
| 220 | + timestamp = _timestamp; |
| 221 | + category = _category; |
| 222 | + type = _type; |
| 223 | + origin = _origin; |
| 224 | + message = _message; |
| 225 | + data = _data; |
| 226 | + } |
| 227 | + |
61 | 228 | NSMutableDictionary *serializedData = [[NSMutableDictionary alloc] init]; |
62 | 229 |
|
63 | | - [serializedData setValue:nameForSentryLevel(self.level) forKey:@"level"]; |
64 | | - if (self.timestamp != nil) { |
65 | | - [serializedData |
66 | | - setValue:sentry_toIso8601String(SENTRY_UNWRAP_NULLABLE(NSDate, self.timestamp)) |
67 | | - forKey:@"timestamp"]; |
68 | | - } |
69 | | - [serializedData setValue:self.category forKey:@"category"]; |
70 | | - [serializedData setValue:self.type forKey:@"type"]; |
71 | | - [serializedData setValue:self.origin forKey:@"origin"]; |
72 | | - [serializedData setValue:self.message forKey:@"message"]; |
73 | | - [serializedData setValue:sentry_sanitize(self.data) forKey:@"data"]; |
| 230 | + [serializedData setValue:nameForSentryLevel(level) forKey:@"level"]; |
| 231 | + if (timestamp != nil) { |
| 232 | + [serializedData setValue:sentry_toIso8601String(SENTRY_UNWRAP_NULLABLE(NSDate, timestamp)) |
| 233 | + forKey:@"timestamp"]; |
| 234 | + } |
| 235 | + [serializedData setValue:category forKey:@"category"]; |
| 236 | + [serializedData setValue:type forKey:@"type"]; |
| 237 | + [serializedData setValue:origin forKey:@"origin"]; |
| 238 | + [serializedData setValue:message forKey:@"message"]; |
| 239 | + [serializedData setValue:sentry_sanitize(data) forKey:@"data"]; |
74 | 240 | return serializedData; |
75 | 241 | } |
76 | 242 |
|
| 243 | +#pragma mark - Equality |
| 244 | + |
77 | 245 | - (BOOL)isEqual:(id _Nullable)other |
78 | 246 | { |
79 | 247 | if (other == self) |
|
0 commit comments