Make Attribute detection more strict

The old Attribute.isAttribute would allow objects lacking the toBer
method to be attached to Change objects.  This would result in errors
during serialization.

With the stricter Attribute detection, it's necessary to convert
Attribute-like objects containing type (string) and vals (array)
properties into real Attribute objects.  This precise detection is
necessary to avoid falling back to the object-keys-into-attributes

That other logic which will turn a well structured Attribute-like object
such as this:

  { type: 'valid', vals: ['something'] }

... and turn it into something broken like this:

  [
    { type: 'type', vals: ['valid'] },
    { type: 'vals', vals: ['something'] }
  ]
This commit is contained in:
Patrick Mooney 2014-07-17 13:33:02 -05:00
parent 48bd7bfe82
commit 59bebe537b
4 changed files with 105 additions and 20 deletions

View File

@ -98,8 +98,9 @@ Attribute.prototype.addValue = function (val) {
/* BEGIN JSSTYLED */
Attribute.compare = function compare(a, b) {
if (!(a instanceof Attribute) || !(b instanceof Attribute))
if (!(Attribute.isAttribute(a)) || !(Attribute.isAttribute(b))) {
throw new TypeError('can only compare Attributes');
}
if (a.type < b.type) return -1;
if (a.type > b.type) return 1;
@ -156,36 +157,41 @@ Attribute.prototype.toBer = function (ber) {
return ber;
};
Attribute.toBer = function (attr, ber) {
return Attribute.prototype.toBer.call(attr, ber);
};
/* BEGIN JSSTYLED */
Attribute.isAttribute = function (attr) {
if (!attr) return false;
if (typeof (attr) !== 'object') return false;
if (attr instanceof Attribute) return true;
if (!attr.type || typeof (attr.type) !== 'string') return false;
if (!attr.vals || !Array.isArray(attr.vals)) return false;
for (var i = 0; i < attr.vals.length; i++) {
if (typeof (attr.vals[i]) !== 'string' && !Buffer.isBuffer(attr.vals[i]))
return false;
if (!attr || typeof (attr) !== 'object') {
return false;
}
return true;
if (attr instanceof Attribute) {
return true;
}
if ((typeof (attr.toBer) === 'function') &&
(typeof (attr.type) === 'string') &&
(Array.isArray(attr.vals)) &&
(attr.vals.filter(function (item) {
return (typeof (item) === 'string' ||
Buffer.isBuffer(item));
}).length === attr.vals.length)) {
return true;
}
return false;
};
/* END JSSTLYED */
Attribute.prototype.toString = function () {
return JSON.stringify(this.json);
};
function formatGuid(format, data) {
for (var i = 0; i < data.length; i++) {
var re = new RegExp('\\{' + i + '\\}', 'g');
// Leading 0 is needed if value of data[i] is less than 16 (of 10 as hex).
// Leading 0 is needed if value of data[i] is less than 16 (of 10 as hex).
var dataStr = data[i].toString(16);
format = format.replace(re, data[i] >= 16 ? dataStr : '0' + dataStr);
}

View File

@ -52,12 +52,22 @@ function Change(options) {
this.__defineGetter__('modification', function () {
return self._modification;
});
this.__defineSetter__('modification', function (attr) {
if (Attribute.isAttribute(attr)) {
self._modification = attr;
return;
}
// Does it have an attribute-like structure
if (Object.keys(attr).length == 2 &&
typeof (attr.type) === 'string' &&
Array.isArray(attr.vals)) {
self._modification = new Attribute({
type: attr.type,
vals: attr.vals
});
return;
}
var keys = Object.keys(attr);
if (keys.length > 1)
throw new Error('Only one attribute per Change allowed');

View File

@ -606,14 +606,25 @@ Client.prototype.modify = function modify(name, change, controls, callback) {
if (typeof (change.modification) !== 'object')
throw new Error('change.modification (object) required');
Object.keys(change.modification).forEach(function (k) {
var mod = {};
mod[k] = change.modification[k];
if (Object.keys(change.modification).length == 2 &&
typeof (change.modification.type) === 'string' &&
Array.isArray(change.modification.vals)) {
// Use modification directly if it's already normalized:
changes.push(new Change({
operation: change.operation || change.type,
modification: mod
modification: change.modification
}));
});
} else {
// Normalize the modification object
Object.keys(change.modification).forEach(function (k) {
var mod = {};
mod[k] = change.modification[k];
changes.push(new Change({
operation: change.operation || change.type,
modification: mod
}));
});
}
}
if (Change.isChange(change)) {

View File

@ -80,3 +80,61 @@ test('parse', function (t) {
t.equal(attr.vals[1], 'bar');
t.end();
});
test('isAttribute', function (t) {
var isA = Attribute.isAttribute;
t.notOk(isA(null));
t.notOk(isA('asdf'));
t.ok(isA(new Attribute({
type: 'foobar',
vals: ['asdf']
})));
t.ok(isA({
type: 'foo',
vals: ['item', new Buffer(5)],
toBer: function () { /* placeholder */ }
}));
// bad type in vals
t.notOk(isA({
type: 'foo',
vals: ['item', null],
toBer: function () { /* placeholder */ }
}));
t.end();
});
test('compare', function (t) {
var comp = Attribute.compare;
var a = new Attribute({
type: 'foo',
vals: ['bar']
});
var b = new Attribute({
type: 'foo',
vals: ['bar']
});
t.equal(comp(a, b), 0);
// Different types
a.type = 'boo';
t.equal(comp(a, b), -1);
t.equal(comp(b, a), 1);
a.type = 'foo';
// Different value counts
a.vals = ['bar', 'baz'];
t.equal(comp(a, b), 1);
t.equal(comp(b, a), -1);
// Different value contents (same count)
a.vals = ['baz'];
t.equal(comp(a, b), 1);
t.equal(comp(b, a), -1);
t.end();
});