-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathluhn-string.js
396 lines (356 loc) · 14.3 KB
/
luhn-string.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
/* jshint expr: false */
/* globals require, module */
/**
* LUHN String module.
* @module luhn-string
*/
'use strict';
/** All Random strings are generated using NodeJS' crypto library */
var crypto = require('crypto');
var luhnValidChars = '0123456789BCDFGHJKLMNPQRSTVWXZ';
/* Returns a string that is the reversed version of the original.
* @param {string} str - the string to reverse
* @returns {string} reverse of `str`
*/
function reverse(s) {
// the reverser below is bad generally
// Ref: http://stackoverflow.com/a/16776621/1636522
// but good enough for the range we expect to work with
return s.split("").reverse().join("");
}
/* Returns the index of `str` in a set of valid chars.
* @param {string} str - the character whose ordinal we need
* @param {string} validChars - the valid characters to search in
* @returns {intResult} obj - index of `str` in the `validChars`
*/
function indexOfChar(str, validChars) {
var val = validChars.indexOf(str);
return {error: (val===-1) ? new Error(str + ' is not a valid digit. Expected one of "'+validChars.split('').join(', ')+'"') : null, result: val};
}
/* Returns the character at index `inte` in a set of valid chars.
* @param {int} inte - the index whose character we need
* @param {string} validChars - the valid characters to search in
* @returns {string} - character at index `inte` in the `validChars`
*/
function charAtIndex(inte, validChars) {
if (isNaN(inte) || parseInt(inte) !== inte || inte > (validChars.length - 1)) {
throw new Error('Invalid Ordinal');
} else {
return validChars[inte].toUpperCase();
}
}
/* Returns an object separating the two sent parameters clearly into string and callback.
This helper function allows functions that allow for an optional string before a callback
to work properly. Both are optional.
* @param {(string|callback)} [callbackOrChars] - the parameter we allow to be either a string or callback
* @param {callback} [callback] - this should only be a callback, but can be undefined too
* @returns {resolvedCallback} obj - an object separating the two sent parameters clearly into string and callback.
*/
function charsAndCallback(callbackOrChars, callback){
// callbackOrChars can be a callback or the chars
if(typeof callbackOrChars === 'string'){
return {validChars: callbackOrChars.toUpperCase(), callback: callback};
} else if(typeof callbackOrChars === typeof (function(){})){
return {validChars: luhnValidChars, callback: callbackOrChars};
} else {
return {validChars: luhnValidChars, callback: null};
}
}
/* Returns a string of length `length` from characters `chars`.
* @alias module:luhn-string.cryptoRandomString
* @param {int} length - the length of string we require
* @param {string} [chars] - characters to randomly select from defaults
to '0123456789BCDFGHJKLMNPQRSTVWXZ'. max of 256 characters.
* @returns {string} str -
* @throws err - Any errors encountered.
*/
function randomString(length, chars) {
// if a set of valid characters is sent
chars = ((typeof chars === 'string') ? chars : luhnValidChars);
var charsLength = chars.length, /* characters length */
randomBytes = crypto.randomBytes(length), /* random bytes supplied by crypto library */
result = new Array(length), /* Array to hold characters in string to send */
cursor = 0, /* cursor to hold increasing index of characters to pick */
i = 0; /* iterator for byte picking loop */
// check the length of the chars sent, above 256 shouldn't be entertained
if (charsLength > 256) {
throw new Error('Argument \'chars\' should not have more than 256 characters, '+
'otherwise unpredictability will be broken');
}
for (i ; i < length; i++) {
// increase cursor
cursor += randomBytes[i];
// add a character to array based on cursor
result[i] = chars[cursor % charsLength];
}
return result.join('');
}
/** Returns a random ascii string of length `length`.
* @alias module:luhn-string.cryptoRandomAsciiString
* @param {int} length - the length of string we require
* @returns {string} str - an object separating the two sent parameters clearly into string and callback.
* @throws err - Any errors encountered.
*/
function randomAsciiString(length) {
return randomString(length,
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789');
}
/* Returns a checksummed string of the length specified.
* @param {int} length - the length of string we require must be at least 2 (the last is always a checksum char)
* @param {(string|generatorCallback)} [callbackOrChars] - the parameter we allow to be either a
sequence of validChars or a callback. Characters sent thus will be overridden when this
function is called from within an instance of a LuhnString Object.
* @param {generatorCallback} [callback] - this should only be a callback, but can be undefined too.
*/
function random(length, callbackOrChars, callback) {
var validChars, cc;
// second argument can be a callback or the chars that
// are to be seen as valid in final result
cc = charsAndCallback(callbackOrChars, callback);
validChars = cc.validChars;
callback = cc.callback;
// length must be a positive integer
if (length !== parseInt(length, 10)) {
if (callback) {
return callback(new Error('\'length\' must be a positive integer'), null);
} else {
throw new Error('\'length\' must be a positive integer');
}
}
// length must be above 1 or where will the checksum digit be?
if (length <= 1) {
if (callback) {
return callback(new Error('The least checksumable string length is 2'), null);
} else {
throw new Error('The least checksumable string length is 2');
}
}
//
if (callback) {
return addChecksum(randomString(length - 1, validChars), validChars, callback);
} else {
return addChecksum(randomString(length - 1, validChars), validChars);
}
}
/* Checks a checksummed string `str` as being valid.
* @param {string} str - the string we want to check, length must be at least 2
* @param {(string|checkCallback)} [callbackOrChars] - the parameter we allow to be either a
sequence of validChars or a callback. Characters sent thus will be overridden when this
function is called from within an instance of a LuhnString Object.
* @param {checkCallback} [callback] - this should only be a callback, but can be undefined too.
*/
function check(str, callbackOrChars, callback) {
var validChars, cc, k, l, j=0, stringWithoutCheck;
cc = charsAndCallback(callbackOrChars, callback);
validChars = cc.validChars;
callback = cc.callback;
if (str.length <= 1) {
// invalid as there is only one character, the check character?
if (callback) {
return callback(new Error(str + ' is too short to be checked.'), false);
} else {
return {error: new Error(str + ' is too short to be checked.'), result: false};
}
}
str = str.toUpperCase();
// check has invalid chars
for (j; j < str.len; j++) {
if (validChars.indexOf(str[j]) === -1) {
// invalid as there is an invalid character!
if (callback) {
return callback(new Error(str + ' has invalid character: ' + str[j] + ' at index: ' + j), false);
} else {
return {error: new Error(str + ' has invalid character: ' + str[j] + ' at index: ' + j), result: false};
}
}
}
// take out the string minus its checksum character
stringWithoutCheck = str.substring(0, str.length - 1);
if (callback) {
return checksumDigit(stringWithoutCheck, validChars, function (errc, resc) {
if (errc) {
return callback(errc, null);
}
k = indexOfChar(str[str.length - 1], validChars);
if (k.error) {
return callback(k.error, null);
}
return callback(null, k.result === resc);
});
} else {
k = checksumDigit(stringWithoutCheck, validChars);
l = indexOfChar(str[str.length - 1], validChars);
if (k.error) {
return {error: k.error, result: null};
}
if (l.error) {
return {error: l.error, result: null};
}
return {error: null, result: (k.result === l.result)};
}
}
function checksumDigit(stringWithoutCheck, callbackOrChars, callback) {
var validChars, cc, xar, sum = 0, str, x1, cd, k; // k holds intermediate results colecting error info
cc = charsAndCallback(callbackOrChars, callback);
validChars = cc.validChars;
callback = cc.callback;
var string = reverse(stringWithoutCheck);
for (var x = 0; x < string.length; x++) {
k = indexOfChar(string[x], validChars);
if (k.error) {
if (callback) {
return callback(k.error, null);
}
return {error: k.error, result: null} ;
}
xar = k.result;
x1 = xar * Math.pow(2, (x + 1) % 2);
str = x1.toString();
for (var i = 0; i < str.length; i++) {
sum += parseInt(str.charAt(i), 10);
}
}
cd = (sum * (validChars.length-1)) % validChars.length;
if (callback) {
return callback(null, cd);
} else {
return {error: null, result: cd};
}
}
function addChecksum(r, callbackOrChars, callback) {
var validChars, cc, k, j=0;
if (r.length <= 0) {
// invalid as there is no character!
if (callback) {
return callback(new Error('"'+r + '" is too short to be checksummed.'), false);
} else {
throw new Error('"'+r + '" is too short to be checksummed.');
}
}
cc = charsAndCallback(callbackOrChars, callback);
validChars = cc.validChars;
callback = cc.callback;
r = r.toUpperCase();
// check has invalid chars
for (j; j < r.len; j++) {
if (validChars.indexOf(r[j]) === -1) {
// invalid as there is an invalid character!
if (callback) {
return callback(new Error(r + ' has invalid character: "' + r[j] + '" at index: ' + j), false);
} else {
throw new Error(r + ' has invalid character: "' + r[j] + '" at index: ' + j);
}
}
}
if (callback) {
checksumDigit(r, validChars, function (error, result) {
if (error) {
return callback(error, null);
}
return callback(null, r + charAtIndex(result, validChars));
});
} else {
k = checksumDigit(r, validChars);
if (k.error) {
throw k.error;
}
return r + charAtIndex(k.result, validChars);
}
}
/**
* Represents a Luhn String generator and checker.
* @constructor
* @param {string} [validChars] - The valid characters for our luhn string objects, in sequence.
LuhnString Objects created with '0123456789' will return strings that do not validate with those
created using the sequence '1234567890'.
In case you do not supply characters, a LuhnString will be created that uses the default sequence:
'0123456789BCDFGHJKLMNPQRSTVWXZ'.
*/
var LuhnString = function(validChars){
/**
* @typedef checkResult
* @type Object
* @property {Error} error - Errors encountered if any
* @property {boolean} result - whether the str was valid or not
*/
/**
* @typedef stringResult
* @type Object
* @property {Error} error - Errors encountered if any
* @property {string} result - string value requested
*/
/**
* @typedef intResult
* @type Object
* @property {Error} error - Errors encountered if any
* @property {int} result - int value requested
*/
/**
* @typedef LuhnString~resolvedCallback
* @type Object
* @property {string} validChars - Valid characters sent, if any
* @property {requestCallback} callback - Callback sent, if any
*/
if(validChars === 'undefined'){
validChars = luhnValidChars;
} else if(typeof validChars !== 'string'){
throw new Error('validChars must be a string containing all valid characters in sequence');
} else if(validChars.length <= 1){
throw new Error('validChars must contain at least 2 characters in sequence');
}
/** Checks a checksummed string as being valid.
* @param {string} str - the string we want to check, length must be at least 2
* @param {checkCallback} [callback] - callback to handle the response.
* @returns {checkResult} [str] - an object showing any error encountered and the result of the check, not returned if a callback is sent.
*/
this.check = function(str, callback){
return check(str, validChars, callback);
};
/** Adds a checksum digit to a string.
* @param {string} str - the string we want to add a checksum _digit_ to
* @param {generatorCallback} [callback] - callback to handle the response.
* @returns {string} [str] - not returned if a callback is sent.
* @throws {Error} [err] - not thrown if a callback is sent.
*/
this.addChecksum = function(str, callback){
return addChecksum(str, validChars, callback);
};
/** Generate a random checksummed string of length `length`.
* @param {int} length - the length of string we require. must be at least 2 (the last is always a checksum _digit_)
* @param {generatorCallback} [callback] - calback to handle the response.
* @returns {stringResult} [str] - not returned if a callback is sent.
*/
this.random = function(length, callback){
return random(length, validChars, callback);
};
/** Generate a random string of length `length`.
* @param {int} length - the length of string we require. must be at least 1
* @returns {string} [str]
* @throws {Error} [err]
*/
this.cryptoRandomString = function(length){
return randomString(length, validChars);
};
};
LuhnString.check= check;
LuhnString.addChecksum= addChecksum;
LuhnString.cryptoRandomAsciiString= randomAsciiString;
LuhnString.cryptoRandomString= randomString;
LuhnString.random= random;
/** This module exports a factory for objects that generate alphanumeric strings that end
with a checksum _"digit"_. _Digits_ in the string are in the range _0-9B-DF-HJ-NP-TV-XZ_. All uppercase.
To use `'012345'` as your sequence rather than this, simply create a new object with:
var obj = new require('luhn-string')('012345');*/
module.exports = LuhnString;
/**
* Callback to be sent to the `check` function.
* @callback checkCallback
* @param {Error} - Any errors encouuntered during the checking process
* @param {boolean} - Whether the string sent was valid or not
*/
/**
* Callback to be sent to the `random` or `addChecksum` function.
* @callback generatorCallback
* @param {Error} - Any errors encouuntered during the checking process
* @param {result} - A valid checksummed string
*/