Skip to content
File

Blob: src/node/internal/validators.ts

typescript452 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.
28import { isArrayBufferView } from 'node-internal:internal_types';
29import { normalizeEncoding } from 'node-internal:internal_utils';
30import {
31 ERR_INVALID_ARG_TYPE,
32 ERR_INVALID_ARG_VALUE,
33 ERR_SOCKET_BAD_PORT,
34 ERR_OUT_OF_RANGE,
35 ERR_INVALID_THIS,
36} from 'node-internal:internal_errors';
37import { default as bufferUtil } from 'node-internal:buffer';
38 
39// TODO(someday): Not current implementing parseFileMode
40 
41export function isInt32(value: unknown): value is number {
42 if (typeof value !== 'number') return false;
43 return value === (value | 0);
44}
45export function isUint32(value: unknown): value is number {
46 // @ts-expect-error Due to value being unknown
47 return value === value >>> 0;
48}
49 
50export function validateBuffer(
51 buffer: unknown,
52 name = 'buffer'
53): asserts buffer is Buffer {
54 if (!isArrayBufferView(buffer)) {
55 throw new ERR_INVALID_ARG_TYPE(
56 name,
57 ['Buffer', 'TypedArray', 'DataView'],
58 buffer
59 );
60 }
61}
62 
63export function validateInteger(
64 value: unknown,
65 name: string,
66 min = Number.MIN_SAFE_INTEGER,
67 max = Number.MAX_SAFE_INTEGER
68): asserts value is number {
69 if (typeof value !== 'number') {
70 throw new ERR_INVALID_ARG_TYPE(name, 'number', value);
71 }
72 if (!Number.isInteger(value)) {
73 throw new ERR_OUT_OF_RANGE(name, 'an integer', value);
74 }
75 if (value < min || value > max) {
76 throw new ERR_OUT_OF_RANGE(name, `>= ${min} && <= ${max}`, value);
77 }
78}
79 
80export function validateObject<T extends object>(
81 value: T | null | undefined,
82 name: string,
83 options?: number
84): asserts value is T;
85export function validateObject(
86 value: unknown,
87 name: string,
88 options?: number
89): asserts value is Record<string, unknown>;
90export function validateObject(
91 value: unknown,
92 name: string,
93 options: number = kValidateObjectNone
94): asserts value is Record<string, unknown> {
95 if (options === kValidateObjectNone) {
96 if (value === null || Array.isArray(value)) {
97 throw new ERR_INVALID_ARG_TYPE(name, 'Object', value);
98 }
99 
100 if (typeof value !== 'object') {
101 throw new ERR_INVALID_ARG_TYPE(name, 'Object', value);
102 }
103 } else {
104 const throwOnNullable = (kValidateObjectAllowNullable & options) === 0;
105 
106 if (throwOnNullable && value === null) {
107 throw new ERR_INVALID_ARG_TYPE(name, 'Object', value);
108 }
109 
110 const throwOnArray = (kValidateObjectAllowArray & options) === 0;
111 
112 if (throwOnArray && Array.isArray(value)) {
113 throw new ERR_INVALID_ARG_TYPE(name, 'Object', value);
114 }
115 
116 const throwOnFunction = (kValidateObjectAllowFunction & options) === 0;
117 const typeofValue = typeof value;
118 
119 if (
120 typeofValue !== 'object' &&
121 (throwOnFunction || typeofValue !== 'function')
122 ) {
123 throw new ERR_INVALID_ARG_TYPE(name, 'Object', value);
124 }
125 }
126}
127 
128export function validateInt32(
129 value: unknown,
130 name: string,
131 min = -2147483648,
132 max = 2147483647
133): asserts value is number {
134 if (!isInt32(value)) {
135 if (typeof value !== 'number') {
136 throw new ERR_INVALID_ARG_TYPE(name, 'number', value);
137 }
138 
139 if (!Number.isInteger(value)) {
140 throw new ERR_OUT_OF_RANGE(name, 'an integer', value);
141 }
142 
143 throw new ERR_OUT_OF_RANGE(name, `>= ${min} && <= ${max}`, value);
144 }
145 
146 if (value < min || value > max) {
147 throw new ERR_OUT_OF_RANGE(name, `>= ${min} && <= ${max}`, value);
148 }
149}
150 
151export function validateUint32(
152 value: unknown,
153 name: string,
154 positive?: boolean
155): asserts value is number {
156 if (!isUint32(value)) {
157 if (typeof value !== 'number') {
158 throw new ERR_INVALID_ARG_TYPE(name, 'number', value);
159 }
160 if (!Number.isInteger(value)) {
161 throw new ERR_OUT_OF_RANGE(name, 'an integer', value);
162 }
163 const min = positive ? 1 : 0;
164 // 2 ** 32 === 4294967296
165 throw new ERR_OUT_OF_RANGE(name, `>= ${min} && < 4294967296`, value);
166 }
167 if (positive && value === 0) {
168 throw new ERR_OUT_OF_RANGE(name, '>= 1 && < 4294967296', value);
169 }
170}
171 
172export function validateString(
173 value: unknown,
174 name: string
175): asserts value is string {
176 if (typeof value !== 'string') {
177 throw new ERR_INVALID_ARG_TYPE(name, 'string', value);
178 }
179}
180 
181export function validateStringArray(
182 value: unknown,
183 name: string
184): asserts value is string[] {
185 validateArray(value, name);
186 for (let i = 0; i < value.length; ++i) {
187 // Don't use validateString here for performance reasons, as
188 // we would generate intermediate strings for the name.
189 if (typeof value[i] !== 'string') {
190 throw new ERR_INVALID_ARG_TYPE(`${name}[${i}]`, 'string', value[i]);
191 }
192 }
193}
194 
195export function validateNumber(
196 value: unknown,
197 name: string,
198 min?: number,
199 max?: number
200): asserts value is number {
201 if (typeof value !== 'number') {
202 throw new ERR_INVALID_ARG_TYPE(name, 'number', value);
203 }
204 
205 if (
206 (min != null && value < min) ||
207 (max != null && value > max) ||
208 ((min != null || max != null) && Number.isNaN(value))
209 ) {
210 throw new ERR_OUT_OF_RANGE(
211 name,
212 `${min != null ? `>= ${min}` : ''}${min != null && max != null ? ' && ' : ''}${max != null ? `<= ${max}` : ''}`,
213 value
214 );
215 }
216}
217 
218export function validateBoolean(
219 value: unknown,
220 name: string
221): asserts value is boolean {
222 if (typeof value !== 'boolean') {
223 throw new ERR_INVALID_ARG_TYPE(name, 'boolean', value);
224 }
225}
226 
227export function validateOneOf<T>(
228 value: T,
229 name: string,
230 oneOf: T[]
231): asserts value is T {
232 if (!Array.prototype.includes.call(oneOf, value)) {
233 const allowed = Array.prototype.join.call(
234 Array.prototype.map.call(oneOf, (v) =>
235 typeof v === 'string' ? `'${v}'` : String(v)
236 ),
237 ', '
238 );
239 const reason = 'must be one of: ' + allowed;
240 
241 throw new ERR_INVALID_ARG_VALUE(name, value, reason);
242 }
243}
244 
245export function validateEncoding(
246 data: unknown,
247 encoding: string
248): asserts data is string {
249 const normalizedEncoding = normalizeEncoding(encoding);
250 const length = (data as any).length; // eslint-disable-line
251 
252 if (normalizedEncoding === bufferUtil.HEX && length % 2 !== 0) {
253 throw new ERR_INVALID_ARG_VALUE(
254 'encoding',
255 encoding,
256 `is invalid for data of length ${length}`
257 );
258 }
259}
260 
261export function validateAbortSignal(
262 signal: unknown,
263 name: string
264): asserts signal is AbortSignal {
265 if (
266 signal !== undefined &&
267 (signal === null || typeof signal !== 'object' || !('aborted' in signal))
268 ) {
269 throw new ERR_INVALID_ARG_TYPE(name, 'AbortSignal', signal);
270 }
271}
272 
273export function validateFunction(
274 value: unknown,
275 name: string
276): asserts value is (...args: unknown[]) => unknown {
277 if (typeof value !== 'function') {
278 throw new ERR_INVALID_ARG_TYPE(name, 'Function', value);
279 }
280}
281 
282export function validateArray(
283 value: unknown,
284 name: string,
285 minLength = 0
286): asserts value is unknown[] {
287 if (!Array.isArray(value)) {
288 throw new ERR_INVALID_ARG_TYPE(name, 'Array', value);
289 }
290 if (value.length < minLength) {
291 const reason = `must be longer than ${minLength}`;
292 throw new ERR_INVALID_ARG_VALUE(name, value, reason);
293 }
294}
295 
296// 1. Returns false for undefined and NaN
297// 2. Returns true for finite numbers
298// 3. Throws ERR_INVALID_ARG_TYPE for non-numbers
299// 4. Throws ERR_OUT_OF_RANGE for infinite numbers
300export function checkFiniteNumber(
301 number: unknown,
302 name: string
303): number is number {
304 // Common case
305 if (number === undefined) {
306 return false;
307 }
308 
309 if (Number.isFinite(number)) {
310 return true; // Is a valid number
311 }
312 
313 if (Number.isNaN(number)) {
314 return false;
315 }
316 
317 validateNumber(number, name);
318 
319 // Infinite numbers
320 throw new ERR_OUT_OF_RANGE(name, 'a finite number', number);
321}
322 
323// 1. Returns def for number when it's undefined or NaN
324// 2. Returns number for finite numbers >= lower and <= upper
325// 3. Throws ERR_INVALID_ARG_TYPE for non-numbers
326// 4. Throws ERR_OUT_OF_RANGE for infinite numbers or numbers > upper or < lower
327export function checkRangesOrGetDefault(
328 number: unknown,
329 name: string,
330 lower: number,
331 upper: number,
332 def: number
333): number;
334export function checkRangesOrGetDefault(
335 number: unknown,
336 name: string,
337 lower: number,
338 upper: number,
339 def?: number
340): number | undefined;
341export function checkRangesOrGetDefault(
342 number: unknown,
343 name: string,
344 lower: number,
345 upper: number,
346 def?: number
347): number | undefined {
348 if (!checkFiniteNumber(number, name)) {
349 return def;
350 }
351 if (number < lower || number > upper) {
352 throw new ERR_OUT_OF_RANGE(name, `>= ${lower} and <= ${upper}`, number);
353 }
354 return number;
355}
356 
357export function validatePort(
358 port: unknown,
359 name = 'Port',
360 allowZero = true
361): number {
362 if (
363 (typeof port !== 'number' && typeof port !== 'string') ||
364 (typeof port === 'string' && port.trim().length === 0) ||
365 +port !== +port >>> 0 ||
366 +port > 0xffff ||
367 (port === 0 && !allowZero)
368 ) {
369 throw new ERR_SOCKET_BAD_PORT(name, port, allowZero);
370 }
371 return +port | 0;
372}
373 
374const octalReg = /^[0-7]+$/;
375const modeDesc = 'must be a 32-bit unsigned integer or an octal string';
376 
377export function parseFileMode(
378 value: unknown,
379 name: string,
380 def?: number
381): number {
382 if (def != null) {
383 value ??= def;
384 }
385 if (typeof value === 'string') {
386 if (!octalReg.test(value)) {
387 throw new ERR_INVALID_ARG_VALUE(name, value, modeDesc);
388 }
389 value = Number.parseInt(value, 8);
390 }
391 
392 validateUint32(value, name);
393 return value;
394}
395 
396export function validateThisInternalField(
397 object: object | null,
398 fieldKey: string | symbol,
399 className: string
400): void {
401 if (
402 typeof object !== 'object' ||
403 object === null ||
404 !Object.prototype.hasOwnProperty.call(object, fieldKey)
405 ) {
406 throw new ERR_INVALID_THIS(className);
407 }
408}
409 
410export const kValidateObjectNone = 0;
411export const kValidateObjectAllowNullable = 1 << 0;
412export const kValidateObjectAllowArray = 1 << 1;
413export const kValidateObjectAllowFunction = 1 << 2;
414export const kValidateObjectAllowObjects =
415 kValidateObjectAllowArray | kValidateObjectAllowFunction;
416export const kValidateObjectAllowObjectsAndNull =
417 kValidateObjectAllowNullable |
418 kValidateObjectAllowArray |
419 kValidateObjectAllowFunction;
420 
421export default {
422 isInt32,
423 isUint32,
424 validateAbortSignal,
425 validateArray,
426 validateBoolean,
427 validateBuffer,
428 validateFunction,
429 validateInt32,
430 validateInteger,
431 validateNumber,
432 validateObject,
433 validateOneOf,
434 validateString,
435 validateUint32,
436 validatePort,
437 validateThisInternalField,
438 
439 // Zlib specific
440 checkFiniteNumber,
441 checkRangesOrGetDefault,
442 
443 // Filesystem specific
444 parseFileMode,
445 kValidateObjectNone,
446 kValidateObjectAllowNullable,
447 kValidateObjectAllowArray,
448 kValidateObjectAllowFunction,
449 kValidateObjectAllowObjects,
450 kValidateObjectAllowObjectsAndNull,
451};