From a967e427ab3bc5c1e6236b01f484a87b74a92be1 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Fri, 2 Oct 2020 11:01:25 +0200 Subject: [PATCH] fix(encoding): differentiate keys and values in query --- __tests__/encoding.spec.ts | 32 ++++++++++++++++++++++---------- __tests__/parseQuery.spec.ts | 8 +++++++- __tests__/stringifyQuery.spec.ts | 8 ++++++++ __tests__/urlEncoding.spec.ts | 22 +++++++++++++--------- src/encoding.ts | 14 +++++++++++--- src/query.ts | 21 ++++++++++----------- 6 files changed, 71 insertions(+), 34 deletions(-) diff --git a/__tests__/encoding.spec.ts b/__tests__/encoding.spec.ts index f1e41a38..8fd7ab99 100644 --- a/__tests__/encoding.spec.ts +++ b/__tests__/encoding.spec.ts @@ -1,7 +1,8 @@ import { encodeHash, encodeParam, - encodeQueryProperty, + encodeQueryKey, + encodeQueryValue, // decode, } from '../src/encoding' @@ -58,8 +59,16 @@ describe('Encoding', () => { describe('query params', () => { const safePerSpec = "!$'*+,:;@[]_|?/{}^()`" - const toEncode = ' "<>#&=' - const encodedToEncode = toEncode + const toEncodeForKey = ' "<>#&=' + const toEncodeForValue = ' "<>#&' + const encodedToEncodeForKey = toEncodeForKey + .split('') + .map(c => { + const hex = c.charCodeAt(0).toString(16).toUpperCase() + return '%' + (hex.length > 1 ? hex : '0' + hex) + }) + .join('') + const encodedToEncodeForValue = toEncodeForValue .split('') .map(c => { const hex = c.charCodeAt(0).toString(16).toUpperCase() @@ -68,25 +77,28 @@ describe('Encoding', () => { .join('') it('does not encode safe chars', () => { - expect(encodeQueryProperty(unreservedSet)).toBe(unreservedSet) + expect(encodeQueryValue(unreservedSet)).toBe(unreservedSet) + expect(encodeQueryKey(unreservedSet)).toBe(unreservedSet) }) it('encodes non-ascii', () => { - expect(encodeQueryProperty('é')).toBe('%C3%A9') + expect(encodeQueryValue('é')).toBe('%C3%A9') + expect(encodeQueryKey('é')).toBe('%C3%A9') }) it('encodes non-printable ascii', () => { - expect(encodeQueryProperty(nonPrintableASCII)).toBe( - encodedNonPrintableASCII - ) + expect(encodeQueryValue(nonPrintableASCII)).toBe(encodedNonPrintableASCII) + expect(encodeQueryKey(nonPrintableASCII)).toBe(encodedNonPrintableASCII) }) it('does not encode a safe set', () => { - expect(encodeQueryProperty(safePerSpec)).toBe(safePerSpec) + expect(encodeQueryValue(safePerSpec)).toBe(safePerSpec) + expect(encodeQueryKey(safePerSpec)).toBe(safePerSpec) }) it('encodes a specific charset', () => { - expect(encodeQueryProperty(toEncode)).toBe(encodedToEncode) + expect(encodeQueryKey(toEncodeForKey)).toBe(encodedToEncodeForKey) + expect(encodeQueryValue(toEncodeForValue)).toBe(encodedToEncodeForValue) }) }) diff --git a/__tests__/parseQuery.spec.ts b/__tests__/parseQuery.spec.ts index 49909616..11542cb9 100644 --- a/__tests__/parseQuery.spec.ts +++ b/__tests__/parseQuery.spec.ts @@ -36,7 +36,13 @@ describe('parseQuery', () => { }) }) - it('decodes empty values as null', () => { + it('allows = inside values', () => { + expect(parseQuery('e=c=a')).toEqual({ + e: 'c=a', + }) + }) + + it('parses empty values as null', () => { expect(parseQuery('e&b&c=a')).toEqual({ e: null, b: null, diff --git a/__tests__/stringifyQuery.spec.ts b/__tests__/stringifyQuery.spec.ts index 038e07d4..c39d1b4e 100644 --- a/__tests__/stringifyQuery.spec.ts +++ b/__tests__/stringifyQuery.spec.ts @@ -39,4 +39,12 @@ describe('stringifyQuery', () => { it('encodes values in arrays', () => { expect(stringifyQuery({ e: ['%', 'a'], b: 'c' })).toEqual('e=%25&e=a&b=c') }) + + it('encodes = in key', () => { + expect(stringifyQuery({ '=': 'a' })).toEqual('%3D=a') + }) + + it('keeps = in value', () => { + expect(stringifyQuery({ a: '=' })).toEqual('a==') + }) }) diff --git a/__tests__/urlEncoding.spec.ts b/__tests__/urlEncoding.spec.ts index 49008ac4..c4663b27 100644 --- a/__tests__/urlEncoding.spec.ts +++ b/__tests__/urlEncoding.spec.ts @@ -96,9 +96,10 @@ describe('URL Encoding', () => { it('calls encodeQueryProperty with query', async () => { const router = createRouter() await router.push({ name: 'home', query: { p: 'foo' } }) - expect(encoding.encodeQueryProperty).toHaveBeenCalledTimes(2) - expect(encoding.encodeQueryProperty).toHaveBeenNthCalledWith(1, 'p') - expect(encoding.encodeQueryProperty).toHaveBeenNthCalledWith(2, 'foo') + expect(encoding.encodeQueryValue).toHaveBeenCalledTimes(1) + expect(encoding.encodeQueryKey).toHaveBeenCalledTimes(1) + expect(encoding.encodeQueryKey).toHaveBeenNthCalledWith(1, 'p') + expect(encoding.encodeQueryValue).toHaveBeenNthCalledWith(1, 'foo') }) it('calls decode with query', async () => { @@ -113,21 +114,24 @@ describe('URL Encoding', () => { it('calls encodeQueryProperty with arrays in query', async () => { const router = createRouter() await router.push({ name: 'home', query: { p: ['foo', 'bar'] } }) - expect(encoding.encodeQueryProperty).toHaveBeenCalledTimes(3) - expect(encoding.encodeQueryProperty).toHaveBeenNthCalledWith(1, 'p') - expect(encoding.encodeQueryProperty).toHaveBeenNthCalledWith(2, 'foo') - expect(encoding.encodeQueryProperty).toHaveBeenNthCalledWith(3, 'bar') + expect(encoding.encodeQueryValue).toHaveBeenCalledTimes(2) + expect(encoding.encodeQueryKey).toHaveBeenCalledTimes(1) + expect(encoding.encodeQueryKey).toHaveBeenNthCalledWith(1, 'p') + expect(encoding.encodeQueryValue).toHaveBeenNthCalledWith(1, 'foo') + expect(encoding.encodeQueryValue).toHaveBeenNthCalledWith(2, 'bar') }) it('keeps decoded values in query', async () => { // @ts-ignore: override to make the difference encoding.decode = () => 'd' // @ts-ignore - encoding.encodeQueryProperty = () => 'e' + encoding.encodeQueryValue = () => 'ev' + // @ts-ignore + encoding.encodeQueryKey = () => 'ek' const router = createRouter() await router.push({ name: 'home', query: { p: '%' } }) expect(router.currentRoute.value).toMatchObject({ - fullPath: '/?e=e', + fullPath: '/?ek=ev', query: { p: '%' }, }) }) diff --git a/src/encoding.ts b/src/encoding.ts index 260b6c37..2c2df76a 100644 --- a/src/encoding.ts +++ b/src/encoding.ts @@ -61,23 +61,31 @@ export function encodeHash(text: string): string { } /** - * Encode characters that need to be encoded query keys and values on the query + * Encode characters that need to be encoded query values on the query * section of the URL. * * @param text - string to encode * @returns encoded string */ -export function encodeQueryProperty(text: string | number): string { +export function encodeQueryValue(text: string | number): string { return commonEncode(text) .replace(HASH_RE, '%23') .replace(AMPERSAND_RE, '%26') - .replace(EQUAL_RE, '%3D') .replace(ENC_BACKTICK_RE, '`') .replace(ENC_CURLY_OPEN_RE, '{') .replace(ENC_CURLY_CLOSE_RE, '}') .replace(ENC_CARET_RE, '^') } +/** + * Like `encodeQueryValue` but also encodes the `=` character. + * + * @param text - string to encode + */ +export function encodeQueryKey(text: string | number): string { + return encodeQueryValue(text).replace(EQUAL_RE, '%3D') +} + /** * Encode characters that need to be encoded on the path section of the URL. * diff --git a/src/query.ts b/src/query.ts index 7826ad9c..b8a1c02d 100644 --- a/src/query.ts +++ b/src/query.ts @@ -1,4 +1,4 @@ -import { decode, encodeQueryProperty } from './encoding' +import { decode, encodeQueryKey, encodeQueryValue } from './encoding' /** * Possible values in normalized {@link LocationQuery} @@ -50,13 +50,12 @@ export function parseQuery(search: string): LocationQuery { const hasLeadingIM = search[0] === '?' const searchParams = (hasLeadingIM ? search.slice(1) : search).split('&') for (let i = 0; i < searchParams.length; ++i) { - let [key, rawValue] = searchParams[i].split('=') as [ - string, - string | undefined - ] - key = decode(key) - // avoid decoding null - let value = rawValue == null ? null : decode(rawValue) + const searchParam = searchParams[i] + // allow the = character + let eqPos = searchParam.indexOf('=') + let key = decode(eqPos < 0 ? searchParam : searchParam.slice(0, eqPos)) + let value = eqPos < 0 ? null : decode(searchParam.slice(eqPos + 1)) + if (key in query) { // an extra variable for ts types let currentValue = query[key] @@ -85,7 +84,7 @@ export function stringifyQuery(query: LocationQueryRaw): string { for (let key in query) { if (search.length) search += '&' const value = query[key] - key = encodeQueryProperty(key) + key = encodeQueryKey(key) if (value == null) { // only null adds the value if (value !== undefined) search += key @@ -93,8 +92,8 @@ export function stringifyQuery(query: LocationQueryRaw): string { } // keep null values let values: LocationQueryValueRaw[] = Array.isArray(value) - ? value.map(v => v && encodeQueryProperty(v)) - : [value && encodeQueryProperty(value)] + ? value.map(v => v && encodeQueryValue(v)) + : [value && encodeQueryValue(value)] for (let i = 0; i < values.length; i++) { // only append & with i > 0 -- 2.47.3