n3 2.1.2 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -75,6 +75,16 @@ class Term {
75
75
  // ## NamedNode constructor
76
76
  exports.Term = Term;
77
77
  class NamedNode extends Term {
78
+ // ### Creates a named node
79
+ /**
80
+ * @deprecated Create named nodes through a data factory instead
81
+ * (`DataFactory.namedNode(iri)`), so that term validation can be applied;
82
+ * the constructor assumes an already-validated IRI.
83
+ */
84
+ constructor(iri) {
85
+ super(iri);
86
+ }
87
+
78
88
  // ### The term type of this term
79
89
  get termType() {
80
90
  return 'NamedNode';
@@ -84,6 +94,17 @@ class NamedNode extends Term {
84
94
  // ## Literal constructor
85
95
  exports.NamedNode = NamedNode;
86
96
  class Literal extends Term {
97
+ // ### Creates a literal
98
+ /**
99
+ * @deprecated Create literals through a data factory instead
100
+ * (`DataFactory.literal(value, languageOrDatatype)`), so that term
101
+ * validation can be applied; the constructor takes the internal
102
+ * id representation and assumes it is already valid.
103
+ */
104
+ constructor(id) {
105
+ super(id);
106
+ }
107
+
87
108
  // ### The term type of this term
88
109
  get termType() {
89
110
  return 'Literal';
@@ -155,6 +176,12 @@ class Literal extends Term {
155
176
  // ## BlankNode constructor
156
177
  exports.Literal = Literal;
157
178
  class BlankNode extends Term {
179
+ // ### Creates a blank node
180
+ /**
181
+ * @deprecated Create blank nodes through a data factory instead
182
+ * (`DataFactory.blankNode(name)`), so that term validation can be applied;
183
+ * the constructor assumes an already-validated name.
184
+ */
158
185
  constructor(name) {
159
186
  super(`_:${name}`);
160
187
  }
@@ -171,6 +198,12 @@ class BlankNode extends Term {
171
198
  }
172
199
  exports.BlankNode = BlankNode;
173
200
  class Variable extends Term {
201
+ // ### Creates a variable
202
+ /**
203
+ * @deprecated Create variables through a data factory instead
204
+ * (`DataFactory.variable(name)`), so that term validation can be applied;
205
+ * the constructor assumes an already-validated name.
206
+ */
174
207
  constructor(name) {
175
208
  super(`?${name}`);
176
209
  }
@@ -189,6 +222,11 @@ class Variable extends Term {
189
222
  // ## DefaultGraph constructor
190
223
  exports.Variable = Variable;
191
224
  class DefaultGraph extends Term {
225
+ // ### Creates the default graph
226
+ /**
227
+ * @deprecated Obtain the default graph through a data factory instead
228
+ * (`DataFactory.defaultGraph()`).
229
+ */
192
230
  constructor() {
193
231
  super('');
194
232
  return DEFAULTGRAPH || this;
@@ -296,6 +334,12 @@ function termToId(term, nested) {
296
334
 
297
335
  // ## Quad constructor
298
336
  class Quad extends Term {
337
+ // ### Creates a quad
338
+ /**
339
+ * @deprecated Create quads through a data factory instead
340
+ * (`DataFactory.quad(subject, predicate, object, graph)`), so that term
341
+ * validation can be applied; the constructor assumes already-validated terms.
342
+ */
299
343
  constructor(subject, predicate, object, graph) {
300
344
  super('');
301
345
  this._subject = subject;
package/lib/N3Lexer.js CHANGED
@@ -45,8 +45,10 @@ const escapeReplacements = {
45
45
  '%': '%'
46
46
  };
47
47
  const illegalIriChars = /[\x00-\x20<>\\"\{\}\|\^\`]/;
48
- function isSurrogateCodePoint(charCode) {
49
- return charCode >= 0xD800 && charCode <= 0xDFFF;
48
+
49
+ // A valid code point is a Unicode scalar value: at most U+10FFFF and not a surrogate
50
+ function isValidCodePoint(charCode) {
51
+ return charCode <= 0x10FFFF && (charCode < 0xD800 || charCode > 0xDFFF);
50
52
  }
51
53
  const lineModeRegExps = {
52
54
  _iri: true,
@@ -425,7 +427,7 @@ class N3Lexer {
425
427
  // 4-digit unicode character
426
428
  if (typeof unicode4 === 'string') {
427
429
  const charCode = Number.parseInt(unicode4, 16);
428
- if (isSurrogateCodePoint(charCode)) {
430
+ if (!isValidCodePoint(charCode)) {
429
431
  invalid = true;
430
432
  return '';
431
433
  }
@@ -434,7 +436,7 @@ class N3Lexer {
434
436
  // 8-digit unicode character
435
437
  if (typeof unicode8 === 'string') {
436
438
  let charCode = Number.parseInt(unicode8, 16);
437
- if (isSurrogateCodePoint(charCode)) {
439
+ if (!isValidCodePoint(charCode)) {
438
440
  invalid = true;
439
441
  return '';
440
442
  }
package/lib/N3Parser.js CHANGED
@@ -292,6 +292,13 @@ class N3Parser {
292
292
  case ';':
293
293
  // Additional semicolons can be safely ignored
294
294
  return this._predicate !== null ? this._readPredicate : this._error('Expected predicate but got ;', token);
295
+ case 'literal':
296
+ if (!this._n3Mode) return this._error('Unexpected literal', token);
297
+ if (token.prefix.length === 0) {
298
+ this._literalValue = token.value;
299
+ return this._completePredicateLiteral;
300
+ } else this._predicate = this._factory.literal(token.value, this._factory.namedNode(token.prefix));
301
+ break;
295
302
  case '[':
296
303
  if (this._n3Mode) {
297
304
  // Start a new quad with a new blank node as subject
@@ -569,22 +576,45 @@ class N3Parser {
569
576
  language: this._literalLanguage,
570
577
  direction: token.value
571
578
  });
572
- if (component === 'subject') this._subject = term;else this._object = term;
579
+ if (component === 'subject') this._subject = term;else if (component === 'predicate') this._predicate = term;else this._object = term;
573
580
  this._literalLanguage = undefined;
574
581
  token = null;
575
582
  }
576
583
  if (component === 'subject') return token === null ? this._readPredicateOrNamedGraph : this._readPredicateOrNamedGraph(token);
584
+ if (component === 'predicate') return token === null ? this._readObject : this._readObject(token);
577
585
  return this._completeObjectLiteralPost(token, listItem);
578
586
  }
579
587
 
580
- // Completes a literal in subject position
581
- _completeSubjectLiteral(token) {
582
- const completed = this._completeLiteral(token, 'subject');
583
- this._subject = completed.literal;
588
+ // Completes a literal in subject or predicate position
589
+ _completeTermLiteral(token, component) {
590
+ const completed = this._completeLiteral(token, component);
591
+ if (!completed) return;
592
+ let next;
593
+ if (component === 'subject') {
594
+ this._subject = completed.literal;
595
+ next = this._readPredicateOrNamedGraph;
596
+ } else {
597
+ this._predicate = completed.literal;
598
+ this._validAnnotation = true;
599
+ next = this._readObject;
600
+ }
584
601
 
585
602
  // Postpone completion if the literal is only partially completed (such as lang+dir).
586
603
  if (completed.readCb) return completed.readCb.bind(this, false);
587
- return this._readPredicateOrNamedGraph;
604
+
605
+ // If the token was consumed as a datatype, continue with the next component;
606
+ // otherwise, consume the token now
607
+ return completed.token === null ? next : next.call(this, completed.token);
608
+ }
609
+
610
+ // Completes a literal in subject position
611
+ _completeSubjectLiteral(token) {
612
+ return this._completeTermLiteral(token, 'subject');
613
+ }
614
+
615
+ // Completes a literal in predicate position
616
+ _completePredicateLiteral(token) {
617
+ return this._completeTermLiteral(token, 'predicate');
588
618
  }
589
619
 
590
620
  // Completes a literal in object position
package/lib/N3Reasoner.js CHANGED
@@ -26,14 +26,22 @@ function getRulesFromDataset(dataset) {
26
26
  return rules;
27
27
  }
28
28
  class N3Reasoner {
29
- constructor(store) {
29
+ constructor(store, options = {}) {
30
30
  this._store = store;
31
+ // Optional safety budgets for reasoning over untrusted rules or data
32
+ this._maxDerivations = options.maxDerivations === undefined ? Infinity : options.maxDerivations;
33
+ // Caps a rule's premise count, as `_evaluatePremise` recurses once per premise
34
+ this._maxPremiseDepth = options.maxPremiseDepth === undefined ? Infinity : options.maxPremiseDepth;
31
35
  }
32
36
  _add(subject, predicate, object, graphItem, cb) {
33
37
  // Only add to the remaining indexes if there is not already a value in the index
34
38
  if (!this._store._addToIndex(graphItem.subjects, subject, predicate, object)) return;
35
39
  this._store._addToIndex(graphItem.predicates, predicate, object, subject);
36
40
  this._store._addToIndex(graphItem.objects, object, subject, predicate);
41
+ // Count genuinely new derivations and fail past the budget. The check comes
42
+ // after all three indexes are updated, so a caught error leaves the store
43
+ // in a consistent state (the reasoning result is merely incomplete).
44
+ if (++this._derivations > this._maxDerivations) throw new Error(`Reasoning exceeded the maximum of ${this._maxDerivations} derivations`);
37
45
  cb();
38
46
  }
39
47
  _evaluatePremise(rule, content, cb, i = 0) {
@@ -143,10 +151,18 @@ class N3Reasoner {
143
151
  };
144
152
  }
145
153
  reason(rules) {
154
+ this._derivations = 0;
146
155
  if (!Array.isArray(rules)) {
147
156
  rules = getRulesFromDataset(rules);
148
157
  }
149
158
  rules = rules.map(rule => this._createRule(rule));
159
+
160
+ // Reject rules with more body triples than the configured premise depth:
161
+ // `_evaluatePremise` recurses once per premise, so an over-long rule would
162
+ // otherwise overflow the stack with an uncatchable RangeError.
163
+ for (const rule of rules) {
164
+ if (rule.premise.length > this._maxPremiseDepth) throw new Error(`Reasoning rule exceeds the maximum premise depth of ${this._maxPremiseDepth}`);
165
+ }
150
166
  for (const r1 of rules) {
151
167
  for (const r2 of rules) {
152
168
  for (let i = 0; i < r2.premise.length; i++) {
@@ -190,10 +206,15 @@ class N3Reasoner {
190
206
  rule.premise = rule.premise.map(p => getIndex(p, set));
191
207
  }
192
208
  const graphs = this._store._getGraphs();
193
- for (const graphId in graphs) {
194
- this._reasonGraphNaive(rules, graphs[graphId]);
209
+ try {
210
+ for (const graphId in graphs) {
211
+ this._reasonGraphNaive(rules, graphs[graphId]);
212
+ }
213
+ } finally {
214
+ // Invalidate the cached size even if a derivation budget was exceeded,
215
+ // so a caught budget error leaves the store fully consistent.
216
+ this._store._size = null;
195
217
  }
196
- this._store._size = null;
197
218
  }
198
219
  }
199
220
  exports.default = N3Reasoner;
package/lib/N3Store.js CHANGED
@@ -275,6 +275,18 @@ class N3Store {
275
275
  }
276
276
  }
277
277
 
278
+ // ### `_loopByKey0Deep` executes the callback on all keys of index 2
279
+ // for a certain entry in index 0, possibly repeating keys
280
+ _loopByKey0Deep(index0, key0, callback) {
281
+ let index1, index2, key1, key2;
282
+ if (index1 = index0[key0]) {
283
+ for (key1 in index1) {
284
+ index2 = index1[key1];
285
+ for (key2 in index2) callback(key2);
286
+ }
287
+ }
288
+ }
289
+
278
290
  // ### `_countInIndex` counts matching quads in a three-layered index.
279
291
  // The index base is `index0` and the keys at each level are `key0`, `key1`, and `key2`.
280
292
  // Any of these keys can be undefined, which is interpreted as a wildcard.
@@ -685,8 +697,9 @@ class N3Store {
685
697
  if (predicateId)
686
698
  // If subject and predicate are given, the SPO index is best.
687
699
  this._loopBy2Keys(content.subjects, subjectId, predicateId, callback);else
688
- // If only subject is given, the OSP index is best.
689
- this._loopByKey1(content.objects, subjectId, callback);
700
+ // If only subject is given, descending the SPO index
701
+ // visits only the subject's own quads.
702
+ this._loopByKey0Deep(content.subjects, subjectId, callback);
690
703
  } else if (predicateId)
691
704
  // If only predicate is given, the POS index is best.
692
705
  this._loopByKey0(content.predicates, predicateId, callback);else
package/lib/N3Writer.js CHANGED
@@ -46,6 +46,7 @@ class N3Writer {
46
46
  constructor(outputStream, options) {
47
47
  // ### `_prefixRegex` matches a prefixed name or IRI that begins with one of the added prefixes
48
48
  this._prefixRegex = /$0^/;
49
+ this._hasPrefixes = false;
49
50
 
50
51
  // Shift arguments if the first argument is not a stream
51
52
  if (outputStream && typeof outputStream.write !== 'function') options = outputStream, outputStream = null;
@@ -154,7 +155,7 @@ class N3Writer {
154
155
  if (entity.termType !== 'NamedNode') {
155
156
  // If it is a list head, pretty-print it
156
157
  if (this._lists && entity.value in this._lists) entity = this.list(this._lists[entity.value]);
157
- return 'id' in entity ? entity.id : `_:${entity.value}`;
158
+ return entity.termType === 'Variable' ? `?${entity.value}` : 'id' in entity ? entity.id : `_:${entity.value}`;
158
159
  }
159
160
  let iri = entity.value;
160
161
  // Use relative IRIs if requested and possible
@@ -163,8 +164,8 @@ class N3Writer {
163
164
  }
164
165
  // Escape special characters
165
166
  if (escape.test(iri)) iri = iri.replace(escapeAll, characterReplacer);
166
- // Try to represent the IRI as prefixed name
167
- const prefixMatch = this._prefixRegex.exec(iri);
167
+ // Try to represent the IRI as prefixed name, unless no prefixes were added
168
+ const prefixMatch = this._hasPrefixes ? this._prefixRegex.exec(iri) : null;
168
169
  return !prefixMatch ? `<${iri}>` : !prefixMatch[1] ? iri : this._prefixIRIs[prefixMatch[1]] + prefixMatch[2];
169
170
  }
170
171
 
@@ -282,6 +283,7 @@ class N3Writer {
282
283
  }
283
284
  // Recreate the prefix matcher
284
285
  if (hasPrefixes) {
286
+ this._hasPrefixes = true;
285
287
  let IRIlist = '',
286
288
  prefixList = '';
287
289
  for (const prefixIRI in this._prefixIRIs) {
@@ -289,7 +291,7 @@ class N3Writer {
289
291
  prefixList += (prefixList ? '|' : '') + this._prefixIRIs[prefixIRI];
290
292
  }
291
293
  IRIlist = (0, _Util.escapeRegex)(IRIlist, /[\]\/\(\)\*\+\?\.\\\$]/g, '\\$&');
292
- this._prefixRegex = new RegExp(`^(?:${prefixList})[^\/]*$|` + `^(${IRIlist})([_a-zA-Z0-9][\\-_a-zA-Z0-9]*)$`);
294
+ this._prefixRegex = new RegExp(`^(?:${prefixList})[^\/]*$|` + `^(${IRIlist})([_a-zA-Z0-9](?:\\.?[\\-_a-zA-Z0-9])*)$`);
293
295
  }
294
296
  // End a prefix block with a newline
295
297
  this._write(hasPrefixes ? '\n' : '', done);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n3",
3
- "version": "2.1.2",
3
+ "version": "2.2.0",
4
4
  "description": "Lightning fast, asynchronous, streaming Turtle / N3 / RDF library.",
5
5
  "author": "Ruben Verborgh <ruben.verborgh@gmail.com>",
6
6
  "keywords": [
@@ -65,6 +65,16 @@ export class Term {
65
65
 
66
66
  // ## NamedNode constructor
67
67
  export class NamedNode extends Term {
68
+ // ### Creates a named node
69
+ /**
70
+ * @deprecated Create named nodes through a data factory instead
71
+ * (`DataFactory.namedNode(iri)`), so that term validation can be applied;
72
+ * the constructor assumes an already-validated IRI.
73
+ */
74
+ constructor(iri) {
75
+ super(iri);
76
+ }
77
+
68
78
  // ### The term type of this term
69
79
  get termType() {
70
80
  return 'NamedNode';
@@ -73,6 +83,17 @@ export class NamedNode extends Term {
73
83
 
74
84
  // ## Literal constructor
75
85
  export class Literal extends Term {
86
+ // ### Creates a literal
87
+ /**
88
+ * @deprecated Create literals through a data factory instead
89
+ * (`DataFactory.literal(value, languageOrDatatype)`), so that term
90
+ * validation can be applied; the constructor takes the internal
91
+ * id representation and assumes it is already valid.
92
+ */
93
+ constructor(id) {
94
+ super(id);
95
+ }
96
+
76
97
  // ### The term type of this term
77
98
  get termType() {
78
99
  return 'Literal';
@@ -146,6 +167,12 @@ export class Literal extends Term {
146
167
 
147
168
  // ## BlankNode constructor
148
169
  export class BlankNode extends Term {
170
+ // ### Creates a blank node
171
+ /**
172
+ * @deprecated Create blank nodes through a data factory instead
173
+ * (`DataFactory.blankNode(name)`), so that term validation can be applied;
174
+ * the constructor assumes an already-validated name.
175
+ */
149
176
  constructor(name) {
150
177
  super(`_:${name}`);
151
178
  }
@@ -162,6 +189,12 @@ export class BlankNode extends Term {
162
189
  }
163
190
 
164
191
  export class Variable extends Term {
192
+ // ### Creates a variable
193
+ /**
194
+ * @deprecated Create variables through a data factory instead
195
+ * (`DataFactory.variable(name)`), so that term validation can be applied;
196
+ * the constructor assumes an already-validated name.
197
+ */
165
198
  constructor(name) {
166
199
  super(`?${name}`);
167
200
  }
@@ -179,6 +212,11 @@ export class Variable extends Term {
179
212
 
180
213
  // ## DefaultGraph constructor
181
214
  export class DefaultGraph extends Term {
215
+ // ### Creates the default graph
216
+ /**
217
+ * @deprecated Obtain the default graph through a data factory instead
218
+ * (`DataFactory.defaultGraph()`).
219
+ */
182
220
  constructor() {
183
221
  super('');
184
222
  return DEFAULTGRAPH || this;
@@ -299,6 +337,12 @@ export function termToId(term, nested) {
299
337
 
300
338
  // ## Quad constructor
301
339
  export class Quad extends Term {
340
+ // ### Creates a quad
341
+ /**
342
+ * @deprecated Create quads through a data factory instead
343
+ * (`DataFactory.quad(subject, predicate, object, graph)`), so that term
344
+ * validation can be applied; the constructor assumes already-validated terms.
345
+ */
302
346
  constructor(subject, predicate, object, graph) {
303
347
  super('');
304
348
  this._subject = subject;
package/src/N3Lexer.js CHANGED
@@ -15,8 +15,9 @@ const escapeReplacements = {
15
15
  };
16
16
  const illegalIriChars = /[\x00-\x20<>\\"\{\}\|\^\`]/;
17
17
 
18
- function isSurrogateCodePoint(charCode) {
19
- return charCode >= 0xD800 && charCode <= 0xDFFF;
18
+ // A valid code point is a Unicode scalar value: at most U+10FFFF and not a surrogate
19
+ function isValidCodePoint(charCode) {
20
+ return charCode <= 0x10FFFF && (charCode < 0xD800 || charCode > 0xDFFF);
20
21
  }
21
22
 
22
23
  const lineModeRegExps = {
@@ -433,7 +434,7 @@ export default class N3Lexer {
433
434
  // 4-digit unicode character
434
435
  if (typeof unicode4 === 'string') {
435
436
  const charCode = Number.parseInt(unicode4, 16);
436
- if (isSurrogateCodePoint(charCode)) {
437
+ if (!isValidCodePoint(charCode)) {
437
438
  invalid = true;
438
439
  return '';
439
440
  }
@@ -442,7 +443,7 @@ export default class N3Lexer {
442
443
  // 8-digit unicode character
443
444
  if (typeof unicode8 === 'string') {
444
445
  let charCode = Number.parseInt(unicode8, 16);
445
- if (isSurrogateCodePoint(charCode)) {
446
+ if (!isValidCodePoint(charCode)) {
446
447
  invalid = true;
447
448
  return '';
448
449
  }
package/src/N3Parser.js CHANGED
@@ -302,6 +302,18 @@ export default class N3Parser {
302
302
  // Additional semicolons can be safely ignored
303
303
  return this._predicate !== null ? this._readPredicate :
304
304
  this._error('Expected predicate but got ;', token);
305
+ case 'literal':
306
+ if (!this._n3Mode)
307
+ return this._error('Unexpected literal', token);
308
+
309
+ if (token.prefix.length === 0) {
310
+ this._literalValue = token.value;
311
+ return this._completePredicateLiteral;
312
+ }
313
+ else
314
+ this._predicate = this._factory.literal(token.value, this._factory.namedNode(token.prefix));
315
+
316
+ break;
305
317
  case '[':
306
318
  if (this._n3Mode) {
307
319
  // Start a new quad with a new blank node as subject
@@ -603,6 +615,8 @@ export default class N3Parser {
603
615
  const term = this._factory.literal(this._literalValue, { language: this._literalLanguage, direction: token.value });
604
616
  if (component === 'subject')
605
617
  this._subject = term;
618
+ else if (component === 'predicate')
619
+ this._predicate = term;
606
620
  else
607
621
  this._object = term;
608
622
  this._literalLanguage = undefined;
@@ -611,19 +625,45 @@ export default class N3Parser {
611
625
 
612
626
  if (component === 'subject')
613
627
  return token === null ? this._readPredicateOrNamedGraph : this._readPredicateOrNamedGraph(token);
628
+ if (component === 'predicate')
629
+ return token === null ? this._readObject : this._readObject(token);
614
630
  return this._completeObjectLiteralPost(token, listItem);
615
631
  }
616
632
 
617
- // Completes a literal in subject position
618
- _completeSubjectLiteral(token) {
619
- const completed = this._completeLiteral(token, 'subject');
620
- this._subject = completed.literal;
633
+ // Completes a literal in subject or predicate position
634
+ _completeTermLiteral(token, component) {
635
+ const completed = this._completeLiteral(token, component);
636
+ if (!completed)
637
+ return;
638
+
639
+ let next;
640
+ if (component === 'subject') {
641
+ this._subject = completed.literal;
642
+ next = this._readPredicateOrNamedGraph;
643
+ }
644
+ else {
645
+ this._predicate = completed.literal;
646
+ this._validAnnotation = true;
647
+ next = this._readObject;
648
+ }
621
649
 
622
650
  // Postpone completion if the literal is only partially completed (such as lang+dir).
623
651
  if (completed.readCb)
624
652
  return completed.readCb.bind(this, false);
625
653
 
626
- return this._readPredicateOrNamedGraph;
654
+ // If the token was consumed as a datatype, continue with the next component;
655
+ // otherwise, consume the token now
656
+ return completed.token === null ? next : next.call(this, completed.token);
657
+ }
658
+
659
+ // Completes a literal in subject position
660
+ _completeSubjectLiteral(token) {
661
+ return this._completeTermLiteral(token, 'subject');
662
+ }
663
+
664
+ // Completes a literal in predicate position
665
+ _completePredicateLiteral(token) {
666
+ return this._completeTermLiteral(token, 'predicate');
627
667
  }
628
668
 
629
669
  // Completes a literal in object position
package/src/N3Reasoner.js CHANGED
@@ -14,8 +14,12 @@ export function getRulesFromDataset(dataset) {
14
14
  }
15
15
 
16
16
  export default class N3Reasoner {
17
- constructor(store) {
17
+ constructor(store, options = {}) {
18
18
  this._store = store;
19
+ // Optional safety budgets for reasoning over untrusted rules or data
20
+ this._maxDerivations = options.maxDerivations === undefined ? Infinity : options.maxDerivations;
21
+ // Caps a rule's premise count, as `_evaluatePremise` recurses once per premise
22
+ this._maxPremiseDepth = options.maxPremiseDepth === undefined ? Infinity : options.maxPremiseDepth;
19
23
  }
20
24
 
21
25
  _add(subject, predicate, object, graphItem, cb) {
@@ -23,6 +27,11 @@ export default class N3Reasoner {
23
27
  if (!this._store._addToIndex(graphItem.subjects, subject, predicate, object)) return;
24
28
  this._store._addToIndex(graphItem.predicates, predicate, object, subject);
25
29
  this._store._addToIndex(graphItem.objects, object, subject, predicate);
30
+ // Count genuinely new derivations and fail past the budget. The check comes
31
+ // after all three indexes are updated, so a caught error leaves the store
32
+ // in a consistent state (the reasoning result is merely incomplete).
33
+ if (++this._derivations > this._maxDerivations)
34
+ throw new Error(`Reasoning exceeded the maximum of ${this._maxDerivations} derivations`);
26
35
  cb();
27
36
  }
28
37
 
@@ -129,11 +138,20 @@ export default class N3Reasoner {
129
138
  }
130
139
 
131
140
  reason(rules) {
141
+ this._derivations = 0;
132
142
  if (!Array.isArray(rules)) {
133
143
  rules = getRulesFromDataset(rules);
134
144
  }
135
145
  rules = rules.map(rule => this._createRule(rule));
136
146
 
147
+ // Reject rules with more body triples than the configured premise depth:
148
+ // `_evaluatePremise` recurses once per premise, so an over-long rule would
149
+ // otherwise overflow the stack with an uncatchable RangeError.
150
+ for (const rule of rules) {
151
+ if (rule.premise.length > this._maxPremiseDepth)
152
+ throw new Error(`Reasoning rule exceeds the maximum premise depth of ${this._maxPremiseDepth}`);
153
+ }
154
+
137
155
  for (const r1 of rules) {
138
156
  for (const r2 of rules) {
139
157
  for (let i = 0; i < r2.premise.length; i++) {
@@ -179,11 +197,16 @@ export default class N3Reasoner {
179
197
  }
180
198
 
181
199
  const graphs = this._store._getGraphs();
182
- for (const graphId in graphs) {
183
- this._reasonGraphNaive(rules, graphs[graphId]);
200
+ try {
201
+ for (const graphId in graphs) {
202
+ this._reasonGraphNaive(rules, graphs[graphId]);
203
+ }
204
+ }
205
+ finally {
206
+ // Invalidate the cached size even if a derivation budget was exceeded,
207
+ // so a caught budget error leaves the store fully consistent.
208
+ this._store._size = null;
184
209
  }
185
-
186
- this._store._size = null;
187
210
  }
188
211
  }
189
212
 
package/src/N3Store.js CHANGED
@@ -296,6 +296,19 @@ export default class N3Store {
296
296
  }
297
297
  }
298
298
 
299
+ // ### `_loopByKey0Deep` executes the callback on all keys of index 2
300
+ // for a certain entry in index 0, possibly repeating keys
301
+ _loopByKey0Deep(index0, key0, callback) {
302
+ let index1, index2, key1, key2;
303
+ if (index1 = index0[key0]) {
304
+ for (key1 in index1) {
305
+ index2 = index1[key1];
306
+ for (key2 in index2)
307
+ callback(key2);
308
+ }
309
+ }
310
+ }
311
+
299
312
  // ### `_countInIndex` counts matching quads in a three-layered index.
300
313
  // The index base is `index0` and the keys at each level are `key0`, `key1`, and `key2`.
301
314
  // Any of these keys can be undefined, which is interpreted as a wildcard.
@@ -719,8 +732,9 @@ export default class N3Store {
719
732
  // If subject and predicate are given, the SPO index is best.
720
733
  this._loopBy2Keys(content.subjects, subjectId, predicateId, callback);
721
734
  else
722
- // If only subject is given, the OSP index is best.
723
- this._loopByKey1(content.objects, subjectId, callback);
735
+ // If only subject is given, descending the SPO index
736
+ // visits only the subject's own quads.
737
+ this._loopByKey0Deep(content.subjects, subjectId, callback);
724
738
  }
725
739
  else if (predicateId)
726
740
  // If only predicate is given, the POS index is best.
package/src/N3Writer.js CHANGED
@@ -31,6 +31,7 @@ export default class N3Writer {
31
31
  constructor(outputStream, options) {
32
32
  // ### `_prefixRegex` matches a prefixed name or IRI that begins with one of the added prefixes
33
33
  this._prefixRegex = /$0^/;
34
+ this._hasPrefixes = false;
34
35
 
35
36
  // Shift arguments if the first argument is not a stream
36
37
  if (outputStream && typeof outputStream.write !== 'function')
@@ -151,7 +152,8 @@ export default class N3Writer {
151
152
  // If it is a list head, pretty-print it
152
153
  if (this._lists && (entity.value in this._lists))
153
154
  entity = this.list(this._lists[entity.value]);
154
- return 'id' in entity ? entity.id : `_:${entity.value}`;
155
+ return entity.termType === 'Variable' ? `?${entity.value}` :
156
+ 'id' in entity ? entity.id : `_:${entity.value}`;
155
157
  }
156
158
  let iri = entity.value;
157
159
  // Use relative IRIs if requested and possible
@@ -161,8 +163,8 @@ export default class N3Writer {
161
163
  // Escape special characters
162
164
  if (escape.test(iri))
163
165
  iri = iri.replace(escapeAll, characterReplacer);
164
- // Try to represent the IRI as prefixed name
165
- const prefixMatch = this._prefixRegex.exec(iri);
166
+ // Try to represent the IRI as prefixed name, unless no prefixes were added
167
+ const prefixMatch = this._hasPrefixes ? this._prefixRegex.exec(iri) : null;
166
168
  return !prefixMatch ? `<${iri}>` :
167
169
  (!prefixMatch[1] ? iri : this._prefixIRIs[prefixMatch[1]] + prefixMatch[2]);
168
170
  }
@@ -294,6 +296,7 @@ export default class N3Writer {
294
296
  }
295
297
  // Recreate the prefix matcher
296
298
  if (hasPrefixes) {
299
+ this._hasPrefixes = true;
297
300
  let IRIlist = '', prefixList = '';
298
301
  for (const prefixIRI in this._prefixIRIs) {
299
302
  IRIlist += IRIlist ? `|${prefixIRI}` : prefixIRI;
@@ -301,7 +304,7 @@ export default class N3Writer {
301
304
  }
302
305
  IRIlist = escapeRegex(IRIlist, /[\]\/\(\)\*\+\?\.\\\$]/g, '\\$&');
303
306
  this._prefixRegex = new RegExp(`^(?:${prefixList})[^\/]*$|` +
304
- `^(${IRIlist})([_a-zA-Z0-9][\\-_a-zA-Z0-9]*)$`);
307
+ `^(${IRIlist})([_a-zA-Z0-9](?:\\.?[\\-_a-zA-Z0-9])*)$`);
305
308
  }
306
309
  // End a prefix block with a newline
307
310
  this._write(hasPrefixes ? '\n' : '', done);