From: Eduardo San Martin Morote Date: Thu, 2 Jan 2020 19:01:25 +0000 (+0100) Subject: feat: add encoding functions X-Git-Tag: v4.0.0-alpha.0~117 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=b6d40fe3eaab4d2b64c9d7c3ecaf48199bbd3a7d;p=thirdparty%2Fvuejs%2Frouter.git feat: add encoding functions --- diff --git a/__tests__/encoding.spec.ts b/__tests__/encoding.spec.ts new file mode 100644 index 00000000..6d204957 --- /dev/null +++ b/__tests__/encoding.spec.ts @@ -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) + }) + }) +}) diff --git a/__tests__/url-encoding.spec.ts b/__tests__/url-encoding.spec.ts index 7c79c442..7dffd27c 100644 --- a/__tests__/url-encoding.spec.ts +++ b/__tests__/url-encoding.spec.ts @@ -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 index 00000000..33fae6ca --- /dev/null +++ b/src/utils/encoding.ts @@ -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