Coverage Report

Created: 2025-08-28 09:57

/src/node/src/node_url.cc
Line
Count
Source (jump to first uncovered line)
1
#include "node_url.h"
2
#include "ada.h"
3
#include "base_object-inl.h"
4
#include "node_errors.h"
5
#include "node_external_reference.h"
6
#include "node_i18n.h"
7
#include "node_metadata.h"
8
#include "node_process-inl.h"
9
#include "util-inl.h"
10
#include "v8-fast-api-calls.h"
11
#include "v8.h"
12
13
#include <cstdint>
14
#include <cstdio>
15
#include <numeric>
16
17
namespace node {
18
namespace url {
19
20
using v8::CFunction;
21
using v8::Context;
22
using v8::FastOneByteString;
23
using v8::FunctionCallbackInfo;
24
using v8::HandleScope;
25
using v8::Isolate;
26
using v8::Local;
27
using v8::NewStringType;
28
using v8::Object;
29
using v8::ObjectTemplate;
30
using v8::String;
31
using v8::Value;
32
33
0
void BindingData::MemoryInfo(MemoryTracker* tracker) const {
34
0
  tracker->TrackField("url_components_buffer", url_components_buffer_);
35
0
}
36
37
BindingData::BindingData(Realm* realm, v8::Local<v8::Object> object)
38
122k
    : SnapshotableObject(realm, object, type_int),
39
122k
      url_components_buffer_(realm->isolate(), kURLComponentsLength) {
40
122k
  object
41
122k
      ->Set(realm->context(),
42
122k
            FIXED_ONE_BYTE_STRING(realm->isolate(), "urlComponents"),
43
122k
            url_components_buffer_.GetJSArray())
44
122k
      .Check();
45
122k
  url_components_buffer_.MakeWeak();
46
122k
}
47
48
bool BindingData::PrepareForSerialization(v8::Local<v8::Context> context,
49
0
                                          v8::SnapshotCreator* creator) {
50
  // We'll just re-initialize the buffers in the constructor since their
51
  // contents can be thrown away once consumed in the previous call.
52
0
  url_components_buffer_.Release();
53
  // Return true because we need to maintain the reference to the binding from
54
  // JS land.
55
0
  return true;
56
0
}
57
58
0
InternalFieldInfoBase* BindingData::Serialize(int index) {
59
0
  DCHECK_IS_SNAPSHOT_SLOT(index);
60
0
  InternalFieldInfo* info =
61
0
      InternalFieldInfoBase::New<InternalFieldInfo>(type());
62
0
  return info;
63
0
}
64
65
void BindingData::Deserialize(v8::Local<v8::Context> context,
66
                              v8::Local<v8::Object> holder,
67
                              int index,
68
0
                              InternalFieldInfoBase* info) {
69
0
  DCHECK_IS_SNAPSHOT_SLOT(index);
70
0
  v8::HandleScope scope(context->GetIsolate());
71
0
  Realm* realm = Realm::GetCurrent(context);
72
0
  BindingData* binding = realm->AddBindingData<BindingData>(holder);
73
0
  CHECK_NOT_NULL(binding);
74
0
}
75
76
0
void BindingData::DomainToASCII(const FunctionCallbackInfo<Value>& args) {
77
0
  Environment* env = Environment::GetCurrent(args);
78
0
  CHECK_GE(args.Length(), 1);
79
0
  CHECK(args[0]->IsString());
80
81
0
  std::string input = Utf8Value(env->isolate(), args[0]).ToString();
82
0
  if (input.empty()) {
83
0
    return args.GetReturnValue().Set(String::Empty(env->isolate()));
84
0
  }
85
86
  // It is important to have an initial value that contains a special scheme.
87
  // Since it will change the implementation of `set_hostname` according to URL
88
  // spec.
89
0
  auto out = ada::parse<ada::url>("ws://x");
90
0
  DCHECK(out);
91
0
  if (!out->set_hostname(input)) {
92
0
    return args.GetReturnValue().Set(String::Empty(env->isolate()));
93
0
  }
94
0
  std::string host = out->get_hostname();
95
0
  args.GetReturnValue().Set(
96
0
      String::NewFromUtf8(env->isolate(), host.c_str()).ToLocalChecked());
97
0
}
98
99
0
void BindingData::DomainToUnicode(const FunctionCallbackInfo<Value>& args) {
100
0
  Environment* env = Environment::GetCurrent(args);
101
0
  CHECK_GE(args.Length(), 1);
102
0
  CHECK(args[0]->IsString());
103
104
0
  std::string input = Utf8Value(env->isolate(), args[0]).ToString();
105
0
  if (input.empty()) {
106
0
    return args.GetReturnValue().Set(String::Empty(env->isolate()));
107
0
  }
108
109
  // It is important to have an initial value that contains a special scheme.
110
  // Since it will change the implementation of `set_hostname` according to URL
111
  // spec.
112
0
  auto out = ada::parse<ada::url>("ws://x");
113
0
  DCHECK(out);
114
0
  if (!out->set_hostname(input)) {
115
0
    return args.GetReturnValue().Set(String::Empty(env->isolate()));
116
0
  }
117
0
  std::string result = ada::unicode::to_unicode(out->get_hostname());
118
119
0
  args.GetReturnValue().Set(String::NewFromUtf8(env->isolate(),
120
0
                                                result.c_str(),
121
0
                                                NewStringType::kNormal,
122
0
                                                result.length())
123
0
                                .ToLocalChecked());
124
0
}
125
126
0
void BindingData::GetOrigin(const v8::FunctionCallbackInfo<Value>& args) {
127
0
  CHECK_GE(args.Length(), 1);
128
0
  CHECK(args[0]->IsString());  // input
129
130
0
  Environment* env = Environment::GetCurrent(args);
131
0
  HandleScope handle_scope(env->isolate());
132
133
0
  Utf8Value input(env->isolate(), args[0]);
134
0
  std::string_view input_view = input.ToStringView();
135
0
  auto out = ada::parse<ada::url_aggregator>(input_view);
136
137
0
  if (!out) {
138
0
    THROW_ERR_INVALID_URL(env, "Invalid URL");
139
0
    return;
140
0
  }
141
142
0
  std::string origin = out->get_origin();
143
0
  args.GetReturnValue().Set(String::NewFromUtf8(env->isolate(),
144
0
                                                origin.data(),
145
0
                                                NewStringType::kNormal,
146
0
                                                origin.length())
147
0
                                .ToLocalChecked());
148
0
}
149
150
0
void BindingData::CanParse(const FunctionCallbackInfo<Value>& args) {
151
0
  CHECK_GE(args.Length(), 1);
152
0
  CHECK(args[0]->IsString());  // input
153
  // args[1] // base url
154
155
0
  Environment* env = Environment::GetCurrent(args);
156
0
  HandleScope handle_scope(env->isolate());
157
158
0
  Utf8Value input(env->isolate(), args[0]);
159
0
  std::string_view input_view = input.ToStringView();
160
161
0
  bool can_parse{};
162
0
  if (args[1]->IsString()) {
163
0
    Utf8Value base(env->isolate(), args[1]);
164
0
    std::string_view base_view = base.ToStringView();
165
0
    can_parse = ada::can_parse(input_view, &base_view);
166
0
  } else {
167
0
    can_parse = ada::can_parse(input_view);
168
0
  }
169
170
0
  args.GetReturnValue().Set(can_parse);
171
0
}
172
173
bool BindingData::FastCanParse(Local<Value> receiver,
174
0
                               const FastOneByteString& input) {
175
0
  return ada::can_parse(std::string_view(input.data, input.length));
176
0
}
177
178
bool BindingData::FastCanParseWithBase(Local<Value> receiver,
179
                                       const FastOneByteString& input,
180
0
                                       const FastOneByteString& base) {
181
0
  auto base_view = std::string_view(base.data, base.length);
182
0
  return ada::can_parse(std::string_view(input.data, input.length), &base_view);
183
0
}
184
185
CFunction BindingData::fast_can_parse_methods_[] = {
186
    CFunction::Make(FastCanParse), CFunction::Make(FastCanParseWithBase)};
187
188
0
void BindingData::Format(const FunctionCallbackInfo<Value>& args) {
189
0
  CHECK_GT(args.Length(), 4);
190
0
  CHECK(args[0]->IsString());  // url href
191
192
0
  Environment* env = Environment::GetCurrent(args);
193
0
  Isolate* isolate = env->isolate();
194
195
0
  Utf8Value href(isolate, args[0].As<String>());
196
0
  const bool hash = args[1]->IsTrue();
197
0
  const bool unicode = args[2]->IsTrue();
198
0
  const bool search = args[3]->IsTrue();
199
0
  const bool auth = args[4]->IsTrue();
200
201
  // ada::url provides a faster alternative to ada::url_aggregator if we
202
  // directly want to manipulate the url components without using the respective
203
  // setters. therefore we are using ada::url here.
204
0
  auto out = ada::parse<ada::url>(href.ToStringView());
205
0
  CHECK(out);
206
207
0
  if (!hash) {
208
0
    out->hash = std::nullopt;
209
0
  }
210
211
0
  if (unicode && out->has_hostname()) {
212
0
    out->host = ada::idna::to_unicode(out->get_hostname());
213
0
  }
214
215
0
  if (!search) {
216
0
    out->query = std::nullopt;
217
0
  }
218
219
0
  if (!auth) {
220
0
    out->username = "";
221
0
    out->password = "";
222
0
  }
223
224
0
  std::string result = out->get_href();
225
0
  args.GetReturnValue().Set(String::NewFromUtf8(env->isolate(),
226
0
                                                result.data(),
227
0
                                                NewStringType::kNormal,
228
0
                                                result.length())
229
0
                                .ToLocalChecked());
230
0
}
231
232
3
void BindingData::Parse(const FunctionCallbackInfo<Value>& args) {
233
3
  CHECK_GE(args.Length(), 1);
234
3
  CHECK(args[0]->IsString());  // input
235
  // args[1] // base url
236
237
3
  Realm* realm = Realm::GetCurrent(args);
238
3
  BindingData* binding_data = realm->GetBindingData<BindingData>();
239
3
  Isolate* isolate = realm->isolate();
240
3
  std::optional<std::string> base_{};
241
242
3
  Utf8Value input(isolate, args[0]);
243
3
  ada::result<ada::url_aggregator> base;
244
3
  ada::url_aggregator* base_pointer = nullptr;
245
3
  if (args[1]->IsString()) {
246
0
    base_ = Utf8Value(isolate, args[1]).ToString();
247
0
    base = ada::parse<ada::url_aggregator>(*base_);
248
0
    if (!base) {
249
0
      return ThrowInvalidURL(realm->env(), input.ToStringView(), base_);
250
0
    }
251
0
    base_pointer = &base.value();
252
0
  }
253
3
  auto out =
254
3
      ada::parse<ada::url_aggregator>(input.ToStringView(), base_pointer);
255
256
3
  if (!out) {
257
0
    return ThrowInvalidURL(realm->env(), input.ToStringView(), base_);
258
0
  }
259
260
3
  binding_data->UpdateComponents(out->get_components(), out->type);
261
262
3
  args.GetReturnValue().Set(
263
3
      ToV8Value(realm->context(), out->get_href(), isolate).ToLocalChecked());
264
3
}
265
266
3
void BindingData::Update(const FunctionCallbackInfo<Value>& args) {
267
3
  CHECK(args[0]->IsString());    // href
268
3
  CHECK(args[1]->IsNumber());    // action type
269
3
  CHECK(args[2]->IsString());    // new value
270
271
3
  Realm* realm = Realm::GetCurrent(args);
272
3
  BindingData* binding_data = realm->GetBindingData<BindingData>();
273
3
  Isolate* isolate = realm->isolate();
274
275
3
  enum url_update_action action = static_cast<enum url_update_action>(
276
3
      args[1]->Uint32Value(realm->context()).FromJust());
277
3
  Utf8Value input(isolate, args[0].As<String>());
278
3
  Utf8Value new_value(isolate, args[2].As<String>());
279
280
3
  std::string_view new_value_view = new_value.ToStringView();
281
3
  auto out = ada::parse<ada::url_aggregator>(input.ToStringView());
282
3
  CHECK(out);
283
284
3
  bool result{true};
285
286
3
  switch (action) {
287
1
    case kPathname: {
288
1
      result = out->set_pathname(new_value_view);
289
1
      break;
290
0
    }
291
1
    case kHash: {
292
1
      out->set_hash(new_value_view);
293
1
      break;
294
0
    }
295
0
    case kHost: {
296
0
      result = out->set_host(new_value_view);
297
0
      break;
298
0
    }
299
0
    case kHostname: {
300
0
      result = out->set_hostname(new_value_view);
301
0
      break;
302
0
    }
303
0
    case kHref: {
304
0
      result = out->set_href(new_value_view);
305
0
      break;
306
0
    }
307
0
    case kPassword: {
308
0
      result = out->set_password(new_value_view);
309
0
      break;
310
0
    }
311
0
    case kPort: {
312
0
      result = out->set_port(new_value_view);
313
0
      break;
314
0
    }
315
0
    case kProtocol: {
316
0
      result = out->set_protocol(new_value_view);
317
0
      break;
318
0
    }
319
1
    case kSearch: {
320
1
      out->set_search(new_value_view);
321
1
      break;
322
0
    }
323
0
    case kUsername: {
324
0
      result = out->set_username(new_value_view);
325
0
      break;
326
0
    }
327
0
    default:
328
0
      UNREACHABLE("Unsupported URL update action");
329
3
  }
330
331
3
  if (!result) {
332
0
    return args.GetReturnValue().Set(false);
333
0
  }
334
335
3
  binding_data->UpdateComponents(out->get_components(), out->type);
336
3
  args.GetReturnValue().Set(
337
3
      ToV8Value(realm->context(), out->get_href(), isolate).ToLocalChecked());
338
3
}
339
340
void BindingData::UpdateComponents(const ada::url_components& components,
341
6
                                   const ada::scheme::type type) {
342
6
  url_components_buffer_[0] = components.protocol_end;
343
6
  url_components_buffer_[1] = components.username_end;
344
6
  url_components_buffer_[2] = components.host_start;
345
6
  url_components_buffer_[3] = components.host_end;
346
6
  url_components_buffer_[4] = components.port;
347
6
  url_components_buffer_[5] = components.pathname_start;
348
6
  url_components_buffer_[6] = components.search_start;
349
6
  url_components_buffer_[7] = components.hash_start;
350
6
  url_components_buffer_[8] = type;
351
6
  static_assert(kURLComponentsLength == 9,
352
6
                "kURLComponentsLength should be up-to-date");
353
6
}
354
355
void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data,
356
122k
                                             Local<ObjectTemplate> target) {
357
122k
  Isolate* isolate = isolate_data->isolate();
358
122k
  SetMethodNoSideEffect(isolate, target, "domainToASCII", DomainToASCII);
359
122k
  SetMethodNoSideEffect(isolate, target, "domainToUnicode", DomainToUnicode);
360
122k
  SetMethodNoSideEffect(isolate, target, "format", Format);
361
122k
  SetMethodNoSideEffect(isolate, target, "getOrigin", GetOrigin);
362
122k
  SetMethod(isolate, target, "parse", Parse);
363
122k
  SetMethod(isolate, target, "update", Update);
364
122k
  SetFastMethodNoSideEffect(
365
122k
      isolate, target, "canParse", CanParse, {fast_can_parse_methods_, 2});
366
122k
}
367
368
void BindingData::CreatePerContextProperties(Local<Object> target,
369
                                             Local<Value> unused,
370
                                             Local<Context> context,
371
122k
                                             void* priv) {
372
122k
  Realm* realm = Realm::GetCurrent(context);
373
122k
  realm->AddBindingData<BindingData>(target);
374
122k
}
375
376
void BindingData::RegisterExternalReferences(
377
0
    ExternalReferenceRegistry* registry) {
378
0
  registry->Register(DomainToASCII);
379
0
  registry->Register(DomainToUnicode);
380
0
  registry->Register(Format);
381
0
  registry->Register(GetOrigin);
382
0
  registry->Register(Parse);
383
0
  registry->Register(Update);
384
0
  registry->Register(CanParse);
385
0
  registry->Register(FastCanParse);
386
0
  registry->Register(FastCanParseWithBase);
387
388
0
  for (const CFunction& method : fast_can_parse_methods_) {
389
0
    registry->Register(method.GetTypeInfo());
390
0
  }
391
0
}
392
393
void ThrowInvalidURL(node::Environment* env,
394
                     std::string_view input,
395
0
                     std::optional<std::string> base) {
396
0
  Local<Value> err = ERR_INVALID_URL(env->isolate(), "Invalid URL");
397
0
  DCHECK(err->IsObject());
398
399
0
  auto err_object = err.As<Object>();
400
401
0
  USE(err_object->Set(env->context(),
402
0
                      env->input_string(),
403
0
                      v8::String::NewFromUtf8(env->isolate(),
404
0
                                              input.data(),
405
0
                                              v8::NewStringType::kNormal,
406
0
                                              input.size())
407
0
                          .ToLocalChecked()));
408
409
0
  if (base.has_value()) {
410
0
    USE(err_object->Set(env->context(),
411
0
                        env->base_string(),
412
0
                        v8::String::NewFromUtf8(env->isolate(),
413
0
                                                base.value().c_str(),
414
0
                                                v8::NewStringType::kNormal,
415
0
                                                base.value().size())
416
0
                            .ToLocalChecked()));
417
0
  }
418
419
0
  env->isolate()->ThrowException(err);
420
0
}
421
422
0
std::string FromFilePath(std::string_view file_path) {
423
  // Avoid unnecessary allocations.
424
0
  size_t pos = file_path.empty() ? std::string_view::npos : file_path.find('%');
425
0
  if (pos == std::string_view::npos) {
426
0
    return ada::href_from_file(file_path);
427
0
  }
428
  // Escape '%' characters to a temporary string.
429
0
  std::string escaped_file_path;
430
0
  do {
431
0
    escaped_file_path += file_path.substr(0, pos + 1);
432
0
    escaped_file_path += "25";
433
0
    file_path = file_path.substr(pos + 1);
434
0
    pos = file_path.empty() ? std::string_view::npos : file_path.find('%');
435
0
  } while (pos != std::string_view::npos);
436
0
  escaped_file_path += file_path;
437
0
  return ada::href_from_file(escaped_file_path);
438
0
}
439
440
std::optional<std::string> FileURLToPath(Environment* env,
441
0
                                         const ada::url_aggregator& file_url) {
442
0
  if (file_url.type != ada::scheme::FILE) {
443
0
    THROW_ERR_INVALID_URL_SCHEME(env->isolate());
444
0
    return std::nullopt;
445
0
  }
446
447
0
  std::string_view pathname = file_url.get_pathname();
448
#ifdef _WIN32
449
  size_t first_percent = std::string::npos;
450
  size_t pathname_size = pathname.size();
451
  std::string pathname_escaped_slash;
452
453
  for (size_t i = 0; i < pathname_size; i++) {
454
    if (pathname[i] == '/') {
455
      pathname_escaped_slash += '\\';
456
    } else {
457
      pathname_escaped_slash += pathname[i];
458
    }
459
460
    if (pathname[i] != '%') continue;
461
462
    if (first_percent == std::string::npos) {
463
      first_percent = i;
464
    }
465
466
    // just safe-guard against access the pathname
467
    // outside the bounds
468
    if ((i + 2) >= pathname_size) continue;
469
470
    char third = pathname[i + 2] | 0x20;
471
472
    bool is_slash = pathname[i + 1] == '2' && third == 102;
473
    bool is_forward_slash = pathname[i + 1] == '5' && third == 99;
474
475
    if (!is_slash && !is_forward_slash) continue;
476
477
    THROW_ERR_INVALID_FILE_URL_PATH(
478
        env->isolate(),
479
        "File URL path must not include encoded \\ or / characters");
480
    return std::nullopt;
481
  }
482
483
  std::string_view hostname = file_url.get_hostname();
484
  std::string decoded_pathname = ada::unicode::percent_decode(
485
      std::string_view(pathname_escaped_slash), first_percent);
486
487
  if (hostname.size() > 0) {
488
    // If hostname is set, then we have a UNC path
489
    // Pass the hostname through domainToUnicode just in case
490
    // it is an IDN using punycode encoding. We do not need to worry
491
    // about percent encoding because the URL parser will have
492
    // already taken care of that for us. Note that this only
493
    // causes IDNs with an appropriate `xn--` prefix to be decoded.
494
    return "\\\\" + ada::unicode::to_unicode(hostname) + decoded_pathname;
495
  }
496
497
  char letter = decoded_pathname[1] | 0x20;
498
  char sep = decoded_pathname[2];
499
500
  // a..z A..Z
501
  if (letter < 'a' || letter > 'z' || sep != ':') {
502
    THROW_ERR_INVALID_FILE_URL_PATH(env->isolate(),
503
                                    "File URL path must be absolute");
504
    return std::nullopt;
505
  }
506
507
  return decoded_pathname.substr(1);
508
#else   // _WIN32
509
0
  std::string_view hostname = file_url.get_hostname();
510
511
0
  if (hostname.size() > 0) {
512
0
    THROW_ERR_INVALID_FILE_URL_HOST(
513
0
        env->isolate(),
514
0
        "File URL host must be \"localhost\" or empty on ",
515
0
        std::string(per_process::metadata.platform));
516
0
    return std::nullopt;
517
0
  }
518
519
0
  size_t first_percent = std::string::npos;
520
0
  for (size_t i = 0; (i + 2) < pathname.size(); i++) {
521
0
    if (pathname[i] != '%') continue;
522
523
0
    if (first_percent == std::string::npos) {
524
0
      first_percent = i;
525
0
    }
526
527
0
    if (pathname[i + 1] == '2' && (pathname[i + 2] | 0x20) == 102) {
528
0
      THROW_ERR_INVALID_FILE_URL_PATH(
529
0
          env->isolate(),
530
0
          "File URL path must not include encoded / characters");
531
0
      return std::nullopt;
532
0
    }
533
0
  }
534
535
0
  return ada::unicode::percent_decode(pathname, first_percent);
536
0
#endif  // _WIN32
537
0
}
538
539
// Reverse the logic applied by path.toNamespacedPath() to create a
540
// namespace-prefixed path.
541
0
void FromNamespacedPath(std::string* path) {
542
#ifdef _WIN32
543
  if (path->compare(0, 8, "\\\\?\\UNC\\", 8) == 0) {
544
    *path = path->substr(8);
545
    path->insert(0, "\\\\");
546
  } else if (path->compare(0, 4, "\\\\?\\", 4) == 0) {
547
    *path = path->substr(4);
548
  }
549
#endif
550
0
}
551
552
}  // namespace url
553
554
}  // namespace node
555
556
NODE_BINDING_CONTEXT_AWARE_INTERNAL(
557
    url, node::url::BindingData::CreatePerContextProperties)
558
NODE_BINDING_PER_ISOLATE_INIT(
559
    url, node::url::BindingData::CreatePerIsolateProperties)
560
NODE_BINDING_EXTERNAL_REFERENCE(
561
    url, node::url::BindingData::RegisterExternalReferences)