/src/serenity/Userland/Libraries/LibJS/Runtime/PrimitiveString.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> |
3 | | * Copyright (c) 2022, Linus Groh <linusg@serenityos.org> |
4 | | * |
5 | | * SPDX-License-Identifier: BSD-2-Clause |
6 | | */ |
7 | | |
8 | | #include <AK/CharacterTypes.h> |
9 | | #include <AK/FlyString.h> |
10 | | #include <AK/StringBuilder.h> |
11 | | #include <AK/Utf16View.h> |
12 | | #include <AK/Utf8View.h> |
13 | | #include <LibJS/Runtime/AbstractOperations.h> |
14 | | #include <LibJS/Runtime/GlobalObject.h> |
15 | | #include <LibJS/Runtime/PrimitiveString.h> |
16 | | #include <LibJS/Runtime/PropertyKey.h> |
17 | | #include <LibJS/Runtime/VM.h> |
18 | | #include <LibJS/Runtime/Value.h> |
19 | | |
20 | | namespace JS { |
21 | | |
22 | | JS_DEFINE_ALLOCATOR(PrimitiveString); |
23 | | |
24 | | PrimitiveString::PrimitiveString(PrimitiveString& lhs, PrimitiveString& rhs) |
25 | 0 | : m_is_rope(true) |
26 | 0 | , m_lhs(&lhs) |
27 | 0 | , m_rhs(&rhs) |
28 | 0 | { |
29 | 0 | } |
30 | | |
31 | | PrimitiveString::PrimitiveString(String string) |
32 | 7.88k | : m_utf8_string(move(string)) |
33 | 7.88k | { |
34 | 7.88k | } |
35 | | |
36 | | PrimitiveString::PrimitiveString(ByteString string) |
37 | 10.5k | : m_byte_string(move(string)) |
38 | 10.5k | { |
39 | 10.5k | } |
40 | | |
41 | | PrimitiveString::PrimitiveString(Utf16String string) |
42 | 0 | : m_utf16_string(move(string)) |
43 | 0 | { |
44 | 0 | } |
45 | | |
46 | | PrimitiveString::~PrimitiveString() |
47 | 18.4k | { |
48 | 18.4k | if (has_utf8_string()) |
49 | 7.88k | vm().string_cache().remove(*m_utf8_string); |
50 | 18.4k | if (has_utf16_string()) |
51 | 0 | vm().utf16_string_cache().remove(*m_utf16_string); |
52 | 18.4k | if (has_byte_string()) |
53 | 10.5k | vm().byte_string_cache().remove(*m_byte_string); |
54 | 18.4k | } |
55 | | |
56 | | void PrimitiveString::visit_edges(Cell::Visitor& visitor) |
57 | 0 | { |
58 | 0 | Base::visit_edges(visitor); |
59 | 0 | if (m_is_rope) { |
60 | 0 | visitor.visit(m_lhs); |
61 | 0 | visitor.visit(m_rhs); |
62 | 0 | } |
63 | 0 | } |
64 | | |
65 | | bool PrimitiveString::is_empty() const |
66 | 0 | { |
67 | 0 | if (m_is_rope) { |
68 | | // NOTE: We never make an empty rope string. |
69 | 0 | return false; |
70 | 0 | } |
71 | | |
72 | 0 | if (has_utf16_string()) |
73 | 0 | return m_utf16_string->is_empty(); |
74 | 0 | if (has_utf8_string()) |
75 | 0 | return m_utf8_string->is_empty(); |
76 | 0 | if (has_byte_string()) |
77 | 0 | return m_byte_string->is_empty(); |
78 | 0 | VERIFY_NOT_REACHED(); |
79 | 0 | } |
80 | | |
81 | | String PrimitiveString::utf8_string() const |
82 | 0 | { |
83 | 0 | resolve_rope_if_needed(EncodingPreference::UTF8); |
84 | |
|
85 | 0 | if (!has_utf8_string()) { |
86 | 0 | if (has_byte_string()) |
87 | 0 | m_utf8_string = MUST(String::from_byte_string(*m_byte_string)); |
88 | 0 | else if (has_utf16_string()) |
89 | 0 | m_utf8_string = m_utf16_string->to_utf8(); |
90 | 0 | else |
91 | 0 | VERIFY_NOT_REACHED(); |
92 | 0 | } |
93 | | |
94 | 0 | return *m_utf8_string; |
95 | 0 | } |
96 | | |
97 | | StringView PrimitiveString::utf8_string_view() const |
98 | 0 | { |
99 | 0 | (void)utf8_string(); |
100 | 0 | return m_utf8_string->bytes_as_string_view(); |
101 | 0 | } |
102 | | |
103 | | ByteString PrimitiveString::byte_string() const |
104 | 0 | { |
105 | 0 | resolve_rope_if_needed(EncodingPreference::UTF8); |
106 | |
|
107 | 0 | if (!has_byte_string()) { |
108 | 0 | if (has_utf8_string()) |
109 | 0 | m_byte_string = m_utf8_string->to_byte_string(); |
110 | 0 | else if (has_utf16_string()) |
111 | 0 | m_byte_string = m_utf16_string->to_byte_string(); |
112 | 0 | else |
113 | 0 | VERIFY_NOT_REACHED(); |
114 | 0 | } |
115 | | |
116 | 0 | return *m_byte_string; |
117 | 0 | } |
118 | | |
119 | | Utf16String PrimitiveString::utf16_string() const |
120 | 0 | { |
121 | 0 | resolve_rope_if_needed(EncodingPreference::UTF16); |
122 | |
|
123 | 0 | if (!has_utf16_string()) { |
124 | 0 | if (has_utf8_string()) { |
125 | 0 | m_utf16_string = Utf16String::create(m_utf8_string->bytes_as_string_view()); |
126 | 0 | } else { |
127 | 0 | VERIFY(has_byte_string()); |
128 | 0 | m_utf16_string = Utf16String::create(*m_byte_string); |
129 | 0 | } |
130 | 0 | } |
131 | | |
132 | 0 | return *m_utf16_string; |
133 | 0 | } |
134 | | |
135 | | Utf16View PrimitiveString::utf16_string_view() const |
136 | 0 | { |
137 | 0 | (void)utf16_string(); |
138 | 0 | return m_utf16_string->view(); |
139 | 0 | } |
140 | | |
141 | | ThrowCompletionOr<Optional<Value>> PrimitiveString::get(VM& vm, PropertyKey const& property_key) const |
142 | 0 | { |
143 | 0 | if (property_key.is_symbol()) |
144 | 0 | return Optional<Value> {}; |
145 | 0 | if (property_key.is_string()) { |
146 | 0 | if (property_key.as_string() == vm.names.length.as_string()) { |
147 | 0 | auto length = utf16_string().length_in_code_units(); |
148 | 0 | return Value(static_cast<double>(length)); |
149 | 0 | } |
150 | 0 | } |
151 | 0 | auto index = canonical_numeric_index_string(property_key, CanonicalIndexMode::IgnoreNumericRoundtrip); |
152 | 0 | if (!index.is_index()) |
153 | 0 | return Optional<Value> {}; |
154 | 0 | auto str = utf16_string_view(); |
155 | 0 | auto length = str.length_in_code_units(); |
156 | 0 | if (length <= index.as_index()) |
157 | 0 | return Optional<Value> {}; |
158 | 0 | return create(vm, Utf16String::create(str.substring_view(index.as_index(), 1))); |
159 | 0 | } |
160 | | |
161 | | NonnullGCPtr<PrimitiveString> PrimitiveString::create(VM& vm, Utf16String string) |
162 | 0 | { |
163 | 0 | if (string.is_empty()) |
164 | 0 | return vm.empty_string(); |
165 | | |
166 | 0 | if (string.length_in_code_units() == 1) { |
167 | 0 | u16 code_unit = string.code_unit_at(0); |
168 | 0 | if (is_ascii(code_unit)) |
169 | 0 | return vm.single_ascii_character_string(static_cast<u8>(code_unit)); |
170 | 0 | } |
171 | | |
172 | 0 | auto& string_cache = vm.utf16_string_cache(); |
173 | 0 | if (auto it = string_cache.find(string); it != string_cache.end()) |
174 | 0 | return *it->value; |
175 | | |
176 | 0 | auto new_string = vm.heap().allocate_without_realm<PrimitiveString>(string); |
177 | 0 | string_cache.set(move(string), new_string); |
178 | 0 | return *new_string; |
179 | 0 | } |
180 | | |
181 | | NonnullGCPtr<PrimitiveString> PrimitiveString::create(VM& vm, String string) |
182 | 1.02k | { |
183 | 1.02k | if (string.is_empty()) |
184 | 231 | return vm.empty_string(); |
185 | | |
186 | 792 | if (auto bytes = string.bytes_as_string_view(); bytes.length() == 1) { |
187 | 0 | auto ch = static_cast<u8>(bytes[0]); |
188 | 0 | if (is_ascii(ch)) |
189 | 0 | return vm.single_ascii_character_string(ch); |
190 | 0 | } |
191 | | |
192 | 792 | auto& string_cache = vm.string_cache(); |
193 | 792 | if (auto it = string_cache.find(string); it != string_cache.end()) |
194 | 0 | return *it->value; |
195 | | |
196 | 792 | auto new_string = vm.heap().allocate_without_realm<PrimitiveString>(string); |
197 | 792 | string_cache.set(move(string), new_string); |
198 | 792 | return *new_string; |
199 | 792 | } |
200 | | |
201 | | NonnullGCPtr<PrimitiveString> PrimitiveString::create(VM& vm, FlyString const& string) |
202 | 0 | { |
203 | 0 | return create(vm, string.to_string()); |
204 | 0 | } |
205 | | |
206 | | NonnullGCPtr<PrimitiveString> PrimitiveString::create(VM& vm, StringView string) |
207 | 0 | { |
208 | 0 | return create(vm, String::from_utf8(string).release_value()); |
209 | 0 | } |
210 | | |
211 | | NonnullGCPtr<PrimitiveString> PrimitiveString::create(VM& vm, ByteString string) |
212 | 24.6k | { |
213 | 24.6k | if (string.is_empty()) |
214 | 11.6k | return vm.empty_string(); |
215 | | |
216 | 12.9k | if (string.length() == 1) { |
217 | 0 | auto ch = static_cast<u8>(string.characters()[0]); |
218 | 0 | if (is_ascii(ch)) |
219 | 0 | return vm.single_ascii_character_string(ch); |
220 | 0 | } |
221 | | |
222 | 12.9k | auto& string_cache = vm.byte_string_cache(); |
223 | 12.9k | auto it = string_cache.find(string); |
224 | 12.9k | if (it == string_cache.end()) { |
225 | 10.0k | auto new_string = vm.heap().allocate_without_realm<PrimitiveString>(string); |
226 | 10.0k | string_cache.set(move(string), new_string); |
227 | 10.0k | return *new_string; |
228 | 10.0k | } |
229 | 2.87k | return *it->value; |
230 | 12.9k | } |
231 | | |
232 | | NonnullGCPtr<PrimitiveString> PrimitiveString::create(VM& vm, DeprecatedFlyString const& string) |
233 | 13.0k | { |
234 | 13.0k | return create(vm, ByteString { string }); |
235 | 13.0k | } |
236 | | |
237 | | NonnullGCPtr<PrimitiveString> PrimitiveString::create(VM& vm, PrimitiveString& lhs, PrimitiveString& rhs) |
238 | 0 | { |
239 | | // We're here to concatenate two strings into a new rope string. |
240 | | // However, if any of them are empty, no rope is required. |
241 | |
|
242 | 0 | bool lhs_empty = lhs.is_empty(); |
243 | 0 | bool rhs_empty = rhs.is_empty(); |
244 | |
|
245 | 0 | if (lhs_empty && rhs_empty) |
246 | 0 | return vm.empty_string(); |
247 | | |
248 | 0 | if (lhs_empty) |
249 | 0 | return rhs; |
250 | | |
251 | 0 | if (rhs_empty) |
252 | 0 | return lhs; |
253 | | |
254 | 0 | return vm.heap().allocate_without_realm<PrimitiveString>(lhs, rhs); |
255 | 0 | } |
256 | | |
257 | | void PrimitiveString::resolve_rope_if_needed(EncodingPreference preference) const |
258 | 0 | { |
259 | 0 | if (!m_is_rope) |
260 | 0 | return; |
261 | | |
262 | | // This vector will hold all the pieces of the rope that need to be assembled |
263 | | // into the resolved string. |
264 | 0 | Vector<PrimitiveString const*> pieces; |
265 | | |
266 | | // NOTE: We traverse the rope tree without using recursion, since we'd run out of |
267 | | // stack space quickly when handling a long sequence of unresolved concatenations. |
268 | 0 | Vector<PrimitiveString const*> stack; |
269 | 0 | stack.append(m_rhs); |
270 | 0 | stack.append(m_lhs); |
271 | 0 | while (!stack.is_empty()) { |
272 | 0 | auto const* current = stack.take_last(); |
273 | 0 | if (current->m_is_rope) { |
274 | 0 | stack.append(current->m_rhs); |
275 | 0 | stack.append(current->m_lhs); |
276 | 0 | continue; |
277 | 0 | } |
278 | 0 | pieces.append(current); |
279 | 0 | } |
280 | |
|
281 | 0 | if (preference == EncodingPreference::UTF16) { |
282 | | // The caller wants a UTF-16 string, so we can simply concatenate all the pieces |
283 | | // into a UTF-16 code unit buffer and create a Utf16String from it. |
284 | |
|
285 | 0 | Utf16Data code_units; |
286 | 0 | for (auto const* current : pieces) |
287 | 0 | code_units.extend(current->utf16_string().string()); |
288 | |
|
289 | 0 | m_utf16_string = Utf16String::create(move(code_units)); |
290 | 0 | m_is_rope = false; |
291 | 0 | m_lhs = nullptr; |
292 | 0 | m_rhs = nullptr; |
293 | 0 | return; |
294 | 0 | } |
295 | | |
296 | | // Now that we have all the pieces, we can concatenate them using a StringBuilder. |
297 | 0 | StringBuilder builder; |
298 | | |
299 | | // We keep track of the previous piece in order to handle surrogate pairs spread across two pieces. |
300 | 0 | PrimitiveString const* previous = nullptr; |
301 | 0 | for (auto const* current : pieces) { |
302 | 0 | if (!previous) { |
303 | | // This is the very first piece, just append it and continue. |
304 | 0 | builder.append(current->utf8_string()); |
305 | 0 | previous = current; |
306 | 0 | continue; |
307 | 0 | } |
308 | | |
309 | | // Get the UTF-8 representations for both strings. |
310 | 0 | auto current_string_as_utf8 = current->utf8_string_view(); |
311 | 0 | auto previous_string_as_utf8 = previous->utf8_string_view(); |
312 | | |
313 | | // NOTE: Now we need to look at the end of the previous string and the start |
314 | | // of the current string, to see if they should be combined into a surrogate. |
315 | | |
316 | | // Surrogates encoded as UTF-8 are 3 bytes. |
317 | 0 | if ((previous_string_as_utf8.length() < 3) || (current_string_as_utf8.length() < 3)) { |
318 | 0 | builder.append(current_string_as_utf8); |
319 | 0 | previous = current; |
320 | 0 | continue; |
321 | 0 | } |
322 | | |
323 | | // Might the previous string end with a UTF-8 encoded surrogate? |
324 | 0 | if ((static_cast<u8>(previous_string_as_utf8[previous_string_as_utf8.length() - 3]) & 0xf0) != 0xe0) { |
325 | | // If not, just append the current string and continue. |
326 | 0 | builder.append(current_string_as_utf8); |
327 | 0 | previous = current; |
328 | 0 | continue; |
329 | 0 | } |
330 | | |
331 | | // Might the current string begin with a UTF-8 encoded surrogate? |
332 | 0 | if ((static_cast<u8>(current_string_as_utf8[0]) & 0xf0) != 0xe0) { |
333 | | // If not, just append the current string and continue. |
334 | 0 | builder.append(current_string_as_utf8); |
335 | 0 | previous = current; |
336 | 0 | continue; |
337 | 0 | } |
338 | | |
339 | 0 | auto high_surrogate = *Utf8View(previous_string_as_utf8.substring_view(previous_string_as_utf8.length() - 3)).begin(); |
340 | 0 | auto low_surrogate = *Utf8View(current_string_as_utf8).begin(); |
341 | |
|
342 | 0 | if (!Utf16View::is_high_surrogate(high_surrogate) || !Utf16View::is_low_surrogate(low_surrogate)) { |
343 | 0 | builder.append(current_string_as_utf8); |
344 | 0 | previous = current; |
345 | 0 | continue; |
346 | 0 | } |
347 | | |
348 | | // Remove 3 bytes from the builder and replace them with the UTF-8 encoded code point. |
349 | 0 | builder.trim(3); |
350 | 0 | builder.append_code_point(Utf16View::decode_surrogate_pair(high_surrogate, low_surrogate)); |
351 | | |
352 | | // Append the remaining part of the current string. |
353 | 0 | builder.append(current_string_as_utf8.substring_view(3)); |
354 | 0 | previous = current; |
355 | 0 | } |
356 | | |
357 | | // NOTE: We've already produced valid UTF-8 above, so there's no need for additional validation. |
358 | 0 | m_utf8_string = builder.to_string_without_validation(); |
359 | 0 | m_is_rope = false; |
360 | 0 | m_lhs = nullptr; |
361 | 0 | m_rhs = nullptr; |
362 | 0 | } |
363 | | |
364 | | } |