Skip to content
Closed
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
8 changes: 4 additions & 4 deletions templates/full/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -537,8 +537,8 @@ export const scan = awaitInit(async (query: string): Promise<ScanResult> => {
resultPtr = callWasm(() => wasmModule._wasm_scan(queryPtr));
const resultStr = ptrToString(resultPtr);

if (resultStr.startsWith('syntax error') || resultStr.startsWith('deparse error') || resultStr.startsWith('ERROR')) {
throw new Error(resultStr);
if (!resultStr.startsWith('{')) {
throw new SqlError(resultStr);
}

return JSON.parse(resultStr);
Expand All @@ -564,8 +564,8 @@ export function scanSync(query: string): ScanResult {
resultPtr = callWasm(() => wasmModule._wasm_scan(queryPtr));
const resultStr = ptrToString(resultPtr);

if (resultStr.startsWith('syntax error') || resultStr.startsWith('deparse error') || resultStr.startsWith('ERROR')) {
throw new Error(resultStr);
if (!resultStr.startsWith('{')) {
throw new SqlError(resultStr);
}

return JSON.parse(resultStr);
Expand Down
10 changes: 6 additions & 4 deletions templates/full/wasm_wrapper.c
Original file line number Diff line number Diff line change
Expand Up @@ -272,14 +272,14 @@ static char* build_scan_json(PgQuery__ScanResult *scan_result, const char* origi
}

// Size the buffer from token text length, not token count: each token
// contributes at most ~170 bytes of fixed JSON plus up to 2 bytes per input
// byte once escaped.
// contributes at most ~170 bytes of fixed JSON plus up to 6 bytes per input
// byte once escaped (\u00XX for control characters).
size_t estimated_size = 1024;
for (size_t i = 0; i < scan_result->n_tokens; i++) {
PgQuery__ScanToken *token = scan_result->tokens[i];
int token_length = token->end - token->start;
if (token_length < 0) token_length = 0;
estimated_size += (size_t) token_length * 2 + 200;
estimated_size += (size_t) token_length * 6 + 200;
}
char* json = safe_malloc(estimated_size);
if (!json) {
Expand All @@ -305,7 +305,7 @@ static char* build_scan_json(PgQuery__ScanResult *scan_result, const char* origi
token_text[token_length] = '\0';

// Escape token text for JSON
char* escaped_text = safe_malloc(token_length * 2 + 1);
char* escaped_text = safe_malloc(token_length * 6 + 1);
if (!escaped_text) {
free(token_text);
continue;
Expand All @@ -326,6 +326,8 @@ static char* build_scan_json(PgQuery__ScanResult *scan_result, const char* origi
} else if (c == '\t') {
escaped_text[escaped_pos++] = '\\';
escaped_text[escaped_pos++] = 't';
} else if ((unsigned char) c < 0x20) {
escaped_pos += sprintf(escaped_text + escaped_pos, "\\u%04x", (unsigned char) c);
} else {
escaped_text[escaped_pos++] = c;
}
Expand Down
8 changes: 4 additions & 4 deletions versions/18/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -544,8 +544,8 @@ export const scan = awaitInit(async (query: string): Promise<ScanResult> => {
resultPtr = callWasm(() => wasmModule._wasm_scan(queryPtr));
const resultStr = ptrToString(resultPtr);

if (resultStr.startsWith('syntax error') || resultStr.startsWith('deparse error') || resultStr.startsWith('ERROR')) {
throw new Error(resultStr);
if (!resultStr.startsWith('{')) {
throw new SqlError(resultStr);
}

return JSON.parse(resultStr);
Expand All @@ -571,8 +571,8 @@ export function scanSync(query: string): ScanResult {
resultPtr = callWasm(() => wasmModule._wasm_scan(queryPtr));
const resultStr = ptrToString(resultPtr);

if (resultStr.startsWith('syntax error') || resultStr.startsWith('deparse error') || resultStr.startsWith('ERROR')) {
throw new Error(resultStr);
if (!resultStr.startsWith('{')) {
throw new SqlError(resultStr);
}

return JSON.parse(resultStr);
Expand Down
10 changes: 6 additions & 4 deletions versions/18/src/wasm_wrapper.c
Original file line number Diff line number Diff line change
Expand Up @@ -279,14 +279,14 @@ static char* build_scan_json(PgQuery__ScanResult *scan_result, const char* origi
}

// Size the buffer from token text length, not token count: each token
// contributes at most ~170 bytes of fixed JSON plus up to 2 bytes per input
// byte once escaped.
// contributes at most ~170 bytes of fixed JSON plus up to 6 bytes per input
// byte once escaped (\u00XX for control characters).
size_t estimated_size = 1024;
for (size_t i = 0; i < scan_result->n_tokens; i++) {
PgQuery__ScanToken *token = scan_result->tokens[i];
int token_length = token->end - token->start;
if (token_length < 0) token_length = 0;
estimated_size += (size_t) token_length * 2 + 200;
estimated_size += (size_t) token_length * 6 + 200;
}
char* json = safe_malloc(estimated_size);
if (!json) {
Expand All @@ -312,7 +312,7 @@ static char* build_scan_json(PgQuery__ScanResult *scan_result, const char* origi
token_text[token_length] = '\0';

// Escape token text for JSON
char* escaped_text = safe_malloc(token_length * 2 + 1);
char* escaped_text = safe_malloc(token_length * 6 + 1);
if (!escaped_text) {
free(token_text);
continue;
Expand All @@ -333,6 +333,8 @@ static char* build_scan_json(PgQuery__ScanResult *scan_result, const char* origi
} else if (c == '\t') {
escaped_text[escaped_pos++] = '\\';
escaped_text[escaped_pos++] = 't';
} else if ((unsigned char) c < 0x20) {
escaped_pos += sprintf(escaped_text + escaped_pos, "\\u%04x", (unsigned char) c);
} else {
escaped_text[escaped_pos++] = c;
}
Expand Down
16 changes: 16 additions & 0 deletions versions/18/test/scan.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -303,5 +303,21 @@ comment */ SELECT 2`;
assert.equal(t.text, `$$${literal}$$`);
}
});

it("should escape control characters in token text", () => {
for (const c of ["\x01", "\b", "\f", "\x1f"]) {
const sql = `SELECT 'a${c}b', $$${c}$$`;
const result = query.scanSync(sql);
assert.deepEqual(result.tokens.map(t => t.text), ["SELECT", `'a${c}b'`, ",", `$$${c}$$`]);
}
});

it("should throw SqlError for input the lexer rejects", async () => {
for (const sql of ["SELECT 'unterminated", "SELECT $$x", "SELECT /* x", ""]) {
assert.throws(() => query.scanSync(sql), query.SqlError, JSON.stringify(sql));
await assert.rejects(query.scan(sql), query.SqlError, JSON.stringify(sql));
}
assert.equal(query.scanSync("SELECT 1").tokens.length, 2);
});
});
});
Loading