diff --git a/.github/workflows/ci-module.yml b/.github/workflows/ci-module.yml index 54426ca..4b7ee12 100644 --- a/.github/workflows/ci-module.yml +++ b/.github/workflows/ci-module.yml @@ -1,14 +1,12 @@ name: ci on: - push: - branches: - - master - pull_request: - workflow_dispatch: + push: + branches: + - master + pull_request: + workflow_dispatch: jobs: - test: - uses: hapijs/.github/.github/workflows/ci-module.yml@master - with: - min-node-version: 14 + test: + uses: hapijs/.github/.github/workflows/ci-module.yml@min-node-22-hapi-21 diff --git a/.gitignore b/.gitignore index 8f679c9..af5a347 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ **/node_modules **/package-lock.json -coverage.* +coverage/ **/.DS_Store **/._* diff --git a/API.md b/API.md index d17e430..7403a3d 100644 --- a/API.md +++ b/API.md @@ -1,14 +1,11 @@ - ### encode(buffer) Base64 encode the buffer and return it as a new Buffer. - ### decode(buffer) Base64 decode the buffer and return the result as a new buffer. - ### Encoder Transform stream that base64 encodes each chunk of the stream. @@ -27,7 +24,6 @@ const encoder = new B64.Encoder(); stream.pipe(encoder).pipe(process.stdout); ``` - ### Decoder Transform stream that base64 decodes each chunk of the stream. @@ -52,4 +48,4 @@ Encodes value of string or buffer type in Base64 or URL encoding, function will ### base64urlDecode(value) -Decodes string into Base64 or URL encoding, function throws an error on invalid input and returns a string or buffer depending on encoding provided. Default encoding is binary. +Decodes string into Base64 or URL encoding, function throws an error on invalid input and returns a string or buffer depending on encoding provided. Default encoding is binary. diff --git a/lib/index.js b/lib/index.js deleted file mode 100755 index fc5a70d..0000000 --- a/lib/index.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict'; - -const Hoek = require('@hapi/hoek'); - -const Decoder = require('./decoder'); -const Encoder = require('./encoder'); - - -exports.decode = Decoder.decode; - -exports.encode = Encoder.encode; - -exports.Decoder = Decoder.Decoder; - -exports.Encoder = Encoder.Encoder; - - -// Base64url (RFC 4648) encode - -exports.base64urlEncode = function (value, encoding) { - - Hoek.assert(typeof value === 'string' || Buffer.isBuffer(value), 'value must be string or buffer'); - const buf = (Buffer.isBuffer(value) ? value : Buffer.from(value, encoding || 'binary')); - return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/\=/g, ''); -}; - - -// Base64url (RFC 4648) decode - -exports.base64urlDecode = function (value, encoding) { - - if (typeof value !== 'string') { - - throw new Error('Value not a string'); - } - - if (!/^[\w\-]*$/.test(value)) { - - throw new Error('Invalid character'); - } - - const buf = Buffer.from(value, 'base64'); - return (encoding === 'buffer' ? buf : buf.toString(encoding || 'binary')); -}; diff --git a/oxfmt.config.ts b/oxfmt.config.ts new file mode 100644 index 0000000..12e357b --- /dev/null +++ b/oxfmt.config.ts @@ -0,0 +1,8 @@ +import DefaultOxfmtConfig from '@hapi/oxc-plugin/oxfmt'; +import { defineConfig } from 'oxfmt'; + +import type { OxfmtConfig } from 'oxfmt'; + +export default defineConfig({ + ...DefaultOxfmtConfig, +}) as OxfmtConfig; diff --git a/oxlint.config.ts b/oxlint.config.ts new file mode 100644 index 0000000..fd4548e --- /dev/null +++ b/oxlint.config.ts @@ -0,0 +1,11 @@ +import HapiRecommended from '@hapi/oxc-plugin/oxlint'; +import { defineConfig } from 'oxlint'; + +import type { OxlintConfig } from 'oxlint'; + +export default defineConfig({ + extends: [HapiRecommended], + env: { + ...HapiRecommended.env, + }, +}) as OxlintConfig; diff --git a/package.json b/package.json index cd65b6f..b0f451a 100644 --- a/package.json +++ b/package.json @@ -1,36 +1,51 @@ { - "name": "@hapi/b64", - "description": "Base64 streaming encoder and decoder", - "version": "6.0.1", - "repository": "git://github.com/hapijs/b64", - "main": "lib/index.js", - "files": [ - "lib" - ], - "keywords": [ - "buffer", - "base64", - "decode", - "encode", - "stream" - ], - "eslintConfig": { - "extends": [ - "plugin:@hapi/module" - ] - }, - "dependencies": { - "@hapi/hoek": "^11.0.2" - }, - "devDependencies": { - "@hapi/code": "^9.0.0", - "@hapi/eslint-plugin": "^6.0.0", - "@hapi/lab": "^25.0.1", - "@hapi/wreck": "^18.0.0" - }, - "scripts": { - "test": "lab -a @hapi/code -t 100 -L", - "test-cov-html": "lab -a @hapi/code -r html -o coverage.html" - }, - "license": "BSD-3-Clause" + "name": "@hapi/b64", + "version": "6.0.1", + "description": "Base64 streaming encoder and decoder", + "keywords": [ + "base64", + "buffer", + "decode", + "encode", + "stream" + ], + "license": "BSD-3-Clause", + "repository": "git://github.com/hapijs/b64", + "files": [ + "src", + "API.md" + ], + "type": "module", + "types": "src/index.d.ts", + "exports": { + ".": { + "types": "./src/index.d.ts", + "default": "./src/index.js" + } + }, + "scripts": { + "test": "vitest run --coverage", + "typecheck": "tsc --noEmit", + "lint": "oxlint", + "lint:fix": "oxlint --fix", + "fmt": "oxfmt --check", + "fmt:fix": "oxfmt", + "check": "npm run lint && npm run fmt && npm run typecheck && npm test" + }, + "dependencies": { + "@hapi/hoek": "^12.0.0-rc.0" + }, + "devDependencies": { + "@hapi/oxc-plugin": "^1.1.1", + "@hapi/wreck": "^18.0.0", + "@types/node": "^22.19.20", + "@vitest/coverage-v8": "^5.0.5", + "oxfmt": "^0.75.0", + "oxlint": "^1.90.0", + "typescript": "^6.0.3", + "vitest": "^5.0.5" + }, + "engines": { + "node": ">=22" + } } diff --git a/lib/decoder.js b/src/decoder.js similarity index 56% rename from lib/decoder.js rename to src/decoder.js index 23efe06..c2ce007 100755 --- a/lib/decoder.js +++ b/src/decoder.js @@ -1,31 +1,20 @@ -'use strict'; - /* Decode functions adapted from: Version 1.0 12/25/99 Copyright (C) 1999 Masanao Izumo http://www.onicos.com/staff/iz/amuse/javascript/expert/base64.txt */ -const Stream = require('stream'); - - -const internals = { - decodeChars: [ - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, - -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, - 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, - -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, - 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1 - ] -}; - +import Stream from 'stream'; -exports.decode = function (buffer) { +const decodeChars = [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, + -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, + 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, +]; - const decodeChars = internals.decodeChars; +export function decode(buffer) { const len = buffer.length; const allocated = Math.ceil(len / 4) * 3; const result = Buffer.alloc(allocated); @@ -36,11 +25,10 @@ exports.decode = function (buffer) { let c4; let j = 0; - for (let i = 0; i < len; ) { + for (let i = 0; i < len;) { do { c1 = decodeChars[buffer[i++] & 0xff]; - } - while (i < len && c1 === -1); + } while (i < len && c1 === -1); if (c1 === -1) { break; @@ -48,8 +36,7 @@ exports.decode = function (buffer) { do { c2 = decodeChars[buffer[i++] & 0xff]; - } - while (i < len && c2 === -1); + } while (i < len && c2 === -1); if (c2 === -1) { break; @@ -59,13 +46,12 @@ exports.decode = function (buffer) { do { c3 = buffer[i++] & 0xff; - if (c3 === 61) { // = + if (c3 === 61 /* = */) { return result.slice(0, j); } c3 = decodeChars[c3]; - } - while (i < len && c3 === -1); + } while (i < len && c3 === -1); if (c3 === -1) { break; @@ -75,52 +61,46 @@ exports.decode = function (buffer) { do { c4 = buffer[i++] & 0xff; - if (c4 === 61) { // = + if (c4 === 61 /* = */) { return result.slice(0, j); } c4 = decodeChars[c4]; - } - while (i < len && c4 === -1); + } while (i < len && c4 === -1); if (c4 !== -1) { result[j++] = ((c3 & 0x03) << 6) | c4; } } - return (j === allocated ? result : result.slice(0, j)); -}; - + return j === allocated ? result : result.slice(0, j); +} -exports.Decoder = class Decoder extends Stream.Transform { +export class Decoder extends Stream.Transform { constructor() { - super(); this._reminder = null; } _transform(chunk, encoding, callback) { - let part = this._reminder ? Buffer.concat([this._reminder, chunk]) : chunk; const remaining = part.length % 4; if (remaining) { this._reminder = part.slice(part.length - remaining); part = part.slice(0, part.length - remaining); - } - else { + } else { this._reminder = null; } - this.push(exports.decode(part)); + this.push(decode(part)); return callback(); } _flush(callback) { - if (this._reminder) { - this.push(exports.decode(this._reminder)); + this.push(decode(this._reminder)); } return callback(); } -}; +} diff --git a/lib/encoder.js b/src/encoder.js similarity index 72% rename from lib/encoder.js rename to src/encoder.js index c32ef6f..9e7747e 100755 --- a/lib/encoder.js +++ b/src/encoder.js @@ -1,52 +1,40 @@ -'use strict'; - /* Encode functions adapted from: Version 1.0 12/25/99 Copyright (C) 1999 Masanao Izumo http://www.onicos.com/staff/iz/amuse/javascript/expert/base64.txt */ -const Stream = require('stream'); - - -const internals = {}; - - -exports.encode = function (buffer) { +import Stream from 'stream'; +export function encode(buffer) { return Buffer.from(buffer.toString('base64')); -}; +} - -exports.Encoder = class Encoder extends Stream.Transform { +export class Encoder extends Stream.Transform { constructor() { - super(); this._reminder = null; } _transform(chunk, encoding, callback) { - let part = this._reminder ? Buffer.concat([this._reminder, chunk]) : chunk; const remaining = part.length % 3; if (remaining) { this._reminder = part.slice(part.length - remaining); part = part.slice(0, part.length - remaining); - } - else { + } else { this._reminder = null; } - this.push(exports.encode(part)); + this.push(encode(part)); return callback(); } _flush(callback) { - if (this._reminder) { - this.push(exports.encode(this._reminder)); + this.push(encode(this._reminder)); } return callback(); } -}; +} diff --git a/src/index.d.ts b/src/index.d.ts new file mode 100644 index 0000000..f9de37e --- /dev/null +++ b/src/index.d.ts @@ -0,0 +1,52 @@ +/// + +import { Transform } from 'stream'; + +/** + * Decodes a base64 buffer. + * + * @param buffer - The base64-encoded buffer. + * @returns The decoded buffer. + */ +export function decode(buffer: Buffer): Buffer; + +/** + * Encodes a buffer to base64. + * + * @param buffer - The buffer to encode. + * @returns The base64-encoded buffer. + */ +export function encode(buffer: Buffer): Buffer; + +/** + * Base64url (RFC 4648) encodes a value. + * + * @param value - The string or buffer to encode. + * @param encoding - The encoding of `value` when a string. Defaults to `'binary'`. + * @returns The base64url-encoded string. + */ +export function base64urlEncode(value: string | Buffer, encoding?: BufferEncoding): string; + +/** + * Base64url (RFC 4648) decodes a string into a buffer. + * + * @param value - The base64url-encoded string. + * @param encoding - `'buffer'` to return a buffer. + * @returns The decoded buffer. + */ +export function base64urlDecode(value: string, encoding: 'buffer'): Buffer; + +/** + * Base64url (RFC 4648) decodes a string. + * + * @param value - The base64url-encoded string. + * @param encoding - The output encoding. Defaults to `'binary'`. + * @returns The decoded string. + */ +export function base64urlDecode(value: string, encoding?: BufferEncoding): string; + +/** A streaming base64 decoder (a `stream.Transform`). */ +export class Decoder extends Transform {} + +/** A streaming base64 encoder (a `stream.Transform`). */ +export class Encoder extends Transform {} diff --git a/src/index.js b/src/index.js new file mode 100755 index 0000000..b959c2f --- /dev/null +++ b/src/index.js @@ -0,0 +1,27 @@ +import * as Hoek from '@hapi/hoek'; + +export { decode, Decoder } from './decoder.js'; +export { encode, Encoder } from './encoder.js'; + +// Base64url (RFC 4648) encode + +export function base64urlEncode(value, encoding) { + Hoek.assert(typeof value === 'string' || Buffer.isBuffer(value), 'value must be string or buffer'); + const buf = Buffer.isBuffer(value) ? value : Buffer.from(value, encoding || 'binary'); + return buf.toString('base64url'); +} + +// Base64url (RFC 4648) decode + +export function base64urlDecode(value, encoding) { + if (typeof value !== 'string') { + throw new Error('Value not a string'); + } + + if (!/^[\w-]*$/.test(value)) { + throw new Error('Invalid character'); + } + + const buf = Buffer.from(value, 'base64url'); + return encoding === 'buffer' ? buf : buf.toString(encoding || 'binary'); +} diff --git a/test/esm.js b/test/esm.js deleted file mode 100644 index 5f4e0c4..0000000 --- a/test/esm.js +++ /dev/null @@ -1,32 +0,0 @@ -'use strict'; - -const Code = require('@hapi/code'); -const Lab = require('@hapi/lab'); - - -const { before, describe, it } = exports.lab = Lab.script(); -const expect = Code.expect; - - -describe('import()', () => { - - let B64; - - before(async () => { - - B64 = await import('../lib/index.js'); - }); - - it('exposes all methods and classes as named imports', () => { - - expect(Object.keys(B64)).to.equal([ - 'Decoder', - 'Encoder', - 'base64urlDecode', - 'base64urlEncode', - 'decode', - 'default', - 'encode' - ]); - }); -}); diff --git a/test/index.js b/test/index.js index 00ac810..38d46aa 100755 --- a/test/index.js +++ b/test/index.js @@ -1,100 +1,76 @@ -'use strict'; +import Crypto from 'crypto'; +import Fs from 'fs'; +import Path from 'path'; +import Stream from 'stream'; +import Util from 'util'; -const Crypto = require('crypto'); -const Fs = require('fs'); -const Path = require('path'); -const Stream = require('stream'); -const Util = require('util'); - -const B64 = require('..'); -const Code = require('@hapi/code'); -const Lab = require('@hapi/lab'); -const Wreck = require('@hapi/wreck'); +import * as Wreck from '@hapi/wreck'; +import { describe, expect, it } from 'vitest'; +import * as B64 from '../src/index.js'; const internals = {}; - -const { describe, it } = exports.lab = Lab.script(); -const expect = Code.expect; - - it('pipes buffer through encoder and decoder', async () => { - const buffer = Crypto.randomBytes(1024); const payload = await internals.test(buffer); - expect(payload).to.equal(buffer.toString()); + expect(payload).toEqual(buffer.toString()); }); - describe('decode()', () => { - it('decodes a short buffer (1)', () => { - const value = '0'; const encoded = B64.encode(Buffer.from(value)); - expect(B64.decode(encoded).toString()).to.equal(value); + expect(B64.decode(encoded).toString()).toEqual(value); }); it('decodes an incomplete buffer', () => { - const value = ''; const encoded = Buffer.from('A'); - expect(B64.decode(encoded).toString()).to.equal(value); + expect(B64.decode(encoded).toString()).toEqual(value); }); it('decodes an whitespace buffer', () => { - const value = ''; const encoded = Buffer.from(' '); - expect(B64.decode(encoded).toString()).to.equal(value); + expect(B64.decode(encoded).toString()).toEqual(value); }); it('decodes a buffer with whitespace', () => { - const value = '0123456789'; const encoded = Buffer.from('M D\nEy\tMz\r\nQ1Nj\rc4\r\nO Q =='); - expect(B64.decode(encoded).toString()).to.equal(value); + expect(B64.decode(encoded).toString()).toEqual(value); }); it('decodes a buffer with 4th invalid character', () => { - const value = '01'; const encoded = Buffer.from('MDE$'); - expect(B64.decode(encoded).toString()).to.equal(value); + expect(B64.decode(encoded).toString()).toEqual(value); }); }); - describe('Encoder', () => { - it('process remainder', async () => { - const buffer = [Crypto.randomBytes(5), Crypto.randomBytes(5), Crypto.randomBytes(5), Crypto.randomBytes(5)]; const payload = await internals.test(buffer); - expect(payload).to.equal(Buffer.concat(buffer).toString()); + expect(payload).toEqual(Buffer.concat(buffer).toString()); }); it('flushes remainder', async () => { - const buffer = [Crypto.randomBytes(5), Crypto.randomBytes(5), Crypto.randomBytes(5), Crypto.randomBytes(1)]; const payload = await internals.test(buffer); - expect(payload).to.equal(Buffer.concat(buffer).toString()); + expect(payload).toEqual(Buffer.concat(buffer).toString()); }); it('skips empty remainder', async () => { - const buffer = [Crypto.randomBytes(5), Crypto.randomBytes(5), Crypto.randomBytes(5), Crypto.randomBytes(3)]; const payload = await internals.test(buffer); - expect(payload).to.equal(Buffer.concat(buffer).toString()); + expect(payload).toEqual(Buffer.concat(buffer).toString()); }); }); - describe('Decoder', () => { - it('process remainder', async () => { - const value = Crypto.randomBytes(100); const encoded = B64.encode(value); @@ -102,103 +78,89 @@ describe('Decoder', () => { const source = stream.pipe(new B64.Decoder()); const payload = await Wreck.read(source); - expect(payload.toString()).to.equal(value.toString()); + expect(payload.toString()).toEqual(value.toString()); }); it('flushes remainder', async () => { - const value = '0123456789'; - const encoded = B64.encode(Buffer.from(value)); // MDEyMzQ1Njc4OQ== + const encoded = B64.encode(Buffer.from(value)); // MDEyMzQ1Njc4OQ== const stream = new internals.Payload([encoded.slice(0, 14)]); const source = stream.pipe(new B64.Decoder()); const payload = await Wreck.read(source); - expect(payload.toString()).to.equal(value.toString()); + expect(payload.toString()).toEqual(value.toString()); }); }); describe('Base64Url', () => { - - const base64str = 'AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0-P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn-AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq-wsbKztLW2t7i5uru8vb6_wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t_g4eLj5OXm5-jp6uvs7e7v8PHy8_T19vf4-fr7_P3-_w'; - const str = unescape('%00%01%02%03%04%05%06%07%08%09%0A%0B%0C%0D%0E%0F%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%20%21%22%23%24%25%26%27%28%29*+%2C-./0123456789%3A%3B%3C%3D%3E%3F@ABCDEFGHIJKLMNOPQRSTUVWXYZ%5B%5C%5D%5E_%60abcdefghijklmnopqrstuvwxyz%7B%7C%7D%7E%7F%80%81%82%83%84%85%86%87%88%89%8A%8B%8C%8D%8E%8F%90%91%92%93%94%95%96%97%98%99%9A%9B%9C%9D%9E%9F%A0%A1%A2%A3%A4%A5%A6%A7%A8%A9%AA%AB%AC%AD%AE%AF%B0%B1%B2%B3%B4%B5%B6%B7%B8%B9%BA%BB%BC%BD%BE%BF%C0%C1%C2%C3%C4%C5%C6%C7%C8%C9%CA%CB%CC%CD%CE%CF%D0%D1%D2%D3%D4%D5%D6%D7%D8%D9%DA%DB%DC%DD%DE%DF%E0%E1%E2%E3%E4%E5%E6%E7%E8%E9%EA%EB%EC%ED%EE%EF%F0%F1%F2%F3%F4%F5%F6%F7%F8%F9%FA%FB%FC%FD%FE%FF'); + const base64str = + 'AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0-P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn-AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq-wsbKztLW2t7i5uru8vb6_wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t_g4eLj5OXm5-jp6uvs7e7v8PHy8_T19vf4-fr7_P3-_w'; + const str = unescape( + '%00%01%02%03%04%05%06%07%08%09%0A%0B%0C%0D%0E%0F%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%20%21%22%23%24%25%26%27%28%29*+%2C-./0123456789%3A%3B%3C%3D%3E%3F@ABCDEFGHIJKLMNOPQRSTUVWXYZ%5B%5C%5D%5E_%60abcdefghijklmnopqrstuvwxyz%7B%7C%7D%7E%7F%80%81%82%83%84%85%86%87%88%89%8A%8B%8C%8D%8E%8F%90%91%92%93%94%95%96%97%98%99%9A%9B%9C%9D%9E%9F%A0%A1%A2%A3%A4%A5%A6%A7%A8%A9%AA%AB%AC%AD%AE%AF%B0%B1%B2%B3%B4%B5%B6%B7%B8%B9%BA%BB%BC%BD%BE%BF%C0%C1%C2%C3%C4%C5%C6%C7%C8%C9%CA%CB%CC%CD%CE%CF%D0%D1%D2%D3%D4%D5%D6%D7%D8%D9%DA%DB%DC%DD%DE%DF%E0%E1%E2%E3%E4%E5%E6%E7%E8%E9%EA%EB%EC%ED%EE%EF%F0%F1%F2%F3%F4%F5%F6%F7%F8%F9%FA%FB%FC%FD%FE%FF', + ); describe('base64urlEncode()', () => { - it('should assert function input is a string or buffer', () => { - const number = 1024; const func = () => { - return B64.base64urlEncode(number); }; - expect(func).throws(Error); + expect(func).toThrow(Error); }); it('should base64 URL-safe a string', () => { - - expect(B64.base64urlEncode(str)).to.equal(base64str); + expect(B64.base64urlEncode(str)).toEqual(base64str); }); it('encodes a buffer', () => { - - expect(B64.base64urlEncode(Buffer.from(str, 'binary'))).to.equal(base64str); + expect(B64.base64urlEncode(Buffer.from(str, 'binary'))).toEqual(base64str); }); it('should base64 URL-safe a hex string', () => { - const buffer = Buffer.from(str, 'binary'); - expect(B64.base64urlEncode(buffer.toString('hex'), 'hex')).to.equal(base64str); + expect(B64.base64urlEncode(buffer.toString('hex'), 'hex')).toEqual(base64str); }); it('works on larger input strings', () => { - - const input = Fs.readFileSync(Path.join(__dirname, 'index.js')).toString(); + const input = Fs.readFileSync(Path.join(import.meta.dirname, 'index.js')).toString(); const encoded = B64.base64urlEncode(input); - expect(encoded).to.not.contain('+'); - expect(encoded).to.not.contain('/'); + expect(encoded).not.toContain('+'); + expect(encoded).not.toContain('/'); const decoded = B64.base64urlDecode(encoded); - expect(decoded).to.equal(input); + expect(decoded).toEqual(input); }); }); describe('base64urlDecode()', () => { - it('should un-base64 URL-safe a string', () => { - - expect(B64.base64urlDecode(base64str)).to.equal(str); + expect(B64.base64urlDecode(base64str)).toEqual(str); }); it('should un-base64 URL-safe a string into hex', () => { - - expect(B64.base64urlDecode(base64str, 'hex')).to.equal(Buffer.from(str, 'binary').toString('hex')); + expect(B64.base64urlDecode(base64str, 'hex')).toEqual(Buffer.from(str, 'binary').toString('hex')); }); it('should un-base64 URL-safe a string and return a buffer', () => { - const buf = B64.base64urlDecode(base64str, 'buffer'); - expect(buf instanceof Buffer).to.equal(true); - expect(buf.toString('binary')).to.equal(str); + expect(buf instanceof Buffer).toEqual(true); + expect(buf.toString('binary')).toEqual(str); }); it('throws error on invalid input', () => { - - expect(() => B64.base64urlDecode(1024)).to.throw('Value not a string'); + expect(() => B64.base64urlDecode(1024)).toThrow('Value not a string'); }); it('throws error on invalid input', () => { - - expect(() => B64.base64urlDecode('*')).to.throw('Invalid character'); + expect(() => B64.base64urlDecode('*')).toThrow('Invalid character'); }); }); }); - internals.Payload = function (payload) { - Stream.Readable.call(this); this._data = [].concat(payload); @@ -207,21 +169,16 @@ internals.Payload = function (payload) { Util.inherits(internals.Payload, Stream.Readable); - internals.Payload.prototype._read = function (size) { - const chunk = this._data[this._position++]; if (chunk) { this.push(chunk); - } - else { + } else { this.push(null); } }; - internals.test = async function (buffer) { - const stream = new internals.Payload(buffer); const source = stream.pipe(new B64.Encoder()).pipe(new B64.Decoder()); diff --git a/test/typings.ts b/test/typings.ts new file mode 100644 index 0000000..82fa37b --- /dev/null +++ b/test/typings.ts @@ -0,0 +1,79 @@ +import { Transform } from 'node:stream'; + +import { describe, expectTypeOf, it } from 'vitest'; + +import * as B64 from '../src/index.js'; + +describe('typings', () => { + describe('encode', () => { + it('returns a buffer', () => { + expectTypeOf(B64.encode(Buffer.from('a'))).toEqualTypeOf(); + }); + + it('invalid calls rejected', () => { + // @ts-expect-error input must be a buffer + B64.encode('a'); + }); + }); + + describe('decode', () => { + it('returns a buffer', () => { + expectTypeOf(B64.decode(Buffer.from('YQ=='))).toEqualTypeOf(); + }); + + it('invalid calls rejected', () => { + // @ts-expect-error input must be a buffer + B64.decode('YQ=='); + }); + }); + + describe('base64urlEncode', () => { + it('accepts a string or a buffer', () => { + expectTypeOf(B64.base64urlEncode('a')).toEqualTypeOf(); + expectTypeOf(B64.base64urlEncode('61', 'hex')).toEqualTypeOf(); + expectTypeOf(B64.base64urlEncode(Buffer.from('a'))).toEqualTypeOf(); + }); + + it('invalid calls rejected', () => { + try { + // @ts-expect-error value must be a string or a buffer + B64.base64urlEncode(1); + } catch {} + + try { + // @ts-expect-error encoding must be a buffer encoding + B64.base64urlEncode('a', 'nope'); + } catch {} + }); + }); + + describe('base64urlDecode', () => { + it('returns a string by default', () => { + expectTypeOf(B64.base64urlDecode('YQ')).toEqualTypeOf(); + expectTypeOf(B64.base64urlDecode('YQ', 'hex')).toEqualTypeOf(); + }); + + it('returns a buffer with the buffer encoding', () => { + expectTypeOf(B64.base64urlDecode('YQ', 'buffer')).toEqualTypeOf(); + }); + + it('invalid calls rejected', () => { + try { + // @ts-expect-error value must be a string + B64.base64urlDecode(Buffer.from('YQ')); + } catch {} + }); + }); + + describe('Encoder', () => { + it('is a transform stream', () => { + expectTypeOf(new B64.Encoder()).toExtend(); + }); + }); + + describe('Decoder', () => { + it('is a transform stream', () => { + expectTypeOf(new B64.Decoder()).toExtend(); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..849762f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "exactOptionalPropertyTypes": true, + "forceConsistentCasingInFileNames": true, + "inlineSources": true, + "isolatedDeclarations": true, + "isolatedModules": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipDefaultLibCheck": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "ESNext" + } +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..20fc611 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,24 @@ +import Oxc from '@hapi/oxc-plugin/vitest'; +import { defineConfig } from 'vitest/config'; + +import type { ViteUserConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [Oxc()], + test: { + environment: 'node', + include: ['test/**/*.{js,ts}'], + typecheck: { + enabled: true, + include: ['test/**/*.{js,ts}'], + }, + coverage: { + provider: 'v8', + include: ['src/**'], + exclude: ['**/*.d.ts'], + thresholds: { + 100: true, + }, + }, + }, +}) as ViteUserConfig;