Skip to content

Commit

Permalink
Fix integer attrib convertion with signed integer type (#1246) (#1258)
Browse files Browse the repository at this point in the history
  • Loading branch information
Benjamin authored Mar 8, 2024
1 parent e36339c commit 41c0089
Show file tree
Hide file tree
Showing 3 changed files with 426 additions and 48 deletions.
3 changes: 2 additions & 1 deletion src-electron/rest/user-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,8 @@ function httpPostAttributeUpdate(db) {
db,
endpointTypeIdList[0],
id,
clusterRef
clusterRef,
request.zapSessionId
)

response.status(StatusCodes.OK).json({
Expand Down
211 changes: 197 additions & 14 deletions src-electron/validation/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,40 @@ 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')

async function validateAttribute(db, endpointTypeId, attributeRef, clusterRef) {
/**
* Main attribute validation function.
* Returns a promise of an object which stores a list of validational 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
)
let attribute = await queryZcl.selectAttributeById(db, attributeRef)
return validateSpecificAttribute(endpointAttribute, attribute)
return validateSpecificAttribute(
endpointAttribute,
attribute,
db,
zapSessionId,
clusterRef
)
}

async function validateEndpoint(db, endpointId) {
Expand Down Expand Up @@ -65,7 +89,22 @@ async function validateNoDuplicateEndpoints(
return count.length <= 1
}

function validateSpecificAttribute(endpointAttribute, attribute) {
/**
* Checks the attributes type then validates the incoming input string.
* @param {*} endpointAttribute
* @param {*} attribute
* @param {*} db
* @param {*} zapSessionId
* @param {*} clusterRef
* @returns List of issues wrapped in an object
*/
async function validateSpecificAttribute(
endpointAttribute,
attribute,
db,
zapSessionId,
clusterRef
) {
let defaultAttributeIssues = []
if (attribute.isNullable && endpointAttribute.defaultValue == null) {
return { defaultValue: defaultAttributeIssues }
Expand All @@ -77,15 +116,35 @@ function validateSpecificAttribute(endpointAttribute, attribute) {
if (!checkAttributeBoundsFloat(attribute, endpointAttribute))
defaultAttributeIssues.push('Out of range')
} else if (types.isSignedInteger(attribute.type)) {
if (!isValidSignedNumberString(endpointAttribute.defaultValue))
if (!isValidSignedNumberString(endpointAttribute.defaultValue)) {
defaultAttributeIssues.push('Invalid Integer')
if (!checkAttributeBoundsInteger(attribute, endpointAttribute))
} else if (
//we shouldn't check boundaries for an invalid number string
!(await checkAttributeBoundsInteger(
attribute,
endpointAttribute,
db,
zapSessionId,
clusterRef
))
) {
defaultAttributeIssues.push('Out of range')
}
} else {
if (!isValidNumberString(endpointAttribute.defaultValue))
if (!isValidNumberString(endpointAttribute.defaultValue)) {
defaultAttributeIssues.push('Invalid Integer')
if (!checkAttributeBoundsInteger(attribute, endpointAttribute))
} else if (
// we shouldn't check boundaries for an invalid number string
!(await checkAttributeBoundsInteger(
attribute,
endpointAttribute,
db,
zapSessionId,
clusterRef
))
) {
defaultAttributeIssues.push('Out of range')
}
}
} else if (types.isString(attribute.type)) {
let maxLengthForString =
Expand Down Expand Up @@ -127,11 +186,18 @@ 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))
}

function isValidSignedNumberString(value) {
return /^(0x)?[\dA-F]+$/i.test(value) || Number.isInteger(Number(value))
}

function isValidHexString(value) {
return /^(0x)?[\dA-F]+$/i.test(value)
}

function isValidDecimalString(value) {
return /^\d+$/.test(value)
}

function isValidFloat(value) {
return !/^0x/i.test(value) && !isNaN(Number(value))
}
Expand All @@ -140,6 +206,13 @@ 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)
Expand All @@ -150,16 +223,123 @@ function extractIntegerValue(value) {
}
}

function getBoundsInteger(attribute) {
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)
}
}

function isBigInteger(bits) {
return bits >= 32
}

async function getBoundsInteger(attribute, typeSize, isSigned) {
return {
min: extractIntegerValue(attribute.min),
max: extractIntegerValue(attribute.max),
min: await getIntegerFromAttribute(attribute.min, typeSize, isSigned),
max: await getIntegerFromAttribute(attribute.max, typeSize, isSigned),
}
}

function checkAttributeBoundsInteger(attribute, endpointAttribute) {
let { min, max } = getBoundsInteger(attribute)
let defaultValue = extractIntegerValue(endpointAttribute.defaultValue)
/**
* 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 {*} clusterRef
* @param {*} attribType
* @returns {*} { size: bit representation , isSigned: is signed type }
*/
async function getIntegerAttributeSize(
db,
zapSessionId,
clusterRef,
attribType
) {
let packageIds = await queryPackage.getSessionZclPackageIds(db, zapSessionId)
let attribData = await queryZcl.selectNumberByNameAndClusterId(
db,
attribType,
clusterRef,
packageIds
)
return attribData
? { size: attribData.size * 8, isSigned: attribData.isSigned }
: { 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
* @param {*} clusterRef
* @returns boolean
*/
async function checkAttributeBoundsInteger(
attribute,
endpointAttribute,
db,
zapSessionId,
clusterRef
) {
const { size, isSigned } = await getIntegerAttributeSize(
db,
zapSessionId,
clusterRef,
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)
}

Expand Down Expand Up @@ -202,3 +382,6 @@ exports.getBoundsInteger = getBoundsInteger
exports.checkBoundsInteger = checkBoundsInteger
exports.getBoundsFloat = getBoundsFloat
exports.checkBoundsFloat = checkBoundsFloat
exports.unsignedToSignedInteger = unsignedToSignedInteger
exports.extractBigIntegerValue = extractBigIntegerValue
exports.getIntegerAttributeSize = getIntegerAttributeSize
Loading

0 comments on commit 41c0089

Please sign in to comment.