1use rquickjs::{Function, Object, Type, Value, function::This};
7
8#[derive(Clone, Copy, PartialEq, Eq)]
10pub enum Mode {
11 Loose,
13 Strict,
16}
17
18const MAX_DEPTH: usize = 64;
22
23pub fn deep_equal<'js>(a: &Value<'js>, b: &Value<'js>, mode: Mode) -> rquickjs::Result<bool> {
29 equal_at(a, b, mode, 0)
30}
31
32#[must_use]
35pub fn strict_equal<'js>(a: &Value<'js>, b: &Value<'js>) -> bool {
36 if a.type_of() != b.type_of() {
37 return false;
38 }
39 match a.type_of() {
40 Type::Int | Type::Float => number_eq(a, b, Mode::Strict),
41 Type::String => a
42 .as_string()
43 .and_then(|s| s.to_string().ok())
44 .eq(&b.as_string().and_then(|s| s.to_string().ok())),
45 Type::Bool => a.as_bool() == b.as_bool(),
46 Type::Undefined | Type::Null | Type::Uninitialized => true,
47 _ => a == b,
48 }
49}
50
51#[must_use]
53pub fn loose_equal<'js>(a: &Value<'js>, b: &Value<'js>) -> bool {
54 if a.type_of() == b.type_of() {
55 return match a.type_of() {
58 Type::Int | Type::Float => number_eq(a, b, Mode::Loose),
59 _ => strict_equal(a, b),
60 };
61 }
62 loose_primitive_eq(a, b)
63}
64
65fn equal_at<'js>(a: &Value<'js>, b: &Value<'js>, mode: Mode, depth: usize) -> rquickjs::Result<bool> {
66 if depth > MAX_DEPTH {
67 return Ok(false);
68 }
69 if a.type_of() != b.type_of() {
70 return Ok(mode == Mode::Loose && loose_primitive_eq(a, b));
72 }
73
74 match a.type_of() {
75 Type::Undefined | Type::Null | Type::Uninitialized => Ok(true),
76 Type::Bool => Ok(a.as_bool() == b.as_bool()),
77 Type::Int | Type::Float => Ok(number_eq(a, b, mode)),
78 Type::String => Ok(js_string(a)? == js_string(b)?),
79 Type::Symbol => Ok(a.as_symbol() == b.as_symbol()),
80 Type::Array => array_eq(a, b, mode, depth),
81 Type::Object | Type::Exception => object_eq(a, b, mode, depth),
82 _ => Ok(a == b),
84 }
85}
86
87fn js_string(value: &Value<'_>) -> rquickjs::Result<String> {
88 value.as_string().map_or_else(|| Ok(String::new()), rquickjs::String::to_string)
89}
90
91fn number_eq(a: &Value<'_>, b: &Value<'_>, mode: Mode) -> bool {
94 let (Some(x), Some(y)) = (a.as_number(), b.as_number()) else {
95 return false;
96 };
97 match mode {
98 Mode::Strict => {
99 if x.is_nan() && y.is_nan() {
100 true
101 } else {
102 x == y && x.is_sign_negative() == y.is_sign_negative()
103 }
104 },
105 Mode::Loose => x == y,
106 }
107}
108
109fn loose_primitive_eq(a: &Value<'_>, b: &Value<'_>) -> bool {
114 let nullish = |v: &Value<'_>| v.is_null() || v.is_undefined();
115 if nullish(a) || nullish(b) {
116 return nullish(a) && nullish(b);
117 }
118 match (coerce_number(a), coerce_number(b)) {
119 (Some(x), Some(y)) => x == y,
120 _ => false,
121 }
122}
123
124fn coerce_number(value: &Value<'_>) -> Option<f64> {
126 match value.type_of() {
127 Type::Int | Type::Float => value.as_number(),
128 Type::Bool => value.as_bool().map(|b| if b { 1.0 } else { 0.0 }),
129 Type::String => {
130 let text = value.as_string()?.to_string().ok()?;
131 let trimmed = text.trim();
132 if trimmed.is_empty() {
133 return Some(0.0);
134 }
135 trimmed.parse::<f64>().ok()
136 },
137 _ => None,
138 }
139}
140
141fn array_eq<'js>(a: &Value<'js>, b: &Value<'js>, mode: Mode, depth: usize) -> rquickjs::Result<bool> {
142 let (Some(x), Some(y)) = (a.as_array(), b.as_array()) else {
143 return Ok(false);
144 };
145 if x.len() != y.len() {
146 return Ok(false);
147 }
148 for i in 0..x.len() {
149 let (lhs, rhs): (Value<'_>, Value<'_>) = (x.get(i)?, y.get(i)?);
150 if !equal_at(&lhs, &rhs, mode, depth + 1)? {
151 return Ok(false);
152 }
153 }
154 Ok(true)
155}
156
157fn object_eq<'js>(a: &Value<'js>, b: &Value<'js>, mode: Mode, depth: usize) -> rquickjs::Result<bool> {
158 let (Some(x), Some(y)) = (a.as_object(), b.as_object()) else {
159 return Ok(false);
160 };
161
162 if mode == Mode::Strict && constructor_name(x)? != constructor_name(y)? {
163 return Ok(false);
164 }
165
166 if let (Some(lhs), Some(rhs)) = (value_of_number(x)?, value_of_number(y)?) {
168 return Ok(lhs == rhs || (lhs.is_nan() && rhs.is_nan()));
169 }
170 if let (Some(lhs), Some(rhs)) = (regexp_source(x)?, regexp_source(y)?) {
171 return Ok(lhs == rhs);
172 }
173
174 let keys: Vec<String> = own_keys(x)?;
175 let other: Vec<String> = own_keys(y)?;
176 if keys.len() != other.len() {
177 return Ok(false);
178 }
179 for key in keys {
180 if !y.contains_key(key.as_str())? {
181 return Ok(false);
182 }
183 let (lhs, rhs): (Value<'_>, Value<'_>) = (x.get(key.as_str())?, y.get(key.as_str())?);
184 if !equal_at(&lhs, &rhs, mode, depth + 1)? {
185 return Ok(false);
186 }
187 }
188 Ok(true)
189}
190
191fn own_keys(object: &Object<'_>) -> rquickjs::Result<Vec<String>> {
192 object.keys::<String>().collect::<rquickjs::Result<Vec<String>>>()
193}
194
195fn constructor_name(object: &Object<'_>) -> rquickjs::Result<Option<String>> {
196 let ctor: Option<Object<'_>> = object.get("constructor").ok();
197 match ctor {
198 Some(c) => Ok(c.get::<_, String>("name").ok()),
199 None => Ok(None),
200 }
201}
202
203fn value_of_number(object: &Object<'_>) -> rquickjs::Result<Option<f64>> {
206 if constructor_name(object)?.as_deref() != Some("Date") {
207 return Ok(None);
208 }
209 let value_of: Function<'_> = object.get("valueOf")?;
210 let millis: f64 = value_of.call((This(object.clone()),))?;
211 Ok(Some(millis))
212}
213
214fn regexp_source(object: &Object<'_>) -> rquickjs::Result<Option<String>> {
215 if constructor_name(object)?.as_deref() != Some("RegExp") {
216 return Ok(None);
217 }
218 let source: String = object.get("source")?;
219 let flags: String = object.get("flags").unwrap_or_default();
220 Ok(Some(format!("/{source}/{flags}")))
221}