File
Blob: src/node/internal/validators.ts
| 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 | import { isArrayBufferView } from 'node-internal:internal_types'; |
| 29 | import { normalizeEncoding } from 'node-internal:internal_utils'; |
| 30 | import { |
| 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'; |
| 37 | import { default as bufferUtil } from 'node-internal:buffer'; |
| 38 | |
| 39 | // TODO(someday): Not current implementing parseFileMode |
| 40 | |
| 41 | export function isInt32(value: unknown): value is number { |
| 42 | if (typeof value !== 'number') return false; |
| 43 | return value === (value | 0); |
| 44 | } |
| 45 | export function isUint32(value: unknown): value is number { |
| 46 | // @ts-expect-error Due to value being unknown |
| 47 | return value === value >>> 0; |
| 48 | } |
| 49 | |
| 50 | export 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 | |
| 63 | export 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 | |
| 80 | export function validateObject<T extends object>( |
| 81 | value: T | null | undefined, |
| 82 | name: string, |
| 83 | options?: number |
| 84 | ): asserts value is T; |
| 85 | export function validateObject( |
| 86 | value: unknown, |
| 87 | name: string, |
| 88 | options?: number |
| 89 | ): asserts value is Record<string, unknown>; |
| 90 | export 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 | |
| 128 | export 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 | |
| 151 | export 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 | |
| 172 | export 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 | |
| 181 | export 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 | |
| 195 | export 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 | |
| 218 | export 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 | |
| 227 | export 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 | |
| 245 | export 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 | |
| 261 | export 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 | |
| 273 | export 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 | |
| 282 | export 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 |
| 300 | export 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 |
| 327 | export function checkRangesOrGetDefault( |
| 328 | number: unknown, |
| 329 | name: string, |
| 330 | lower: number, |
| 331 | upper: number, |
| 332 | def: number |
| 333 | ): number; |
| 334 | export function checkRangesOrGetDefault( |
| 335 | number: unknown, |
| 336 | name: string, |
| 337 | lower: number, |
| 338 | upper: number, |
| 339 | def?: number |
| 340 | ): number | undefined; |
| 341 | export 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 | |
| 357 | export 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 | |
| 374 | const octalReg = /^[0-7]+$/; |
| 375 | const modeDesc = 'must be a 32-bit unsigned integer or an octal string'; |
| 376 | |
| 377 | export 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 | |
| 396 | export 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 | |
| 410 | export const kValidateObjectNone = 0; |
| 411 | export const kValidateObjectAllowNullable = 1 << 0; |
| 412 | export const kValidateObjectAllowArray = 1 << 1; |
| 413 | export const kValidateObjectAllowFunction = 1 << 2; |
| 414 | export const kValidateObjectAllowObjects = |
| 415 | kValidateObjectAllowArray | kValidateObjectAllowFunction; |
| 416 | export const kValidateObjectAllowObjectsAndNull = |
| 417 | kValidateObjectAllowNullable | |
| 418 | kValidateObjectAllowArray | |
| 419 | kValidateObjectAllowFunction; |
| 420 | |
| 421 | export 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 | }; |