Skip to content

Commit 09f44c1

Browse files
committed
feat: b64encode() and b64decode()
1 parent 423a566 commit 09f44c1

16 files changed

Lines changed: 727 additions & 11 deletions

package.json

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,33 @@
11
{
2-
"name": "npm-base",
2+
"name": "@waiting/base64",
33
"author": "waiting",
44
"version": "0.0.1",
5-
"description": "npm boilerplate",
5+
"description": "Base64 encoding/decoding in pure JS on both modern Browsers and Node.js",
66
"keywords": [
7-
"种子项目",
7+
"base64",
8+
"browser",
9+
"Node.js",
10+
"rfc4648",
11+
"TextEncoder",
12+
"TextDecoder",
813
"typescript"
914
],
1015
"engines": {
1116
"node": ">=8.10.0"
1217
},
1318
"bin": {},
14-
"browser": "",
19+
"browser": "./dist/index.esm.js",
1520
"es2015": "./dist/index.esm.js",
1621
"main": "./dist/index.cjs.js",
1722
"module": "./dist/index.js",
1823
"types": "./dist/index.d.ts",
1924
"bugs": {
20-
"url": "https://github.com/waitingsong/npm-base/issues"
25+
"url": "https://github.com/waitingsong/base64/issues"
2126
},
22-
"homepage": "https://github.com/waitingsong/npm-base#readme",
27+
"homepage": "https://github.com/waitingsong/base64#readme",
2328
"repository": {
2429
"type": "git",
25-
"url": "git+https://github.com/waitingsong/npm-base.git"
30+
"url": "git+https://github.com/waitingsong/base64.git"
2631
},
2732
"license": "MIT",
2833
"nyc": {
@@ -46,16 +51,14 @@
4651
],
4752
"all": true
4853
},
49-
"dependencies": {
50-
"@waiting/shared-core": "^1.9.1",
51-
"rxjs": "^6.5.2"
52-
},
54+
"dependencies": {},
5355
"devDependencies": {
5456
"@types/mocha": "*",
5557
"@types/node": "^12.0.2",
5658
"@types/power-assert": "*",
5759
"@types/rewire": "*",
5860
"@types/rimraf": "*",
61+
"@waiting/shared-core": "^1.9.1",
5962
"@types/yargs": "^13.0.0",
6063
"coveralls": "^3.0.3",
6164
"intelli-espower-loader": "^1.0.1",

src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
export * from './lib/index'
3+
export * from './lib/model'
4+
export { ErrorMsg } from './lib/config'
5+
export { validB64Chars } from './lib/helper'

src/lib/browser.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import {
2+
ErrorMsg,
3+
} from './config'
4+
import { fromUint8Array } from './from_buffer'
5+
import {
6+
parseDecodeInputBase64,
7+
parseEncodeInputString,
8+
parseTextDecoder,
9+
parseTextEncoder,
10+
} from './helper'
11+
import { TextDecoderFn, TextEncoderFn } from './model'
12+
import { toUint8Array } from './to_buffer'
13+
14+
15+
export function browserEncode(
16+
input: string | number | bigint,
17+
textEncoder?: TextEncoderFn,
18+
): string {
19+
20+
const str: string = parseEncodeInputString(input)
21+
const Encoder = parseTextEncoder(textEncoder)
22+
const u8arr = new Encoder().encode(str)
23+
const ret = fromBuffer(u8arr)
24+
25+
return ret
26+
}
27+
28+
29+
/** Encode to base64, source from ArrayBuffer or Uint8Array */
30+
export function fromBuffer(buf: ArrayBuffer | Uint8Array): string {
31+
let input: Uint8Array
32+
33+
if (! buf) {
34+
throw new TypeError(ErrorMsg.fromArrayBufferInvalidParam)
35+
}
36+
else if (ArrayBuffer.isView(buf)) {
37+
input = buf
38+
}
39+
else {
40+
input = new Uint8Array(buf)
41+
}
42+
43+
return fromUint8Array(input)
44+
}
45+
46+
47+
export function browserDecode(
48+
base64: string,
49+
outputEncoding: string = 'utf-8',
50+
textDecoder?: TextDecoderFn,
51+
): string {
52+
53+
const Decoder = parseTextDecoder(textDecoder)
54+
const u8arr = toBuffer(base64)
55+
const ret = new Decoder(outputEncoding).decode(u8arr)
56+
57+
return ret
58+
}
59+
60+
61+
export function toBuffer(base64: string): Uint8Array {
62+
const str = parseDecodeInputBase64(base64)
63+
const u8arr = toUint8Array(str)
64+
65+
return u8arr
66+
}

src/lib/config.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
2+
export const baseChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
3+
export const lookup: string[] = [...baseChars]
4+
export const revLookup: number[] = []
5+
6+
for (let i = 0, len = lookup.length; i < len; ++i) {
7+
revLookup[lookup[i].charCodeAt(0)] = i
8+
}
9+
10+
// Support decoding URL-safe base64 strings, as Node.js does.
11+
// See: https://en.wikipedia.org/wiki/Base64#URL_applications
12+
revLookup['-'.charCodeAt(0)] = 62
13+
revLookup['_'.charCodeAt(0)] = 63
14+
15+
export const ErrorMsg = {
16+
fromArrayBufferInvalidParam: 'Invalid input, should be ArrayBuffer or Uint8Array',
17+
base64Invalidlength: 'Invalid string. Length must be a multiple of 4',
18+
notString: 'Invalid value of parameter, should be string',
19+
encodeInvalidParam: 'Invalid value of parameter of encode(), should be string|number|bigint',
20+
notValidB64String: 'Valid base64 string only contains /^[a-zA-Z0-9+/_-]+={0,2}$/',
21+
textEncoderUndefined: 'TextEncoder undefined!',
22+
textDecoderUndefined: 'TextDecoder undefined!',
23+
}
24+
25+
export const defaultConfig = {
26+
forceBrowser: false,
27+
}

src/lib/from_buffer.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// rewrite from https://github.com/beatgammit/base64-js
2+
import { lookup } from './config'
3+
4+
5+
export function fromUint8Array(input: Uint8Array): string {
6+
/* tslint:disable: no-bitwise */
7+
const len = input.length
8+
const extraBytes = len % 3 // if we have 1 byte left, pad 2 bytes
9+
const len2 = len - extraBytes
10+
const maxChunkLength = 12000 // must be multiple of 3
11+
const parts: string[] = new Array(
12+
Math.ceil(len2 / maxChunkLength) + (extraBytes ? 1 : 0),
13+
)
14+
let curChunk = 0
15+
16+
// go through the array every three bytes, we'll deal with trailing stuff later
17+
for (let i = 0, nextI = 0; i < len2; i = nextI) {
18+
nextI = i + maxChunkLength
19+
parts[curChunk] = encodeChunk(input, i, Math.min(nextI, len2))
20+
curChunk += 1
21+
}
22+
23+
// pad the end with zeros, but make sure to not forget the extra bytes
24+
if (extraBytes === 1) {
25+
const tmp = input[len2] & 0xFF
26+
parts[curChunk] = lookup[tmp >> 2] + lookup[tmp << 4 & 0x3F] + '=='
27+
}
28+
else if (extraBytes === 2) {
29+
const tmp = (input[len2] & 0xFF) << 8 | (input[len2 + 1] & 0xFF)
30+
parts[curChunk] = lookup[tmp >> 10] +
31+
lookup[tmp >> 4 & 0x3F] +
32+
lookup[tmp << 2 & 0x3F] +
33+
'='
34+
}
35+
36+
/* tslint:enable: no-bitwise */
37+
return parts.join('')
38+
}
39+
40+
41+
function encodeChunk(input: Uint8Array, start: number, end: number): string {
42+
/* tslint:disable: no-bitwise */
43+
if (start > end) {
44+
throw new Error('parameters of start should less then end')
45+
}
46+
const ret: string[] = new Array((end - start) / 3)
47+
for (let i = start, curTriplet = 0; i < end; i += 3) {
48+
ret[curTriplet++] = tripletToBase64(
49+
(input[i] & 0xFF) << 16 |
50+
(input[i + 1] & 0xFF) << 8 |
51+
(input[i + 2] & 0xFF),
52+
)
53+
}
54+
/* tslint:enable: no-bitwise */
55+
return ret.join('')
56+
}
57+
58+
function tripletToBase64(pos: number): string {
59+
/* tslint:disable: no-bitwise */
60+
const ret = lookup[pos >> 18 & 0x3F] +
61+
lookup[pos >> 12 & 0x3F] +
62+
lookup[pos >> 6 & 0x3F] +
63+
lookup[pos & 0x3F]
64+
65+
/* tslint:enable: no-bitwise */
66+
return ret
67+
}

src/lib/helper.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { ErrorMsg } from './config'
2+
import { TextDecoderFn, TextEncoderFn } from './model'
3+
4+
5+
export function parseEncodeInputString(input: string | number | bigint): string {
6+
const ret: string | null = typeof input === 'string'
7+
? input
8+
// tslint:disable-next-line: valid-typeof
9+
: (typeof input === 'number' || typeof input === 'bigint' ? input.toString() : null)
10+
11+
if (ret === null) {
12+
throw new TypeError(ErrorMsg.encodeInvalidParam)
13+
}
14+
15+
return ret
16+
}
17+
18+
19+
export function parseDecodeInputBase64(base64: string): string {
20+
if (typeof base64 !== 'string') {
21+
throw new TypeError(ErrorMsg.notString)
22+
}
23+
else if (!validB64Chars(base64)) {
24+
throw new TypeError(ErrorMsg.notValidB64String)
25+
}
26+
27+
return base64
28+
}
29+
30+
31+
export function parseTextEncoder(textEncoder?: TextEncoderFn): TextEncoderFn {
32+
const Encoder = typeof textEncoder === 'function'
33+
? textEncoder
34+
: (typeof TextEncoder === 'function' ? TextEncoder : null)
35+
36+
validateEncoder(Encoder)
37+
return <TextEncoderFn> Encoder
38+
}
39+
40+
41+
export function parseTextDecoder(textDecoder?: TextDecoderFn): TextDecoderFn {
42+
const Decoder = typeof textDecoder === 'function'
43+
? textDecoder
44+
: (typeof TextDecoder === 'function' ? TextDecoder : null)
45+
46+
validateEncoder(Decoder)
47+
return <TextDecoderFn> Decoder
48+
}
49+
50+
51+
/** Throw error if input be null */
52+
export function validateEncoder(input: any): void {
53+
if (input === null) {
54+
throw new TypeError(ErrorMsg.textEncoderUndefined)
55+
}
56+
}
57+
58+
59+
/** Whether string contains valid base64 characters */
60+
export function validB64Chars(input: string): boolean {
61+
const valid = /^[a-zA-Z0-9+/_-]+={0,2}$/.test(input)
62+
return valid
63+
}
64+
65+
66+
export function isBrowser(): boolean {
67+
// Buffer exists under karma testing
68+
const ret = typeof Buffer === 'function' && typeof window === 'undefined'
69+
? false
70+
: true
71+
return ret
72+
}

src/lib/index.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {
2+
browserDecode,
3+
browserEncode,
4+
} from './browser'
5+
import { defaultConfig } from './config'
6+
import { isBrowser } from './helper'
7+
import { TextDecoderFn, TextEncoderFn } from './model'
8+
import { NodeDecode, NodeEncode } from './nodejs'
9+
10+
11+
export function b64encode(
12+
input: string | number | bigint,
13+
textEncoder?: TextEncoderFn,
14+
): string {
15+
16+
const ret = defaultConfig.forceBrowser || isBrowser()
17+
? browserEncode(input, textEncoder)
18+
: NodeEncode(input)
19+
return ret
20+
}
21+
22+
23+
/** Encode to base64, source from string */
24+
export function b64decode(
25+
base64: string,
26+
outputEncoding: string = 'utf-8',
27+
textDecoder?: TextDecoderFn,
28+
): string {
29+
30+
const ret = defaultConfig.forceBrowser || isBrowser()
31+
? browserDecode(base64, outputEncoding, textDecoder)
32+
: NodeDecode(base64, outputEncoding)
33+
return ret
34+
}

src/lib/model.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import {
2+
TextDecoder as NodeTextDecoder,
3+
TextEncoder as NodeTextEncoder,
4+
} from 'util'
5+
6+
export type BrowserTextEncoderType = typeof TextEncoder
7+
export type BrowserTextDecoderType = typeof TextDecoder
8+
9+
export type NodeTextEncoderType = typeof NodeTextEncoder
10+
export type NodeTextDecoderType = typeof NodeTextDecoder
11+
12+
export type TextEncoderFn = BrowserTextEncoderType | NodeTextEncoderType
13+
export type TextDecoderFn = BrowserTextDecoderType | NodeTextDecoderType

src/lib/nodejs.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { ErrorMsg } from './config'
2+
import {
3+
parseDecodeInputBase64, parseEncodeInputString,
4+
} from './helper'
5+
6+
7+
export function NodeEncode(
8+
input: string | number | bigint,
9+
): string {
10+
11+
const str = parseEncodeInputString(input)
12+
const ret = Buffer.from(str).toString('base64')
13+
14+
return ret
15+
}
16+
17+
export function NodeDecode(
18+
base64: string,
19+
outputEncoding: string = 'utf-8',
20+
): string {
21+
22+
const str = parseDecodeInputBase64(base64)
23+
const ret = Buffer.from(str, 'base64').toString(outputEncoding)
24+
25+
return ret
26+
}
27+
28+
29+
/** Encode to base64, source from ArrayBuffer or Uint8Array */
30+
export function fromBuffer(buf: ArrayBuffer | Uint8Array): string {
31+
let inst: Buffer
32+
33+
if (! buf) {
34+
throw new TypeError(ErrorMsg.fromArrayBufferInvalidParam)
35+
}
36+
else if (ArrayBuffer.isView(buf)) {
37+
inst = Buffer.from(buf)
38+
}
39+
else {
40+
inst = Buffer.from(buf)
41+
}
42+
43+
const ret = inst.toString('base64')
44+
return ret
45+
}

src/lib/shared.ts

Whitespace-only changes.

0 commit comments

Comments
 (0)