import { CID } from 'multiformats/cid' import { graphemeLen, utf8Len } from '@atproto/common-web' import { Lexicons } from '../lexicons' import { LexBoolean, LexBytes, LexInteger, LexString, LexUserType, ValidationError, ValidationResult, } from '../types' import * as formats from './formats' export function validate( lexicons: Lexicons, path: string, def: LexUserType, value: unknown, ): ValidationResult { switch (def.type) { case 'boolean': return boolean(lexicons, path, def, value) case 'integer': return integer(lexicons, path, def, value) case 'string': return string(lexicons, path, def, value) case 'bytes': return bytes(lexicons, path, def, value) case 'cid-link': return cidLink(lexicons, path, def, value) case 'unknown': return unknown(lexicons, path, def, value) default: return { success: false, error: new ValidationError(`Unexpected lexicon type: ${def.type}`), } } } function boolean( lexicons: Lexicons, path: string, def: LexUserType, value: unknown, ): ValidationResult { def = def as LexBoolean // type const type = typeof value if (type === 'undefined') { if (typeof def.default === 'boolean') { return { success: true, value: def.default } } return { success: false, error: new ValidationError(`${path} must be a boolean`), } } else if (type !== 'boolean') { return { success: false, error: new ValidationError(`${path} must be a boolean`), } } // const if (typeof def.const === 'boolean') { if (value !== def.const) { return { success: false, error: new ValidationError(`${path} must be ${def.const}`), } } } return { success: true, value } } function integer( lexicons: Lexicons, path: string, def: LexUserType, value: unknown, ): ValidationResult { def = def as LexInteger // type const type = typeof value if (type === 'undefined') { if (typeof def.default === 'number') { return { success: true, value: def.default } } return { success: false, error: new ValidationError(`${path} must be an integer`), } } else if (!Number.isInteger(value)) { return { success: false, error: new ValidationError(`${path} must be an integer`), } } // const if (typeof def.const === 'number') { if (value !== def.const) { return { success: false, error: new ValidationError(`${path} must be ${def.const}`), } } } // enum if (Array.isArray(def.enum)) { if (!def.enum.includes(value as number)) { return { success: false, error: new ValidationError( `${path} must be one of (${def.enum.join('|')})`, ), } } } // maximum if (typeof def.maximum === 'number') { if ((value as number) > def.maximum) { return { success: false, error: new ValidationError( `${path} can not be greater than ${def.maximum}`, ), } } } // minimum if (typeof def.minimum === 'number') { if ((value as number) < def.minimum) { return { success: false, error: new ValidationError( `${path} can not be less than ${def.minimum}`, ), } } } return { success: true, value } } function string( lexicons: Lexicons, path: string, def: LexUserType, value: unknown, ): ValidationResult { def = def as LexString // type if (typeof value === 'undefined') { if (typeof def.default === 'string') { return { success: true, value: def.default } } return { success: false, error: new ValidationError(`${path} must be a string`), } } else if (typeof value !== 'string') { return { success: false, error: new ValidationError(`${path} must be a string`), } } // const if (typeof def.const === 'string') { if (value !== def.const) { return { success: false, error: new ValidationError(`${path} must be ${def.const}`), } } } // enum if (Array.isArray(def.enum)) { if (!def.enum.includes(value as string)) { return { success: false, error: new ValidationError( `${path} must be one of (${def.enum.join('|')})`, ), } } } // maxLength and minLength if (typeof def.minLength === 'number' || typeof def.maxLength === 'number') { // If the JavaScript string length * 3 is below the maximum limit, // its UTF8 length (which <= .length * 3) will also be below. if (typeof def.minLength === 'number' && value.length * 3 < def.minLength) { return { success: false, error: new ValidationError( `${path} must not be shorter than ${def.minLength} characters`, ), } } // If the JavaScript string length * 3 is within the maximum limit, // its UTF8 length (which <= .length * 3) will also be within. // When there's no minimal length, this lets us skip the UTF8 length check. let canSkipUtf8LenChecks = false if ( typeof def.minLength === 'undefined' && typeof def.maxLength === 'number' && value.length * 3 <= def.maxLength ) { canSkipUtf8LenChecks = true } if (!canSkipUtf8LenChecks) { const len = utf8Len(value) if (typeof def.maxLength === 'number') { if (len > def.maxLength) { return { success: false, error: new ValidationError( `${path} must not be longer than ${def.maxLength} characters`, ), } } } if (typeof def.minLength === 'number') { if (len < def.minLength) { return { success: false, error: new ValidationError( `${path} must not be shorter than ${def.minLength} characters`, ), } } } } } // maxGraphemes and minGraphemes if ( typeof def.maxGraphemes === 'number' || typeof def.minGraphemes === 'number' ) { let needsMaxGraphemesCheck = false let needsMinGraphemesCheck = false if (typeof def.maxGraphemes === 'number') { if (value.length <= def.maxGraphemes) { // If the JavaScript string length (UTF-16) is within the maximum limit, // its grapheme length (which <= .length) will also be within. needsMaxGraphemesCheck = false } else { needsMaxGraphemesCheck = true } } if (typeof def.minGraphemes === 'number') { if (value.length < def.minGraphemes) { // If the JavaScript string length (UTF-16) is below the minimal limit, // its grapheme length (which <= .length) will also be below. // Fail early. return { success: false, error: new ValidationError( `${path} must not be shorter than ${def.minGraphemes} graphemes`, ), } } else { needsMinGraphemesCheck = true } } if (needsMaxGraphemesCheck || needsMinGraphemesCheck) { const len = graphemeLen(value) if (typeof def.maxGraphemes === 'number') { if (len > def.maxGraphemes) { return { success: false, error: new ValidationError( `${path} must not be longer than ${def.maxGraphemes} graphemes`, ), } } } if (typeof def.minGraphemes === 'number') { if (len < def.minGraphemes) { return { success: false, error: new ValidationError( `${path} must not be shorter than ${def.minGraphemes} graphemes`, ), } } } } } if (typeof def.format === 'string') { switch (def.format) { case 'datetime': return formats.datetime(path, value) case 'uri': return formats.uri(path, value) case 'at-uri': return formats.atUri(path, value) case 'did': return formats.did(path, value) case 'handle': return formats.handle(path, value) case 'at-identifier': return formats.atIdentifier(path, value) case 'nsid': return formats.nsid(path, value) case 'cid': return formats.cid(path, value) case 'language': return formats.language(path, value) case 'tid': return formats.tid(path, value) case 'record-key': return formats.recordKey(path, value) } } return { success: true, value } } function bytes( lexicons: Lexicons, path: string, def: LexUserType, value: unknown, ): ValidationResult { def = def as LexBytes if (!value || !(value instanceof Uint8Array)) { return { success: false, error: new ValidationError(`${path} must be a byte array`), } } // maxLength if (typeof def.maxLength === 'number') { if (value.byteLength > def.maxLength) { return { success: false, error: new ValidationError( `${path} must not be larger than ${def.maxLength} bytes`, ), } } } // minLength if (typeof def.minLength === 'number') { if (value.byteLength < def.minLength) { return { success: false, error: new ValidationError( `${path} must not be smaller than ${def.minLength} bytes`, ), } } } return { success: true, value } } function cidLink( lexicons: Lexicons, path: string, def: LexUserType, value: unknown, ): ValidationResult { if (CID.asCID(value) === null) { return { success: false, error: new ValidationError(`${path} must be a CID`), } } return { success: true, value } } function unknown( lexicons: Lexicons, path: string, def: LexUserType, value: unknown, ): ValidationResult { // type if (!value || typeof value !== 'object') { return { success: false, error: new ValidationError(`${path} must be an object`), } } return { success: true, value } }