Skip to content

Commit 2cf1f59

Browse files
authored
Fix incorrect handling of protected prefixes
Closes rubensworks/jsonld-streaming-parser.js#112 Closes rubensworks/jsonld-streaming-parser.js#117
1 parent 426120c commit 2cf1f59

File tree

3 files changed

+289
-23
lines changed

3 files changed

+289
-23
lines changed

lib/ContextParser.ts

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -239,11 +239,11 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR
239239
}
240240

241241
/**
242-
* Normalize and apply context-levevl @protected terms onto each term separately.
242+
* Normalize and apply context-level @protected terms onto each term separately.
243243
* @param {IJsonLdContextNormalizedRaw} context A context.
244244
* @param {number} processingMode The processing mode.
245245
*/
246-
public applyScopedProtected(context: IJsonLdContextNormalizedRaw, { processingMode }: IParseOptions) {
246+
public applyScopedProtected(context: IJsonLdContextNormalizedRaw, { processingMode }: IParseOptions, expandOptions: IExpandOptions) {
247247
if (processingMode && processingMode >= 1.1) {
248248
if (context['@protected']) {
249249
for (const key of Object.keys(context)) {
@@ -264,6 +264,9 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR
264264
'@id': value,
265265
'@protected': true,
266266
};
267+
if (Util.isSimpleTermDefinitionPrefix(value, expandOptions)) {
268+
context[key]['@prefix'] = true
269+
}
267270
}
268271
}
269272
}
@@ -287,16 +290,7 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR
287290
// If the new entry is in string-mode, convert it to object-mode
288291
// before checking if it is identical.
289292
if (typeof contextAfter[key] === 'string') {
290-
const isPrefix = Util.isSimpleTermDefinitionPrefix(contextAfter[key], expandOptions);
291293
contextAfter[key] = { '@id': contextAfter[key] };
292-
293-
// If the simple term def was a prefix, explicitly mark the term as a prefix in the expanded term definition,
294-
// because otherwise we loose this information due to JSON-LD interpreting prefixes differently
295-
// in simple vs expanded term definitions.
296-
if (isPrefix) {
297-
contextAfter[key]['@prefix'] = true;
298-
contextBefore[key]['@prefix'] = true; // Also on before, to make sure the next step still considers them ==
299-
}
300294
}
301295

302296
// Convert term values to strings for each comparison
@@ -729,9 +723,6 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
729723
parentContext = <IJsonLdContextNormalizedRaw> JSON.parse(JSON.stringify(parentContext));
730724
}
731725

732-
// We have an actual context object.
733-
let newContext: IJsonLdContextNormalizedRaw = {};
734-
735726
// According to the JSON-LD spec, @base must be ignored from external contexts.
736727
if (external) {
737728
delete context['@base'];
@@ -768,13 +759,14 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
768759
}
769760
}
770761

771-
// Merge different parts of the final context in order
772-
newContext = {
773-
...newContext,
774-
...(typeof parentContext === 'object' ? parentContext : {}),
775-
...importContext,
776-
...context,
777-
};
762+
this.applyScopedProtected(importContext, { processingMode }, defaultExpandOptions);
763+
let newContext: IJsonLdContextNormalizedRaw = { ...importContext, ...context };
764+
if (typeof parentContext === 'object') {
765+
// Merge different parts of the final context in order
766+
this.applyScopedProtected(newContext, { processingMode }, defaultExpandOptions);
767+
newContext = { ...parentContext, ...newContext };
768+
}
769+
778770
const newContextWrapped = new JsonLdContextNormalized(newContext);
779771

780772
// Parse inner contexts with minimal processing
@@ -803,7 +795,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
803795
}
804796

805797
this.normalize(newContext, { processingMode, normalizeLanguageTags });
806-
this.applyScopedProtected(newContext, { processingMode });
798+
this.applyScopedProtected(newContext, { processingMode }, defaultExpandOptions);
807799
if (this.validateContext) {
808800
this.validate(newContext, { processingMode });
809801
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
"test": "jest ${1}",
7777
"test-watch": "jest ${1} --watch",
7878
"coveralls": "jest --coverage && cat ./coverage/lcov.info | coveralls",
79-
"lint": "tslint index.ts lib/**/*.ts test/**/*.ts --exclude '**/*.d.ts'",
79+
"lint": "tslint index.ts lib/**/*.ts test/**/*.ts test/*.ts --exclude '**/*.d.ts'",
8080
"build": "tsc",
8181
"build-watch": "tsc --watch",
8282
"validate": "npm ls",

test/ContextParser-test.ts

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2132,6 +2132,210 @@ Tried mapping @id to {}`, ERROR_CODES.KEYWORD_REDEFINITION));
21322132
}));
21332133
});
21342134

2135+
it('should parse 2 contexts where one is protected', () => {
2136+
return expect(parser.parse([
2137+
{
2138+
"@version": 1.0,
2139+
"ex":"https://example.org/ns/"
2140+
},
2141+
{
2142+
"@version": 1.1,
2143+
"@protected": true,
2144+
"VerifiableCredential": {
2145+
"@id": "https://www.w3.org/2018/credentials#VerifiableCredential",
2146+
}
2147+
}
2148+
])).resolves.toEqual(new JsonLdContextNormalized({
2149+
"@version": 1.1,
2150+
VerifiableCredential: {
2151+
"@id": "https://www.w3.org/2018/credentials#VerifiableCredential",
2152+
"@protected": true,
2153+
},
2154+
ex: "https://example.org/ns/"
2155+
}));
2156+
});
2157+
2158+
const contexts = [
2159+
{
2160+
"@version": 1.1,
2161+
"@protected": true,
2162+
"VerifiableCredential": {
2163+
"@id": "https://www.w3.org/2018/credentials#VerifiableCredential",
2164+
}
2165+
},
2166+
{
2167+
"ex":"https://example.org/ns/",
2168+
"@version": 1.1,
2169+
"@protected": false,
2170+
}
2171+
];
2172+
2173+
const result = new JsonLdContextNormalized({
2174+
"@version": 1.1,
2175+
"@protected": false,
2176+
VerifiableCredential: {
2177+
"@id": "https://www.w3.org/2018/credentials#VerifiableCredential",
2178+
"@protected": true,
2179+
},
2180+
ex: "https://example.org/ns/"
2181+
});
2182+
2183+
it('should parse 2 contexts where one is protected and one globally has protected false', () => {
2184+
return expect(parser.parse(contexts)).resolves.toEqual(result);
2185+
});
2186+
2187+
it('should parse 2 contexts where one is protected and one globally has protected false [reverse order]', () => {
2188+
return expect(parser.parse([contexts[1], contexts[0]])).resolves.toEqual(result);
2189+
});
2190+
2191+
const BASIC_VC_CONTEXT = {
2192+
"@version": 1.1,
2193+
"@protected": true,
2194+
"VerifiableCredential": {
2195+
"@id": "https://www.w3.org/2018/credentials#VerifiableCredential",
2196+
}
2197+
}
2198+
2199+
it('protected context should override the unprotected context', () => {
2200+
return expect(parser.parse([
2201+
{
2202+
"@version": 1.0,
2203+
"VerifiableCredential":"https://example.org/ns/"
2204+
},
2205+
BASIC_VC_CONTEXT
2206+
])).resolves.toEqual(new JsonLdContextNormalized({
2207+
"@version": 1.1,
2208+
VerifiableCredential: {
2209+
"@id": "https://www.w3.org/2018/credentials#VerifiableCredential",
2210+
"@protected": true,
2211+
}
2212+
}));
2213+
});
2214+
2215+
describe('with imported contexts', () => {
2216+
beforeEach(() => {
2217+
jest.mocked(globalThis.fetch).mockImplementationOnce(async (...args) => {
2218+
return new Response(JSON.stringify({
2219+
"@context": {
2220+
"MyType": {
2221+
"@id": "http://example.org#MyType",
2222+
}
2223+
}
2224+
}), { headers: new Headers([
2225+
['content-type', 'application/ld+json']
2226+
]) })
2227+
});
2228+
});
2229+
2230+
it('protected should be applied to the imported context', () => {
2231+
return expect(parser.parse([
2232+
{
2233+
...BASIC_VC_CONTEXT,
2234+
"@import": "http://example.org/imported/context"
2235+
}
2236+
])).resolves.toEqual(new JsonLdContextNormalized({
2237+
"@version": 1.1,
2238+
VerifiableCredential: {
2239+
"@id": "https://www.w3.org/2018/credentials#VerifiableCredential",
2240+
"@protected": true,
2241+
},
2242+
MyType: {
2243+
"@id": "http://example.org#MyType",
2244+
"@protected": true,
2245+
}
2246+
}));
2247+
});
2248+
2249+
it.each([
2250+
['child', [BASIC_VC_CONTEXT, {"@import": "http://example.org/imported/context"}]],
2251+
['parent', [{"@import": "http://example.org/imported/context"}, BASIC_VC_CONTEXT]],
2252+
])('protected should not be applied to the imported contexts in other %s contexts', (_, context) => {
2253+
return expect(parser.parse(context)).resolves.toEqual(new JsonLdContextNormalized({
2254+
"@version": 1.1,
2255+
VerifiableCredential: {
2256+
"@id": "https://www.w3.org/2018/credentials#VerifiableCredential",
2257+
"@protected": true,
2258+
},
2259+
MyType: { "@id": "http://example.org#MyType" }
2260+
}));
2261+
});
2262+
});
2263+
2264+
2265+
describe('with imported contexts containing the @protected keyword', () => {
2266+
2267+
const BASIC_VC_CONTEXT_WITHOUT_PROTECTED = {
2268+
"@version": 1.1,
2269+
"VerifiableCredential": {
2270+
"@id": "https://www.w3.org/2018/credentials#VerifiableCredential",
2271+
}
2272+
}
2273+
2274+
beforeEach(() => {
2275+
jest.mocked(globalThis.fetch).mockImplementationOnce(async (...args) => {
2276+
return new Response(JSON.stringify({
2277+
"@context": {
2278+
"@protected": true,
2279+
"MyType": "http://example.org#MyType"
2280+
}
2281+
}), { headers: new Headers([
2282+
['content-type', 'application/ld+json']
2283+
]) })
2284+
});
2285+
});
2286+
2287+
it('protected should be applied to the imported context', () => {
2288+
return expect(parser.parse([
2289+
{
2290+
...BASIC_VC_CONTEXT_WITHOUT_PROTECTED,
2291+
"@import": "http://example.org/imported/context"
2292+
}
2293+
])).resolves.toEqual(new JsonLdContextNormalized({
2294+
"@version": 1.1,
2295+
VerifiableCredential: {
2296+
"@id": "https://www.w3.org/2018/credentials#VerifiableCredential",
2297+
},
2298+
MyType: {
2299+
"@id": "http://example.org#MyType",
2300+
"@protected": true,
2301+
}
2302+
}));
2303+
});
2304+
2305+
it.each([
2306+
['child', [BASIC_VC_CONTEXT_WITHOUT_PROTECTED, {"@import": "http://example.org/imported/context"}]],
2307+
['parent', [{"@import": "http://example.org/imported/context"}, BASIC_VC_CONTEXT_WITHOUT_PROTECTED]],
2308+
])('protected should not be applied to the imported contexts in other %s contexts', (_, context) => {
2309+
return expect(parser.parse(context)).resolves.toEqual(new JsonLdContextNormalized({
2310+
"@version": 1.1,
2311+
VerifiableCredential: {
2312+
"@id": "https://www.w3.org/2018/credentials#VerifiableCredential",
2313+
},
2314+
MyType: { "@id": "http://example.org#MyType", "@protected": true }
2315+
}));
2316+
});
2317+
});
2318+
2319+
it('should parse 2 contexts where one is protected', () => {
2320+
return expect(parser.parse([
2321+
{"ex":"https://example.org/ns/"},
2322+
{
2323+
"@version": 1.1,
2324+
"@protected": true,
2325+
"VerifiableCredential": {
2326+
"@id": "https://www.w3.org/2018/credentials#VerifiableCredential",
2327+
}
2328+
}
2329+
])).resolves.toEqual(new JsonLdContextNormalized({
2330+
"@version": 1.1,
2331+
VerifiableCredential: {
2332+
"@id": "https://www.w3.org/2018/credentials#VerifiableCredential",
2333+
"@protected": true,
2334+
},
2335+
ex: "https://example.org/ns/"
2336+
}));
2337+
});
2338+
21352339
it('should parse a single keyword alias', () => {
21362340
return expect(parser.parse({
21372341
id: {
@@ -2214,6 +2418,60 @@ Tried mapping @id to {}`, ERROR_CODES.KEYWORD_REDEFINITION));
22142418
ERROR_CODES.PROTECTED_TERM_REDEFINITION));
22152419
});
22162420

2421+
it('should error on a protected term with override when the overriding version is 1.0', () => {
2422+
return expect(parser.parse([
2423+
{
2424+
name: {
2425+
'@id': 'http://xmlns.com/foaf/0.1/name',
2426+
'@protected': true,
2427+
},
2428+
},
2429+
{
2430+
"@version": 1.0,
2431+
name: 'http://schema.org/name',
2432+
},
2433+
])).rejects.toThrow(new ErrorCoded(
2434+
'Attempted to override the protected keyword name from ' +
2435+
'"http://xmlns.com/foaf/0.1/name" to "http://schema.org/name"',
2436+
ERROR_CODES.PROTECTED_TERM_REDEFINITION));
2437+
});
2438+
2439+
it('should error on an outer scope protected term with override of another protected term', () => {
2440+
return expect(parser.parse([
2441+
{
2442+
"VerifiableCredential":"https://example.org/ns/",
2443+
"@protected": true
2444+
},
2445+
{
2446+
"@version": 1.1,
2447+
"@protected": true,
2448+
"VerifiableCredential": {
2449+
"@id": "https://www.w3.org/2018/credentials#VerifiableCredential",
2450+
}
2451+
}
2452+
], { processingMode: 1.1 })).rejects.toThrow(new ErrorCoded(
2453+
'Attempted to override the protected keyword VerifiableCredential from ' +
2454+
'"https://example.org/ns/" to "https://www.w3.org/2018/credentials#VerifiableCredential"',
2455+
ERROR_CODES.PROTECTED_TERM_REDEFINITION));
2456+
});
2457+
2458+
it('should error on an outer scope protected term with override', () => {
2459+
return expect(parser.parse([
2460+
{
2461+
'@protected': true,
2462+
name: {
2463+
'@id': 'http://xmlns.com/foaf/0.1/name',
2464+
},
2465+
},
2466+
{
2467+
name: 'http://schema.org/name',
2468+
},
2469+
], { processingMode: 1.1 })).rejects.toThrow(new ErrorCoded(
2470+
'Attempted to override the protected keyword name from ' +
2471+
'"http://xmlns.com/foaf/0.1/name" to "http://schema.org/name"',
2472+
ERROR_CODES.PROTECTED_TERM_REDEFINITION));
2473+
});
2474+
22172475
it('should not error on a protected term with override if ignoreProtection is true', () => {
22182476
return expect(parser.parse([
22192477
{
@@ -2482,6 +2740,21 @@ Tried mapping @id to {}`, ERROR_CODES.KEYWORD_REDEFINITION));
24822740
}));
24832741
});
24842742

2743+
it('should parse a globally protected string term ending in gen-delim', () => {
2744+
return expect(parser.parse([
2745+
{
2746+
'@protected': true,
2747+
'foo': 'http://example/foo#',
2748+
}
2749+
], { processingMode: 1.1 })).resolves.toEqual(new JsonLdContextNormalized({
2750+
foo: {
2751+
'@id': 'http://example/foo#',
2752+
'@prefix': true,
2753+
'@protected': true,
2754+
},
2755+
}));
2756+
});
2757+
24852758
it('should parse a globally protected string term ending in non-gen-delim with identical override', () => {
24862759
return expect(parser.parse([
24872760
{
@@ -2623,6 +2896,7 @@ Tried mapping @id to {}`, ERROR_CODES.KEYWORD_REDEFINITION));
26232896
"@base": "http://base.org/",
26242897
"ex": {
26252898
"@id": "http://ex.org/",
2899+
"@prefix": true,
26262900
"@protected": true
26272901
},
26282902
"foo": {

0 commit comments

Comments
 (0)