]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
fix(encoding): differentiate keys and values in query
authorEduardo San Martin Morote <posva13@gmail.com>
Fri, 2 Oct 2020 09:01:25 +0000 (11:01 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Fri, 2 Oct 2020 09:01:25 +0000 (11:01 +0200)
__tests__/encoding.spec.ts
__tests__/parseQuery.spec.ts
__tests__/stringifyQuery.spec.ts
__tests__/urlEncoding.spec.ts
src/encoding.ts
src/query.ts

index f1e41a38b40f38d07322ec80d863cc27d7164f2b..8fd7ab992d2d0865b8d8db553d551f1f94d0e540 100644 (file)
@@ -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)
     })
   })
 
index 49909616ccb1ee59ec5b2b7a4a93884abe97efac..11542cb9919b1f09af1dbf7680fd55e2ceec9b75 100644 (file)
@@ -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,
index 038e07d49cf4bfee0dd6c7602055b200f9b00867..c39d1b4e8dfe458599067a099b6055e4d865ddbd 100644 (file)
@@ -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==')
+  })
 })
index 49008ac47eac6d51ac6e86862910dc3d80d7b8ff..c4663b2730c9611f9c759390e071e2a7c3dc09fe 100644 (file)
@@ -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: '%' },
     })
   })
index 260b6c374d8f437e018ca14de865b79aefdc2299..2c2df76a72f8b9d88fe1af1ac9caa4bb750fe08c 100644 (file)
@@ -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.
  *
index 7826ad9c57609569a5daa2e8cccd47b8d63f5f39..b8a1c02d7019626dac8d0c209e23ac03bf9acf2b 100644 (file)
@@ -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