Skip to content

Fix handling of protected prefixes #65

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 14 additions & 22 deletions lib/ContextParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,11 +239,11 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR
}

/**
* Normalize and apply context-levevl @protected terms onto each term separately.
* Normalize and apply context-level @protected terms onto each term separately.
* @param {IJsonLdContextNormalizedRaw} context A context.
* @param {number} processingMode The processing mode.
*/
public applyScopedProtected(context: IJsonLdContextNormalizedRaw, { processingMode }: IParseOptions) {
public applyScopedProtected(context: IJsonLdContextNormalizedRaw, { processingMode }: IParseOptions, expandOptions: IExpandOptions) {
if (processingMode && processingMode >= 1.1) {
if (context['@protected']) {
for (const key of Object.keys(context)) {
Expand All @@ -264,6 +264,9 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR
'@id': value,
'@protected': true,
};
if (Util.isSimpleTermDefinitionPrefix(value, expandOptions)) {
context[key]['@prefix'] = true
}
}
}
}
Expand All @@ -287,16 +290,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') {
const isPrefix = Util.isSimpleTermDefinitionPrefix(contextAfter[key], expandOptions);
contextAfter[key] = { '@id': contextAfter[key] };

// If the simple term def was a prefix, explicitly mark the term as a prefix in the expanded term definition,
// because otherwise we loose this information due to JSON-LD interpreting prefixes differently
// in simple vs expanded term definitions.
if (isPrefix) {
contextAfter[key]['@prefix'] = true;
contextBefore[key]['@prefix'] = true; // Also on before, to make sure the next step still considers them ==
}
}

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

// We have an actual context object.
let newContext: IJsonLdContextNormalizedRaw = {};

// According to the JSON-LD spec, @base must be ignored from external contexts.
if (external) {
delete context['@base'];
Expand Down Expand Up @@ -768,13 +759,14 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
}
}

// Merge different parts of the final context in order
newContext = {
...newContext,
...(typeof parentContext === 'object' ? parentContext : {}),
...importContext,
...context,
};
this.applyScopedProtected(importContext, { processingMode }, defaultExpandOptions);
let newContext: IJsonLdContextNormalizedRaw = { ...importContext, ...context };
if (typeof parentContext === 'object') {
// Merge different parts of the final context in order
this.applyScopedProtected(newContext, { processingMode }, defaultExpandOptions);
newContext = { ...parentContext, ...newContext };
}

const newContextWrapped = new JsonLdContextNormalized(newContext);

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

this.normalize(newContext, { processingMode, normalizeLanguageTags });
this.applyScopedProtected(newContext, { processingMode });
this.applyScopedProtected(newContext, { processingMode }, defaultExpandOptions);
if (this.validateContext) {
this.validate(newContext, { processingMode });
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
"test": "jest ${1}",
"test-watch": "jest ${1} --watch",
"coveralls": "jest --coverage && cat ./coverage/lcov.info | coveralls",
"lint": "tslint index.ts lib/**/*.ts test/**/*.ts --exclude '**/*.d.ts'",
"lint": "tslint index.ts lib/**/*.ts test/**/*.ts test/*.ts --exclude '**/*.d.ts'",
"build": "tsc",
"build-watch": "tsc --watch",
"validate": "npm ls",
Expand Down
274 changes: 274 additions & 0 deletions test/ContextParser-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2132,6 +2132,210 @@ Tried mapping @id to {}`, ERROR_CODES.KEYWORD_REDEFINITION));
}));
});

it('should parse 2 contexts where one is protected', () => {
return expect(parser.parse([
{
"@version": 1.0,
"ex":"https://example.org/ns/"
},
{
"@version": 1.1,
"@protected": true,
"VerifiableCredential": {
"@id": "https://www.w3.org/2018/credentials#VerifiableCredential",
}
}
])).resolves.toEqual(new JsonLdContextNormalized({
"@version": 1.1,
VerifiableCredential: {
"@id": "https://www.w3.org/2018/credentials#VerifiableCredential",
"@protected": true,
},
ex: "https://example.org/ns/"
}));
});

const contexts = [
{
"@version": 1.1,
"@protected": true,
"VerifiableCredential": {
"@id": "https://www.w3.org/2018/credentials#VerifiableCredential",
}
},
{
"ex":"https://example.org/ns/",
"@version": 1.1,
"@protected": false,
}
];

const result = new JsonLdContextNormalized({
"@version": 1.1,
"@protected": false,
VerifiableCredential: {
"@id": "https://www.w3.org/2018/credentials#VerifiableCredential",
"@protected": true,
},
ex: "https://example.org/ns/"
});

it('should parse 2 contexts where one is protected and one globally has protected false', () => {
return expect(parser.parse(contexts)).resolves.toEqual(result);
});

it('should parse 2 contexts where one is protected and one globally has protected false [reverse order]', () => {
return expect(parser.parse([contexts[1], contexts[0]])).resolves.toEqual(result);
});

const BASIC_VC_CONTEXT = {
"@version": 1.1,
"@protected": true,
"VerifiableCredential": {
"@id": "https://www.w3.org/2018/credentials#VerifiableCredential",
}
}

it('protected context should override the unprotected context', () => {
return expect(parser.parse([
{
"@version": 1.0,
"VerifiableCredential":"https://example.org/ns/"
},
BASIC_VC_CONTEXT
])).resolves.toEqual(new JsonLdContextNormalized({
"@version": 1.1,
VerifiableCredential: {
"@id": "https://www.w3.org/2018/credentials#VerifiableCredential",
"@protected": true,
}
}));
});

describe('with imported contexts', () => {
beforeEach(() => {
jest.mocked(globalThis.fetch).mockImplementationOnce(async (...args) => {
return new Response(JSON.stringify({
"@context": {
"MyType": {
"@id": "http://example.org#MyType",
}
}
}), { headers: new Headers([
['content-type', 'application/ld+json']
]) })
});
});

it('protected should be applied to the imported context', () => {
return expect(parser.parse([
{
...BASIC_VC_CONTEXT,
"@import": "http://example.org/imported/context"
}
])).resolves.toEqual(new JsonLdContextNormalized({
"@version": 1.1,
VerifiableCredential: {
"@id": "https://www.w3.org/2018/credentials#VerifiableCredential",
"@protected": true,
},
MyType: {
"@id": "http://example.org#MyType",
"@protected": true,
}
}));
});

it.each([
['child', [BASIC_VC_CONTEXT, {"@import": "http://example.org/imported/context"}]],
['parent', [{"@import": "http://example.org/imported/context"}, BASIC_VC_CONTEXT]],
])('protected should not be applied to the imported contexts in other %s contexts', (_, context) => {
return expect(parser.parse(context)).resolves.toEqual(new JsonLdContextNormalized({
"@version": 1.1,
VerifiableCredential: {
"@id": "https://www.w3.org/2018/credentials#VerifiableCredential",
"@protected": true,
},
MyType: { "@id": "http://example.org#MyType" }
}));
});
});


describe('with imported contexts containing the @protected keyword', () => {

const BASIC_VC_CONTEXT_WITHOUT_PROTECTED = {
"@version": 1.1,
"VerifiableCredential": {
"@id": "https://www.w3.org/2018/credentials#VerifiableCredential",
}
}

beforeEach(() => {
jest.mocked(globalThis.fetch).mockImplementationOnce(async (...args) => {
return new Response(JSON.stringify({
"@context": {
"@protected": true,
"MyType": "http://example.org#MyType"
}
}), { headers: new Headers([
['content-type', 'application/ld+json']
]) })
});
});

it('protected should be applied to the imported context', () => {
return expect(parser.parse([
{
...BASIC_VC_CONTEXT_WITHOUT_PROTECTED,
"@import": "http://example.org/imported/context"
}
])).resolves.toEqual(new JsonLdContextNormalized({
"@version": 1.1,
VerifiableCredential: {
"@id": "https://www.w3.org/2018/credentials#VerifiableCredential",
},
MyType: {
"@id": "http://example.org#MyType",
"@protected": true,
}
}));
});

it.each([
['child', [BASIC_VC_CONTEXT_WITHOUT_PROTECTED, {"@import": "http://example.org/imported/context"}]],
['parent', [{"@import": "http://example.org/imported/context"}, BASIC_VC_CONTEXT_WITHOUT_PROTECTED]],
])('protected should not be applied to the imported contexts in other %s contexts', (_, context) => {
return expect(parser.parse(context)).resolves.toEqual(new JsonLdContextNormalized({
"@version": 1.1,
VerifiableCredential: {
"@id": "https://www.w3.org/2018/credentials#VerifiableCredential",
},
MyType: { "@id": "http://example.org#MyType", "@protected": true }
}));
});
});

it('should parse 2 contexts where one is protected', () => {
return expect(parser.parse([
{"ex":"https://example.org/ns/"},
{
"@version": 1.1,
"@protected": true,
"VerifiableCredential": {
"@id": "https://www.w3.org/2018/credentials#VerifiableCredential",
}
}
])).resolves.toEqual(new JsonLdContextNormalized({
"@version": 1.1,
VerifiableCredential: {
"@id": "https://www.w3.org/2018/credentials#VerifiableCredential",
"@protected": true,
},
ex: "https://example.org/ns/"
}));
});

it('should parse a single keyword alias', () => {
return expect(parser.parse({
id: {
Expand Down Expand Up @@ -2214,6 +2418,60 @@ Tried mapping @id to {}`, ERROR_CODES.KEYWORD_REDEFINITION));
ERROR_CODES.PROTECTED_TERM_REDEFINITION));
});

it('should error on a protected term with override when the overriding version is 1.0', () => {
return expect(parser.parse([
{
name: {
'@id': 'http://xmlns.com/foaf/0.1/name',
'@protected': true,
},
},
{
"@version": 1.0,
name: 'http://schema.org/name',
},
])).rejects.toThrow(new ErrorCoded(
'Attempted to override the protected keyword name from ' +
'"http://xmlns.com/foaf/0.1/name" to "http://schema.org/name"',
ERROR_CODES.PROTECTED_TERM_REDEFINITION));
});

it('should error on an outer scope protected term with override of another protected term', () => {
return expect(parser.parse([
{
"VerifiableCredential":"https://example.org/ns/",
"@protected": true
},
{
"@version": 1.1,
"@protected": true,
"VerifiableCredential": {
"@id": "https://www.w3.org/2018/credentials#VerifiableCredential",
}
}
], { processingMode: 1.1 })).rejects.toThrow(new ErrorCoded(
'Attempted to override the protected keyword VerifiableCredential from ' +
'"https://example.org/ns/" to "https://www.w3.org/2018/credentials#VerifiableCredential"',
ERROR_CODES.PROTECTED_TERM_REDEFINITION));
});

it('should error on an outer scope protected term with override', () => {
return expect(parser.parse([
{
'@protected': true,
name: {
'@id': 'http://xmlns.com/foaf/0.1/name',
},
},
{
name: 'http://schema.org/name',
},
], { processingMode: 1.1 })).rejects.toThrow(new ErrorCoded(
'Attempted to override the protected keyword name from ' +
'"http://xmlns.com/foaf/0.1/name" to "http://schema.org/name"',
ERROR_CODES.PROTECTED_TERM_REDEFINITION));
});

it('should not error on a protected term with override if ignoreProtection is true', () => {
return expect(parser.parse([
{
Expand Down Expand Up @@ -2482,6 +2740,21 @@ Tried mapping @id to {}`, ERROR_CODES.KEYWORD_REDEFINITION));
}));
});

it('should parse a globally protected string term ending in gen-delim', () => {
return expect(parser.parse([
{
'@protected': true,
'foo': 'http://example/foo#',
}
], { processingMode: 1.1 })).resolves.toEqual(new JsonLdContextNormalized({
foo: {
'@id': 'http://example/foo#',
'@prefix': true,
'@protected': true,
},
}));
});

it('should parse a globally protected string term ending in non-gen-delim with identical override', () => {
return expect(parser.parse([
{
Expand Down Expand Up @@ -2623,6 +2896,7 @@ Tried mapping @id to {}`, ERROR_CODES.KEYWORD_REDEFINITION));
"@base": "http://base.org/",
"ex": {
"@id": "http://ex.org/",
"@prefix": true,
"@protected": true
},
"foo": {
Expand Down