From 68b262525919ee816f048e429892270bab0c9548 Mon Sep 17 00:00:00 2001 From: "Siddharth Gelera (reaper)" Date: Fri, 7 Nov 2025 19:09:55 +0530 Subject: [PATCH] feat(common): add patternHasher utility for hashing and restoring string from special characters (#6032) * feat: add patternHasher utility for variable hashing This utility function hashes input strings containing variables and allows for restoration of the original string. It includes support for custom matchers and handles cases where no variables are matched. * Update packages/bruno-common/src/utils/template-hasher.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Sid Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/utils/template-hasher.spec.ts | 32 ++++++++++++ .../bruno-common/src/utils/template-hasher.ts | 49 +++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 packages/bruno-common/src/utils/template-hasher.spec.ts create mode 100644 packages/bruno-common/src/utils/template-hasher.ts diff --git a/packages/bruno-common/src/utils/template-hasher.spec.ts b/packages/bruno-common/src/utils/template-hasher.spec.ts new file mode 100644 index 000000000..e4ad1aeee --- /dev/null +++ b/packages/bruno-common/src/utils/template-hasher.spec.ts @@ -0,0 +1,32 @@ +import { describe, it, expect } from '@jest/globals'; +import { patternHasher } from './template-hasher'; + +describe('patternHasher', () => { + it('hashes and restore are mathematically reproducible', () => { + const originalUrl = '{{host}}.example.com'; + const { hashed, restore } = patternHasher(originalUrl); + expect(hashed).toMatchInlineSnapshot(`"bruno-var-hash--163450413.example.com"`); + expect(restore(hashed)).toEqual(originalUrl); + }); + + it('hashes more than once', () => { + const originalUrl = '{{host}}.example.{{new}}'; + const { hashed, restore } = patternHasher(originalUrl); + expect(hashed).toMatchInlineSnapshot(`"bruno-var-hash--163450413.example.bruno-var-hash-652560383"`); + expect(restore(hashed)).toEqual(originalUrl); + }); + + it('allows custom matchers', () => { + const originalUrl = '$name.example.com'; + const { hashed, restore } = patternHasher(originalUrl, /\$(\w+)/); + expect(hashed).toMatchInlineSnapshot(`"bruno-var-hash-180907786.example.com"`); + expect(restore(hashed)).toEqual(originalUrl); + }); + + it('ignore unless matched', () => { + const originalUrl = '$name.example.com'; + const { hashed, restore } = patternHasher(originalUrl); + expect(hashed).toMatchInlineSnapshot(`"$name.example.com"`); + expect(restore(hashed)).toEqual(originalUrl); + }); +}); diff --git a/packages/bruno-common/src/utils/template-hasher.ts b/packages/bruno-common/src/utils/template-hasher.ts new file mode 100644 index 000000000..290c8a3e9 --- /dev/null +++ b/packages/bruno-common/src/utils/template-hasher.ts @@ -0,0 +1,49 @@ +const VARIABLE_REGEX = /\{\{([^}]+)\}\}/g; + +/** + * Was implemented specifically for request.url where the url might have variables + * that might need to be sanitised before being passed to a URL validator that doesn't + * allow special characters that bruno uses as variables (`{{var_name}}`) + * + * The function replaces the input string with a unique hash that can be restored + * later by the helper returned by this function + */ +export function patternHasher(input: string, pattern: string | RegExp = VARIABLE_REGEX) { + const usableRegex = new RegExp(pattern, 'g'); + function hash(toHash: string) { + let hash = 5381; + let c; + for (let i = 0; i < toHash.length; i++) { + c = toHash.charCodeAt(i); + hash = ((hash << 5) + hash + c) | 0; + } + return '' + hash; + } + + const prefix = `bruno-var-hash-`; + const hashToOriginal: Record = {}; + let result = input; + let hashed = false; + if (usableRegex.test(input)) { + hashed = true; + result = input.replace(usableRegex, function (matchedVar) { + const hashedValue = `${prefix}${hash(matchedVar)}`; + hashToOriginal[hashedValue] = matchedVar; + return hashedValue; + }); + } + return { + hashed: result, + restore(current: string) { + if (!hashed) { + return current; + } + let clone = current; + for (const hash in hashToOriginal) { + const value = hashToOriginal[hash]; + clone = clone.replace(hash, value); + } + return clone; + } + }; +}