Skip to content

Commit 7e5cbb5

Browse files
Made indexed assignment throw on read-only indexable collections
Indexed assignment (obj[i] = x) silently no-op'd for any non-NSMutableArray target, letting V8 store an unreachable JS property on the wrapper. Now that non-NSArray collections are readable by index, throw a TypeError for indexable read-only targets (responds to count + objectAtIndex:, not NSMutableArray) so a dropped write surfaces instead of leaking an orphaned expando. NSArray keeps its silent no-op for backward compatibility.
1 parent 530c40c commit 7e5cbb5

2 files changed

Lines changed: 35 additions & 0 deletions

File tree

NativeScript/runtime/ArgConverter.mm

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,6 +961,16 @@
961961
ObjCDataWrapper* objcDataWrapper = static_cast<ObjCDataWrapper*>(wrapper);
962962
id target = objcDataWrapper->Data();
963963
if (![target isKindOfClass:[NSMutableArray class]]) {
964+
// Indexed assignment is only supported for NSMutableArray. Other indexable
965+
// collections (e.g. NSOrderedSet, PHFetchResult) reject the write so it can't be
966+
// silently dropped into an unreachable JS property on the wrapper. NSArray is
967+
// excluded from the throw and falls through to a silent no-op.
968+
if ([target respondsToSelector:@selector(count)] &&
969+
[target respondsToSelector:@selector(objectAtIndex:)] &&
970+
![target isKindOfClass:[NSArray class]]) {
971+
isolate->ThrowException(Exception::TypeError(
972+
tns::ToV8String(isolate, "Indexed assignment is only supported for NSMutableArray")));
973+
}
964974
return;
965975
}
966976

TestRunner/app/tests/ApiTests.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,31 @@ describe(module.id, function () {
3636
expect(set[3]).toBeUndefined();
3737
});
3838

39+
it("indexed assignment throws for read-only indexable collections", function () {
40+
const set = NSOrderedSet.orderedSetWithArray(['a', 'b', 'c']);
41+
// NSOrderedSet is indexable but not writable, so indexed assignment throws.
42+
// The index is irrelevant; the whole collection rejects writes.
43+
expect(function () { set[0] = 'ghost'; }).toThrowError(TypeError, /Indexed assignment is only supported for NSMutableArray/);
44+
expect(function () { set[5] = 'ghost'; }).toThrowError(TypeError);
45+
// The rejected out-of-range write left no stray JS property behind, the native
46+
// collection is untouched, and reads still resolve native elements.
47+
expect(Object.prototype.hasOwnProperty.call(set, '5')).toBe(false);
48+
expect(set.count).toBe(3);
49+
expect(set[0]).toBe('a');
50+
expect(set[5]).toBeUndefined();
51+
});
52+
53+
it("indexed assignment still works for NSMutableArray", function () {
54+
const first = NSObject.new();
55+
const arr = NSMutableArray.arrayWithArray([first, NSObject.new()]);
56+
const replacement = NSObject.new();
57+
58+
arr[0] = replacement;
59+
expect(arr.count).toBe(2);
60+
expect(arr.objectAtIndex(0)).toBe(replacement);
61+
expect(arr[0]).toBe(replacement);
62+
});
63+
3964
it("MethodCalledInDealloc", function () {
4065
expect(function () {
4166
(function () {

0 commit comments

Comments
 (0)