@@ -164,4 +164,122 @@ + (BOOL)getConfigBooleanValue:(NSString *)tag key:(NSString *)key defaultValue:(
164164 return enabled;
165165}
166166
167+ /* *
168+ * Decodes null sentinel objects back to NSNull values.
169+ * Uses iterative stack-based traversal to avoid stack overflow on deeply nested structures.
170+ *
171+ * This reverses the encoding done on the JavaScript side where null values in object
172+ * properties are replaced with {__rnfbNull: true} sentinel objects to survive iOS
173+ * TurboModule serialization.
174+ *
175+ * Process:
176+ * 1. Detects sentinel objects: dictionaries with single key "__rnfbNull" set to true
177+ * 2. Replaces sentinels with NSNull in object properties and arrays
178+ * 3. Preserves regular NSNull values that were in arrays (never encoded as sentinels)
179+ * 4. Deep processes all nested objects and arrays using a stack-based iteration
180+ *
181+ * @param value - The value to decode (dictionary, array, or primitive)
182+ * @return The decoded value with sentinels replaced by NSNull
183+ */
184+ + (id )decodeNullSentinels : (id )value {
185+ // Non-container values are returned as-is
186+ if (![value isKindOfClass: [NSDictionary class ]] && ![value isKindOfClass: [NSArray class ]]) {
187+ return value;
188+ }
189+
190+ // Helper to detect the sentinel
191+ BOOL (^isNullSentinel)(NSDictionary *) = ^BOOL (NSDictionary *dict) {
192+ id flag = dict[@" __rnfbNull" ];
193+ return (dict.count == 1 && flag != nil && [flag boolValue ]);
194+ };
195+
196+ // Helper to process a child element and add it to the parent container
197+ void (^processChild)(id , id , id , BOOL , NSMutableArray *) =
198+ ^void (id child, id parentMutable, id keyOrNil, BOOL isParentDict, NSMutableArray *stack) {
199+ id processedValue = nil ;
200+
201+ if ([child isKindOfClass: [NSDictionary class ]]) {
202+ NSDictionary *childDict = (NSDictionary *)child;
203+
204+ if (isNullSentinel (childDict)) {
205+ // Replace sentinel with NSNull
206+ processedValue = [NSNull null ];
207+ } else {
208+ // Process nested dictionary
209+ NSMutableDictionary *childMut =
210+ [NSMutableDictionary dictionaryWithCapacity: childDict.count];
211+ processedValue = childMut;
212+ [stack addObject: @{@" original" : childDict, @" mutable" : childMut}];
213+ }
214+ } else if ([child isKindOfClass: [NSArray class ]]) {
215+ // Process nested array
216+ NSArray *childArray = (NSArray *)child;
217+ NSMutableArray *childMut = [NSMutableArray arrayWithCapacity: childArray.count];
218+ processedValue = childMut;
219+ [stack addObject: @{@" original" : childArray, @" mutable" : childMut}];
220+ } else {
221+ // Preserve primitive values
222+ processedValue = child ?: [NSNull null ];
223+ }
224+
225+ // Add to parent container based on type
226+ if (isParentDict) {
227+ NSMutableDictionary *mutDict = (NSMutableDictionary *)parentMutable;
228+ if (processedValue) {
229+ mutDict[keyOrNil] = processedValue;
230+ }
231+ // NSDictionary can't store nil, and original code wouldn't see nil values either.
232+ } else {
233+ NSMutableArray *mutArray = (NSMutableArray *)parentMutable;
234+ [mutArray addObject: processedValue];
235+ }
236+ };
237+
238+ // Root-level sentinel case
239+ if ([value isKindOfClass: [NSDictionary class ]] && isNullSentinel ((NSDictionary *)value)) {
240+ return [NSNull null ];
241+ }
242+
243+ id rootOriginal = value;
244+ id rootMutable = nil ;
245+
246+ if ([value isKindOfClass: [NSDictionary class ]]) {
247+ NSDictionary *dict = (NSDictionary *)value;
248+ rootMutable = [NSMutableDictionary dictionaryWithCapacity: dict.count];
249+ } else {
250+ NSArray *array = (NSArray *)value;
251+ rootMutable = [NSMutableArray arrayWithCapacity: array.count];
252+ }
253+
254+ // Stack-based iteration to process nested structures without recursion
255+ // Stack frames: { @"original": container, @"mutable": mutableContainer }
256+ NSMutableArray <NSDictionary *> *stack = [NSMutableArray array ];
257+ [stack addObject: @{@" original" : rootOriginal, @" mutable" : rootMutable}];
258+
259+ while (stack.count > 0 ) {
260+ NSDictionary *frame = [stack lastObject ];
261+ [stack removeLastObject ];
262+
263+ id original = frame[@" original" ];
264+ id mutable = frame[@" mutable" ];
265+
266+ if ([original isKindOfClass: [NSDictionary class ]]) {
267+ NSDictionary *origDict = (NSDictionary *)original;
268+
269+ for (id key in origDict) {
270+ id child = origDict[key];
271+ processChild (child, mutable, key, YES , stack);
272+ }
273+ } else if ([original isKindOfClass: [NSArray class ]]) {
274+ NSArray *origArray = (NSArray *)original;
275+
276+ for (id child in origArray) {
277+ processChild (child, mutable, nil , NO , stack);
278+ }
279+ }
280+ }
281+
282+ return rootMutable;
283+ }
284+
167285@end
0 commit comments