diff --git a/package-lock.json b/package-lock.json index f841a4f2f..145a12960 100644 --- a/package-lock.json +++ b/package-lock.json @@ -92,9 +92,10 @@ "snackbarjs": "^1.1.0", "sortablejs": "^1.15.7", "split.js": "^1.6.5", + "sql-formatter": "^15.6.5", "ssdeep.js": "0.0.3", "stream-browserify": "^3.0.0", - "tesseract.js": "5.1.1", + "tesseract.js": "^6.0.1", "ua-parser-js": "^1.0.41", "unorm": "^1.6.0", "url": "^0.11.4", @@ -8161,6 +8162,11 @@ "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "license": "MIT" }, + "node_modules/discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==" + }, "node_modules/dns-packet": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", @@ -11473,12 +11479,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-electron": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.2.tgz", - "integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==", - "license": "MIT" - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -13210,6 +13210,11 @@ "node": "*" } }, + "node_modules/moo": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==" + }, "node_modules/more-entropy": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/more-entropy/-/more-entropy-0.0.7.tgz", @@ -13315,6 +13320,32 @@ "dev": true, "license": "MIT" }, + "node_modules/nearley": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", + "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", + "dependencies": { + "commander": "^2.19.0", + "moo": "^0.5.0", + "railroad-diagrams": "^1.0.0", + "randexp": "0.4.6" + }, + "bin": { + "nearley-railroad": "bin/nearley-railroad.js", + "nearley-test": "bin/nearley-test.js", + "nearley-unparse": "bin/nearley-unparse.js", + "nearleyc": "bin/nearleyc.js" + }, + "funding": { + "type": "individual", + "url": "https://nearley.js.org/#give-to-nearley" + } + }, + "node_modules/nearley/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -15116,6 +15147,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==" + }, + "node_modules/randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "dependencies": { + "discontinuous-range": "1.0.0", + "ret": "~0.1.10" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -15540,6 +15588,14 @@ "node": ">=8" } }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "engines": { + "node": ">=0.12" + } + }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -16446,6 +16502,18 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/sql-formatter": { + "version": "15.6.5", + "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.6.5.tgz", + "integrity": "sha512-fr4TyM1udCSrOHOmouotwUi8dxIDhSLpYNmPePGFVzxq8/i8jd828IapE49QXG7Gzkswxo5WwdAGnYX4YpKoTg==", + "dependencies": { + "argparse": "^2.0.1", + "nearley": "^2.20.1" + }, + "bin": { + "sql-formatter": "bin/sql-formatter-cli.cjs" + } + }, "node_modules/ssdeep.js": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/ssdeep.js/-/ssdeep.js-0.0.3.tgz", @@ -16905,28 +16973,27 @@ "license": "MIT" }, "node_modules/tesseract.js": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-5.1.1.tgz", - "integrity": "sha512-lzVl/Ar3P3zhpUT31NjqeCo1f+D5+YfpZ5J62eo2S14QNVOmHBTtbchHm/YAbOOOzCegFnKf4B3Qih9LuldcYQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-6.0.1.tgz", + "integrity": "sha512-/sPvMvrCtgxnNRCjbTYbr7BRu0yfWDsMZQ2a/T5aN/L1t8wUQN6tTWv6p6FwzpoEBA0jrN2UD2SX4QQFRdoDbA==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "bmp-js": "^0.1.0", "idb-keyval": "^6.2.0", - "is-electron": "^2.2.2", "is-url": "^1.2.4", "node-fetch": "^2.6.9", "opencollective-postinstall": "^2.0.3", "regenerator-runtime": "^0.13.3", - "tesseract.js-core": "^5.1.1", + "tesseract.js-core": "^6.0.0", "wasm-feature-detect": "^1.2.11", "zlibjs": "^0.3.1" } }, "node_modules/tesseract.js-core": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tesseract.js-core/-/tesseract.js-core-5.1.1.tgz", - "integrity": "sha512-KX3bYSU5iGcO1XJa+QGPbi+Zjo2qq6eBhNjSGR5E5q0JtzkoipJKOUQD7ph8kFyteCEfEQ0maWLu8MCXtvX5uQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tesseract.js-core/-/tesseract.js-core-6.0.0.tgz", + "integrity": "sha512-1Qncm/9oKM7xgrQXZXNB+NRh19qiXGhxlrR8EwFbK5SaUbPZnS5OMtP/ghtqfd23hsr1ZvZbZjeuAGcMxd/ooA==", "license": "Apache-2.0" }, "node_modules/thingies": { diff --git a/package.json b/package.json index c558d31ce..5864cdd86 100644 --- a/package.json +++ b/package.json @@ -175,9 +175,10 @@ "snackbarjs": "^1.1.0", "sortablejs": "^1.15.7", "split.js": "^1.6.5", + "sql-formatter": "^15.6.5", "ssdeep.js": "0.0.3", "stream-browserify": "^3.0.0", - "tesseract.js": "5.1.1", + "tesseract.js": "^6.0.1", "ua-parser-js": "^1.0.41", "unorm": "^1.6.0", "url": "^0.11.4", diff --git a/src/core/operations/SQLBeautify.mjs b/src/core/operations/SQLBeautify.mjs index 0f3d2e3c2..2171f7fc1 100644 --- a/src/core/operations/SQLBeautify.mjs +++ b/src/core/operations/SQLBeautify.mjs @@ -3,8 +3,7 @@ * @copyright Crown Copyright 2016 * @license Apache-2.0 */ - -import vkbeautify from "vkbeautify"; +import { format } from "sql-formatter"; import Operation from "../Operation.mjs"; /** @@ -39,7 +38,26 @@ class SQLBeautify extends Operation { */ run(input, args) { const indentStr = args[0]; - return vkbeautify.sql(input, indentStr); + // Extract and replace bind variables like :Bind1 with __BIND_0__ + const bindRegex = /:\w+/g; + const bindMap = {}; + let bindCounter=0; + const placeholderInput = input.replace(bindRegex, (match) => { + const placeholder = `__BIND_${bindCounter++}__`; + bindMap[placeholder] = match; + return placeholder; + }); + // Format the SQL with chosen options + let formatted= format(placeholderInput, { + language: "mysql", // Use MySQL as the default dialect for better compatibility with real-world SQL + useTabs: indentStr==="\t", // true if tab, false if spaces + tabWidth: indentStr.length || 4, // fallback if empty + indentStyle: "standard" // fine for most SQL + }); + // Replace placeholders back with original bind variables + formatted = formatted.replace(/__BIND_\d+__/g, match => bindMap[match] || match); + + return formatted; } } diff --git a/tests/node/tests/operations.mjs b/tests/node/tests/operations.mjs index 022b07013..41eddd821 100644 --- a/tests/node/tests/operations.mjs +++ b/tests/node/tests/operations.mjs @@ -867,13 +867,15 @@ pCGTErs= }), it("SQL Beautify", () => { - const result = chef.SQLBeautify(`SELECT MONTH, ID, RAIN_I, TEMP_F -FROM STATS;`); - const expected = `SELECT MONTH, - ID, - RAIN_I, - TEMP_F -FROM STATS;`; + const result = chef.SQLBeautify(`SELECT MONTH, ID, RAIN_I, TEMP_F FROM STATS;`); + const expected = +`SELECT + MONTH, + ID, + RAIN_I, + TEMP_F +FROM + STATS;`; assert.strictEqual(result.toString(), expected); }), diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index 9fccd0513..9d803bcc9 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -153,6 +153,7 @@ import "./tests/SIGABA.mjs"; import "./tests/SM2.mjs"; import "./tests/SM4.mjs"; // import "./tests/SplitColourChannels.mjs"; // Cannot test operations that use the File type yet +import "./tests/SQLBeautify.mjs"; import "./tests/StrUtils.mjs"; import "./tests/StripIPv4Header.mjs"; import "./tests/StripTCPHeader.mjs"; diff --git a/tests/operations/tests/SQLBeautify.mjs b/tests/operations/tests/SQLBeautify.mjs new file mode 100644 index 000000000..92d02ade7 --- /dev/null +++ b/tests/operations/tests/SQLBeautify.mjs @@ -0,0 +1,54 @@ +/** + * SQLBeautify tests. + * + * @author GCHQDeveloper581 + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "SQL Beautify - basic", + input: "SELECT MONTH, ID, RAIN_I, TEMP_F FROM STATS;", + expectedOutput: +`SELECT + MONTH, + ID, + RAIN_I, + TEMP_F +FROM + STATS;`, + recipeConfig: [ + { + op: "SQL Beautify", + args: [" "], + }, + ], + }, + { + name: "SQL Beautify - upsert", + input: "INSERT INTO Table1 SELECT * FROM (SELECT :Bind1 as Field1, :Bind2 as Field2, :id as id) as new_data ON DUPLICATE KEY UPDATE Field1 = new_data.Field1, Field2 = new_data.Field2;", + expectedOutput: +`INSERT INTO + Table1 +SELECT + * +FROM + ( + SELECT + :Bind1 as Field1, + :Bind2 as Field2, + :id as id + ) as new_data +ON DUPLICATE KEY UPDATE + Field1 = new_data.Field1, + Field2 = new_data.Field2;`, + recipeConfig: [ + { + op: "SQL Beautify", + args: [" "], + }, + ], + }, +]);