From 3f8c98f2a89da42a9080b703abdbc09ecf0b7314 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sat, 28 Oct 2023 22:18:37 +0100 Subject: [PATCH 1/7] fix: deep copy imported contexts --- lib/ContextParser.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ContextParser.ts b/lib/ContextParser.ts index 64fe917..549a6d4 100644 --- a/lib/ContextParser.ts +++ b/lib/ContextParser.ts @@ -863,8 +863,8 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP * @param importContextIri The full URI of an @import value. */ public async loadImportContext(importContextIri: string): Promise { - // Load the context - const importContext = await this.load(importContextIri); + // Load the context - and do a deep clone since we are about to mutate it + const importContext = JSON.parse(JSON.stringify(await this.load(importContextIri))); // Require the context to be a non-array object if (typeof importContext !== 'object' || Array.isArray(importContext)) { From 776e12a95811febed84cd9088669cbaa5e94a800 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sat, 28 Oct 2023 22:55:26 +0100 Subject: [PATCH 2/7] chore: add assignment markers --- lib/ContextParser.ts | 44 ++++++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/lib/ContextParser.ts b/lib/ContextParser.ts index 549a6d4..4b4714e 100644 --- a/lib/ContextParser.ts +++ b/lib/ContextParser.ts @@ -92,8 +92,7 @@ export class ContextParser { * @return {IJsonLdContextNormalizedRaw} The mutated input context. */ public idifyReverseTerms(context: IJsonLdContextNormalizedRaw): IJsonLdContextNormalizedRaw { - for (const key of Object.keys(context)) { - const value: IPrefixValue = context[key]; + for (const value of Object.values(context)) { if (value && typeof value === 'object') { if (value['@reverse'] && !value['@id']) { if (typeof value['@reverse'] !== 'string' || Util.isValidKeyword(value['@reverse'])) { @@ -102,6 +101,7 @@ export class ContextParser { } value['@id'] = value['@reverse']; if (Util.isPotentialKeyword(value['@reverse'])) { + // @assignment delete value['@reverse']; } else { value['@reverse'] = true; @@ -152,6 +152,7 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR const value: IPrefixValue = contextRaw[key]; let changed: boolean = false; if (typeof value === 'string') { + // @assignment contextRaw[key] = context.expandTerm(value, true); changed = changed || value !== contextRaw[key]; } else { @@ -162,6 +163,7 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR if ('@id' in value) { // Use @id value for expansion if (id !== undefined && id !== null && typeof id === 'string') { + // @assignment contextRaw[key]['@id'] = context.expandTerm(id, true); changed = changed || id !== contextRaw[key]['@id']; } @@ -170,6 +172,7 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR const newId = context.expandTerm(key, true); if (newId !== key) { // Don't set @id if expansion failed + // @assignment contextRaw[key]['@id'] = newId; changed = true; } @@ -178,8 +181,10 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR && (!value['@container'] || !( value['@container'])['@type']) && canAddIdEntry) { // First check @vocab, then fallback to @base + // @assignment contextRaw[key]['@type'] = context.expandTerm(type, true); if (expandContentTypeToBase && type === contextRaw[key]['@type']) { + // @assignment contextRaw[key]['@type'] = context.expandTerm(type, false); } changed = changed || type !== contextRaw[key]['@type']; @@ -222,20 +227,22 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR * @param {IJsonLdContextNormalizedRaw} context A context. */ public containersToHash(context: IJsonLdContextNormalizedRaw) { - for (const key of Object.keys(context)) { - const value = context[key]; + for (const value of Object.values(context)) { if (value && typeof value === 'object') { if (typeof value['@container'] === 'string') { + // @assignment value['@container'] = { [value['@container']]: true }; } else if (Array.isArray(value['@container'])) { const newValue: {[key: string]: boolean} = {}; for (const containerValue of value['@container']) { newValue[containerValue] = true; } + // @assignment value['@container'] = newValue; } } } + return context; } /** @@ -256,20 +263,24 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR if (value && typeof value === 'object') { if (!('@protected' in context[key])) { // Mark terms with object values as protected if they don't have an @protected: false annotation + // @assignment context[key]['@protected'] = true; } } else { // Convert string-based term values to object-based values with @protected: true + // @assignment context[key] = { '@id': value, '@protected': true, }; if (Util.isSimpleTermDefinitionPrefix(value, expandOptions)) { + // @assignment context[key]['@prefix'] = true } } } } + // @assignment delete context['@protected']; } } @@ -290,6 +301,7 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR // If the new entry is in string-mode, convert it to object-mode // before checking if it is identical. if (typeof contextAfter[key] === 'string') { + // @assignment contextAfter[key] = { '@id': contextAfter[key] }; } @@ -298,6 +310,7 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR // We modify this deliberately, // as we need it for the value comparison (they must be identical modulo '@protected')), // and for the fact that this new value will override the first one. + // @assignment contextAfter[key]['@protected'] = true; const valueAfter = canonicalizeJson(contextAfter[key]); @@ -543,8 +556,10 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP // Give priority to @base in the parent context if (inheritFromParent && !('@base' in context) && options.parentContext && typeof options.parentContext === 'object' && '@base' in options.parentContext) { + // @assignment context['@base'] = options.parentContext['@base']; if (options.parentContext['@__baseDocument']) { + // @assignment context['@__baseDocument'] = true; } } @@ -553,11 +568,14 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP if (options.baseIRI && !options.external) { if (!('@base' in context)) { // The context base is the document base + // @assignment context['@base'] = options.baseIRI; + // @assignment context['@__baseDocument'] = true; } else if (context['@base'] !== null && typeof context['@base'] === 'string' && !Util.isValidIri( context['@base'])) { // The context base is relative to the document base + // @assignment context['@base'] = resolve( context['@base'], options.parentContext && options.parentContext['@base'] || options.baseIRI); } @@ -596,6 +614,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP */ public async parseInnerContexts(context: IJsonLdContextNormalizedRaw, options: IParseOptions) : Promise { + let newContext: IJsonLdContextNormalizedRaw = {}; for (const key of Object.keys(context)) { const value = context[key]; if (value && typeof value === 'object') { @@ -607,8 +626,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP // https://w3c.github.io/json-ld-api/#h-note-10 if (this.validateContext) { try { - const parentContext = {...context}; - parentContext[key] = {...parentContext[key]}; + const parentContext = {...context, [key]: {...context[key]}}; delete parentContext[key]['@context']; await this.parse(value['@context'], { ...options, external: false, parentContext, ignoreProtection: true, ignoreRemoteScopedContexts: true, ignoreScopedContexts: true }); @@ -616,10 +634,9 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP throw new ErrorCoded(e.message, ERROR_CODES.INVALID_SCOPED_CONTEXT); } } - value['@context'] = (await this.parse(value['@context'], { ...options, external: false, minimalProcessing: true, ignoreRemoteScopedContexts: true, parentContext: context })) - .getContextRaw(); + .getContextRaw() ; } } } @@ -725,6 +742,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP // According to the JSON-LD spec, @base must be ignored from external contexts. if (external) { + // @assignment delete context['@base']; } @@ -733,7 +751,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP // Hashify container entries // Do this before protected term validation as that influences term format - this.containersToHash(context); + context = this.containersToHash(context); // Don't perform any other modifications if only minimal processing is needed. if (minimalProcessing) { @@ -752,6 +770,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP // Load context importContext = await this.loadImportContext(this.normalizeContextIri(context['@import'], baseIRI)); + // @assignment delete context['@import']; } else { throw new ErrorCoded('Context importing is not supported in JSON-LD 1.0', @@ -776,10 +795,12 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP if ((newContext && newContext['@version'] || ContextParser.DEFAULT_PROCESSING_MODE) >= 1.1 && ((context['@vocab'] && typeof context['@vocab'] === 'string') || context['@vocab'] === '')) { if (parentContext && '@vocab' in parentContext && context['@vocab'].indexOf(':') < 0) { + // @assignment newContext['@vocab'] = parentContext['@vocab'] + context['@vocab']; } else { if (Util.isCompactIri(context['@vocab']) || context['@vocab'] in newContextWrapped.getContextRaw()) { // @vocab is a compact IRI or refers exactly to a prefix + // @assignment newContext['@vocab'] = newContextWrapped.expandTerm(context['@vocab'], true); } } @@ -816,7 +837,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP // First try to retrieve the context from cache const cached = this.documentCache[url]; if (cached) { - return typeof cached === 'string' ? cached : Array.isArray(cached) ? cached.slice() : {... cached}; + return cached; } // If not in cache, load it @@ -880,8 +901,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP // Containers have to be converted into hash values the same way as for the importing context // Otherwise context validation will fail for container values - this.containersToHash(importContext); - return importContext; + return this.containersToHash(importContext); } } From c15dda3bdea1059f25067537238d387c2974e621 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sat, 28 Oct 2023 23:01:33 +0100 Subject: [PATCH 3/7] chore: remove some assignments --- lib/ContextParser.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/ContextParser.ts b/lib/ContextParser.ts index 4b4714e..1b9805c 100644 --- a/lib/ContextParser.ts +++ b/lib/ContextParser.ts @@ -614,9 +614,9 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP */ public async parseInnerContexts(context: IJsonLdContextNormalizedRaw, options: IParseOptions) : Promise { - let newContext: IJsonLdContextNormalizedRaw = {}; - for (const key of Object.keys(context)) { - const value = context[key]; + let newContext: IJsonLdContextNormalizedRaw = {...context}; + for (const key of Object.keys(newContext)) { + const value = newContext[key]; if (value && typeof value === 'object') { if ('@context' in value && value['@context'] !== null && !options.ignoreScopedContexts) { // Simulate a processing based on the parent context to check if there are any (potential errors). @@ -626,7 +626,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP // https://w3c.github.io/json-ld-api/#h-note-10 if (this.validateContext) { try { - const parentContext = {...context, [key]: {...context[key]}}; + const parentContext = {...newContext, [key]: {...newContext[key]}}; delete parentContext[key]['@context']; await this.parse(value['@context'], { ...options, external: false, parentContext, ignoreProtection: true, ignoreRemoteScopedContexts: true, ignoreScopedContexts: true }); @@ -636,11 +636,11 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP } value['@context'] = (await this.parse(value['@context'], { ...options, external: false, minimalProcessing: true, ignoreRemoteScopedContexts: true, parentContext: context })) - .getContextRaw() ; + .getContextRaw(); } } } - return context; + return newContext; } /** @@ -786,10 +786,11 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP newContext = { ...parentContext, ...newContext }; } + // Parse inner contexts with minimal processing + newContext = await this.parseInnerContexts(newContext, options); + const newContextWrapped = new JsonLdContextNormalized(newContext); - // Parse inner contexts with minimal processing - await this.parseInnerContexts(newContext, options); // In JSON-LD 1.1, @vocab can be relative to @vocab in the parent context, or a compact IRI. if ((newContext && newContext['@version'] || ContextParser.DEFAULT_PROCESSING_MODE) >= 1.1 From 4d8bb125ec34a91c69dd1a110ba41c3d2ef7dcd6 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sun, 29 Oct 2023 00:00:23 +0100 Subject: [PATCH 4/7] perf: remove need for deep cloning --- lib/ContextParser.ts | 111 +++++++++++++++++++++++-------------------- 1 file changed, 59 insertions(+), 52 deletions(-) diff --git a/lib/ContextParser.ts b/lib/ContextParser.ts index 1b9805c..154293a 100644 --- a/lib/ContextParser.ts +++ b/lib/ContextParser.ts @@ -9,6 +9,13 @@ import {Util} from "./Util"; // tslint:disable-next-line:no-var-requires const canonicalizeJson = require('canonicalize'); +const deepFreeze = (obj: any) => { + Object.keys(obj).forEach(prop => { + if (typeof obj[prop] === 'object' && !Object.isFrozen(obj[prop])) deepFreeze(obj[prop]); + }); + return Object.freeze(obj); + // return obj; +}; /** * Parses JSON-LD contexts. @@ -92,16 +99,18 @@ export class ContextParser { * @return {IJsonLdContextNormalizedRaw} The mutated input context. */ public idifyReverseTerms(context: IJsonLdContextNormalizedRaw): IJsonLdContextNormalizedRaw { - for (const value of Object.values(context)) { + for (const key of Object.keys(context)) { + let value = context[key]; if (value && typeof value === 'object') { if (value['@reverse'] && !value['@id']) { if (typeof value['@reverse'] !== 'string' || Util.isValidKeyword(value['@reverse'])) { throw new ErrorCoded(`Invalid @reverse value, must be absolute IRI or blank node: '${value['@reverse']}'`, ERROR_CODES.INVALID_IRI_MAPPING); } + // @-assignment + value = context[key] = {...value, '@id': value['@reverse']}; value['@id'] = value['@reverse']; if (Util.isPotentialKeyword(value['@reverse'])) { - // @assignment delete value['@reverse']; } else { value['@reverse'] = true; @@ -152,7 +161,7 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR const value: IPrefixValue = contextRaw[key]; let changed: boolean = false; if (typeof value === 'string') { - // @assignment + // @-assignment contextRaw[key] = context.expandTerm(value, true); changed = changed || value !== contextRaw[key]; } else { @@ -163,8 +172,8 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR if ('@id' in value) { // Use @id value for expansion if (id !== undefined && id !== null && typeof id === 'string') { - // @assignment - contextRaw[key]['@id'] = context.expandTerm(id, true); + // @-assignment + contextRaw[key] = { ...contextRaw[key], '@id': context.expandTerm(id, true) }; changed = changed || id !== contextRaw[key]['@id']; } } else if (!Util.isPotentialKeyword(key) && canAddIdEntry) { @@ -172,8 +181,8 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR const newId = context.expandTerm(key, true); if (newId !== key) { // Don't set @id if expansion failed - // @assignment - contextRaw[key]['@id'] = newId; + // @-assignment + contextRaw[key] = { ...contextRaw[key], '@id': newId }; changed = true; } } @@ -181,13 +190,12 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR && (!value['@container'] || !( value['@container'])['@type']) && canAddIdEntry) { // First check @vocab, then fallback to @base - // @assignment - contextRaw[key]['@type'] = context.expandTerm(type, true); - if (expandContentTypeToBase && type === contextRaw[key]['@type']) { - // @assignment - contextRaw[key]['@type'] = context.expandTerm(type, false); + // @-assignment + const expandedType = context.expandTerm(type, !(expandContentTypeToBase && type === contextRaw[key]['@type'])); + if (expandedType !== type) { + changed = true; + contextRaw[key] = { ...contextRaw[key], '@type': expandedType }; } - changed = changed || type !== contextRaw[key]['@type']; } } if (!changed) { @@ -214,7 +222,10 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR const value = context[key]; if (value && typeof value === 'object') { if (typeof value['@language'] === 'string') { - value['@language'] = value['@language'].toLowerCase(); + const lowercase = value['@language'].toLowerCase(); + if (lowercase !== value['@language']) { + context[key] = {...value, '@language': lowercase}; + } } } } @@ -227,18 +238,20 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR * @param {IJsonLdContextNormalizedRaw} context A context. */ public containersToHash(context: IJsonLdContextNormalizedRaw) { - for (const value of Object.values(context)) { + // let context = { ...c} + for (const key of Object.keys(context)) { + const value = context[key]; if (value && typeof value === 'object') { if (typeof value['@container'] === 'string') { - // @assignment - value['@container'] = { [value['@container']]: true }; + // @-assignment + context[key] = { ...value, '@container': { [value['@container']]: true } }; } else if (Array.isArray(value['@container'])) { const newValue: {[key: string]: boolean} = {}; for (const containerValue of value['@container']) { newValue[containerValue] = true; } - // @assignment - value['@container'] = newValue; + // @-assignment + context[key] = { ...value, '@container': newValue }; } } } @@ -263,24 +276,23 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR if (value && typeof value === 'object') { if (!('@protected' in context[key])) { // Mark terms with object values as protected if they don't have an @protected: false annotation - // @assignment - context[key]['@protected'] = true; + context[key] = {...context[key], '@protected': true}; } } else { // Convert string-based term values to object-based values with @protected: true - // @assignment + // @-assignment context[key] = { '@id': value, '@protected': true, }; if (Util.isSimpleTermDefinitionPrefix(value, expandOptions)) { - // @assignment - context[key]['@prefix'] = true + // @-assignment + context[key] = {...context[key], '@prefix': true}; } } } } - // @assignment + // @-assignment delete context['@protected']; } } @@ -301,7 +313,7 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR // If the new entry is in string-mode, convert it to object-mode // before checking if it is identical. if (typeof contextAfter[key] === 'string') { - // @assignment + // @-assignment contextAfter[key] = { '@id': contextAfter[key] }; } @@ -310,8 +322,8 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR // We modify this deliberately, // as we need it for the value comparison (they must be identical modulo '@protected')), // and for the fact that this new value will override the first one. - // @assignment - contextAfter[key]['@protected'] = true; + // @-assignment + contextAfter[key] = {...contextAfter[key], '@protected': true}; const valueAfter = canonicalizeJson(contextAfter[key]); // Error if they are not identical @@ -556,10 +568,10 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP // Give priority to @base in the parent context if (inheritFromParent && !('@base' in context) && options.parentContext && typeof options.parentContext === 'object' && '@base' in options.parentContext) { - // @assignment + // @-assignment context['@base'] = options.parentContext['@base']; if (options.parentContext['@__baseDocument']) { - // @assignment + // @-assignment context['@__baseDocument'] = true; } } @@ -568,14 +580,14 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP if (options.baseIRI && !options.external) { if (!('@base' in context)) { // The context base is the document base - // @assignment + // @-assignment context['@base'] = options.baseIRI; - // @assignment + // @-assignment context['@__baseDocument'] = true; } else if (context['@base'] !== null && typeof context['@base'] === 'string' && !Util.isValidIri( context['@base'])) { // The context base is relative to the document base - // @assignment + // @-assignment context['@base'] = resolve( context['@base'], options.parentContext && options.parentContext['@base'] || options.baseIRI); } @@ -614,9 +626,8 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP */ public async parseInnerContexts(context: IJsonLdContextNormalizedRaw, options: IParseOptions) : Promise { - let newContext: IJsonLdContextNormalizedRaw = {...context}; - for (const key of Object.keys(newContext)) { - const value = newContext[key]; + for (const key of Object.keys(context)) { + const value = context[key]; if (value && typeof value === 'object') { if ('@context' in value && value['@context'] !== null && !options.ignoreScopedContexts) { // Simulate a processing based on the parent context to check if there are any (potential errors). @@ -626,7 +637,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP // https://w3c.github.io/json-ld-api/#h-note-10 if (this.validateContext) { try { - const parentContext = {...newContext, [key]: {...newContext[key]}}; + const parentContext = {...context, [key]: {...context[key]}}; delete parentContext[key]['@context']; await this.parse(value['@context'], { ...options, external: false, parentContext, ignoreProtection: true, ignoreRemoteScopedContexts: true, ignoreScopedContexts: true }); @@ -634,13 +645,13 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP throw new ErrorCoded(e.message, ERROR_CODES.INVALID_SCOPED_CONTEXT); } } - value['@context'] = (await this.parse(value['@context'], - { ...options, external: false, minimalProcessing: true, ignoreRemoteScopedContexts: true, parentContext: context })) - .getContextRaw(); + context[key] = {...value, '@context': (await this.parse(value['@context'], + { ...options, external: false, minimalProcessing: true, ignoreRemoteScopedContexts: true, parentContext: context })) + .getContextRaw()} } } } - return newContext; + return context; } /** @@ -735,14 +746,13 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP } // Make a deep clone of the given context, to avoid modifying it. - context = JSON.parse(JSON.stringify(context)); // No better way in JS at the moment. + context = {...context}; // No better way in JS at the moment. if (parentContext && !minimalProcessing) { - parentContext = JSON.parse(JSON.stringify(parentContext)); + parentContext = {...parentContext}; } // According to the JSON-LD spec, @base must be ignored from external contexts. if (external) { - // @assignment delete context['@base']; } @@ -770,7 +780,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP // Load context importContext = await this.loadImportContext(this.normalizeContextIri(context['@import'], baseIRI)); - // @assignment + // @-assignment delete context['@import']; } else { throw new ErrorCoded('Context importing is not supported in JSON-LD 1.0', @@ -796,14 +806,10 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP if ((newContext && newContext['@version'] || ContextParser.DEFAULT_PROCESSING_MODE) >= 1.1 && ((context['@vocab'] && typeof context['@vocab'] === 'string') || context['@vocab'] === '')) { if (parentContext && '@vocab' in parentContext && context['@vocab'].indexOf(':') < 0) { - // @assignment newContext['@vocab'] = parentContext['@vocab'] + context['@vocab']; - } else { - if (Util.isCompactIri(context['@vocab']) || context['@vocab'] in newContextWrapped.getContextRaw()) { + } else if (Util.isCompactIri(context['@vocab']) || context['@vocab'] in newContext) { // @vocab is a compact IRI or refers exactly to a prefix - // @assignment - newContext['@vocab'] = newContextWrapped.expandTerm(context['@vocab'], true); - } + newContext['@vocab'] = newContextWrapped.expandTerm(context['@vocab'], true); } } @@ -886,7 +892,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP */ public async loadImportContext(importContextIri: string): Promise { // Load the context - and do a deep clone since we are about to mutate it - const importContext = JSON.parse(JSON.stringify(await this.load(importContextIri))); + let importContext = await this.load(importContextIri); // Require the context to be a non-array object if (typeof importContext !== 'object' || Array.isArray(importContext)) { @@ -899,6 +905,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP throw new ErrorCoded('An imported context can not import another context: ' + importContextIri, ERROR_CODES.INVALID_CONTEXT_ENTRY); } + importContext = {...importContext}; // Containers have to be converted into hash values the same way as for the importing context // Otherwise context validation will fail for container values From 3694fc585aa718f09a2e60791fb0981f176a34b0 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sun, 29 Oct 2023 00:03:14 +0100 Subject: [PATCH 5/7] perf: remove all deep cloning --- lib/ContextParser.ts | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/lib/ContextParser.ts b/lib/ContextParser.ts index 154293a..a8519f5 100644 --- a/lib/ContextParser.ts +++ b/lib/ContextParser.ts @@ -9,13 +9,6 @@ import {Util} from "./Util"; // tslint:disable-next-line:no-var-requires const canonicalizeJson = require('canonicalize'); -const deepFreeze = (obj: any) => { - Object.keys(obj).forEach(prop => { - if (typeof obj[prop] === 'object' && !Object.isFrozen(obj[prop])) deepFreeze(obj[prop]); - }); - return Object.freeze(obj); - // return obj; -}; /** * Parses JSON-LD contexts. @@ -107,7 +100,6 @@ export class ContextParser { throw new ErrorCoded(`Invalid @reverse value, must be absolute IRI or blank node: '${value['@reverse']}'`, ERROR_CODES.INVALID_IRI_MAPPING); } - // @-assignment value = context[key] = {...value, '@id': value['@reverse']}; value['@id'] = value['@reverse']; if (Util.isPotentialKeyword(value['@reverse'])) { @@ -161,7 +153,6 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR const value: IPrefixValue = contextRaw[key]; let changed: boolean = false; if (typeof value === 'string') { - // @-assignment contextRaw[key] = context.expandTerm(value, true); changed = changed || value !== contextRaw[key]; } else { @@ -172,7 +163,6 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR if ('@id' in value) { // Use @id value for expansion if (id !== undefined && id !== null && typeof id === 'string') { - // @-assignment contextRaw[key] = { ...contextRaw[key], '@id': context.expandTerm(id, true) }; changed = changed || id !== contextRaw[key]['@id']; } @@ -181,7 +171,6 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR const newId = context.expandTerm(key, true); if (newId !== key) { // Don't set @id if expansion failed - // @-assignment contextRaw[key] = { ...contextRaw[key], '@id': newId }; changed = true; } @@ -190,7 +179,6 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR && (!value['@container'] || !( value['@container'])['@type']) && canAddIdEntry) { // First check @vocab, then fallback to @base - // @-assignment const expandedType = context.expandTerm(type, !(expandContentTypeToBase && type === contextRaw[key]['@type'])); if (expandedType !== type) { changed = true; @@ -238,19 +226,16 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR * @param {IJsonLdContextNormalizedRaw} context A context. */ public containersToHash(context: IJsonLdContextNormalizedRaw) { - // let context = { ...c} for (const key of Object.keys(context)) { const value = context[key]; if (value && typeof value === 'object') { if (typeof value['@container'] === 'string') { - // @-assignment context[key] = { ...value, '@container': { [value['@container']]: true } }; } else if (Array.isArray(value['@container'])) { const newValue: {[key: string]: boolean} = {}; for (const containerValue of value['@container']) { newValue[containerValue] = true; } - // @-assignment context[key] = { ...value, '@container': newValue }; } } @@ -280,19 +265,16 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR } } else { // Convert string-based term values to object-based values with @protected: true - // @-assignment context[key] = { '@id': value, '@protected': true, }; if (Util.isSimpleTermDefinitionPrefix(value, expandOptions)) { - // @-assignment context[key] = {...context[key], '@prefix': true}; } } } } - // @-assignment delete context['@protected']; } } @@ -313,7 +295,6 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR // If the new entry is in string-mode, convert it to object-mode // before checking if it is identical. if (typeof contextAfter[key] === 'string') { - // @-assignment contextAfter[key] = { '@id': contextAfter[key] }; } @@ -322,7 +303,6 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR // We modify this deliberately, // as we need it for the value comparison (they must be identical modulo '@protected')), // and for the fact that this new value will override the first one. - // @-assignment contextAfter[key] = {...contextAfter[key], '@protected': true}; const valueAfter = canonicalizeJson(contextAfter[key]); @@ -568,10 +548,8 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP // Give priority to @base in the parent context if (inheritFromParent && !('@base' in context) && options.parentContext && typeof options.parentContext === 'object' && '@base' in options.parentContext) { - // @-assignment context['@base'] = options.parentContext['@base']; if (options.parentContext['@__baseDocument']) { - // @-assignment context['@__baseDocument'] = true; } } @@ -580,14 +558,11 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP if (options.baseIRI && !options.external) { if (!('@base' in context)) { // The context base is the document base - // @-assignment context['@base'] = options.baseIRI; - // @-assignment context['@__baseDocument'] = true; } else if (context['@base'] !== null && typeof context['@base'] === 'string' && !Util.isValidIri( context['@base'])) { // The context base is relative to the document base - // @-assignment context['@base'] = resolve( context['@base'], options.parentContext && options.parentContext['@base'] || options.baseIRI); } @@ -780,7 +755,6 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP // Load context importContext = await this.loadImportContext(this.normalizeContextIri(context['@import'], baseIRI)); - // @-assignment delete context['@import']; } else { throw new ErrorCoded('Context importing is not supported in JSON-LD 1.0', From 9d54b88555de9d10c8a32821c3c43841dcbe302d Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sun, 29 Oct 2023 00:10:44 +0100 Subject: [PATCH 6/7] feat: fix term expansion code --- lib/ContextParser.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/ContextParser.ts b/lib/ContextParser.ts index a8519f5..ac363ab 100644 --- a/lib/ContextParser.ts +++ b/lib/ContextParser.ts @@ -179,7 +179,10 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR && (!value['@container'] || !( value['@container'])['@type']) && canAddIdEntry) { // First check @vocab, then fallback to @base - const expandedType = context.expandTerm(type, !(expandContentTypeToBase && type === contextRaw[key]['@type'])); + let expandedType = context.expandTerm(type, true); + if (expandContentTypeToBase && type === expandedType) { + expandedType = context.expandTerm(type, false); + } if (expandedType !== type) { changed = true; contextRaw[key] = { ...contextRaw[key], '@type': expandedType }; From 786f1547e9c26b7a6f0abe0b6bd731fc6b076e88 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sun, 29 Oct 2023 00:23:27 +0100 Subject: [PATCH 7/7] Update lib/ContextParser.ts --- lib/ContextParser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ContextParser.ts b/lib/ContextParser.ts index ac363ab..107ffcc 100644 --- a/lib/ContextParser.ts +++ b/lib/ContextParser.ts @@ -724,7 +724,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP } // Make a deep clone of the given context, to avoid modifying it. - context = {...context}; // No better way in JS at the moment. + context = {...context}; if (parentContext && !minimalProcessing) { parentContext = {...parentContext}; }