diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..bf97c5e --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,12 @@ +{ + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint", "prettier"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended" + ], + "rules": { + "prettier/prettier": "error" + } +} diff --git a/.gitignore b/.gitignore index d5f19d8..87807d9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules package-lock.json +dist diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..320e24e --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": false, + "semi": false, + "tabWidth": 4 + } + \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2c63c08 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,2 @@ +{ +} diff --git a/bin/base32 b/bin/base32 new file mode 100755 index 0000000..0f0c490 --- /dev/null +++ b/bin/base32 @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +require("../dist/node/cli") diff --git a/bower.json b/bower.json deleted file mode 100644 index 352c477..0000000 --- a/bower.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "base32", - "version": "0.0.6", - "homepage": "https://github.com/agnoster/base32-js", - "authors": [ - "i@agnoster.net" - ], - "description": "Base32 encoding for JavaScript, based (loosely) on Crockford's Base32", - "main": "dist/base32.min.js", - "moduleType": [ - "globals", - "node" - ], - "keywords": [ - "base32" - ], - "license": "MIT", - "ignore": [ - "**/.*", - "node_modules", - "bower_components", - "test", - "tests" - ] -} diff --git a/dist/base32.js b/dist/base32.js deleted file mode 100644 index 45bf174..0000000 --- a/dist/base32.js +++ /dev/null @@ -1,251 +0,0 @@ -;(function(){ - -// This would be the place to edit if you want a different -// Base32 implementation - -var alphabet = '0123456789abcdefghjkmnpqrtuvwxyz' -var alias = { o:0, i:1, l:1, s:5 } - -/** - * Build a lookup table and memoize it - * - * Return an object that maps a character to its - * byte value. - */ - -var lookup = function() { - var table = {} - // Invert 'alphabet' - for (var i = 0; i < alphabet.length; i++) { - table[alphabet[i]] = i - } - // Splice in 'alias' - for (var key in alias) { - if (!alias.hasOwnProperty(key)) continue - table[key] = table['' + alias[key]] - } - lookup = function() { return table } - return table -} - -/** - * A streaming encoder - * - * var encoder = new base32.Encoder() - * var output1 = encoder.update(input1) - * var output2 = encoder.update(input2) - * var lastoutput = encode.update(lastinput, true) - */ - -function Encoder() { - var skip = 0 // how many bits we will skip from the first byte - var bits = 0 // 5 high bits, carry from one byte to the next - - this.output = '' - - // Read one byte of input - // Should not really be used except by "update" - this.readByte = function(byte) { - // coerce the byte to an int - if (typeof byte == 'string') byte = byte.charCodeAt(0) - - if (skip < 0) { // we have a carry from the previous byte - bits |= (byte >> (-skip)) - } else { // no carry - bits = (byte << skip) & 248 - } - - if (skip > 3) { - // not enough data to produce a character, get us another one - skip -= 8 - return 1 - } - - if (skip < 4) { - // produce a character - this.output += alphabet[bits >> 3] - skip += 5 - } - - return 0 - } - - // Flush any remaining bits left in the stream - this.finish = function(check) { - var output = this.output + (skip < 0 ? alphabet[bits >> 3] : '') + (check ? '$' : '') - this.output = '' - return output - } -} - -/** - * Process additional input - * - * input: string of bytes to convert - * flush: boolean, should we flush any trailing bits left - * in the stream - * returns: a string of characters representing 'input' in base32 - */ - -Encoder.prototype.update = function(input, flush) { - for (var i = 0; i < input.length; ) { - i += this.readByte(input[i]) - } - // consume all output - var output = this.output - this.output = '' - if (flush) { - output += this.finish() - } - return output -} - -// Functions analogously to Encoder - -function Decoder() { - var skip = 0 // how many bits we have from the previous character - var byte = 0 // current byte we're producing - - this.output = '' - - // Consume a character from the stream, store - // the output in this.output. As before, better - // to use update(). - this.readChar = function(char) { - if (typeof char != 'string'){ - if (typeof char == 'number') { - char = String.fromCharCode(char) - } - } - char = char.toLowerCase() - var val = lookup()[char] - if (typeof val == 'undefined') { - // character does not exist in our lookup table - return // skip silently. An alternative would be: - // throw Error('Could not find character "' + char + '" in lookup table.') - } - val <<= 3 // move to the high bits - byte |= val >>> skip - skip += 5 - if (skip >= 8) { - // we have enough to preduce output - this.output += String.fromCharCode(byte) - skip -= 8 - if (skip > 0) byte = (val << (5 - skip)) & 255 - else byte = 0 - } - - } - - this.finish = function(check) { - var output = this.output + (skip < 0 ? alphabet[bits >> 3] : '') + (check ? '$' : '') - this.output = '' - return output - } -} - -Decoder.prototype.update = function(input, flush) { - for (var i = 0; i < input.length; i++) { - this.readChar(input[i]) - } - var output = this.output - this.output = '' - if (flush) { - output += this.finish() - } - return output -} - -/** Convenience functions - * - * These are the ones to use if you just have a string and - * want to convert it without dealing with streams and whatnot. - */ - -// String of data goes in, Base32-encoded string comes out. -function encode(input) { - var encoder = new Encoder() - var output = encoder.update(input, true) - return output -} - -// Base32-encoded string goes in, decoded data comes out. -function decode(input) { - var decoder = new Decoder() - var output = decoder.update(input, true) - return output -} - -/** - * sha1 functions wrap the hash function from Node.js - * - * Several ways to use this: - * - * var hash = base32.sha1('Hello World') - * base32.sha1(process.stdin, function (err, data) { - * if (err) return console.log("Something went wrong: " + err.message) - * console.log("Your SHA1: " + data) - * } - * base32.sha1.file('/my/file/path', console.log) - */ - -var crypto, fs -function sha1(input, cb) { - if (typeof crypto == 'undefined') crypto = require('crypto') - var hash = crypto.createHash('sha1') - hash.digest = (function(digest) { - return function() { - return encode(digest.call(this, 'binary')) - } - })(hash.digest) - if (cb) { // streaming - if (typeof input == 'string' || Buffer.isBuffer(input)) { - try { - return cb(null, sha1(input)) - } catch (err) { - return cb(err, null) - } - } - if (!typeof input.on == 'function') return cb({ message: "Not a stream!" }) - input.on('data', function(chunk) { hash.update(chunk) }) - input.on('end', function() { cb(null, hash.digest()) }) - return - } - - // non-streaming - if (input) { - return hash.update(input).digest() - } - return hash -} -sha1.file = function(filename, cb) { - if (filename == '-') { - process.stdin.resume() - return sha1(process.stdin, cb) - } - if (typeof fs == 'undefined') fs = require('fs') - return fs.stat(filename, function(err, stats) { - if (err) return cb(err, null) - if (stats.isDirectory()) return cb({ dir: true, message: "Is a directory" }) - return sha1(require('fs').createReadStream(filename), cb) - }) -} - -var base32 = { - Decoder: Decoder, - Encoder: Encoder, - encode: encode, - decode: decode, - sha1: sha1 -} - -if (typeof window !== 'undefined') { - // we're in a browser - OMG! - window.base32 = base32 -} - -if (typeof module !== 'undefined' && module.exports) { - // nodejs/browserify - module.exports = base32 -} -})(); diff --git a/dist/base32.min.js b/dist/base32.min.js deleted file mode 100644 index ff406aa..0000000 --- a/dist/base32.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(){function t(){var t=0,e=0;this.output="",this.readByte=function(r){return"string"==typeof r&&(r=r.charCodeAt(0)),0>t?e|=r>>-t:e=r<3?(t-=8,1):(4>t&&(this.output+=i[e>>3],t+=5),0)},this.finish=function(r){var n=this.output+(0>t?i[e>>3]:"")+(r?"$":"");return this.output="",n}}function e(){var t=0,e=0;this.output="",this.readChar=function(r){"string"!=typeof r&&"number"==typeof r&&(r=String.fromCharCode(r)),r=r.toLowerCase();var n=s()[r];"undefined"!=typeof n&&(n<<=3,e|=n>>>t,t+=5,t>=8&&(this.output+=String.fromCharCode(e),t-=8,e=t>0?n<<5-t&255:0))},this.finish=function(e){var r=this.output+(0>t?i[bits>>3]:"")+(e?"$":"");return this.output="",r}}function r(e){var r=new t,n=r.update(e,!0);return n}function n(t){var r=new e,n=r.update(t,!0);return n}function u(t,e){"undefined"==typeof f&&(f=require("crypto"));var n=f.createHash("sha1");if(n.digest=function(t){return function(){return r(t.call(this,"binary"))}}(n.digest),e){if("string"==typeof t||Buffer.isBuffer(t))try{return e(null,u(t))}catch(i){return e(i,null)}return t.on("data",function(t){n.update(t)}),void t.on("end",function(){e(null,n.digest())})}return t?n.update(t).digest():n}var i="0123456789abcdefghjkmnpqrtuvwxyz",o={o:0,i:1,l:1,s:5},s=function(){for(var t={},e=0;e> (-skip)) - } else { // no carry - bits = (byte << skip) & 248 - } - - if (skip > 3) { - // not enough data to produce a character, get us another one - skip -= 8 - return 1 - } - - if (skip < 4) { - // produce a character - this.output += alphabet[bits >> 3] - skip += 5 - } - - return 0 - } - - // Flush any remaining bits left in the stream - this.finish = function(check) { - var output = this.output + (skip < 0 ? alphabet[bits >> 3] : '') + (check ? '$' : '') - this.output = '' - return output - } -} - -/** - * Process additional input - * - * input: string of bytes to convert - * flush: boolean, should we flush any trailing bits left - * in the stream - * returns: a string of characters representing 'input' in base32 - */ - -Encoder.prototype.update = function(input, flush) { - for (var i = 0; i < input.length; ) { - i += this.readByte(input[i]) - } - // consume all output - var output = this.output - this.output = '' - if (flush) { - output += this.finish() - } - return output -} - -// Functions analogously to Encoder - -function Decoder() { - var skip = 0 // how many bits we have from the previous character - var byte = 0 // current byte we're producing - - this.output = '' - - // Consume a character from the stream, store - // the output in this.output. As before, better - // to use update(). - this.readChar = function(char) { - if (typeof char != 'string'){ - if (typeof char == 'number') { - char = String.fromCharCode(char) - } - } - char = char.toLowerCase() - var val = lookup()[char] - if (typeof val == 'undefined') { - // character does not exist in our lookup table - return // skip silently. An alternative would be: - // throw Error('Could not find character "' + char + '" in lookup table.') - } - val <<= 3 // move to the high bits - byte |= val >>> skip - skip += 5 - if (skip >= 8) { - // we have enough to preduce output - this.output += String.fromCharCode(byte) - skip -= 8 - if (skip > 0) byte = (val << (5 - skip)) & 255 - else byte = 0 - } - - } - - this.finish = function(check) { - var output = this.output + (skip < 0 ? alphabet[bits >> 3] : '') + (check ? '$' : '') - this.output = '' - return output - } -} - -Decoder.prototype.update = function(input, flush) { - for (var i = 0; i < input.length; i++) { - this.readChar(input[i]) - } - var output = this.output - this.output = '' - if (flush) { - output += this.finish() - } - return output -} - -/** Convenience functions - * - * These are the ones to use if you just have a string and - * want to convert it without dealing with streams and whatnot. - */ - -// String of data goes in, Base32-encoded string comes out. -function encode(input) { - var encoder = new Encoder() - var output = encoder.update(input, true) - return output -} - -// Base32-encoded string goes in, decoded data comes out. -function decode(input) { - var decoder = new Decoder() - var output = decoder.update(input, true) - return output -} - -/** - * sha1 functions wrap the hash function from Node.js - * - * Several ways to use this: - * - * var hash = base32.sha1('Hello World') - * base32.sha1(process.stdin, function (err, data) { - * if (err) return console.log("Something went wrong: " + err.message) - * console.log("Your SHA1: " + data) - * } - * base32.sha1.file('/my/file/path', console.log) - */ - -var crypto, fs -function sha1(input, cb) { - if (typeof crypto == 'undefined') crypto = require('crypto') - var hash = crypto.createHash('sha1') - hash.digest = (function(digest) { - return function() { - return encode(digest.call(this, 'binary')) - } - })(hash.digest) - if (cb) { // streaming - if (typeof input == 'string' || Buffer.isBuffer(input)) { - try { - return cb(null, sha1(input)) - } catch (err) { - return cb(err, null) - } - } - if (!typeof input.on == 'function') return cb({ message: "Not a stream!" }) - input.on('data', function(chunk) { hash.update(chunk) }) - input.on('end', function() { cb(null, hash.digest()) }) - return - } - - // non-streaming - if (input) { - return hash.update(input).digest() - } - return hash -} -sha1.file = function(filename, cb) { - if (filename == '-') { - process.stdin.resume() - return sha1(process.stdin, cb) - } - if (typeof fs == 'undefined') fs = require('fs') - return fs.stat(filename, function(err, stats) { - if (err) return cb(err, null) - if (stats.isDirectory()) return cb({ dir: true, message: "Is a directory" }) - return sha1(require('fs').createReadStream(filename), cb) - }) -} - -var base32 = { - Decoder: Decoder, - Encoder: Encoder, - encode: encode, - decode: decode, - sha1: sha1 -} - -if (typeof window !== 'undefined') { - // we're in a browser - OMG! - window.base32 = base32 -} - -if (typeof module !== 'undefined' && module.exports) { - // nodejs/browserify - module.exports = base32 -} -})(); diff --git a/package.json b/package.json index 3fbb00b..e3a206f 100644 --- a/package.json +++ b/package.json @@ -1,30 +1,47 @@ { - "name": "base32", - "description": "Base32 encoding and decoding", - "version": "0.0.7", - "author": "Isaac Wolkerstorfer (http://agnoster.net/)", - "homepage": "https://github.com/agnoster/base32-js", - "repository": { - "type": "git", - "url": "git://github.com/agnoster/base32-js.git" - }, - "main": "./lib/base32", - "bin": "./bin/base32.js", - "scripts": { - "test": "./node_modules/.bin/coffee test/*-test.coffee", - "bin": "$npm_package_bin_base32" - }, - "engines": { - "node": ">0.10" - }, - "dependencies": { - "minimist": "^1.2.6" - }, - "devDependencies": { - "coffee-script": ">=1.0.0", - "gulp": "^3.9.1", - "gulp-rename": "^1.2.0", - "gulp-uglify": "^3.0.2", - "vows": ">=0.5.6" - } -} \ No newline at end of file + "name": "base32", + "description": "Base32 encoding and decoding", + "version": "1.0.0", + "author": "Isaac Wolkerstorfer (http://agnoster.net/)", + "homepage": "https://github.com/agnoster/base32-js", + "repository": { + "type": "git", + "url": "git://github.com/agnoster/base32-js.git" + }, + "main": "./dist/index.js", + "bin": { + "base32": "./bin/base32" + }, + "scripts": { + "test": "jest", + "lint": "eslint . --ext .ts", + "build": "tsc", + "check": "tsc --noEmit", + "format": "prettier --write \"src/**/*.ts\"", + "bin": "$npm_package_bin_base32", + "prepublishOnly": "npm run build" + }, + "files": [ + "dist", + "bin" + ], + "devDependencies": { + "@types/jest": "^29.5.0", + "@typescript-eslint/eslint-plugin": "^5.58.0", + "@typescript-eslint/parser": "^5.58.0", + "eslint": "^8.38.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-prettier": "^4.2.1", + "prettier": "^2.8.7", + "jest": "^29.1.0", + "ts-jest": "^29.1.0", + "typescript": "^5.0.4" + }, + "jest": { + "preset": "ts-jest", + "testEnvironment": "node" + }, + "dependencies": { + "yargs": "^17.7.1" + } +} diff --git a/src/alphacodec.ts b/src/alphacodec.ts new file mode 100644 index 0000000..c47fae2 --- /dev/null +++ b/src/alphacodec.ts @@ -0,0 +1,46 @@ +/** + * Defines an encoding of bytes to chars and back that uses an "alphabet", + * mapping each valid character of the encoding to a single binary sub-byte + * value. + * + * Uniquely defined by: + * - alphabet: the primary alphabet, a string of characters in order. The length + * must be a power of 2 + * - alias: additional characters that can be read as aliases for another + * character in the alphabet - for example, in a case-insensitive encoding, + * 'a' could be an alias for 'A'. + * + * Two encodings with the same alphabet and aliases are identical. + */ +export default class AlphaCodec { + #characterValue: Record = {} + #alphabet: string + #case_insensitive = true + + normalize(str: string): string { + if (this.#case_insensitive) { + return str.toLowerCase() + } + return str + } + + constructor(alphabet: string, aliases: Record = {}) { + this.#alphabet = alphabet + + for (let i = 0; i < this.#alphabet.length; i++) { + this.#characterValue[this.normalize(alphabet[i])] = i + } + + for (const char in aliases) { + this.#characterValue[char] = this.characterValue(aliases[char]) + } + } + + characterValue(char: string): number { + return this.#characterValue[this.normalize(char)] + } + + characterForValue(value: number): string { + return this.#alphabet[value] + } +} diff --git a/src/decoder.ts b/src/decoder.ts new file mode 100644 index 0000000..5af4c17 --- /dev/null +++ b/src/decoder.ts @@ -0,0 +1,69 @@ +import AlphaCodec from "./alphacodec" +import { base32_js } from "./encoding" + +type DecoderOutput = T extends null ? Buffer : string + +export default class Decoder< + T extends BufferEncoding | undefined | null = undefined +> { + #skip = 0 // how many bits we have from the previous character + #byte = 0 // current byte we're producing + private output = Buffer.alloc(0) + public readonly buffer_encoding: T + + constructor( + public readonly base32_encoding: AlphaCodec = base32_js, + buffer_encoding?: T + ) { + this.buffer_encoding = + buffer_encoding === undefined ? ("utf-8" as T) : buffer_encoding + } + + update(input: string | Buffer, flush?: boolean): DecoderOutput { + for (let i = 0; i < input.length; i++) { + this.readChar(input[i]) + } + let output = this.output + this.output = Buffer.alloc(0) + if (flush) { + output = Buffer.concat([output, this.finish()]) + } + return this._value(output) + } + + finish(): Buffer { + return this.output + } + + private _value(data: Buffer): DecoderOutput { + return ( + this.buffer_encoding ? data.toString(this.buffer_encoding) : data + ) as DecoderOutput + } + + private readChar(char: string | number) { + if (typeof char !== "string") { + if (typeof char === "number") { + char = String.fromCharCode(char) + } + } + char = char.toLowerCase() + let val = this.base32_encoding.characterValue(char) + if (typeof val === "undefined") { + throw Error(`Could not find character "${char}" in lookup table.`) + } + val <<= 3 // move to the high bits + this.#byte |= val >>> this.#skip + this.#skip += 5 + if (this.#skip >= 8) { + // we have enough to produce output + this.output = Buffer.concat([ + this.output, + Buffer.from([this.#byte]), + ]) + this.#skip -= 8 + if (this.#skip > 0) this.#byte = (val << (5 - this.#skip)) & 255 + else this.#byte = 0 + } + } +} diff --git a/src/encoder.ts b/src/encoder.ts new file mode 100644 index 0000000..dde10f6 --- /dev/null +++ b/src/encoder.ts @@ -0,0 +1,68 @@ +import AlphaCodec from "./alphacodec" +import { base32_js } from "./encoding" + +export default class Encoder { + #skip = 0 // how many bits we will skip from the first byte + #bits = 0 // 5 high bits, carry from one byte to the next + private output = "" + + constructor(public readonly base32_encoding: AlphaCodec = base32_js) {} + + update(input: string | Buffer, flush?: boolean): string { + if (typeof input === "string") { + input = Buffer.from(input) + } + for (let i = 0; i < input.length; ) { + i += this.readByte(input[i]) + } + // consume all output + let output = this.output + this.output = "" + if (flush) { + output += this.finish() + } + return output + } + + finish(check?: boolean): string { + const output = + this.output + + (this.#skip < 0 + ? this.base32_encoding.characterForValue(this.#bits >> 3) + : "") + + (check ? "$" : "") + this.output = "" + return output + } + + // Read one byte of input + // Should not really be used except by "update" + private readByte(byte: string | number) { + // coerce the byte to an int + if (typeof byte === "string") byte = byte.charCodeAt(0) + + if (this.#skip < 0) { + // we have a carry from the previous byte + this.#bits |= byte >> -this.#skip + } else { + // no carry + this.#bits = (byte << this.#skip) & 248 + } + + if (this.#skip > 3) { + // not enough data to produce a character, get us another one + this.#skip -= 8 + return 1 + } + + if (this.#skip < 4) { + // produce a character + this.output += this.base32_encoding.characterForValue( + this.#bits >> 3 + ) + this.#skip += 5 + } + + return 0 + } +} diff --git a/src/encoding.ts b/src/encoding.ts new file mode 100644 index 0000000..c0b8fd3 --- /dev/null +++ b/src/encoding.ts @@ -0,0 +1,29 @@ +import AlphaCodec from "./alphacodec" + +export const base32_js = new AlphaCodec("0123456789abcdefghjkmnpqrtuvwxyz", { + o: "0", + i: "1", + l: "1", + s: "5", +}) + +export const crockford = new AlphaCodec("0123456789ABCDEFGHJKMNPQRSTVWXYZ", { + I: "1", + L: "1", + O: "0", + U: "V", +}) + +export const base32hex = new AlphaCodec("0123456789ABCDEFGHIJKLMNOPQRSTUV") + +export const rfc4648 = new AlphaCodec("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", { + 0: "O", + 1: "I", + 8: "B", +}) + +export const base32 = rfc4648 + +export const zbase32 = new AlphaCodec("ybndrfg8ejkmcpqxot1uwisza345h769") + +export const geohash = new AlphaCodec("0123456789bcdefghjkmnpqrstuvwxyz") diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..fa09780 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,41 @@ +import Encoder from "./encoder" +import AlphaCodec from "./alphacodec" +import * as Encoding from "./encoding" +import Decoder from "./decoder" + +class Base32 { + constructor(public readonly encoding: AlphaCodec = Encoding.base32_js) {} + + makeEncoder(): Encoder { + return new Encoder(this.encoding) + } + + makeDecoder(): Decoder + makeDecoder(buffer_encoding: T): Decoder + makeDecoder( + buffer_encoding?: T + ) { + return new Decoder(this.encoding, buffer_encoding) + } + + encode(data: string | Buffer): string { + const encoder = this.makeEncoder() + return encoder.update(data, true) + } + + decode(data: string): string + decode(data: string, buffer_encoding: null): Buffer + decode(data: string, buffer_encoding: BufferEncoding): string + decode( + data: string, + buffer_encoding: BufferEncoding | undefined | null = "utf8" + ): Buffer | string { + if (buffer_encoding === undefined) { + buffer_encoding = "utf8" + } + const decoder = this.makeDecoder(buffer_encoding) + return decoder.update(data, true) + } +} + +export default Base32 diff --git a/src/legacy.ts b/src/legacy.ts new file mode 100644 index 0000000..c691a92 --- /dev/null +++ b/src/legacy.ts @@ -0,0 +1,38 @@ +import Encoder from "./encoder" +import AlphaCodec from "./alphacodec" +import * as Encoding from "./encoding" +import Decoder from "./decoder" +import legacy_sha1 from "./node/sha1" + +export function encode( + data: string | Buffer, + base32_encoding: AlphaCodec = Encoding.base32_js +): string { + const encoder = new Encoder(base32_encoding) + return encoder.update(data, true) +} + +export function decode(data: string, base32_encoding?: AlphaCodec): string +export function decode( + data: string, + base32_encoding: AlphaCodec, + buffer_encoding: null +): Buffer +export function decode( + data: string, + base32_encoding: AlphaCodec, + buffer_encoding: BufferEncoding +): string +export function decode( + data: string, + base32_encoding: AlphaCodec = Encoding.base32_js, + buffer_encoding: BufferEncoding | undefined | null = "utf8" +): Buffer | string { + if (buffer_encoding === undefined) { + buffer_encoding = "utf8" + } + const decoder = new Decoder(base32_encoding, buffer_encoding) + return decoder.update(data, true) +} + +export { Encoder, Decoder, AlphaCodec, Encoding, legacy_sha1 as sha1 } diff --git a/src/node/cli.ts b/src/node/cli.ts new file mode 100644 index 0000000..d0c0231 --- /dev/null +++ b/src/node/cli.ts @@ -0,0 +1,155 @@ +import { Readable, Writable } from "node:stream" +import yargs from "yargs" +import { hideBin } from "yargs/helpers" +import { ReadStream, WriteStream } from "node:tty" +import sha1 from "./sha1" +import * as fs from "node:fs" +import Decoder from "../decoder" +import Encoder from "../encoder" + +const argv = yargs(hideBin(process.argv)) + .usage( + "Usage: base32 [input_file] [-o output_file] [-d|--decode] [-s|--sha]" + ) + .option("o", { + type: "string", + describe: "Output file", + default: "-", + }) + .option("d", { + alias: "decode", + type: "boolean", + describe: "Decode input", + }) + .option("s", { + alias: ["sha", "sha1", "hash"], + type: "boolean", + describe: "Hash input", + }) + .option("r", { + type: "boolean", + describe: "Recursive hashing", + }) + .option("v", { + type: "boolean", + describe: "Version", + }) + .option("h", { + alias: "help", + type: "boolean", + describe: "Help", + }) + .parseSync() + +function isTTY(stream: Readable): stream is ReadStream +function isTTY(stream: Writable): stream is WriteStream +function isTTY(stream: Readable | Writable): boolean { + return "isTTY" in stream +} + +function stream( + input: Readable, + output: Writable, + processor: Encoder | Decoder +): void { + let out: Buffer | string | null + + input.on("data", (chunk: Buffer) => { + out = processor.update(chunk) + if (out) { + output.write(out) + if (isTTY(input) && isTTY(output)) output.write("\n") + } + }) + + input.on("end", () => { + out = processor.finish() + if (out) output.write(out) + if (isTTY(output)) output.write("\n") + }) +} + +function hash_file(filename: string, output: Writable): void { + sha1.file(filename, (err: unknown, hash: string | null) => { + if (err && typeof err === "object") { + if ("dir" in err) { + if (argv.r || argv.d) { + fs.readdir( + filename, + ( + err: NodeJS.ErrnoException | null, + files: string[] + ) => { + if (err) { + return process.stderr.write( + `base32: ${filename}: ${err.message}\n` + ) + } + for (const file of files) { + hash_file(`${filename}/${file}`, output) + } + } + ) + } + if (!argv.r && !argv.d && "message" in err) { + return process.stderr.write( + `base32: ${filename}: ${err?.message}\n` + ) + } + } + return + } + output.write(`${hash} ${filename}\n`) + }) +} + +// Your stream and hash_file functions should be defined here +export function runCli() { + if (argv.h) { + yargs.showHelp() + return + } + + if (argv.v) { + console.log("v0.0.2") + return + } + + let processor: Encoder | Decoder + let input: fs.ReadStream | NodeJS.ReadStream + let output: fs.WriteStream | NodeJS.WriteStream + + if (argv.d || argv.decode) { + processor = new Decoder() + } else { + processor = new Encoder() + } + + if (argv.o && argv.o !== "-") { + output = fs.createWriteStream(argv.o) + } else { + output = process.stdout + } + + if (argv._.length === 0) argv._.push("-") + + if (argv.s || argv.hash || argv.sha || argv.sha1) { + if (argv._.length === 0) argv._ = ["-"] + for (const filename of argv._) { + hash_file(`${filename}`, output) + } + return + } + + for (const filename of argv._) { + if (filename === "-") { + input = process.stdin + process.stdin.resume() + } else { + input = fs.createReadStream(`${filename}`) + } + stream(input, output, processor) + } +} + +runCli() diff --git a/src/node/sha1.ts b/src/node/sha1.ts new file mode 100644 index 0000000..693db63 --- /dev/null +++ b/src/node/sha1.ts @@ -0,0 +1,99 @@ +import * as crypto from "node:crypto" +import * as fs from "node:fs" +import { AlphaCodec, Encoding, encode } from "../legacy" + +type Callback = (err: unknown | null, result: T | null) => unknown + +type Input = string | Buffer +type InputGenerator = { + [Symbol.asyncIterator](): AsyncIterableIterator +} + +class Base32Hash { + #hash: crypto.Hash + private encoding: AlphaCodec + + constructor(hash: crypto.Hash, encoding: AlphaCodec) { + this.#hash = hash + this.encoding = encoding + } + + update(input: Input): this { + this.#hash.update(input) + return this + } + + async consume(generator: InputGenerator): Promise { + for await (const chunk of generator) { + this.#hash.update(chunk) + } + return this + } + + digest(): string { + return encode(this.#hash.digest(), this.encoding) + } + + static create( + algorithm: string, + encoding: AlphaCodec = Encoding.base32_js, + options?: crypto.HashOptions + ): Base32Hash { + return new Base32Hash(crypto.createHash(algorithm, options), encoding) + } + + async hash(input: Input | InputGenerator): Promise { + if (typeof input === "string" || Buffer.isBuffer(input)) { + this.update(input) + } else if (typeof input[Symbol.asyncIterator] === "function") { + await this.consume(input) + } else { + throw new Error("Invalid input: " + input) + } + return this.digest() + } +} + +function legacy_sha1(): Base32Hash +function legacy_sha1(input: Input): string +function legacy_sha1(input: Input | InputGenerator, cb: Callback): void +function legacy_sha1( + input?: Buffer | string | InputGenerator, + cb?: Callback +): string | Base32Hash | void { + const hash = new Base32Hash(crypto.createHash("sha1"), Encoding.base32_js) + + if (input !== undefined) { + if (cb !== undefined) { + hash.hash(input).then( + (result) => cb(null, result), + (err) => cb(err, null) + ) + return + } else { + if (typeof input === "string" || Buffer.isBuffer(input)) { + return hash.update(input).digest() + } else { + throw new Error("Invalid input for synchronous call: " + input) + } + } + } + + // return hash + return hash +} + +legacy_sha1.file = async function (filename: string, cb: Callback) { + if (filename === "-") { + process.stdin.resume() + return legacy_sha1(process.stdin, cb) + } + fs.stat(filename, (err, stats) => { + if (err) return cb(err, null) + if (stats.isDirectory()) + return cb({ dir: true, message: "Is a directory" }, null) + return legacy_sha1(fs.createReadStream(filename), cb) + }) +} + +export default legacy_sha1 diff --git a/test/base32-test.coffee b/test/base32-test.coffee deleted file mode 100644 index 4a9a6b4..0000000 --- a/test/base32-test.coffee +++ /dev/null @@ -1,76 +0,0 @@ -vows = require 'vows' -assert = require 'assert' -base32 = require '../lib/base32.js' -crypto = require 'crypto' - -suite = vows.describe 'Base32 Encoding' - -teststring = 'lowercase UPPERCASE 1234567 !@#$%^&*' - -suite = suite.addBatch - 'When encoding a test string': - topic: -> - base32.encode teststring - - 'it has the right value': (topic) -> - assert.equal topic, 'dhqqetbjcdgq6t90an850haj8d0n6h9064t36d1n6rvj08a04cj2aqh658' - - 'it decodes to the right value': (topic) -> - assert.equal base32.decode(topic), teststring - - 'When encoding a sha1 sum': - topic: -> - sha1 = crypto.createHash('sha1').update(teststring).digest('binary') - original: sha1, encoded: base32.encode sha1 - - 'it has the right value': (topic) -> - assert.equal topic.encoded, '1wwn60g9bv8n5g8n72udmk7yqm80dvtu' - - 'it has 32 characters': (topic) -> - assert.equal topic.encoded.length, 32 - - 'it decodes correctly': (topic) -> - assert.equal topic.original, base32.decode(topic.encoded) - - 'When using the built-in hasher': - topic: -> - hash = base32.sha1(teststring) - - 'it produces the same value': (topic) -> - assert.equal topic, '1wwn60g9bv8n5g8n72udmk7yqm80dvtu' - - 'When streaming a string to encode': - topic: -> - enc = new base32.Encoder - output = enc.update teststring.substr(0,10) - output+= enc.update teststring.substr(10) - output+= enc.finish() - output - - 'it should produce the correct value': (topic) -> - assert.equal topic, 'dhqqetbjcdgq6t90an850haj8d0n6h9064t36d1n6rvj08a04cj2aqh658' - - 'When decoding a string with common errors': - topic: -> - base32.decode 'dHqqetbjcdgq6t9Oan850hAj8d0n6h9O64t36dLn6rvjO8a04cj2aqh6S8' - - 'it should be the same as the original': (topic) -> - assert.equal topic, teststring - - 'When using a streaming hash': - topic: -> - base32.sha1() - - 'it should calculate the right digest': (hash) -> - hash.update(teststring.substr(0,10)) - hash.update(teststring.substr(10)) - assert.equal hash.digest(), '1wwn60g9bv8n5g8n72udmk7yqm80dvtu' - - 'When we hash a file': - topic: -> - base32.sha1.file('LICENSE', this.callback) - - 'it should give the right value': (hash) -> - assert.equal hash, 'za118kbdknm728mwx9r5g9rtv3mw2y4d' - -suite.run() diff --git a/test/base32.spec.ts b/test/base32.spec.ts new file mode 100644 index 0000000..620b0b9 --- /dev/null +++ b/test/base32.spec.ts @@ -0,0 +1,81 @@ +import * as base32 from "../src/legacy" +import { Decoder, Encoder, Encoding } from "../src/legacy" +import * as crypto from "crypto" + +const teststring = "lowercase UPPERCASE 1234567 !@#$%^&*" + +describe("Base32 Encoding", () => { + test("When encoding a test string", () => { + const encoded = base32.encode(teststring) + expect(encoded).toBe( + "dhqqetbjcdgq6t90an850haj8d0n6h9064t36d1n6rvj08a04cj2aqh658" + ) + }) + + test("When decoding a test string", () => { + const encoded = + "dhqqetbjcdgq6t90an850haj8d0n6h9064t36d1n6rvj08a04cj2aqh658" + expect(base32.decode(encoded)).toBe(teststring) + }) + + test("When encoding a sha1 buffer", () => { + const sha1 = crypto.createHash("sha1").update(teststring).digest() + const encoded = base32.encode(sha1) + expect(encoded).toBe("1wwn60g9bv8n5g8n72udmk7yqm80dvtu") + expect(encoded.length).toBe(32) + expect( + base32.decode(encoded, base32.Encoding.base32_js, null) + ).toStrictEqual(sha1) + }) + + test("When streaming a string to encode", () => { + const enc = new Encoder() + let output = enc.update(teststring.substring(0, 10)) + output += enc.update(teststring.substring(10)) + output += enc.finish() + expect(output).toBe( + "dhqqetbjcdgq6t90an850haj8d0n6h9064t36d1n6rvj08a04cj2aqh658" + ) + }) + + test("When decoding a string with common errors", () => { + const decoded = base32.decode( + "dHqqetbjcdgq6t9Oan850hAj8d0n6h9O64t36dLn6rvjO8a04cj2aqh6S8" + ) + expect(decoded).toBe(teststring) + }) + + test("When encoding an empty string", () => { + const encoded = base32.encode("") + expect(encoded).toBe("") + }) + + test("When decoding an empty string", () => { + const decoded = base32.decode("") + expect(decoded).toBe("") + }) + + test("When encoding and decoding a string containing only special characters", () => { + const specialString = "!@#$%^&*()_+-=[]{}|;:,.<>?" + const encoded = base32.encode(specialString) + const decoded = base32.decode(encoded) + expect(decoded).toBe(specialString) + }) + + test("When streaming a string to decode", () => { + const encoded = + "dhqqetbjcdgq6t90an850haj8d0n6h9064t36d1n6rvj08a04cj2aqh658" + const dec = new Decoder(Encoding.base32_js, "utf8") + let output = dec.update(encoded.substring(0, 10)) + output += dec.update(encoded.substring(10)) + output += dec.finish() + expect(output).toBe(teststring) + }) + + test("When encoding and decoding a buffer", () => { + const buffer = crypto.randomBytes(64) + const encoded = base32.encode(buffer) + const decoded = base32.decode(encoded, Encoding.base32_js, null) + expect(decoded).toStrictEqual(buffer) + }) +}) diff --git a/test/base32min-test.coffee b/test/base32min-test.coffee deleted file mode 100644 index 5fcbad5..0000000 --- a/test/base32min-test.coffee +++ /dev/null @@ -1,76 +0,0 @@ -vows = require 'vows' -assert = require 'assert' -base32 = require '../dist/base32.min.js' -crypto = require 'crypto' - -suite = vows.describe 'Base32 Encoding' - -teststring = 'lowercase UPPERCASE 1234567 !@#$%^&*' - -suite = suite.addBatch - 'When encoding a test string': - topic: -> - base32.encode teststring - - 'it has the right value': (topic) -> - assert.equal topic, 'dhqqetbjcdgq6t90an850haj8d0n6h9064t36d1n6rvj08a04cj2aqh658' - - 'it decodes to the right value': (topic) -> - assert.equal base32.decode(topic), teststring - - 'When encoding a sha1 sum': - topic: -> - sha1 = crypto.createHash('sha1').update(teststring).digest('binary') - original: sha1, encoded: base32.encode sha1 - - 'it has the right value': (topic) -> - assert.equal topic.encoded, '1wwn60g9bv8n5g8n72udmk7yqm80dvtu' - - 'it has 32 characters': (topic) -> - assert.equal topic.encoded.length, 32 - - 'it decodes correctly': (topic) -> - assert.equal topic.original, base32.decode(topic.encoded) - - 'When using the built-in hasher': - topic: -> - hash = base32.sha1(teststring) - - 'it produces the same value': (topic) -> - assert.equal topic, '1wwn60g9bv8n5g8n72udmk7yqm80dvtu' - - 'When streaming a string to encode': - topic: -> - enc = new base32.Encoder - output = enc.update teststring.substr(0,10) - output+= enc.update teststring.substr(10) - output+= enc.finish() - output - - 'it should produce the correct value': (topic) -> - assert.equal topic, 'dhqqetbjcdgq6t90an850haj8d0n6h9064t36d1n6rvj08a04cj2aqh658' - - 'When decoding a string with common errors': - topic: -> - base32.decode 'dHqqetbjcdgq6t9Oan850hAj8d0n6h9O64t36dLn6rvjO8a04cj2aqh6S8' - - 'it should be the same as the original': (topic) -> - assert.equal topic, teststring - - 'When using a streaming hash': - topic: -> - base32.sha1() - - 'it should calculate the right digest': (hash) -> - hash.update(teststring.substr(0,10)) - hash.update(teststring.substr(10)) - assert.equal hash.digest(), '1wwn60g9bv8n5g8n72udmk7yqm80dvtu' - - 'When we hash a file': - topic: -> - base32.sha1.file('LICENSE', this.callback) - - 'it should give the right value': (hash) -> - assert.equal hash, 'za118kbdknm728mwx9r5g9rtv3mw2y4d' - -suite.run() diff --git a/test/cli.spec.ts b/test/cli.spec.ts new file mode 100644 index 0000000..b61b96b --- /dev/null +++ b/test/cli.spec.ts @@ -0,0 +1,73 @@ +import execa from "execa" +import * as fs from "node:fs" + +async function base32( + args: string[], + options?: execa.Options +): Promise { + const { stdout } = await execa("./bin/base32", [...args], options) + return stdout +} + +describe("base32 CLI", () => { + it("encodes a file", async () => { + const inputFile = "test/fixtures/LICENSE.txt" + const expectedOutput = fs.readFileSync( + "test/fixtures/LICENSE.txt.base32", + "utf8" + ) + + const stdout = await base32([inputFile]) + + expect(stdout).toEqual(expectedOutput) + }) + + it("decodes a file", async () => { + const inputFile = "test/fixtures/LICENSE.txt.base32" + const expectedOutput = fs.readFileSync( + "test/fixtures/LICENSE.txt", + "utf8" + ) + + const stdout = await base32(["-d", inputFile]) + + expect(stdout).toEqual(expectedOutput) + }) + + it("encodes from stdin", async () => { + const input = "Hello, World!" + const expectedOutput = "91jprv3f5gg5evvjdhj22" + + const stdout = await base32([], { input }) + + expect(stdout.trim()).toEqual(expectedOutput) + }) + + it("decodes from stdin", async () => { + const input = "91jprv3f5gg5evvjdhj22" + const expectedOutput = "Hello, World!" + + const stdout = await base32(["-d"], { input }) + + expect(stdout.trim()).toEqual(expectedOutput) + }) + + it("hashes a file", async () => { + const inputFile = "test/fixtures/LICENSE.txt" + const expectedOutput = + "pwd0mnqvj63u99125jevb6vxn8w4bvaq test/fixtures/LICENSE.txt" + + const stdout = await base32(["-s", inputFile]) + + expect(stdout.trim()).toEqual(expectedOutput) + }) + + it("hashes from stdin", async () => { + const input = "Hello, World!" + const expectedOutput = "1859yak7eaa2anxbadaxeuqm8bwfcqg1 -" + + const stdout = await base32(["-s"], { input }) + + expect(stdout.trim()).toEqual(expectedOutput) + }) +}) diff --git a/test/compare.coffee b/test/compare.coffee deleted file mode 100644 index db7a154..0000000 --- a/test/compare.coffee +++ /dev/null @@ -1,22 +0,0 @@ -crypto = require 'crypto' -base32 = require 'base32' - -Number.prototype.times = (fn) -> - if this > 1 - fn.call() - (this - 1).times fn - -console.log "Hexadecimal:\n" -5.times -> - str = 'foo' + Math.random() - console.log ' ' + crypto.createHash('sha1').update(str).digest('hex') - -console.log "\nBase 64:\n" -5.times -> - str = 'foo' + Math.random() - console.log ' ' + crypto.createHash('sha1').update(str).digest('base64').substr(0,27).replace('/', '_').replace('\+', '-') - -console.log "\nBase 32:\n" -5.times -> - str = 'foo' + Math.random() - console.log ' ' + base32.sha1(str) diff --git a/test/compare.ts b/test/compare.ts new file mode 100644 index 0000000..ff3a3fc --- /dev/null +++ b/test/compare.ts @@ -0,0 +1,27 @@ +import * as crypto from "crypto" +import * as base32 from "../src/legacy" + +const samples = Array.from({ length: 5 }, () => `foo${Math.random()}`) + +function sha1( + str: string, + encoding: base32.AlphaCodec | crypto.BinaryToTextEncoding +): string { + const hash = crypto.createHash("sha1").update(str) + return typeof encoding === "string" + ? hash.digest(encoding) + : base32.encode(hash.digest(), encoding) +} + +const encodings = { + Hexadecimal: "hex", + Base64: "base64", + ...base32.Encoding, +} as const + +Object.entries(encodings).map(([name, encoding]) => { + console.log(`\n${name}:\n`) + samples.forEach((str) => { + console.log(` ${sha1(str, encoding)}`) + }) +}) diff --git a/test/encode.spec.ts b/test/encode.spec.ts new file mode 100644 index 0000000..3df56c5 --- /dev/null +++ b/test/encode.spec.ts @@ -0,0 +1,31 @@ +import { encode } from "../src/legacy" + +describe("base32 encode function", () => { + test("should encode a simple string", () => { + const input = "Hello, world!" + const expectedOutput = "91jprv3f5gg7evvjdhj22" + + expect(encode(input)).toBe(expectedOutput) + }) + + test("should encode a string with special characters", () => { + const input = "@$%&*()-_=+[]{};:`~" + const expectedOutput = "80j2a9ha50mjuqtx5ddnuyvx7cx60zg" + + expect(encode(input)).toBe(expectedOutput) + }) + + test("should encode an empty string", () => { + const input = "" + const expectedOutput = "" + + expect(encode(input)).toBe(expectedOutput) + }) + + test("should encode a string containing numbers", () => { + const input = "1234567890" + const expectedOutput = "64t36d1n6rvkge9g" + + expect(encode(input)).toBe(expectedOutput) + }) +}) diff --git a/test/encodings.spec.ts b/test/encodings.spec.ts new file mode 100644 index 0000000..065c7b4 --- /dev/null +++ b/test/encodings.spec.ts @@ -0,0 +1,116 @@ +import * as Encodings from "../src/encoding" +import * as base32 from "../src/legacy" +import AlphaCodec from "../src/alphacodec" + +const teststring = "lowercase UPPERCASE 1234567 !@#$%^&*" + +// A buffer with every possible byte value +const testbuffer = Buffer.from(Array.from({ length: 255 }, (_, i) => i)) + +function testStringEncoding( + encoding: AlphaCodec, + teststring: string, + expected: string +) { + const encoded = base32.encode(teststring, encoding) + expect(encoded).toBe(expected) + const decoded = base32.decode(encoded, encoding) + expect(decoded).toBe(teststring) +} + +function testBufferEncoding( + encoding: AlphaCodec, + testbuffer: Buffer, + expected: string +) { + const encoded = base32.encode(testbuffer, encoding) + expect(encoded).toBe(expected) + const decoded = base32.decode(encoded, encoding, null) + expect(decoded).toStrictEqual(testbuffer) +} + +describe("Base32 Encodings", () => { + test("Encoding and decoding with default base32_js encoding", () => { + const encoding = Encodings.base32_js + testStringEncoding( + encoding, + teststring, + "dhqqetbjcdgq6t90an850haj8d0n6h9064t36d1n6rvj08a04cj2aqh658" + ) + testBufferEncoding( + encoding, + testbuffer, + "000g40r40m30e209185gr38e1w8124gk2gahc5rr34d1p70x3rfj08924cj2a9h750mjmatc5mq2yc1h68tk8d9p6ww3jehv7gykwfu085146h258t3mgjaa9d64ukjfa18n4mumanb5ep2tb9dnrqaybxg62rk3chjpctv8d5n6pv3ddtqq0wbjedu7axkqf1wqmyvwfnz7z041ga1r91c6gy48k2mbhj6rx3wgj699754njubth6cukee9v7mzm2gu58x4mpkafa59nanutbdenyrb3cnkpjuvddxrq6xbqf5xquzw1ge2rf2cbhp7t34wnjyctq7czm6hub9x9nepuzcdkppvvkexxqz0w7he7t75wvkyhufaxfpevvqfy3rz5wzmyqvffy7tzbxztzfy" + ) + }) + + test("Encoding and decoding with crockford encoding", () => { + const encoding = Encodings.crockford + testStringEncoding( + encoding, + teststring, + "DHQQESBJCDGQ6S90AN850HAJ8D0N6H9064S36D1N6RVJ08A04CJ2AQH658" + ) + testBufferEncoding( + encoding, + testbuffer, + "000G40R40M30E209185GR38E1W8124GK2GAHC5RR34D1P70X3RFJ08924CJ2A9H750MJMASC5MQ2YC1H68SK8D9P6WW3JEHV7GYKWFT085146H258S3MGJAA9D64TKJFA18N4MTMANB5EP2SB9DNRQAYBXG62RK3CHJPCSV8D5N6PV3DDSQQ0WBJEDT7AXKQF1WQMYVWFNZ7Z041GA1R91C6GY48K2MBHJ6RX3WGJ699754NJTBSH6CTKEE9V7MZM2GT58X4MPKAFA59NANTSBDENYRB3CNKPJTVDDXRQ6XBQF5XQTZW1GE2RF2CBHP7S34WNJYCSQ7CZM6HTB9X9NEPTZCDKPPVVKEXXQZ0W7HE7S75WVKYHTFAXFPEVVQFY3RZ5WZMYQVFFY7SZBXZSZFY" + ) + }) + + test("Encoding and decoding with base32hex encoding", () => { + const encoding = Encodings.base32hex + testStringEncoding( + encoding, + teststring, + "DHNNEPBICDGN6P90AL850HAI8D0L6H9064P36D1L6ORI08A04CI2ANH658" + ) + testBufferEncoding( + encoding, + testbuffer, + "000G40O40K30E209185GO38E1S8124GJ2GAHC5OO34D1M70T3OFI08924CI2A9H750KIKAPC5KN2UC1H68PJ8D9M6SS3IEHR7GUJSFQ085146H258P3KGIAA9D64QJIFA18L4KQKALB5EM2PB9DLONAUBTG62OJ3CHIMCPR8D5L6MR3DDPNN0SBIEDQ7ATJNF1SNKURSFLV7V041GA1O91C6GU48J2KBHI6OT3SGI699754LIQBPH6CQJEE9R7KVK2GQ58T4KMJAFA59LALQPBDELUOB3CLJMIQRDDTON6TBNF5TNQVS1GE2OF2CBHM7P34SLIUCPN7CVK6HQB9T9LEMQVCDJMMRRJETTNV0S7HE7P75SRJUHQFATFMERRNFU3OV5SVKUNRFFU7PVBTVPVFU" + ) + }) + + test("Encoding and decoding with rfc4648 encoding", () => { + const encoding = Encodings.rfc4648 + testStringEncoding( + encoding, + teststring, + "NRXXOZLSMNQXGZJAKVIFARKSINAVGRJAGEZDGNBVGY3SAIKAEMSCKXRGFI" + ) + testBufferEncoding( + encoding, + testbuffer, + "AAAQEAYEAUDAOCAJBIFQYDIOB4IBCEQTCQKRMFYYDENBWHA5DYPSAIJCEMSCKJRHFAUSUKZMFUXC6MBRGIZTINJWG44DSOR3HQ6T4P2AIFBEGRCFIZDUQSKKJNGE2TSPKBIVEU2UKVLFOWCZLJNVYXK6L5QGCYTDMRSWMZ3INFVGW3DNNZXXA4LSON2HK5TXPB4XU634PV7H7AEBQKBYJBMGQ6EITCULRSGY5D4QSGJJHFEVS2LZRGM2TOOJ3HU7UCQ2FI5EUWTKPKFJVKV2ZLNOV6YLDMVTWS23NN5YXG5LXPF5X274BQOCYPCMLRWHZDE4VS6MZXHM7UGR2LJ5JVOW27MNTWW33TO55X7A4HROHZHF43T6R2PK5PWO33XP6DY7F47U6X3PP6HZ7L57Z7P6" + ) + }) + + test("Encoding and decoding with zbase32 encoding", () => { + const encoding = Encodings.zbase32 + testStringEncoding( + encoding, + teststring, + "ptzzq3m1cpozg3jykiefytk1epyigtjygr3dgpbiga51yekyrc1nkztgfe" + ) + testBufferEncoding( + encoding, + testbuffer, + "yyyoryarywdyqnyjbefoadeqbhebnrounoktcfaadrpbs8y7dax1yejnrc1nkjt8fyw1wk3cfwzn6cbtge3uepjsghhd1qt58o6uhx4yefbrgtnfe3dwo1kkjpgr4u1xkbeirw4wkimfqsn3mjpiazk6m7ognaudct1sc35epfigs5dpp3zzyhm1qp48k7uzxbhzw65hxi989yrbokbajbcgo6reunwmt1ga7dho1gjj8fri14m3tgc4uqqj58w9wno4fe7rwsukxkfjiki43mpqi6amdcius145pp7azg7mzxf7z49hboqnaxncmts83drhi16c3z8c9wgt4mj7jiqs49cpuss55uq77z9yh8tq838fh5u6t4xk7xsq55zx6da9fh9w6z5xx6839m7939x6" + ) + }) + + test("Encoding and decoding with geohash encoding", () => { + const encoding = Encodings.geohash + testStringEncoding( + encoding, + teststring, + "ejrrftckdehr6t90bp850jbk8e0p6j9064t36e1p6svk08b04dk2brj658" + ) + testBufferEncoding( + encoding, + testbuffer, + "000h40s40n30f209185hs38f1w8124hm2hbjd5ss34e1q70x3sgk08924dk2b9j750nknbtd5nr2yd1j68tm8e9q6ww3kfjv7hymwgu085146j258t3nhkbb9e64umkgb18p4nunbpc5fq2tc9epsrbycxh62sm3djkqdtv8e5p6qv3eetrr0wckfeu7bxmrg1wrnyvwgpz7z041hb1s91d6hy48m2ncjk6sx3whk699754pkuctj6dumff9v7nzn2hu58x4nqmbgb59pbputcefpysc3dpmqkuveexsr6xcrg5xruzw1hf2sg2dcjq7t34wpkydtr7dzn6juc9x9pfquzdemqqvvmfxxrz0w7jf7t75wvmyjugbxgqfvvrgy3sz5wznyrvggy7tzcxztzgy" + ) + }) +}) diff --git a/test/fixtures/LICENSE.txt b/test/fixtures/LICENSE.txt new file mode 100644 index 0000000..a249be7 --- /dev/null +++ b/test/fixtures/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (C) 2011 by Isaac Wolkerstorfer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/test/fixtures/LICENSE.txt.base32 b/test/fixtures/LICENSE.txt.base32 new file mode 100644 index 0000000..9cc1acf --- /dev/null +++ b/test/fixtures/LICENSE.txt.base32 @@ -0,0 +1 @@ +8dqq0ybjd5kpgx10511jj81j60rk2832f4g4jwv1c5hj0nvfdhnpawkkehqq4tk5e850mm35e9ppjwvkd5qpw839ecg6gtbjcnh7j837e9gpwx35cgp20tkjcnjj0vv641hpgrbjcxjjr83mdwg62vkt41r6awkkdxq20vv2ehgpjvk9dtkj0r90cdqq0y8adxk20x38d5tj0wvfctu7erbjcmg62vk441gq6wvfcdmp2x35cgg68vv3enppavkmc5u6jvve41k6jv35ecg2gx38cmg24mvfctu7erbjcmh2jb10ehqj0t35c5p0mube41u6gt90adqpcx3qc5t6a83qd5u6gvvnegg74tbkeht6jrvmd5qpwb10d5q66v3nchmpwtt0exmq8u3fenu20v39dnmq8rbmd5qpw83md1jj0wk9cxm78wraehqj0xbkcmp20rvfe1wjr83ddxj6jtkt5gg6utbjcxjjr83genh6rubkd0p20t39edu74ub2enu6ab10edup4v39cdjpwwv55gg62vk45xqq483kcnp6r2k3dxr6jtbk41qpc83md1jj0mvfctu7erbjcmp20rbecgg78vt0e1jq4vb9egg70tbjedqpwwt0ehqj0xv8dxpj0x38cmg56vv6ehvp2wk541mq62k6ent6wubkd1jp883mdwg68vt0edqjr83kenh6mtb3egg78vt0ehm6a836dxp6rvvqd5q6e833dxq68ubmd5qpwwtu18558u3541gp4vvpcmg66vvgf5t6jtv8egg6wvvmd5hpa831dtj20x38d5tj0w35e9ppjwvkd5qpw83edxu6jrv541tpgrbcdgg64t90d5q66v3nchjp8839dr562v3c41hpyw39cntj0vvj41tqarkkehgpwx39c5p20w3fe9u6jvveecg6yth0ehm6a82kdxk78xv1e9jjw2gaah44a82k9x358nu1a92j0jak41854kup9524ah10490n6829ach2r82q95a4gkunagg5egaja90mwn2t417mc8219tcj0ju99t22r825b1854hakacg4ymga956n0k298n22r8299t1mrna49574e822ana20kjfagg4rjad95a4ah10ah7j0n288mg5egaja90mwn298n9j0ku6416mamj3910mwn21894mrjamb4p0mhj9ah74amuk4134ymh084g50gajah4m6nac85920m2na984ymu5410mwh109t7mwjae8t94jkj78n6makjm5rg4jkh09t7j0hap8n75882k910mrk10ah44a2j1ana4gkujacg4ymh08d7n0paj953mgn10917mrh25a99j0gj54164jga29h2j0hjfa8g42kjt411mrga99mp20h219n0mehak417n482fah44amga9h4m2gj99h4n8p9c41bmgham912n48299rg42kh0851n8jaf9rg4yhh08d7mwn2j851n8b10ah7n4n109x920kum912n4nu9ad2jr821a94n6jae8wg4cmjf9mp0mkunagg4yhh09x920jae411mykje8n1n8jaf9rg5ejam90g58j25419myhjmax0n4h909x920n288mg5amu5417n482fah44amh08h2m2k299t3n68299r558j25419myhjmax0n4h9e \ No newline at end of file diff --git a/test/hi-base32-compat.spec.ts b/test/hi-base32-compat.spec.ts new file mode 100644 index 0000000..a425e1c --- /dev/null +++ b/test/hi-base32-compat.spec.ts @@ -0,0 +1,29 @@ +import Base32 from "../src/index" +import * as Encoding from "../src/encoding" + +const base32 = new Base32(Encoding.base32) + +describe("compatibility with hi-base32 package", () => { + test("Long text encoding", () => { + const input = + "Man is distinguished, not only by his reason, but by this singular passion from other animals, which is a lust of the mind, that by a perseverance of delight in the continued and indefatigable generation of knowledge, exceeds the short vehemence of any carnal pleasure." + const expected = + "JVQW4IDJOMQGI2LTORUW4Z3VNFZWQZLEFQQG433UEBXW43DZEBRHSIDINFZSA4TFMFZW63RMEBRHK5BAMJ4SA5DINFZSA43JNZTXK3DBOIQHAYLTONUW63RAMZZG63JAN52GQZLSEBQW42LNMFWHGLBAO5UGSY3IEBUXGIDBEBWHK43UEBXWMIDUNBSSA3LJNZSCYIDUNBQXIIDCPEQGCIDQMVZHGZLWMVZGC3TDMUQG6ZRAMRSWY2LHNB2CA2LOEB2GQZJAMNXW45DJNZ2WKZBAMFXGIIDJNZSGKZTBORUWOYLCNRSSAZ3FNZSXEYLUNFXW4IDPMYQGW3TPO5WGKZDHMUWCAZLYMNSWKZDTEB2GQZJAONUG64TUEB3GK2DFNVSW4Y3FEBXWMIDBNZ4SAY3BOJXGC3BAOBWGKYLTOVZGKLQ" + expect(base32.encode(input)).toBe(expected) + expect(base32.decode(expected)).toBe(input) + }) + + test("Decoding to string", () => { + expect(base32.decode("JBSWY3DP")).toBe("Hello") + }) + + test("Decoding to bytes", () => { + const buffer = base32.decode("JBSWY3DP", null) + const bytes = Array.from(buffer) + expect(bytes).toEqual([72, 101, 108, 108, 111]) + }) + + test("UTF-8 encoding", () => { + expect(base32.encode("中文")).toBe("4S4K3ZUWQ4") + }) +}) diff --git a/test/sha1.spec._ts b/test/sha1.spec._ts new file mode 100644 index 0000000..6d7f0b0 --- /dev/null +++ b/test/sha1.spec._ts @@ -0,0 +1,30 @@ +import * as base32 from '../src/base32'; + +const teststring = 'lowercase UPPERCASE 1234567 !@#$%^&*'; + +describe('Base32 sha1', () => { + + test('When using the built-in hasher', () => { + const hash = base32.sha1(teststring); + expect(hash).toBe('1wwn60g9bv8n5g8n72udmk7yqm80dvtu'); + }); + + test('When using a streaming hash', () => { + const hash = base32.sha1(); + hash.update(teststring.substr(0, 10)); + hash.update(teststring.substr(10)); + expect(hash.digest()).toBe('1wwn60g9bv8n5g8n72udmk7yqm80dvtu'); + }); + + test('When we hash a file', (done) => { + base32.sha1.file('./LICENSE', (error, hash) => { + if (error) { + done(error); + } else { + expect(hash).toBe('za118kbdknm728mwx9r5g9rtv3mw2y4d'); + done(); + } + }); + }); + +}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..66de752 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "declaration": true, + "module": "commonjs", + "esModuleInterop": true, + "target": "ES2019", + "moduleResolution": "node", + "sourceMap": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "forceConsistentCasingInFileNames": true, + "lib": ["ES2019"] + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules"] +}