ut-codec
Version:
UT codec module.
208 lines (199 loc) • 10.2 kB
JavaScript
'use strict';
var merge = require('lodash.merge');
var defaultFields = require('./iso8583.fields.json');
var bitSyntax = require('ut-bitsyntax');
var errors = require('./isoErrors');
var emv = require('./emv');
function getFormat(format, fallback) {
return (format && {'numeric': 'string-left-zero', 'string': 'string-right-space', 'amount': 'string-left-zero', 'bcdamount': 'string'}[format]) || format || fallback || 'binary';
}
function Iso8583(config) {
this.networkCodes = Object.assign({
'001': 'keyChange',
'002': 'signOff',
'061': 'echo',
'161': 'keyChange',
'201': 'cutOver',
'301': 'echo'
}, config.networkCodes);
this.emvTagsField = config.emvTagsField || 55;
this.fieldFormat = merge({}, defaultFields[(config.version || '0') + (config.baseEncoding || 'ascii')], config.fieldFormat);
this.framePattern = bitSyntax.matcher('header:' + this.fieldFormat.header.size + '/' + getFormat(this.fieldFormat.header.format) +
', mtid:' + this.fieldFormat.mtid.size + '/' + getFormat(this.fieldFormat.mtid.format) +
', field0:' + this.fieldFormat['0'].size + '/' + getFormat(this.fieldFormat['0'].format) +
', rest/binary');
this.fieldPatterns = [];
this.fieldBuilders = [bitSyntax.parse('field:fieldSize/' + getFormat(this.fieldFormat['0'].format))];
this.fieldBuilders.mtid = bitSyntax.parse('field:fieldSize/' + getFormat(this.fieldFormat.mtid.format));
this.fieldBuilders.header = bitSyntax.parse('field:fieldSize/' + getFormat(this.fieldFormat.header.format));
this.fieldBuilders.footer = bitSyntax.parse('field:fieldSize/' + getFormat(this.fieldFormat.footer.format));
this.prefixBuilders = [null];
this.footerMatcher = bitSyntax.matcher('footer:' + this.fieldFormat.footer.size + '/' + getFormat(this.fieldFormat.footer.format) + ', rest/binary');
var group = 0;
while (this.fieldFormat[(group + 1) * 64]) {
var pattern = [];
for (var i = 1; i <= 64; i += 1) {
var field = group * 64 + i;
if (this.fieldFormat[field].prefixSize) { // if the field is with variable size
pattern.push('prefix' + field + ':field' + field + 'Size/' + getFormat(this.fieldFormat[field].prefixFormat, 'string-left-zero') +
', field' + field + ':prefix' + field + '/' + getFormat(this.fieldFormat[field].format));
this.prefixBuilders.push(bitSyntax.parse('prefix:' + this.fieldFormat[field].prefixSize + '/' +
getFormat(this.fieldFormat[field].prefixFormat, 'string-left-zero')));
} else { // if the field is with fixed size
pattern.push('field' + field + ':field' + field + 'Size/' + getFormat(this.fieldFormat[field].format));
this.prefixBuilders.push(null);
}
this.fieldBuilders.push(bitSyntax.parse('field:fieldSize/' + getFormat(this.fieldFormat[field].format)));
}
pattern.push('rest/binary');
this.fieldPatterns.push(bitSyntax.matcher(pattern.join(', ')));
group += 1;
}
}
Iso8583.prototype.fieldSizes = function(bitmap, start) {
/* jshint bitwise: false */
var result = {};
for (var i = 0; i <= 63; i += 1) {
var size = bitmap && ((bitmap[i >> 3] & (128 >> (i % 8))) !== 0);
if (size) {
result['field' + (start + i) + 'Size'] = this.fieldFormat[start + i].prefixSize || this.fieldFormat[start + i].size;
} else {
result['field' + (start + i) + 'Size'] = 0;
}
}
return result;
};
Iso8583.prototype.decode = function(buffer, $meta) {
var frame = this.framePattern(buffer);
var bitmapField = 0;
if (frame) {
var message = {'header': frame.header, 'mtid': frame.mtid, '0': frame.field0};
var parsedLength = buffer.length - frame.rest.length;
var group = 0;
while (frame) {
var fieldPattern = this.fieldPatterns[group];
if (!fieldPattern) {
if (frame.rest && frame.rest.length) {
if (this.fieldFormat.footer && this.fieldFormat.footer.size) {
frame = this.footerMatcher(frame.rest || Buffer.alloc(0));
message.footer = frame && frame.footer;
}
if (frame.rest && frame.rest.length) {
throw new Error('Not all data was parsed. Remaining ' + frame.rest.length + ' bytes at offset ' + parsedLength +
' starting with 0x' + frame.rest.toString('hex') + '\r\nmessage:' + JSON.stringify(message));
}
}
break;
}
var fieldSizes = this.fieldSizes(frame['field' + bitmapField], group * 64 + 1);
var rest = frame.rest;
frame = fieldPattern && fieldPattern(rest, fieldSizes);
if (!frame && fieldPattern) {
for (var failField = (group + 1) * 64; failField >= group * 64 + 1; failField -= 1) { // find at which field we failed by skipping fields from the end
fieldSizes['field' + failField + 'Size'] = 0;
frame = fieldPattern && fieldPattern(rest, fieldSizes);
if (frame && frame.rest && frame.rest.length && this.fieldFormat.footer && this.fieldFormat.footer.size) {
frame = this.footerMatcher(frame.rest || Buffer.alloc(0));
message.footer = frame && frame.footer;
}
if (frame) {
parsedLength += rest.length - frame.rest.length;
throw new Error('Parsing failed at field ' + failField + '. Remaining ' + frame.rest.length + ' bytes at offset ' + parsedLength +
' starting with 0x' + frame.rest.toString('hex') + '\r\nmessage:' + JSON.stringify(message));
}
}
throw new Error('Parsing failed at unknown field');
}
parsedLength += rest.length - frame.rest.length;
bitmapField = group * 64 + 1;
for (var fieldNo = group * 64 + 1; fieldNo <= (group + 1) * 64; fieldNo += 1) {
if (fieldSizes['field' + fieldNo + 'Size']) {
message[fieldNo] = frame['field' + fieldNo];
}
}
group += 1;
}
if (message.mtid === '0800' || message.mtid === '0810') {
$meta.opcode = String(message[70] || '');
$meta.opcode = this.networkCodes[$meta.opcode] || $meta.opcode;
} else {
$meta.opcode = String(message[3] || '').substr(0, 2);
}
$meta.trace = message[11];
if (message.mtid && message.mtid.slice) {
$meta.mtid = {
'0': 'request',
'1': (parseInt(message[39]) === 0) ? 'response' : 'error',
'2': 'request',
'3': (parseInt(message[39]) === 0) ? 'response' : 'error',
'4': 'notification',
'5': 'notification'
}[(message.mtid.slice(-2).substr(0, 1))] || 'error';
}
$meta.method = message.mtid + ($meta.opcode ? '.' + $meta.opcode : '');
if ($meta.mtid === 'error') {
var err = errors['' + message[39]] || errors.generic;
message = err(message);
}
if (message[this.emvTagsField]) {
message = Object.assign(message, {emvTags: emv.tagsDecode(message[this.emvTagsField])});
}
return message;
} else {
throw new Error('Unable to parse message type or first bitmap!');
}
};
Iso8583.prototype.encodeField = function(fieldName, fieldValue) {
var prefixBuilder = this.prefixBuilders[fieldName];
var field = bitSyntax.build(
this.fieldBuilders[fieldName],
{'field': fieldValue, 'fieldSize': prefixBuilder ? fieldValue.length : this.fieldFormat[fieldName].size}
);
return prefixBuilder ? Buffer.concat([bitSyntax.build(prefixBuilder, {'prefix': field.length}), field]) : field;
};
Iso8583.prototype.encode = function(message, $meta, context) {
/* jshint bitwise: false */
var buffers = new Array(64 * this.fieldPatterns.length);
var emptyBuffer = new Buffer([]);
var trace = message[11];
if (message.emvTags) {
message[this.emvTagsField] = emv.encode(message.emvTags);
}
if (trace === undefined || trace === null) {
trace = $meta.trace = ('000000' + context.trace).substr(-6);
context.trace += 1;
if (context.trace > 999999) {
context.trace = 0;
}
}
message[11] = trace;
var bitmaps = Array.apply(null, new Array(8 * this.fieldPatterns.length)).map(Number.prototype.valueOf, 0); // zero filled array
for (var i = 64 * this.fieldPatterns.length; i >= 0; i -= 1) {
if (i === 0) {
buffers[i] = this.encodeField(i, new Buffer(bitmaps.slice(0, 8)));
} else if (i % 64 === 1 && i < 64 * (this.fieldPatterns.length - 1)) {
var index = (i >> 6) << 3;
var bitmap = bitmaps.slice(index + 8, index + 16);
if (bitmap.reduce(function(p, n) { return p + n; })) {
bitmaps[(i - 1) >> 3] |= (128 >> (i - 1) % 8);
buffers[i] = this.encodeField(i, new Buffer(bitmap));
} else {
buffers[i] = emptyBuffer;
}
} else if (message[i] != null) {
bitmaps[(i - 1) >> 3] |= (128 >> (i - 1) % 8);
buffers[i] = this.encodeField(i, message[i]);
} else {
buffers[i] = emptyBuffer;
}
}
buffers.unshift(this.encodeField('mtid', message.mtid || new Buffer([])));
if (this.fieldFormat.header && this.fieldFormat.header.size) {
buffers.unshift(this.encodeField('header', message.header || Buffer.alloc(this.fieldFormat.header.size)));
}
if (this.fieldFormat.footer && this.fieldFormat.footer.size) {
buffers.push(this.encodeField('footer', message.footer || Buffer.alloc(this.fieldFormat.footer.size)));
}
return Buffer.concat(buffers);
};
module.exports = Iso8583;