Skip to content

Commit

Permalink
Add support for async generation
Browse files Browse the repository at this point in the history
  • Loading branch information
Harley Nuss committed Mar 24, 2017
1 parent f504e85 commit 05c6d66
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 38 deletions.
32 changes: 20 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,24 +33,32 @@ randomstring.generate({
charset: 'abc'
});
// >> "accbaabbbbcccbccccaacacbbcbbcbbc"

randomstring.generate({
charset: 'abc'
}, cb);
// >> "cb(generatedString) {}"

```

## API

`randomstring.`

- `generate(options)`
- `length` - the length of the random string. (default: 32) [OPTIONAL]
- `readable` - exclude poorly readable chars: 0OIl. (default: false) [OPTIONAL]
- `charset` - define the character set for the string. (default: 'alphanumeric') [OPTIONAL]
- `alphanumeric` - [0-9 a-z A-Z]
- `alphabetic` - [a-z A-Z]
- `numeric` - [0-9]
- `hex` - [0-9 a-f]
- `custom` - any given characters
- `capitalization` - define whether the output should be lowercase / uppercase only. (default: null) [OPTIONAL]
- `lowercase`
- `uppercase`
- `generate(options, cb)`
- `options`
- `length` - the length of the random string. (default: 32) [OPTIONAL]
- `readable` - exclude poorly readable chars: 0OIl. (default: false) [OPTIONAL]
- `charset` - define the character set for the string. (default: 'alphanumeric') [OPTIONAL]
- `alphanumeric` - [0-9 a-z A-Z]
- `alphabetic` - [a-z A-Z]
- `numeric` - [0-9]
- `hex` - [0-9 a-f]
- `custom` - any given characters
- `capitalization` - define whether the output should be lowercase / uppercase only. (default: null) [OPTIONAL]
- `lowercase`
- `uppercase`
- `cb` - Optional. If provided uses async version of `crypto.randombytes`

## Command Line Usage

Expand Down
61 changes: 43 additions & 18 deletions lib/randomstring.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,56 @@ function safeRandomBytes(length) {
}
}

exports.generate = function(options) {

function processString(buf, initialString, chars, reqLen, maxByte) {
var string = initialString;
for (var i = 0; i < buf.length && string.length < reqLen; i++) {
var randomByte = buf.readUInt8(i);
if (randomByte < maxByte) {
string += chars.charAt(randomByte % chars.length);
}
}
return string;
}

function getAsyncString(string, chars, length, maxByte, cb) {
crypto.randomBytes(length, function(err, buf) {
if (err) {
// Since it is waiting for entropy, errors are legit and we shouldn't just keep retrying
cb(err);
}
var generatedString = processString(buf, string, chars, length, maxByte);
if (generatedString.length < length) {
getAsyncString(generatedString, chars, length, maxByte, cb);
} else {
cb(null, generatedString);
}
})
}

exports.generate = function(options, cb) {
var charset = new Charset();

var length, chars, capitalization, string = '';

// Handle options
if (typeof options === 'object') {
length = options.length || 32;

if (options.charset) {
charset.setType(options.charset);
}
else {
charset.setType('alphanumeric');
}

if (options.capitalization) {
charset.setcapitalization(options.capitalization);
}

if (options.readable) {
charset.removeUnreadable();
}

charset.removeDuplicates();
}
else if (typeof options === 'number') {
Expand All @@ -48,20 +73,20 @@ exports.generate = function(options) {
length = 32;
charset.setType('alphanumeric');
}

// Generate the string
var charsLen = charset.chars.length;
var maxByte = 256 - (256 % charsLen);
while (length > 0) {
var buf = safeRandomBytes(Math.ceil(length * 256 / maxByte));
for (var i = 0; i < buf.length && length > 0; i++) {
var randomByte = buf.readUInt8(i);
if (randomByte < maxByte) {
string += charset.chars.charAt(randomByte % charsLen);
length--;
}

if (!cb) {
while (string.length < length) {
var buf = safeRandomBytes(Math.ceil(length * 256 / maxByte));
string = processString(buf, string, charset.chars, length, maxByte);
}

return string;
}

return string;
getAsyncString(string, charset.chars, length, maxByte, cb);

};
96 changes: 88 additions & 8 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,55 +11,95 @@ describe("randomstring.generate(options)", function() {
assert.equal(typeof(rds), "string");
});

it("returns a string async", function(done) {
random(undefined, function(err, string) {
assert.equal(typeof(string), "string");
done();
});
});

it("defaults to 32 characters in length", function() {
assert.equal(random().length, 32);
});

it("accepts length as an optional first argument", function() {
assert.equal(random(10).length, 10);
});

it("accepts length as an option param", function() {
assert.equal(random({ length: 7 }).length, 7);
});


it("accepts length as an option param async", function(done) {
random(({ length: 7 }), function(err, string) {
assert.equal(string.length, 7);
done();
});
});

it("accepts 'numeric' as charset option", function() {
var testData = random({ length: testLength, charset: 'numeric' });
var search = testData.search(/\D/ig);
assert.equal(search, -1);
});


it("accepts 'numeric' as charset option async", function(done) {
random({ length: testLength, charset: 'numeric' }, function(err, testData) {
assert.equal(testData.length, testLength);
var search = testData.search(/\D/ig);
assert.equal(search, -1);
done();
});
});

it("accepts 'alphabetic' as charset option", function() {
var testData = random({ length: testLength, charset: 'alphabetic' });
var search = testData.search(/\d/ig);
assert.equal(search, -1);
});


it("accepts 'alphabetic' as charset option async", function(done) {
var testData = random({ length: testLength, charset: 'alphabetic' }, function(err, testData) {
var search = testData.search(/\d/ig);
assert.equal(search, -1);
done();
});
});

it("accepts 'hex' as charset option", function() {
var testData = random({ length: testLength, charset: 'hex' });
var search = testData.search(/[^0-9a-f]/ig);
assert.equal(search, -1);
});

it("accepts custom charset", function() {
var charset = "abc";
var testData = random({ length: testLength, charset: charset });
var search = testData.search(/[^abc]/ig);
assert.equal(search, -1);
});


it("accepts custom charset async", function(done) {
var charset = "abc";
random({ length: testLength, charset: charset }, function(err, testData) {
var search = testData.search(/[^abc]/ig);
assert.equal(search, -1);
done();
});
});

it("accepts readable option", function() {
var testData = random({ length: testLength, readable: true });
var search = testData.search(/[0OIl]/g);
assert.equal(search, -1);
});

it("accepts 'uppercase' as capitalization option", function() {
var testData = random({ length: testLength, capitalization: 'uppercase'});
var search = testData.search(/[a-z]/g);
assert.equal(search, -1);
});

it("accepts 'lowercase' as capitalization option", function() {
var testData = random({ length: testLength, capitalization: 'lowercase'});
var search = testData.search(/[A-Z]/g);
Expand All @@ -76,6 +116,23 @@ describe("randomstring.generate(options)", function() {
return true;
});

it("returns unique strings async", function(done) {
var results = [];
function doTest() {
random(undefined, function(err, string) {
assert.equal(results.indexOf(string), -1);
results.push(string);
if (results.length >= 1000) {
done();
} else {
doTest();
}
});
}

doTest();
});

it("returns unbiased strings", function() {
var charset = 'abcdefghijklmnopqrstuvwxyz';
var slen = 100000;
Expand All @@ -97,4 +154,27 @@ describe("randomstring.generate(options)", function() {
});
});

it("returns unbiased strings async", function(done) {
var charset = 'abcdefghijklmnopqrstuvwxyz';
var slen = 100000;
random({ charset: charset, length: slen }, function(err, s) {
var counts = {};
for (var i = 0; i < s.length; i++) {
var c = s.charAt(i);
if (typeof counts[c] === "undefined") {
counts[c] = 0;
} else {
counts[c]++;
}
}
var avg = slen / charset.length;
Object.keys(counts).sort().forEach(function(k) {
var diff = counts[k] / avg;
assert(diff > 0.95 && diff < 1.05,
"bias on `" + k + "': expected average is " + avg + ", got " + counts[k]);
});
done();
});

});
});

0 comments on commit 05c6d66

Please sign in to comment.