Skip to content

Commit

Permalink
Merge branch 'release/2.0.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
devbanana committed Jul 24, 2023
2 parents f963a47 + acedaf3 commit 8995bb8
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 116 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ jobs:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[skip ci]')"
steps:
- uses: actions/checkout@v1
- uses: artiomtr/jest-coverage-report-action@v2.0-rc.6
- uses: actions/checkout@v3
- uses: artiomtr/jest-coverage-report-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
threshold: 100
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.0.0]: 2023-07-24

### Fixed

- Read and encode input from leftmost bits first per [the spec](https://www.crockford.com/base32.html)

### Removed

- Removed `stripLeadingZeros` option

## [1.1.0] - 2021-10-27

### Added
Expand Down Expand Up @@ -33,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ability to encode a number to base 32
- Ability to decode a base 32 string to a buffer

[2.0.0]: https://github.com/devbanana/crockford-base32/compare/1.1.0...2.0.0
[1.1.0]: https://github.com/devbanana/crockford-base32/compare/1.0.1...1.1.0
[1.0.1]: https://github.com/devbanana/crockford-base32/compare/1.0.0...1.0.1
[1.0.0]: https://github.com/devbanana/crockford-base32/releases/tag/1.0.0
46 changes: 14 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,40 +13,22 @@ npm install --save crockford-base32
```javascript
const { CrockfordBase32 } = require('crockford-base32');

CrockfordBase32.encode(Buffer.from('some string')); // 3KDXPPA83KEHS6JVK7
CrockfordBase32.decode('3KDXPPA83KEHS6JVK7').toString(); // some string
```

## Encoding Options

You can pass options to the `encode()` method to change how it performs the encoding:
CrockfordBase32.encode(Buffer.from('some string')); // EDQPTS90EDT74TBECW
CrockfordBase32.decode('EDQPTS90EDT74TBECW').toString(); // some string

| Option | Type | Default | Description |
| ----------------- | --------- | ------- | ---------------------------------------------------- |
| stripLeadingZeros | `boolean` | `false` | Returns the encoded string without any leading zeros |
// It will ignore hyphens
CrockfordBase32.decode('EDQPTS-90EDT7-4TBECW').toString(); // some string

### Example

```javascript
CrockfordBase32.encode(Buffer.from('\x00test')); // 01T6AWVM
CrockfordBase32.encode(Buffer.from('\x00test'), { stripLeadingZeros: true }); // 1T6AWVM
```
// It will convert the letters I and L to 1, and O to 0
CrockfordBase32.decode('1P10E').toString('hex'); // 0d8207
CrockfordBase32.decode('IPLOE').toString('hex'); // 0d8207
CrockfordBase32.decode('iploe').toString('hex'); // 0d8207

## Decoding Options

You can also pass options to `decode()` as follows:

| Option | Type | Default | Description |
| ----------------- | --------- | ------- | ----------------------------------------------------------------------------------- |
| stripLeadingZeros | `boolean` | `false` | Strips all zeros from the decoded string. |
| asNumber | `boolean` | `false` | `true` to return the decoded output as a `bigint`, `false` to return as a `Buffer`. |

##### Example

```javascript
CrockfordBase32.decode('01T6AWVM').toString(); // \x00test
CrockfordBase32.decode('01T6AWVM', { stripLeadingZeros: true }).toString(); // test
// Encode and decode a number
CrockfordBase32.encode(822354); // 1J654
CrockfordBase32.decode('1J654', { asNumber: true }); // 822354n

CrockfordBase32.decode('CSCW'); // <Buffer 06 65 9c>
CrockfordBase32.decode('CSCW', { asNumber: true }); // 419228n
// Or a bigint
CrockfordBase32.encode(275_789_480_204_545_813_933_268_697_807_617_179_845n); // SXXHYC0JSN77K601AW3K31P0RM
CrockfordBase32.decode('SXXHYC0JSN77K601AW3K31P0RM', { asNumber: true }); // 275789480204545813933268697807617179845n
```
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "crockford-base32",
"version": "1.1.0",
"version": "2.0.0",
"description": "An implementation of Douglas Crockford's base 32 encoding algorithm",
"main": "lib/index.js",
"types": "./lib/index.d.ts",
Expand Down
70 changes: 31 additions & 39 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,31 @@ describe('Base32Encoder', () => {
});

it('can encode a single byte', () => {
expect(CrockfordBase32.encode(Buffer.from([0x74]))).toBe('3M');
expect(CrockfordBase32.encode(Buffer.from([0x74]))).toBe('EG');
});

it('can encode two bytes', () => {
expect(CrockfordBase32.encode(Buffer.from([0x74, 0x74]))).toBe('EHT0');
});

it('can encode a large number', () => {
expect(
CrockfordBase32.encode(Buffer.from('593f8759e8431f5f', 'hex')),
).toBe('5JFW7B7M467TZ');
).toBe('B4ZREPF88CFNY');
});

it('does not strip off leading zeros', () => {
expect(CrockfordBase32.encode(Buffer.from([0, 0, 0xa9]))).toBe('00059');
expect(CrockfordBase32.encode(Buffer.from([0, 0, 0xa9]))).toBe('000AJ');
});

it('can encode a number', () => {
expect(CrockfordBase32.encode(388_864)).toBe('BVR0');
expect(CrockfordBase32.encode(388_864)).toBe('0QQG0');
});

it('can encode a bigint', () => {
expect(
CrockfordBase32.encode(10_336_657_440_695_546_835_250_649_691n),
).toBe('8B691DAR2GC0Q2466JV');
).toBe('45K4GPNC1860BH2339DG');
});

it('cannot take a negative number', () => {
Expand All @@ -52,23 +56,15 @@ describe('Base32Encoder', () => {
CrockfordBase32.encode(
Buffer.from('017cb3b93bcb40b6147d7813c5ad2339', 'hex'),
),
).toBe('01FJSVJEYB82V18ZBR2F2TT8SS');
).toBe('05YB7E9VSD0BC53XF09WBB9374');
});

it("doesn't modify the input buffer", () => {
const buffer = Buffer.from('test');
// noinspection SpellCheckingInspection
expect(CrockfordBase32.encode(buffer)).toBe('1T6AWVM');
expect(CrockfordBase32.encode(buffer)).toBe('EHJQ6X0');
expect(buffer.toString()).toBe('test');
});

it('can strip leading zeros', () => {
expect(
CrockfordBase32.encode(Buffer.from('0000a9', 'hex'), {
stripLeadingZeros: true,
}),
).toBe('59');
});
});

describe('when decoding', () => {
Expand All @@ -80,31 +76,31 @@ describe('Base32Encoder', () => {
});

it('can decode a single byte', () => {
expect(CrockfordBase32.decode('3M').toString()).toBe('t');
expect(CrockfordBase32.decode('EG').toString()).toBe('t');
});

it('can decode two bytes', () => {
expect(CrockfordBase32.decode('EHT0').toString()).toBe('tt');
});

it('can decode a large number', () => {
expect(CrockfordBase32.decode('5JFW7B7M467TZ').toString('hex')).toBe(
expect(CrockfordBase32.decode('B4ZREPF88CFNY').toString('hex')).toBe(
'593f8759e8431f5f',
);
});

it('keeps leading zeros when decoding', () => {
expect(CrockfordBase32.decode('00059').toString('hex')).toBe('0000a9');
});

it('pads to the next byte', () => {
expect(CrockfordBase32.decode('M3kV').toString('hex')).toBe('0a0e7b');
expect(CrockfordBase32.decode('000AJ').toString('hex')).toBe('0000a9');
});

it.each`
inputChar | translatedChar | input | output
${'I'} | ${'1'} | ${'AIm'} | ${'2834'}
${'i'} | ${'1'} | ${'Aim'} | ${'2834'}
${'L'} | ${'1'} | ${'ALm'} | ${'2834'}
${'l'} | ${'1'} | ${'Alm'} | ${'2834'}
${'O'} | ${'0'} | ${'AOm'} | ${'2814'}
${'o'} | ${'0'} | ${'Aom'} | ${'2814'}
inputChar | translatedChar | input | output
${'I'} | ${'1'} | ${'AIm0'} | ${'5068'}
${'i'} | ${'1'} | ${'Aim0'} | ${'5068'}
${'L'} | ${'1'} | ${'ALm0'} | ${'5068'}
${'l'} | ${'1'} | ${'Alm0'} | ${'5068'}
${'O'} | ${'0'} | ${'AOM0'} | ${'5028'}
${'o'} | ${'0'} | ${'AoM0'} | ${'5028'}
`(
'translates $inputChar to $translatedChar when decoding',
({ input, output }: { input: string; output: string }) => {
Expand All @@ -115,20 +111,16 @@ describe('Base32Encoder', () => {
it('can decode a ULID', () => {
// noinspection SpellCheckingInspection
expect(
CrockfordBase32.decode('01FJSVJEYB82V18ZBR2F2TT8SS').toString('hex'),
CrockfordBase32.decode('05YB7E9VSD0BC53XF09WBB9374').toString('hex'),
).toBe('017cb3b93bcb40b6147d7813c5ad2339');
});

it('can strip leading zeros', () => {
expect(
CrockfordBase32.decode('00059', { stripLeadingZeros: true }).toString(
'hex',
),
).toBe('a9');
it('does not add up to a complete byte', () => {
expect(CrockfordBase32.decode('A1M').toString('hex')).toBe('5068');
});

it('can return a number', () => {
expect(CrockfordBase32.decode('G3T', { asNumber: true })).toBe(16_506n);
expect(CrockfordBase32.decode('81X0', { asNumber: true })).toBe(16_506n);
});

it('rejects any invalid base 32 character', () => {
Expand All @@ -139,14 +131,14 @@ describe('Base32Encoder', () => {

it('ignores hyphens', () => {
// noinspection SpellCheckingInspection
expect(CrockfordBase32.decode('3KDXPP-A83KEH-S6JVK7').toString()).toBe(
expect(CrockfordBase32.decode('EDQPTS-90EDT7-4TBECW').toString()).toBe(
'some string',
);
});

it('ignores multiple adjacent hyphens', () => {
// noinspection SpellCheckingInspection
expect(CrockfordBase32.decode('3KDXPP--A83KEH---S6JVK7').toString()).toBe(
expect(CrockfordBase32.decode('EDQPTS--90EDT7---4TBECW').toString()).toBe(
'some string',
);
});
Expand Down
54 changes: 14 additions & 40 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,63 +3,43 @@ import { Buffer } from 'buffer';
// noinspection SpellCheckingInspection
const characters = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';

interface EncodeOptions {
stripLeadingZeros?: boolean;
}

type DecodeAsNumberOptions = { asNumber: true } & EncodeOptions;
type DecodeAsBufferOptions = { asNumber?: false } & EncodeOptions;
type DecodeAsNumberOptions = { asNumber: true };
type DecodeAsBufferOptions = { asNumber: false };

/**
* An implementation of the Crockford Base32 algorithm.
*
* Spec: https://www.crockford.com/base32.html
*/
export class CrockfordBase32 {
static encode(
input: Buffer | number | bigint,
options?: EncodeOptions,
): string {
let stripZeros = options?.stripLeadingZeros || false;

static encode(input: Buffer | number | bigint): string {
if (input instanceof Buffer) {
// Copy the input buffer so it isn't modified when we call `reverse()`
input = Buffer.from(input);
} else {
input = this.createBuffer(input);
stripZeros = true;
}

const output: number[] = [];
let bitsRead = 0;
let buffer = 0;

// Work from the end of the buffer
input.reverse();

for (const byte of input) {
// Add current byte to start of buffer
buffer |= byte << bitsRead;
buffer = (buffer << 8) | byte;
bitsRead += 8;

while (bitsRead >= 5) {
output.unshift(buffer & 0x1f);
buffer >>>= 5;
output.push((buffer >>> (bitsRead - 5)) & 0x1f);
bitsRead -= 5;
}
}

if (bitsRead > 0) {
output.unshift(buffer & 0x1f);
output.push((buffer << (5 - bitsRead)) & 0x1f);
}

let dataFound = false;
return output
.filter(byte =>
stripZeros && !dataFound && byte === 0 ? false : (dataFound = true),
)
.map(byte => characters.charAt(byte))
.join('');
return output.map(byte => characters.charAt(byte)).join('');
}

static decode(input: string, options: DecodeAsNumberOptions): bigint;
Expand All @@ -77,9 +57,6 @@ export class CrockfordBase32 {
.replace(/[IL]/g, '1')
.replace(/-+/g, '');

// Work from the end
input = input.split('').reverse().join('');

const output: number[] = [];
let bitsRead = 0;
let buffer = 0;
Expand All @@ -92,22 +69,19 @@ export class CrockfordBase32 {
);
}

buffer |= byte << bitsRead;
bitsRead += 5;

while (bitsRead >= 8) {
output.unshift(buffer & 0xff);
buffer >>>= 8;
if (bitsRead >= 8) {
bitsRead -= 8;
output.push(buffer | (byte >> bitsRead));
buffer = (byte << (8 - bitsRead)) & 0xff;
} else {
buffer |= byte << (8 - bitsRead);
}
}

if (bitsRead >= 5 || buffer > 0) {
output.unshift(buffer & 0xff);
}

if (options?.stripLeadingZeros === true) {
while (output[0] === 0) output.shift();
if (buffer > 0) {
output.push(buffer);
}

if (options?.asNumber === true) {
Expand Down

0 comments on commit 8995bb8

Please sign in to comment.