1use std::{collections::HashMap, os::raw::c_char};
2use std::os::raw::{c_int, c_void};
3use std::ptr::null_mut;
4
5use libquickjs_sys as q;
6
7use crate::{JsValue, RawJSValue, ResourceValue, ValueError};
8
9use super::{droppable_value::DroppableValue, JsClass, make_cstring, Resource, ResourceObject};
10
11use super::{
12 TAG_BOOL, TAG_EXCEPTION, TAG_FLOAT64, TAG_INT, TAG_NULL, TAG_OBJECT, TAG_STRING, TAG_UNDEFINED,
13};
14
15#[cfg(feature = "bigint")]
16use {
17 super::TAG_BIG_INT,
18 crate::value::bigint::{BigInt, BigIntOrI64},
19};
20use libquickjs_sys::{JS_GetClassID, JS_GetOpaque, JS_GetOpaque2, JS_NewClass, JS_NewClassID, JS_NewObjectClass, JS_SetOpaque, JSClassDef, JSRuntime, JSValue, JS_VALUE_GET_TAG};
21
22#[cfg(feature = "chrono")]
23fn js_date_constructor(context: *mut q::JSContext) -> q::JSValue {
24 let global = unsafe { q::JS_GetGlobalObject(context) };
25 assert!(q::JS_IsObject(global));
26
27 let date_constructor = unsafe {
28 q::JS_GetPropertyStr(
29 context,
30 global,
31 std::ffi::CStr::from_bytes_with_nul(b"Date\0")
32 .unwrap()
33 .as_ptr(),
34 )
35 };
36 assert!(q::JS_IsObject(date_constructor));
37 unsafe { q::JS_FreeValue(context, global) };
38 date_constructor
39}
40
41#[cfg(feature = "bigint")]
42fn js_create_bigint_function(context: *mut q::JSContext) -> q::JSValue {
43 let global = unsafe { q::JS_GetGlobalObject(context) };
44 assert_eq!(global.tag, TAG_OBJECT);
45
46 let bigint_function = unsafe {
47 q::JS_GetPropertyStr(
48 context,
49 global,
50 std::ffi::CStr::from_bytes_with_nul(b"BigInt\0")
51 .unwrap()
52 .as_ptr(),
53 )
54 };
55 assert_eq!(bigint_function.tag, TAG_OBJECT);
56 unsafe { q::JS_FreeValue(context, global) };
57 bigint_function
58}
59
60pub fn serialize_value(
63 context: *mut q::JSContext,
64 value: JsValue,
65) -> Result<q::JSValue, ValueError> {
66 let v = match value {
67 JsValue::Undefined => q::JS_UNDEFINED,
68 JsValue::Null => q::JS_NULL,
69 JsValue::Bool(flag) => q::JS_MKVAL(q::JS_TAG_BOOL, if flag { 1 } else { 0 }),
70 JsValue::Int(val) => q::JS_MKVAL(q::JS_TAG_INT, val),
71 JsValue::Float(val) => q::__JS_NewFloat64(val),
72 JsValue::String(val) => {
73 let qval = unsafe {
74 q::JS_NewStringLen(context, val.as_ptr() as *const c_char, val.len() as _)
75 };
76
77 if q::JS_IsException(qval) {
78 return Err(ValueError::Internal(
79 "Could not create string in runtime".into(),
80 ));
81 }
82
83 qval
84 }
85 JsValue::Array(values) => {
86 let arr = unsafe { q::JS_NewArray(context) };
88 if q::JS_IsException(arr) {
89 return Err(ValueError::Internal(
90 "Could not create array in runtime".into(),
91 ));
92 }
93
94 for (index, value) in values.into_iter().enumerate() {
95 let qvalue = match serialize_value(context, value) {
96 Ok(qval) => qval,
97 Err(e) => {
98 unsafe {
102 q::JS_FreeValue(context, arr);
103 }
104
105 return Err(e);
106 }
107 };
108
109 let ret = unsafe {
110 q::JS_DefinePropertyValueUint32(
111 context,
112 arr,
113 index as u32,
114 qvalue,
115 q::JS_PROP_C_W_E as i32,
116 )
117 };
118 if ret < 0 {
119 unsafe {
122 q::JS_FreeValue(context, arr);
123 }
124 return Err(ValueError::Internal(
125 "Could not append element to array".into(),
126 ));
127 }
128 }
129 arr
130 }
131 JsValue::Object(map) => {
132 let obj = unsafe { q::JS_NewObject(context) };
133 if q::JS_IsException(obj) {
134 return Err(ValueError::Internal("Could not create object".into()));
135 }
136
137 for (key, value) in map {
138 let ckey = make_cstring(key)?;
139
140 let qvalue = serialize_value(context, value).map_err(|e| {
141 unsafe {
143 q::JS_FreeValue(context, obj);
144 }
145 e
146 })?;
147
148 let ret = unsafe {
149 q::JS_DefinePropertyValueStr(
150 context,
151 obj,
152 ckey.as_ptr(),
153 qvalue,
154 q::JS_PROP_C_W_E as i32,
155 )
156 };
157 if ret < 0 {
158 unsafe {
160 q::JS_FreeValue(context, obj);
161 }
162 return Err(ValueError::Internal(
163 "Could not add add property to object".into(),
164 ));
165 }
166 }
167
168 obj
169 }
170 JsValue::Raw(raw) => {
171 unsafe {
172 raw.create_js_value()
173 }
174 }
175 JsValue::Exception(raw) => {
176 unsafe {
177 raw.create_js_value()
178 }
179 }
180 JsValue::Resource(raw) => {
181 create_resource(context, raw)
182 }
183 #[cfg(feature = "chrono")]
184 JsValue::Date(datetime) => {
185 let date_constructor = js_date_constructor(context);
186
187 let f = datetime.timestamp_millis() as f64;
188
189 let timestamp = q::JS_NewFloat64(f);
190
191 let mut args = vec![timestamp];
192
193 let value = unsafe {
194 q::JS_CallConstructor(
195 context,
196 date_constructor,
197 args.len() as i32,
198 args.as_mut_ptr(),
199 )
200 };
201 unsafe {
202 q::JS_FreeValue(context, date_constructor);
203 }
204
205 if !q::JS_IsObject(value) {
206 return Err(ValueError::Internal(
207 "Could not construct Date object".into(),
208 ));
209 }
210 value
211 }
212 #[cfg(feature = "bigint")]
213 JsValue::BigInt(int) => match int.inner {
214 BigIntOrI64::Int(int) => unsafe { q::JS_NewBigInt64(context, int) },
215 BigIntOrI64::BigInt(bigint) => {
216 let bigint_string = bigint.to_str_radix(10);
217 let s = unsafe {
218 q::JS_NewStringLen(
219 context,
220 bigint_string.as_ptr() as *const c_char,
221 bigint_string.len() as q::size_t,
222 )
223 };
224 let s = DroppableValue::new(s, |&mut s| unsafe {
225 q::JS_FreeValue(context, s);
226 });
227 if (*s).tag != TAG_STRING {
228 return Err(ValueError::Internal(
229 "Could not construct String object needed to create BigInt object".into(),
230 ));
231 }
232
233 let mut args = vec![*s];
234
235 let bigint_function = js_create_bigint_function(context);
236 let bigint_function =
237 DroppableValue::new(bigint_function, |&mut bigint_function| unsafe {
238 q::JS_FreeValue(context, bigint_function);
239 });
240 let js_bigint = unsafe {
241 q::JS_Call(
242 context,
243 *bigint_function,
244 q::JSValue {
245 u: q::JSValueUnion { int32: 0 },
246 tag: TAG_NULL,
247 },
248 1,
249 args.as_mut_ptr(),
250 )
251 };
252
253 if js_bigint.tag != TAG_BIG_INT {
254 return Err(ValueError::Internal(
255 "Could not construct BigInt object".into(),
256 ));
257 }
258
259 js_bigint
260 }
261 },
262 JsValue::__NonExhaustive => unreachable!(),
263 };
264 Ok(v)
265}
266
267pub fn create_resource(context: *mut q::JSContext, resource: ResourceValue) -> JSValue {
268 unsafe {
269 let class_id = Resource::class_id();
270 if class_id.id.get() == 0 {
271 let runtime = q::JS_GetRuntime(context);
272 let mut cls_id = 0;
273 JS_NewClassID(runtime, &mut cls_id);
274 class_id.id.set(cls_id);
275 extern fn finalizer(rt: *mut JSRuntime, val: JSValue) {
276 unsafe {
278 let cls_id = JS_GetClassID(val);
279 let opaque = JS_GetOpaque(val, cls_id) as *mut ResourceObject;
280 let _ = Box::from_raw(opaque);
281 }
282 }
284 let cls_def = JSClassDef {
285 class_name: Resource::NAME.as_ptr() as *const std::ffi::c_char,
286 finalizer: Some(finalizer),
287 gc_mark: None,
288 call: None,
289 exotic: null_mut(),
290 };
291 JS_NewClass(runtime, cls_id, &cls_def);
292 }
293
294 let class_id = class_id.id.get();
295 let res = JS_NewObjectClass(context, class_id as c_int);
296 let opaque = Box::into_raw(Box::new(ResourceObject {
297 data: resource,
298 }));
299 JS_SetOpaque(res, opaque as *mut c_void);
300 res
301 }
302
303}
304
305fn deserialize_array(
306 context: *mut q::JSContext,
307 raw_value: &q::JSValue,
308) -> Result<JsValue, ValueError> {
309 assert!(q::JS_IsObject(*raw_value));
310
311 let length_name = make_cstring("length")?;
312
313 let len_raw = unsafe { q::JS_GetPropertyStr(context, *raw_value, length_name.as_ptr()) };
314
315 let len_res = deserialize_value(context, &len_raw);
316 unsafe { q::JS_FreeValue(context, len_raw) };
317 let len = match len_res? {
318 JsValue::Int(x) => x,
319 _ => {
320 return Err(ValueError::Internal(
321 "Could not determine array length".into(),
322 ));
323 }
324 };
325
326 let mut values = Vec::new();
327 for index in 0..(len as usize) {
328 let value_raw = unsafe { q::JS_GetPropertyUint32(context, *raw_value, index as u32) };
329 if q::JS_IsException(value_raw) {
330 return Err(ValueError::Internal("Could not build array".into()));
331 }
332 let value_res = deserialize_value(context, &value_raw);
333 unsafe { q::JS_FreeValue(context, value_raw) };
334
335 let value = value_res?;
336 values.push(value);
337 }
338
339 Ok(JsValue::Array(values))
340}
341
342pub fn deserialize_object(context: *mut q::JSContext, obj: &q::JSValue) -> Result<HashMap<String, JsValue>, ValueError> {
343 assert_eq!(JS_VALUE_GET_TAG(*obj), q::JS_TAG_OBJECT);
344
345 let mut properties: *mut q::JSPropertyEnum = std::ptr::null_mut();
346 let mut count: u32 = 0;
347
348 let flags = (q::JS_GPN_STRING_MASK | q::JS_GPN_SYMBOL_MASK | q::JS_GPN_ENUM_ONLY) as i32;
349 let ret =
350 unsafe { q::JS_GetOwnPropertyNames(context, &mut properties, &mut count, *obj, flags) };
351 if ret != 0 {
352 return Err(ValueError::Internal(
353 "Could not get object properties".into(),
354 ));
355 }
356
357 let properties = DroppableValue::new(properties, |&mut properties| {
359 for index in 0..count {
360 let prop = unsafe { properties.offset(index as isize) };
361 unsafe {
362 q::JS_FreeAtom(context, (*prop).atom);
363 }
364 }
365 unsafe {
366 q::js_free(context, properties as *mut std::ffi::c_void);
367 }
368 });
369
370 let mut map = HashMap::new();
371 for index in 0..count {
372 let prop = unsafe { (*properties).offset(index as isize) };
373 let raw_value = unsafe { q::JS_GetProperty(context, *obj, (*prop).atom) };
374 if q::JS_IsException(raw_value) {
375 return Err(ValueError::Internal("Could not get object property".into()));
376 }
377
378 let value_res = deserialize_value(context, &raw_value);
379 unsafe {
380 q::JS_FreeValue(context, raw_value);
381 }
382 let value = value_res?;
383
384 let key_value = unsafe { q::JS_AtomToString(context, (*prop).atom) };
385 if q::JS_IsException(key_value) {
386 return Err(ValueError::Internal(
387 "Could not get object property name".into(),
388 ));
389 }
390
391 let key_res = deserialize_value(context, &key_value);
392 unsafe {
393 q::JS_FreeValue(context, key_value);
394 }
395 let key = match key_res? {
396 JsValue::String(s) => s,
397 _ => {
398 return Err(ValueError::Internal("Could not get property name".into()));
399 }
400 };
401 map.insert(key, value);
402 }
403
404 Ok(map)
406}
407
408pub fn deserialize_value(
409 context: *mut q::JSContext,
410 value: &q::JSValue,
411) -> Result<JsValue, ValueError> {
412 let r = value;
413
414 match q::JS_VALUE_GET_TAG(*r) {
415 q::JS_TAG_INT => {
417 let val = unsafe { q::JS_VALUE_GET_INT(*r) };
418 Ok(JsValue::Int(val))
419 }
420 q::JS_TAG_BOOL => {
422 let val = unsafe { q::JS_VALUE_GET_BOOL(*r) };
423 Ok(JsValue::Bool(val))
424 }
425 q::JS_TAG_NULL => Ok(JsValue::Null),
427 q::JS_TAG_UNDEFINED => Ok(JsValue::Undefined),
429 q::JS_TAG_FLOAT64 => {
431 let val = unsafe { q::JS_VALUE_GET_FLOAT64(*r) };
432 Ok(JsValue::Float(val))
433 }
434 q::JS_TAG_STRING => {
436 let ptr = unsafe { q::JS_ToCStringLen2(context, std::ptr::null_mut(), *r, false) };
437
438 if ptr.is_null() {
439 return Err(ValueError::Internal(
440 "Could not convert string: got a null pointer".into(),
441 ));
442 }
443
444 let cstr = unsafe { std::ffi::CStr::from_ptr(ptr) };
445
446 let s = cstr
447 .to_str()
448 .map_err(ValueError::InvalidString)?
449 .to_string();
450
451 unsafe { q::JS_FreeCString(context, ptr) };
453
454 Ok(JsValue::String(s))
455 }
456 q::JS_TAG_OBJECT => {
458 let is_func = unsafe { q::JS_IsFunction(context, *r)};
459 if is_func {
460 let raw_js_value = RawJSValue::new(context, value);
462 return Ok(JsValue::Raw(raw_js_value));
463 }
464 let is_array = unsafe { q::JS_IsArray(context, *r) } > 0;
465 if is_array {
466 deserialize_array(context, r)
467 } else {
468 let is_resource = unsafe {
469 Resource::class_id().id.get() > 0 && q::JS_GetClassID(*r) == Resource::class_id().id.get()
470 };
471 if is_resource {
472 unsafe {
473 let cls_id = JS_GetClassID(*value);
474 let cls_obj = JS_GetOpaque2(context, *value, cls_id) as *mut ResourceObject;
475 let res = (*cls_obj).data.resource.clone();
476 return Ok(JsValue::Resource(ResourceValue {
477 resource: res
478 }))
479 }
480 }
481 #[cfg(feature = "chrono")]
482 {
483 use chrono::offset::TimeZone;
484
485 let date_constructor = js_date_constructor(context);
486 let is_date = unsafe { q::JS_IsInstanceOf(context, *r, date_constructor) > 0 };
487
488 if is_date {
489 let getter = unsafe {
490 q::JS_GetPropertyStr(
491 context,
492 *r,
493 std::ffi::CStr::from_bytes_with_nul(b"getTime\0")
494 .unwrap()
495 .as_ptr(),
496 )
497 };
498 assert_eq!(q::JS_VALUE_GET_TAG(getter), q::JS_TAG_OBJECT);
499
500 let timestamp_raw =
501 unsafe { q::JS_Call(context, getter, *r, 0, std::ptr::null_mut()) };
502
503 unsafe {
504 q::JS_FreeValue(context, getter);
505 q::JS_FreeValue(context, date_constructor);
506 };
507
508 let res = if q::JS_IsFloat64(timestamp_raw) {
509 let f = unsafe { q::JS_VALUE_GET_FLOAT64(timestamp_raw) } as i64;
510 let datetime = chrono::Utc.timestamp_millis(f);
511 Ok(JsValue::Date(datetime))
512 } else if q::JS_IsInt(timestamp_raw) {
513 let f = unsafe { q::JS_VALUE_GET_INT(timestamp_raw) } as i64;
514 let datetime = chrono::Utc.timestamp_millis(f);
515 Ok(JsValue::Date(datetime))
516 } else {
517 Err(ValueError::Internal(
518 "Could not convert 'Date' instance to timestamp".into(),
519 ))
520 };
521 return res;
522 } else {
523 unsafe { q::JS_FreeValue(context, date_constructor) };
524 }
525 }
526 let raw_js_value = RawJSValue::new(context, value);
527 return Ok(JsValue::Raw(raw_js_value));
528 }
529 }
530 #[cfg(feature = "bigint")]
532 TAG_BIG_INT => {
533 let mut int: i64 = 0;
534 let ret = unsafe { q::JS_ToBigInt64(context, &mut int, *r) };
535 if ret == 0 {
536 Ok(JsValue::BigInt(BigInt {
537 inner: BigIntOrI64::Int(int),
538 }))
539 } else {
540 let ptr = unsafe { q::JS_ToCStringLen2(context, std::ptr::null_mut(), *r, 0) };
541
542 if ptr.is_null() {
543 return Err(ValueError::Internal(
544 "Could not convert BigInt to string: got a null pointer".into(),
545 ));
546 }
547
548 let cstr = unsafe { std::ffi::CStr::from_ptr(ptr) };
549 let bigint = num_bigint::BigInt::parse_bytes(cstr.to_bytes(), 10).unwrap();
550
551 unsafe { q::JS_FreeCString(context, ptr) };
553
554 Ok(JsValue::BigInt(BigInt {
555 inner: BigIntOrI64::BigInt(bigint),
556 }))
557 }
558 }
559 q::JS_TAG_EXCEPTION => {
560 let raw_js_value = RawJSValue::new(context, value);
561 Ok(JsValue::Exception(raw_js_value))
562 }
563 t => {
564 if q::JS_IsFloat64(*value) {
565 Ok(JsValue::Float(unsafe {
566 q::JS_VALUE_GET_FLOAT64(*value)
567 }))
568 } else {
569 let raw_js_value = RawJSValue::new(context, value);
571 Ok(JsValue::Raw(raw_js_value))
572 }
573 }
574 }
575}