Skip to content

Commit 8cc59bf

Browse files
author
Adrian Niculescu
committed
Make V8 string to NSString conversions UTF-16 faithful
V8 strings are UTF-16, but several bridge points round-tripped them through UTF-8 before building an NSString (and one reverse direction did too). That path corrupts lone surrogates, which get replaced with U+FFFD, and where it used C-string APIs (stringWithUTF8String:, [NSString UTF8String] folded into a std::string) it truncated at an embedded NUL. Rework tns::ToUtf16String to read the V8 string's native two-byte buffer directly, which also drops the deprecated std::codecvt_utf8_utf16. Switch the DictionaryAdapter, Interop and ArgConverter string sites to ToUtf16String + stringWithCharacters:length:, and pass NSString straight to ToV8String instead of going through a UTF8String C string. Add TestRunner cases asserting lone surrogates survive the JS to NSString bridge: each reads the bridged string's first UTF-16 code unit straight out of its buffer and checks it is unchanged (high U+D834 and low U+DC00), whereas the old UTF-8 round trip would have turned either into U+FFFD.
1 parent 05224d3 commit 8cc59bf

5 files changed

Lines changed: 67 additions & 17 deletions

File tree

NativeScript/runtime/ArgConverter.mm

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,8 @@
294294
} else if (value->IsString()) {
295295
if (type == BinaryTypeEncodingType::IdEncoding ||
296296
type == BinaryTypeEncodingType::InterfaceDeclarationReference) {
297-
id data = tns::ToNSString(isolate, value);
297+
std::u16string strValue = tns::ToUtf16String(isolate, value);
298+
id data = [NSString stringWithCharacters:(const unichar*)strValue.data() length:strValue.size()];
298299
// this feels wrong but follows the other CFBridgingRetain calls
299300
// and also solves a leak
300301
auto ref = CFBridgingRetain(data);
@@ -929,8 +930,8 @@
929930
}
930931

931932
if ([obj isKindOfClass:[NSString class]]) {
932-
const char* str = [obj UTF8String];
933-
args.GetReturnValue().Set(tns::ToV8String(isolate, str));
933+
NSString* nativeStr = (NSString*)obj;
934+
args.GetReturnValue().Set(tns::ToV8String(isolate, nativeStr));
934935
return;
935936
}
936937

NativeScript/runtime/DictionaryAdapter.mm

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ - (id)nextObject {
4848
bool success = array->Get(context, self->index_).ToLocal(&key);
4949
tns::Assert(success, isolate);
5050
self->index_ += 2;
51-
NSString* result = tns::ToNSString(isolate, key);
51+
std::u16string keyStr = tns::ToUtf16String(isolate, key);
52+
NSString* result = [NSString stringWithCharacters:(const unichar*)keyStr.data() length:keyStr.length()];
5253
return result;
5354
}
5455

@@ -116,8 +117,8 @@ - (id)nextObject {
116117
bool success = properties->Get(context, (uint)self->index_).ToLocal(&value);
117118
tns::Assert(success, isolate);
118119
self->index_++;
119-
std::string result = tns::ToString(isolate, value);
120-
return [NSString stringWithUTF8String:result.c_str()];
120+
std::u16string result = tns::ToUtf16String(isolate, value);
121+
return [NSString stringWithCharacters:(const unichar*)result.data() length:result.size()];
121122
}
122123

123124
return nil;
@@ -139,8 +140,8 @@ - (NSArray*)allObjects {
139140
Local<Value> value;
140141
bool success = properties->Get(context, i).ToLocal(&value);
141142
tns::Assert(success, isolate);
142-
std::string result = tns::ToString(isolate, value);
143-
[array addObject:[NSString stringWithUTF8String:result.c_str()]];
143+
std::u16string result = tns::ToUtf16String(isolate, value);
144+
[array addObject:[NSString stringWithCharacters:(const unichar*)result.data() length:result.size()]];
144145
}
145146

146147
return array;
@@ -214,7 +215,7 @@ - (id)objectForKey:(id)aKey {
214215
bool success = obj->Get(context, key).ToLocal(&value);
215216
tns::Assert(success, isolate);
216217
} else if ([aKey isKindOfClass:[NSString class]]) {
217-
const char* key = [aKey UTF8String];
218+
NSString* key = (NSString*)aKey;
218219
Local<v8::String> keyV8Str = tns::ToV8String(isolate, key);
219220

220221
if (obj->IsMap()) {

NativeScript/runtime/Helpers.mm

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,26 @@
2424
} // namespace
2525

2626
std::u16string tns::ToUtf16String(Isolate* isolate, const Local<Value>& value) {
27-
std::string valueStr = tns::ToString(isolate, value);
28-
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
29-
// FIXME: std::codecvt_utf8_utf16 is deprecated
30-
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> convert;
31-
std::u16string value16 = convert.from_bytes(valueStr);
27+
// Read the V8 string's native UTF-16 buffer directly instead of round-tripping
28+
// through UTF-8, which corrupts lone surrogates (replaced with U+FFFD) and is
29+
// slower. This also drops the deprecated std::codecvt_utf8_utf16.
30+
if (value.IsEmpty()) {
31+
return std::u16string();
32+
}
33+
34+
if (value->IsStringObject()) {
35+
Local<v8::String> obj = value.As<StringObject>()->ValueOf();
36+
return tns::ToUtf16String(isolate, obj);
37+
}
38+
39+
v8::String::Value result(isolate, value);
40+
41+
uint16_t* val = *result;
42+
if (val == nullptr) {
43+
return std::u16string();
44+
}
3245

33-
return value16;
46+
return std::u16string((char16_t*)val, result.length());
3447
}
3548

3649
std::vector<uint16_t> tns::ToVector(const std::string& value) {

NativeScript/runtime/Interop.mm

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,8 @@ inline bool isBool() {
324324
} else if (argHelper.isString() &&
325325
(typeEncoding->type == BinaryTypeEncodingType::InterfaceDeclarationReference ||
326326
typeEncoding->type == BinaryTypeEncodingType::IdEncoding)) {
327-
NSString* result = tns::ToNSString(isolate, arg);
327+
std::u16string str = tns::ToUtf16String(isolate, arg);
328+
NSString* result = [NSString stringWithCharacters:(const unichar*)str.data() length:str.size()];
328329
Interop::SetValue(dest, result);
329330
} else if (Interop::IsNumbericType(typeEncoding->type) || tns::IsNumber(arg)) {
330331
double value = tns::ToNumber(isolate, arg);
@@ -686,7 +687,8 @@ inline bool isBool() {
686687
if (arg.IsEmpty() || arg->IsNullOrUndefined()) {
687688
return nil;
688689
} else if (tns::IsString(arg)) {
689-
NSString* result = tns::ToNSString(isolate, arg);
690+
std::u16string value = tns::ToUtf16String(isolate, arg);
691+
NSString* result = [NSString stringWithCharacters:(const unichar*)value.data() length:value.size()];
690692
return result;
691693
} else if (tns::IsNumber(arg)) {
692694
double value = tns::ToNumber(isolate, arg);

TestRunner/app/tests/ApiTests.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,39 @@ describe(module.id, function () {
1212
expect(object.hash).toBe(3);
1313
});
1414

15+
it("preserves a lone high surrogate when bridging a JS string to NSString", function () {
16+
// A lone high surrogate (U+D834, range U+D800-U+DBFF) is a valid JS string
17+
// code unit but has no UTF-8 encoding. The old UTF-8 round-trip replaced it
18+
// with U+FFFD; faithful UTF-16 bridging keeps it. Read the code unit straight
19+
// out of the bridged string's UTF-16 buffer as a number: reading it back as a
20+
// JS string would re-corrupt a lone surrogate, and converting it to UTF-8 to
21+
// measure it is not reliable across OS versions.
22+
var ns = NSString.stringWithString("\uD834");
23+
expect(ns.length).toBe(1);
24+
25+
var buffer = interop.alloc(interop.sizeof(interop.types.uint16));
26+
ns.getCharactersRange(buffer, NSMakeRange(0, 1));
27+
var codeUnit = new interop.Reference(interop.types.uint16, buffer).value;
28+
interop.free(buffer);
29+
30+
expect(codeUnit).toBe(0xD834); // 0xFFFD (65533) after a lossy UTF-8 round-trip
31+
});
32+
33+
it("preserves a lone low surrogate when bridging a JS string to NSString", function () {
34+
// The low surrogate range (U+DC00-U+DFFF) is a different bit pattern that also
35+
// has no UTF-8 encoding and must survive the bridge intact; observed the same
36+
// way as the high-surrogate case above.
37+
var ns = NSString.stringWithString("\uDC00");
38+
expect(ns.length).toBe(1);
39+
40+
var buffer = interop.alloc(interop.sizeof(interop.types.uint16));
41+
ns.getCharactersRange(buffer, NSMakeRange(0, 1));
42+
var codeUnit = new interop.Reference(interop.types.uint16, buffer).value;
43+
interop.free(buffer);
44+
45+
expect(codeUnit).toBe(0xDC00); // 0xFFFD (65533) after a lossy UTF-8 round-trip
46+
});
47+
1548
it("NSArray from native (uncached) array access", function () {
1649
const res = TNSObjCTypes.new().getNSArrayOfNSURLs();
1750
console.log(res);

0 commit comments

Comments
 (0)