Coverage Report

Created: 2026-01-21 08:52

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/node/src/node_url.cc
Line
Count
Source
1
#include "node_url.h"
2
#include "ada.h"
3
#include "base_object-inl.h"
4
#include "node_debug.h"
5
#include "node_errors.h"
6
#include "node_external_reference.h"
7
#include "node_i18n.h"
8
#include "node_metadata.h"
9
#include "node_process-inl.h"
10
#include "path.h"
11
#include "util-inl.h"
12
#include "v8-fast-api-calls.h"
13
#include "v8-local-handle.h"
14
#include "v8.h"
15
16
#include <cstdint>
17
#include <cstdio>
18
#include <numeric>
19
20
namespace node {
21
namespace url {
22
23
using v8::CFunction;
24
using v8::Context;
25
using v8::FastApiCallbackOptions;
26
using v8::FunctionCallbackInfo;
27
using v8::HandleScope;
28
using v8::Isolate;
29
using v8::Local;
30
using v8::Object;
31
using v8::ObjectTemplate;
32
using v8::SnapshotCreator;
33
using v8::String;
34
using v8::Value;
35
36
0
void BindingData::MemoryInfo(MemoryTracker* tracker) const {
37
0
  tracker->TrackField("url_components_buffer", url_components_buffer_);
38
0
}
39
40
BindingData::BindingData(Realm* realm, Local<Object> object)
41
35
    : SnapshotableObject(realm, object, type_int),
42
35
      url_components_buffer_(realm->isolate(), kURLComponentsLength) {
43
35
  object
44
35
      ->Set(realm->context(),
45
35
            FIXED_ONE_BYTE_STRING(realm->isolate(), "urlComponents"),
46
35
            url_components_buffer_.GetJSArray())
47
35
      .Check();
48
35
  url_components_buffer_.MakeWeak();
49
35
}
50
51
bool BindingData::PrepareForSerialization(Local<Context> context,
52
0
                                          SnapshotCreator* creator) {
53
  // We'll just re-initialize the buffers in the constructor since their
54
  // contents can be thrown away once consumed in the previous call.
55
0
  url_components_buffer_.Release();
56
  // Return true because we need to maintain the reference to the binding from
57
  // JS land.
58
0
  return true;
59
0
}
60
61
0
InternalFieldInfoBase* BindingData::Serialize(int index) {
62
0
  DCHECK_IS_SNAPSHOT_SLOT(index);
63
0
  InternalFieldInfo* info =
64
0
      InternalFieldInfoBase::New<InternalFieldInfo>(type());
65
0
  return info;
66
0
}
67
68
void BindingData::Deserialize(Local<Context> context,
69
                              Local<Object> holder,
70
                              int index,
71
0
                              InternalFieldInfoBase* info) {
72
0
  DCHECK_IS_SNAPSHOT_SLOT(index);
73
0
  HandleScope scope(Isolate::GetCurrent());
74
0
  Realm* realm = Realm::GetCurrent(context);
75
0
  BindingData* binding = realm->AddBindingData<BindingData>(holder);
76
0
  CHECK_NOT_NULL(binding);
77
0
}
78
79
#ifndef LARGEST_ASCII_CHAR_CODE_TO_ENCODE
80
0
#define LARGEST_ASCII_CHAR_CODE_TO_ENCODE '~'
81
#endif
82
83
// RFC1738 defines the following chars as "unsafe" for URLs
84
// @see https://www.ietf.org/rfc/rfc1738.txt 2.2. URL Character Encoding Issues
85
constexpr auto lookup_table = []() consteval {
86
  // Each entry is an array that can hold up to 3 chars + null terminator
87
  std::array<std::array<char, 4>, LARGEST_ASCII_CHAR_CODE_TO_ENCODE + 1>
88
      result{};
89
90
  for (uint8_t i = 0; i <= LARGEST_ASCII_CHAR_CODE_TO_ENCODE; i++) {
91
    switch (i) {
92
#define ENCODE_CHAR(CHAR, HEX_DIGIT_2, HEX_DIGIT_1)                            \
93
  case CHAR:                                                                   \
94
    result[i] = {{'%', HEX_DIGIT_2, HEX_DIGIT_1, 0}};                          \
95
    break;
96
97
      ENCODE_CHAR('\0', '0', '0')  // '\0' == 0x00
98
      ENCODE_CHAR('\t', '0', '9')  // '\t' == 0x09
99
      ENCODE_CHAR('\n', '0', 'A')  // '\n' == 0x0A
100
      ENCODE_CHAR('\r', '0', 'D')  // '\r' == 0x0D
101
      ENCODE_CHAR(' ', '2', '0')   // ' ' == 0x20
102
      ENCODE_CHAR('"', '2', '2')   // '"' == 0x22
103
      ENCODE_CHAR('#', '2', '3')   // '#' == 0x23
104
      ENCODE_CHAR('%', '2', '5')   // '%' == 0x25
105
      ENCODE_CHAR('?', '3', 'F')   // '?' == 0x3F
106
      ENCODE_CHAR('[', '5', 'B')   // '[' == 0x5B
107
      ENCODE_CHAR('\\', '5', 'C')  // '\\' == 0x5C
108
      ENCODE_CHAR(']', '5', 'D')   // ']' == 0x5D
109
      ENCODE_CHAR('^', '5', 'E')   // '^' == 0x5E
110
      ENCODE_CHAR('|', '7', 'C')   // '|' == 0x7C
111
      ENCODE_CHAR('~', '7', 'E')   // '~' == 0x7E
112
#undef ENCODE_CHAR
113
114
      default:
115
        result[i] = {{static_cast<char>(i), '\0', '\0', '\0'}};
116
        break;
117
    }
118
  }
119
120
  return result;
121
}
122
();
123
124
enum class OS { WINDOWS, POSIX };
125
126
0
std::string EncodePathChars(std::string_view input_str, OS operating_system) {
127
0
  std::string encoded = "file://";
128
0
  encoded.reserve(input_str.size() +
129
0
                  7);  // Reserve space for "file://" and input_str
130
0
  for (size_t i : input_str) {
131
0
    if (i > LARGEST_ASCII_CHAR_CODE_TO_ENCODE) [[unlikely]] {
132
0
      encoded.push_back(i);
133
0
      continue;
134
0
    }
135
0
    if (operating_system == OS::WINDOWS) {
136
0
      if (i == '\\') {
137
0
        encoded.push_back('/');
138
0
        continue;
139
0
      }
140
0
    }
141
0
    encoded.append(lookup_table[i].data());
142
0
  }
143
144
0
  return encoded;
145
0
}
146
147
0
void BindingData::PathToFileURL(const FunctionCallbackInfo<Value>& args) {
148
0
  CHECK_GE(args.Length(), 2);  // input
149
0
  CHECK(args[0]->IsString());
150
0
  CHECK(args[1]->IsBoolean());
151
152
0
  Realm* realm = Realm::GetCurrent(args);
153
0
  BindingData* binding_data = realm->GetBindingData<BindingData>();
154
0
  Isolate* isolate = realm->isolate();
155
0
  OS os = args[1]->IsTrue() ? OS::WINDOWS : OS::POSIX;
156
157
0
  Utf8Value input(isolate, args[0]);
158
0
  auto input_str = input.ToStringView();
159
0
  CHECK(!input_str.empty());
160
161
0
  auto out =
162
0
      ada::parse<ada::url_aggregator>(EncodePathChars(input_str, os), nullptr);
163
164
0
  if (!out) {
165
0
    return ThrowInvalidURL(realm->env(), input.ToStringView(), std::nullopt);
166
0
  }
167
168
0
  if (os == OS::WINDOWS && args.Length() > 2 && !args[2]->IsUndefined())
169
0
      [[unlikely]] {
170
0
    CHECK(args[2]->IsString());
171
0
    Utf8Value hostname(isolate, args[2]);
172
0
    CHECK(out->set_hostname(hostname.ToStringView()));
173
0
  }
174
175
0
  binding_data->UpdateComponents(out->get_components(), out->type);
176
177
0
  Local<Value> ret;
178
0
  if (ToV8Value(realm->context(), out->get_href(), isolate).ToLocal(&ret))
179
0
      [[likely]] {
180
0
    args.GetReturnValue().Set(ret);
181
0
  }
182
0
}
183
184
0
void BindingData::DomainToASCII(const FunctionCallbackInfo<Value>& args) {
185
0
  Environment* env = Environment::GetCurrent(args);
186
0
  CHECK_GE(args.Length(), 1);  // input
187
0
  CHECK(args[0]->IsString());
188
189
0
  Utf8Value input(env->isolate(), args[0]);
190
0
  if (input.ToStringView().empty()) {
191
0
    return args.GetReturnValue().SetEmptyString();
192
0
  }
193
194
  // It is important to have an initial value that contains a special scheme.
195
  // Since it will change the implementation of `set_hostname` according to URL
196
  // spec.
197
0
  auto out = ada::parse<ada::url>("ws://x");
198
0
  DCHECK(out);
199
0
  if (!out->set_hostname(input.ToStringView())) {
200
0
    return args.GetReturnValue().Set(String::Empty(env->isolate()));
201
0
  }
202
0
  std::string host = out->get_hostname();
203
204
0
  Local<Value> ret;
205
0
  if (ToV8Value(env->context(), host, env->isolate()).ToLocal(&ret))
206
0
      [[likely]] {
207
0
    args.GetReturnValue().Set(ret);
208
0
  }
209
0
}
210
211
0
void BindingData::DomainToUnicode(const FunctionCallbackInfo<Value>& args) {
212
0
  Environment* env = Environment::GetCurrent(args);
213
0
  CHECK_GE(args.Length(), 1);  // input
214
0
  CHECK(args[0]->IsString());
215
216
0
  Utf8Value input(env->isolate(), args[0]);
217
0
  if (input.ToStringView().empty()) {
218
0
    return args.GetReturnValue().SetEmptyString();
219
0
  }
220
221
  // It is important to have an initial value that contains a special scheme.
222
  // Since it will change the implementation of `set_hostname` according to URL
223
  // spec.
224
0
  auto out = ada::parse<ada::url>("ws://x");
225
0
  DCHECK(out);
226
0
  if (!out->set_hostname(input.ToStringView())) {
227
0
    return args.GetReturnValue().Set(String::Empty(env->isolate()));
228
0
  }
229
0
  std::string result = ada::idna::to_unicode(out->get_hostname());
230
231
0
  Local<Value> ret;
232
0
  if (ToV8Value(env->context(), result, env->isolate()).ToLocal(&ret))
233
0
      [[likely]] {
234
0
    args.GetReturnValue().Set(ret);
235
0
  }
236
0
}
237
238
0
void BindingData::GetOrigin(const FunctionCallbackInfo<Value>& args) {
239
0
  CHECK_GE(args.Length(), 1);
240
0
  CHECK(args[0]->IsString());  // input
241
242
0
  Environment* env = Environment::GetCurrent(args);
243
0
  HandleScope handle_scope(env->isolate());
244
245
0
  Utf8Value input(env->isolate(), args[0]);
246
0
  std::string_view input_view = input.ToStringView();
247
0
  auto out = ada::parse<ada::url_aggregator>(input_view);
248
249
0
  if (!out) {
250
0
    THROW_ERR_INVALID_URL(env, "Invalid URL");
251
0
    return;
252
0
  }
253
254
0
  std::string origin = out->get_origin();
255
256
0
  Local<Value> ret;
257
0
  if (ToV8Value(env->context(), origin, env->isolate()).ToLocal(&ret))
258
0
      [[likely]] {
259
0
    args.GetReturnValue().Set(ret);
260
0
  }
261
0
}
262
263
0
void BindingData::CanParse(const FunctionCallbackInfo<Value>& args) {
264
0
  CHECK_GE(args.Length(), 1);
265
0
  CHECK(args[0]->IsString());  // input
266
  // args[1] // base url
267
268
0
  Environment* env = Environment::GetCurrent(args);
269
0
  HandleScope handle_scope(env->isolate());
270
271
0
  Utf8Value input(env->isolate(), args[0]);
272
0
  std::string_view input_view = input.ToStringView();
273
274
0
  bool can_parse{};
275
0
  if (args[1]->IsString()) {
276
0
    Utf8Value base(env->isolate(), args[1]);
277
0
    std::string_view base_view = base.ToStringView();
278
0
    can_parse = ada::can_parse(input_view, &base_view);
279
0
  } else {
280
0
    can_parse = ada::can_parse(input_view);
281
0
  }
282
283
0
  args.GetReturnValue().Set(can_parse);
284
0
}
285
286
bool BindingData::FastCanParse(
287
    Local<Value> receiver,
288
    Local<Value> input,
289
    // NOLINTNEXTLINE(runtime/references) This is V8 api.
290
0
    FastApiCallbackOptions& options) {
291
0
  TRACK_V8_FAST_API_CALL("url.canParse");
292
0
  auto isolate = options.isolate;
293
0
  HandleScope handleScope(isolate);
294
0
  Local<String> str;
295
0
  if (!input->ToString(isolate->GetCurrentContext()).ToLocal(&str)) {
296
0
    return false;
297
0
  }
298
0
  Utf8Value utf8(isolate, str);
299
0
  return ada::can_parse(utf8.ToStringView());
300
0
}
301
302
bool BindingData::FastCanParseWithBase(
303
    Local<Value> receiver,
304
    Local<Value> input,
305
    Local<Value> base,
306
    // NOLINTNEXTLINE(runtime/references) This is V8 api.
307
0
    FastApiCallbackOptions& options) {
308
0
  TRACK_V8_FAST_API_CALL("url.canParse.withBase");
309
0
  auto isolate = options.isolate;
310
0
  HandleScope handleScope(isolate);
311
0
  auto context = isolate->GetCurrentContext();
312
0
  Local<String> input_str;
313
0
  if (!input->ToString(context).ToLocal(&input_str)) {
314
0
    return false;
315
0
  }
316
0
  Local<String> base_str;
317
0
  if (!base->ToString(context).ToLocal(&base_str)) {
318
0
    return false;
319
0
  }
320
0
  Utf8Value input_utf8(isolate, input_str);
321
0
  Utf8Value base_utf8(isolate, base_str);
322
323
0
  auto base_view = base_utf8.ToStringView();
324
0
  return ada::can_parse(input_utf8.ToStringView(), &base_view);
325
0
}
326
327
CFunction BindingData::fast_can_parse_methods_[] = {
328
    CFunction::Make(FastCanParse), CFunction::Make(FastCanParseWithBase)};
329
330
0
void BindingData::Format(const FunctionCallbackInfo<Value>& args) {
331
0
  CHECK_GT(args.Length(), 4);
332
0
  CHECK(args[0]->IsString());  // url href
333
334
0
  Environment* env = Environment::GetCurrent(args);
335
0
  Isolate* isolate = env->isolate();
336
337
0
  Utf8Value href(isolate, args[0].As<String>());
338
0
  const bool hash = args[1]->IsTrue();
339
0
  const bool unicode = args[2]->IsTrue();
340
0
  const bool search = args[3]->IsTrue();
341
0
  const bool auth = args[4]->IsTrue();
342
343
  // ada::url provides a faster alternative to ada::url_aggregator if we
344
  // directly want to manipulate the url components without using the respective
345
  // setters. therefore we are using ada::url here.
346
0
  auto out = ada::parse<ada::url>(href.ToStringView());
347
0
  CHECK(out);
348
349
0
  if (!hash) {
350
0
    out->hash = std::nullopt;
351
0
  }
352
353
0
  if (unicode && out->has_hostname()) {
354
0
    out->host = ada::idna::to_unicode(out->get_hostname());
355
0
  }
356
357
0
  if (!search) {
358
0
    out->query = std::nullopt;
359
0
  }
360
361
0
  if (!auth) {
362
0
    out->username = "";
363
0
    out->password = "";
364
0
  }
365
366
0
  std::string result = out->get_href();
367
368
0
  Local<Value> ret;
369
0
  if (ToV8Value(env->context(), result, env->isolate()).ToLocal(&ret))
370
0
      [[likely]] {
371
0
    args.GetReturnValue().Set(ret);
372
0
  }
373
0
}
374
375
0
void BindingData::Parse(const FunctionCallbackInfo<Value>& args) {
376
0
  CHECK_GE(args.Length(), 1);
377
0
  CHECK(args[0]->IsString());  // input
378
  // args[1] // base url
379
  // args[2] // raise Exception
380
381
0
  const bool raise_exception = args.Length() > 2 && args[2]->IsTrue();
382
383
0
  Realm* realm = Realm::GetCurrent(args);
384
0
  BindingData* binding_data = realm->GetBindingData<BindingData>();
385
0
  Isolate* isolate = realm->isolate();
386
0
  std::optional<std::string> base_{};
387
388
0
  Utf8Value input(isolate, args[0]);
389
0
  ada::result<ada::url_aggregator> base;
390
0
  ada::url_aggregator* base_pointer = nullptr;
391
0
  if (args[1]->IsString()) {
392
0
    base_ = Utf8Value(isolate, args[1]).ToString();
393
0
    base = ada::parse<ada::url_aggregator>(*base_);
394
0
    if (!base && raise_exception) {
395
0
      return ThrowInvalidURL(realm->env(), input.ToStringView(), base_);
396
0
    } else if (!base) {
397
0
      return;
398
0
    }
399
0
    base_pointer = &base.value();
400
0
  }
401
0
  auto out =
402
0
      ada::parse<ada::url_aggregator>(input.ToStringView(), base_pointer);
403
404
0
  if (!out && raise_exception) {
405
0
    return ThrowInvalidURL(realm->env(), input.ToStringView(), base_);
406
0
  } else if (!out) {
407
0
    return;
408
0
  }
409
410
0
  binding_data->UpdateComponents(out->get_components(), out->type);
411
412
0
  Local<Value> ret;
413
0
  if (ToV8Value(realm->context(), out->get_href(), isolate).ToLocal(&ret))
414
0
      [[likely]] {
415
0
    args.GetReturnValue().Set(ret);
416
0
  }
417
0
}
418
419
0
void BindingData::Update(const FunctionCallbackInfo<Value>& args) {
420
0
  CHECK(args[0]->IsString());    // href
421
0
  CHECK(args[1]->IsNumber());    // action type
422
0
  CHECK(args[2]->IsString());    // new value
423
424
0
  Realm* realm = Realm::GetCurrent(args);
425
0
  BindingData* binding_data = realm->GetBindingData<BindingData>();
426
0
  Isolate* isolate = realm->isolate();
427
428
0
  uint32_t val;
429
0
  if (!args[1]->Uint32Value(realm->context()).To(&val)) {
430
0
    return;
431
0
  }
432
0
  enum url_update_action action = static_cast<enum url_update_action>(val);
433
0
  Utf8Value input(isolate, args[0].As<String>());
434
0
  Utf8Value new_value(isolate, args[2].As<String>());
435
436
0
  std::string_view new_value_view = new_value.ToStringView();
437
0
  auto out = ada::parse<ada::url_aggregator>(input.ToStringView());
438
0
  CHECK(out);
439
440
0
  bool result{true};
441
442
0
  switch (action) {
443
0
    case kPathname: {
444
0
      result = out->set_pathname(new_value_view);
445
0
      break;
446
0
    }
447
0
    case kHash: {
448
0
      out->set_hash(new_value_view);
449
0
      break;
450
0
    }
451
0
    case kHost: {
452
0
      result = out->set_host(new_value_view);
453
0
      break;
454
0
    }
455
0
    case kHostname: {
456
0
      result = out->set_hostname(new_value_view);
457
0
      break;
458
0
    }
459
0
    case kHref: {
460
0
      result = out->set_href(new_value_view);
461
0
      break;
462
0
    }
463
0
    case kPassword: {
464
0
      result = out->set_password(new_value_view);
465
0
      break;
466
0
    }
467
0
    case kPort: {
468
0
      result = out->set_port(new_value_view);
469
0
      break;
470
0
    }
471
0
    case kProtocol: {
472
0
      result = out->set_protocol(new_value_view);
473
0
      break;
474
0
    }
475
0
    case kSearch: {
476
0
      out->set_search(new_value_view);
477
0
      break;
478
0
    }
479
0
    case kUsername: {
480
0
      result = out->set_username(new_value_view);
481
0
      break;
482
0
    }
483
0
    default:
484
0
      UNREACHABLE("Unsupported URL update action");
485
0
  }
486
487
0
  if (!result) {
488
0
    return args.GetReturnValue().Set(false);
489
0
  }
490
491
0
  binding_data->UpdateComponents(out->get_components(), out->type);
492
493
0
  Local<Value> ret;
494
0
  if (ToV8Value(realm->context(), out->get_href(), isolate).ToLocal(&ret))
495
0
      [[likely]] {
496
0
    args.GetReturnValue().Set(ret);
497
0
  }
498
0
}
499
500
void BindingData::UpdateComponents(const ada::url_components& components,
501
0
                                   const ada::scheme::type type) {
502
0
  url_components_buffer_[0] = components.protocol_end;
503
0
  url_components_buffer_[1] = components.username_end;
504
0
  url_components_buffer_[2] = components.host_start;
505
0
  url_components_buffer_[3] = components.host_end;
506
0
  url_components_buffer_[4] = components.port;
507
0
  url_components_buffer_[5] = components.pathname_start;
508
0
  url_components_buffer_[6] = components.search_start;
509
0
  url_components_buffer_[7] = components.hash_start;
510
0
  url_components_buffer_[8] = type;
511
0
  static_assert(kURLComponentsLength == 9,
512
0
                "kURLComponentsLength should be up-to-date");
513
0
}
514
515
void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data,
516
35
                                             Local<ObjectTemplate> target) {
517
35
  Isolate* isolate = isolate_data->isolate();
518
35
  SetMethodNoSideEffect(isolate, target, "domainToASCII", DomainToASCII);
519
35
  SetMethodNoSideEffect(isolate, target, "domainToUnicode", DomainToUnicode);
520
35
  SetMethodNoSideEffect(isolate, target, "format", Format);
521
35
  SetMethodNoSideEffect(isolate, target, "getOrigin", GetOrigin);
522
35
  SetMethod(isolate, target, "parse", Parse);
523
35
  SetMethod(isolate, target, "pathToFileURL", PathToFileURL);
524
35
  SetMethod(isolate, target, "update", Update);
525
35
  SetFastMethodNoSideEffect(
526
35
      isolate, target, "canParse", CanParse, {fast_can_parse_methods_, 2});
527
35
}
528
529
void BindingData::CreatePerContextProperties(Local<Object> target,
530
                                             Local<Value> unused,
531
                                             Local<Context> context,
532
35
                                             void* priv) {
533
35
  Realm* realm = Realm::GetCurrent(context);
534
35
  realm->AddBindingData<BindingData>(target);
535
35
}
536
537
void BindingData::RegisterExternalReferences(
538
0
    ExternalReferenceRegistry* registry) {
539
0
  registry->Register(DomainToASCII);
540
0
  registry->Register(DomainToUnicode);
541
0
  registry->Register(Format);
542
0
  registry->Register(GetOrigin);
543
0
  registry->Register(Parse);
544
0
  registry->Register(PathToFileURL);
545
0
  registry->Register(Update);
546
0
  registry->Register(CanParse);
547
0
  for (const CFunction& method : fast_can_parse_methods_) {
548
0
    registry->Register(method);
549
0
  }
550
0
}
551
552
void ThrowInvalidURL(node::Environment* env,
553
                     std::string_view input,
554
0
                     std::optional<std::string> base) {
555
0
  Local<Value> err = ERR_INVALID_URL(env->isolate(), "Invalid URL");
556
0
  DCHECK(err->IsObject());
557
558
0
  auto err_object = err.As<Object>();
559
560
0
  Local<Value> tmp;
561
0
  if (!ToV8Value(env->context(), input, env->isolate()).ToLocal(&tmp) ||
562
0
      err_object->Set(env->context(), env->input_string(), tmp).IsNothing())
563
0
      [[unlikely]] {
564
    // A superseding error has been thrown.
565
0
    return;
566
0
  }
567
568
0
  if (base.has_value()) {
569
0
    if (!ToV8Value(env->context(), base.value(), env->isolate())
570
0
             .ToLocal(&tmp) ||
571
0
        err_object->Set(env->context(), env->base_string(), tmp).IsNothing())
572
0
        [[unlikely]] {
573
0
      return;
574
0
    }
575
0
  }
576
577
0
  env->isolate()->ThrowException(err);
578
0
}
579
580
0
std::string FromFilePath(std::string_view file_path) {
581
  // Avoid unnecessary allocations.
582
0
  size_t pos = file_path.empty() ? std::string_view::npos : file_path.find('%');
583
0
  if (pos == std::string_view::npos) {
584
0
    return ada::href_from_file(file_path);
585
0
  }
586
  // Escape '%' characters to a temporary string.
587
0
  std::string escaped_file_path;
588
0
  do {
589
0
    escaped_file_path += file_path.substr(0, pos + 1);
590
0
    escaped_file_path += "25";
591
0
    file_path = file_path.substr(pos + 1);
592
0
    pos = file_path.empty() ? std::string_view::npos : file_path.find('%');
593
0
  } while (pos != std::string_view::npos);
594
0
  escaped_file_path += file_path;
595
0
  return ada::href_from_file(escaped_file_path);
596
0
}
597
598
std::optional<std::string> FileURLToPath(Environment* env,
599
0
                                         const ada::url_aggregator& file_url) {
600
0
  if (file_url.type != ada::scheme::FILE) {
601
0
    THROW_ERR_INVALID_URL_SCHEME(env->isolate());
602
0
    return std::nullopt;
603
0
  }
604
605
0
  std::string_view pathname = file_url.get_pathname();
606
#ifdef _WIN32
607
  size_t first_percent = std::string::npos;
608
  size_t pathname_size = pathname.size();
609
  std::string pathname_escaped_slash;
610
611
  for (size_t i = 0; i < pathname_size; i++) {
612
    if (pathname[i] == '/') {
613
      pathname_escaped_slash += '\\';
614
    } else {
615
      pathname_escaped_slash += pathname[i];
616
    }
617
618
    if (pathname[i] != '%') continue;
619
620
    if (first_percent == std::string::npos) {
621
      first_percent = i;
622
    }
623
624
    // just safe-guard against access the pathname
625
    // outside the bounds
626
    if ((i + 2) >= pathname_size) continue;
627
628
    char third = pathname[i + 2] | 0x20;
629
630
    bool is_slash = pathname[i + 1] == '2' && third == 102;
631
    bool is_forward_slash = pathname[i + 1] == '5' && third == 99;
632
633
    if (!is_slash && !is_forward_slash) continue;
634
635
    THROW_ERR_INVALID_FILE_URL_PATH(
636
        env->isolate(),
637
        "File URL path must not include encoded \\ or / characters");
638
    return std::nullopt;
639
  }
640
641
  std::string_view hostname = file_url.get_hostname();
642
  std::string decoded_pathname = ada::unicode::percent_decode(
643
      std::string_view(pathname_escaped_slash), first_percent);
644
645
  if (hostname.size() > 0) {
646
    // If hostname is set, then we have a UNC path
647
    // Pass the hostname through domainToUnicode just in case
648
    // it is an IDN using punycode encoding. We do not need to worry
649
    // about percent encoding because the URL parser will have
650
    // already taken care of that for us. Note that this only
651
    // causes IDNs with an appropriate `xn--` prefix to be decoded.
652
    return "\\\\" + ada::idna::to_unicode(hostname) + decoded_pathname;
653
  }
654
655
  char letter = decoded_pathname[1] | 0x20;
656
  char sep = decoded_pathname[2];
657
658
  // a..z A..Z
659
  if (letter < 'a' || letter > 'z' || sep != ':') {
660
    THROW_ERR_INVALID_FILE_URL_PATH(env->isolate(),
661
                                    "File URL path must be absolute");
662
    return std::nullopt;
663
  }
664
665
  return decoded_pathname.substr(1);
666
#else   // _WIN32
667
0
  std::string_view hostname = file_url.get_hostname();
668
669
0
  if (hostname.size() > 0) {
670
0
    THROW_ERR_INVALID_FILE_URL_HOST(
671
0
        env->isolate(),
672
0
        "File URL host must be \"localhost\" or empty on ",
673
0
        std::string(per_process::metadata.platform));
674
0
    return std::nullopt;
675
0
  }
676
677
0
  size_t first_percent = std::string::npos;
678
0
  for (size_t i = 0; (i + 2) < pathname.size(); i++) {
679
0
    if (pathname[i] != '%') continue;
680
681
0
    if (first_percent == std::string::npos) {
682
0
      first_percent = i;
683
0
    }
684
685
0
    if (pathname[i + 1] == '2' && (pathname[i + 2] | 0x20) == 102) {
686
0
      THROW_ERR_INVALID_FILE_URL_PATH(
687
0
          env->isolate(),
688
0
          "File URL path must not include encoded / characters");
689
0
      return std::nullopt;
690
0
    }
691
0
  }
692
693
0
  return ada::unicode::percent_decode(pathname, first_percent);
694
0
#endif  // _WIN32
695
0
}
696
697
}  // namespace url
698
699
}  // namespace node
700
701
NODE_BINDING_CONTEXT_AWARE_INTERNAL(
702
    url, node::url::BindingData::CreatePerContextProperties)
703
NODE_BINDING_PER_ISOLATE_INIT(
704
    url, node::url::BindingData::CreatePerIsolateProperties)
705
NODE_BINDING_EXTERNAL_REFERENCE(
706
    url, node::url::BindingData::RegisterExternalReferences)