]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
feat: add encoding functions
authorEduardo San Martin Morote <posva13@gmail.com>
Thu, 2 Jan 2020 19:01:25 +0000 (20:01 +0100)
committerEduardo San Martin Morote <posva13@gmail.com>
Thu, 2 Jan 2020 19:01:25 +0000 (20:01 +0100)
__tests__/encoding.spec.ts [new file with mode: 0644]
__tests__/url-encoding.spec.ts
src/utils/encoding.ts [new file with mode: 0644]

diff --git a/__tests__/encoding.spec.ts b/__tests__/encoding.spec.ts
new file mode 100644 (file)
index 0000000..6d20495
--- /dev/null
@@ -0,0 +1,133 @@
+import {
+  encodeHash,
+  encodeParam,
+  encodeQueryProperty,
+  // decode,
+} from '../src/utils/encoding'
+
+describe('Encoding', () => {
+  // all ascii chars with a non ascii char at the beginning
+  // let allChars = ''
+  // for (let i = 32; i < 127; i++) allChars += String.fromCharCode(i)
+
+  // per RFC 3986 (2005), strictest safe set
+  const unreservedSet =
+    '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-._~'
+  // Other safePerSpec sets are defined by following the URL Living standard https://url.spec.whatwg.org without chars from unreservedSet
+
+  let nonPrintableASCII = ''
+  let encodedNonPrintableASCII = ''
+  for (let i = 0; i < 32; i++) {
+    nonPrintableASCII += String.fromCharCode(i)
+    const hex = i.toString(16).toUpperCase()
+    encodedNonPrintableASCII += '%' + (hex.length > 1 ? hex : '0' + hex)
+  }
+
+  describe('params', () => {
+    // excludes ^ and ` even though they are safe per spec because all browsers encode it when manually entered
+    const safePerSpec = "!$&'()*+,:;=@[]_|"
+    const toEncode = ' "<>#?{}/^`'
+    const encodedToEncode = toEncode
+      .split('')
+      .map(c => {
+        const hex = c
+          .charCodeAt(0)
+          .toString(16)
+          .toUpperCase()
+        return '%' + (hex.length > 1 ? hex : '0' + hex)
+      })
+      .join('')
+
+    it('does not encode safe chars', () => {
+      expect(encodeParam(unreservedSet)).toBe(unreservedSet)
+    })
+
+    it('encodes non-ascii', () => {
+      expect(encodeParam('é')).toBe('%C3%A9')
+    })
+
+    it('encodes non-printable ascii', () => {
+      expect(encodeParam(nonPrintableASCII)).toBe(encodedNonPrintableASCII)
+    })
+
+    it('does not encode a safe set', () => {
+      expect(encodeParam(safePerSpec)).toBe(safePerSpec)
+    })
+
+    it('encodes a specific charset', () => {
+      expect(encodeParam(toEncode)).toBe(encodedToEncode)
+    })
+  })
+
+  describe('query params', () => {
+    const safePerSpec = "!$'*+,:;@[]_|?/{}^()`"
+    const toEncode = ' "<>#&='
+    const encodedToEncode = toEncode
+      .split('')
+      .map(c => {
+        const hex = c
+          .charCodeAt(0)
+          .toString(16)
+          .toUpperCase()
+        return '%' + (hex.length > 1 ? hex : '0' + hex)
+      })
+      .join('')
+
+    it('does not encode safe chars', () => {
+      expect(encodeQueryProperty(unreservedSet)).toBe(unreservedSet)
+    })
+
+    it('encodes non-ascii', () => {
+      expect(encodeQueryProperty('é')).toBe('%C3%A9')
+    })
+
+    it('encodes non-printable ascii', () => {
+      expect(encodeQueryProperty(nonPrintableASCII)).toBe(
+        encodedNonPrintableASCII
+      )
+    })
+
+    it('does not encode a safe set', () => {
+      expect(encodeQueryProperty(safePerSpec)).toBe(safePerSpec)
+    })
+
+    it('encodes a specific charset', () => {
+      expect(encodeQueryProperty(toEncode)).toBe(encodedToEncode)
+    })
+  })
+
+  describe('hash', () => {
+    const safePerSpec = "!$'*+,:;@[]_|?/{}^()#&="
+    const toEncode = ' "<>`'
+    const encodedToEncode = toEncode
+      .split('')
+      .map(c => {
+        const hex = c
+          .charCodeAt(0)
+          .toString(16)
+          .toUpperCase()
+        return '%' + (hex.length > 1 ? hex : '0' + hex)
+      })
+      .join('')
+
+    it('does not encode safe chars', () => {
+      expect(encodeHash(unreservedSet)).toBe(unreservedSet)
+    })
+
+    it('encodes non-ascii', () => {
+      expect(encodeHash('é')).toBe('%C3%A9')
+    })
+
+    it('encodes non-printable ascii', () => {
+      expect(encodeHash(nonPrintableASCII)).toBe(encodedNonPrintableASCII)
+    })
+
+    it('does not encode a safe set', () => {
+      expect(encodeHash(safePerSpec)).toBe(safePerSpec)
+    })
+
+    it('encodes a specific charset', () => {
+      expect(encodeHash(toEncode)).toBe(encodedToEncode)
+    })
+  })
+})
index 7c79c442df02e95d789f8370c1c3a706e8b2e02c..7dffd27caae4ef9882947b99a3635c6054b0a321 100644 (file)
@@ -17,7 +17,7 @@ function createHistory() {
   return routerHistory
 }
 
-// TODO: add encoding
+// TODO: test by spying on encode functions since things are already tested by encoding.spec.ts
 describe.skip('URL Encoding', () => {
   beforeAll(() => {
     createDom()
diff --git a/src/utils/encoding.ts b/src/utils/encoding.ts
new file mode 100644 (file)
index 0000000..33fae6c
--- /dev/null
@@ -0,0 +1,64 @@
+/**
+ * Encoding Rules
+ * ␣ = Space
+ * Path: ␣ " < > # ? { }
+ * Query: ␣ " < > # & =
+ * Hash: ␣ " < > `
+ *
+ * On top of that the RFC3986 (https://tools.ietf.org/html/rfc3986#section-2.2) defines some extra characters to be encoded. Most browsers do not encode them in encodeURI https://github.com/whatwg/url/issues/369 so it may be safer to also encode !'()*. Leaving unencoded only ASCII alphanum plus -._~
+ * This extra safety should be applied to query by patching the string returned by encodeURIComponent
+ * encodeURI also encodes [\]^
+ * \ should be encoded to avoid ambiguity. Browsers (IE, FF, C) transform a \ into a / if directly typed in
+ * ` should also be encoded everywhere because some browsers like FF encode it when directly written while others don't
+ * Safari and IE don't encode "<>{}` in hash
+ */
+// const EXTRA_RESERVED_RE = /[!'()*]/g
+// const encodeReservedReplacer = (c: string) => '%' + c.charCodeAt(0).toString(16)
+
+const HASH_RE = /#/g // %23
+const AMPERSAND_RE = /&/g // %26
+const EQUAL_RE = /=/g // %3D
+const IM_RE = /\?/g // %3F
+const SLASH_RE = /\//g // %2F
+
+const ENC_BRACKET_OPEN_RE = /%5B/g // [
+const ENC_BRACKET_CLOSE_RE = /%5D/g // ]
+const ENC_CARET_RE = /%5E/g // ^
+const ENC_CURLY_OPEN_RE = /%7B/g // {
+const ENC_PIPE_RE = /%7C/g // |
+const ENC_CURLY_CLOSE_RE = /%7D/g // }
+const ENC_BACKTICK_RE = /%60/g // `
+
+function commonEncode(text: string): string {
+  return encodeURI(text)
+    .replace(ENC_PIPE_RE, '|')
+    .replace(ENC_BRACKET_OPEN_RE, '[')
+    .replace(ENC_BRACKET_CLOSE_RE, ']')
+}
+
+export function encodeHash(text: string): string {
+  return commonEncode(text)
+    .replace(ENC_CURLY_OPEN_RE, '{')
+    .replace(ENC_CURLY_CLOSE_RE, '}')
+    .replace(ENC_CARET_RE, '^')
+}
+
+export function encodeQueryProperty(text: string): 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, '^')
+}
+
+export function encodeParam(text: string): string {
+  return commonEncode(text)
+    .replace(SLASH_RE, '%2F')
+    .replace(HASH_RE, '%23')
+    .replace(IM_RE, '%3F')
+}
+
+export const decode = decodeURIComponent