UNPKG

6.96 kBJavaScriptView Raw
1/**
2 * @license
3 * Copyright 2018 Google LLC
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18/* eslint id-length: 0, complexity: ["error", { "max": 15 }] */
19
20'use strict';
21
22const {
23 CHARS_GLOBAL_REGEXP,
24 escapeSeries,
25 isSeries,
26 isSqlFragment,
27 makeEscaper,
28} = require('./escapers.js');
29
30const { toString: bufferProtoToString } = Buffer.prototype;
31const { isBuffer } = Buffer;
32const { apply } = Reflect;
33
34const QUAL_GLOBAL_REGEXP = /\./g;
35const PG_ID_REGEXP = /^(?:"(?:[^"]|"")+"|u&"(?:[^"\\]|""|\\.)+")$/i;
36const PG_QUAL_ID_REGEXP = /^(?:(?:"(?:[^"]|"")+"|u&"(?:[^"\\]|""|\\.)+")(?:[.](?!$)|$))+$/;
37
38// Note: NULs are not allowed in text data value.
39// https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS-ESCAPE says
40// "The character with the code zero cannot be in a string constant."
41// Similarly
42// "Quoted identifiers can contain any character, except the character with code zero."
43
44const PG_CHARS_ESCAPE_MAP = {
45 __proto__: null,
46 // See note on NUL above
47 '\0': '',
48 '\b': '\b',
49 '\t': '\t',
50 '\n': '\n',
51 '\r': '\r',
52 '\x1a': '\x1a',
53 '"': '"',
54 '$': '$',
55 '\'': '\'\'',
56 '\\': '\\',
57};
58
59const PG_ID_ESCAPE_MAP = {
60 __proto__: null,
61 // See note on NUL above
62 '\0': '',
63 '\b': '\b',
64 '\t': '\t',
65 '\n': '\n',
66 '\r': '\r',
67 '\x1a': '\x1a',
68 '"': '""',
69 '$': '$',
70 '\'': '\'',
71 '\\': '\\',
72};
73
74const PG_E_CHARS_ESCAPE_MAP = {
75 __proto__: null,
76 // See note on NUL above
77 '\0': '',
78 '\b': '\\b',
79 '\t': '\\t',
80 '\n': '\\n',
81 '\r': '\\r',
82 '\x1a': '\\x1a',
83 '"': '\\"',
84 '$': '\\$',
85 // This fails safe when we pick the wrong escaping convention for a
86 // single-quote delimited string.
87 // Empirically, from a psql10 client,
88 // # SELECT e'foo''bar';
89 // ?column?
90 // ----------
91 // foo'bar
92 '\'': '\'\'',
93 '\\': '\\\\',
94};
95
96const PG_U_CHARS_ESCAPE_MAP = {
97 __proto__: null,
98 // See note on NUL above
99 '\0': '',
100 '\b': '\\0008',
101 '\t': '\\0009',
102 '\n': '\\000a',
103 '\r': '\\000d',
104 '\x1a': '\\001a',
105 '"': '\\0022',
106 '$': '\\0024',
107 '\'': '\\0027',
108 '\\': '\\005c',
109};
110
111const HEX_GLOBAL_REGEXP = /[0-9A-Fa-f]/g;
112const HEX_TO_BINARY_TABLE = {
113 __proto__: null,
114 '0': '0000',
115 '1': '0001',
116 '2': '0010',
117 '3': '0011',
118 '4': '0100',
119 '5': '0101',
120 '6': '0110',
121 '7': '0111',
122 '8': '1000',
123 '9': '1001',
124 'A': '1010',
125 'B': '1011',
126 'C': '1100',
127 'D': '1101',
128 'E': '1110',
129 'F': '1111',
130 'a': '1010',
131 'b': '1011',
132 'c': '1100',
133 'd': '1101',
134 'e': '1110',
135 'f': '1111',
136};
137
138function hexDigitToBinary(digit) {
139 return HEX_TO_BINARY_TABLE[digit];
140}
141
142function hexToBinary(str) {
143 return str.replace(HEX_GLOBAL_REGEXP, hexDigitToBinary);
144}
145
146function pgEscapeStringBody(str, escapeMap) {
147 let chunkIndex = 0;
148 let escapedVal = '';
149
150 CHARS_GLOBAL_REGEXP.lastIndex = 0;
151 for (let match; (match = CHARS_GLOBAL_REGEXP.exec(str));) {
152 escapedVal += str.substring(chunkIndex, match.index) + escapeMap[match[0]];
153 chunkIndex = CHARS_GLOBAL_REGEXP.lastIndex;
154 }
155
156 if (chunkIndex === 0) {
157 // Nothing was escaped
158 return str;
159 }
160
161 if (chunkIndex < str.length) {
162 escapedVal += str.substring(chunkIndex);
163 }
164
165 return escapedVal;
166}
167
168function pgEscapeId(val, forbidQualified, unicode) {
169 if (isSqlFragment(val)) {
170 const { content } = val;
171 if ((forbidQualified ? PG_ID_REGEXP : PG_QUAL_ID_REGEXP).test(content)) {
172 return content;
173 }
174 throw new Error(`Expected id, got ${ content }`);
175 }
176 if (isSeries(val)) {
177 return escapeSeries(val, (element) => pgEscapeId(element, forbidQualified, unicode), false);
178 }
179 let escaped = unicode ?
180 pgEscapeStringBody(`${ val }`, PG_U_CHARS_ESCAPE_MAP) :
181 pgEscapeStringBody(`${ val }`, PG_ID_ESCAPE_MAP);
182 if (!forbidQualified) {
183 escaped = escaped.replace(QUAL_GLOBAL_REGEXP, unicode ? '".u&"' : '"."');
184 }
185 return `${ unicode ? 'u&"' : '"' }${ escaped }"`;
186}
187
188const PG_ID_DELIMS_REGEXP = /^(?:[Uu]&)?"|"$/g;
189
190function pgEscapeString(val) {
191 const str = `${ val }`;
192
193 const escapedVal = pgEscapeStringBody(val, PG_E_CHARS_ESCAPE_MAP);
194
195 if (escapedVal === str) {
196 return `'${ escapedVal }'`;
197 }
198
199 // If there are any backslashes or quotes, we use e'...' style strings since
200 // those allow a consistent scheme for escaping all string meta-characters so entail
201 // the fewest assumptions.
202 return `e'${ escapedVal }'`;
203}
204
205const pgEscape = makeEscaper(pgEscapeId, pgEscapeString);
206
207function pgEscapeDelimitedString(strValue, delimiter) {
208 switch (delimiter) {
209 case '\'':
210 case 'b\'':
211 case 'x\'':
212 return pgEscapeStringBody(strValue, PG_CHARS_ESCAPE_MAP);
213 case 'e\'':
214 return pgEscapeStringBody(strValue, PG_E_CHARS_ESCAPE_MAP);
215 case 'e':
216 return `'${ pgEscapeStringBody(strValue, PG_E_CHARS_ESCAPE_MAP) }'`;
217 case 'u&\'':
218 return pgEscapeStringBody(strValue, PG_U_CHARS_ESCAPE_MAP);
219 default:
220 break;
221 }
222
223 if (delimiter[0] === '$' && delimiter.indexOf('$', 1) === delimiter.length - 1) {
224 // Handle literal strings like $tag$...$tag$
225 let embedHazard = strValue.indexOf(delimiter) >= 0;
226 if (!embedHazard) {
227 const lastDollar = strValue.lastIndexOf('$');
228 if (lastDollar >= 0) {
229 const tail = strValue.substring(lastDollar);
230 embedHazard = (tail === delimiter.substring(0, tail.length));
231 }
232 }
233 if (embedHazard) {
234 throw new Error(`Cannot embed ${ JSON.stringify(strValue) } between ${ delimiter }`);
235 }
236 return strValue;
237 }
238 throw new Error(`Cannot escape with ${ delimiter }`);
239}
240
241function pgEscapeDelimited(value, delimiter, timeZone, forbidQualified) {
242 if (delimiter === '"') {
243 return pgEscapeId(value, forbidQualified, false).replace(PG_ID_DELIMS_REGEXP, '');
244 } else if (delimiter === 'u&"') {
245 return pgEscapeId(value, forbidQualified, true).replace(PG_ID_DELIMS_REGEXP, '');
246 }
247
248 let strValue = value;
249 if (isBuffer(value)) {
250 const wantsBinaryDigits = delimiter === 'b\'';
251 const encoding = wantsBinaryDigits || delimiter === 'x\'' ? 'hex' : 'binary';
252 strValue = apply(bufferProtoToString, value, [ encoding ]);
253 if (wantsBinaryDigits) {
254 // encoding='binary' to buffer means something very different from binary
255 // encoding in PGSql.
256 strValue = hexToBinary(strValue);
257 }
258 }
259 return pgEscapeDelimitedString(`${ strValue }`, delimiter);
260}
261
262module.exports = Object.freeze({
263 escape: pgEscape,
264 escapeId: pgEscapeId,
265 escapeDelimited: pgEscapeDelimited,
266});