node/lib/internal/validators.js
Yagiz Nizipli ba07067b92
zlib: simplify validators
PR-URL: https://github.com/nodejs/node/pull/54442
Reviewed-By: James M Snell <jasnell@gmail.com>
2024-08-27 14:58:36 +00:00

648 lines
18 KiB
JavaScript

/* eslint jsdoc/require-jsdoc: "error" */
'use strict';
const {
ArrayIsArray,
ArrayPrototypeIncludes,
ArrayPrototypeJoin,
ArrayPrototypeMap,
NumberIsFinite,
NumberIsInteger,
NumberIsNaN,
NumberMAX_SAFE_INTEGER,
NumberMIN_SAFE_INTEGER,
NumberParseInt,
ObjectPrototypeHasOwnProperty,
RegExpPrototypeExec,
String,
StringPrototypeToUpperCase,
StringPrototypeTrim,
} = primordials;
const {
codes: {
ERR_INVALID_ARG_TYPE: { HideStackFramesError: ERR_INVALID_ARG_TYPE },
ERR_INVALID_ARG_VALUE: { HideStackFramesError: ERR_INVALID_ARG_VALUE },
ERR_OUT_OF_RANGE: { HideStackFramesError: ERR_OUT_OF_RANGE },
ERR_SOCKET_BAD_PORT: { HideStackFramesError: ERR_SOCKET_BAD_PORT },
ERR_UNKNOWN_SIGNAL: { HideStackFramesError: ERR_UNKNOWN_SIGNAL },
},
hideStackFrames,
} = require('internal/errors');
const { normalizeEncoding } = require('internal/util');
const {
isAsyncFunction,
isArrayBufferView,
} = require('internal/util/types');
const { signals } = internalBinding('constants').os;
/**
* @param {*} value
* @returns {boolean}
*/
function isInt32(value) {
return value === (value | 0);
}
/**
* @param {*} value
* @returns {boolean}
*/
function isUint32(value) {
return value === (value >>> 0);
}
const octalReg = /^[0-7]+$/;
const modeDesc = 'must be a 32-bit unsigned integer or an octal string';
/**
* Parse and validate values that will be converted into mode_t (the S_*
* constants). Only valid numbers and octal strings are allowed. They could be
* converted to 32-bit unsigned integers or non-negative signed integers in the
* C++ land, but any value higher than 0o777 will result in platform-specific
* behaviors.
* @param {*} value Values to be validated
* @param {string} name Name of the argument
* @param {number} [def] If specified, will be returned for invalid values
* @returns {number}
*/
function parseFileMode(value, name, def) {
value ??= def;
if (typeof value === 'string') {
if (RegExpPrototypeExec(octalReg, value) === null) {
throw new ERR_INVALID_ARG_VALUE(name, value, modeDesc);
}
value = NumberParseInt(value, 8);
}
validateUint32(value, name);
return value;
}
/**
* @callback validateInteger
* @param {*} value
* @param {string} name
* @param {number} [min]
* @param {number} [max]
* @returns {asserts value is number}
*/
/** @type {validateInteger} */
const validateInteger = hideStackFrames(
(value, name, min = NumberMIN_SAFE_INTEGER, max = NumberMAX_SAFE_INTEGER) => {
if (typeof value !== 'number')
throw new ERR_INVALID_ARG_TYPE(name, 'number', value);
if (!NumberIsInteger(value))
throw new ERR_OUT_OF_RANGE(name, 'an integer', value);
if (value < min || value > max)
throw new ERR_OUT_OF_RANGE(name, `>= ${min} && <= ${max}`, value);
},
);
/**
* @callback validateInt32
* @param {*} value
* @param {string} name
* @param {number} [min]
* @param {number} [max]
* @returns {asserts value is number}
*/
/** @type {validateInt32} */
const validateInt32 = hideStackFrames(
(value, name, min = -2147483648, max = 2147483647) => {
// The defaults for min and max correspond to the limits of 32-bit integers.
if (typeof value !== 'number') {
throw new ERR_INVALID_ARG_TYPE(name, 'number', value);
}
if (!NumberIsInteger(value)) {
throw new ERR_OUT_OF_RANGE(name, 'an integer', value);
}
if (value < min || value > max) {
throw new ERR_OUT_OF_RANGE(name, `>= ${min} && <= ${max}`, value);
}
},
);
/**
* @callback validateUint32
* @param {*} value
* @param {string} name
* @param {boolean} [positive=false]
* @returns {asserts value is number}
*/
/** @type {validateUint32} */
const validateUint32 = hideStackFrames((value, name, positive = false) => {
if (typeof value !== 'number') {
throw new ERR_INVALID_ARG_TYPE(name, 'number', value);
}
if (!NumberIsInteger(value)) {
throw new ERR_OUT_OF_RANGE(name, 'an integer', value);
}
const min = positive ? 1 : 0;
// 2 ** 32 === 4294967296
const max = 4_294_967_295;
if (value < min || value > max) {
throw new ERR_OUT_OF_RANGE(name, `>= ${min} && <= ${max}`, value);
}
});
/**
* @callback validateString
* @param {*} value
* @param {string} name
* @returns {asserts value is string}
*/
/** @type {validateString} */
const validateString = hideStackFrames((value, name) => {
if (typeof value !== 'string')
throw new ERR_INVALID_ARG_TYPE(name, 'string', value);
});
/**
* @callback validateNumber
* @param {*} value
* @param {string} name
* @param {number} [min]
* @param {number} [max]
* @returns {asserts value is number}
*/
/** @type {validateNumber} */
const validateNumber = hideStackFrames((value, name, min = undefined, max) => {
if (typeof value !== 'number')
throw new ERR_INVALID_ARG_TYPE(name, 'number', value);
if ((min != null && value < min) || (max != null && value > max) ||
((min != null || max != null) && NumberIsNaN(value))) {
throw new ERR_OUT_OF_RANGE(
name,
`${min != null ? `>= ${min}` : ''}${min != null && max != null ? ' && ' : ''}${max != null ? `<= ${max}` : ''}`,
value);
}
});
/**
* @callback validateOneOf
* @template T
* @param {T} value
* @param {string} name
* @param {T[]} oneOf
*/
/** @type {validateOneOf} */
const validateOneOf = hideStackFrames((value, name, oneOf) => {
if (!ArrayPrototypeIncludes(oneOf, value)) {
const allowed = ArrayPrototypeJoin(
ArrayPrototypeMap(oneOf, (v) =>
(typeof v === 'string' ? `'${v}'` : String(v))),
', ');
const reason = 'must be one of: ' + allowed;
throw new ERR_INVALID_ARG_VALUE(name, value, reason);
}
});
/**
* @callback validateBoolean
* @param {*} value
* @param {string} name
* @returns {asserts value is boolean}
*/
/** @type {validateBoolean} */
const validateBoolean = hideStackFrames((value, name) => {
if (typeof value !== 'boolean')
throw new ERR_INVALID_ARG_TYPE(name, 'boolean', value);
});
const kValidateObjectNone = 0;
const kValidateObjectAllowNullable = 1 << 0;
const kValidateObjectAllowArray = 1 << 1;
const kValidateObjectAllowFunction = 1 << 2;
const kValidateObjectAllowObjects = kValidateObjectAllowArray |
kValidateObjectAllowFunction;
const kValidateObjectAllowObjectsAndNull = kValidateObjectAllowNullable |
kValidateObjectAllowArray |
kValidateObjectAllowFunction;
/**
* @callback validateObject
* @param {*} value
* @param {string} name
* @param {number} [options]
*/
/** @type {validateObject} */
const validateObject = hideStackFrames(
(value, name, options = kValidateObjectNone) => {
if (options === kValidateObjectNone) {
if (value === null || ArrayIsArray(value)) {
throw new ERR_INVALID_ARG_TYPE(name, 'Object', value);
}
if (typeof value !== 'object') {
throw new ERR_INVALID_ARG_TYPE(name, 'Object', value);
}
} else {
const throwOnNullable = (kValidateObjectAllowNullable & options) === 0;
if (throwOnNullable && value === null) {
throw new ERR_INVALID_ARG_TYPE(name, 'Object', value);
}
const throwOnArray = (kValidateObjectAllowArray & options) === 0;
if (throwOnArray && ArrayIsArray(value)) {
throw new ERR_INVALID_ARG_TYPE(name, 'Object', value);
}
const throwOnFunction = (kValidateObjectAllowFunction & options) === 0;
const typeofValue = typeof value;
if (typeofValue !== 'object' && (throwOnFunction || typeofValue !== 'function')) {
throw new ERR_INVALID_ARG_TYPE(name, 'Object', value);
}
}
});
/**
* @callback validateDictionary - We are using the Web IDL Standard definition
* of "dictionary" here, which means any value
* whose Type is either Undefined, Null, or
* Object (which includes functions).
* @param {*} value
* @param {string} name
* @see https://webidl.spec.whatwg.org/#es-dictionary
* @see https://tc39.es/ecma262/#table-typeof-operator-results
*/
/** @type {validateDictionary} */
const validateDictionary = hideStackFrames(
(value, name) => {
if (value != null && typeof value !== 'object' && typeof value !== 'function') {
throw new ERR_INVALID_ARG_TYPE(name, 'a dictionary', value);
}
});
/**
* @callback validateArray
* @param {*} value
* @param {string} name
* @param {number} [minLength]
* @returns {asserts value is any[]}
*/
/** @type {validateArray} */
const validateArray = hideStackFrames((value, name, minLength = 0) => {
if (!ArrayIsArray(value)) {
throw new ERR_INVALID_ARG_TYPE(name, 'Array', value);
}
if (value.length < minLength) {
const reason = `must be longer than ${minLength}`;
throw new ERR_INVALID_ARG_VALUE(name, value, reason);
}
});
/**
* @callback validateStringArray
* @param {*} value
* @param {string} name
* @returns {asserts value is string[]}
*/
/** @type {validateStringArray} */
const validateStringArray = hideStackFrames((value, name) => {
validateArray(value, name);
for (let i = 0; i < value.length; ++i) {
// Don't use validateString here for performance reasons, as
// we would generate intermediate strings for the name.
if (typeof value[i] !== 'string') {
throw new ERR_INVALID_ARG_TYPE(`${name}[${i}]`, 'string', value[i]);
}
}
});
/**
* @callback validateBooleanArray
* @param {*} value
* @param {string} name
* @returns {asserts value is boolean[]}
*/
/** @type {validateBooleanArray} */
const validateBooleanArray = hideStackFrames((value, name) => {
validateArray(value, name);
for (let i = 0; i < value.length; ++i) {
// Don't use validateBoolean here for performance reasons, as
// we would generate intermediate strings for the name.
if (value[i] !== true && value[i] !== false) {
throw new ERR_INVALID_ARG_TYPE(`${name}[${i}]`, 'boolean', value[i]);
}
}
});
/**
* @callback validateAbortSignalArray
* @param {*} value
* @param {string} name
* @returns {asserts value is AbortSignal[]}
*/
/** @type {validateAbortSignalArray} */
function validateAbortSignalArray(value, name) {
validateArray(value, name);
for (let i = 0; i < value.length; i++) {
const signal = value[i];
const indexedName = `${name}[${i}]`;
if (signal == null) {
throw new ERR_INVALID_ARG_TYPE(indexedName, 'AbortSignal', signal);
}
validateAbortSignal(signal, indexedName);
}
}
/**
* @param {*} signal
* @param {string} [name='signal']
* @returns {asserts signal is keyof signals}
*/
const validateSignalName = hideStackFrames((signal, name = 'signal') => {
validateString(signal, name);
if (signals[signal] === undefined) {
if (signals[StringPrototypeToUpperCase(signal)] !== undefined) {
throw new ERR_UNKNOWN_SIGNAL(signal +
' (signals must use all capital letters)');
}
throw new ERR_UNKNOWN_SIGNAL(signal);
}
});
/**
* @callback validateBuffer
* @param {*} buffer
* @param {string} [name='buffer']
* @returns {asserts buffer is ArrayBufferView}
*/
/** @type {validateBuffer} */
const validateBuffer = hideStackFrames((buffer, name = 'buffer') => {
if (!isArrayBufferView(buffer)) {
throw new ERR_INVALID_ARG_TYPE(name,
['Buffer', 'TypedArray', 'DataView'],
buffer);
}
});
/**
* @param {string} data
* @param {string} encoding
*/
const validateEncoding = hideStackFrames((data, encoding) => {
const normalizedEncoding = normalizeEncoding(encoding);
const length = data.length;
if (normalizedEncoding === 'hex' && length % 2 !== 0) {
throw new ERR_INVALID_ARG_VALUE('encoding', encoding,
`is invalid for data of length ${length}`);
}
});
/**
* Check that the port number is not NaN when coerced to a number,
* is an integer and that it falls within the legal range of port numbers.
* @param {*} port
* @param {string} [name='Port']
* @param {boolean} [allowZero=true]
* @returns {number}
*/
const validatePort = hideStackFrames((port, name = 'Port', allowZero = true) => {
if ((typeof port !== 'number' && typeof port !== 'string') ||
(typeof port === 'string' && StringPrototypeTrim(port).length === 0) ||
+port !== (+port >>> 0) ||
port > 0xFFFF ||
(port === 0 && !allowZero)) {
throw new ERR_SOCKET_BAD_PORT(name, port, allowZero);
}
return port | 0;
});
/**
* @callback validateAbortSignal
* @param {*} signal
* @param {string} name
*/
/** @type {validateAbortSignal} */
const validateAbortSignal = hideStackFrames((signal, name) => {
if (signal !== undefined &&
(signal === null ||
typeof signal !== 'object' ||
!('aborted' in signal))) {
throw new ERR_INVALID_ARG_TYPE(name, 'AbortSignal', signal);
}
});
/**
* @callback validateFunction
* @param {*} value
* @param {string} name
* @returns {asserts value is Function}
*/
/** @type {validateFunction} */
const validateFunction = hideStackFrames((value, name) => {
if (typeof value !== 'function')
throw new ERR_INVALID_ARG_TYPE(name, 'Function', value);
});
/**
* @callback validatePlainFunction
* @param {*} value
* @param {string} name
* @returns {asserts value is Function}
*/
/** @type {validatePlainFunction} */
const validatePlainFunction = hideStackFrames((value, name) => {
if (typeof value !== 'function' || isAsyncFunction(value))
throw new ERR_INVALID_ARG_TYPE(name, 'Function', value);
});
/**
* @callback validateUndefined
* @param {*} value
* @param {string} name
* @returns {asserts value is undefined}
*/
/** @type {validateUndefined} */
const validateUndefined = hideStackFrames((value, name) => {
if (value !== undefined)
throw new ERR_INVALID_ARG_TYPE(name, 'undefined', value);
});
/**
* @template T
* @param {T} value
* @param {string} name
* @param {T[]} union
*/
function validateUnion(value, name, union) {
if (!ArrayPrototypeIncludes(union, value)) {
throw new ERR_INVALID_ARG_TYPE(name, `('${ArrayPrototypeJoin(union, '|')}')`, value);
}
}
/*
The rules for the Link header field are described here:
https://www.rfc-editor.org/rfc/rfc8288.html#section-3
This regex validates any string surrounded by angle brackets
(not necessarily a valid URI reference) followed by zero or more
link-params separated by semicolons.
*/
const linkValueRegExp = /^(?:<[^>]*>)(?:\s*;\s*[^;"\s]+(?:=(")?[^;"\s]*\1)?)*$/;
/**
* @param {any} value
* @param {string} name
*/
const validateLinkHeaderFormat = hideStackFrames((value, name) => {
if (
typeof value === 'undefined' ||
!RegExpPrototypeExec(linkValueRegExp, value)
) {
throw new ERR_INVALID_ARG_VALUE(
name,
value,
'must be an array or string of format "</styles.css>; rel=preload; as=style"',
);
}
});
const validateInternalField = hideStackFrames((object, fieldKey, className) => {
if (typeof object !== 'object' || object === null || !ObjectPrototypeHasOwnProperty(object, fieldKey)) {
throw new ERR_INVALID_ARG_TYPE('this', className, object);
}
});
/**
* @param {any} hints
* @return {string}
*/
const validateLinkHeaderValue = hideStackFrames((hints) => {
if (typeof hints === 'string') {
validateLinkHeaderFormat.withoutStackTrace(hints, 'hints');
return hints;
} else if (ArrayIsArray(hints)) {
const hintsLength = hints.length;
let result = '';
if (hintsLength === 0) {
return result;
}
for (let i = 0; i < hintsLength; i++) {
const link = hints[i];
validateLinkHeaderFormat.withoutStackTrace(link, 'hints');
result += link;
if (i !== hintsLength - 1) {
result += ', ';
}
}
return result;
}
throw new ERR_INVALID_ARG_VALUE(
'hints',
hints,
'must be an array or string of format "</styles.css>; rel=preload; as=style"',
);
});
// 1. Returns false for undefined and NaN
// 2. Returns true for finite numbers
// 3. Throws ERR_INVALID_ARG_TYPE for non-numbers
// 4. Throws ERR_OUT_OF_RANGE for infinite numbers
const validateFiniteNumber = hideStackFrames((number, name) => {
// Common case
if (number === undefined) {
return false;
}
if (NumberIsFinite(number)) {
return true; // Is a valid number
}
if (NumberIsNaN(number)) {
return false;
}
validateNumber(number, name);
// Infinite numbers
throw new ERR_OUT_OF_RANGE(name, 'a finite number', number);
});
// 1. Returns def for number when it's undefined or NaN
// 2. Returns number for finite numbers >= lower and <= upper
// 3. Throws ERR_INVALID_ARG_TYPE for non-numbers
// 4. Throws ERR_OUT_OF_RANGE for infinite numbers or numbers > upper or < lower
const checkRangesOrGetDefault = hideStackFrames(
(number, name, lower, upper, def) => {
if (!validateFiniteNumber(number, name)) {
return def;
}
if (number < lower || number > upper) {
throw new ERR_OUT_OF_RANGE(name, `>= ${lower} and <= ${upper}`, number);
}
return number;
},
);
module.exports = {
isInt32,
isUint32,
parseFileMode,
validateArray,
validateStringArray,
validateBooleanArray,
validateAbortSignalArray,
validateBoolean,
validateBuffer,
validateDictionary,
validateEncoding,
validateFunction,
validateInt32,
validateInteger,
validateNumber,
validateObject,
kValidateObjectNone,
kValidateObjectAllowNullable,
kValidateObjectAllowArray,
kValidateObjectAllowFunction,
kValidateObjectAllowObjects,
kValidateObjectAllowObjectsAndNull,
validateOneOf,
validatePlainFunction,
validatePort,
validateSignalName,
validateString,
validateUint32,
validateUndefined,
validateUnion,
validateAbortSignal,
validateLinkHeaderValue,
validateInternalField,
validateFiniteNumber,
checkRangesOrGetDefault,
};