Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@exodus/schemasafe

Package Overview
Dependencies
Maintainers
37
Versions
30
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@exodus/schemasafe - npm Package Compare versions

Comparing version 1.0.0-beta.4 to 1.0.0-beta.5

4

package.json
{
"name": "@exodus/schemasafe",
"version": "1.0.0-beta.4",
"version": "1.0.0-beta.5",
"description": "JSON Safe Parser & Schema Validator",

@@ -33,3 +33,3 @@ "license": "MIT",

"coverage:lcov": "c8 --reporter=lcovonly npm run test",
"test": "npm run test:normal | tap-spec && npm run test:module | tap-spec",
"test": "npm run test:raw | tap-spec",
"test:raw": "npm run test:normal && npm run test:module",

@@ -36,0 +36,0 @@ "test:module": "tape -r ./test/tools/test-module.js test/*.js test/regressions/*.js",

'use strict'
const { format, safe, safeand, safeor, safenot } = require('./safe-format')
const { format, safe, safeand, safenot, safenotor } = require('./safe-format')
const genfun = require('./generate-function')

@@ -37,2 +37,8 @@ const { resolveReference, joinPath } = require('./pointer')

const constantValue = (schema) => {
if (typeof schema === 'boolean') return schema
if (schemaTypes.get('object')(schema) && Object.keys(schema).length === 0) return true
return undefined
}
const rootMeta = new WeakMap()

@@ -46,4 +52,6 @@ const compileSchema = (schema, root, opts, scope, basePathRoot = '') => {

allErrors = false,
dryRun = false,
dryRun, // unused, just for rest siblings
allowUnusedKeywords = opts.mode === 'lax',
allowUnreachable = opts.mode === 'lax',
requireSchema = opts.mode === 'strong',
requireValidation = opts.mode === 'strong',

@@ -76,2 +84,4 @@ requireStringValidation = opts.mode === 'strong',

if (!includeErrors && allErrors) throw new Error('allErrors requires includeErrors to be enabled')
if (requireSchema && $schemaDefault) throw new Error('requireSchema forbids $schemaDefault')
if (mode === 'strong' && !requireSchema) throw new Error('Strong mode demands requireSchema')

@@ -153,5 +163,3 @@ const { gensym, getref, genref, genformat } = scopeMethods(scope)

fun.write('validate.errors.push(...%s.map(e => errorMerge(e, %j, %s)))', ...args)
} else {
fun.write('validate.errors = [errorMerge(%s[0], %j, %s)]', ...args)
}
} else fun.write('validate.errors = [errorMerge(%s[0], %j, %s)]', ...args)
} else if (includeErrors === true && errors) {

@@ -162,6 +170,3 @@ const errorJS = format('{ keywordLocation: %j, instanceLocation: %s }', schemaP, dataP)

fun.write('%s.push(%s)', errors, errorJS)
} else {
// Array assignment is significantly faster, do not refactor the two branches
fun.write('%s = [%s]', errors, errorJS)
}
} else fun.write('%s = [%s]', errors, errorJS) // Array assignment is significantly faster, do not refactor the two branches
}

@@ -172,6 +177,3 @@ if (suberr) mergeerror(suberr) // can only happen in allErrors

}
const errorIf = (condition, errorArgs) => {
if (includeErrors === true && errors) fun.if(condition, () => error(errorArgs))
else fun.write('if (%s) return false', condition) // fast-track and inline for more readable code
}
const errorIf = (condition, errorArgs) => fun.if(condition, () => error(errorArgs))

@@ -185,5 +187,8 @@ const fail = (msg, value) => {

const enforceMinMax = (a, b) => laxMode(!(node[b] < node[a]), `Invalid ${a} / ${b} combination`)
const enforceValidation = (msg, suffix = 'must be specified') =>
const enforceValidation = (msg, suffix = 'should be specified') =>
enforce(!requireValidation, `[requireValidation] ${msg} ${suffix}`)
const subPath = (...args) => [...schemaPath, ...args]
const uncertain = (msg) =>
enforce(!removeAdditional && !useDefaults, `[removeAdditional/useDefaults] uncertain: ${msg}`)
const complex = (msg, arg) => enforce(!complexityChecks, `[complexityChecks] ${msg}`, arg)

@@ -196,13 +201,7 @@ // evaluated tracing

if (node === true) {
// any is valid
enforceValidation('schema = true', 'is not allowed')
enforceValidation('schema = true', 'is not allowed') // any is valid here
return { stat } // nothing is evaluated for true
} else if (definitelyPresent) {
// node === false always fails in this case
error({})
} else {
// node === false
errorIf(present(current), {})
}
evaluateDelta({ properties: [true], items: Infinity, type: [] }) // everything is evaluated for false
errorIf(definitelyPresent || current.inKeys ? true : present(current), {}) // node === false
evaluateDelta({ type: [] }) // everything is evaluated for false
return { stat }

@@ -216,3 +215,3 @@ }

if (Object.keys(node).length === 0) {
enforceValidation('empty rules node', 'encountered')
enforceValidation('empty rules node', 'is not allowed')
return { stat } // nothing to validate here, basically the same as node === true

@@ -244,8 +243,2 @@ }

const finish = (local) => {
if (!definitelyPresent) fun.write('}') // undefined check
enforce(unused.size === 0 || allowUnusedKeywords, 'Unprocessed keywords:', [...unused])
return { stat, local } // return statically evaluated
}
if (node === root) {

@@ -262,3 +255,3 @@ const $schema = get('$schema', 'string') || $schemaDefault

})
}
} else enforce(!requireSchema, '[requireSchema] $schema is required')
handle('$vocabulary', ['object'], ($vocabulary) => {

@@ -291,12 +284,2 @@ for (const [vocab, flag] of Object.entries($vocabulary)) {

if (node.default !== undefined && useDefaults) {
if (definitelyPresent) fail('Can not apply default value here (e.g. at root)')
fun.write('if (%s) {', safenot(present(current)))
fun.write('%s = %j', name, get('default', 'jsonval'))
fun.write('} else {')
} else {
handle('default', ['jsonval'], null) // unused
if (!definitelyPresent) fun.write('if (%s) {', present(current))
}
// evaluated: declare dynamic

@@ -309,4 +292,2 @@ const needUnevaluated = (rule) =>

})
if (local.items) fun.write('const %s = [0]', local.items)
if (local.props) fun.write('const %s = [[], []]', local.props)
const dyn = { items: local.items || trace.items, props: local.props || trace.props }

@@ -318,12 +299,10 @@ const canSkipDynamic = () =>

if (dyn.items && delta.items > stat.items) fun.write('%s.push(%d)', dyn.items, delta.items)
if (dyn.props) {
if (dyn.props && delta.properties.includes(true) && !stat.properties.includes(true)) {
fun.write('%s[0].push(true)', dyn.props)
} else if (dyn.props) {
const inStat = (properties, patterns) => inProperties(stat, { properties, patterns })
const properties = delta.properties.filter((x) => !inStat([x], []))
const patterns = delta.patterns.filter((x) => !inStat([], [x]))
if (properties.includes(true)) {
fun.write('%s[0].push(true)', dyn.props)
} else {
if (properties.length > 0) fun.write('%s[0].push(...%j)', dyn.props, properties)
if (patterns.length > 0) fun.write('%s[1].push(...%s)', dyn.props, patterns)
}
if (properties.length > 0) fun.write('%s[0].push(...%j)', dyn.props, properties)
if (patterns.length > 0) fun.write('%s[1].push(...%s)', dyn.props, patterns)
}

@@ -364,19 +343,2 @@ }

}
handle('$ref', ['string'], ($ref) => {
const resolved = resolveReference(root, schemas, node.$ref, basePath())
const [sub, subRoot, path] = resolved[0] || []
if (!sub && sub !== false) fail('failed to resolve $ref:', node.$ref)
const n = getref(sub) || compileSchema(sub, subRoot, opts, scope, path)
return applyRef(n, { path: ['$ref'] })
})
if (node.$ref && getMeta().exclusiveRefs) {
enforce(!opts[optDynamic], 'unevaluated* is supported only on draft2019-09 schemas and above')
return finish() // ref overrides any sibling keywords for older schemas
}
handle('$recursiveRef', ['string'], ($recursiveRef) => {
enforce($recursiveRef === '#', 'Behavior of $recursiveRef is defined only for "#"')
// Apply deep recursion from here only if $recursiveAnchor is true, else just run self
const n = recursiveAnchor ? format('(recursive || validate)') : format('validate')
return applyRef(n, { path: ['$recursiveRef'] })
})

@@ -406,4 +368,4 @@ /* Preparation and methods, post-$ref validation will begin at the end of the function */

enforce(/^\^.*\$$/.test(source), 'Should start with ^ and end with $:', source)
if (complexityChecks && ((source.match(/[{+*]/g) || []).length > 1 || /\)[{+*]/.test(source)))
enforce(target.maxLength !== undefined, 'maxLength should be specified for:', source)
if ((/[{+*].*[{+*]/.test(source) || /\)[{+*]/.test(source)) && target.maxLength === undefined)
complex('maxLength should be specified for pattern:', source)
}

@@ -417,9 +379,15 @@

// Can not be used before undefined check above! The one performed by present()
const rule = (...args) => visit(errors, [...history, { stat, prop: current }], ...args).stat
const nexthistory = () => [...history, { stat, prop: current }]
// Can not be used before undefined check! The one performed by present()
const rule = (...args) => visit(errors, nexthistory(), ...args).stat
const subrule = (suberr, ...args) => {
if (args[0] === current) {
const constval = constantValue(args[1])
if (constval === true) return { sub: format('true'), delta: {} }
if (constval === false) return { sub: format('false'), delta: { type: [] } }
}
const sub = gensym('sub')
fun.write('const %s = (() => {', sub)
if (allErrors) fun.write('let errorCount = 0') // scoped error counter
const { stat: delta } = visit(suberr, [...history, { stat, prop: current }], ...args)
const { stat: delta } = visit(suberr, nexthistory(), ...args)
if (allErrors) {

@@ -439,3 +407,3 @@ fun.write('return errorCount === 0')

// suberror can be null e.g. on failed empty contains
if (suberr !== null) fun.write('if (%s) %s.push(...%s)', suberr, errors, suberr)
if (suberr !== null) fun.if(suberr, () => fun.write('%s.push(...%s)', errors, suberr))
}

@@ -470,4 +438,4 @@

safeand(
...[...new Set(properties)].map((p) => format('%s !== %j', key, p)),
...[...new Set(patternProperties)].map((p) => safenot(patternTest(p, key)))
...properties.map((p) => format('%s !== %j', key, p)),
...patternProperties.map((p) => safenot(patternTest(p, key)))
)

@@ -528,9 +496,8 @@

enforce(valid, 'Invalid format used:', fmtname)
const n = genformat(formatImpl)
if (formatImpl instanceof RegExp) {
// built-in formats are fine, check only ones from options
if (functions.hasOwn(optFormats, fmtname)) enforceRegex(formatImpl.source)
return format('!%s.test(%s)', n, target)
return format('!%s.test(%s)', genformat(formatImpl), target)
}
return format('!%s(%s)', n, target)
return format('!%s(%s)', genformat(formatImpl), target)
}

@@ -546,4 +513,3 @@

evaluateDelta({ fullstring: true })
if (noopRegExps.has(pattern)) return null
return safenot(patternTest(pattern, name))
return noopRegExps.has(pattern) ? null : safenot(patternTest(pattern, name))
})

@@ -622,2 +588,3 @@

handle('contains', ['object', 'boolean'], () => {
uncertain('contains')
const passes = gensym('passes')

@@ -629,3 +596,3 @@ fun.write('let %s = 0', passes)

const { sub } = subrule(suberr, prop, node.contains, subPath('contains'))
fun.write('if (%s) %s++', sub, passes)
fun.if(sub, () => fun.write('%s++', passes))
// evaluateDelta({ unknown: true }) // draft2020: contains counts towards evaluatedItems

@@ -643,3 +610,3 @@ })

const uniqueIsSimple = () => {
const uniqueSimple = () => {
if (node.maxItems !== undefined) return true

@@ -659,6 +626,4 @@ if (typeof node.items === 'object') {

if (uniqueItems === false) return null
if (complexityChecks)
enforce(uniqueIsSimple(), 'maxItems should be specified for non-primitive uniqueItems')
scope.unique = functions.unique
scope.deepEqual = functions.deepEqual
if (!uniqueSimple()) complex('maxItems should be specified for non-primitive uniqueItems')
Object.assign(scope, { unique: functions.unique, deepEqual: functions.deepEqual })
return format('!unique(%s)', name)

@@ -669,2 +634,7 @@ })

// if allErrors is false, we can skip present check for required properties validated before
const checked = (p) =>
!allErrors &&
(stat.required.includes(p) || queryCurrent().some((h) => h.stat.required.includes(p)))
const checkObjects = () => {

@@ -685,7 +655,2 @@ const propertiesCount = format('Object.keys(%s).length', name)

// if allErrors is false, we can skip present check for required properties validated before
const checked = (p) =>
!allErrors &&
(stat.required.includes(p) || queryCurrent().some((h) => h.stat.required.includes(p)))
handle('required', ['array'], (required) => {

@@ -722,8 +687,8 @@ for (const req of required) {

) {
const body = () => {
uncertain(dependencies)
fun.if(item.checked ? true : present(item), () => {
const delta = rule(current, deps, subPath(dependencies, key), dyn)
evaluateDelta(orDelta({}, delta))
evaluateDeltaDynamic(delta)
}
fun.if(item.checked ? true : present(item), body)
})
} else fail(`Unexpected ${dependencies} entry`)

@@ -769,3 +734,3 @@ }

const primitive = vals.filter((value) => !(value && typeof value === 'object'))
return safenot(safeor(...[...primitive, ...objects].map((value) => compare(name, value))))
return safenotor(...[...primitive, ...objects].map((value) => compare(name, value)))
})

@@ -776,33 +741,101 @@ }

handle('not', ['object', 'boolean'], (not) => subrule(null, current, not, subPath('not')).sub)
if (node.not) uncertain('not')
const thenOrElse = node.then || node.then === false || node.else || node.else === false
if ((node.if || node.if === false) && thenOrElse) {
const { sub, delta: deltaIf } = subrule(null, current, node.if, subPath('if'), dyn)
let deltaElse, deltaThen
fun.write('if (%s) {', safenot(sub))
if (node.else || node.else === false) {
deltaElse = rule(current, node.else, subPath('else'), dyn)
evaluateDeltaDynamic(deltaElse)
consume('else', 'object', 'boolean')
} else deltaElse = {}
if (node.then || node.then === false) {
fun.write('} else {')
deltaThen = rule(current, node.then, subPath('then'), dyn)
evaluateDeltaDynamic(andDelta(deltaIf, deltaThen))
consume('then', 'object', 'boolean')
} else deltaThen = {}
fun.write('}')
evaluateDelta(orDelta(deltaElse, andDelta(deltaIf, deltaThen)))
consume('if', 'object', 'boolean')
}
if (thenOrElse)
handle('if', ['object', 'boolean'], (ifS) => {
uncertain('if/then/else')
const { sub, delta: deltaIf } = subrule(null, current, ifS, subPath('if'), dyn)
let handleElse, handleThen, deltaElse, deltaThen
handle('else', ['object', 'boolean'], (elseS) => {
handleElse = () => {
deltaElse = rule(current, elseS, subPath('else'), dyn)
evaluateDeltaDynamic(deltaElse)
}
return null
})
handle('then', ['object', 'boolean'], (thenS) => {
handleThen = () => {
deltaThen = rule(current, thenS, subPath('then'), dyn)
evaluateDeltaDynamic(andDelta(deltaIf, deltaThen))
}
return null
})
fun.if(sub, handleThen, handleElse)
evaluateDelta(orDelta(deltaElse || {}, andDelta(deltaIf, deltaThen || {})))
return null
})
handle('allOf', ['array'], (allOf) => {
const performAllOf = (allOf, rulePath = 'allOf') => {
enforce(allOf.length > 0, `${rulePath} cannot be empty`)
for (const [key, sch] of Object.entries(allOf))
evaluateDelta(rule(current, sch, subPath('allOf', key), dyn))
evaluateDelta(rule(current, sch, subPath(rulePath, key), dyn))
return null
}
handle('allOf', ['array'], (allOf) => performAllOf(allOf))
let handleDiscriminator = null
handle('discriminator', ['object'], (discriminator) => {
const seen = new Set()
const fix = (check, message, arg) => enforce(check, `[discriminator]: ${message}`, arg)
const { propertyName: pname, mapping: map, ...e0 } = discriminator
const prop = currPropImm(pname)
fix(pname && !node.oneOf !== !node.anyOf, 'need propertyName, oneOf OR anyOf')
fix(Object.keys(e0).length === 0, 'only "propertyName" and "mapping" are supported')
const keylen = (obj) => (schemaTypes.get('object')(obj) ? Object.keys(obj).length : null)
handleDiscriminator = (branches, ruleName) => {
fix(map === undefined || keylen(map) === branches.length, 'mismatching mapping size')
const runDiscriminator = () => {
fun.write('switch (%s) {', buildName(prop)) // we could also have used ifs for complex types
let delta
for (const [i, { properties, ...branch }] of Object.entries(branches)) {
const { [pname]: { const: ownval, ...e1 } = {}, ...props } = properties || {}
let val = ownval
if (!val && branch.$ref) {
const [sub] = resolveReference(root, schemas, branch.$ref, basePath())[0] || []
enforce(schemaTypes.get('object')(sub), 'failed to resolve $ref:', branch.$ref)
val = ((sub.properties || {})[pname] || {}).const
}
const ok = typeof val === 'string' && !seen.has(val) && Object.keys(e1).length === 0
fix(ok, 'branches need unique string const values for [propertyName]')
seen.add(val)
const okMapping = !map || (functions.hasOwn(map, val) && map[val] === branch.$ref)
fix(okMapping, 'mismatching mapping for', val)
fun.write('case %j: {', val)
const subdelta = rule(current, { properties: props, ...branch }, subPath(ruleName, i))
evaluateDeltaDynamic(subdelta)
delta = delta ? orDelta(delta, subdelta) : subdelta
fun.write('}')
fun.write('break')
}
evaluateDelta(delta)
fun.write('default:')
error({ path: [ruleName] })
fun.write('}')
}
const propCheck = () => {
if (!checked(pname)) {
const errorPath = ['discriminator', 'propertyName']
fun.if(present(prop), runDiscriminator, () => error({ path: errorPath, prop }))
} else runDiscriminator()
}
if (allErrors || !functions.deepEqual(stat.type, ['object'])) {
fun.if(types.get('object')(name), propCheck, () => error({ path: ['discriminator'] }))
} else propCheck()
// can't evaluateDelta on type and required to not break the checks below, but discriminator
// is usually used with refs anyway so those won't be of much use
fix(functions.deepEqual(stat.type, ['object']), 'has to be checked for type:', 'object')
fix(stat.required.includes(pname), 'propertyName should be placed in required:', pname)
return null
}
return null
})
handle('anyOf', ['array'], (anyOf) => {
enforce(anyOf.length > 0, 'anyOf cannot be empty')
if (anyOf.length === 1) return performAllOf(anyOf)
if (handleDiscriminator) return handleDiscriminator(anyOf, 'anyOf')
uncertain('anyOf, use discriminator to make it certain')
const suberr = suberror()
if (anyOf.length > 0 && !canSkipDynamic()) {
if (!canSkipDynamic()) {
// In this case, all have to be checked to gather evaluated properties

@@ -813,3 +846,3 @@ const entries = Object.entries(anyOf).map(([key, sch]) =>

evaluateDelta(entries.reduce((acc, cur) => orDelta(acc, cur.delta), {}))
const condition = safenot(safeor(...entries.map(({ sub }) => sub)))
const condition = safenotor(...entries.map(({ sub }) => sub))
errorIf(condition, { path: ['anyOf'], suberr })

@@ -820,10 +853,13 @@ for (const { delta, sub } of entries) fun.if(sub, () => evaluateDeltaDynamic(delta))

let delta
for (const [key, sch] of Object.entries(anyOf)) {
const { sub, delta: deltaVariant } = subrule(suberr, current, sch, subPath('anyOf', key))
fun.write('if (%s) {', safenot(sub))
delta = delta ? orDelta(delta, deltaVariant) : deltaVariant
let body = () => error({ path: ['anyOf'], suberr })
for (const [key, sch] of Object.entries(anyOf).reverse()) {
const oldBody = body
body = () => {
const { sub, delta: deltaVar } = subrule(suberr, current, sch, subPath('anyOf', key))
fun.if(safenot(sub), oldBody)
delta = delta ? orDelta(delta, deltaVar) : deltaVar
}
}
if (anyOf.length > 0) evaluateDelta(delta)
error({ path: ['anyOf'], suberr })
anyOf.forEach(() => fun.write('}'))
body()
evaluateDelta(delta)
return null

@@ -833,2 +869,6 @@ })

handle('oneOf', ['array'], (oneOf) => {
enforce(oneOf.length > 0, 'oneOf cannot be empty')
if (oneOf.length === 1) return performAllOf(oneOf)
if (handleDiscriminator) return handleDiscriminator(oneOf, 'oneOf')
uncertain('oneOf, use discriminator to make it certain')
const passes = gensym('passes')

@@ -842,7 +882,7 @@ fun.write('let %s = 0', passes)

const entry = subrule(suberr, current, sch, subPath('oneOf', key), dyn)
fun.write('if (%s) %s++', entry.sub, passes)
fun.if(entry.sub, () => fun.write('%s++', passes))
delta = delta ? orDelta(delta, entry.delta) : entry.delta
return entry
})
if (oneOf.length > 0) evaluateDelta(delta)
evaluateDelta(delta)
errorIf(format('%s !== 1', passes), { path: ['oneOf'] })

@@ -897,4 +937,2 @@ fun.if(format('%s === 0', passes), () => mergeerror(suberr)) // if none matched, dump all errors

/* Actual post-$ref validation happens below */
const performValidation = () => {

@@ -925,26 +963,80 @@ if (prev !== null) fun.write('const %s = errorCount', prev)

let typeIfAdded = false
handle('type', ['string', 'array'], (type) => {
const typearr = Array.isArray(type) ? type : [type]
for (const t of typearr) enforce(typeof t === 'string' && types.has(t), 'Unknown type:', t)
if (current.type) {
enforce(functions.deepEqual(typearr, [current.type]), 'One type is allowed:', current.type)
evaluateDelta({ type: [current.type] })
// main post-presence check validation function
const writeMain = () => {
if (local.items) fun.write('const %s = [0]', local.items)
if (local.props) fun.write('const %s = [[], []]', local.props)
// refs
handle('$ref', ['string'], ($ref) => {
const resolved = resolveReference(root, schemas, $ref, basePath())
const [sub, subRoot, path] = resolved[0] || []
if (!sub && sub !== false) fail('failed to resolve $ref:', $ref)
const n = getref(sub) || compileSchema(sub, subRoot, opts, scope, path)
return applyRef(n, { path: ['$ref'] })
})
if (node.$ref && getMeta().exclusiveRefs) {
enforce(!opts[optDynamic], 'unevaluated* is supported only on draft2019-09 and above')
return // ref overrides any sibling keywords for older schemas
}
handle('$recursiveRef', ['string'], ($recursiveRef) => {
enforce($recursiveRef === '#', 'Behavior of $recursiveRef is defined only for "#"')
// Apply deep recursion from here only if $recursiveAnchor is true, else just run self
const n = recursiveAnchor ? format('(recursive || validate)') : format('validate')
return applyRef(n, { path: ['$recursiveRef'] })
})
// typecheck
let typeCheck = null
handle('type', ['string', 'array'], (type) => {
const typearr = Array.isArray(type) ? type : [type]
for (const t of typearr) enforce(typeof t === 'string' && types.has(t), 'Unknown type:', t)
if (current.type) {
enforce(functions.deepEqual(typearr, [current.type]), 'One type allowed:', current.type)
evaluateDelta({ type: [current.type] })
return null
}
if (parentCheckedType(...typearr)) return null
const filteredTypes = typearr.filter((t) => typeApplicable(t))
if (filteredTypes.length === 0) fail('No valid types possible')
evaluateDelta({ type: typearr }) // can be safely done here, filteredTypes already prepared
typeCheck = safenotor(...filteredTypes.map((t) => types.get(t)(name)))
return null
})
// main validation block
// if type validation was needed and did not return early, wrap this inside an else clause.
if (typeCheck && allErrors) {
fun.if(typeCheck, () => error({ path: ['type'] }), performValidation)
} else {
if (typeCheck) errorIf(typeCheck, { path: ['type'] })
performValidation()
}
if (parentCheckedType(...typearr)) return null
const filteredTypes = typearr.filter((t) => typeApplicable(t))
if (filteredTypes.length === 0) fail('No valid types possible')
evaluateDelta({ type: typearr }) // can be safely done here, filteredTypes already prepared
typeIfAdded = true
return safenot(safeor(...filteredTypes.map((t) => types.get(t)(name))))
})
// If type validation was needed and did not return early, wrap this inside an else clause.
if (typeIfAdded && allErrors) fun.block('else {', [], '}', performValidation)
else performValidation()
// account for maxItems to recheck if they limit items. TODO: perhaps we could keep track of this in stat?
if (stat.items < Infinity && node.maxItems <= stat.items) evaluateDelta({ items: Infinity })
}
if (!isSub) {
// presence check and call main validation block
if (node.default !== undefined && useDefaults) {
if (definitelyPresent) fail('Can not apply default value here (e.g. at root)')
const defvalue = get('default', 'jsonval')
fun.if(present(current), writeMain, () => fun.write('%s = %j', name, defvalue))
} else {
handle('default', ['jsonval'], null) // unused
fun.if(definitelyPresent ? true : present(current), writeMain)
}
// Checks related to static schema analysis
if (!allowUnreachable) enforce(!fun.optimizedOut, 'some checks are never reachable')
if (isSub) {
const logicalOp = ['not', 'if', 'then', 'else'].includes(schemaPath[schemaPath.length - 1])
const branchOp = ['oneOf', 'anyOf', 'allOf'].includes(schemaPath[schemaPath.length - 2])
const depOp = ['dependencies', 'dependentSchemas'].includes(schemaPath[schemaPath.length - 2])
// Coherence check, unreachable, double-check that we came from expected path
enforce(logicalOp || branchOp || depOp, 'Unexpected')
} else if (!schemaPath.includes('not')) {
// 'not' does not mark anything as evaluated (unlike even if/then/else), so it's safe to exclude from these
// checks, as we are sure that everything will be checked without it. It can be viewed as a pure add-on.
if (!stat.type) enforceValidation('type')
if (typeApplicable('array') && stat.items !== Infinity && !(node.maxItems <= stat.items))
if (typeApplicable('array') && stat.items !== Infinity)
enforceValidation(node.items ? 'additionalItems or unevaluatedItems' : 'items rule')

@@ -957,15 +1049,10 @@ if (typeApplicable('object') && !stat.properties.includes(true))

if (!stat.fullstring && requireStringValidation) {
const stringWarning = 'pattern, format or contentSchema must be specified for strings'
const stringWarning = 'pattern, format or contentSchema should be specified for strings'
fail(`[requireStringValidation] ${stringWarning}, use pattern: ^[\\s\\S]*$ to opt-out`)
}
} else {
const n0 = schemaPath[schemaPath.length - 1]
const n1 = schemaPath[schemaPath.length - 2]
const allowed0 = ['not', 'if', 'then', 'else']
const allowed1 = ['oneOf', 'anyOf', 'allOf', 'dependencies', 'dependentSchemas']
// Sanity check, unreachable, double-check that we came from expected path
enforce(allowed0.includes(n0) || allowed1.includes(n1), 'Unexpected')
}
if (node.properties && !node.required) enforceValidation('if properties is used, required')
enforce(unused.size === 0 || allowUnusedKeywords, 'Unprocessed keywords:', [...unused])
return finish(local)
return { stat, local } // return statically evaluated
}

@@ -981,9 +1068,7 @@

if (allErrors) {
fun.write('return errorCount === 0')
} else fun.write('return true')
if (allErrors) fun.write('return errorCount === 0')
else fun.write('return true')
fun.write('}')
if (dryRun) return
validate = fun.makeFunction(scope)

@@ -1003,6 +1088,4 @@ validate[evaluatedStatic] = stat

// it enabled if needed. Enabling it without need can give up to about 40% performance drop.
if (e.message === 'Dynamic unevaluated tracing is not enabled') {
const scope = Object.create(null)
return { scope, ref: compileSchema(schema, schema, { ...opts, [optDynamic]: true }, scope) }
}
if (!opts[optDynamic] && e.message === 'Dynamic unevaluated tracing is not enabled')
return compile(schema, { ...opts, [optDynamic]: true })
throw e

@@ -1009,0 +1092,0 @@ }

'use strict'
const { format, safe } = require('./safe-format')
const { format, safe, safenot } = require('./safe-format')
const { jaystring } = require('./javascript')

@@ -15,3 +15,3 @@

if (INDENT_END.test(line.trim()[0])) indent--
lines.push(`${' '.repeat(indent * 2)}${line}`)
lines.push({ indent, code: line })
if (INDENT_START.test(line[line.length - 1])) indent++

@@ -22,3 +22,3 @@ }

if (indent !== 0) throw new Error('Unexpected indent at build()')
const joined = lines.join('\n')
const joined = lines.map((line) => `${' '.repeat(line.indent * 2)}${line.code}`).join('\n')
return /^[a-z][a-z0-9]*$/i.test(joined) ? `return ${joined}` : `return (${joined})`

@@ -38,2 +38,3 @@ }

return {
optimizedOut: false, // some branch of code has been optimized out
size: () => lines.length,

@@ -44,8 +45,9 @@

if (fmt.includes('\n')) throw new Error('Only single lines are supported')
pushLine(args.length > 0 ? format(fmt, ...args) : fmt)
pushLine(format(fmt, ...args))
return true // code was written
},
block(fmt, args, close, writeBody) {
block(prefix, writeBody, noInline = false) {
const oldIndent = indent
this.write(fmt, ...args)
this.write('%s {', prefix)
const length = lines.length

@@ -57,12 +59,28 @@ writeBody()

indent = oldIndent
return
return false // nothing written
} else if (length === lines.length - 1 && !noInline) {
// a single line has been written, inline it if opt-in allows
const { code } = lines[lines.length - 1]
// check below is just for generating more readable code, it's safe to inline all !noInline
if (!/^(if|for) /.test(code)) {
lines.length -= 2
indent = oldIndent
return this.write('%s %s', prefix, code)
}
}
this.write(close)
return this.write('}')
},
if(condition, writeBody) {
/* c8 ignore next */
if (`${condition}` === 'false') throw new Error('Unexpected: false if condition')
if (`${condition}` === 'true') return writeBody()
this.block('if (%s) {', [condition], '}', writeBody)
if(condition, writeBody, writeElse) {
if (`${condition}` === 'false') {
if (writeElse) writeElse()
if (writeBody) this.optimizedOut = true
} else if (`${condition}` === 'true') {
if (writeBody) writeBody()
if (writeElse) this.optimizedOut = true
} else if (writeBody && this.block(format('if (%s)', condition), writeBody, !!writeElse)) {
if (writeElse) this.block(format('else'), writeElse) // !!writeElse above ensures {} wrapping before `else`
} else if (writeElse) {
this.if(safenot(condition), writeElse)
}
},

@@ -69,0 +87,0 @@

@@ -49,3 +49,3 @@ 'use strict'

const present = (obj) => {
const name = buildName(obj) // also checks for sanity, do not remove
const name = buildName(obj) // also checks for coherence, do not remove
const { parent, keyval, keyname, inKeys, checked } = obj

@@ -71,3 +71,3 @@ /* c8 ignore next */

const key = gensym('key')
fun.block('for (const %s of Object.keys(%s)) {', [key, buildName(obj)], '}', () => {
fun.block(format('for (const %s of Object.keys(%s))', key, buildName(obj)), () => {
writeBody(propvar(obj, key, true), key) // always own property here

@@ -80,3 +80,3 @@ })

const name = buildName(obj)
fun.block('for (let %s = %s; %s < %s.length; %s++) {', [i, start, i, name, i], '}', () => {
fun.block(format('for (let %s = %s; %s < %s.length; %s++)', i, start, i, name, i), () => {
writeBody(propvar(obj, i, unmodifiedPrototypes, true), i) // own property in Array if proto not mangled

@@ -83,0 +83,0 @@ })

@@ -20,2 +20,3 @@ 'use strict'

...['deprecated', 'description', 'title', 'examples', '$comment'], // unused
'discriminator', // optimization hint and error filtering only, does not affect validation result
]

@@ -22,0 +23,0 @@

@@ -78,2 +78,5 @@ 'use strict'

const safepriority = (arg) =>
// simple expression and single brackets can not break priority
/^[a-z][a-z0-9_().]*$/i.test(arg) || /^\([^()]+\)$/i.test(arg) ? arg : format('(%s)', arg)
const safeor = safewrap(

@@ -88,7 +91,7 @@ (...args) => (args.some((arg) => `${arg}` === 'true') ? 'true' : args.join(' || ') || 'false')

if (`${arg}` === 'false') return safe('true')
// simple expression and single brackets can not break priority
if (/^[a-z][a-z0-9_().]*$/i.test(arg) || /^\([^()]+\)$/i.test(arg)) return format('!%s', arg)
return format('!(%s)', arg)
return format('!%s', safepriority(arg))
}
// this function is priority-safe, unlike safeor, hence it's exported and safeor is not atm
const safenotor = (...args) => safenot(safeor(...args))
module.exports = { format, safe, safeor, safeand, safenot }
module.exports = { format, safe, safeand, safenot, safenotor }

@@ -9,3 +9,3 @@ 'use strict'

// A isMultipleOf B: shortest decimal denoted as A % shortest decimal denoted as B === 0
// Optimized, sanity checks and precomputation are outside of this method
// Optimized, coherence checks and precomputation are outside of this method
const isMultipleOf = (value, divisor, factor, factorMultiple) => {

@@ -12,0 +12,0 @@ if (value % divisor === 0) return true

@@ -12,27 +12,68 @@ 'use strict'

* for items and properties, for use with unevaluatedItems and unevaluatedProperties.
*
* WARNING: it is important that this doesn't produce invalid information. i.e.:
* * Extra properties or patterns, too high items
* * Missing dyn.properties or dyn.patterns, too low dyn.items
* * Extra fullstring flag or required entries
* * Missing types, if type is present
* * Missing unknown
*
* The other way around is non-optimal but safe.
*
* null means any type (i.e. any type is possible, not validated)
* true in properties means any property (i.e. all properties were evaluated)
* fullstring means that the object is not an unvalidated string (i.e. is either validated or not a string)
* unknown means that there could be evaluated items or properties unknown to both top-level or dyn
*
* For normalization:
* 1. If type is applicable:
* * dyn.items >= items,
* * dyn.properties includes properties
* * dyn.patterns includes patterns.
* 2. If type is not applicable, the following rules apply:
* * `fullstring = true` if `string` type is not applicable
* * `items = Infinity`, `dyn.items = 0` if `array` type is not applicable
* * `properties = [true]`, `dyn.properties = []` if `object` type is not applicable
* * `patterns = dyn.patterns = []` if `object` type is not applicable
* * `required = []` if `object` type is not applicable
*
* That allows to simplify the `or` operation.
*/
const initTracing = () => ({
...{ properties: [], patterns: [], required: [], items: 0, type: null, fullstring: false },
dyn: { properties: [], patterns: [], items: 0 },
unknown: false,
const merge = (a, b) => [...new Set([...a, ...b])].sort()
const intersect = (a, b) => a.filter((x) => b.includes(x))
const wrapArgs = (f) => (...args) => f(...args.map(normalize))
const wrapFull = (f) => (...args) => normalize(f(...args.map(normalize)))
const typeIsNot = (type, t) => type && !type.includes(t) // type=null means any and includes anything
const normalize = ({ type = null, dyn: d = {}, ...A }) => ({
type: type ? [...type].sort() : type,
items: typeIsNot(type, 'array') ? Infinity : A.items || 0,
properties: typeIsNot(type, 'object') ? [true] : [...(A.properties || [])].sort(),
patterns: typeIsNot(type, 'object') ? [] : [...(A.patterns || [])].sort(),
required: typeIsNot(type, 'object') ? [] : [...(A.required || [])].sort(),
fullstring: typeIsNot(type, 'string') || A.fullstring || false,
dyn: {
items: typeIsNot(type, 'array') ? 0 : Math.max(A.items || 0, d.items || 0),
properties: typeIsNot(type, 'object') ? [] : merge(A.properties || [], d.properties || []),
patterns: typeIsNot(type, 'object') ? [] : merge(A.patterns || [], d.patterns || []),
},
unknown: (A.unknown && !(typeIsNot(type, 'object') && typeIsNot(type, 'array'))) || false,
})
const wrap = (A) => ({ ...initTracing(), ...A }) // sets default empty values
const wrapFun = (f) => (...args) => f(...args.map(wrap))
const stringValidated = (A) => A.fullstring || (A.type && !A.type.includes('string'))
const initTracing = () => normalize({})
// Result means that both sets A and B are correct
// type is intersected, lists of known properties are merged
const andDelta = wrapFun((A, B) => ({
const andDelta = wrapFull((A, B) => ({
type: A.type && B.type ? intersect(A.type, B.type) : A.type || B.type || null,
items: Math.max(A.items, B.items),
properties: [...A.properties, ...B.properties],
patterns: [...A.patterns, ...B.patterns],
required: [...A.required, ...B.required],
type: A.type && B.type ? A.type.filter((x) => B.type.includes(x)) : A.type || B.type || null,
fullstring: stringValidated(A) || stringValidated(B),
properties: merge(A.properties, B.properties),
patterns: merge(A.patterns, B.patterns),
required: merge(A.required, B.required),
fullstring: A.fullstring || B.fullstring,
dyn: {
items: Math.max(A.dyn.items, B.dyn.items),
properties: [...A.dyn.properties, ...B.dyn.properties],
patterns: [...A.dyn.patterns, ...B.dyn.patterns],
properties: merge(A.dyn.properties, B.dyn.properties),
patterns: merge(A.dyn.patterns, B.dyn.patterns),
},

@@ -42,30 +83,33 @@ unknown: A.unknown || B.unknown,

const regtest = (pattern, value) => new RegExp(pattern, 'u').test(value)
const regtest = (pattern, value) => value !== true && new RegExp(pattern, 'u').test(value)
const orProperties = ({ properties: a, patterns: rega }, { properties: b, patterns: regb }) => {
if (a.includes(true)) return b
if (b.includes(true)) return a
const afiltered = a.filter((x) => b.includes(x) || regb.some((p) => regtest(p, x)))
const bfiltered = b.filter((x) => rega.some((p) => regtest(p, x)))
return [...afiltered, ...bfiltered]
const intersectProps = ({ properties: a, patterns: rega }, { properties: b, patterns: regb }) => {
// properties
const af = a.filter((x) => b.includes(x) || b.includes(true) || regb.some((p) => regtest(p, x)))
const bf = b.filter((x) => a.includes(x) || a.includes(true) || rega.some((p) => regtest(p, x)))
// patterns
const ar = rega.filter((x) => regb.includes(x) || b.includes(true))
const br = regb.filter((x) => rega.includes(x) || a.includes(true))
return { properties: merge(af, bf), patterns: merge(ar, br) }
}
const inProperties = ({ properties: a, patterns: rega }, { properties: b, patterns: regb }) =>
a.includes(true) ||
(regb.every((x) => rega.includes(x)) &&
b.every((x) => a.includes(x) || rega.some((p) => regtest(p, x))))
b.every((x) => a.includes(x) || a.includes(true) || rega.some((p) => regtest(p, x))) &&
regb.every((x) => rega.includes(x) || a.includes(true))
// Result means that at least one of sets A and B is correct
// type is merged, lists of known properties are intersected
const orDelta = wrapFun((A, B) => ({
// type is merged, lists of known properties are intersected, lists of dynamic properties are merged
const orDelta = wrapFull((A, B) => ({
type: A.type && B.type ? merge(A.type, B.type) : null,
items: Math.min(A.items, B.items),
properties: orProperties(A, B),
patterns: A.patterns.filter((x) => B.patterns.includes(x)),
required: A.required.filter((x) => B.required.includes(x)),
type: A.type && B.type ? [...new Set([...A.type, ...B.type])] : null,
fullstring: stringValidated(A) && stringValidated(B),
...intersectProps(A, B),
required:
(typeIsNot(A.type, 'object') && B.required) ||
(typeIsNot(B.type, 'object') && A.required) ||
intersect(A.required, B.required),
fullstring: A.fullstring && B.fullstring,
dyn: {
items: Math.max(A.items, B.items, A.dyn.items, B.dyn.items),
properties: [...A.properties, ...B.properties, ...A.dyn.properties, ...B.dyn.properties],
patterns: [...A.patterns, ...B.patterns, ...A.dyn.patterns, ...B.dyn.patterns],
items: Math.max(A.dyn.items, B.dyn.items),
properties: merge(A.dyn.properties, B.dyn.properties),
patterns: merge(A.dyn.patterns, B.dyn.patterns),
},

@@ -75,17 +119,5 @@ unknown: A.unknown || B.unknown,

const applyDelta = (stat, delta) => {
if (delta.items) stat.items = Math.max(stat.items, delta.items)
if (delta.properties) stat.properties.push(...delta.properties)
if (delta.patterns) stat.patterns.push(...delta.patterns)
if (delta.required) stat.required.push(...delta.required)
if (delta.type)
stat.type = stat.type ? stat.type.filter((x) => delta.type.includes(x)) : delta.type
if (delta.fullstring || (stat.type && !stat.type.includes('string'))) stat.fullstring = true
if (delta.dyn) stat.dyn.items = Math.max(stat.dyn.items, delta.dyn.items)
if (delta.dyn) stat.dyn.properties.push(...delta.dyn.properties)
if (delta.dyn) stat.dyn.patterns.push(...delta.dyn.patterns)
if (delta.unknown) stat.unknown = true
}
const applyDelta = (stat, delta) => Object.assign(stat, andDelta(stat, delta))
const isDynamic = wrapFun(({ unknown, items, dyn, ...stat }) => ({
const isDynamic = wrapArgs(({ unknown, items, dyn, ...stat }) => ({
items: items !== Infinity && (unknown || dyn.items > items),

@@ -92,0 +124,0 @@ properties: !stat.properties.includes(true) && (unknown || !inProperties(stat, dyn)),

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc