blob: 2751f5b5ef2fc7a4ef804e82c57377719ba22383 [file] [log] [blame]
/**
*
* Copyright (c) 2020 Silicon Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* This module provides the APIs for validating inputs to the database, and returning flags indicating if
* things were successful or not.
*
* @module Validation API: Validation APIs
*/
const queryZcl = require('../db/query-zcl.js')
const queryConfig = require('../db/query-config.js')
const queryEndpoint = require('../db/query-endpoint.js')
const types = require('../util/types.js')
const queryPackage = require('../db/query-package.js')
const env = require('../util/env')
/**
* Main attribute validation function.
* Returns a promise of an object which stores a list of validation issues.
* Such issues as "Invalid type" or "Out of Range".
* @param {*} db db reference
* @param {*} endpointTypeId endpoint reference
* @param {*} attributeRef attribute reference
* @param {*} clusterRef cluster reference
* @param {*} zapSessionId session reference
* @returns Promise of the list of issues
*/
async function validateAttribute(
db,
endpointTypeId,
attributeRef,
clusterRef,
zapSessionId
) {
let endpointAttribute = await queryZcl.selectEndpointTypeAttribute(
db,
endpointTypeId,
attributeRef,
clusterRef
)
// Null check for endpointAttribute
if (!endpointAttribute) {
env.logWarning(
`validateAttribute called with invalid parameters for endpointAttribute:\n
- endpointTypeId: ${endpointTypeId}\n
- attributeRef: ${attributeRef}\n
- clusterRef: ${clusterRef}`
)
return { defaultValue: ['Attribute not found in endpoint configuration'] }
}
let attribute = await queryZcl.selectAttributeById(db, attributeRef)
// Null check for attribute
if (!attribute) {
env.logWarning(
`validateAttribute called with invalid parameters for attribute:\n
- attributeRef: ${attributeRef}`
)
return { defaultValue: ['Attribute definition not found'] }
}
return validateSpecificAttribute(
endpointAttribute,
attribute,
db,
zapSessionId
)
}
/**
* Get issues in an endpoint.
*
* @param {*} db
* @param {*} endpointId
* @returns object
*/
async function validateEndpoint(db, endpointId) {
let endpoint = await queryEndpoint.selectEndpoint(db, endpointId)
let currentIssues = validateSpecificEndpoint(endpoint)
let noDuplicates = await validateNoDuplicateEndpoints(
db,
endpoint.endpointId,
endpoint.sessionRef
)
if (!noDuplicates) {
currentIssues.endpointId.push('Duplicate EndpointIds Exist')
}
return currentIssues
}
/**
* Check if there are no duplicate endpoints.
*
* @param {*} db
* @param {*} endpointIdentifier
* @param {*} sessionRef
* @returns boolean
*/
async function validateNoDuplicateEndpoints(
db,
endpointIdentifier,
sessionRef
) {
let count =
await queryConfig.selectCountOfEndpointsWithGivenEndpointIdentifier(
db,
endpointIdentifier,
sessionRef
)
return count.length <= 1
}
/**
* Checks the attributes type then validates the incoming input string.
* @param {*} endpointAttribute
* @param {*} attribute
* @param {*} db
* @param {*} zapSessionId
* @returns List of issues wrapped in an object
*/
async function validateSpecificAttribute(
endpointAttribute,
attribute,
db,
zapSessionId
) {
if (!endpointAttribute || !attribute) {
env.logWarning(
`validateSpecificAttribute called with invalid parameters:\n
- endpointAttribute: ${JSON.stringify(endpointAttribute)}\n
- attribute: ${JSON.stringify(attribute)}`
)
return { defaultValue: ['Missing attribute or endpoint configuration'] }
}
let defaultAttributeIssues = []
if (attribute.isNullable && endpointAttribute.defaultValue == null) {
return { defaultValue: defaultAttributeIssues }
} else if (!types.isString(attribute.type)) {
if (types.isFloat(attribute.type)) {
if (!isValidFloat(endpointAttribute.defaultValue))
defaultAttributeIssues.push('Invalid Float')
//Interpreting float values
if (!checkAttributeBoundsFloat(attribute, endpointAttribute))
defaultAttributeIssues.push('Out of range')
} else {
// we shouldn't check boundaries for an invalid number string
if (!isValidNumberString(endpointAttribute.defaultValue)) {
defaultAttributeIssues.push('Invalid Integer')
} else if (
!(await checkAttributeBoundsInteger(
attribute,
endpointAttribute,
db,
zapSessionId
))
) {
defaultAttributeIssues.push('Out of range')
}
}
} else if (types.isString(attribute.type)) {
let maxLengthForString =
attribute.type == 'char_string' || attribute.type == 'octet_string'
? 254
: 65534
let maxAllowedLength = attribute.maxLength
? attribute.maxLength
: maxLengthForString
if (
typeof endpointAttribute.defaultValue === 'string' &&
endpointAttribute.defaultValue.length > maxAllowedLength
) {
defaultAttributeIssues.push('String length out of range')
}
}
return { defaultValue: defaultAttributeIssues }
}
/**
* Get endpoint and newtork issue on an endpoint.
*
* @param {*} endpoint
* @returns object
*/
function validateSpecificEndpoint(endpoint) {
let zclEndpointIdIssues = []
let zclNetworkIdIssues = []
if (!isValidNumberString(endpoint.endpointId))
zclEndpointIdIssues.push('EndpointId is invalid number string')
if (
extractIntegerValue(endpoint.endpointId) > 0xffff ||
extractIntegerValue(endpoint.endpointId) < 0
)
zclEndpointIdIssues.push('EndpointId is out of valid range')
if (!isValidNumberString(endpoint.networkId))
zclNetworkIdIssues.push('NetworkId is invalid number string')
if (extractIntegerValue(endpoint.endpointId) == 0)
zclEndpointIdIssues.push('0 is not a valid endpointId')
return {
endpointId: zclEndpointIdIssues,
networkId: zclNetworkIdIssues
}
}
/**
* Check if value is a valid number in string form.
* This applies to both actual numbers as well as octet strings.
*
* @param {*} value
* @returns boolean
*/
function isValidNumberString(value) {
//We test to see if the number is valid in hex. Decimals numbers also pass this test
return /^(0x)?[\dA-F]+$/i.test(value) || Number.isInteger(Number(value))
}
/**
* Check if value is a valid hex string.
*
* @param {*} value
* @returns boolean
*/
function isValidHexString(value) {
return /^(0x)?[\dA-F]+$/i.test(value)
}
/**
* Check if value is a valid decimal string.
*
* @param {*} value
* @returns boolean
*/
function isValidDecimalString(value) {
return /^\d+$/.test(value)
}
/**
* Check if value is a valid float value.
*
* @param {*} value
* @returns boolean
*/
function isValidFloat(value) {
return !/^0x/i.test(value) && !isNaN(Number(value))
}
/**
* Get float value from the given value.
*
* @param {*} value
* @returns float value
*/
function extractFloatValue(value) {
return parseFloat(value)
}
/**
* Expects a number string , parse it back on a default base 10 if its a decimal.
* If its a hexadecimal or anything else , parse it back on base 16.
* Loses precision after javascripts Number.MAX_SAFE_INTEGER range.
* @param {*} value
* @returns A decimal number
*/
function extractIntegerValue(value) {
if (/^-?\d+$/.test(value)) {
return parseInt(value)
} else if (/^[0-9A-F]+$/i.test(value)) {
return parseInt(value, 16)
} else {
return parseInt(value, 16)
}
}
/**
* Get value of bit integer.
*
* @param {*} value
* @returns BigInt
*/
function extractBigIntegerValue(value) {
if (/^-?\d+$/.test(value)) {
return BigInt(value)
} else if (/^[0-9A-F]+$/i.test(value)) {
return BigInt('0x' + value)
} else {
return BigInt(value)
}
}
/**
* Check if integer is greater than 4 bytes.
*
* @param {*} bits
* @returns boolean
*/
function isBigInteger(bits) {
return bits >= 32
}
/**
* Get the integer attribute's bounds.
*
* @param {*} attribute
* @param {*} typeSize
* @param {*} isSigned
* @returns object
*/
async function getBoundsInteger(attribute, typeSize, isSigned) {
return {
min: attribute.min
? await getIntegerFromAttribute(attribute.min, typeSize, isSigned)
: getTypeRange(typeSize, isSigned, true),
max: attribute.max
? await getIntegerFromAttribute(attribute.max, typeSize, isSigned)
: getTypeRange(typeSize, isSigned, false)
}
}
/**
* Gets the range of an integer type.
*
* @param {*} typeSize
* @param {*} isSigned
* @param {*} isMin
* @returns integer
*/
function getTypeRange(typeSize, isSigned, isMin) {
if (isMin) {
return isSigned ? -Math.pow(2, typeSize - 1) : 0
}
return isSigned ? Math.pow(2, typeSize - 1) - 1 : Math.pow(2, typeSize) - 1
}
/**
* Converts an unsigned integer to its signed value. Returns the same integer if its not a signed type.
* Works for both BigInts and regular numbers.
* @param {*} value - integer to convert
* @param {*} typeSize - bit representation
* @returns A decimal number
*/
function unsignedToSignedInteger(value, typeSize) {
const isSigned = value.toString(2).padStart(typeSize, '0').charAt(0) === '1'
if (isSigned) {
value = ~value
value += isBigInteger(typeSize) ? 1n : 1
}
return value
}
/**
* Converts an attribute (number string) into a decimal number without losing precision.
* Accepts both decimal and hexadecimal strings (former has priority) in any bit representation.
* Shifts signed hexadecimals to their correct value.
* @param {*} attribute - attribute to convert
* @param {*} typeSize - bit representation size
* @param {*} isSigned - is type is signed
* @returns A decimal number
*/
async function getIntegerFromAttribute(attribute, typeSize, isSigned) {
let value = isBigInteger(typeSize)
? extractBigIntegerValue(attribute)
: extractIntegerValue(attribute)
if (
!isValidDecimalString(attribute) &&
isValidHexString(attribute) &&
isSigned
) {
value = unsignedToSignedInteger(value, typeSize)
}
return value
}
/**
* Returns information about an integer type.
* @param {*} db
* @param {*} zapSessionId
* @param {*} attribType
* @returns {*} { size: bit representation , isSigned: is signed type }
*/
async function getIntegerAttributeSize(db, zapSessionId, attribType) {
let packageIds = await queryPackage.getSessionZclPackageIds(db, zapSessionId)
const attribData = await types.getSignAndSizeOfZclType(
db,
attribType,
packageIds
)
if (attribData) {
return {
size: attribData.dataTypesize * 8,
isSigned: attribData.isTypeSigned
}
} else {
return { size: undefined, isSigned: undefined }
}
}
/**
* Checks if the incoming integer is within it's attributes bound while handling signed and unsigned cases.
* @param {*} attribute
* @param {*} endpointAttribute
* @param {*} db
* @param {*} zapSessionId
* @returns boolean
*/
async function checkAttributeBoundsInteger(
attribute,
endpointAttribute,
db,
zapSessionId
) {
const { size, isSigned } = await getIntegerAttributeSize(
db,
zapSessionId,
attribute.type
)
if (size === undefined || isSigned === undefined) {
return false
}
let { min, max } = await getBoundsInteger(attribute, size, isSigned)
let defaultValue = await getIntegerFromAttribute(
endpointAttribute.defaultValue,
size,
isSigned
)
return checkBoundsInteger(defaultValue, min, max)
}
/**
* Check if an integer value is within the bounds.
*
* @param {*} defaultValue
* @param {*} min
* @param {*} max
* @returns boolean
*/
function checkBoundsInteger(defaultValue, min, max) {
if (min == null || Number.isNaN(min)) min = Number.MIN_SAFE_INTEGER
if (max == null || Number.isNaN(max)) max = Number.MAX_SAFE_INTEGER
return defaultValue >= min && defaultValue <= max
}
/**
* Check if float attribute's value is within the bounds.
*
* @param {*} attribute
* @param {*} endpointAttribute
* @returns boolean
*/
function checkAttributeBoundsFloat(attribute, endpointAttribute) {
let { min, max } = getBoundsFloat(attribute)
let defaultValue = extractFloatValue(endpointAttribute.defaultValue)
return checkBoundsFloat(defaultValue, min, max)
}
/**
* Get the bounds on a float attribute's value.
*
* @param {*} attribute
* @returns object
*/
function getBoundsFloat(attribute) {
return {
min: extractFloatValue(attribute.min),
max: extractFloatValue(attribute.max)
}
}
/**
* Check if float value is within the min/max bounds.
*
* @param {*} defaultValue
* @param {*} min
* @param {*} max
* @returns boolean
*/
function checkBoundsFloat(defaultValue, min, max) {
if (Number.isNaN(min)) min = Number.MIN_VALUE
if (Number.isNaN(max)) max = Number.MAX_VALUE
return defaultValue >= min && defaultValue <= max
}
// exports
exports.validateAttribute = validateAttribute
exports.validateEndpoint = validateEndpoint
exports.validateNoDuplicateEndpoints = validateNoDuplicateEndpoints
exports.validateSpecificAttribute = validateSpecificAttribute
exports.validateSpecificEndpoint = validateSpecificEndpoint
exports.isValidNumberString = isValidNumberString
exports.isValidFloat = isValidFloat
exports.extractFloatValue = extractFloatValue
exports.extractIntegerValue = extractIntegerValue
exports.getBoundsInteger = getBoundsInteger
exports.checkBoundsInteger = checkBoundsInteger
exports.getBoundsFloat = getBoundsFloat
exports.checkBoundsFloat = checkBoundsFloat
exports.unsignedToSignedInteger = unsignedToSignedInteger
exports.extractBigIntegerValue = extractBigIntegerValue
exports.getIntegerAttributeSize = getIntegerAttributeSize