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