/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */ // SPDX-License-Identifier: MIT OR LGPL-2.0-or-later // SPDX-FileCopyrightText: 2010 litl, LLC #include #include #include #include #include #include #include #include #include #include #include // for UniqueChars #include #include // for JS_NewPlainObject #include "gi/boxed.h" #include "gjs/atoms.h" #include "gjs/byteArray.h" #include "gjs/context-private.h" #include "gjs/deprecation.h" #include "gjs/jsapi-util-args.h" #include "gjs/jsapi-util.h" #include "gjs/macros.h" #include "gjs/text-encoding.h" #include "util/misc.h" // for _gjs_memdup2 // Callback to use with JS::NewExternalArrayBuffer() static void bytes_unref_arraybuffer(void* contents [[maybe_unused]], void* user_data) { auto* gbytes = static_cast(user_data); g_bytes_unref(gbytes); } GJS_JSAPI_RETURN_CONVENTION static bool to_string_func(JSContext* cx, unsigned argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); JS::UniqueChars encoding; JS::RootedObject byte_array(cx); if (!gjs_parse_call_args(cx, "toString", args, "o|s", "byteArray", &byte_array, "encoding", &encoding)) return false; const char* actual_encoding = encoding ? encoding.get() : "utf-8"; JS::RootedString str( cx, gjs_decode_from_uint8array(cx, byte_array, actual_encoding, GjsStringTermination::ZERO_TERMINATED, true)); if (!str) return false; args.rval().setString(str); return true; } /* Workaround to keep existing code compatible. This function is tacked onto * any Uint8Array instances created in situations where previously a ByteArray * would have been created. It logs a compatibility warning. */ GJS_JSAPI_RETURN_CONVENTION static bool instance_to_string_func(JSContext* cx, unsigned argc, JS::Value* vp) { GJS_GET_THIS(cx, argc, vp, args, this_obj); JS::UniqueChars encoding; _gjs_warn_deprecated_once_per_callsite( cx, GjsDeprecationMessageId::ByteArrayInstanceToString); if (!gjs_parse_call_args(cx, "toString", args, "|s", "encoding", &encoding)) return false; const char* actual_encoding = encoding ? encoding.get() : "utf-8"; JS::RootedString str( cx, gjs_decode_from_uint8array(cx, this_obj, actual_encoding, GjsStringTermination::ZERO_TERMINATED, true)); if (!str) return false; args.rval().setString(str); return true; } GJS_JSAPI_RETURN_CONVENTION static bool define_legacy_tostring(JSContext* cx, JS::HandleObject array) { const GjsAtoms& atoms = GjsContextPrivate::atoms(cx); return JS_DefineFunctionById(cx, array, atoms.to_string(), instance_to_string_func, 1, 0); } /* fromString() function implementation */ GJS_JSAPI_RETURN_CONVENTION static bool from_string_func(JSContext* cx, unsigned argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); JS::RootedString str(cx); JS::UniqueChars encoding; if (!gjs_parse_call_args(cx, "fromString", args, "S|s", "string", &str, "encoding", &encoding)) return false; const char* actual_encoding = encoding ? encoding.get() : "utf-8"; JS::RootedObject uint8array( cx, gjs_encode_to_uint8array(cx, str, actual_encoding, GjsStringTermination::ZERO_TERMINATED)); if (!uint8array || !define_legacy_tostring(cx, uint8array)) return false; args.rval().setObject(*uint8array); return true; } GJS_JSAPI_RETURN_CONVENTION static bool from_gbytes_func(JSContext *context, unsigned argc, JS::Value *vp) { JS::CallArgs argv = JS::CallArgsFromVp (argc, vp); JS::RootedObject bytes_obj(context); GBytes *gbytes; if (!gjs_parse_call_args(context, "fromGBytes", argv, "o", "bytes", &bytes_obj)) return false; if (!BoxedBase::typecheck(context, bytes_obj, nullptr, G_TYPE_BYTES)) return false; gbytes = BoxedBase::to_c_ptr(context, bytes_obj); if (!gbytes) return false; size_t len; const void* data = g_bytes_get_data(gbytes, &len); if (len == 0) { JS::RootedObject empty_array(context, JS_NewUint8Array(context, 0)); if (!empty_array || !define_legacy_tostring(context, empty_array)) return false; argv.rval().setObject(*empty_array); return true; } JS::RootedObject array_buffer( context, JS::NewExternalArrayBuffer( context, len, const_cast(data), // the ArrayBuffer won't modify the data bytes_unref_arraybuffer, gbytes)); if (!array_buffer) return false; g_bytes_ref(gbytes); // now owned by both ArrayBuffer and BoxedBase JS::RootedObject obj( context, JS_NewUint8ArrayWithBuffer(context, array_buffer, 0, -1)); if (!obj || !define_legacy_tostring(context, obj)) return false; argv.rval().setObject(*obj); return true; } JSObject* gjs_byte_array_from_data(JSContext* cx, size_t nbytes, void* data) { JS::RootedObject array_buffer(cx); // a null data pointer takes precedence over whatever `nbytes` says if (data) array_buffer = JS::NewArrayBufferWithContents( cx, nbytes, _gjs_memdup2(data, nbytes)); else array_buffer = JS::NewArrayBuffer(cx, 0); if (!array_buffer) return nullptr; JS::RootedObject array(cx, JS_NewUint8ArrayWithBuffer(cx, array_buffer, 0, -1)); const GjsAtoms& atoms = GjsContextPrivate::atoms(cx); if (!JS_DefineFunctionById(cx, array, atoms.to_string(), instance_to_string_func, 1, 0)) return nullptr; return array; } JSObject* gjs_byte_array_from_byte_array(JSContext* cx, GByteArray* array) { return gjs_byte_array_from_data(cx, array->len, array->data); } GBytes* gjs_byte_array_get_bytes(JSObject* obj) { bool is_shared_memory; size_t len; uint8_t* data; js::GetUint8ArrayLengthAndData(obj, &len, &is_shared_memory, &data); return g_bytes_new(data, len); } GByteArray* gjs_byte_array_get_byte_array(JSObject* obj) { return g_bytes_unref_to_array(gjs_byte_array_get_bytes(obj)); } static JSFunctionSpec gjs_byte_array_module_funcs[] = { JS_FN("fromString", from_string_func, 2, 0), JS_FN("fromGBytes", from_gbytes_func, 1, 0), JS_FN("toString", to_string_func, 2, 0), JS_FS_END}; bool gjs_define_byte_array_stuff(JSContext *cx, JS::MutableHandleObject module) { module.set(JS_NewPlainObject(cx)); return JS_DefineFunctions(cx, module, gjs_byte_array_module_funcs); }