Skip to content

Commit

Permalink
feat: add base64
Browse files Browse the repository at this point in the history
  • Loading branch information
fjc0k committed May 29, 2020
1 parent 60a12fa commit 348e8cb
Show file tree
Hide file tree
Showing 3 changed files with 338 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from 'lodash-es'

// @index(['./**/*.ts', '!./**/*.test.*'], f => `export * from '${f.path}'`)
export * from './utils/base64'
export * from './utils/dedent'
export * from './utils/EventBus'
export * from './utils/indent'
Expand Down
169 changes: 169 additions & 0 deletions src/utils/base64.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
describe('base64', () => {
const data: Array<[string, string, string]> = [
['', '', ''],
['v', 'dg==', 'dg'],
['vtils', 'dnRpbHM=', 'dnRpbHM'],
[
'vtils.base64Encode',
'dnRpbHMuYmFzZTY0RW5jb2Rl',
'dnRpbHMuYmFzZTY0RW5jb2Rl',
],
[
'JavaScript 工具库',
'SmF2YVNjcmlwdCDlt6XlhbflupM=',
'SmF2YVNjcmlwdCDlt6XlhbflupM',
],
[
'JavaScript\n工具库',
'SmF2YVNjcmlwdArlt6XlhbflupM=',
'SmF2YVNjcmlwdArlt6XlhbflupM',
],
['\0', 'AA==', 'AA'],
['1', 'MQ==', 'MQ'],
['-1', 'LTE=', 'LTE'],
[
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#0^&*();:<>,. []{}',
'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMjM0NTY3ODkhQCMwXiYqKCk7Ojw+LC4gW117fQ==',
'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMjM0NTY3ODkhQCMwXiYqKCk7Ojw-LC4gW117fQ',
],
[
'😁😎=-#@`.,?/|{*+😁',
'8J+YgfCfmI49LSNAYC4sPy98eyor8J+YgQ==',
'8J-YgfCfmI49LSNAYC4sPy98eyor8J-YgQ',
],
[
'❥(ゝω・✿ฺ)※▓●²♠⑲Ⅲ∵molÇùㄡεətsフぽㅚ㉢д╢┉(๑╹◡╹)ノ"""',
'4p2lKOOCnc+J44O74py/4Li6KeKAu+KWk+KXj8Ky4pmg4pGy4oWi4oi1bW9sw4fDueOEoc61yZl0c+ODleOBveOFmuOJotC04pWi4pSJKOC5keKVueKXoeKVuSnvvokiIiI=',
'4p2lKOOCnc-J44O74py_4Li6KeKAu-KWk-KXj8Ky4pmg4pGy4oWi4oi1bW9sw4fDueOEoc61yZl0c-ODleOBveOFmuOJotC04pWi4pSJKOC5keKVueKXoeKVuSnvvokiIiI',
],
['a\u{10126}ĉc车头', 'YfCQhKbEiWPovablpLQ=', 'YfCQhKbEiWPovablpLQ'],
]

beforeEach(() => {
jest.resetModules()
})

describe('在 NodeJS 环境中', () => {
test('编码正常', async () => {
const { base64Encode } = await import('./base64')
data.forEach(([str, encodedStr]) => {
expect(base64Encode(str)).toBe(encodedStr)
})
})

test('解码正常', async () => {
const { base64Decode } = await import('./base64')
data.forEach(([str, encodedStr]) => {
expect(base64Decode(encodedStr)).toBe(str)
})
})

test('URL 编码正常', async () => {
const { base64UrlEncode } = await import('./base64')
data.forEach(([str, , encodedUrlStr]) => {
expect(base64UrlEncode(str)).toBe(encodedUrlStr)
})
})

test('URL 解码正常', async () => {
const { base64UrlDecode } = await import('./base64')
data.forEach(([str, , encodedUrlStr]) => {
expect(base64UrlDecode(encodedUrlStr)).toBe(str)
})
})
})

describe('不在 NodeJS 环境中但有 atob, btoa', () => {
const bufferFrom = Buffer.from
beforeAll(() => {
Object.defineProperty(Buffer, 'from', {
value: null,
})
})
afterAll(() => {
Object.defineProperty(Buffer, 'from', {
value: bufferFrom,
})
})

test('编码正常', async () => {
const { base64Encode } = await import('./base64')
data.forEach(([str, encodedStr]) => {
expect(base64Encode(str)).toBe(encodedStr)
})
})

test('解码正常', async () => {
const { base64Decode } = await import('./base64')
data.forEach(([str, encodedStr]) => {
expect(base64Decode(encodedStr)).toBe(str)
})
})

test('URL 编码正常', async () => {
const { base64UrlEncode } = await import('./base64')
data.forEach(([str, , encodedUrlStr]) => {
expect(base64UrlEncode(str)).toBe(encodedUrlStr)
})
})

test('URL 解码正常', async () => {
const { base64UrlDecode } = await import('./base64')
data.forEach(([str, , encodedUrlStr]) => {
expect(base64UrlDecode(encodedUrlStr)).toBe(str)
})
})
})

describe('不在 NodeJS 环境中也没有 atob, btoa', () => {
const bufferFrom = Buffer.from
const globalWindow = { ...global.window }
beforeAll(() => {
Object.defineProperty(Buffer, 'from', {
value: null,
})
jest.spyOn(global, 'window', 'get').mockImplementation(
() =>
({
...globalWindow,
atob: undefined,
btoa: undefined,
} as any),
)
})
afterAll(() => {
Object.defineProperty(Buffer, 'from', {
value: bufferFrom,
})
jest.restoreAllMocks()
})

test('编码正常', async () => {
const { base64Encode } = await import('./base64')
data.forEach(([str, encodedStr]) => {
expect(base64Encode(str)).toBe(encodedStr)
})
})

test('解码正常', async () => {
const { base64Decode } = await import('./base64')
data.forEach(([str, encodedStr]) => {
expect(base64Decode(encodedStr)).toBe(str)
})
})

test('URL 编码正常', async () => {
const { base64UrlEncode } = await import('./base64')
data.forEach(([str, , encodedUrlStr]) => {
expect(base64UrlEncode(str)).toBe(encodedUrlStr)
})
})

test('URL 解码正常', async () => {
const { base64UrlDecode } = await import('./base64')
data.forEach(([str, , encodedUrlStr]) => {
expect(base64UrlDecode(encodedUrlStr)).toBe(str)
})
})
})
})
168 changes: 168 additions & 0 deletions src/utils/base64.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/**
* base64.js
* Dan Kogai (https://github.com/dankogai)
* Licensed under the BSD 3-Clause License
* https://github.com/dankogai/js-base64/blob/master/LICENSE.md
*
* Modified by Jay Fong
*/

type XToY = (value: string) => string

const canUseBufferFrom =
typeof Buffer !== 'undefined' && typeof Buffer.from === 'function'

const base64Chars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

const base64Table: { [key: string]: number } = {}
for (let i = 0; i < base64Chars.length; i++) {
base64Table[base64Chars[i]] = i
}

const fromCharCode = String.fromCharCode

// binaryToAscii
function binaryToAsciiReplacer(str: string) {
const padlen = [0, 2, 1][str.length % 3]
const ord =
(str.charCodeAt(0) << 16) |
((str.length > 1 ? str.charCodeAt(1) : 0) << 8) |
(str.length > 2 ? str.charCodeAt(2) : 0)
const chars = [
base64Chars.charAt(ord >>> 18),
base64Chars.charAt((ord >>> 12) & 63),
padlen >= 2 ? '=' : base64Chars.charAt((ord >>> 6) & 63),
padlen >= 1 ? '=' : base64Chars.charAt(ord & 63),
]
return chars.join('')
}
const binaryToAscii: XToY =
(typeof window !== 'undefined' && window.btoa) ||
(value => value.replace(/[\s\S]{1,3}/g, binaryToAsciiReplacer))

// utf8ToBinary
// eslint-disable-next-line no-control-regex
const utf8ToBinaryRegExp = /[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g
function utf8ToBinaryReplacer(str: string) {
if (str.length < 2) {
const cc = str.charCodeAt(0)
return cc < 0x80
? str
: cc < 0x800
? fromCharCode(0xc0 | (cc >>> 6)) + fromCharCode(0x80 | (cc & 0x3f))
: fromCharCode(0xe0 | ((cc >>> 12) & 0x0f)) +
fromCharCode(0x80 | ((cc >>> 6) & 0x3f)) +
fromCharCode(0x80 | (cc & 0x3f))
}
const cc =
0x10000 +
(str.charCodeAt(0) - 0xd800) * 0x400 +
(str.charCodeAt(1) - 0xdc00)
return (
fromCharCode(0xf0 | ((cc >>> 18) & 0x07)) +
fromCharCode(0x80 | ((cc >>> 12) & 0x3f)) +
fromCharCode(0x80 | ((cc >>> 6) & 0x3f)) +
fromCharCode(0x80 | (cc & 0x3f))
)
}
const utf8ToBinary: XToY = value =>
value.replace(utf8ToBinaryRegExp, utf8ToBinaryReplacer)

// utf8ToAscii
const utf8ToAscii: XToY = value => binaryToAscii(utf8ToBinary(value))

// asciiToBinary
function asciiToBinaryReplacer(str: string) {
const len = str.length
const padlen = len % 4
const n =
(len > 0 ? base64Table[str.charAt(0)] << 18 : 0) |
(len > 1 ? base64Table[str.charAt(1)] << 12 : 0) |
(len > 2 ? base64Table[str.charAt(2)] << 6 : 0) |
(len > 3 ? base64Table[str.charAt(3)] : 0)
const chars = [
fromCharCode(n >>> 16),
fromCharCode((n >>> 8) & 0xff),
fromCharCode(n & 0xff),
]
chars.length -= [0, 0, 2, 1][padlen]
return chars.join('')
}
const asciiToBinary: XToY =
(typeof window !== 'undefined' && window.atob) ||
(value => value.replace(/\S{1,4}/g, asciiToBinaryReplacer))

// binaryToUtf8
const binaryToUtf8RegExp = /[\xC0-\xDF][\x80-\xBF]|[\xE0-\xEF][\x80-\xBF]{2}|[\xF0-\xF7][\x80-\xBF]{3}/g
function binaryToUtf8Replacer(str: string) {
switch (str.length) {
case 4: {
const cp =
((0x07 & str.charCodeAt(0)) << 18) |
((0x3f & str.charCodeAt(1)) << 12) |
((0x3f & str.charCodeAt(2)) << 6) |
(0x3f & str.charCodeAt(3))
const offset = cp - 0x10000
return (
fromCharCode((offset >>> 10) + 0xd800) +
fromCharCode((offset & 0x3ff) + 0xdc00)
)
}
case 3:
return fromCharCode(
((0x0f & str.charCodeAt(0)) << 12) |
((0x3f & str.charCodeAt(1)) << 6) |
(0x3f & str.charCodeAt(2)),
)
default:
return fromCharCode(
((0x1f & str.charCodeAt(0)) << 6) | (0x3f & str.charCodeAt(1)),
)
}
}
const binaryToUtf8: XToY = value =>
value.replace(binaryToUtf8RegExp, binaryToUtf8Replacer)

// asciiToUtf8
const asciiToUtf8: XToY = value =>
binaryToUtf8(asciiToBinary(value.replace(/=+$/, '')))

/**
* base64Encode。
*
* @param value 值
* @returns 返回结果
*/
export function base64Encode(value: string): string {
if (canUseBufferFrom) {
return Buffer.from(value, 'utf8').toString('base64')
}

return utf8ToAscii(value)
}

/**
* base64Encode。
*
* @param value 值
* @returns 返回结果
*/
export function base64Decode(value: string): string {
if (canUseBufferFrom) {
return Buffer.from(value, 'base64').toString('utf8')
}

return asciiToUtf8(value)
}

export function base64UrlEncode(value: string): string {
return base64Encode(value)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '')
}

export function base64UrlDecode(value: string): string {
return base64Decode(value.replace(/-/g, '+').replace(/_/g, '/'))
}

0 comments on commit 348e8cb

Please sign in to comment.