diff --git a/packages/core/src/storage/functions.ts b/packages/core/src/storage/functions.ts index ddc9690..a05c9f9 100644 --- a/packages/core/src/storage/functions.ts +++ b/packages/core/src/storage/functions.ts @@ -1,6 +1,7 @@ import type { IconifyJSON, IconifyIcon } from '@iconify/types'; import type { FullIconifyIcon } from '@iconify/utils/lib/icon'; import { parseIconSet } from '@iconify/utils/lib/icon-set/parse'; +import { quicklyValidateIconSet } from '@iconify/utils/lib/icon-set/validate-basic'; import type { IconifyIconName } from '@iconify/utils/lib/icon/name'; import { stringToIcon, validateIcon } from '@iconify/utils/lib/icon/name'; import { @@ -108,21 +109,17 @@ export function addCollection(data: IconifyJSON, provider?: string): boolean { ) { // Simple names: add icons one by one let added = false; - parseIconSet( - data, - (name, icon) => { + + if (quicklyValidateIconSet(data)) { + // Reset prefix + data.prefix = ''; + + parseIconSet(data, (name, icon) => { if (icon && addIcon(name, icon)) { added = true; } - }, - { - // Validate icon set and set prefix to empty - validate: { - fix: true, - prefix: '', - }, - } - ); + }); + } return added; } diff --git a/packages/core/src/storage/storage.ts b/packages/core/src/storage/storage.ts index 9cebaad..3b0faa5 100644 --- a/packages/core/src/storage/storage.ts +++ b/packages/core/src/storage/storage.ts @@ -2,6 +2,7 @@ import type { IconifyJSON, IconifyIcon } from '@iconify/types'; import type { FullIconifyIcon } from '@iconify/utils/lib/icon'; import { fullIcon } from '@iconify/utils/lib/icon'; import { parseIconSet } from '@iconify/utils/lib/icon-set/parse'; +import { quicklyValidateIconSet } from '@iconify/utils/lib/icon-set/validate-basic'; /** * List of icons @@ -99,6 +100,10 @@ export function getStorage(provider: string, prefix: string): IconStorage { * Returns array of added icons */ export function addIconSet(storage: IconStorage, data: IconifyJSON): string[] { + if (!quicklyValidateIconSet(data)) { + return []; + } + const t = Date.now(); return parseIconSet(data, (name, icon: FullIconifyIcon | null) => { if (icon) { diff --git a/packages/react/src/offline.ts b/packages/react/src/offline.ts index e8b2e26..787bc17 100644 --- a/packages/react/src/offline.ts +++ b/packages/react/src/offline.ts @@ -7,6 +7,7 @@ import type { } from '@iconify/utils/lib/customisations'; import { fullIcon } from '@iconify/utils/lib/icon'; import { parseIconSet } from '@iconify/utils/lib/icon-set/parse'; +import { quicklyValidateIconSet } from '@iconify/utils/lib/icon-set/validate-basic'; import type { IconifyIconCustomisations, IconifyIconProps, @@ -116,19 +117,11 @@ export function addCollection( : prefix !== false && typeof data.prefix === 'string' ? data.prefix + ':' : ''; - parseIconSet( - data, - (name, icon) => { + + quicklyValidateIconSet(data) && + parseIconSet(data, (name, icon) => { if (icon) { storage[iconPrefix + name] = icon; } - }, - { - // Allow empty prefix - validate: { - fix: true, - prefix: iconPrefix, - }, - } - ); + }); } diff --git a/packages/svelte/src/offline-functions.ts b/packages/svelte/src/offline-functions.ts index 84e5328..647d510 100644 --- a/packages/svelte/src/offline-functions.ts +++ b/packages/svelte/src/offline-functions.ts @@ -1,6 +1,7 @@ import type { IconifyIcon, IconifyJSON } from '@iconify/types'; import { fullIcon } from '@iconify/utils/lib/icon'; import { parseIconSet } from '@iconify/utils/lib/icon-set/parse'; +import { quicklyValidateIconSet } from '@iconify/utils/lib/icon-set/validate-basic'; import { render } from './render'; import type { RenderResult } from './render'; import type { IconProps } from './props'; @@ -60,19 +61,10 @@ export function addCollection( : prefix !== false && typeof data.prefix === 'string' ? data.prefix + ':' : ''; - parseIconSet( - data, - (name, icon) => { + quicklyValidateIconSet(data) && + parseIconSet(data, (name, icon) => { if (icon) { storage[iconPrefix + name] = icon; } - }, - { - // Allow empty prefix - validate: { - fix: true, - prefix: iconPrefix, - }, - } - ); + }); } diff --git a/packages/utils/package.json b/packages/utils/package.json index 3570f4c..f0c135b 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -102,6 +102,10 @@ "require": "./lib/icon-set/validate.cjs", "import": "./lib/icon-set/validate.mjs" }, + "./lib/icon-set/validate-basic": { + "require": "./lib/icon-set/validate-basic.cjs", + "import": "./lib/icon-set/validate-basic.mjs" + }, "./lib/icon": { "require": "./lib/icon/index.cjs", "import": "./lib/icon/index.mjs" diff --git a/packages/utils/src/icon-set/parse.ts b/packages/utils/src/icon-set/parse.ts index fb0a8e9..10c77ab 100644 --- a/packages/utils/src/icon-set/parse.ts +++ b/packages/utils/src/icon-set/parse.ts @@ -1,7 +1,6 @@ import type { IconifyAlias, IconifyJSON } from '@iconify/types'; import { FullIconifyIcon, iconDefaults } from '../icon'; import { getIconData } from './get-icon'; -import { IconSetValidationOptions, validateIconSet } from './validate'; /** * Which aliases to parse: @@ -35,7 +34,6 @@ export function isVariation(item: IconifyAlias): boolean { } export interface ParseIconSetOptions { - validate?: boolean | IconSetValidationOptions; aliases?: ParseIconSetAliases; } @@ -59,20 +57,6 @@ export function parseIconSet( return names; } - // Validate icon set - const validate = options.validate; - if (validate !== false) { - // Validate icon set - try { - validateIconSet( - data, - typeof validate === 'object' ? validate : { fix: true } - ); - } catch (err) { - return names; - } - } - // Check for missing icons list returned by API if (data.not_found instanceof Array) { data.not_found.forEach((name) => { diff --git a/packages/utils/src/icon-set/validate-basic.ts b/packages/utils/src/icon-set/validate-basic.ts new file mode 100644 index 0000000..ac12d1a --- /dev/null +++ b/packages/utils/src/icon-set/validate-basic.ts @@ -0,0 +1,99 @@ +import type { IconifyJSON } from '@iconify/types'; +import { iconDefaults, matchName } from '../icon'; + +/** + * Optional properties + */ +const optionalProperties = { + provider: 'string', + aliases: 'object', + not_found: 'object', +} as Record; + +for (const prop in iconDefaults) { + optionalProperties[prop] = + typeof iconDefaults[prop as keyof typeof iconDefaults]; +} + +/** + * Validate icon set, return it as IconifyJSON on success, null on failure + * + * Unlike validateIconSet(), this function is very basic. + * It does not throw exceptions, it does not check metadata, it does not fix stuff. + */ +export function quicklyValidateIconSet(obj: unknown): IconifyJSON | null { + // Check for object with 'icons' nested object + if (typeof obj !== 'object' || obj === null) { + return null; + } + + // Convert type + const data = obj as IconifyJSON; + + // Check for prefix and icons + if ( + typeof data.prefix !== 'string' || + !(obj as Record).icons || + typeof (obj as Record).icons !== 'object' + ) { + return null; + } + + // Check for optional properties + for (const prop in optionalProperties) { + if ( + (obj as Record)[prop] !== void 0 && + typeof (obj as Record)[prop] !== + optionalProperties[prop] + ) { + return null; + } + } + + // Check all icons + const icons = data.icons; + for (const name in icons) { + const icon = icons[name]; + if (!name.match(matchName) || typeof icon.body !== 'string') { + return null; + } + + for (const prop in iconDefaults) { + if ( + icon[prop as keyof typeof icon] !== void 0 && + typeof icon[prop as keyof typeof icon] !== + typeof iconDefaults[prop as keyof typeof iconDefaults] + ) { + return null; + } + } + } + + // Check all aliases + const aliases = data.aliases; + if (aliases) { + for (const name in aliases) { + const icon = aliases[name]; + const parent = icon.parent; + if ( + !name.match(matchName) || + typeof parent !== 'string' || + (!icons[parent] && !aliases[parent]) + ) { + return null; + } + + for (const prop in iconDefaults) { + if ( + icon[prop as keyof typeof icon] !== void 0 && + typeof icon[prop as keyof typeof icon] !== + typeof iconDefaults[prop as keyof typeof iconDefaults] + ) { + return null; + } + } + } + } + + return data; +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index e2817a4..4a71af6 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -27,6 +27,7 @@ export { // Icon set functions export { parseIconSet, isVariation } from './icon-set/parse'; export { validateIconSet } from './icon-set/validate'; +export { quicklyValidateIconSet } from './icon-set/validate-basic'; export { expandIconSet } from './icon-set/expand'; export { minifyIconSet } from './icon-set/minify'; export { getIcons } from './icon-set/get-icons'; diff --git a/packages/utils/tests/validate-basic-test.ts b/packages/utils/tests/validate-basic-test.ts new file mode 100644 index 0000000..bc374ce --- /dev/null +++ b/packages/utils/tests/validate-basic-test.ts @@ -0,0 +1,152 @@ +import { quicklyValidateIconSet } from '../lib/icon-set/validate-basic'; + +describe('Testing validation', () => { + test('Not object', () => { + expect(quicklyValidateIconSet(void 0)).toBe(null); + expect(quicklyValidateIconSet({})).toBe(null); + expect(quicklyValidateIconSet(null)).toBe(null); + expect(quicklyValidateIconSet([])).toBe(null); + }); + + test('Valid sets', () => { + expect( + quicklyValidateIconSet({ + prefix: 'foo', + icons: { + bar: { + body: '', + }, + }, + width: 24, + height: 24, + }) + ).toEqual({ + prefix: 'foo', + icons: { + bar: { + body: '', + }, + }, + width: 24, + height: 24, + }); + + expect( + quicklyValidateIconSet({ + prefix: 'foo', + icons: { + bar: { + body: '', + width: 32, + height: 32, + rotate: 0, + hFlip: false, + vFlip: true, + // Legacy property + verticalAlign: -0.14, + }, + }, + aliases: { + baz: { + parent: 'bar', + hFlip: true, + }, + }, + width: 24, + height: 24, + }) + ).toEqual({ + prefix: 'foo', + icons: { + bar: { + body: '', + width: 32, + height: 32, + rotate: 0, + hFlip: false, + vFlip: true, + verticalAlign: -0.14, + }, + }, + aliases: { + baz: { + parent: 'bar', + hFlip: true, + }, + }, + width: 24, + height: 24, + }); + + // Empty is allowed + expect( + quicklyValidateIconSet({ + prefix: 'foo', + icons: {}, + }) + ).toEqual({ + prefix: 'foo', + icons: {}, + }); + }); + + test('Missing required properties', () => { + expect( + quicklyValidateIconSet({ + prefix: 'foo', + }) + ).toBe(null); + + expect( + quicklyValidateIconSet({ + icons: {}, + }) + ).toBe(null); + }); + + test('Invalid optional properties', () => { + expect( + quicklyValidateIconSet({ + prefix: 'foo', + icons: { + icon1: { + body: '', + }, + }, + height: 24, + // Object + rotate: { + foo: 1, + }, + }) + ).toBe(null); + + expect( + quicklyValidateIconSet({ + prefix: 'foo', + icons: { + icon1: { + body: '', + }, + }, + height: 24, + // Object + hFlip: null, + }) + ).toBe(null); + + expect( + quicklyValidateIconSet({ + prefix: 'foo', + icons: { + icon1: { + body: '', + }, + }, + height: 24, + // String + width: '32', + }) + ).toBe(null); + }); +}); diff --git a/packages/vue/src/offline.ts b/packages/vue/src/offline.ts index 64f9f91..f9ffbbb 100644 --- a/packages/vue/src/offline.ts +++ b/packages/vue/src/offline.ts @@ -16,6 +16,7 @@ import type { } from '@iconify/utils/lib/customisations'; import { fullIcon } from '@iconify/utils/lib/icon'; import { parseIconSet } from '@iconify/utils/lib/icon-set/parse'; +import { quicklyValidateIconSet } from '@iconify/utils/lib/icon-set/validate-basic'; import type { IconifyIconCustomisations, IconifyIconProps, @@ -70,21 +71,12 @@ export function addCollection( : prefix !== false && typeof data.prefix === 'string' ? data.prefix + ':' : ''; - parseIconSet( - data, - (name, icon) => { + quicklyValidateIconSet(data) && + parseIconSet(data, (name, icon) => { if (icon) { storage[iconPrefix + name] = icon; } - }, - { - // Allow empty prefix - validate: { - fix: true, - prefix: iconPrefix, - }, - } - ); + }); } /** diff --git a/packages/vue2/src/offline.ts b/packages/vue2/src/offline.ts index 7c1e6fa..ea1e085 100644 --- a/packages/vue2/src/offline.ts +++ b/packages/vue2/src/offline.ts @@ -9,6 +9,7 @@ import type { } from '@iconify/utils/lib/customisations'; import { fullIcon } from '@iconify/utils/lib/icon'; import { parseIconSet } from '@iconify/utils/lib/icon-set/parse'; +import { quicklyValidateIconSet } from '@iconify/utils/lib/icon-set/validate-basic'; import type { IconifyIconCustomisations, IconifyIconProps, @@ -63,16 +64,10 @@ export function addCollection( : prefix !== false && typeof data.prefix === 'string' ? data.prefix + ':' : ''; - parseIconSet(data, (name, icon) => { + quicklyValidateIconSet(data) && parseIconSet(data, (name, icon) => { if (icon) { storage[iconPrefix + name] = icon; } - }, { - // Allow empty prefix - validate: { - fix: true, - prefix: iconPrefix, - }, }); }