diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..e8ab86e --- /dev/null +++ b/.babelrc @@ -0,0 +1,10 @@ +{ + "presets": ["es2015"], + "plugins": [ + "add-module-exports", + "transform-es2015-modules-umd", + ["rename-umd-globals", { + "hashids": "Hashids" + }] + ] +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..86cb3d2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# editorconfig.org +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = tab +indent_size = 4 + +[{package.json,.babelrc}] +indent_style = space +indent_size = 2 diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..83dc03f --- /dev/null +++ b/.eslintrc @@ -0,0 +1,22 @@ +{ + "env": { + "browser": true, + "node": true, + "mocha": true, + "es6": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module" + }, + "rules": { + "eqeqeq": 2, + "indent": ["error", "tab"], + "no-var": 2, + "prefer-const": 2, + "no-unused-vars": [2, {"vars": "all", "args": "none", "varsIgnorePattern": "[iI]gnored"}], + "no-alert": 2, + "no-trailing-spaces": 2 + } +} diff --git a/.gitignore b/.gitignore index 39c1eef..05f8eb7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ +node_modules .DS_Store -Hashids.tmproj +npm-debug.log +.nyc_output +coverage diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..22de789 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: node_js + +node_js: + - "0.10" + - "0.12" + - "4" + - "6" + +sudo: false diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ecc6e08 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,83 @@ + +# CHANGELOG + +**1.1.0**: + +- ES6 source (lib itself is minified ES5) +- UMD support ([Node.js repo](https://github.com/ivanakimov/hashids.node.js) is merging into this one) +- Added Eslint (npm run lint) +- Added Mocha (npm run test) +- Added Coveralls (npm run coveralls) +- Added build script (npm run build) +- Moved CHANGELOG out of `README.md` +- `README.md` completely updated +- `examples/` folder removed; all examples are now in the README + +**1.0.2**: + +- Support for older browsers (using `charAt`) by [@tauanz](https://github.com/tauanz): + +**1.0.1**: + +- *require.js* support by [@nleclerc](https://github.com/nleclerc): + +**1.0.0**: + +- Several public functions are renamed to be more appropriate: + - Function `encrypt()` changed to `encode()` + - Function `decrypt()` changed to `decode()` + - Function `encryptHex()` changed to `encodeHex()` + - Function `decryptHex()` changed to `decodeHex()` + + Hashids was designed to encode integers, primary ids at most. We've had several requests to encrypt sensitive data with Hashids and this is the wrong algorithm for that. So to encourage more appropriate use, `encrypt/decrypt` is being "downgraded" to `encode/decode`. + +- Version tag added: `1.0` +- `README.md` updated + +**0.3.0**: + +**PRODUCED HASHES IN THIS VERSION ARE DIFFERENT THAN IN 0.1.4, DO NOT UPDATE IF YOU NEED THEM TO KEEP WORKING:** + +- Same algorithm as [PHP](https://github.com/ivanakimov/hashids.php) and [Node.js](https://github.com/ivanakimov/hashids.node.js) versions now +- Overall approximately **4x** faster +- Consistent shuffle function uses slightly modified version of [Fisher–Yates algorithm](http://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm) +- Generate large hash strings faster (where _minHashLength_ is more than 1000 chars) +- When using _minHashLength_, hash character disorder has been improved +- Basic English curse words will now be avoided even with custom alphabet +- _encrypt_ function now also accepts array of integers as input +- Passing JSLint now +- Support for [Bower](http://bower.io/) package manager + +**0.1.4**: + +- Global var leak for hashSplit (thanks to [@BryanDonovan](https://github.com/BryanDonovan)) +- Class capitalization (thanks to [@BryanDonovan](https://github.com/BryanDonovan)) + +**0.1.3**: + + Warning: If you are using 0.1.2 or below, updating to this version will change your hashes. + +- Updated default alphabet (thanks to [@speps](https://github.com/speps)) +- Constructor removes duplicate characters for default alphabet as well (thanks to [@speps](https://github.com/speps)) + +**0.1.2**: + + Warning: If you are using 0.1.1 or below, updating to this version will change your hashes. + +- Minimum hash length can now be specified +- Added more randomness to hashes +- Added unit tests +- Added example files +- Changed warnings that can be thrown +- Renamed `encode/decode` to `encrypt/decrypt` +- Consistent shuffle does not depend on md5 anymore +- Speed improvements + +**0.1.1**: + +- Speed improvements +- Bug fixes + +**0.1.0**: + +- First commit diff --git a/LICENSE b/LICENSE index 05a36e9..5837872 100644 --- a/LICENSE +++ b/LICENSE @@ -1,22 +1,19 @@ -Copyright (c) 2012 Ivan Akimov +Copyright (c) 2012-2016 Ivan Akimov -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: +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 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. +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. diff --git a/README.md b/README.md index 9545342..833ed43 100644 --- a/README.md +++ b/README.md @@ -1,305 +1,165 @@ -![hashids](http://hashids.org/public/img/hashids-logo-normal.png "Hashids") +[![hashids](http://hashids.org/public/img/hashids.gif "Hashids")](http://hashids.org/) ====== +[![Build Status via Travis CI](https://travis-ci.org/ivanakimov/hashids.js.svg?branch=master)](https://travis-ci.org/ivanakimov/hashids.js) +[![NPM version](https://img.shields.io/npm/v/hashids.svg)](https://www.npmjs.com/package/hashids) +[![Coverage Status](https://coveralls.io/repos/ivanakimov/hashids.js/badge.svg?branch=master)](https://coveralls.io/r/ivanakimov/hashids.js?branch=master) +[![License](https://img.shields.io/packagist/l/hashids/hashids.svg?style=flat)](https://packagist.org/packages/hashids/hashids) +[![Join the chat at https://gitter.im/hashids/hashids](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/hashids/hashids?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -Full Documentation -------- - -A small JavaScript class to generate YouTube-like ids from one or many numbers. Use hashids when you do not want to expose your database ids to the user. Read full documentation at: [http://hashids.org/javascript](http://hashids.org/javascript) - -Node.js -------- - -If you are looking for a backend **Node.js version**, there's a separate repo: [https://github.com/ivanakimov/hashids.node.js](https://github.com/ivanakimov/hashids.node.js) - -Installation -------- - -1. Bower it up: [http://bower.io/](http://bower.io/) -2. Install: - - `bower install hashids` - -Or just: +**Hashids** is small JavaScript library to generate YouTube-like ids from numbers. Use it when you don't want to expose your database ids to the user: [http://hashids.org/javascript](http://hashids.org/javascript) - - -Updating from v0.3 to 1.0? +Getting started ------- -Read the `CHANGELOG` at the bottom of this readme! +Install Hashids via: -Usage -------- +- [node.js](): `npm install --save hashids` +- [bower](http://bower.io/): `bower install hashids` +- [jam](http://jamjs.org/): `jam install hashids` -#### Encoding one number +(or just use the code at `dist/hashids.js`) -You can pass a unique salt value so your hashids differ from everyone else's. I use "this is my salt" as an example. +Use in the browser (wherever ES5 is supported; 5KB): ```javascript + + ``` -`id` is now going to be: - - NkK9 - -#### Decoding - -Notice during decoding, same salt value is used: +Use in Node.js: ```javascript +var Hashids = require('hashids'); +var hashids = new Hashids(); -var Hashids = require("hashids"), - hashids = new Hashids("this is my salt"); - -var numbers = hashids.decode("NkK9"); +console.log(hashids.encode(1)); ``` -`numbers` is now going to be: - - [ 12345 ] - -#### Decoding with different salt - -Decoding will not work if salt is changed: +Quick example +------- ```javascript +var hashids = new Hashids(); -var Hashids = require("hashids"), - hashids = new Hashids("this is my pepper"); - -var numbers = hashids.decode("NkK9"); +var id = hashids.encode(1, 2, 3); // o2fXhV +var numbers = hashids.decode(id); // [1, 2, 3] ``` -`numbers` is now going to be: - - [] - -#### Encoding several numbers - -```javascript - -var Hashids = require("hashids"), - hashids = new Hashids("this is my salt"); - -var id = hashids.encode(683, 94108, 123, 5); -``` +More options +------- -`id` is now going to be: - - aBMswoO2UB3Sj - -You can also pass an array: +**Pass an array of numbers:** ```javascript +var hashids = new Hashids(); +var numbers = [1, 2, 3]; -var arr = [683, 94108, 123, 5]; -var id = hashids.encode(arr); +console.log(hashids.encode(numbers)); // o2fXhV ``` -#### Decoding is done the same way +**Make your ids unique:** -```javascript +Pass a project name to make your ids unique: -var Hashids = require("hashids"), - hashids = new Hashids("this is my salt"); +```javascript +var hashids = new Hashids('My Project'); +console.log(hashids.encode(1, 2, 3)); // Z4UrtW -var numbers = hashids.decode("aBMswoO2UB3Sj"); +var hashids = new Hashids('My Other Project'); +console.log(hashids.encode(1, 2, 3)); // gPUasb ``` -`numbers` is now going to be: - - [ 683, 94108, 123, 5 ] - -#### Encoding and specifying minimum id length +**Use padding to make your ids longer:** -Here we encode integer 1, and set the **minimum** id length to **8** (by default it's **0** -- meaning hashes will be the shortest possible length). +Note that ids are only padded to fit **at least** a certain length. It doesn't mean that your ids will be *exactly* that length. ```javascript +var hashids = new Hashids(); // no padding +console.log(hashids.encode(1)); // jR -var Hashids = require("hashids"), - hashids = new Hashids("this is my salt", 8); - -var id = hashids.encode(1); +var hashids = new Hashids('', 10); // pad to length 10 +console.log(hashids.encode(1)); // VolejRejNm ``` -`id` is now going to be: - - gB0NV05e - -#### Decoding +**Pass a custom alphabet:** ```javascript - -var Hashids = require("hashids"), - hashids = new Hashids("this is my salt", 8); - -var numbers = hashids.decode("gB0NV05e"); +var hashids = new Hashids('', 0, 'abcdefghijklmnopqrstuvwxyz'); // all lowercase +console.log(hashids.encode(1, 2, 3)); // mdfphx ``` -`numbers` is now going to be: - - [ 1 ] - -#### Specifying custom id alphabet +**Encode hex instead of numbers:** -Here we set the alphabet to consist of valid hex characters: "0123456789abcdef" +Useful if you want to encode [Mongo](https://www.mongodb.com/)'s ObjectIds. Note that *there is no limit* on how large of a hex number you can pass (it does not have to be Mongo's ObjectId). ```javascript +var hashids = new Hashids(); -var Hashids = require("hashids"), - hashids = new Hashids("this is my salt", 0, "0123456789abcdef"); - -var id = hashids.encode(1234567); +var id = hashids.encodeHex('507f1f77bcf86cd799439011'); // y42LW46J9luq3Xq9XMly +var hex = hashids.decodeHex(id); // 507f1f77bcf86cd799439011 ``` -`id` is now going to be: - - b332db5 - -Randomness +Pitfalls ------- -The primary purpose of hashids is to obfuscate ids. It's not meant or tested to be used for security purposes or compression. -Having said that, this algorithm does try to make these hashes unguessable and unpredictable: - -#### Repeating numbers +1. When decoding, output is always an array of numbers (even if you encode only one number): -```javascript + ```javascript + var hashids = new Hashids(); -var Hashids = require("hashids"), - hashids = new Hashids("this is my salt"); + var id = hashids.encode(1); + console.log(hashids.decode(id)); // [1] + ``` -var id = hashids.encode(5, 5, 5, 5); -``` +2. Encoding negative numbers is not supported. +3. Do not use this library as a security tool and do not encode sensitive data. This is **not** an encryption library. -You don't see any repeating patterns that might show there's 4 identical numbers in the hash: +Randomness +------- - 1Wc8cwcE +The primary purpose of Hashids is to obfuscate ids. It's not meant or tested to be used as a security or compression tool. Having said that, this algorithm does try to make these ids random and unpredictable: -Same with incremented numbers: +No repeating patterns showing there are 3 identical numbers in the id: ```javascript - -var Hashids = require("hashids"), - hashids = new Hashids("this is my salt"); - -var id = hashids.encode(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); +var hashids = new Hashids(); +console.log(hashids.encode(5, 5, 5)); // A6t1tQ ``` -`id` will be : - - kRHnurhptKcjIDTWC3sx - -#### Incrementing number ids: +Same with incremented numbers: ```javascript +var hashids = new Hashids(); -var Hashids = require("hashids"), - hashids = new Hashids("this is my salt"); +console.log(hashids.encode(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); // wpfLh9iwsqt0uyCEFjHM -var id1 = hashids.encode(1), /* NV */ - id2 = hashids.encode(2), /* 6m */ - id3 = hashids.encode(3), /* yD */ - id4 = hashids.encode(4), /* 2l */ - id5 = hashids.encode(5); /* rD */ +console.log(hashids.encode(1)); // jR +console.log(hashids.encode(2)); // k5 +console.log(hashids.encode(3)); // l5 +console.log(hashids.encode(4)); // mO +console.log(hashids.encode(5)); // nR ``` Curses! #$%@ ------- -This code was written with the intent of placing created hashes in visible places - like the URL. Which makes it unfortunate if generated hashes accidentally formed a bad word. - -Therefore, the algorithm tries to avoid generating most common English curse words. This is done by never placing the following letters next to each other: - - c, C, s, S, f, F, h, H, u, U, i, I, t, T - -Changelog -------- - -**1.0.2** - -- Support for older browsers (using `charAt`) by [@tauanz](https://github.com/tauanz): - -**1.0.1** - -- *require.js* support by [@nleclerc](https://github.com/nleclerc): - -**1.0.0** - -- Several public functions are renamed to be more appropriate: - - Function `encrypt()` changed to `encode()` - - Function `decrypt()` changed to `decode()` - - Function `encryptHex()` changed to `encodeHex()` - - Function `decryptHex()` changed to `decodeHex()` - - Hashids was designed to encode integers, primary ids at most. We've had several requests to encrypt sensitive data with Hashids and this is the wrong algorithm for that. So to encourage more appropriate use, `encrypt/decrypt` is being "downgraded" to `encode/decode`. +This code was written with the intent of placing created ids in visible places, like the URL. Therefore, the algorithm tries to avoid generating most common English curse words by generating ids that never have the following letters next to each other: -- Version tag added: `1.0` -- `README.md` updated + c, f, h, i, s, t, u -**0.3.0 - Current Stable** - -**PRODUCED HASHES IN THIS VERSION ARE DIFFERENT THAN IN 0.1.4, DO NOT UPDATE IF YOU NEED THEM TO KEEP WORKING:** - -- Same algorithm as [PHP](https://github.com/ivanakimov/hashids.php) and [Node.js](https://github.com/ivanakimov/hashids.node.js) versions now -- Overall approximately **4x** faster -- Consistent shuffle function uses slightly modified version of [Fisher–Yates algorithm](http://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm) -- Generate large hash strings faster (where _minHashLength_ is more than 1000 chars) -- When using _minHashLength_, hash character disorder has been improved -- Basic English curse words will now be avoided even with custom alphabet -- _encrypt_ function now also accepts array of integers as input -- Passing JSLint now -- Support for [Bower](http://bower.io/) package manager - -**0.1.4** - -- Global var leak for hashSplit (thanks to [@BryanDonovan](https://github.com/BryanDonovan)) -- Class capitalization (thanks to [@BryanDonovan](https://github.com/BryanDonovan)) - -**0.1.3** - - Warning: If you are using 0.1.2 or below, updating to this version will change your hashes. - -- Updated default alphabet (thanks to [@speps](https://github.com/speps)) -- Constructor removes duplicate characters for default alphabet as well (thanks to [@speps](https://github.com/speps)) - -**0.1.2** - - Warning: If you are using 0.1.1 or below, updating to this version will change your hashes. - -- Minimum hash length can now be specified -- Added more randomness to hashes -- Added unit tests -- Added example files -- Changed warnings that can be thrown -- Renamed `encode/decode` to `encrypt/decrypt` -- Consistent shuffle does not depend on md5 anymore -- Speed improvements - -**0.1.1** - -- Speed improvements -- Bug fixes - -**0.1.0** - -- First commit - -Contact +Support ------- -Follow me [@IvanAkimov](http://twitter.com/ivanakimov) - -Or [http://ivanakimov.com](http://ivanakimov.com) +Have a question? Ping me [@IvanAkimov](http://twitter.com/ivanakimov) or [ivanakimov.com](http://ivanakimov.com) License ------- -MIT License. See the `LICENSE` file. You can use Hashids in open source projects and commercial products. Don't break the Internet. Kthxbye. \ No newline at end of file +MIT License. See the `LICENSE` file. You can use Hashids in open source projects and commercial products. Don't break the Internet. Kthxbye. diff --git a/bower.json b/bower.json index 46045b1..968966b 100644 --- a/bower.json +++ b/bower.json @@ -1,12 +1,17 @@ { "name": "hashids", - "version": "1.0.2", - "main": "lib/hashids.min.js", + "description": "Generate YouTube-like ids from numbers. Use Hashids when you do not want to expose your database ids to the user.", + "main": "dist/hashids.js", + "homepage": "https://github.com/ivanakimov/hashids.js", + "license": "MIT", + "authors": [ + "Ivan Akimov (https://twitter.com/IvanAkimov)" + ], "ignore": [ "**/.*", "node_modules", "bower_components", - "test", - "tests" + "tests", + "package.json" ] -} \ No newline at end of file +} diff --git a/dist/hashids.js b/dist/hashids.js new file mode 100644 index 0000000..73137a8 --- /dev/null +++ b/dist/hashids.js @@ -0,0 +1,362 @@ +(function (global, factory) { + if (typeof define === "function" && define.amd) { + define(['module', 'exports'], factory); + } else if (typeof exports !== "undefined") { + factory(module, exports); + } else { + var mod = { + exports: {} + }; + factory(mod, mod.exports); + global.Hashids = mod.exports; + } +})(this, function (module, exports) { + 'use strict'; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } + } + + var _createClass = function () { + function defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } + } + + return function (Constructor, protoProps, staticProps) { + if (protoProps) defineProperties(Constructor.prototype, protoProps); + if (staticProps) defineProperties(Constructor, staticProps); + return Constructor; + }; + }(); + + var Hashids = function () { + function Hashids() { + var salt = arguments.length <= 0 || arguments[0] === undefined ? '' : arguments[0]; + var minLength = arguments.length <= 1 || arguments[1] === undefined ? 0 : arguments[1]; + var alphabet = arguments.length <= 2 || arguments[2] === undefined ? 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890' : arguments[2]; + + _classCallCheck(this, Hashids); + + var minAlphabetLength = 16; + var sepDiv = 3.5; + var guardDiv = 12; + + var errorAlphabetLength = 'error: alphabet must contain at least X unique characters'; + var errorAlphabetSpace = 'error: alphabet cannot contain spaces'; + + var uniqueAlphabet = '', + sepsLength = void 0, + diff = void 0; + + /* alphabet vars */ + + this.seps = 'cfhistuCFHISTU'; + this.minLength = parseInt(minLength, 10) > 0 ? minLength : 0; + this.salt = typeof salt === 'string' ? salt : ''; + + if (typeof alphabet === 'string') { + this.alphabet = alphabet; + } + + for (var i = 0; i !== this.alphabet.length; i++) { + if (uniqueAlphabet.indexOf(this.alphabet.charAt(i)) === -1) { + uniqueAlphabet += this.alphabet.charAt(i); + } + } + + this.alphabet = uniqueAlphabet; + + if (this.alphabet.length < minAlphabetLength) { + throw errorAlphabetLength.replace('X', minAlphabetLength); + } + + if (this.alphabet.search(' ') !== -1) { + throw errorAlphabetSpace; + } + + /* + `this.seps` should contain only characters present in `this.alphabet` + `this.alphabet` should not contains `this.seps` + */ + + for (var _i = 0; _i !== this.seps.length; _i++) { + + var j = this.alphabet.indexOf(this.seps.charAt(_i)); + if (j === -1) { + this.seps = this.seps.substr(0, _i) + ' ' + this.seps.substr(_i + 1); + } else { + this.alphabet = this.alphabet.substr(0, j) + ' ' + this.alphabet.substr(j + 1); + } + } + + this.alphabet = this.alphabet.replace(/ /g, ''); + + this.seps = this.seps.replace(/ /g, ''); + this.seps = this._shuffle(this.seps, this.salt); + + if (!this.seps.length || this.alphabet.length / this.seps.length > sepDiv) { + + sepsLength = Math.ceil(this.alphabet.length / sepDiv); + + if (sepsLength > this.seps.length) { + + diff = sepsLength - this.seps.length; + this.seps += this.alphabet.substr(0, diff); + this.alphabet = this.alphabet.substr(diff); + } + } + + this.alphabet = this._shuffle(this.alphabet, this.salt); + var guardCount = Math.ceil(this.alphabet.length / guardDiv); + + if (this.alphabet.length < 3) { + this.guards = this.seps.substr(0, guardCount); + this.seps = this.seps.substr(guardCount); + } else { + this.guards = this.alphabet.substr(0, guardCount); + this.alphabet = this.alphabet.substr(guardCount); + } + } + + _createClass(Hashids, [{ + key: 'encode', + value: function encode() { + for (var _len = arguments.length, numbers = Array(_len), _key = 0; _key < _len; _key++) { + numbers[_key] = arguments[_key]; + } + + var ret = ''; + + if (!numbers.length) { + return ret; + } + + if (numbers[0] instanceof Array) { + numbers = numbers[0]; + if (!numbers.length) { + return ret; + } + } + + for (var i = 0; i !== numbers.length; i++) { + if (typeof numbers[i] !== 'number' || numbers[i] % 1 !== 0 || numbers[i] < 0) { + return ret; + } + } + + return this._encode(numbers); + } + }, { + key: 'decode', + value: function decode(id) { + + var ret = []; + + if (!id || !id.length || typeof id !== 'string') { + return ret; + } + + return this._decode(id, this.alphabet); + } + }, { + key: 'encodeHex', + value: function encodeHex(hex) { + + hex = hex.toString(); + if (!/^[0-9a-fA-F]+$/.test(hex)) { + return ''; + } + + var numbers = hex.match(/[\w\W]{1,12}/g); + + for (var i = 0; i !== numbers.length; i++) { + numbers[i] = parseInt('1' + numbers[i], 16); + } + + return this.encode.apply(this, numbers); + } + }, { + key: 'decodeHex', + value: function decodeHex(id) { + + var ret = []; + + var numbers = this.decode(id); + + for (var i = 0; i !== numbers.length; i++) { + ret += numbers[i].toString(16).substr(1); + } + + return ret; + } + }, { + key: '_encode', + value: function _encode(numbers) { + + var ret = void 0, + alphabet = this.alphabet, + numbersIdInt = 0; + + for (var i = 0; i !== numbers.length; i++) { + numbersIdInt += numbers[i] % (i + 100); + } + + ret = alphabet.charAt(numbersIdInt % alphabet.length); + var lottery = ret; + + for (var _i2 = 0; _i2 !== numbers.length; _i2++) { + + var number = numbers[_i2]; + var buffer = lottery + this.salt + alphabet; + + alphabet = this._shuffle(alphabet, buffer.substr(0, alphabet.length)); + var last = this._toAlphabet(number, alphabet); + + ret += last; + + if (_i2 + 1 < numbers.length) { + number %= last.charCodeAt(0) + _i2; + var sepsIndex = number % this.seps.length; + ret += this.seps.charAt(sepsIndex); + } + } + + if (ret.length < this.minLength) { + + var guardIndex = (numbersIdInt + ret[0].charCodeAt(0)) % this.guards.length; + var guard = this.guards[guardIndex]; + + ret = guard + ret; + + if (ret.length < this.minLength) { + + guardIndex = (numbersIdInt + ret[2].charCodeAt(0)) % this.guards.length; + guard = this.guards[guardIndex]; + + ret += guard; + } + } + + var halfLength = parseInt(alphabet.length / 2, 10); + while (ret.length < this.minLength) { + + alphabet = this._shuffle(alphabet, alphabet); + ret = alphabet.substr(halfLength) + ret + alphabet.substr(0, halfLength); + + var excess = ret.length - this.minLength; + if (excess > 0) { + ret = ret.substr(excess / 2, this.minLength); + } + } + + return ret; + } + }, { + key: '_decode', + value: function _decode(id, alphabet) { + + var ret = [], + i = 0, + r = new RegExp('[' + this.guards + ']', 'g'), + idBreakdown = id.replace(r, ' '), + idArray = idBreakdown.split(' '); + + if (idArray.length === 3 || idArray.length === 2) { + i = 1; + } + + idBreakdown = idArray[i]; + if (typeof idBreakdown[0] !== 'undefined') { + + var lottery = idBreakdown[0]; + idBreakdown = idBreakdown.substr(1); + + r = new RegExp('[' + this.seps + ']', 'g'); + idBreakdown = idBreakdown.replace(r, ' '); + idArray = idBreakdown.split(' '); + + for (var j = 0; j !== idArray.length; j++) { + + var subId = idArray[j]; + var buffer = lottery + this.salt + alphabet; + + alphabet = this._shuffle(alphabet, buffer.substr(0, alphabet.length)); + ret.push(this._fromAlphabet(subId, alphabet)); + } + + if (this._encode(ret) !== id) { + ret = []; + } + } + + return ret; + } + }, { + key: '_shuffle', + value: function _shuffle(alphabet, salt) { + + var integer = void 0; + + if (!salt.length) { + return alphabet; + } + + for (var i = alphabet.length - 1, v = 0, p = 0, j = 0; i > 0; i--, v++) { + + v %= salt.length; + p += integer = salt.charAt(v).charCodeAt(0); + j = (integer + v + p) % i; + + var tmp = alphabet[j]; + alphabet = alphabet.substr(0, j) + alphabet.charAt(i) + alphabet.substr(j + 1); + alphabet = alphabet.substr(0, i) + tmp + alphabet.substr(i + 1); + } + + return alphabet; + } + }, { + key: '_toAlphabet', + value: function _toAlphabet(input, alphabet) { + + var id = ''; + + do { + id = alphabet.charAt(input % alphabet.length) + id; + input = parseInt(input / alphabet.length, 10); + } while (input); + + return id; + } + }, { + key: '_fromAlphabet', + value: function _fromAlphabet(input, alphabet) { + + var number = 0; + + for (var i = 0; i < input.length; i++) { + var pos = alphabet.indexOf(input[i]); + number += pos * Math.pow(alphabet.length, input.length - i - 1); + } + + return number; + } + }]); + + return Hashids; + }(); + + exports.default = Hashids; + module.exports = exports['default']; +}); diff --git a/dist/hashids.min.js b/dist/hashids.min.js new file mode 100644 index 0000000..edf41e6 --- /dev/null +++ b/dist/hashids.min.js @@ -0,0 +1,2 @@ +!function(t,e){if("function"==typeof define&&define.amd)define(["module","exports"],e);else if("undefined"!=typeof exports)e(module,exports);else{var s={exports:{}};e(s,s.exports),t.Hashids=s.exports}}(this,function(t,e){"use strict";function s(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(e,"__esModule",{value:!0});var h=function(){function t(t,e){for(var s=0;s0?h:0,this.salt="string"==typeof e?e:"","string"==typeof r&&(this.alphabet=r);for(var g=0;g!==this.alphabet.length;g++)o.indexOf(this.alphabet.charAt(g))===-1&&(o+=this.alphabet.charAt(g));if(this.alphabet=o,this.alphabet.lengthn)&&(p=Math.ceil(this.alphabet.length/n),p>this.seps.length&&(f=p-this.seps.length,this.seps+=this.alphabet.substr(0,f),this.alphabet=this.alphabet.substr(f))),this.alphabet=this._shuffle(this.alphabet,this.salt);var d=Math.ceil(this.alphabet.length/i);this.alphabet.length<3?(this.guards=this.seps.substr(0,d),this.seps=this.seps.substr(d)):(this.guards=this.alphabet.substr(0,d),this.alphabet=this.alphabet.substr(d))}return h(t,[{key:"encode",value:function(){for(var t=arguments.length,e=Array(t),s=0;s0&&(e=e.substr(c/2,this.minLength))}return e}},{key:"_decode",value:function(t,e){var s=[],h=0,r=new RegExp("["+this.guards+"]","g"),a=t.replace(r," "),n=a.split(" ");if(3!==n.length&&2!==n.length||(h=1),a=n[h],"undefined"!=typeof a[0]){var i=a[0];a=a.substr(1),r=new RegExp("["+this.seps+"]","g"),a=a.replace(r," "),n=a.split(" ");for(var l=0;l!==n.length;l++){var u=n[l],o=i+this.salt+e;e=this._shuffle(e,o.substr(0,e.length)),s.push(this._fromAlphabet(u,e))}this._encode(s)!==t&&(s=[])}return s}},{key:"_shuffle",value:function(t,e){var s=void 0;if(!e.length)return t;for(var h=t.length-1,r=0,a=0,n=0;h>0;h--,r++){r%=e.length,a+=s=e.charAt(r).charCodeAt(0),n=(s+r+a)%h;var i=t[n];t=t.substr(0,n)+t.charAt(h)+t.substr(n+1),t=t.substr(0,h)+i+t.substr(h+1)}return t}},{key:"_toAlphabet",value:function(t,e){var s="";do s=e.charAt(t%e.length)+s,t=parseInt(t/e.length,10);while(t);return s}},{key:"_fromAlphabet",value:function(t,e){for(var s=0,h=0;h 0 ? minLength : 0; + this.salt = (typeof salt === 'string') ? salt : ''; + + if (typeof alphabet === 'string') { + this.alphabet = alphabet; + } + + for (let i = 0; i !== this.alphabet.length; i++) { + if (uniqueAlphabet.indexOf(this.alphabet.charAt(i)) === -1) { + uniqueAlphabet += this.alphabet.charAt(i); + } + } + + this.alphabet = uniqueAlphabet; + + if (this.alphabet.length < minAlphabetLength) { + throw errorAlphabetLength.replace('X', minAlphabetLength); + } + + if (this.alphabet.search(' ') !== -1) { + throw errorAlphabetSpace; + } + + /* + `this.seps` should contain only characters present in `this.alphabet` + `this.alphabet` should not contains `this.seps` + */ + + for (let i = 0; i !== this.seps.length; i++) { + + const j = this.alphabet.indexOf(this.seps.charAt(i)); + if (j === -1) { + this.seps = this.seps.substr(0, i) + ' ' + this.seps.substr(i + 1); + } else { + this.alphabet = this.alphabet.substr(0, j) + ' ' + this.alphabet.substr(j + 1); + } + + } + + this.alphabet = this.alphabet.replace(/ /g, ''); + + this.seps = this.seps.replace(/ /g, ''); + this.seps = this._shuffle(this.seps, this.salt); + + if (!this.seps.length || (this.alphabet.length / this.seps.length) > sepDiv) { + + sepsLength = Math.ceil(this.alphabet.length / sepDiv); + + if (sepsLength > this.seps.length) { + + diff = sepsLength - this.seps.length; + this.seps += this.alphabet.substr(0, diff); + this.alphabet = this.alphabet.substr(diff); + + } + + } + + this.alphabet = this._shuffle(this.alphabet, this.salt); + const guardCount = Math.ceil(this.alphabet.length / guardDiv); + + if (this.alphabet.length < 3) { + this.guards = this.seps.substr(0, guardCount); + this.seps = this.seps.substr(guardCount); + } else { + this.guards = this.alphabet.substr(0, guardCount); + this.alphabet = this.alphabet.substr(guardCount); + } + + } + + encode(...numbers) { + + const ret = ''; + + if (!numbers.length) { + return ret; + } + + if (numbers[0] instanceof Array) { + numbers = numbers[0]; + if (!numbers.length) { + return ret; + } + } + + for (let i = 0; i !== numbers.length; i++) { + if (typeof numbers[i] !== 'number' || numbers[i] % 1 !== 0 || numbers[i] < 0) { + return ret; + } + } + + return this._encode(numbers); + + } + + decode(id) { + + const ret = []; + + if (!id || !id.length || typeof id !== 'string') { + return ret; + } + + return this._decode(id, this.alphabet); + + } + + encodeHex(hex) { + + hex = hex.toString(); + if (!/^[0-9a-fA-F]+$/.test(hex)) { + return ''; + } + + const numbers = hex.match(/[\w\W]{1,12}/g); + + for (let i = 0; i !== numbers.length; i++) { + numbers[i] = parseInt('1' + numbers[i], 16); + } + + return this.encode.apply(this, numbers); + + } + + decodeHex(id) { + + let ret = []; + + const numbers = this.decode(id); + + for (let i = 0; i !== numbers.length; i++) { + ret += (numbers[i]).toString(16).substr(1); + } + + return ret; + + } + + _encode(numbers) { + + let ret, + alphabet = this.alphabet, + numbersIdInt = 0; + + for (let i = 0; i !== numbers.length; i++) { + numbersIdInt += (numbers[i] % (i + 100)); + } + + ret = alphabet.charAt(numbersIdInt % alphabet.length); + const lottery = ret; + + for (let i = 0; i !== numbers.length; i++) { + + let number = numbers[i]; + const buffer = lottery + this.salt + alphabet; + + alphabet = this._shuffle(alphabet, buffer.substr(0, alphabet.length)); + const last = this._toAlphabet(number, alphabet); + + ret += last; + + if (i + 1 < numbers.length) { + number %= (last.charCodeAt(0) + i); + const sepsIndex = number % this.seps.length; + ret += this.seps.charAt(sepsIndex); + } + + } + + if (ret.length < this.minLength) { + + let guardIndex = (numbersIdInt + ret[0].charCodeAt(0)) % this.guards.length; + let guard = this.guards[guardIndex]; + + ret = guard + ret; + + if (ret.length < this.minLength) { + + guardIndex = (numbersIdInt + ret[2].charCodeAt(0)) % this.guards.length; + guard = this.guards[guardIndex]; + + ret += guard; + + } + + } + + const halfLength = parseInt(alphabet.length / 2, 10); + while (ret.length < this.minLength) { + + alphabet = this._shuffle(alphabet, alphabet); + ret = alphabet.substr(halfLength) + ret + alphabet.substr(0, halfLength); + + const excess = ret.length - this.minLength; + if (excess > 0) { + ret = ret.substr(excess / 2, this.minLength); + } + + } + + return ret; + + } + + _decode(id, alphabet) { + + let ret = [], i = 0, + r = new RegExp('[' + this.guards + ']', 'g'), + idBreakdown = id.replace(r, ' '), + idArray = idBreakdown.split(' '); + + if (idArray.length === 3 || idArray.length === 2) { + i = 1; + } + + idBreakdown = idArray[i]; + if (typeof idBreakdown[0] !== 'undefined') { + + const lottery = idBreakdown[0]; + idBreakdown = idBreakdown.substr(1); + + r = new RegExp('[' + this.seps + ']', 'g'); + idBreakdown = idBreakdown.replace(r, ' '); + idArray = idBreakdown.split(' '); + + for (let j = 0; j !== idArray.length; j++) { + + const subId = idArray[j]; + const buffer = lottery + this.salt + alphabet; + + alphabet = this._shuffle(alphabet, buffer.substr(0, alphabet.length)); + ret.push(this._fromAlphabet(subId, alphabet)); + + } + + if (this._encode(ret) !== id) { + ret = []; + } + + } + + return ret; + + } + + _shuffle(alphabet, salt) { + + let integer; + + if (!salt.length) { + return alphabet; + } + + for (let i = alphabet.length - 1, v = 0, p = 0, j = 0; i > 0; i--, v++) { + + v %= salt.length; + p += integer = salt.charAt(v).charCodeAt(0); + j = (integer + v + p) % i; + + const tmp = alphabet[j]; + alphabet = alphabet.substr(0, j) + alphabet.charAt(i) + alphabet.substr(j + 1); + alphabet = alphabet.substr(0, i) + tmp + alphabet.substr(i + 1); + + } + + return alphabet; + + } + + _toAlphabet(input, alphabet) { + + let id = ''; + + do { + id = alphabet.charAt(input % alphabet.length) + id; + input = parseInt(input / alphabet.length, 10); + } while (input); + + return id; + + } + + _fromAlphabet(input, alphabet) { + + let number = 0; + + for (let i = 0; i < input.length; i++) { + const pos = alphabet.indexOf(input[i]); + number += pos * Math.pow(alphabet.length, input.length - i - 1); + } + + return number; + + } + +} diff --git a/lib/hashids.js b/lib/hashids.js deleted file mode 100644 index fc214d8..0000000 --- a/lib/hashids.js +++ /dev/null @@ -1,352 +0,0 @@ - -/* - - Hashids - http://hashids.org/javascript - (c) 2013 Ivan Akimov - - https://github.com/ivanakimov/hashids.js - hashids may be freely distributed under the MIT license. - -*/ - -/*jslint plusplus: true, nomen: true, browser: true */ -/*global define */ - -var Hashids = (function () { - - "use strict"; - - function Hashids(salt, minHashLength, alphabet) { - - var uniqueAlphabet, i, j, len, sepsLength, diff, guardCount; - - this.version = "1.0.2"; - - /* internal settings */ - - this.minAlphabetLength = 16; - this.sepDiv = 3.5; - this.guardDiv = 12; - - /* error messages */ - - this.errorAlphabetLength = "error: alphabet must contain at least X unique characters"; - this.errorAlphabetSpace = "error: alphabet cannot contain spaces"; - - /* alphabet vars */ - - this.alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; - this.seps = "cfhistuCFHISTU"; - this.minHashLength = parseInt(minHashLength, 10) > 0 ? minHashLength : 0; - this.salt = (typeof salt === "string") ? salt : ""; - - if (typeof alphabet === "string") { - this.alphabet = alphabet; - } - - for (uniqueAlphabet = "", i = 0, len = this.alphabet.length; i !== len; i++) { - if (uniqueAlphabet.indexOf(this.alphabet.charAt(i)) === -1) { - uniqueAlphabet += this.alphabet.charAt(i); - } - } - - this.alphabet = uniqueAlphabet; - - if (this.alphabet.length < this.minAlphabetLength) { - throw this.errorAlphabetLength.replace("X", this.minAlphabetLength); - } - - if (this.alphabet.search(" ") !== -1) { - throw this.errorAlphabetSpace; - } - - /* seps should contain only characters present in alphabet; alphabet should not contains seps */ - - for (i = 0, len = this.seps.length; i !== len; i++) { - - j = this.alphabet.indexOf(this.seps.charAt(i)); - if (j === -1) { - this.seps = this.seps.substr(0, i) + " " + this.seps.substr(i + 1); - } else { - this.alphabet = this.alphabet.substr(0, j) + " " + this.alphabet.substr(j + 1); - } - - } - - this.alphabet = this.alphabet.replace(/ /g, ""); - - this.seps = this.seps.replace(/ /g, ""); - this.seps = this.consistentShuffle(this.seps, this.salt); - - if (!this.seps.length || (this.alphabet.length / this.seps.length) > this.sepDiv) { - - sepsLength = Math.ceil(this.alphabet.length / this.sepDiv); - - if (sepsLength === 1) { - sepsLength++; - } - - if (sepsLength > this.seps.length) { - - diff = sepsLength - this.seps.length; - this.seps += this.alphabet.substr(0, diff); - this.alphabet = this.alphabet.substr(diff); - - } else { - this.seps = this.seps.substr(0, sepsLength); - } - - } - - this.alphabet = this.consistentShuffle(this.alphabet, this.salt); - guardCount = Math.ceil(this.alphabet.length / this.guardDiv); - - if (this.alphabet.length < 3) { - this.guards = this.seps.substr(0, guardCount); - this.seps = this.seps.substr(guardCount); - } else { - this.guards = this.alphabet.substr(0, guardCount); - this.alphabet = this.alphabet.substr(guardCount); - } - - } - - Hashids.prototype.encode = function () { - - var ret = "", i, len, - numbers = Array.prototype.slice.call(arguments); - - if (!numbers.length) { - return ret; - } - - if (numbers[0] instanceof Array) { - numbers = numbers[0]; - } - - for (i = 0, len = numbers.length; i !== len; i++) { - if (typeof numbers[i] !== "number" || numbers[i] % 1 !== 0 || numbers[i] < 0) { - return ret; - } - } - - return this._encode(numbers); - - }; - - Hashids.prototype.decode = function (hash) { - - var ret = []; - - if (!hash.length || typeof hash !== "string") { - return ret; - } - - return this._decode(hash, this.alphabet); - - }; - - Hashids.prototype.encodeHex = function (str) { - - var i, len, numbers; - - str = str.toString(); - if (!/^[0-9a-fA-F]+$/.test(str)) { - return ""; - } - - numbers = str.match(/[\w\W]{1,12}/g); - - for (i = 0, len = numbers.length; i !== len; i++) { - numbers[i] = parseInt("1" + numbers[i], 16); - } - - return this.encode.apply(this, numbers); - - }; - - Hashids.prototype.decodeHex = function (hash) { - - var ret = [], i, len, - numbers = this.decode(hash); - - for (i = 0, len = numbers.length; i !== len; i++) { - ret += (numbers[i]).toString(16).substr(1); - } - - return ret; - - }; - - Hashids.prototype._encode = function (numbers) { - - var ret, lottery, i, len, number, buffer, last, sepsIndex, guardIndex, guard, halfLength, excess, - alphabet = this.alphabet, - numbersSize = numbers.length, - numbersHashInt = 0; - - for (i = 0, len = numbers.length; i !== len; i++) { - numbersHashInt += (numbers[i] % (i + 100)); - } - - lottery = ret = alphabet.charAt(numbersHashInt % alphabet.length); - for (i = 0, len = numbers.length; i !== len; i++) { - - number = numbers[i]; - buffer = lottery + this.salt + alphabet; - - alphabet = this.consistentShuffle(alphabet, buffer.substr(0, alphabet.length)); - last = this.hash(number, alphabet); - - ret += last; - - if (i + 1 < numbersSize) { - number %= (last.charCodeAt(0) + i); - sepsIndex = number % this.seps.length; - ret += this.seps.charAt(sepsIndex); - } - - } - - if (ret.length < this.minHashLength) { - - guardIndex = (numbersHashInt + ret[0].charCodeAt(0)) % this.guards.length; - guard = this.guards[guardIndex]; - - ret = guard + ret; - - if (ret.length < this.minHashLength) { - - guardIndex = (numbersHashInt + ret[2].charCodeAt(0)) % this.guards.length; - guard = this.guards[guardIndex]; - - ret += guard; - - } - - } - - halfLength = parseInt(alphabet.length / 2, 10); - while (ret.length < this.minHashLength) { - - alphabet = this.consistentShuffle(alphabet, alphabet); - ret = alphabet.substr(halfLength) + ret + alphabet.substr(0, halfLength); - - excess = ret.length - this.minHashLength; - if (excess > 0) { - ret = ret.substr(excess / 2, this.minHashLength); - } - - } - - return ret; - - }; - - Hashids.prototype._decode = function (hash, alphabet) { - - var ret = [], i = 0, - lottery, len, subHash, buffer, - r = new RegExp("[" + this.guards + "]", "g"), - hashBreakdown = hash.replace(r, " "), - hashArray = hashBreakdown.split(" "); - - if (hashArray.length === 3 || hashArray.length === 2) { - i = 1; - } - - hashBreakdown = hashArray[i]; - if (typeof hashBreakdown[0] !== "undefined") { - - lottery = hashBreakdown[0]; - hashBreakdown = hashBreakdown.substr(1); - - r = new RegExp("[" + this.seps + "]", "g"); - hashBreakdown = hashBreakdown.replace(r, " "); - hashArray = hashBreakdown.split(" "); - - for (i = 0, len = hashArray.length; i !== len; i++) { - - subHash = hashArray[i]; - buffer = lottery + this.salt + alphabet; - - alphabet = this.consistentShuffle(alphabet, buffer.substr(0, alphabet.length)); - ret.push(this.unhash(subHash, alphabet)); - - } - - if (this._encode(ret) !== hash) { - ret = []; - } - - } - - return ret; - - }; - - Hashids.prototype.consistentShuffle = function (alphabet, salt) { - - var integer, j, temp, i, v, p; - - if (!salt.length) { - return alphabet; - } - - for (i = alphabet.length - 1, v = 0, p = 0; i > 0; i--, v++) { - - v %= salt.length; - p += integer = salt.charAt(v).charCodeAt(0); - j = (integer + v + p) % i; - - temp = alphabet.charAt(j); - alphabet = alphabet.substr(0, j) + alphabet.charAt(i) + alphabet.substr(j + 1); - alphabet = alphabet.substr(0, i) + temp + alphabet.substr(i + 1); - - } - - return alphabet; - - }; - - Hashids.prototype.hash = function (input, alphabet) { - - var hash = "", - alphabetLength = alphabet.length; - - do { - hash = alphabet.charAt(input % alphabetLength) + hash; - input = parseInt(input / alphabetLength, 10); - } while (input); - - return hash; - - }; - - Hashids.prototype.unhash = function (input, alphabet) { - - var number = 0, pos, i; - - for (i = 0; i < input.length; i++) { - pos = alphabet.indexOf(input[i]); - number += pos * Math.pow(alphabet.length, input.length - i - 1); - } - - return number; - - }; - - /* require.js bit */ - - if (typeof define === "function" && typeof define.amd === "object" && define.amd) { - - define(function () { - return Hashids; - }); - - } - - return Hashids; - -}()); diff --git a/lib/hashids.min.js b/lib/hashids.min.js deleted file mode 100644 index 96be080..0000000 --- a/lib/hashids.min.js +++ /dev/null @@ -1 +0,0 @@ -var Hashids=function(){"use strict";function Hashids(salt,minHashLength,alphabet){var uniqueAlphabet,i,j,len,sepsLength,diff,guardCount;this.version="1.0.2";this.minAlphabetLength=16;this.sepDiv=3.5;this.guardDiv=12;this.errorAlphabetLength="error: alphabet must contain at least X unique characters";this.errorAlphabetSpace="error: alphabet cannot contain spaces";this.alphabet="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";this.seps="cfhistuCFHISTU";this.minHashLength=parseInt(minHashLength,10)>0?minHashLength:0;this.salt=typeof salt==="string"?salt:"";if(typeof alphabet==="string"){this.alphabet=alphabet}for(uniqueAlphabet="",i=0,len=this.alphabet.length;i!==len;i++){if(uniqueAlphabet.indexOf(this.alphabet.charAt(i))===-1){uniqueAlphabet+=this.alphabet.charAt(i)}}this.alphabet=uniqueAlphabet;if(this.alphabet.lengththis.sepDiv){sepsLength=Math.ceil(this.alphabet.length/this.sepDiv);if(sepsLength===1){sepsLength++}if(sepsLength>this.seps.length){diff=sepsLength-this.seps.length;this.seps+=this.alphabet.substr(0,diff);this.alphabet=this.alphabet.substr(diff)}else{this.seps=this.seps.substr(0,sepsLength)}}this.alphabet=this.consistentShuffle(this.alphabet,this.salt);guardCount=Math.ceil(this.alphabet.length/this.guardDiv);if(this.alphabet.length<3){this.guards=this.seps.substr(0,guardCount);this.seps=this.seps.substr(guardCount)}else{this.guards=this.alphabet.substr(0,guardCount);this.alphabet=this.alphabet.substr(guardCount)}}Hashids.prototype.encode=function(){var ret="",i,len,numbers=Array.prototype.slice.call(arguments);if(!numbers.length){return ret}if(numbers[0]instanceof Array){numbers=numbers[0]}for(i=0,len=numbers.length;i!==len;i++){if(typeof numbers[i]!=="number"||numbers[i]%1!==0||numbers[i]<0){return ret}}return this._encode(numbers)};Hashids.prototype.decode=function(hash){var ret=[];if(!hash.length||typeof hash!=="string"){return ret}return this._decode(hash,this.alphabet)};Hashids.prototype.encodeHex=function(str){var i,len,numbers;str=str.toString();if(!/^[0-9a-fA-F]+$/.test(str)){return""}numbers=str.match(/[\w\W]{1,12}/g);for(i=0,len=numbers.length;i!==len;i++){numbers[i]=parseInt("1"+numbers[i],16)}return this.encode.apply(this,numbers)};Hashids.prototype.decodeHex=function(hash){var ret=[],i,len,numbers=this.decode(hash);for(i=0,len=numbers.length;i!==len;i++){ret+=numbers[i].toString(16).substr(1)}return ret};Hashids.prototype._encode=function(numbers){var ret,lottery,i,len,number,buffer,last,sepsIndex,guardIndex,guard,halfLength,excess,alphabet=this.alphabet,numbersSize=numbers.length,numbersHashInt=0;for(i=0,len=numbers.length;i!==len;i++){numbersHashInt+=numbers[i]%(i+100)}lottery=ret=alphabet.charAt(numbersHashInt%alphabet.length);for(i=0,len=numbers.length;i!==len;i++){number=numbers[i];buffer=lottery+this.salt+alphabet;alphabet=this.consistentShuffle(alphabet,buffer.substr(0,alphabet.length));last=this.hash(number,alphabet);ret+=last;if(i+10){ret=ret.substr(excess/2,this.minHashLength)}}return ret};Hashids.prototype._decode=function(hash,alphabet){var ret=[],i=0,lottery,len,subHash,buffer,r=new RegExp("["+this.guards+"]","g"),hashBreakdown=hash.replace(r," "),hashArray=hashBreakdown.split(" ");if(hashArray.length===3||hashArray.length===2){i=1}hashBreakdown=hashArray[i];if(typeof hashBreakdown[0]!=="undefined"){lottery=hashBreakdown[0];hashBreakdown=hashBreakdown.substr(1);r=new RegExp("["+this.seps+"]","g");hashBreakdown=hashBreakdown.replace(r," ");hashArray=hashBreakdown.split(" ");for(i=0,len=hashArray.length;i!==len;i++){subHash=hashArray[i];buffer=lottery+this.salt+alphabet;alphabet=this.consistentShuffle(alphabet,buffer.substr(0,alphabet.length));ret.push(this.unhash(subHash,alphabet))}if(this._encode(ret)!==hash){ret=[]}}return ret};Hashids.prototype.consistentShuffle=function(alphabet,salt){var integer,j,temp,i,v,p;if(!salt.length){return alphabet}for(i=alphabet.length-1,v=0,p=0;i>0;i--,v++){v%=salt.length;p+=integer=salt.charAt(v).charCodeAt(0);j=(integer+v+p)%i;temp=alphabet.charAt(j);alphabet=alphabet.substr(0,j)+alphabet.charAt(i)+alphabet.substr(j+1);alphabet=alphabet.substr(0,i)+temp+alphabet.substr(i+1)}return alphabet};Hashids.prototype.hash=function(input,alphabet){var hash="",alphabetLength=alphabet.length;do{hash=alphabet.charAt(input%alphabetLength)+hash;input=parseInt(input/alphabetLength,10)}while(input);return hash};Hashids.prototype.unhash=function(input,alphabet){var number=0,pos,i;for(i=0;i (https://twitter.com/IvanAkimov)", + "name": "hashids", + "description": "Generate YouTube-like ids from numbers. Use Hashids when you do not want to expose your database ids to the user.", + "version": "1.1.0", + "homepage": "http://hashids.org/javascript", + "repository": { + "type": "git", + "url": "https://github.com/ivanakimov/hashids.js" + }, + "bugs": { + "url": "https://github.com/ivanakimov/issues" + }, + "main": "dist/hashids.js", + "scripts": { + "lint": "eslint hashids.js tests", + "test": "mocha tests --compilers js:babel-core/register", + "coverage": "nyc npm test && nyc report --reporter=text-lcov | coveralls", + "build:node": "babel hashids.js -o dist/hashids.js", + "minify": "uglifyjs dist/hashids.js -o dist/hashids.min.js --source-map dist/hashids.min.map --compress --mangle", + "build": "npm run build:node && npm run minify", + "clean": "rm -rf coverage .nyc_output npm-debug.log", + "all": "npm run lint && npm run coverage && npm run build && npm run clean" + }, + "dependencies": {}, + "devDependencies": { + "babel-cli": "^6.10.1", + "babel-core": "^6.10.4", + "babel-plugin-add-module-exports": "^0.2.1", + "babel-plugin-rename-umd-globals": "0.0.2", + "babel-plugin-transform-es2015-modules-umd": "^6.8.0", + "babel-preset-es2015": "^6.9.0", + "chai": "^3.5.0", + "coveralls": "^2.11.9", + "eslint": "^3.0.1", + "mocha": "^2.5.3", + "nyc": "^7.0.0", + "uglify-js": "^2.7.0" + }, + "license": "MIT", + "keywords": [ + "hashids", + "hashid", + "hash", + "ids", + "youtube", + "bitly", + "obfuscate", + "encode", + "decode", + "encrypt", + "decrypt" + ] +} diff --git a/tests/bad-input.js b/tests/bad-input.js new file mode 100644 index 0000000..aee0e50 --- /dev/null +++ b/tests/bad-input.js @@ -0,0 +1,61 @@ + +import Hashids from '../hashids'; +import { assert } from 'chai'; + +const hashids = new Hashids(); + +describe('bad input', () => { + + it(`should throw an error when small alphabet`, () => { + assert.throws(() => { + const hashidsIgnored = new Hashids('', 0, '1234567890'); + }); + }); + + it(`should throw an error when alphabet has spaces`, () => { + assert.throws(() => { + const hashidsIgnored = new Hashids('', 0, 'a cdefghijklmnopqrstuvwxyz'); + }); + }); + + it(`should return an empty string when encoding nothing`, () => { + const id = hashids.encode(); + assert.equal(id, ''); + }); + + it(`should return an empty string when encoding an empty array`, () => { + const id = hashids.encode([]); + assert.equal(id, ''); + }); + + it(`should return an empty string when encoding an array with non-numeric input`, () => { + const id = hashids.encode(['z']); + assert.equal(id, ''); + }); + + it(`should return an empty array when decoding nothing`, () => { + const numbers = hashids.decode(); + assert.deepEqual(numbers, []); + }); + + it(`should return an empty string when encoding non-numeric input`, () => { + const id = hashids.encode('z'); + assert.equal(id, ''); + }); + + it(`should return an empty array when decoding invalid id`, () => { + const numbers = hashids.decode('f'); + assert.deepEqual(numbers, []); + }); + + it(`should return an empty string when encoding non-hex input`, () => { + const id = hashids.encodeHex('z'); + assert.equal(id, ''); + }); + + it(`should return an empty array when hex-decoding invalid id`, () => { + const numbers = hashids.decodeHex('f'); + assert.deepEqual(numbers, []); + }); + +}); diff --git a/tests/custom-alphabet.js b/tests/custom-alphabet.js new file mode 100644 index 0000000..dce6471 --- /dev/null +++ b/tests/custom-alphabet.js @@ -0,0 +1,43 @@ + +import Hashids from '../hashids'; +import { assert } from 'chai'; + +describe('custom alphabet', () => { + + const testAlphabet = (alphabet) => { + + const hashids = new Hashids('', 0, alphabet); + const numbers = [1, 2, 3]; + + const id = hashids.encode(numbers); + const decodedNumbers = hashids.decode(id); + + assert.deepEqual(decodedNumbers, numbers); + + }; + + it(`should work with the worst alphabet`, () => { + testAlphabet('cCsSfFhHuUiItT01'); + }); + + it(`should work with half the alphabet being separators`, () => { + testAlphabet('abdegjklCFHISTUc'); + }); + + it(`should work with exactly 2 separators`, () => { + testAlphabet('abdegjklmnopqrSF'); + }); + + it(`should work with no separators`, () => { + testAlphabet('abdegjklmnopqrvwxyzABDEGJKLMNOPQRVWXYZ1234567890'); + }); + + it(`should work with super long alphabet`, () => { + testAlphabet('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890`~!@#$%^&*()-_=+\\|\'";:/?.>,<{[}]'); + }); + + it(`should work with a weird alphabet`, () => { + testAlphabet('`~!@#$%^&*()-_=+\\|\'";:/?.>,<{[}]'); + }); + +}); diff --git a/tests/custom-params-hex.js b/tests/custom-params-hex.js new file mode 100644 index 0000000..fb6d25d --- /dev/null +++ b/tests/custom-params-hex.js @@ -0,0 +1,44 @@ + +import Hashids from '../hashids'; +import { assert } from 'chai'; + +const minLength = 30; +const hashids = new Hashids('this is my salt', minLength, 'xzal86grmb4jhysfoqp3we7291kuct5iv0nd'); + +const map = { + '0dbq3jwa8p4b3gk6gb8bv21goerm96': 'deadbeef', + '190obdnk4j02pajjdande7aqj628mr': 'abcdef123456', + 'a1nvl5d9m3yo8pj1fqag8p9pqw4dyl': 'ABCDDD6666DDEEEEEEEEE', + '1nvlml93k3066oas3l9lr1wn1k67dy': '507f1f77bcf86cd799439011', + 'mgyband33ye3c6jj16yq1jayh6krqjbo': 'f00000fddddddeeeee4444444ababab', + '9mnwgllqg1q2tdo63yya35a9ukgl6bbn6qn8': 'abcdef123456abcdef123456abcdef123456', + 'edjrkn9m6o69s0ewnq5lqanqsmk6loayorlohwd963r53e63xmml29': 'f000000000000000000000000000000000000000000000000000f', + 'grekpy53r2pjxwyjkl9aw0k3t5la1b8d5r1ex9bgeqmy93eata0eq0': 'fffffffffffffffffffffffffffffffffffffffffffffffffffff' +}; + +describe('encodeHex/decodeHex using default params', () => { + + for (const id in map) { + + const hex = map[id]; + + it(`should encode '0x${hex.toUpperCase()}' to '${id}'`, () => { + assert.equal(id, hashids.encodeHex(hex)); + }); + + it(`should encode '0x${hex.toUpperCase()}' to '${id}' and decode back correctly`, () => { + + const encodedId = hashids.encodeHex(hex); + const decodedHex = hashids.decodeHex(encodedId); + + assert.deepEqual(hex.toLowerCase(), decodedHex); + + }); + + it(`id length should be at least ${minLength}`, () => { + assert.isAtLeast(hashids.encodeHex(hex).length, minLength); + }); + + } + +}); diff --git a/tests/custom-params.js b/tests/custom-params.js new file mode 100644 index 0000000..3344a96 --- /dev/null +++ b/tests/custom-params.js @@ -0,0 +1,53 @@ + +import Hashids from '../hashids'; +import { assert } from 'chai'; + +const minLength = 30; +const hashids = new Hashids('this is my salt', minLength, 'xzal86grmb4jhysfoqp3we7291kuct5iv0nd'); + +const map = { + 'dw1nqdp92yrajvl9v6k3gl5mb0o8ea': [1], + 'onqr0bk58p642wldq14djmw21ygl39': [928728], + '18apy3wlqkjvd5h1id7mn5ore2d06b': [1, 2, 3], + 'o60edky1ng3vl9hbfavwr5pa2q8mb9': [1, 0, 0], + 'o60edky1ng3vlqfbfp4wr5pa2q8mb9': [0, 0, 1], + 'qek2a08gpl575efrfd7yomj9dwbr63': [0, 0, 0], + 'm3d5a6yn875rae8y81a94gr9kbwpqo': [1000000000000], + '1q3y98ln48w96kpo0wgk314w5mak2d': [9007199254740991], + 'op7qrcdc3cgc2c0cbcrcoc5clce4d6': [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5], + '5430bd2jo0lxyfkfjfyojej5adqdy4': [10000000000, 0, 0, 0, 999999999999999], + 'aa5kow86ano1pt3e1aqm239awkt9pk380w9l3q6': [9007199254740991, 9007199254740991, 9007199254740991], + 'mmmykr5nuaabgwnohmml6dakt00jmo3ainnpy2mk': [1000000001, 1000000002, 1000000003, 1000000004, 1000000005], + 'w1hwinuwt1cbs6xwzafmhdinuotpcosrxaz0fahl': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] +}; + +describe('encode/decode using custom params', () => { + + for (const id in map) { + + const numbers = map[id]; + + it(`should encode [${numbers}] to '${id}' (passing array of numbers)`, () => { + assert.equal(id, hashids.encode(numbers)); + }); + + it(`should encode [${numbers}] to '${id}' (passing numbers)`, () => { + assert.equal(id, hashids.encode.apply(hashids, numbers)); + }); + + it(`should encode [${numbers}] to '${id}' and decode back correctly`, () => { + + const encodedId = hashids.encode(numbers); + const decodedNumbers = hashids.decode(encodedId); + + assert.deepEqual(numbers, decodedNumbers); + + }); + + it(`id length should be at least ${minLength}`, () => { + assert.isAtLeast(hashids.encode(numbers).length, minLength); + }); + + } + +}); diff --git a/tests/custom-salt.js b/tests/custom-salt.js new file mode 100644 index 0000000..d6d7a4b --- /dev/null +++ b/tests/custom-salt.js @@ -0,0 +1,39 @@ + +import Hashids from '../hashids'; +import { assert } from 'chai'; + +describe('custom salt', () => { + + const testSalt = (salt) => { + + const hashids = new Hashids(salt); + const numbers = [1, 2, 3]; + + const id = hashids.encode(numbers); + const decodedNumbers = hashids.decode(id); + + assert.deepEqual(decodedNumbers, numbers); + + }; + + it(`should work with ''`, () => { + testSalt(''); + }); + + it(`should work with ' '`, () => { + testSalt(' '); + }); + + it(`should work with 'this is my salt'`, () => { + testSalt('this is my salt'); + }); + + it(`should work with a really long salt`, () => { + testSalt('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890`~!@#$%^&*()-_=+\\|\'";:/?.>,<{[}]'); + }); + + it(`should work with a weird salt`, () => { + testSalt('`~!@#$%^&*()-_=+\\|\'";:/?.>,<{[}]'); + }); + +}); diff --git a/tests/default-params-hex.js b/tests/default-params-hex.js new file mode 100644 index 0000000..c25b3e3 --- /dev/null +++ b/tests/default-params-hex.js @@ -0,0 +1,39 @@ + +import Hashids from '../hashids'; +import { assert } from 'chai'; + +const hashids = new Hashids(); + +const map = { + 'wpVL4j9g': 'deadbeef', + 'kmP69lB3xv': 'abcdef123456', + '47JWg0kv4VU0G2KBO2': 'ABCDDD6666DDEEEEEEEEE', + 'y42LW46J9luq3Xq9XMly': '507f1f77bcf86cd799439011', + 'm1rO8xBQNquXmLvmO65BUO9KQmj': 'f00000fddddddeeeee4444444ababab', + 'wBlnMA23NLIQDgw7XxErc2mlNyAjpw': 'abcdef123456abcdef123456abcdef123456', + 'VwLAoD9BqlT7xn4ZnBXJFmGZ51ZqrBhqrymEyvYLIP199': 'f000000000000000000000000000000000000000000000000000f', + 'nBrz1rYyV0C0XKNXxB54fWN0yNvVjlip7127Jo3ri0Pqw': 'fffffffffffffffffffffffffffffffffffffffffffffffffffff' +}; + +describe('encodeHex/decodeHex using default params', () => { + + for (const id in map) { + + const hex = map[id]; + + it(`should encode '0x${hex.toUpperCase()}' to '${id}'`, () => { + assert.equal(id, hashids.encodeHex(hex)); + }); + + it(`should encode '0x${hex.toUpperCase()}' to '${id}' and decode back correctly`, () => { + + const encodedId = hashids.encodeHex(hex); + const decodedHex = hashids.decodeHex(encodedId); + + assert.deepEqual(hex.toLowerCase(), decodedHex); + + }); + + } + +}); diff --git a/tests/default-params.js b/tests/default-params.js new file mode 100644 index 0000000..874ccda --- /dev/null +++ b/tests/default-params.js @@ -0,0 +1,48 @@ + +import Hashids from '../hashids'; +import { assert } from 'chai'; + +const hashids = new Hashids(); + +const map = { + 'jR': [1], + 'R8ZN0': [928728], + 'o2fXhV': [1, 2, 3], + 'jRfMcP': [1, 0, 0], + 'jQcMcW': [0, 0, 1], + 'gYcxcr': [0, 0, 0], + 'gLpmopgO6': [1000000000000], + 'lEW77X7g527': [9007199254740991], + 'BrtltWt2tyt1tvt7tJt2t1tD': [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5], + 'G6XOnGQgIpcVcXcqZ4B8Q8B9y': [10000000000, 0, 0, 0, 999999999999999], + '5KoLLVL49RLhYkppOplM6piwWNNANny8N': [9007199254740991, 9007199254740991, 9007199254740991], + 'BPg3Qx5f8VrvQkS16wpmwIgj9Q4Jsr93gqx': [1000000001, 1000000002, 1000000003, 1000000004, 1000000005], + '1wfphpilsMtNumCRFRHXIDSqT2UPcWf1hZi3s7tN': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] +}; + +describe('encode/decode using default params', () => { + + for (const id in map) { + + const numbers = map[id]; + + it(`should encode [${numbers}] to '${id}' (passing array of numbers)`, () => { + assert.equal(id, hashids.encode(numbers)); + }); + + it(`should encode [${numbers}] to '${id}' (passing numbers)`, () => { + assert.equal(id, hashids.encode.apply(hashids, numbers)); + }); + + it(`should encode [${numbers}] to '${id}' and decode back correctly`, () => { + + const encodedId = hashids.encode(numbers); + const decodedNumbers = hashids.decode(encodedId); + + assert.deepEqual(numbers, decodedNumbers); + + }); + + } + +}); diff --git a/tests/min-length.js b/tests/min-length.js new file mode 100644 index 0000000..51bda51 --- /dev/null +++ b/tests/min-length.js @@ -0,0 +1,40 @@ + +import Hashids from '../hashids'; +import { assert } from 'chai'; + +describe('min length', () => { + + const testMinLength = (minLength) => { + + const hashids = new Hashids('', minLength); + const numbers = [1, 2, 3]; + + const id = hashids.encode(numbers); + const decodedNumbers = hashids.decode(id); + + assert.deepEqual(decodedNumbers, numbers); + assert.isAtLeast(id.length, minLength); + + }; + + it(`should work when 0`, () => { + testMinLength(0); + }); + + it(`should work when 1`, () => { + testMinLength(1); + }); + + it(`should work when 10`, () => { + testMinLength(10); + }); + + it(`should work when 999`, () => { + testMinLength(999); + }); + + it(`should work when 1000`, () => { + testMinLength(1000); + }); + +});