mssql-cr-layer 3.0.6 → 3.0.9

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mssql-cr-layer",
3
- "version": "3.0.6",
3
+ "version": "3.0.9",
4
4
  "description": "A MS SQL Server interface layer for common requests",
5
5
  "repository": "andrglo/mssql-cr-layer",
6
6
  "author": {
@@ -37,6 +37,7 @@
37
37
  },
38
38
  "overrides": {
39
39
  "serialize-javascript": "^7.0.5",
40
- "diff": "^8.0.3"
40
+ "diff": "^8.0.3",
41
+ "brace-expansion": "^5.0.8"
41
42
  }
42
43
  }
package/src/index.js CHANGED
@@ -2,6 +2,7 @@ const mssql = require('mssql')
2
2
  const assert = require('assert')
3
3
  const every = require('lodash.every')
4
4
  const debug = require('debug')('layer:mssql')
5
+ const scanMarkers = require('./scan-markers')
5
6
 
6
7
  const connectionParams = new WeakMap() // Hidden connection parameters
7
8
 
@@ -237,19 +238,23 @@ MssqlCrLayer.prototype.query = function(statement, params, options) {
237
238
 
238
239
  const convertParams = function() {
239
240
  if (Array.isArray(params)) {
240
- const match = statement.match(/(\$\w*\b)/g)
241
+ const markers = scanMarkers(statement)
241
242
  assert(
242
- ((match && match.length) || 0) <= Object.keys(params).length,
243
+ markers.length <= Object.keys(params).length,
243
244
  'There are more ' + 'parameters in statement than in object params'
244
245
  )
245
246
  const paramsObj = {}
246
- if (match) {
247
- match.map(function(param) {
248
- const key = param.substr(1)
249
- paramsObj['p' + key] = params[Number(key) - 1]
250
- statement = statement.replace(param, '@p' + key)
251
- })
252
- }
247
+ // rewrite by position: replacing by token would hit the first
248
+ // occurrence anywhere in the statement, comments and literals included
249
+ let converted = ''
250
+ let last = 0
251
+ markers.forEach(function(marker) {
252
+ const key = marker.name
253
+ paramsObj['p' + key] = params[Number(key) - 1]
254
+ converted += statement.slice(last, marker.index) + '@p' + key
255
+ last = marker.index + marker.length
256
+ })
257
+ statement = converted + statement.slice(last)
253
258
  params = paramsObj
254
259
  }
255
260
  }
@@ -310,12 +315,38 @@ MssqlCrLayer.prototype.close = function() {
310
315
  }
311
316
 
312
317
  /**
313
- * Wrap the identifier within the appropriate delimiters
318
+ * Wrap the identifier within the appropriate delimiters, escaping the
319
+ * closing delimiter by doubling it - the standard SQL escape - so an
320
+ * identifier holding a delimiter cannot close the quoted context and
321
+ * append arbitrary SQL. A NUL character cannot be escaped by any
322
+ * delimiter style, so it is rejected, as is a non string identifier
314
323
  * @param identifier {string}
315
324
  * @returns identifier {string}
316
325
  */
317
326
  MssqlCrLayer.prototype.wrap = function(identifier) {
318
- return this.delimiters[0] + identifier + this.delimiters[1]
327
+ if (typeof identifier !== 'string') {
328
+ throw new Error(
329
+ 'Invalid identifier: a string is expected, got ' + typeof identifier
330
+ )
331
+ }
332
+ if (identifier.indexOf('\0') !== -1) {
333
+ throw new Error(
334
+ 'Invalid identifier: the NUL character is not allowed'
335
+ )
336
+ }
337
+ // delimiters is a prototype property a consumer can override. Read it once
338
+ // and check it here, at the point of use: anything but a two character
339
+ // string leaves close undefined or hijackable via Symbol.split, and the
340
+ // escape below silently becomes a no op
341
+ const delimiters = this.delimiters
342
+ if (typeof delimiters !== 'string' || delimiters.length !== 2) {
343
+ throw new Error(
344
+ 'Invalid delimiters: a two character string is expected'
345
+ )
346
+ }
347
+ const open = delimiters[0]
348
+ const close = delimiters[1]
349
+ return open + identifier.split(close).join(close + close) + close
319
350
  }
320
351
 
321
352
  function getType(value, param) {
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Find the parameter markers of a T-SQL statement that are in code context.
3
+ *
4
+ * A regex over the whole statement cannot tell a marker from a marker
5
+ * lookalike sitting inside a string literal, a quoted identifier or a
6
+ * comment - 'Total R$100 a vista' reads as the marker $100 - so the
7
+ * statement is walked once, tracking the context, and only the markers
8
+ * found in code are reported.
9
+ *
10
+ * The marker grammar reproduces the old /(\$\w*\b)/g exactly: the sigil
11
+ * followed by one or more word characters, maximal munch. A sigil not
12
+ * followed by a word character is not a marker, so '%R$%' stays inert.
13
+ *
14
+ * There is no dollar quoting in T-SQL and no backslash escape in its
15
+ * string literals - a quote is escaped by doubling it, and so is the
16
+ * closing bracket of a bracketed identifier.
17
+ *
18
+ * @param statement {string}
19
+ * @returns {Array} [{index, length, name}, ...] in the order they appear,
20
+ * index being the offset of the sigil and name the token without it
21
+ */
22
+ module.exports = function scanMarkers(statement) {
23
+ const markers = []
24
+ const length = statement.length
25
+ let i = 0
26
+ while (i < length) {
27
+ const char = statement[i]
28
+ if (char === '\'') {
29
+ i = skipQuoted(statement, i, '\'')
30
+ } else if (char === '"') {
31
+ i = skipQuoted(statement, i, '"')
32
+ } else if (char === '[') {
33
+ i = skipQuoted(statement, i, ']')
34
+ } else if (char === '-' && statement[i + 1] === '-') {
35
+ const end = statement.indexOf('\n', i + 2)
36
+ i = end === -1 ? length : end + 1
37
+ } else if (char === '/' && statement[i + 1] === '*') {
38
+ i = skipBlockComment(statement, i)
39
+ } else if (char === '$') {
40
+ let end = i + 1
41
+ while (end < length && isWordChar(statement[end])) {
42
+ end++
43
+ }
44
+ if (end > i + 1) {
45
+ markers.push({
46
+ index: i,
47
+ length: end - i,
48
+ name: statement.slice(i + 1, end)
49
+ })
50
+ }
51
+ i = end
52
+ } else {
53
+ i++
54
+ }
55
+ }
56
+ return markers
57
+ }
58
+
59
+ function isWordChar(char) {
60
+ return (
61
+ (char >= 'a' && char <= 'z') ||
62
+ (char >= 'A' && char <= 'Z') ||
63
+ (char >= '0' && char <= '9') ||
64
+ char === '_'
65
+ )
66
+ }
67
+
68
+ /**
69
+ * Skip the quoted run opened at start, whose closing delimiter is close and
70
+ * is escaped by doubling it. Unterminated runs to the end of the statement
71
+ * @returns the offset of the first character after the run
72
+ */
73
+ function skipQuoted(statement, start, close) {
74
+ let i = start + 1
75
+ while (i < statement.length) {
76
+ if (statement[i] === close) {
77
+ if (statement[i + 1] === close) {
78
+ i += 2
79
+ continue
80
+ }
81
+ return i + 1
82
+ }
83
+ i++
84
+ }
85
+ return statement.length
86
+ }
87
+
88
+ /**
89
+ * Skip the block comment opened at start. T-SQL nests block comments, so the
90
+ * depth is tracked and the comment ends only when it gets back to zero
91
+ * @returns the offset of the first character after the comment
92
+ */
93
+ function skipBlockComment(statement, start) {
94
+ let depth = 1
95
+ let i = start + 2
96
+ while (i < statement.length) {
97
+ if (statement[i] === '/' && statement[i + 1] === '*') {
98
+ depth++
99
+ i += 2
100
+ } else if (statement[i] === '*' && statement[i + 1] === '/') {
101
+ depth--
102
+ i += 2
103
+ if (depth === 0) {
104
+ return i
105
+ }
106
+ } else {
107
+ i++
108
+ }
109
+ }
110
+ return statement.length
111
+ }