From b02f6b61ee10500a6b2f0186160a897dd2625893 Mon Sep 17 00:00:00 2001 From: Mateusz Pietryga Date: Thu, 5 Sep 2024 18:59:37 +0200 Subject: [PATCH 1/3] fix: Adjust Bruno grammar to allow quoting some special characters in names query parameters Automatically quote query parameter keys if they contain characters sensitive for bru syntax: ':', '"', '{', '}' or ' '. Quoted keys stored in .bru files now support escaping, so it is possible to store keys that themselves contain '"' character. Fixes #3037 Fixes #2810 Fixes #2878 --- packages/bruno-lang/v2/src/bruToJson.js | 21 +++++++++++++++++-- packages/bruno-lang/v2/src/jsonToBru.js | 8 +++++-- .../bruno-lang/v2/tests/fixtures/request.bru | 3 +++ .../bruno-lang/v2/tests/fixtures/request.json | 18 ++++++++++++++++ 4 files changed, 46 insertions(+), 4 deletions(-) diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index 2fe5fb472..f3b8cb000 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -43,8 +43,14 @@ const grammar = ohm.grammar(`Bru { // Dictionary Blocks dictionary = st* "{" pairlist? tagend pairlist = optionalnl* pair (~tagend stnl* pair)* (~tagend space)* - pair = st* key st* ":" st* value st* - key = keychar* + pair = st* (quoted_key | key) st* ":" st* value st* + disable_char = "~" + quote_char = "\\"" + esc_char = "\\\\" + esc_quote_char = esc_char quote_char + quoted_key_char = ~(quote_char | esc_quote_char | nl) any + quoted_key = disable_char? quote_char (esc_quote_char | quoted_key_char)* quote_char + key = keychar+ value = multilinetextblock | valuechar* // Dictionary for Assert Block @@ -229,6 +235,14 @@ const sem = grammar.createSemantics().addAttribute('ast', { res[key.ast] = value.ast ? value.ast.trim() : ''; return res; }, + esc_quote_char(_1, quote) { + // unescape + return quote.sourceString; + }, + quoted_key(disabled, _1, chars, _2) { + // unquote + return (disabled? disabled.sourceString : "") + chars.ast.join(""); + }, key(chars) { return chars.sourceString ? chars.sourceString.trim() : ''; }, @@ -280,6 +294,9 @@ const sem = grammar.createSemantics().addAttribute('ast', { tagend(_1, _2) { return ''; }, + _terminal(){ + return this.sourceString; + }, _iter(...elements) { return elements.map((e) => e.ast); }, diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index 62b31c2f9..585db4c94 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -4,6 +4,10 @@ const { indentString } = require('../../v1/src/utils'); const enabled = (items = []) => items.filter((item) => item.enabled); const disabled = (items = []) => items.filter((item) => !item.enabled); +const quoteKey = (key) => { + const quotableChars = [':', '"', '{', '}', ' ']; + return quotableChars.some(char => key.includes(char)) ? ('"' + key.replaceAll('"', '\\"') + '"') : key; +} // remove the last line if two new lines are found const stripLastLine = (text) => { @@ -71,7 +75,7 @@ const jsonToBru = (json) => { if (enabled(queryParams).length) { bru += `\n${indentString( enabled(queryParams) - .map((item) => `${item.name}: ${item.value}`) + .map((item) => `${quoteKey(item.name)}: ${item.value}`) .join('\n') )}`; } @@ -79,7 +83,7 @@ const jsonToBru = (json) => { if (disabled(queryParams).length) { bru += `\n${indentString( disabled(queryParams) - .map((item) => `~${item.name}: ${item.value}`) + .map((item) => `~${quoteKey(item.name)}: ${item.value}`) .join('\n') )}`; } diff --git a/packages/bruno-lang/v2/tests/fixtures/request.bru b/packages/bruno-lang/v2/tests/fixtures/request.bru index 1a3efeab7..058297c36 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.bru +++ b/packages/bruno-lang/v2/tests/fixtures/request.bru @@ -13,6 +13,9 @@ get { params:query { apiKey: secret numbers: 998877665 + "colon:parameter": is allowed + "nested escaped \"quote\"": is allowed + ~"disabled:colon:parameter": is allowed ~message: hello } diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json index 9c8ed143d..4ad428215 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.json +++ b/packages/bruno-lang/v2/tests/fixtures/request.json @@ -23,6 +23,24 @@ "type": "query", "enabled": true }, + { + "name" : "colon:parameter", + "value" : "is allowed", + "type": "query", + "enabled": true + }, + { + "name" : "nested escaped \"quote\"", + "value" : "is allowed", + "type": "query", + "enabled": true + }, + { + "name" : "disabled:colon:parameter", + "value" : "is allowed", + "type": "query", + "enabled": false + }, { "name": "message", "value": "hello", From 8c6ce2e0847d43b7dde66ec8e9066aa0966968bb Mon Sep 17 00:00:00 2001 From: Mateusz Pietryga Date: Mon, 23 Sep 2024 19:29:51 +0200 Subject: [PATCH 2/3] fix: Allow quoting some special characters in names of multipart request body parts and headers in bru files Fixes #571 --- packages/bruno-lang/v2/src/jsonToBru.js | 14 +++++------ .../bruno-lang/v2/tests/fixtures/request.bru | 4 ++++ .../bruno-lang/v2/tests/fixtures/request.json | 24 +++++++++++++++++++ 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index 585db4c94..d041f8d5d 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -105,7 +105,7 @@ const jsonToBru = (json) => { if (enabled(headers).length) { bru += `\n${indentString( enabled(headers) - .map((item) => `${item.name}: ${item.value}`) + .map((item) => `${quoteKey(item.name)}: ${item.value}`) .join('\n') )}`; } @@ -113,7 +113,7 @@ const jsonToBru = (json) => { if (disabled(headers).length) { bru += `\n${indentString( disabled(headers) - .map((item) => `~${item.name}: ${item.value}`) + .map((item) => `~${quoteKey(item.name)}: ${item.value}`) .join('\n') )}`; } @@ -179,7 +179,7 @@ ${indentString(`domain: ${auth?.ntlm?.domain || ''}`)} } `; - } + } if (auth && auth.oauth2) { switch (auth?.oauth2?.grantType) { @@ -272,14 +272,14 @@ ${indentString(body.sparql)} if (enabled(body.formUrlEncoded).length) { const enabledValues = enabled(body.formUrlEncoded) - .map((item) => `${item.name}: ${getValueString(item.value)}`) + .map((item) => `${quoteKey(item.name)}: ${getValueString(item.value)}`) .join('\n'); bru += `${indentString(enabledValues)}\n`; } if (disabled(body.formUrlEncoded).length) { const disabledValues = disabled(body.formUrlEncoded) - .map((item) => `~${item.name}: ${getValueString(item.value)}`) + .map((item) => `~${quoteKey(item.name)}: ${getValueString(item.value)}`) .join('\n'); bru += `${indentString(disabledValues)}\n`; } @@ -300,14 +300,14 @@ ${indentString(body.sparql)} item.contentType && item.contentType !== '' ? ' @contentType(' + item.contentType + ')' : ''; if (item.type === 'text') { - return `${enabled}${item.name}: ${getValueString(item.value)}${contentType}`; + return `${enabled}${quoteKey(item.name)}: ${getValueString(item.value)}${contentType}`; } if (item.type === 'file') { let filepaths = item.value || []; let filestr = filepaths.join('|'); const value = `@file(${filestr})`; - return `${enabled}${item.name}: ${value}${contentType}`; + return `${enabled}${quoteKey(item.name)}: ${value}${contentType}`; } }) .join('\n') diff --git a/packages/bruno-lang/v2/tests/fixtures/request.bru b/packages/bruno-lang/v2/tests/fixtures/request.bru index 058297c36..9cbb45837 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.bru +++ b/packages/bruno-lang/v2/tests/fixtures/request.bru @@ -96,13 +96,17 @@ body:sparql { body:form-urlencoded { apikey: secret numbers: +91998877665 + "colon:parameter": is allowed ~message: hello + ~"disabled colon:parameter": is allowed } body:multipart-form { apikey: secret numbers: +91998877665 + "colon:part": is allowed ~message: hello + ~"disabled colon:part": is allowed } body:graphql { diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json index 4ad428215..f679066af 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.json +++ b/packages/bruno-lang/v2/tests/fixtures/request.json @@ -127,10 +127,20 @@ "value": "+91998877665", "enabled": true }, + { + "name": "colon:parameter", + "value": "is allowed", + "enabled": true + }, { "name": "message", "value": "hello", "enabled": false + }, + { + "name": "disabled colon:parameter", + "value": "is allowed", + "enabled": false } ], "multipartForm": [ @@ -148,12 +158,26 @@ "enabled": true, "type": "text" }, + { + "contentType": "", + "name": "colon:part", + "value": "is allowed", + "enabled": true, + "type": "text" + }, { "contentType": "", "name": "message", "value": "hello", "enabled": false, "type": "text" + }, + { + "contentType": "", + "name": "disabled colon:part", + "value": "is allowed", + "enabled": false, + "type": "text" } ] }, From bcf4673a640c1cc822207d0e42843091fe2f59fd Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Sat, 30 Aug 2025 01:44:33 +0530 Subject: [PATCH 3/3] chore: Update Bruno grammar and tests to support keys with spaces, braces, and nested escaped quotes in headers and query parameters --- packages/bruno-lang/v2/src/bruToJson.js | 2 +- .../bruno-lang/v2/tests/dictionary.spec.js | 19 +++++ .../bruno-lang/v2/tests/fixtures/request.bru | 13 ++++ .../bruno-lang/v2/tests/fixtures/request.json | 73 +++++++++++++++++++ 4 files changed, 106 insertions(+), 1 deletion(-) diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index f3b8cb000..7633bbf2e 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -50,7 +50,7 @@ const grammar = ohm.grammar(`Bru { esc_quote_char = esc_char quote_char quoted_key_char = ~(quote_char | esc_quote_char | nl) any quoted_key = disable_char? quote_char (esc_quote_char | quoted_key_char)* quote_char - key = keychar+ + key = keychar* value = multilinetextblock | valuechar* // Dictionary for Assert Block diff --git a/packages/bruno-lang/v2/tests/dictionary.spec.js b/packages/bruno-lang/v2/tests/dictionary.spec.js index dd9ddd472..8e9197576 100644 --- a/packages/bruno-lang/v2/tests/dictionary.spec.js +++ b/packages/bruno-lang/v2/tests/dictionary.spec.js @@ -81,6 +81,25 @@ headers { expect(output).toEqual(expected); }); + it('should parse single header with empty key', () => { + const input = ` +headers { + : world +}`; + + const output = parser(input); + const expected = { + headers: [ + { + name: '', + value: 'world', + enabled: true + } + ] + }; + expect(output).toEqual(expected); + }); + it('should parse multi headers', () => { const input = ` headers { diff --git a/packages/bruno-lang/v2/tests/fixtures/request.bru b/packages/bruno-lang/v2/tests/fixtures/request.bru index 9cbb45837..3f3ce9843 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.bru +++ b/packages/bruno-lang/v2/tests/fixtures/request.bru @@ -13,8 +13,10 @@ get { params:query { apiKey: secret numbers: 998877665 + "key with spaces": is allowed "colon:parameter": is allowed "nested escaped \"quote\"": is allowed + "{braces}": is allowed ~"disabled:colon:parameter": is allowed ~message: hello } @@ -26,6 +28,11 @@ params:path { headers { content-type: application/json Authorization: Bearer 123 + "key with spaces": is allowed + "colon:header": is allowed + "{braces}": is allowed + "nested escaped \"quote\"": is allowed + ~"disabled:colon:header": is allowed ~transaction-id: {{transactionId}} } @@ -96,7 +103,10 @@ body:sparql { body:form-urlencoded { apikey: secret numbers: +91998877665 + "key with spaces": is allowed "colon:parameter": is allowed + "nested escaped \"quote\"": is allowed + "{braces}": is allowed ~message: hello ~"disabled colon:parameter": is allowed } @@ -104,7 +114,10 @@ body:form-urlencoded { body:multipart-form { apikey: secret numbers: +91998877665 + "key with spaces": is allowed "colon:part": is allowed + "nested escaped \"quote\"": is allowed + "{braces}": is allowed ~message: hello ~"disabled colon:part": is allowed } diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json index f679066af..87c36bff3 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.json +++ b/packages/bruno-lang/v2/tests/fixtures/request.json @@ -23,6 +23,12 @@ "type": "query", "enabled": true }, + { + "name": "key with spaces", + "value": "is allowed", + "type": "query", + "enabled": true + }, { "name" : "colon:parameter", "value" : "is allowed", @@ -35,6 +41,12 @@ "type": "query", "enabled": true }, + { + "name": "{braces}", + "value": "is allowed", + "type": "query", + "enabled": true + }, { "name" : "disabled:colon:parameter", "value" : "is allowed", @@ -65,6 +77,31 @@ "value": "Bearer 123", "enabled": true }, + { + "name": "key with spaces", + "value": "is allowed", + "enabled": true + }, + { + "name": "colon:header", + "value": "is allowed", + "enabled": true + }, + { + "name": "{braces}", + "value": "is allowed", + "enabled": true + }, + { + "name": "nested escaped \"quote\"", + "value": "is allowed", + "enabled": true + }, + { + "name": "disabled:colon:header", + "value": "is allowed", + "enabled": false + }, { "name": "transaction-id", "value": "{{transactionId}}", @@ -127,11 +164,26 @@ "value": "+91998877665", "enabled": true }, + { + "name": "key with spaces", + "value": "is allowed", + "enabled": true + }, { "name": "colon:parameter", "value": "is allowed", "enabled": true }, + { + "name": "nested escaped \"quote\"", + "value": "is allowed", + "enabled": true + }, + { + "name": "{braces}", + "value": "is allowed", + "enabled": true + }, { "name": "message", "value": "hello", @@ -158,6 +210,13 @@ "enabled": true, "type": "text" }, + { + "contentType": "", + "name": "key with spaces", + "value": "is allowed", + "enabled": true, + "type": "text" + }, { "contentType": "", "name": "colon:part", @@ -165,6 +224,20 @@ "enabled": true, "type": "text" }, + { + "contentType": "", + "name": "nested escaped \"quote\"", + "value": "is allowed", + "enabled": true, + "type": "text" + }, + { + "contentType": "", + "name": "{braces}", + "value": "is allowed", + "enabled": true, + "type": "text" + }, { "contentType": "", "name": "message",