diff options
Diffstat (limited to 'mojo/public/js/validator.js')
-rw-r--r-- | mojo/public/js/validator.js | 560 |
1 files changed, 560 insertions, 0 deletions
diff --git a/mojo/public/js/validator.js b/mojo/public/js/validator.js new file mode 100644 index 0000000000..283546d4f1 --- /dev/null +++ b/mojo/public/js/validator.js @@ -0,0 +1,560 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +define("mojo/public/js/validator", [ + "mojo/public/js/codec", +], function(codec) { + + var validationError = { + NONE: 'VALIDATION_ERROR_NONE', + MISALIGNED_OBJECT: 'VALIDATION_ERROR_MISALIGNED_OBJECT', + ILLEGAL_MEMORY_RANGE: 'VALIDATION_ERROR_ILLEGAL_MEMORY_RANGE', + UNEXPECTED_STRUCT_HEADER: 'VALIDATION_ERROR_UNEXPECTED_STRUCT_HEADER', + UNEXPECTED_ARRAY_HEADER: 'VALIDATION_ERROR_UNEXPECTED_ARRAY_HEADER', + ILLEGAL_HANDLE: 'VALIDATION_ERROR_ILLEGAL_HANDLE', + UNEXPECTED_INVALID_HANDLE: 'VALIDATION_ERROR_UNEXPECTED_INVALID_HANDLE', + ILLEGAL_POINTER: 'VALIDATION_ERROR_ILLEGAL_POINTER', + UNEXPECTED_NULL_POINTER: 'VALIDATION_ERROR_UNEXPECTED_NULL_POINTER', + MESSAGE_HEADER_INVALID_FLAGS: + 'VALIDATION_ERROR_MESSAGE_HEADER_INVALID_FLAGS', + MESSAGE_HEADER_MISSING_REQUEST_ID: + 'VALIDATION_ERROR_MESSAGE_HEADER_MISSING_REQUEST_ID', + DIFFERENT_SIZED_ARRAYS_IN_MAP: + 'VALIDATION_ERROR_DIFFERENT_SIZED_ARRAYS_IN_MAP', + INVALID_UNION_SIZE: 'VALIDATION_ERROR_INVALID_UNION_SIZE', + UNEXPECTED_NULL_UNION: 'VALIDATION_ERROR_UNEXPECTED_NULL_UNION', + UNKNOWN_ENUM_VALUE: 'VALIDATION_ERROR_UNKNOWN_ENUM_VALUE', + }; + + var NULL_MOJO_POINTER = "NULL_MOJO_POINTER"; + var gValidationErrorObserver = null; + + function reportValidationError(error) { + if (gValidationErrorObserver) { + gValidationErrorObserver.setLastError(error); + } + } + + var ValidationErrorObserverForTesting = (function() { + function Observer() { + this.lastError = validationError.NONE; + this.callback = null; + } + + Observer.prototype.setLastError = function(error) { + this.lastError = error; + if (this.callback) { + this.callback(error); + } + }; + + Observer.prototype.reset = function(error) { + this.lastError = validationError.NONE; + this.callback = null; + }; + + return { + getInstance: function() { + if (!gValidationErrorObserver) { + gValidationErrorObserver = new Observer(); + } + return gValidationErrorObserver; + } + }; + })(); + + function isTestingMode() { + return Boolean(gValidationErrorObserver); + } + + function clearTestingMode() { + gValidationErrorObserver = null; + } + + function isEnumClass(cls) { + return cls instanceof codec.Enum; + } + + function isStringClass(cls) { + return cls === codec.String || cls === codec.NullableString; + } + + function isHandleClass(cls) { + return cls === codec.Handle || cls === codec.NullableHandle; + } + + function isInterfaceClass(cls) { + return cls instanceof codec.Interface; + } + + function isInterfaceRequestClass(cls) { + return cls === codec.InterfaceRequest || + cls === codec.NullableInterfaceRequest; + } + + function isNullable(type) { + return type === codec.NullableString || type === codec.NullableHandle || + type === codec.NullableInterface || + type === codec.NullableInterfaceRequest || + type instanceof codec.NullableArrayOf || + type instanceof codec.NullablePointerTo; + } + + function Validator(message) { + this.message = message; + this.offset = 0; + this.handleIndex = 0; + } + + Object.defineProperty(Validator.prototype, "offsetLimit", { + get: function() { return this.message.buffer.byteLength; } + }); + + Object.defineProperty(Validator.prototype, "handleIndexLimit", { + get: function() { return this.message.handles.length; } + }); + + // True if we can safely allocate a block of bytes from start to + // to start + numBytes. + Validator.prototype.isValidRange = function(start, numBytes) { + // Only positive JavaScript integers that are less than 2^53 + // (Number.MAX_SAFE_INTEGER) can be represented exactly. + if (start < this.offset || numBytes <= 0 || + !Number.isSafeInteger(start) || + !Number.isSafeInteger(numBytes)) + return false; + + var newOffset = start + numBytes; + if (!Number.isSafeInteger(newOffset) || newOffset > this.offsetLimit) + return false; + + return true; + }; + + Validator.prototype.claimRange = function(start, numBytes) { + if (this.isValidRange(start, numBytes)) { + this.offset = start + numBytes; + return true; + } + return false; + }; + + Validator.prototype.claimHandle = function(index) { + if (index === codec.kEncodedInvalidHandleValue) + return true; + + if (index < this.handleIndex || index >= this.handleIndexLimit) + return false; + + // This is safe because handle indices are uint32. + this.handleIndex = index + 1; + return true; + }; + + Validator.prototype.validateEnum = function(offset, enumClass) { + // Note: Assumes that enums are always 32 bits! But this matches + // mojom::generate::pack::PackedField::GetSizeForKind, so it should be okay. + var value = this.message.buffer.getInt32(offset); + return enumClass.validate(value); + } + + Validator.prototype.validateHandle = function(offset, nullable) { + var index = this.message.buffer.getUint32(offset); + + if (index === codec.kEncodedInvalidHandleValue) + return nullable ? + validationError.NONE : validationError.UNEXPECTED_INVALID_HANDLE; + + if (!this.claimHandle(index)) + return validationError.ILLEGAL_HANDLE; + + return validationError.NONE; + }; + + Validator.prototype.validateInterface = function(offset, nullable) { + return this.validateHandle(offset, nullable); + }; + + Validator.prototype.validateInterfaceRequest = function(offset, nullable) { + return this.validateHandle(offset, nullable); + }; + + Validator.prototype.validateStructHeader = function(offset, minNumBytes) { + if (!codec.isAligned(offset)) + return validationError.MISALIGNED_OBJECT; + + if (!this.isValidRange(offset, codec.kStructHeaderSize)) + return validationError.ILLEGAL_MEMORY_RANGE; + + var numBytes = this.message.buffer.getUint32(offset); + + if (numBytes < minNumBytes) + return validationError.UNEXPECTED_STRUCT_HEADER; + + if (!this.claimRange(offset, numBytes)) + return validationError.ILLEGAL_MEMORY_RANGE; + + return validationError.NONE; + }; + + Validator.prototype.validateStructVersion = function(offset, versionSizes) { + var numBytes = this.message.buffer.getUint32(offset); + var version = this.message.buffer.getUint32(offset + 4); + + if (version <= versionSizes[versionSizes.length - 1].version) { + // Scan in reverse order to optimize for more recent versionSizes. + for (var i = versionSizes.length - 1; i >= 0; --i) { + if (version >= versionSizes[i].version) { + if (numBytes == versionSizes[i].numBytes) + break; + return validationError.UNEXPECTED_STRUCT_HEADER; + } + } + } else if (numBytes < versionSizes[versionSizes.length-1].numBytes) { + return validationError.UNEXPECTED_STRUCT_HEADER; + } + + return validationError.NONE; + }; + + Validator.prototype.isFieldInStructVersion = function(offset, fieldVersion) { + var structVersion = this.message.buffer.getUint32(offset + 4); + return fieldVersion <= structVersion; + }; + + // TODO(wangjimmy): Add support for v2 messages. + Validator.prototype.validateMessageHeader = function() { + + var err = this.validateStructHeader(0, codec.kMessageHeaderSize); + if (err != validationError.NONE) + return err; + + var numBytes = this.message.getHeaderNumBytes(); + var version = this.message.getHeaderVersion(); + + var validVersionAndNumBytes = + (version == 0 && numBytes == codec.kMessageHeaderSize) || + (version == 1 && + numBytes == codec.kMessageWithRequestIDHeaderSize) || + (version > 1 && + numBytes >= codec.kMessageWithRequestIDHeaderSize); + if (!validVersionAndNumBytes) + return validationError.UNEXPECTED_STRUCT_HEADER; + + var expectsResponse = this.message.expectsResponse(); + var isResponse = this.message.isResponse(); + + if (version == 0 && (expectsResponse || isResponse)) + return validationError.MESSAGE_HEADER_MISSING_REQUEST_ID; + + if (isResponse && expectsResponse) + return validationError.MESSAGE_HEADER_INVALID_FLAGS; + + return validationError.NONE; + }; + + Validator.prototype.validateMessageIsRequestWithoutResponse = function() { + if (this.message.isResponse() || this.message.expectsResponse()) { + return validationError.MESSAGE_HEADER_INVALID_FLAGS; + } + return validationError.NONE; + }; + + Validator.prototype.validateMessageIsRequestExpectingResponse = function() { + if (this.message.isResponse() || !this.message.expectsResponse()) { + return validationError.MESSAGE_HEADER_INVALID_FLAGS; + } + return validationError.NONE; + }; + + Validator.prototype.validateMessageIsResponse = function() { + if (this.message.expectsResponse() || !this.message.isResponse()) { + return validationError.MESSAGE_HEADER_INVALID_FLAGS; + } + return validationError.NONE; + }; + + // Returns the message.buffer relative offset this pointer "points to", + // NULL_MOJO_POINTER if the pointer represents a null, or JS null if the + // pointer's value is not valid. + Validator.prototype.decodePointer = function(offset) { + var pointerValue = this.message.buffer.getUint64(offset); + if (pointerValue === 0) + return NULL_MOJO_POINTER; + var bufferOffset = offset + pointerValue; + return Number.isSafeInteger(bufferOffset) ? bufferOffset : null; + }; + + Validator.prototype.decodeUnionSize = function(offset) { + return this.message.buffer.getUint32(offset); + }; + + Validator.prototype.decodeUnionTag = function(offset) { + return this.message.buffer.getUint32(offset + 4); + }; + + Validator.prototype.validateArrayPointer = function( + offset, elementSize, elementType, nullable, expectedDimensionSizes, + currentDimension) { + var arrayOffset = this.decodePointer(offset); + if (arrayOffset === null) + return validationError.ILLEGAL_POINTER; + + if (arrayOffset === NULL_MOJO_POINTER) + return nullable ? + validationError.NONE : validationError.UNEXPECTED_NULL_POINTER; + + return this.validateArray(arrayOffset, elementSize, elementType, + expectedDimensionSizes, currentDimension); + }; + + Validator.prototype.validateStructPointer = function( + offset, structClass, nullable) { + var structOffset = this.decodePointer(offset); + if (structOffset === null) + return validationError.ILLEGAL_POINTER; + + if (structOffset === NULL_MOJO_POINTER) + return nullable ? + validationError.NONE : validationError.UNEXPECTED_NULL_POINTER; + + return structClass.validate(this, structOffset); + }; + + Validator.prototype.validateUnion = function( + offset, unionClass, nullable) { + var size = this.message.buffer.getUint32(offset); + if (size == 0) { + return nullable ? + validationError.NONE : validationError.UNEXPECTED_NULL_UNION; + } + + return unionClass.validate(this, offset); + }; + + Validator.prototype.validateNestedUnion = function( + offset, unionClass, nullable) { + var unionOffset = this.decodePointer(offset); + if (unionOffset === null) + return validationError.ILLEGAL_POINTER; + + if (unionOffset === NULL_MOJO_POINTER) + return nullable ? + validationError.NONE : validationError.UNEXPECTED_NULL_UNION; + + return this.validateUnion(unionOffset, unionClass, nullable); + }; + + // This method assumes that the array at arrayPointerOffset has + // been validated. + + Validator.prototype.arrayLength = function(arrayPointerOffset) { + var arrayOffset = this.decodePointer(arrayPointerOffset); + return this.message.buffer.getUint32(arrayOffset + 4); + }; + + Validator.prototype.validateMapPointer = function( + offset, mapIsNullable, keyClass, valueClass, valueIsNullable) { + // Validate the implicit map struct: + // struct {array<keyClass> keys; array<valueClass> values}; + var structOffset = this.decodePointer(offset); + if (structOffset === null) + return validationError.ILLEGAL_POINTER; + + if (structOffset === NULL_MOJO_POINTER) + return mapIsNullable ? + validationError.NONE : validationError.UNEXPECTED_NULL_POINTER; + + var mapEncodedSize = codec.kStructHeaderSize + codec.kMapStructPayloadSize; + var err = this.validateStructHeader(structOffset, mapEncodedSize); + if (err !== validationError.NONE) + return err; + + // Validate the keys array. + var keysArrayPointerOffset = structOffset + codec.kStructHeaderSize; + err = this.validateArrayPointer( + keysArrayPointerOffset, keyClass.encodedSize, keyClass, false, [0], 0); + if (err !== validationError.NONE) + return err; + + // Validate the values array. + var valuesArrayPointerOffset = keysArrayPointerOffset + 8; + var valuesArrayDimensions = [0]; // Validate the actual length below. + if (valueClass instanceof codec.ArrayOf) + valuesArrayDimensions = + valuesArrayDimensions.concat(valueClass.dimensions()); + var err = this.validateArrayPointer(valuesArrayPointerOffset, + valueClass.encodedSize, + valueClass, + valueIsNullable, + valuesArrayDimensions, + 0); + if (err !== validationError.NONE) + return err; + + // Validate the lengths of the keys and values arrays. + var keysArrayLength = this.arrayLength(keysArrayPointerOffset); + var valuesArrayLength = this.arrayLength(valuesArrayPointerOffset); + if (keysArrayLength != valuesArrayLength) + return validationError.DIFFERENT_SIZED_ARRAYS_IN_MAP; + + return validationError.NONE; + }; + + Validator.prototype.validateStringPointer = function(offset, nullable) { + return this.validateArrayPointer( + offset, codec.Uint8.encodedSize, codec.Uint8, nullable, [0], 0); + }; + + // Similar to Array_Data<T>::Validate() + // mojo/public/cpp/bindings/lib/array_internal.h + + Validator.prototype.validateArray = + function (offset, elementSize, elementType, expectedDimensionSizes, + currentDimension) { + if (!codec.isAligned(offset)) + return validationError.MISALIGNED_OBJECT; + + if (!this.isValidRange(offset, codec.kArrayHeaderSize)) + return validationError.ILLEGAL_MEMORY_RANGE; + + var numBytes = this.message.buffer.getUint32(offset); + var numElements = this.message.buffer.getUint32(offset + 4); + + // Note: this computation is "safe" because elementSize <= 8 and + // numElements is a uint32. + var elementsTotalSize = (elementType === codec.PackedBool) ? + Math.ceil(numElements / 8) : (elementSize * numElements); + + if (numBytes < codec.kArrayHeaderSize + elementsTotalSize) + return validationError.UNEXPECTED_ARRAY_HEADER; + + if (expectedDimensionSizes[currentDimension] != 0 && + numElements != expectedDimensionSizes[currentDimension]) { + return validationError.UNEXPECTED_ARRAY_HEADER; + } + + if (!this.claimRange(offset, numBytes)) + return validationError.ILLEGAL_MEMORY_RANGE; + + // Validate the array's elements if they are pointers or handles. + + var elementsOffset = offset + codec.kArrayHeaderSize; + var nullable = isNullable(elementType); + + if (isHandleClass(elementType)) + return this.validateHandleElements(elementsOffset, numElements, nullable); + if (isInterfaceClass(elementType)) + return this.validateInterfaceElements( + elementsOffset, numElements, nullable); + if (isInterfaceRequestClass(elementType)) + return this.validateInterfaceRequestElements( + elementsOffset, numElements, nullable); + if (isStringClass(elementType)) + return this.validateArrayElements( + elementsOffset, numElements, codec.Uint8, nullable, [0], 0); + if (elementType instanceof codec.PointerTo) + return this.validateStructElements( + elementsOffset, numElements, elementType.cls, nullable); + if (elementType instanceof codec.ArrayOf) + return this.validateArrayElements( + elementsOffset, numElements, elementType.cls, nullable, + expectedDimensionSizes, currentDimension + 1); + if (isEnumClass(elementType)) + return this.validateEnumElements(elementsOffset, numElements, + elementType.cls); + + return validationError.NONE; + }; + + // Note: the |offset + i * elementSize| computation in the validateFooElements + // methods below is "safe" because elementSize <= 8, offset and + // numElements are uint32, and 0 <= i < numElements. + + Validator.prototype.validateHandleElements = + function(offset, numElements, nullable) { + var elementSize = codec.Handle.encodedSize; + for (var i = 0; i < numElements; i++) { + var elementOffset = offset + i * elementSize; + var err = this.validateHandle(elementOffset, nullable); + if (err != validationError.NONE) + return err; + } + return validationError.NONE; + }; + + Validator.prototype.validateInterfaceElements = + function(offset, numElements, nullable) { + var elementSize = codec.Interface.prototype.encodedSize; + for (var i = 0; i < numElements; i++) { + var elementOffset = offset + i * elementSize; + var err = this.validateInterface(elementOffset, nullable); + if (err != validationError.NONE) + return err; + } + return validationError.NONE; + }; + + Validator.prototype.validateInterfaceRequestElements = + function(offset, numElements, nullable) { + var elementSize = codec.InterfaceRequest.encodedSize; + for (var i = 0; i < numElements; i++) { + var elementOffset = offset + i * elementSize; + var err = this.validateInterfaceRequest(elementOffset, nullable); + if (err != validationError.NONE) + return err; + } + return validationError.NONE; + }; + + // The elementClass parameter is the element type of the element arrays. + Validator.prototype.validateArrayElements = + function(offset, numElements, elementClass, nullable, + expectedDimensionSizes, currentDimension) { + var elementSize = codec.PointerTo.prototype.encodedSize; + for (var i = 0; i < numElements; i++) { + var elementOffset = offset + i * elementSize; + var err = this.validateArrayPointer( + elementOffset, elementClass.encodedSize, elementClass, nullable, + expectedDimensionSizes, currentDimension); + if (err != validationError.NONE) + return err; + } + return validationError.NONE; + }; + + Validator.prototype.validateStructElements = + function(offset, numElements, structClass, nullable) { + var elementSize = codec.PointerTo.prototype.encodedSize; + for (var i = 0; i < numElements; i++) { + var elementOffset = offset + i * elementSize; + var err = + this.validateStructPointer(elementOffset, structClass, nullable); + if (err != validationError.NONE) + return err; + } + return validationError.NONE; + }; + + Validator.prototype.validateEnumElements = + function(offset, numElements, enumClass) { + var elementSize = codec.Enum.prototype.encodedSize; + for (var i = 0; i < numElements; i++) { + var elementOffset = offset + i * elementSize; + var err = this.validateEnum(elementOffset, enumClass); + if (err != validationError.NONE) + return err; + } + return validationError.NONE; + }; + + var exports = {}; + exports.validationError = validationError; + exports.Validator = Validator; + exports.ValidationErrorObserverForTesting = ValidationErrorObserverForTesting; + exports.reportValidationError = reportValidationError; + exports.isTestingMode = isTestingMode; + exports.clearTestingMode = clearTestingMode; + return exports; +}); |