Skip to content
File

Blob: src/node/internal/internal_utils.ts

typescript488 lines
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4//
5// Adapted from Deno and Node.js:
6// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
7//
8// Adapted from Node.js. Copyright Joyent, Inc. and other Node contributors.
9//
10// Permission is hereby granted, free of charge, to any person obtaining a
11// copy of this software and associated documentation files (the
12// "Software"), to deal in the Software without restriction, including
13// without limitation the rights to use, copy, modify, merge, publish,
14// distribute, sublicense, and/or sell copies of the Software, and to permit
15// persons to whom the Software is furnished to do so, subject to the
16// following conditions:
17//
18// The above copyright notice and this permission notice shall be included
19// in all copies or substantial portions of the Software.
20//
21// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
22// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
23// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
24// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
25// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
26// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
27// USE OR OTHER DEALINGS IN THE SOFTWARE.
28 
29import { default as bufferUtil } from 'node-internal:buffer';
30import type { Encoding } from 'node-internal:buffer';
31import { validateFunction, validateString } from 'node-internal:validators';
32import {
33 ERR_FALSY_VALUE_REJECTION,
34 type Falsy,
35} from 'node-internal:internal_errors';
36 
37const { UTF8, UTF16LE, HEX, ASCII, BASE64, BASE64URL, LATIN1 } = bufferUtil;
38 
39export function normalizeEncoding(enc?: string): Encoding | undefined {
40 if (
41 enc == null ||
42 enc === 'utf8' ||
43 enc === 'utf-8' ||
44 enc === 'UTF8' ||
45 enc === 'UTF-8'
46 )
47 return UTF8;
48 return getEncodingOps(enc);
49}
50 
51export function getEncodingOps(enc: unknown): Encoding | undefined {
52 if (enc === undefined) return UTF8;
53 // eslint-disable-next-line @typescript-eslint/no-base-to-string
54 let encoding = `${enc}`;
55 switch (encoding.length) {
56 case 4:
57 if (encoding === 'UTF8') return UTF8;
58 if (encoding === 'ucs2' || encoding === 'UCS2') return UTF16LE;
59 encoding = encoding.toLowerCase();
60 if (encoding === 'utf8') return UTF8;
61 if (encoding === 'ucs2') return UTF16LE;
62 break;
63 case 3:
64 if (
65 encoding === 'hex' ||
66 encoding === 'HEX' ||
67 encoding.toLowerCase() === 'hex'
68 ) {
69 return HEX;
70 }
71 break;
72 case 5:
73 if (encoding === 'ascii') return ASCII;
74 if (encoding === 'ucs-2') return UTF16LE;
75 if (encoding === 'UTF-8') return UTF8;
76 if (encoding === 'ASCII') return ASCII;
77 if (encoding === 'UCS-2') return UTF16LE;
78 encoding = encoding.toLowerCase();
79 if (encoding === 'utf-8') return UTF8;
80 if (encoding === 'ascii') return ASCII;
81 if (encoding === 'ucs-2') return UTF16LE;
82 break;
83 case 6:
84 if (encoding === 'base64') return BASE64;
85 if (encoding === 'latin1' || encoding === 'binary') return LATIN1;
86 if (encoding === 'BASE64') return BASE64;
87 if (encoding === 'LATIN1' || encoding === 'BINARY') return LATIN1;
88 encoding = encoding.toLowerCase();
89 if (encoding === 'base64') return BASE64;
90 if (encoding === 'latin1' || encoding === 'binary') return LATIN1;
91 break;
92 case 7:
93 if (
94 encoding === 'utf16le' ||
95 encoding === 'UTF16LE' ||
96 encoding.toLowerCase() === 'utf16le'
97 ) {
98 return UTF16LE;
99 }
100 break;
101 case 8:
102 if (
103 encoding === 'utf-16le' ||
104 encoding === 'UTF-16LE' ||
105 encoding.toLowerCase() === 'utf-16le'
106 ) {
107 return UTF16LE;
108 }
109 break;
110 case 9:
111 if (
112 encoding === 'base64url' ||
113 encoding === 'BASE64URL' ||
114 encoding.toLowerCase() === 'base64url'
115 ) {
116 return BASE64URL;
117 }
118 break;
119 default:
120 if (encoding === '') return UTF8;
121 }
122 return undefined;
123}
124 
125export function spliceOne(list: unknown[], index: number): void {
126 for (; index + 1 < list.length; index++) list[index] = list[index + 1];
127 list.pop();
128}
129 
130export const ALL_PROPERTIES = 0;
131export const ONLY_WRITABLE = 1;
132export const ONLY_ENUMERABLE = 2;
133export const ONLY_CONFIGURABLE = 4;
134export const ONLY_ENUM_WRITABLE = 6;
135export const SKIP_STRINGS = 8;
136export const SKIP_SYMBOLS = 16;
137 
138const isNumericLookup: Record<string, boolean> = {};
139export function isArrayIndex(value: unknown): value is number | string {
140 switch (typeof value) {
141 case 'number':
142 return value >= 0 && (value | 0) === value;
143 case 'string': {
144 const result = isNumericLookup[value];
145 if (result !== void 0) {
146 return result;
147 }
148 const length = value.length;
149 if (length === 0) {
150 return (isNumericLookup[value] = false);
151 }
152 let ch = 0;
153 let i = 0;
154 for (; i < length; ++i) {
155 ch = value.charCodeAt(i);
156 if (
157 (i === 0 && ch === 0x30 && length > 1) /* must not start with 0 */ ||
158 ch < 0x30 /* 0 */ ||
159 ch > 0x39 /* 9 */
160 ) {
161 return (isNumericLookup[value] = false);
162 }
163 }
164 return (isNumericLookup[value] = true);
165 }
166 default:
167 return false;
168 }
169}
170 
171export function getOwnNonIndexProperties(
172 // deno-lint-ignore ban-types
173 obj: object,
174 filter: number
175): (string | symbol)[] {
176 let allProperties = [
177 ...Object.getOwnPropertyNames(obj),
178 ...Object.getOwnPropertySymbols(obj),
179 ];
180 
181 if (Array.isArray(obj)) {
182 allProperties = allProperties.filter((k) => !isArrayIndex(k));
183 }
184 
185 if (filter === ALL_PROPERTIES) {
186 return allProperties;
187 }
188 
189 const result: (string | symbol)[] = [];
190 for (const key of allProperties) {
191 const desc = Object.getOwnPropertyDescriptor(obj, key);
192 if (desc === undefined) {
193 continue;
194 }
195 if (filter & ONLY_WRITABLE && !desc.writable) {
196 continue;
197 }
198 if (filter & ONLY_ENUMERABLE && !desc.enumerable) {
199 continue;
200 }
201 if (filter & ONLY_CONFIGURABLE && !desc.configurable) {
202 continue;
203 }
204 if (filter & SKIP_STRINGS && typeof key === 'string') {
205 continue;
206 }
207 if (filter & SKIP_SYMBOLS && typeof key === 'symbol') {
208 continue;
209 }
210 result.push(key);
211 }
212 return result;
213}
214 
215function callbackifyOnRejected(
216 reason: unknown,
217 cb: (error?: unknown) => void
218): void {
219 // `!reason` guard inspired by bluebird (https://github.com/petkaantonov/bluebird/blob/2207fae3572f03b089bc92d3a6cefdd278cff7ab/src/nodeify.js#L30-L43).
220 // Because `null` is a special error value in callbacks which means "no error
221 // occurred", we error-wrap so the callback consumer can distinguish between
222 // "the promise rejected with null" or "the promise fulfilled with undefined".
223 if (!reason) {
224 reason = new ERR_FALSY_VALUE_REJECTION(reason as Falsy);
225 }
226 cb(reason);
227}
228 
229// Types are taken from @types/node package
230// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/dccb0e78c4d3265ae06985789156451bd73312c0/types/node/util.d.ts#L1054
231export function callbackify(
232 fn: () => Promise<void>
233): (callback: (err: Error) => void) => void;
234export function callbackify<TResult>(
235 fn: () => Promise<TResult>
236): (callback: (err: Error, result: TResult) => void) => void;
237export function callbackify<T1>(
238 fn: (arg1: T1) => Promise<void>
239): (arg1: T1, callback: (err: Error) => void) => void;
240export function callbackify<T1, TResult>(
241 fn: (arg1: T1) => Promise<TResult>
242): (arg1: T1, callback: (err: Error, result: TResult) => void) => void;
243export function callbackify<T1, T2>(
244 fn: (arg1: T1, arg2: T2) => Promise<void>
245): (arg1: T1, arg2: T2, callback: (err: Error) => void) => void;
246export function callbackify<T1, T2, TResult>(
247 fn: (arg1: T1, arg2: T2) => Promise<TResult>
248): (
249 arg1: T1,
250 arg2: T2,
251 callback: (err: Error | null, result: TResult) => void
252) => void;
253export function callbackify<T1, T2, T3>(
254 fn: (arg1: T1, arg2: T2, arg3: T3) => Promise<void>
255): (arg1: T1, arg2: T2, arg3: T3, callback: (err: Error) => void) => void;
256export function callbackify<T1, T2, T3, TResult>(
257 fn: (arg1: T1, arg2: T2, arg3: T3) => Promise<TResult>
258): (
259 arg1: T1,
260 arg2: T2,
261 arg3: T3,
262 callback: (err: Error | null, result: TResult) => void
263) => void;
264export function callbackify<T1, T2, T3, T4>(
265 fn: (arg1: T1, arg2: T2, arg3: T3, arg4: T4) => Promise<void>
266): (
267 arg1: T1,
268 arg2: T2,
269 arg3: T3,
270 arg4: T4,
271 callback: (err: Error) => void
272) => void;
273export function callbackify<T1, T2, T3, T4, TResult>(
274 fn: (arg1: T1, arg2: T2, arg3: T3, arg4: T4) => Promise<TResult>
275): (
276 arg1: T1,
277 arg2: T2,
278 arg3: T3,
279 arg4: T4,
280 callback: (err: Error | null, result: TResult) => void
281) => void;
282export function callbackify<T1, T2, T3, T4, T5>(
283 fn: (arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5) => Promise<void>
284): (
285 arg1: T1,
286 arg2: T2,
287 arg3: T3,
288 arg4: T4,
289 arg5: T5,
290 callback: (err: Error) => void
291) => void;
292export function callbackify<T1, T2, T3, T4, T5, TResult>(
293 fn: (arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5) => Promise<TResult>
294): (
295 arg1: T1,
296 arg2: T2,
297 arg3: T3,
298 arg4: T4,
299 arg5: T5,
300 callback: (err: Error | null, result: TResult) => void
301) => void;
302export function callbackify<T1, T2, T3, T4, T5, T6>(
303 fn: (
304 arg1: T1,
305 arg2: T2,
306 arg3: T3,
307 arg4: T4,
308 arg5: T5,
309 arg6: T6
310 ) => Promise<void>
311): (
312 arg1: T1,
313 arg2: T2,
314 arg3: T3,
315 arg4: T4,
316 arg5: T5,
317 arg6: T6,
318 callback: (err: Error) => void
319) => void;
320export function callbackify<T1, T2, T3, T4, T5, T6, TResult>(
321 fn: (
322 arg1: T1,
323 arg2: T2,
324 arg3: T3,
325 arg4: T4,
326 arg5: T5,
327 arg6: T6
328 ) => Promise<TResult>
329): (
330 arg1: T1,
331 arg2: T2,
332 arg3: T3,
333 arg4: T4,
334 arg5: T5,
335 arg6: T6,
336 callback: (err: Error | null, result: TResult) => void
337) => void;
338export function callbackify<T extends (...args: unknown[]) => Promise<unknown>>(
339 original: T
340): T extends (...args: infer TArgs) => Promise<infer TReturn>
341 ? (...params: [...TArgs, (err: Error, ret: TReturn) => unknown]) => void
342 : never {
343 validateFunction(original, 'original');
344 
345 function callbackified(
346 this: unknown,
347 ...args: [...unknown[], (err: unknown, ret: unknown) => void]
348 ): void {
349 const maybeCb = args.pop();
350 validateFunction(maybeCb, 'last argument');
351 const cb = maybeCb.bind(this);
352 Reflect.apply(original, this, args).then(
353 (ret: unknown) => {
354 queueMicrotask(() => cb(null, ret));
355 },
356 (rej: unknown) => {
357 queueMicrotask(() => {
358 callbackifyOnRejected(rej, cb);
359 });
360 }
361 );
362 }
363 
364 const descriptors = Object.getOwnPropertyDescriptors(original);
365 if (typeof descriptors.length?.value === 'number') {
366 descriptors.length.value++;
367 }
368 if (typeof descriptors.name?.value === 'string') {
369 descriptors.name.value += 'Callbackified';
370 }
371 const propertiesValues = Object.values(descriptors);
372 for (let i = 0; i < propertiesValues.length; i++) {
373 Object.setPrototypeOf(propertiesValues[i], null);
374 }
375 Object.defineProperties(callbackified, descriptors);
376 // eslint-disable-next-line @typescript-eslint/ban-ts-comment
377 // @ts-expect-error
378 return callbackified;
379}
380 
381export function parseEnv(content: string): Record<string, string> {
382 validateString(content, 'content');
383 
384 const result: Record<string, string> = {};
385 const lines = content.split('\n');
386 
387 for (let i = 0; i < lines.length; i++) {
388 let line = lines[i];
389 if (line === undefined) continue;
390 
391 if (!line.trim()) continue;
392 if (line.trimStart().startsWith('#')) continue;
393 if (line.trimStart().startsWith('export '))
394 line = line.substring(line.indexOf('export ') + 7);
395 
396 const equalIndex = line.indexOf('=');
397 if (equalIndex === -1) continue;
398 
399 const key = line.substring(0, equalIndex).trim();
400 if (!key) continue;
401 
402 let value = line.substring(equalIndex + 1).trimStart();
403 if (value.length > 0) {
404 const maybeQuote = value[0];
405 if (maybeQuote === '"' || maybeQuote === "'" || maybeQuote === '`') {
406 // Check if the closing quote is on the same line
407 const closeIndex = value.indexOf(maybeQuote, 1);
408 
409 if (closeIndex !== -1) {
410 // Found closing quote on same line
411 value = value.substring(1, closeIndex);
412 // Only handle escape sequences for double quotes
413 if (maybeQuote === '"') value = value.replace(/\\n/g, '\n');
414 // For single quotes and backticks, keep \n as literal
415 } else {
416 // Check for multiline strings
417 let fullValue = value.substring(1); // Remove opening quote
418 let currentLine = i;
419 let foundClosingQuote = false;
420 
421 // Look for closing quote in subsequent lines
422 while (currentLine < lines.length - 1) {
423 currentLine++;
424 const nextLine = lines[currentLine];
425 if (nextLine !== undefined) {
426 const closeInNextLine = nextLine.indexOf(maybeQuote);
427 if (closeInNextLine !== -1) {
428 // Found closing quote
429 fullValue += '\n' + nextLine.substring(0, closeInNextLine);
430 value = fullValue;
431 
432 // Only handle escape sequences for double quotes
433 if (maybeQuote === '"') {
434 value = value.replace(/\\n/g, '\n');
435 }
436 
437 foundClosingQuote = true;
438 i = currentLine; // Update line counter
439 break;
440 } else {
441 // Continue building multiline value
442 fullValue += '\n' + nextLine;
443 }
444 }
445 }
446 
447 if (!foundClosingQuote) {
448 if (value.length === 1) {
449 // Just the quote character, return it as the value
450 value = maybeQuote;
451 } else {
452 // Return content after the opening quote
453 value = value.substring(1);
454 }
455 }
456 }
457 } else {
458 const hashIndex = value.indexOf('#');
459 if (hashIndex !== -1) value = value.substring(0, hashIndex);
460 value = value.trimEnd();
461 }
462 }
463 result[key] = value;
464 }
465 return result;
466}
467 
468export type NonEmptyArray<T> = [T, ...T[]];
469export type PositiveInteger<T extends number> = T extends 0
470 ? never
471 : `${T}` extends `${infer _}.${infer _}`
472 ? never
473 : `${T}` extends `-${infer _}`
474 ? never
475 : T;
476export type FixedLengthArray<
477 T,
478 Length extends number,
479 Accumulator extends NonEmptyArray<T> = [T],
480> =
481 Length extends PositiveInteger<Length>
482 ? number extends Length
483 ? NonEmptyArray<T>
484 : Length extends Accumulator['length']
485 ? Accumulator
486 : FixedLengthArray<T, Length, [T, ...Accumulator]>
487 : never;