From da38c12cea9d188fcb1a44b0d1a4bcaf48c96189 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Mon, 10 Oct 2022 17:21:04 +0200 Subject: [PATCH 01/22] First draft of extension validation, with S2 as example --- .../s2MaximumHeightInvalidType.json | 24 +++ ...MinimumHeightGreaterThanMaximumHeight.json | 24 +++ .../s2MinimumHeightInvalidType.json | 24 +++ .../boundingVolumeS2/s2TokenInvalidType.json | 24 +++ .../boundingVolumeS2/s2TokenInvalidValue.json | 24 +++ .../boundingVolumeS2/validTilesetWithS2.json | 24 +++ src/issues/SemanticValidationIssues.ts | 10 +- src/main.ts | 8 + src/validation/BoundingVolumeValidator.ts | 45 +++++- src/validation/ContentValidator.ts | 17 +-- src/validation/ExtensionsValidationResult.ts | 5 + src/validation/ExtensionsValidator.ts | 99 +++++++++++++ src/validation/TileValidator.ts | 60 ++++---- src/validation/TilesetTraversingValidator.ts | 3 +- .../BoundingVolumeS2ValidationIssues.ts | 12 ++ .../extensions/BoundingVolumeS2Validator.ts | 137 ++++++++++++++++++ 16 files changed, 498 insertions(+), 42 deletions(-) create mode 100644 specs/data/tilesets/boundingVolumeS2/s2MaximumHeightInvalidType.json create mode 100644 specs/data/tilesets/boundingVolumeS2/s2MinimumHeightGreaterThanMaximumHeight.json create mode 100644 specs/data/tilesets/boundingVolumeS2/s2MinimumHeightInvalidType.json create mode 100644 specs/data/tilesets/boundingVolumeS2/s2TokenInvalidType.json create mode 100644 specs/data/tilesets/boundingVolumeS2/s2TokenInvalidValue.json create mode 100644 specs/data/tilesets/boundingVolumeS2/validTilesetWithS2.json create mode 100644 src/validation/ExtensionsValidationResult.ts create mode 100644 src/validation/ExtensionsValidator.ts create mode 100644 src/validation/extensions/BoundingVolumeS2ValidationIssues.ts create mode 100644 src/validation/extensions/BoundingVolumeS2Validator.ts diff --git a/specs/data/tilesets/boundingVolumeS2/s2MaximumHeightInvalidType.json b/specs/data/tilesets/boundingVolumeS2/s2MaximumHeightInvalidType.json new file mode 100644 index 00000000..305efe71 --- /dev/null +++ b/specs/data/tilesets/boundingVolumeS2/s2MaximumHeightInvalidType.json @@ -0,0 +1,24 @@ +{ + "asset": { + "version": "1.1" + }, + "extensionsUsed": [ + "3DTILES_bounding_volume_S2" + ], + "extensionsRequired": [ + "3DTILES_bounding_volume_S2" + ], + "geometricError": 2.0, + "root": { + "geometricError": 1.0, + "boundingVolume": { + "extensions": { + "3DTILES_bounding_volume_S2": { + "token": "1", + "minimumHeight": 0, + "maximumHeight": "NOT_A_NUMBER" + } + } + } + } +} \ No newline at end of file diff --git a/specs/data/tilesets/boundingVolumeS2/s2MinimumHeightGreaterThanMaximumHeight.json b/specs/data/tilesets/boundingVolumeS2/s2MinimumHeightGreaterThanMaximumHeight.json new file mode 100644 index 00000000..c18eb5fa --- /dev/null +++ b/specs/data/tilesets/boundingVolumeS2/s2MinimumHeightGreaterThanMaximumHeight.json @@ -0,0 +1,24 @@ +{ + "asset": { + "version": "1.1" + }, + "extensionsUsed": [ + "3DTILES_bounding_volume_S2" + ], + "extensionsRequired": [ + "3DTILES_bounding_volume_S2" + ], + "geometricError": 2.0, + "root": { + "geometricError": 1.0, + "boundingVolume": { + "extensions": { + "3DTILES_bounding_volume_S2": { + "token": "1", + "minimumHeight": 100, + "maximumHeight": 99 + } + } + } + } +} \ No newline at end of file diff --git a/specs/data/tilesets/boundingVolumeS2/s2MinimumHeightInvalidType.json b/specs/data/tilesets/boundingVolumeS2/s2MinimumHeightInvalidType.json new file mode 100644 index 00000000..acd3e394 --- /dev/null +++ b/specs/data/tilesets/boundingVolumeS2/s2MinimumHeightInvalidType.json @@ -0,0 +1,24 @@ +{ + "asset": { + "version": "1.1" + }, + "extensionsUsed": [ + "3DTILES_bounding_volume_S2" + ], + "extensionsRequired": [ + "3DTILES_bounding_volume_S2" + ], + "geometricError": 2.0, + "root": { + "geometricError": 1.0, + "boundingVolume": { + "extensions": { + "3DTILES_bounding_volume_S2": { + "token": "1", + "minimumHeight": "NOT_A_NUMBER", + "maximumHeight": 10000 + } + } + } + } +} \ No newline at end of file diff --git a/specs/data/tilesets/boundingVolumeS2/s2TokenInvalidType.json b/specs/data/tilesets/boundingVolumeS2/s2TokenInvalidType.json new file mode 100644 index 00000000..fc0c94ce --- /dev/null +++ b/specs/data/tilesets/boundingVolumeS2/s2TokenInvalidType.json @@ -0,0 +1,24 @@ +{ + "asset": { + "version": "1.1" + }, + "extensionsUsed": [ + "3DTILES_bounding_volume_S2" + ], + "extensionsRequired": [ + "3DTILES_bounding_volume_S2" + ], + "geometricError": 2.0, + "root": { + "geometricError": 1.0, + "boundingVolume": { + "extensions": { + "3DTILES_bounding_volume_S2": { + "token": 12345, + "minimumHeight": 0, + "maximumHeight": 10000 + } + } + } + } +} \ No newline at end of file diff --git a/specs/data/tilesets/boundingVolumeS2/s2TokenInvalidValue.json b/specs/data/tilesets/boundingVolumeS2/s2TokenInvalidValue.json new file mode 100644 index 00000000..339e69b9 --- /dev/null +++ b/specs/data/tilesets/boundingVolumeS2/s2TokenInvalidValue.json @@ -0,0 +1,24 @@ +{ + "asset": { + "version": "1.1" + }, + "extensionsUsed": [ + "3DTILES_bounding_volume_S2" + ], + "extensionsRequired": [ + "3DTILES_bounding_volume_S2" + ], + "geometricError": 2.0, + "root": { + "geometricError": 1.0, + "boundingVolume": { + "extensions": { + "3DTILES_bounding_volume_S2": { + "token": "NOT_A_VALID_TOKEN", + "minimumHeight": 0, + "maximumHeight": 10000 + } + } + } + } +} \ No newline at end of file diff --git a/specs/data/tilesets/boundingVolumeS2/validTilesetWithS2.json b/specs/data/tilesets/boundingVolumeS2/validTilesetWithS2.json new file mode 100644 index 00000000..703c03c0 --- /dev/null +++ b/specs/data/tilesets/boundingVolumeS2/validTilesetWithS2.json @@ -0,0 +1,24 @@ +{ + "asset": { + "version": "1.1" + }, + "extensionsUsed": [ + "3DTILES_bounding_volume_S2" + ], + "extensionsRequired": [ + "3DTILES_bounding_volume_S2" + ], + "geometricError": 2.0, + "root": { + "geometricError": 1.0, + "boundingVolume": { + "extensions": { + "3DTILES_bounding_volume_S2": { + "token": "1", + "minimumHeight": 0, + "maximumHeight": 10000 + } + } + } + } +} \ No newline at end of file diff --git a/src/issues/SemanticValidationIssues.ts b/src/issues/SemanticValidationIssues.ts index af8ee350..41d4c9e7 100644 --- a/src/issues/SemanticValidationIssues.ts +++ b/src/issues/SemanticValidationIssues.ts @@ -337,7 +337,15 @@ export class SemanticValidationIssues { const issue = new ValidationIssue(type, path, message, severity); return issue; } - + static EXTENSION_NOT_SUPPORTED(path: string, extensionName: string) { + const type = "EXTENSION_NOT_SUPPORTED"; + const severity = ValidationIssueSeverity.WARNING; + const message = + `The extension '${extensionName}' was used, but ` + + `is not supported`; + const issue = new ValidationIssue(type, path, message, severity); + return issue; + } static SEMANTIC_UNKNOWN( path: string, propertyName: string, diff --git a/src/main.ts b/src/main.ts index 898c860f..aa7d2cca 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,16 @@ //eslint-disable-next-line const yargs = require("yargs/yargs"); +import { BoundingVolumeS2Validator } from "./validation/extensions/BoundingVolumeS2Validator"; +import { ExtensionsValidator } from "./validation/ExtensionsValidator"; import { ValidatorMain } from "./ValidatorMain"; +// XXX Experimental extension validation +const s2Validator = new BoundingVolumeS2Validator(""); +const performDefaultValidation = false; +ExtensionsValidator.register("3DTILES_bounding_volume_S2", s2Validator, performDefaultValidation); +// XXX Experimental extension validation + const args = yargs(process.argv.slice(1)) .help("help") .alias("help", "h") diff --git a/src/validation/BoundingVolumeValidator.ts b/src/validation/BoundingVolumeValidator.ts index 2a61eb6a..368bb937 100644 --- a/src/validation/BoundingVolumeValidator.ts +++ b/src/validation/BoundingVolumeValidator.ts @@ -3,6 +3,7 @@ import { defined } from "../base/defined"; import { ValidationContext } from "./ValidationContext"; import { BasicValidator } from "./BasicValidator"; import { RootPropertyValidator } from "./RootPropertyValidator"; +import { ExtensionsValidator } from "./ExtensionsValidator"; import { BoundingVolume } from "../structure/BoundingVolume"; @@ -25,11 +26,11 @@ export class BoundingVolumeValidator { * @param context The `ValidationContext` that any issues will be added to * @returns Whether the given object is valid */ - static validateBoundingVolume( + static async validateBoundingVolume( boundingVolumePath: string, boundingVolume: BoundingVolume, context: ValidationContext - ): boolean { + ): Promise { // Make sure that the given value is an object if ( !BasicValidator.validateObject( @@ -56,6 +57,46 @@ export class BoundingVolumeValidator { result = false; } + const extensionsValidationResult = + await ExtensionsValidator.validateExtensions( + boundingVolumePath, + "boundingVolume", + boundingVolume, + context + ); + if (!extensionsValidationResult.allValid) { + result = false; + } + if (extensionsValidationResult.performDefaultValidation) { + if ( + !BoundingVolumeValidator.validateBoundingVolumeInternal( + boundingVolumePath, + boundingVolume, + context + ) + ) { + result = false; + } + } + return result; + } + + /** + * Implementation for validateBoundingVolume + * + * @param boundingVolumePath The path that indicates the location of + * the given object, to be used in the validation issue message. + * @param boundingVolume The object to validate + * @param context The `ValidationContext` that any issues will be added to + * @returns Whether the given object is valid + */ + private static validateBoundingVolumeInternal( + boundingVolumePath: string, + boundingVolume: BoundingVolume, + context: ValidationContext + ): boolean { + let result = true; + const box = boundingVolume.box; const region = boundingVolume.region; const sphere = boundingVolume.sphere; diff --git a/src/validation/ContentValidator.ts b/src/validation/ContentValidator.ts index 05e6de0e..1a737689 100644 --- a/src/validation/ContentValidator.ts +++ b/src/validation/ContentValidator.ts @@ -32,12 +32,12 @@ export class ContentValidator { * @param context The `ValidationContext` that any issues will be added to * @returns Whether the given object was valid */ - static validateContent( + static async validateContent( contentPath: string, content: Content, validationState: ValidationState, context: ValidationContext - ): boolean { + ): Promise { // Make sure that the given value is an object if ( !BasicValidator.validateObject(contentPath, "content", content, context) @@ -129,13 +129,12 @@ export class ContentValidator { const boundingVolume = content.boundingVolume; const boundingVolumePath = contentPath + "/boundingVolume"; if (defined(boundingVolume)) { - if ( - !BoundingVolumeValidator.validateBoundingVolume( - boundingVolumePath, - boundingVolume!, - context - ) - ) { + const boundingVolumeValid = await BoundingVolumeValidator.validateBoundingVolume( + boundingVolumePath, + boundingVolume!, + context + ); + if (!boundingVolumeValid) { result = false; } } diff --git a/src/validation/ExtensionsValidationResult.ts b/src/validation/ExtensionsValidationResult.ts new file mode 100644 index 00000000..6000970b --- /dev/null +++ b/src/validation/ExtensionsValidationResult.ts @@ -0,0 +1,5 @@ + +export interface ExtensionsValidationResult { + allValid: boolean; + performDefaultValidation: boolean; +} diff --git a/src/validation/ExtensionsValidator.ts b/src/validation/ExtensionsValidator.ts new file mode 100644 index 00000000..059b14c2 --- /dev/null +++ b/src/validation/ExtensionsValidator.ts @@ -0,0 +1,99 @@ +import { defined } from "../base/defined"; + +import { Validator } from "./Validator"; +import { ValidationContext } from "./ValidationContext"; + +import { RootProperty } from "../structure/RootProperty"; + +import { SemanticValidationIssues } from "../issues/SemanticValidationIssues"; +import { ExtensionsValidationResult } from "./ExtensionsValidationResult"; + +/** + * @private + * @experimental + */ +export class ExtensionsValidator { + static readonly extensionValidators = new Map< + string, + Validator + >(); + static readonly performDefaultValidations = new Map(); + + static register( + extensionName: string, + extensionValidator: Validator, + performDefaultValidation: boolean + ) { + ExtensionsValidator.extensionValidators.set( + extensionName, + extensionValidator + ); + ExtensionsValidator.performDefaultValidations.set( + extensionName, + performDefaultValidation + ); + } + + static async validateExtensions( + path: string, + name: string, + rootProperty: RootProperty, + context: ValidationContext + ): Promise { + + const extensions = rootProperty.extensions; + if (!defined(extensions)) { + return { + allValid: true, + performDefaultValidation: true, + }; + } + let allValid = true; + let performDefaultValidation = true; + + const extensionNames = Object.keys(extensions!); + for (const extensionName of extensionNames) { + const extensionValidator = + ExtensionsValidator.extensionValidators.get(extensionName); + if (!defined(extensionValidator)) { + const issue = SemanticValidationIssues.EXTENSION_NOT_SUPPORTED( + path, + extensionName + ); + context.addIssue(issue); + } else { + const extension = extensions![extensionName]; + const isValid = await ExtensionsValidator.validateExtension( + path, + extensionName, + extension, + extensionValidator!, + context + ); + if (!isValid) { + allValid = false; + } + const performDefault = + ExtensionsValidator.performDefaultValidations.get(extensionName); + if (performDefault === false) { + performDefaultValidation = false; + } + } + } + return { + allValid: allValid, + performDefaultValidation: performDefaultValidation, + }; + } + + private static async validateExtension( + path: string, + name: string, + extension: any, + extensionValidator: Validator, + context: ValidationContext + ): Promise { + const result = await extensionValidator.validateObject(extension, context); + return result; + } +} diff --git a/src/validation/TileValidator.ts b/src/validation/TileValidator.ts index 1f6f4392..e026c6a5 100644 --- a/src/validation/TileValidator.ts +++ b/src/validation/TileValidator.ts @@ -49,12 +49,12 @@ export class TileValidator { * @param context The `ValidationContext` * @returns Whether the object was valid */ - static validateTile( + static async validateTile( tilePath: string, tile: Tile, validationState: ValidationState, context: ValidationContext - ): boolean { + ): Promise { // Make sure that the given value is an object if (!BasicValidator.validateObject(tilePath, "tile", tile, context)) { return false; @@ -78,13 +78,12 @@ export class TileValidator { const boundingVolume = tile.boundingVolume; const boundingVolumePath = tilePath + "/boundingVolume"; // The boundingVolume MUST be defined - if ( - !BoundingVolumeValidator.validateBoundingVolume( - boundingVolumePath, - boundingVolume, - context - ) - ) { + const boundingVolumeValid = await BoundingVolumeValidator.validateBoundingVolume( + boundingVolumePath, + boundingVolume, + context + ); + if (!boundingVolumeValid) { result = false; } @@ -243,13 +242,14 @@ export class TileValidator { result = false; } } else { + const simpleTileValid = await TileValidator.validateSimpleTile( + tilePath, + tile, + validationState, + context + ); if ( - !TileValidator.validateSimpleTile( - tilePath, - tile, - validationState, - context - ) + !simpleTileValid ) { result = false; } @@ -269,12 +269,12 @@ export class TileValidator { * @param context The `ValidationContext` * @returns Whether the object was valid */ - private static validateSimpleTile( + private static async validateSimpleTile( tilePath: string, tile: Tile, validationState: ValidationState, context: ValidationContext - ): boolean { + ): Promise { let result = true; // Note: The check that content and contents may not be present @@ -284,13 +284,14 @@ export class TileValidator { const content = tile.content; const contentPath = tilePath + "/content"; if (defined(content)) { + const contentValid = await ContentValidator.validateContent( + contentPath, + content!, + validationState, + context + ); if ( - !ContentValidator.validateContent( - contentPath, - content!, - validationState, - context - ) + !contentValid ) { result = false; } @@ -319,13 +320,14 @@ export class TileValidator { for (let index = 0; index < contents!.length; index++) { const contentsElementPath = contentsPath + "/" + index; const contentsElement = contents![index]; + const contentValid = await ContentValidator.validateContent( + contentsElementPath, + contentsElement, + validationState, + context + ); if ( - !ContentValidator.validateContent( - contentsElementPath, - contentsElement, - validationState, - context - ) + !contentValid ) { result = false; } diff --git a/src/validation/TilesetTraversingValidator.ts b/src/validation/TilesetTraversingValidator.ts index 15bf6890..3aaaa995 100644 --- a/src/validation/TilesetTraversingValidator.ts +++ b/src/validation/TilesetTraversingValidator.ts @@ -194,7 +194,8 @@ export class TilesetTraversingValidator { const tile = traversedTile.asTile(); // Validate the tile itself - if (!TileValidator.validateTile(path, tile, validationState, context)) { + const tileValid = await TileValidator.validateTile(path, tile, validationState, context); + if (!tileValid) { return false; } diff --git a/src/validation/extensions/BoundingVolumeS2ValidationIssues.ts b/src/validation/extensions/BoundingVolumeS2ValidationIssues.ts new file mode 100644 index 00000000..619f017e --- /dev/null +++ b/src/validation/extensions/BoundingVolumeS2ValidationIssues.ts @@ -0,0 +1,12 @@ +import { ValidationIssue } from "../../validation/ValidationIssue"; +import { ValidationIssueSeverity } from "../../validation/ValidationIssueSeverity"; + +export class BoundingVolumeS2ValidationIssues { + static S2_TOKEN_INVALID(path: string, message: string) { + const type = "S2_TOKEN_INVALID"; + const severity = ValidationIssueSeverity.WARNING; + const issue = new ValidationIssue(type, path, message, severity); + return issue; + } + +} diff --git a/src/validation/extensions/BoundingVolumeS2Validator.ts b/src/validation/extensions/BoundingVolumeS2Validator.ts new file mode 100644 index 00000000..68eb5e63 --- /dev/null +++ b/src/validation/extensions/BoundingVolumeS2Validator.ts @@ -0,0 +1,137 @@ +import { defined } from "../../base/defined"; + +import { Validator } from "../Validator"; +import { ValidationContext } from "../ValidationContext"; +import { BasicValidator } from "../BasicValidator"; +import { RootPropertyValidator } from "../RootPropertyValidator"; + +import { SemanticValidationIssues } from "../../issues/SemanticValidationIssues"; +import { BoundingVolumeS2ValidationIssues } from "./BoundingVolumeS2ValidationIssues"; + +/** + * A class for the validation of `3DTILES_bounding_volume_S2` extension objects + * + * @private + */ +export class BoundingVolumeS2Validator implements Validator { + + // TODO The path should probably be passed into validateObject + private readonly _path: string; + + constructor(path: string) { + this._path = path; + } + + /** + * Performs the validation to ensure that the given object is a + * valid `3DTILES_bounding_volume_S2` object. + * + * @param object The object to validate + * @param context The `ValidationContext` that any issues will be added to + * @returns Whether the object was valid + */ + async validateObject( + object: any, + context: ValidationContext + ): Promise { + const path = this._path; + + // Make sure that the given value is an object + if (!BasicValidator.validateObject(path, "object", object, context)) { + return false; + } + + let result = true; + + // Validate the object as a RootProperty + if ( + !RootPropertyValidator.validateRootProperty( + path, + "object", + object, + context + ) + ) { + result = false; + } + + // Validate the token + const token = object.token; + const tokenPath = path + "/token"; + // The token MUST be defined + // The token MUST be a string + if (!BasicValidator.validateString(tokenPath, "token", token, context)) { + result = false; + } else { + // The token MUST be a valid S2 token + if (!BoundingVolumeS2Validator.isValidToken(token)) { + const message = `The S2 token '${token}' is not valid`; + const issue = BoundingVolumeS2ValidationIssues.S2_TOKEN_INVALID( + tokenPath, + message + ); + context.addIssue(issue); + result = false; + } + } + + // Validate the minimumHeight + const minimumHeight = object.minimumHeight; + const minimumHeightPath = path + "/minimumHeight"; + // The minimumHeight MUST be a number + if ( + !BasicValidator.validateNumber( + minimumHeightPath, + "minimumHeight", + minimumHeight, + context + ) + ) { + result = false; + } + + // Validate the maximumHeight + const maximumHeight = object.maximumHeight; + const maximumHeightPath = path + "/maximumHeight"; + // The maximumHeight MUST be a number + if ( + !BasicValidator.validateNumber( + maximumHeightPath, + "maximumHeight", + maximumHeight, + context + ) + ) { + result = false; + } + + // The minimumHeight MUST NOT be larger + // than the maximumHeight + if (defined(minimumHeight) && defined(maximumHeight)) { + if (minimumHeight > maximumHeight) { + const message = + `The minimumHeight may not be larger than the ` + + `maximumHeight, but the minimumHeight is ${minimumHeight} ` + + `and the maximum height is ${maximumHeight}`; + const issue = SemanticValidationIssues.BOUNDING_VOLUME_INCONSISTENT( + path, + message + ); + context.addIssue(issue); + result = false; + } + } + + return result; + } + + private static isValidToken(token: string): boolean { + // According to cesium/Source/Core/S2Cell.js + if (!/^[0-9a-fA-F]{1,16}$/.test(token)) { + return false; + } + // Further constraints could be added here (e.g. that + // the first digit is only a value in [0,5] ...) + return true; + } +} From 841626f3d6a577cdbd7949cc5f3861cdaa99ae3c Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Tue, 11 Oct 2022 17:49:17 +0200 Subject: [PATCH 02/22] Generally pass path to validateObject method --- src/tileFormats/B3dmValidator.ts | 29 ++++++------ src/tileFormats/CmptValidator.ts | 31 ++++++------- src/tileFormats/GltfValidator.ts | 15 ++----- src/tileFormats/I3dmValidator.ts | 46 ++++++++++---------- src/tileFormats/PntsValidator.ts | 33 +++++++------- src/validation/ContentDataValidator.ts | 25 ++++++----- src/validation/ExtensionsValidator.ts | 2 +- src/validation/SchemaValidator.ts | 5 ++- src/validation/SubtreeValidator.ts | 35 ++++++--------- src/validation/TilesetTraversingValidator.ts | 3 +- src/validation/TilesetValidator.ts | 9 ++-- src/validation/Validator.ts | 2 +- src/validation/Validators.ts | 3 +- 13 files changed, 110 insertions(+), 128 deletions(-) diff --git a/src/tileFormats/B3dmValidator.ts b/src/tileFormats/B3dmValidator.ts index 7377e75b..c90c4fd3 100644 --- a/src/tileFormats/B3dmValidator.ts +++ b/src/tileFormats/B3dmValidator.ts @@ -34,13 +34,9 @@ const featureTableSemantics = { * given as a Buffer. */ export class B3dmValidator implements Validator { - private _uri: string; - - constructor(uri: string) { - this._uri = uri; - } async validateObject( + uri: string, input: Buffer, context: ValidationContext ): Promise { @@ -49,9 +45,9 @@ export class B3dmValidator implements Validator { // will be stored as the 'internal issues' of a // single content validation issue. const derivedContext = context.derive("."); - const result = await this.validateObjectInternal(input, derivedContext); + const result = await this.validateObjectInternal(uri, input, derivedContext); const derivedResult = derivedContext.getResult(); - const issue = ContentValidationIssues.createFrom(this._uri, derivedResult); + const issue = ContentValidationIssues.createFrom(uri, derivedResult); if (issue) { context.addIssue(issue); } @@ -59,6 +55,7 @@ export class B3dmValidator implements Validator { } async validateObjectInternal( + uri: string, input: Buffer, context: ValidationContext ): Promise { @@ -66,7 +63,7 @@ export class B3dmValidator implements Validator { if ( !TileFormatValidator.validateHeader( - this._uri, + uri, input, headerByteLength, "b3dm", @@ -94,7 +91,7 @@ export class B3dmValidator implements Validator { `[batchTableByteLength]. The new format is ` + `[featureTableJsonByteLength] [featureTableBinaryByteLength] ` + `[batchTableJsonByteLength] [batchTableBinaryByteLength].`; - const issue = BinaryValidationIssues.BINARY_INVALID(this._uri, message); + const issue = BinaryValidationIssues.BINARY_INVALID(uri, message); context.addIssue(issue); return false; } @@ -104,13 +101,13 @@ export class B3dmValidator implements Validator { `[batchTableBinaryByteLength] [batchLength]. The new format is ` + `[featureTableJsonByteLength] [featureTableBinaryByteLength] ` + `[batchTableJsonByteLength] [batchTableBinaryByteLength].`; - const issue = BinaryValidationIssues.BINARY_INVALID(this._uri, message); + const issue = BinaryValidationIssues.BINARY_INVALID(uri, message); context.addIssue(issue); return false; } const binaryTableData = TileFormatValidator.extractBinaryTableData( - this._uri, + uri, input, headerByteLength, true, @@ -129,7 +126,7 @@ export class B3dmValidator implements Validator { const featuresLength = featureTableJson.BATCH_LENGTH; if (!defined(featuresLength)) { const message = `Feature table must contain a BATCH_LENGTH property.`; - const issue = IoValidationIssues.JSON_PARSE_ERROR(this._uri, message); + const issue = IoValidationIssues.JSON_PARSE_ERROR(uri, message); context.addIssue(issue); return false; } @@ -142,7 +139,7 @@ export class B3dmValidator implements Validator { ); if (defined(featureTableMessage)) { const issue = ContentValidationIssues.CONTENT_JSON_INVALID( - this._uri, + uri, featureTableMessage! ); context.addIssue(issue); @@ -156,15 +153,15 @@ export class B3dmValidator implements Validator { ); if (defined(batchTableMessage)) { const issue = ContentValidationIssues.CONTENT_JSON_INVALID( - this._uri, + uri, batchTableMessage! ); context.addIssue(issue); return false; } - const gltfValidator = new GltfValidator(this._uri); - const result = await gltfValidator.validateObject(glbData, context); + const gltfValidator = new GltfValidator(); + const result = await gltfValidator.validateObject(uri, glbData, context); return result; } } diff --git a/src/tileFormats/CmptValidator.ts b/src/tileFormats/CmptValidator.ts index 1b8338d3..9156c572 100644 --- a/src/tileFormats/CmptValidator.ts +++ b/src/tileFormats/CmptValidator.ts @@ -18,13 +18,9 @@ import { BinaryValidationIssues } from "../issues/BinaryValidationIssues"; * given as a Buffer. */ export class CmptValidator implements Validator { - private _uri: string; - - constructor(uri: string) { - this._uri = uri; - } async validateObject( + uri: string, input: Buffer, context: ValidationContext ): Promise { @@ -33,9 +29,9 @@ export class CmptValidator implements Validator { // will be stored as the 'internal issues' of a // single content validation issue. const derivedContext = context.derive("."); - const result = await this.validateObjectInternal(input, derivedContext); + const result = await this.validateObjectInternal(uri, input, derivedContext); const derivedResult = derivedContext.getResult(); - const issue = ContentValidationIssues.createFrom(this._uri, derivedResult); + const issue = ContentValidationIssues.createFrom(uri, derivedResult); if (issue) { context.addIssue(issue); } @@ -43,6 +39,7 @@ export class CmptValidator implements Validator { } async validateObjectInternal( + uri: string, input: Buffer, context: ValidationContext ): Promise { @@ -50,7 +47,7 @@ export class CmptValidator implements Validator { if ( !TileFormatValidator.validateHeader( - this._uri, + uri, input, headerByteLength, "cmpt", @@ -68,13 +65,13 @@ export class CmptValidator implements Validator { if (byteOffset + 12 > byteLength) { const message = "Cannot read byte length from inner tile, exceeds cmpt tile's byte length."; - const issue = BinaryValidationIssues.BINARY_INVALID(this._uri, message); + const issue = BinaryValidationIssues.BINARY_INVALID(uri, message); context.addIssue(issue); return false; } if (byteOffset % 8 > 0) { const message = "Inner tile must be aligned to an 8-byte boundary"; - const issue = BinaryValidationIssues.BINARY_INVALID(this._uri, message); + const issue = BinaryValidationIssues.BINARY_INVALID(uri, message); context.addIssue(issue); return false; } @@ -87,8 +84,9 @@ export class CmptValidator implements Validator { ); if (innerTileMagic === "b3dm") { - const innerValidator = new B3dmValidator(this._uri); + const innerValidator = new B3dmValidator(); const innerResult = await innerValidator.validateObject( + uri, innerTile, context ); @@ -96,8 +94,9 @@ export class CmptValidator implements Validator { result = false; } } else if (innerTileMagic === "i3dm") { - const innerValidator = new I3dmValidator(this._uri); + const innerValidator = new I3dmValidator(); const innerResult = await innerValidator.validateObject( + uri, innerTile, context ); @@ -105,8 +104,9 @@ export class CmptValidator implements Validator { result = false; } } else if (innerTileMagic === "pnts") { - const innerValidator = new PntsValidator(this._uri); + const innerValidator = new PntsValidator(); const innerResult = await innerValidator.validateObject( + uri, innerTile, context ); @@ -114,8 +114,9 @@ export class CmptValidator implements Validator { result = false; } } else if (innerTileMagic === "cmpt") { - const innerValidator = new CmptValidator(this._uri); + const innerValidator = new CmptValidator(); const innerResult = await innerValidator.validateObject( + uri, innerTile, context ); @@ -124,7 +125,7 @@ export class CmptValidator implements Validator { } } else { const message = `Invalid inner tile magic: ${innerTileMagic}`; - const issue = BinaryValidationIssues.BINARY_INVALID(this._uri, message); + const issue = BinaryValidationIssues.BINARY_INVALID(uri, message); context.addIssue(issue); result = false; } diff --git a/src/tileFormats/GltfValidator.ts b/src/tileFormats/GltfValidator.ts index 96742fcc..13e2b352 100644 --- a/src/tileFormats/GltfValidator.ts +++ b/src/tileFormats/GltfValidator.ts @@ -16,13 +16,6 @@ const validator = require("gltf-validator"); * in a Buffer. */ export class GltfValidator implements Validator { - private _baseDirectory: string; - private _uri: string; - - constructor(uri: string) { - this._uri = uri; - this._baseDirectory = path.dirname(uri); - } /** * Creates a `ValidationIssue` object for the given 'message' object @@ -51,12 +44,13 @@ export class GltfValidator implements Validator { } async validateObject( + uri: string, input: Buffer, context: ValidationContext ): Promise { const resourceResolver = context.getResourceResolver(); - const gltfResourceResolver = resourceResolver.derive(this._baseDirectory); - const uri = this._uri; + const baseDirectory = path.dirname(uri); + const gltfResourceResolver = resourceResolver.derive(baseDirectory); let gltfResult = undefined; try { gltfResult = await validator.validateBytes(input, { @@ -72,10 +66,9 @@ export class GltfValidator implements Validator { }, }); } catch (error) { - const path = uri; const message = `Content ${uri} caused internal validation error: ${error}`; const issue = ContentValidationIssues.CONTENT_VALIDATION_ERROR( - path, + uri, message ); context.addIssue(issue); diff --git a/src/tileFormats/I3dmValidator.ts b/src/tileFormats/I3dmValidator.ts index 817c4d8a..3bd1547c 100644 --- a/src/tileFormats/I3dmValidator.ts +++ b/src/tileFormats/I3dmValidator.ts @@ -96,13 +96,9 @@ const featureTableSemantics = { * given as a Buffer. */ export class I3dmValidator implements Validator { - private _uri: string; - - constructor(uri: string) { - this._uri = uri; - } async validateObject( + uri: string, input: Buffer, context: ValidationContext ): Promise { @@ -111,9 +107,9 @@ export class I3dmValidator implements Validator { // will be stored as the 'internal issues' of a // single content validation issue. const derivedContext = context.derive("."); - const result = await this.validateObjectInternal(input, derivedContext); + const result = await this.validateObjectInternal(uri, input, derivedContext); const derivedResult = derivedContext.getResult(); - const issue = ContentValidationIssues.createFrom(this._uri, derivedResult); + const issue = ContentValidationIssues.createFrom(uri, derivedResult); if (issue) { context.addIssue(issue); } @@ -121,6 +117,7 @@ export class I3dmValidator implements Validator { } async validateObjectInternal( + uri: string, input: Buffer, context: ValidationContext ): Promise { @@ -128,7 +125,7 @@ export class I3dmValidator implements Validator { if ( !TileFormatValidator.validateHeader( - this._uri, + uri, input, headerByteLength, "i3dm", @@ -142,7 +139,7 @@ export class I3dmValidator implements Validator { if (gltfFormat > 1) { const issue = BinaryValidationIssues.BINARY_INVALID_VALUE( - this._uri, + uri, "gltfFormat", "<=1", gltfFormat @@ -153,7 +150,7 @@ export class I3dmValidator implements Validator { const hasEmbeddedGlb = gltfFormat === 1; const binaryTableData = TileFormatValidator.extractBinaryTableData( - this._uri, + uri, input, headerByteLength, hasEmbeddedGlb, @@ -174,7 +171,7 @@ export class I3dmValidator implements Validator { const featuresLength = featureTableJson!.INSTANCES_LENGTH; if (!defined(featuresLength)) { const message = `Feature table must contain a INSTANCES_LENGTH property.`; - const issue = IoValidationIssues.JSON_PARSE_ERROR(this._uri, message); + const issue = IoValidationIssues.JSON_PARSE_ERROR(uri, message); context.addIssue(issue); } @@ -184,7 +181,7 @@ export class I3dmValidator implements Validator { ) { const message = "Feature table must contain either the POSITION or POSITION_QUANTIZED property."; - const issue = IoValidationIssues.JSON_PARSE_ERROR(this._uri, message); + const issue = IoValidationIssues.JSON_PARSE_ERROR(uri, message); context.addIssue(issue); result = false; } @@ -195,7 +192,7 @@ export class I3dmValidator implements Validator { ) { const message = "Feature table property NORMAL_RIGHT is required when NORMAL_UP is present."; - const issue = IoValidationIssues.JSON_PARSE_ERROR(this._uri, message); + const issue = IoValidationIssues.JSON_PARSE_ERROR(uri, message); context.addIssue(issue); result = false; } @@ -206,7 +203,7 @@ export class I3dmValidator implements Validator { ) { const message = "Feature table property NORMAL_UP is required when NORMAL_RIGHT is present."; - const issue = IoValidationIssues.JSON_PARSE_ERROR(this._uri, message); + const issue = IoValidationIssues.JSON_PARSE_ERROR(uri, message); context.addIssue(issue); result = false; } @@ -217,7 +214,7 @@ export class I3dmValidator implements Validator { ) { const message = "Feature table property NORMAL_RIGHT_OCT32P is required when NORMAL_UP_OCT32P is present."; - const issue = IoValidationIssues.JSON_PARSE_ERROR(this._uri, message); + const issue = IoValidationIssues.JSON_PARSE_ERROR(uri, message); context.addIssue(issue); result = false; } @@ -228,7 +225,7 @@ export class I3dmValidator implements Validator { ) { const message = "Feature table property NORMAL_UP_OCT32P is required when NORMAL_RIGHT_OCT32P is present."; - const issue = IoValidationIssues.JSON_PARSE_ERROR(this._uri, message); + const issue = IoValidationIssues.JSON_PARSE_ERROR(uri, message); context.addIssue(issue); result = false; } @@ -240,7 +237,7 @@ export class I3dmValidator implements Validator { ) { const message = "Feature table properties QUANTIZED_VOLUME_OFFSET and QUANTIZED_VOLUME_SCALE are required when POSITION_QUANTIZED is present."; - const issue = IoValidationIssues.JSON_PARSE_ERROR(this._uri, message); + const issue = IoValidationIssues.JSON_PARSE_ERROR(uri, message); context.addIssue(issue); result = false; } @@ -253,7 +250,7 @@ export class I3dmValidator implements Validator { ); if (defined(featureTableMessage)) { const issue = ContentValidationIssues.CONTENT_JSON_INVALID( - this._uri, + uri, featureTableMessage! ); context.addIssue(issue); @@ -267,7 +264,7 @@ export class I3dmValidator implements Validator { ); if (defined(batchTableMessage)) { const issue = ContentValidationIssues.CONTENT_JSON_INVALID( - this._uri, + uri, batchTableMessage! ); context.addIssue(issue); @@ -276,8 +273,8 @@ export class I3dmValidator implements Validator { // If the GLB data was embdedded, validate it directly if (hasEmbeddedGlb) { - const gltfValidator = new GltfValidator(this._uri); - const gltfResult = await gltfValidator.validateObject(glbData, context); + const gltfValidator = new GltfValidator(); + const gltfResult = await gltfValidator.validateObject(uri, glbData, context); if (!gltfResult) { result = false; } @@ -290,7 +287,7 @@ export class I3dmValidator implements Validator { if (!defined(resolvedGlbData)) { const message = `Could not resolve GLB URI ${glbUri} from I3DM`; const issue = ContentValidationIssues.CONTENT_VALIDATION_ERROR( - this._uri, + uri, message ); context.addIssue(issue); @@ -302,8 +299,9 @@ export class I3dmValidator implements Validator { // single content validation issue. const glbDirectory = path.dirname(glbUri); const derivedContext = context.derive(glbDirectory); - const gltfValidator = new GltfValidator(this._uri); + const gltfValidator = new GltfValidator(); const gltfResult = await gltfValidator.validateObject( + uri, resolvedGlbData!, derivedContext ); @@ -312,7 +310,7 @@ export class I3dmValidator implements Validator { } const derivedResult = derivedContext.getResult(); const issue = ContentValidationIssues.createFrom( - this._uri, + uri, derivedResult ); if (issue) { diff --git a/src/tileFormats/PntsValidator.ts b/src/tileFormats/PntsValidator.ts index 98dd6ffb..be288a95 100644 --- a/src/tileFormats/PntsValidator.ts +++ b/src/tileFormats/PntsValidator.ts @@ -94,13 +94,9 @@ const featureTableSemantics = { * given as a Buffer. */ export class PntsValidator implements Validator { - private _uri: string; - - constructor(uri: string) { - this._uri = uri; - } async validateObject( + uri: string, input: Buffer, context: ValidationContext ): Promise { @@ -109,9 +105,9 @@ export class PntsValidator implements Validator { // will be stored as the 'internal issues' of a // single content validation issue. const derivedContext = context.derive("."); - const result = await this.validateObjectInternal(input, derivedContext); + const result = await this.validateObjectInternal(uri, input, derivedContext); const derivedResult = derivedContext.getResult(); - const issue = ContentValidationIssues.createFrom(this._uri, derivedResult); + const issue = ContentValidationIssues.createFrom(uri, derivedResult); if (issue) { context.addIssue(issue); } @@ -119,6 +115,7 @@ export class PntsValidator implements Validator { } async validateObjectInternal( + uri: string, input: Buffer, context: ValidationContext ): Promise { @@ -126,7 +123,7 @@ export class PntsValidator implements Validator { if ( !TileFormatValidator.validateHeader( - this._uri, + uri, input, headerByteLength, "pnts", @@ -137,7 +134,7 @@ export class PntsValidator implements Validator { } const binaryTableData = TileFormatValidator.extractBinaryTableData( - this._uri, + uri, input, headerByteLength, false, @@ -158,7 +155,7 @@ export class PntsValidator implements Validator { const pointsLength = featureTableJson.POINTS_LENGTH; if (!defined(pointsLength)) { const message = "Feature table must contain a POINTS_LENGTH property."; - const issue = IoValidationIssues.JSON_PARSE_ERROR(this._uri, message); + const issue = IoValidationIssues.JSON_PARSE_ERROR(uri, message); context.addIssue(issue); result = false; } @@ -169,7 +166,7 @@ export class PntsValidator implements Validator { ) { const message = "Feature table must contain either the POSITION or POSITION_QUANTIZED property."; - const issue = IoValidationIssues.JSON_PARSE_ERROR(this._uri, message); + const issue = IoValidationIssues.JSON_PARSE_ERROR(uri, message); context.addIssue(issue); result = false; } @@ -181,7 +178,7 @@ export class PntsValidator implements Validator { ) { const message = "Feature table properties QUANTIZED_VOLUME_OFFSET and QUANTIZED_VOLUME_SCALE are required when POSITION_QUANTIZED is present."; - const issue = IoValidationIssues.JSON_PARSE_ERROR(this._uri, message); + const issue = IoValidationIssues.JSON_PARSE_ERROR(uri, message); context.addIssue(issue); result = false; } @@ -192,7 +189,7 @@ export class PntsValidator implements Validator { ) { const message = "Feature table property BATCH_LENGTH is required when BATCH_ID is present."; - const issue = IoValidationIssues.JSON_PARSE_ERROR(this._uri, message); + const issue = IoValidationIssues.JSON_PARSE_ERROR(uri, message); context.addIssue(issue); result = false; } @@ -203,7 +200,7 @@ export class PntsValidator implements Validator { ) { const message = "Feature table property BATCH_ID is required when BATCH_LENGTH is present."; - const issue = IoValidationIssues.JSON_PARSE_ERROR(this._uri, message); + const issue = IoValidationIssues.JSON_PARSE_ERROR(uri, message); context.addIssue(issue); result = false; } @@ -211,7 +208,7 @@ export class PntsValidator implements Validator { if (batchLength > pointsLength) { const message = "Feature table property BATCH_LENGTH must be less than or equal to POINTS_LENGTH."; - const issue = IoValidationIssues.JSON_PARSE_ERROR(this._uri, message); + const issue = IoValidationIssues.JSON_PARSE_ERROR(uri, message); context.addIssue(issue); result = false; } @@ -229,7 +226,7 @@ export class PntsValidator implements Validator { for (let i = 0; i < length; i++) { if (batchIds[i] >= featureTableJson.BATCH_LENGTH) { const message = 'All the BATCH_IDs must have values less than feature table property BATCH_LENGTH.'; - const issue = IoValidationIssues.JSON_PARSE_ERROR(this._uri, message); + const issue = IoValidationIssues.JSON_PARSE_ERROR(uri, message); context.addIssue(issue); } } @@ -244,7 +241,7 @@ export class PntsValidator implements Validator { ); if (defined(featureTableMessage)) { const issue = ContentValidationIssues.CONTENT_JSON_INVALID( - this._uri, + uri, featureTableMessage! ); context.addIssue(issue); @@ -258,7 +255,7 @@ export class PntsValidator implements Validator { ); if (defined(batchTableMessage)) { const issue = ContentValidationIssues.CONTENT_JSON_INVALID( - this._uri, + uri, batchTableMessage! ); context.addIssue(issue); diff --git a/src/validation/ContentDataValidator.ts b/src/validation/ContentDataValidator.ts index b4b31e15..7ddfd319 100644 --- a/src/validation/ContentDataValidator.ts +++ b/src/validation/ContentDataValidator.ts @@ -112,40 +112,40 @@ export class ContentDataValidator { const isGlb = ResourceTypes.isGlb(contentData); if (isGlb) { console.log("Validating GLB: " + contentUri); - const dataValidator = new GltfValidator(contentUri); - const result = await dataValidator.validateObject(contentData, context); + const dataValidator = new GltfValidator(); + const result = await dataValidator.validateObject(contentUri, contentData, context); return result; } const isB3dm = ResourceTypes.isB3dm(contentData); if (isB3dm) { console.log("Validating B3DM: " + contentUri); - const dataValidator = new B3dmValidator(contentUri); - const result = await dataValidator.validateObject(contentData, context); + const dataValidator = new B3dmValidator(); + const result = await dataValidator.validateObject(contentUri, contentData, context); return result; } const isI3dm = ResourceTypes.isI3dm(contentData); if (isI3dm) { console.log("Validating I3DM: " + contentUri); - const dataValidator = new I3dmValidator(contentUri); - const result = await dataValidator.validateObject(contentData, context); + const dataValidator = new I3dmValidator(); + const result = await dataValidator.validateObject(contentUri, contentData, context); return result; } const isPnts = ResourceTypes.isPnts(contentData); if (isPnts) { console.log("Validating PNTS: " + contentUri); - const dataValidator = new PntsValidator(contentUri); - const result = await dataValidator.validateObject(contentData, context); + const dataValidator = new PntsValidator(); + const result = await dataValidator.validateObject(contentUri, contentData, context); return result; } const isCmpt = ResourceTypes.isCmpt(contentData); if (isCmpt) { console.log("Validating CMPT: " + contentUri); - const dataValidator = new CmptValidator(contentUri); - const result = await dataValidator.validateObject(contentData, context); + const dataValidator = new CmptValidator(); + const result = await dataValidator.validateObject(contentUri, contentData, context); return result; } @@ -258,6 +258,7 @@ export class ContentDataValidator { const derivedContext = context.derive(dirName); const externalValidator = Validators.createDefaultTilesetValidator(); const result = await externalValidator.validateObject( + contentUri, parsedObject, derivedContext ); @@ -275,8 +276,8 @@ export class ContentDataValidator { // The parsed object has an 'asset', but is no tileset. // Assume that it is a glTF: console.log("Validating glTF: " + contentUri); - const gltfValidator = new GltfValidator(contentUri); - const result = await gltfValidator.validateObject(contentData, context); + const gltfValidator = new GltfValidator(); + const result = await gltfValidator.validateObject(contentUri, contentData, context); return result; } const path = contentPath; diff --git a/src/validation/ExtensionsValidator.ts b/src/validation/ExtensionsValidator.ts index 059b14c2..c963b349 100644 --- a/src/validation/ExtensionsValidator.ts +++ b/src/validation/ExtensionsValidator.ts @@ -93,7 +93,7 @@ export class ExtensionsValidator { extensionValidator: Validator, context: ValidationContext ): Promise { - const result = await extensionValidator.validateObject(extension, context); + const result = await extensionValidator.validateObject(path, extension, context); return result; } } diff --git a/src/validation/SchemaValidator.ts b/src/validation/SchemaValidator.ts index 827612c9..35ebaf8d 100644 --- a/src/validation/SchemaValidator.ts +++ b/src/validation/SchemaValidator.ts @@ -32,7 +32,7 @@ export class SchemaValidator implements Validator { ): Promise { try { const object: Schema = JSON.parse(input); - const result = await this.validateObject(object, context); + const result = await this.validateObject("", object, context); return result; } catch (error) { //console.log(error); @@ -52,10 +52,11 @@ export class SchemaValidator implements Validator { * and indicates whether the object was valid or not. */ async validateObject( + path: string, input: Schema, context: ValidationContext ): Promise { - return SchemaValidator.validateSchema("", input, context); + return SchemaValidator.validateSchema(path, input, context); } /** diff --git a/src/validation/SubtreeValidator.ts b/src/validation/SubtreeValidator.ts index bb73ade5..4ae484e6 100644 --- a/src/validation/SubtreeValidator.ts +++ b/src/validation/SubtreeValidator.ts @@ -38,10 +38,6 @@ import { RootPropertyValidator } from "./RootPropertyValidator"; * @private */ export class SubtreeValidator implements Validator { - /** - * The URI that the subtree data was read from - */ - private _uri: string; /** * The `ValidationState` that carries information about @@ -64,13 +60,6 @@ export class SubtreeValidator implements Validator { /** * Creates a new instance. * - * Preliminary: - * - * The given validator will be applied to the `Subtree` - * object, after it has been parsed from the JSON, but before - * any further validation takes place. - * - * @param uri The URI that the subtree data was read from * @param validationState The `ValidationState` * @param implicitTiling The `TileImplicitTiling` that * defines the expected structure of the subtree @@ -78,42 +67,42 @@ export class SubtreeValidator implements Validator { * will be used to resolve buffer URIs. */ constructor( - uri: string, validationState: ValidationState, implicitTiling: TileImplicitTiling | undefined, resourceResolver: ResourceResolver ) { - this._uri = uri; this._validationState = validationState; this._implicitTiling = implicitTiling; this._resourceResolver = resourceResolver; } /** - * Performs the validation of the given buffer, which is supposed to + * Implementation of the `Validator` interface that performs the + * validation of the given buffer, which is supposed to * contain subtree data, either in binary form or as JSON. * + * @param path The path for `ValidationIssue` instances * @param input The subtree data * @param context The `ValidationContext` * @returns A promise that resolves when the validation is finished * and indicates whether the object was valid or not. */ async validateObject( + path: string, input: Buffer, context: ValidationContext ): Promise { const isSubt = ResourceTypes.isSubt(input); if (isSubt) { - const result = await this.validateSubtreeBinaryData(input, context); + const result = await this.validateSubtreeBinaryData(path, input, context); return result; } const isJson = ResourceTypes.isProbablyJson(input); if (isJson) { - const result = await this.validateSubtreeJsonData(input, context); + const result = await this.validateSubtreeJsonData(path, input, context); return result; } const message = `Subtree input data was neither a subtree binary nor JSON`; - const path = this._uri; const issue = IoValidationIssues.IO_ERROR(path, message); context.addIssue(issue); return false; @@ -123,16 +112,17 @@ export class SubtreeValidator implements Validator { * Performs the validation of the given buffer, which contains the * data from a binary subtree file * + * @param path The path for `ValidationIssue` instances * @param input The contents of a binary subtree file * @param context The `ValidationContext` * @returns A promise that resolves when the validation is finished * and indicates whether the object was valid or not. */ private async validateSubtreeBinaryData( + path: string, input: Buffer, context: ValidationContext ): Promise { - const path = this._uri; // Validate the header length const headerByteLength = 24; @@ -223,7 +213,7 @@ export class SubtreeValidator implements Validator { subtree = subtreeJson; } catch (error) { const message = `Could not parse subtree JSON: ${error}`; - const issue = IoValidationIssues.JSON_PARSE_ERROR(this._uri, message); + const issue = IoValidationIssues.JSON_PARSE_ERROR(path, message); context.addIssue(issue); return false; } @@ -246,16 +236,17 @@ export class SubtreeValidator implements Validator { /** * Performs the validation of the subtree JSON data in the given buffer * + * @param path The path for `ValidationIssue` instances * @param input The buffer that contains the subtree JSON data * @param context The `ValidationContext` * @returns A promise that resolves when the validation is finished * and indicates whether the object was valid or not. */ private async validateSubtreeJsonData( + path: string, input: Buffer, context: ValidationContext ): Promise { - const path = this._uri; try { const inputString = input.toString(); const subtree: Subtree = JSON.parse(inputString); @@ -310,6 +301,7 @@ export class SubtreeValidator implements Validator { // Validate the structure of the given subtree object, // on the level of JSON validity const structureIsValid = this.validateSubtreeObject( + path, subtree, hasBinaryBuffer, context @@ -356,6 +348,7 @@ export class SubtreeValidator implements Validator { * Performs the validation of the given `Subtree` object, on * the level of JSON validity. * + * @param path The path for `ValidationIssue` instances * @param subtree The `Subtree` object * @param hasBinaryBuffer Whether the subtree has an associated * binary buffer @@ -363,11 +356,11 @@ export class SubtreeValidator implements Validator { * @returns A promise that resolves when the validation is finished */ private validateSubtreeObject( + path: string, subtree: Subtree, hasBinaryBuffer: boolean, context: ValidationContext ): boolean { - const path = this._uri; if (!this.validateSubtreeBasic(path, subtree, hasBinaryBuffer, context)) { return false; } diff --git a/src/validation/TilesetTraversingValidator.ts b/src/validation/TilesetTraversingValidator.ts index 3aaaa995..c379d598 100644 --- a/src/validation/TilesetTraversingValidator.ts +++ b/src/validation/TilesetTraversingValidator.ts @@ -298,12 +298,11 @@ export class TilesetTraversingValidator { // Validate the subtree data with a `SubtreeValidator` const subtreeValidator = new SubtreeValidator( - subtreeUri, validationState, implicitTiling, subtreeResourceResolver ); - const result = await subtreeValidator.validateObject(subtreeData, context); + const result = await subtreeValidator.validateObject(subtreeUri, subtreeData, context); return result; } diff --git a/src/validation/TilesetValidator.ts b/src/validation/TilesetValidator.ts index 28ac4261..27637c52 100644 --- a/src/validation/TilesetValidator.ts +++ b/src/validation/TilesetValidator.ts @@ -39,7 +39,7 @@ export class TilesetValidator implements Validator { ): Promise { try { const object: Tileset = JSON.parse(input); - await this.validateObject(object, context); + await this.validateObject("", object, context); } catch (error) { //console.log(error); const issue = IoValidationIssues.JSON_PARSE_ERROR("", "" + error); @@ -51,16 +51,18 @@ export class TilesetValidator implements Validator { * Implementation of the `Validator` interface that just the * input to `validateTileset`. * + * @param path The path for `ValidationIssue` instances * @param input The `Tileset` object * @param context The `ValidationContext` * @returns A promise that resolves when the validation is finished * and indicates whether the object was valid or not. */ async validateObject( + path: string, input: Tileset, context: ValidationContext ): Promise { - const result = await TilesetValidator.validateTileset(input, context); + const result = await TilesetValidator.validateTileset(path, input, context); return result; } @@ -71,16 +73,17 @@ export class TilesetValidator implements Validator { * Issues that are encountered during the validation will be added * as `ValidationIssue` instances to the given `ValidationContext`. * + * @param path The path for `ValidationIssue` instances * @param tileset The `Tileset` object * @param context The `ValidationContext` * @returns A promise that resolves when the validation is finished * and indicates whether the object was valid or not. */ static async validateTileset( + path: string, tileset: Tileset, context: ValidationContext ): Promise { - const path = ""; // Make sure that the given value is an object if (!BasicValidator.validateObject(path, "tileset", tileset, context)) { diff --git a/src/validation/Validator.ts b/src/validation/Validator.ts index 81978d3b..66308e39 100644 --- a/src/validation/Validator.ts +++ b/src/validation/Validator.ts @@ -14,5 +14,5 @@ export interface Validator { * @returns A promise that is fulfilled when the validation is finished * and indicates whether the object was valid or not. */ - validateObject(input: T, context: ValidationContext): Promise; + validateObject(path: string, input: T, context: ValidationContext): Promise; } diff --git a/src/validation/Validators.ts b/src/validation/Validators.ts index 2597ba84..c9565b86 100644 --- a/src/validation/Validators.ts +++ b/src/validation/Validators.ts @@ -96,7 +96,6 @@ export class Validators { const resourceResolver = ResourceResolvers.createFileResourceResolver(directory); const validator = new SubtreeValidator( - uri, validationState, implicitTiling, resourceResolver @@ -133,7 +132,7 @@ export class Validators { const issue = IoValidationIssues.IO_ERROR(filePath, message); context.addIssue(issue); } else { - await validator.validateObject(resourceData!, context); + await validator.validateObject(filePath, resourceData!, context); } return context.getResult(); } From 110716e0c73ff9714b6bad8b9cc699a519cd4427 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Tue, 11 Oct 2022 19:12:20 +0200 Subject: [PATCH 03/22] Cleanup pass and comments for extension validation --- specs/TilesetValidationSpec.ts | 10 ++- src/ValidatorMain.ts | 21 +++++ src/main.ts | 8 +- src/validation/BoundingVolumeValidator.ts | 26 +++--- src/validation/ExtensionsValidator.ts | 87 +++++++++++++++---- .../extensions/BoundingVolumeS2Validator.ts | 12 +-- 6 files changed, 115 insertions(+), 49 deletions(-) diff --git a/specs/TilesetValidationSpec.ts b/specs/TilesetValidationSpec.ts index 8c952221..e4f6f3f7 100644 --- a/specs/TilesetValidationSpec.ts +++ b/specs/TilesetValidationSpec.ts @@ -729,8 +729,9 @@ describe("Tileset validation", function () { const result = await Validators.validateTilesetFile( "specs/data/tilesets/extensionFoundButNotUsed.json" ); - expect(result.length).toEqual(1); - expect(result.get(0).type).toEqual("EXTENSION_FOUND_BUT_NOT_USED"); + expect(result.length).toEqual(2); + expect(result.get(0).type).toEqual("EXTENSION_NOT_SUPPORTED"); + expect(result.get(1).type).toEqual("EXTENSION_FOUND_BUT_NOT_USED"); }); it("detects issues in extensionRequiredButNotUsed", async function () { @@ -810,8 +811,9 @@ describe("Tileset validation", function () { const result = await Validators.validateTilesetFile( "specs/data/tilesets/extensionUsedButNotFound.json" ); - expect(result.length).toEqual(1); - expect(result.get(0).type).toEqual("EXTENSION_USED_BUT_NOT_FOUND"); + expect(result.length).toEqual(2); + expect(result.get(0).type).toEqual("EXTENSION_NOT_SUPPORTED"); + expect(result.get(1).type).toEqual("EXTENSION_USED_BUT_NOT_FOUND"); }); it("detects issues in extrasUnexpectedType", async function () { diff --git a/src/ValidatorMain.ts b/src/ValidatorMain.ts index febf281e..7073a8c4 100644 --- a/src/ValidatorMain.ts +++ b/src/ValidatorMain.ts @@ -12,6 +12,8 @@ import { Validators } from "./validation/Validators"; import { TileImplicitTiling } from "./structure/TileImplicitTiling"; import { Schema } from "./structure/Metadata/Schema"; +import { BoundingVolumeS2Validator } from "./validation/extensions/BoundingVolumeS2Validator"; +import { ExtensionsValidator } from "./validation/ExtensionsValidator"; /** * A class summarizing the command-line functions of the validator. @@ -191,6 +193,25 @@ export class ValidatorMain { ); } + /** + * Register the validators for known extensions + */ + static registerExtensionValidators() { + + // Register the validator for 3DTILES_bounding_volume_S2 + { + const s2Validator = new BoundingVolumeS2Validator(); + const performDefaultValidation = false; + ExtensionsValidator.register( + "3DTILES_bounding_volume_S2", + s2Validator, + performDefaultValidation + ); + } + + + } + /** * Derives a file name for a report from the given input file name. * The resulting file name will be a file in the same directory as diff --git a/src/main.ts b/src/main.ts index aa7d2cca..59abc122 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,15 +1,9 @@ //eslint-disable-next-line const yargs = require("yargs/yargs"); -import { BoundingVolumeS2Validator } from "./validation/extensions/BoundingVolumeS2Validator"; -import { ExtensionsValidator } from "./validation/ExtensionsValidator"; import { ValidatorMain } from "./ValidatorMain"; -// XXX Experimental extension validation -const s2Validator = new BoundingVolumeS2Validator(""); -const performDefaultValidation = false; -ExtensionsValidator.register("3DTILES_bounding_volume_S2", s2Validator, performDefaultValidation); -// XXX Experimental extension validation +ValidatorMain.registerExtensionValidators(); const args = yargs(process.argv.slice(1)) .help("help") diff --git a/src/validation/BoundingVolumeValidator.ts b/src/validation/BoundingVolumeValidator.ts index 46054999..93afe83e 100644 --- a/src/validation/BoundingVolumeValidator.ts +++ b/src/validation/BoundingVolumeValidator.ts @@ -57,26 +57,30 @@ export class BoundingVolumeValidator { result = false; } + // Validate the extensions in the given object const extensionsValidationResult = await ExtensionsValidator.validateExtensions( boundingVolumePath, - "boundingVolume", boundingVolume, context ); if (!extensionsValidationResult.allValid) { result = false; } - if (extensionsValidationResult.performDefaultValidation) { - if ( - !BoundingVolumeValidator.validateBoundingVolumeInternal( - boundingVolumePath, - boundingVolume, - context - ) - ) { - result = false; - } + // Only proceed with the default validation if it was + // not requested to be skipped for any extension + if (!extensionsValidationResult.performDefaultValidation) { + return result; + } + + if ( + !BoundingVolumeValidator.validateBoundingVolumeInternal( + boundingVolumePath, + boundingVolume, + context + ) + ) { + result = false; } return result; } diff --git a/src/validation/ExtensionsValidator.ts b/src/validation/ExtensionsValidator.ts index c963b349..87703f67 100644 --- a/src/validation/ExtensionsValidator.ts +++ b/src/validation/ExtensionsValidator.ts @@ -9,16 +9,46 @@ import { SemanticValidationIssues } from "../issues/SemanticValidationIssues"; import { ExtensionsValidationResult } from "./ExtensionsValidationResult"; /** - * @private - * @experimental + * A class for managing the validation of extensions. + * + * It allows registering `Validator` objects for specific extension + * names. The `validateExtensions` function will be called for + * each `RootProperty` (i.e. for each object that may contain + * extensions). When an object with a known extension name is + * found, then the registered validator will be applied to that + * object. */ export class ExtensionsValidator { + + /** + * The mapping from extension names to `Validator` objects + * for the respective extension + */ static readonly extensionValidators = new Map< string, Validator >(); + + /** + * A mapping from extension names to a flag indicating whether + * the default validation will still be performed when the + * respective extension is present. + */ static readonly performDefaultValidations = new Map(); + /** + * Registers a validator for the specified extension. + * + * When an extension object with the given name is found, then + * the given validator will be applied to this object in the + * `validateExtensions` method. + * + * @param extensionName The name of the extension + * @param extensionValidator The `Validator` for the extension + * @param performDefaultValidation Whether a default validation + * should be performed for an object, even when the specified + * extension is present. + */ static register( extensionName: string, extensionValidator: Validator, @@ -34,13 +64,39 @@ export class ExtensionsValidator { ); } + /** + * Perform the validation of the extensions in the given object. + * + * This will check the `extensions` of the given object. If there + * are no extensions, then a `ExtensionsValidationResult` will be + * returned that indicates that all extensions are valid, and + * the default validation should be performed. + * + * If there are extensions, then each of them will be examined: + * If a `Validator` instance has been registered for one of + * them (by calling the `register` method), then this validator + * will be applied to the extension object. + * + * The `ExtensionsValidationResult` will contain information + * about whether all extensions have been valid, and whether + * a default validation should still be peformed. + * + * (If any of the relevant extension validators was registered with + * `performDefaultValidation===false`, then the `performDefaultValidation` + * flag in the result will be `false`) + * + * @param path The path for `ValidationIssue` instances + * @param rootProperty The `RootProperty` that may contain extensions + * @param context The `ValidationContext` + * @returns The `ExtensionsValidationResult` + */ static async validateExtensions( path: string, - name: string, rootProperty: RootProperty, context: ValidationContext ): Promise { + // If there are no extension, just return a positive result const extensions = rootProperty.extensions; if (!defined(extensions)) { return { @@ -48,6 +104,8 @@ export class ExtensionsValidator { performDefaultValidation: true, }; } + + let allValid = true; let performDefaultValidation = true; @@ -55,6 +113,10 @@ export class ExtensionsValidator { for (const extensionName of extensionNames) { const extensionValidator = ExtensionsValidator.extensionValidators.get(extensionName); + + // If an extension was found, but no validator for + // that extension was registered, then issue a + // warning. if (!defined(extensionValidator)) { const issue = SemanticValidationIssues.EXTENSION_NOT_SUPPORTED( path, @@ -62,12 +124,13 @@ export class ExtensionsValidator { ); context.addIssue(issue); } else { + + // Validate the extension with the registered Validator const extension = extensions![extensionName]; - const isValid = await ExtensionsValidator.validateExtension( - path, - extensionName, + const extensionPath = path + "/" + extensionName; + const isValid = await extensionValidator!.validateObject( + extensionPath, extension, - extensionValidator!, context ); if (!isValid) { @@ -86,14 +149,4 @@ export class ExtensionsValidator { }; } - private static async validateExtension( - path: string, - name: string, - extension: any, - extensionValidator: Validator, - context: ValidationContext - ): Promise { - const result = await extensionValidator.validateObject(path, extension, context); - return result; - } } diff --git a/src/validation/extensions/BoundingVolumeS2Validator.ts b/src/validation/extensions/BoundingVolumeS2Validator.ts index 68eb5e63..fc853444 100644 --- a/src/validation/extensions/BoundingVolumeS2Validator.ts +++ b/src/validation/extensions/BoundingVolumeS2Validator.ts @@ -14,28 +14,20 @@ import { BoundingVolumeS2ValidationIssues } from "./BoundingVolumeS2ValidationIs * @private */ export class BoundingVolumeS2Validator implements Validator { - - // TODO The path should probably be passed into validateObject - private readonly _path: string; - - constructor(path: string) { - this._path = path; - } - /** * Performs the validation to ensure that the given object is a * valid `3DTILES_bounding_volume_S2` object. * + * @param path The path for `ValidationIssue` instances * @param object The object to validate * @param context The `ValidationContext` that any issues will be added to * @returns Whether the object was valid */ async validateObject( + path: string, object: any, context: ValidationContext ): Promise { - const path = this._path; - // Make sure that the given value is an object if (!BasicValidator.validateObject(path, "object", object, context)) { return false; From 5718b9b2fb32ab33b15f467ca03da96b28e174c8 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Tue, 11 Oct 2022 21:02:02 +0200 Subject: [PATCH 04/22] Properly handle validation of extended objects Instead of validating onyl the actual extensions, it now validates the extended objects (including the extensions). --- .../boundingVolumeS2/s2AndInvalidBox.json | 25 +++ .../boundingVolumeS2/s2TokenMissing.json | 23 +++ src/ValidatorMain.ts | 11 +- src/validation/BoundingVolumeValidator.ts | 38 +++-- src/validation/ExtendedObjectsValidators.ts | 155 ++++++++++++++++++ src/validation/ExtensionsValidationResult.ts | 5 - src/validation/ExtensionsValidator.ts | 152 ----------------- .../extensions/BoundingVolumeS2Validator.ts | 128 ++++++++++++++- 8 files changed, 352 insertions(+), 185 deletions(-) create mode 100644 specs/data/tilesets/boundingVolumeS2/s2AndInvalidBox.json create mode 100644 specs/data/tilesets/boundingVolumeS2/s2TokenMissing.json create mode 100644 src/validation/ExtendedObjectsValidators.ts delete mode 100644 src/validation/ExtensionsValidationResult.ts delete mode 100644 src/validation/ExtensionsValidator.ts diff --git a/specs/data/tilesets/boundingVolumeS2/s2AndInvalidBox.json b/specs/data/tilesets/boundingVolumeS2/s2AndInvalidBox.json new file mode 100644 index 00000000..fd21e720 --- /dev/null +++ b/specs/data/tilesets/boundingVolumeS2/s2AndInvalidBox.json @@ -0,0 +1,25 @@ +{ + "asset": { + "version": "1.1" + }, + "extensionsUsed": [ + "3DTILES_bounding_volume_S2" + ], + "extensionsRequired": [ + "3DTILES_bounding_volume_S2" + ], + "geometricError": 2.0, + "root": { + "geometricError": 1.0, + "boundingVolume": { + "box": [ 1, 2, 3], + "extensions": { + "3DTILES_bounding_volume_S2": { + "token": "1", + "minimumHeight": 0, + "maximumHeight": 10000 + } + } + } + } +} \ No newline at end of file diff --git a/specs/data/tilesets/boundingVolumeS2/s2TokenMissing.json b/specs/data/tilesets/boundingVolumeS2/s2TokenMissing.json new file mode 100644 index 00000000..c8b3aeb3 --- /dev/null +++ b/specs/data/tilesets/boundingVolumeS2/s2TokenMissing.json @@ -0,0 +1,23 @@ +{ + "asset": { + "version": "1.1" + }, + "extensionsUsed": [ + "3DTILES_bounding_volume_S2" + ], + "extensionsRequired": [ + "3DTILES_bounding_volume_S2" + ], + "geometricError": 2.0, + "root": { + "geometricError": 1.0, + "boundingVolume": { + "extensions": { + "3DTILES_bounding_volume_S2": { + "minimumHeight": 0, + "maximumHeight": 10000 + } + } + } + } +} \ No newline at end of file diff --git a/src/ValidatorMain.ts b/src/ValidatorMain.ts index 7073a8c4..4f91b51d 100644 --- a/src/ValidatorMain.ts +++ b/src/ValidatorMain.ts @@ -13,7 +13,7 @@ import { Validators } from "./validation/Validators"; import { TileImplicitTiling } from "./structure/TileImplicitTiling"; import { Schema } from "./structure/Metadata/Schema"; import { BoundingVolumeS2Validator } from "./validation/extensions/BoundingVolumeS2Validator"; -import { ExtensionsValidator } from "./validation/ExtensionsValidator"; +import { ExtendedObjectsValidators } from "./validation/ExtendedObjectsValidators"; /** * A class summarizing the command-line functions of the validator. @@ -197,19 +197,16 @@ export class ValidatorMain { * Register the validators for known extensions */ static registerExtensionValidators() { - // Register the validator for 3DTILES_bounding_volume_S2 { const s2Validator = new BoundingVolumeS2Validator(); - const performDefaultValidation = false; - ExtensionsValidator.register( + const override = true; + ExtendedObjectsValidators.register( "3DTILES_bounding_volume_S2", s2Validator, - performDefaultValidation + override ); } - - } /** diff --git a/src/validation/BoundingVolumeValidator.ts b/src/validation/BoundingVolumeValidator.ts index 93afe83e..71498459 100644 --- a/src/validation/BoundingVolumeValidator.ts +++ b/src/validation/BoundingVolumeValidator.ts @@ -3,7 +3,7 @@ import { defined } from "../base/defined"; import { ValidationContext } from "./ValidationContext"; import { BasicValidator } from "./BasicValidator"; import { RootPropertyValidator } from "./RootPropertyValidator"; -import { ExtensionsValidator } from "./ExtensionsValidator"; +import { ExtendedObjectsValidators } from "./ExtendedObjectsValidators"; import { BoundingVolume } from "../structure/BoundingVolume"; @@ -57,22 +57,22 @@ export class BoundingVolumeValidator { result = false; } - // Validate the extensions in the given object - const extensionsValidationResult = - await ExtensionsValidator.validateExtensions( + // Perform the validation of the object in view of the + // extensions that it may contain + if ( + !ExtendedObjectsValidators.validateExtendedObject( boundingVolumePath, boundingVolume, context - ); - if (!extensionsValidationResult.allValid) { + ) + ) { result = false; } - // Only proceed with the default validation if it was - // not requested to be skipped for any extension - if (!extensionsValidationResult.performDefaultValidation) { + // If there was an extension validator that overrides the + // default validation, then skip the remaining validation. + if (ExtendedObjectsValidators.hasOverride(boundingVolume)) { return result; } - if ( !BoundingVolumeValidator.validateBoundingVolumeInternal( boundingVolumePath, @@ -99,6 +99,18 @@ export class BoundingVolumeValidator { boundingVolume: BoundingVolume, context: ValidationContext ): boolean { + // Make sure that the given value is an object + if ( + !BasicValidator.validateObject( + boundingVolumePath, + "boundingVolume", + boundingVolume, + context + ) + ) { + return false; + } + let result = true; const box = boundingVolume.box; @@ -167,7 +179,7 @@ export class BoundingVolumeValidator { * @param context The `ValidationContext` * @returns Whether the object was valid */ - private static validateBoundingBox( + static validateBoundingBox( path: string, box: number[], context: ValidationContext @@ -200,7 +212,7 @@ export class BoundingVolumeValidator { * @param context The `ValidationContext` * @returns Whether the object was valid */ - private static validateBoundingSphere( + static validateBoundingSphere( path: string, sphere: number[], context: ValidationContext @@ -247,7 +259,7 @@ export class BoundingVolumeValidator { * @param context The `ValidationContext` * @returns Whether the object was valid */ - private static validateBoundingRegion( + static validateBoundingRegion( path: string, region: number[], context: ValidationContext diff --git a/src/validation/ExtendedObjectsValidators.ts b/src/validation/ExtendedObjectsValidators.ts new file mode 100644 index 00000000..a393f896 --- /dev/null +++ b/src/validation/ExtendedObjectsValidators.ts @@ -0,0 +1,155 @@ +import { defined } from "../base/defined"; + +import { Validator } from "./Validator"; +import { ValidationContext } from "./ValidationContext"; + +import { RootProperty } from "../structure/RootProperty"; + +import { SemanticValidationIssues } from "../issues/SemanticValidationIssues"; + +/** + * A class for managing the validation of objects that contain extensions. + * + * It allows registering `Validator` objects for specific extension + * names. The `validateExtendedObject` function will be called for + * each `RootProperty` (i.e. for each object that may contain + * extensions). When an object contains an extension with one + * of the registered names, then the respective validators will + * be applied to that object. + */ +export class ExtendedObjectsValidators { + + /** + * The mapping from extension names to the validators that + * are used for objects that contain the respective extension. + */ + static readonly extendedObjectValidators = new Map< + string, + Validator + >(); + + static readonly overrides = new Map(); + + /** + * Registers a validator for an object with the specified extension. + * + * When an object has the specified extension, then the given + * validator will be applied to this object. + * + * @param extensionName The name of the extension + * @param extendedObjectValidator The `Validator` for the extended objects + * @param override Whether the given validator should replace the + * default validation. This can be queried with the `hasOverride` method. + */ + static register( + extensionName: string, + extendedObjectValidator: Validator, + override: boolean + ) { + ExtendedObjectsValidators.extendedObjectValidators.set( + extensionName, + extendedObjectValidator + ); + ExtendedObjectsValidators.overrides.set( + extensionName, + override + ); + + } + + /** + * Returns whether the default validation of the given object + * is overridden. This is the case when the object contains + * an extension which has been registered by calling the + * `register` method, with the `override` flag being `true`. + * + * @param rootProperty The `RootProperty` + * @returns Whether the default validation is overridden + * by one of the registered validators. + */ + static hasOverride(rootProperty: RootProperty) : boolean { + const extensions = rootProperty.extensions; + if (!defined(extensions)) { + return false; + } + const extensionNames = Object.keys(extensions!); + for (const extensionName of extensionNames) { + const override = + ExtendedObjectsValidators.overrides.get(extensionName); + if (override === true) { + return true; + } + } + return false; + } + + /** + * Perform the validation of the given (possibly extended) object. + * + * If the given object does not have extensions, then `true` will + * be returned. + * + * If there are extensions, then each of them will be examined: + * + * If a `Validator` instance has been registered for one of the + * extensions (by calling the `register` method), then this + * validator will be applied to the given object. + * + * (If no `Validator` instance has been registered, then + * a warning will be added to the given context, indicating + * that the extension is not supported) + * + * If any of the registered validators returns `false`, then + * `false` will be returned. If all of them consider the object + * to be valid, then `true` will be returned. + * + * @param path The path for `ValidationIssue` instances + * @param rootProperty The `RootProperty` that may contain extensions + * @param context The `ValidationContext` + * @returns Whether the object is valid + */ + static async validateExtendedObject( + path: string, + rootProperty: RootProperty, + context: ValidationContext + ): Promise { + + // If there are no extensions, consider the object to be valid + const extensions = rootProperty.extensions; + if (!defined(extensions)) { + return true; + } + + let allValid = true; + + const extensionNames = Object.keys(extensions!); + for (const extensionName of extensionNames) { + const extendedObjectValidator = + ExtendedObjectsValidators.extendedObjectValidators.get(extensionName); + + // If an extension was found, but no validator for + // that extension was registered, then issue a + // warning. + if (!defined(extendedObjectValidator)) { + const issue = SemanticValidationIssues.EXTENSION_NOT_SUPPORTED( + path, + extensionName + ); + context.addIssue(issue); + } else { + + // Validate the object with the registered Validator + const isValid = await extendedObjectValidator!.validateObject( + path, + rootProperty, + context + ); + if (!isValid) { + allValid = false; + } + } + } + return allValid; + } + +} diff --git a/src/validation/ExtensionsValidationResult.ts b/src/validation/ExtensionsValidationResult.ts deleted file mode 100644 index 6000970b..00000000 --- a/src/validation/ExtensionsValidationResult.ts +++ /dev/null @@ -1,5 +0,0 @@ - -export interface ExtensionsValidationResult { - allValid: boolean; - performDefaultValidation: boolean; -} diff --git a/src/validation/ExtensionsValidator.ts b/src/validation/ExtensionsValidator.ts deleted file mode 100644 index 87703f67..00000000 --- a/src/validation/ExtensionsValidator.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { defined } from "../base/defined"; - -import { Validator } from "./Validator"; -import { ValidationContext } from "./ValidationContext"; - -import { RootProperty } from "../structure/RootProperty"; - -import { SemanticValidationIssues } from "../issues/SemanticValidationIssues"; -import { ExtensionsValidationResult } from "./ExtensionsValidationResult"; - -/** - * A class for managing the validation of extensions. - * - * It allows registering `Validator` objects for specific extension - * names. The `validateExtensions` function will be called for - * each `RootProperty` (i.e. for each object that may contain - * extensions). When an object with a known extension name is - * found, then the registered validator will be applied to that - * object. - */ -export class ExtensionsValidator { - - /** - * The mapping from extension names to `Validator` objects - * for the respective extension - */ - static readonly extensionValidators = new Map< - string, - Validator - >(); - - /** - * A mapping from extension names to a flag indicating whether - * the default validation will still be performed when the - * respective extension is present. - */ - static readonly performDefaultValidations = new Map(); - - /** - * Registers a validator for the specified extension. - * - * When an extension object with the given name is found, then - * the given validator will be applied to this object in the - * `validateExtensions` method. - * - * @param extensionName The name of the extension - * @param extensionValidator The `Validator` for the extension - * @param performDefaultValidation Whether a default validation - * should be performed for an object, even when the specified - * extension is present. - */ - static register( - extensionName: string, - extensionValidator: Validator, - performDefaultValidation: boolean - ) { - ExtensionsValidator.extensionValidators.set( - extensionName, - extensionValidator - ); - ExtensionsValidator.performDefaultValidations.set( - extensionName, - performDefaultValidation - ); - } - - /** - * Perform the validation of the extensions in the given object. - * - * This will check the `extensions` of the given object. If there - * are no extensions, then a `ExtensionsValidationResult` will be - * returned that indicates that all extensions are valid, and - * the default validation should be performed. - * - * If there are extensions, then each of them will be examined: - * If a `Validator` instance has been registered for one of - * them (by calling the `register` method), then this validator - * will be applied to the extension object. - * - * The `ExtensionsValidationResult` will contain information - * about whether all extensions have been valid, and whether - * a default validation should still be peformed. - * - * (If any of the relevant extension validators was registered with - * `performDefaultValidation===false`, then the `performDefaultValidation` - * flag in the result will be `false`) - * - * @param path The path for `ValidationIssue` instances - * @param rootProperty The `RootProperty` that may contain extensions - * @param context The `ValidationContext` - * @returns The `ExtensionsValidationResult` - */ - static async validateExtensions( - path: string, - rootProperty: RootProperty, - context: ValidationContext - ): Promise { - - // If there are no extension, just return a positive result - const extensions = rootProperty.extensions; - if (!defined(extensions)) { - return { - allValid: true, - performDefaultValidation: true, - }; - } - - - let allValid = true; - let performDefaultValidation = true; - - const extensionNames = Object.keys(extensions!); - for (const extensionName of extensionNames) { - const extensionValidator = - ExtensionsValidator.extensionValidators.get(extensionName); - - // If an extension was found, but no validator for - // that extension was registered, then issue a - // warning. - if (!defined(extensionValidator)) { - const issue = SemanticValidationIssues.EXTENSION_NOT_SUPPORTED( - path, - extensionName - ); - context.addIssue(issue); - } else { - - // Validate the extension with the registered Validator - const extension = extensions![extensionName]; - const extensionPath = path + "/" + extensionName; - const isValid = await extensionValidator!.validateObject( - extensionPath, - extension, - context - ); - if (!isValid) { - allValid = false; - } - const performDefault = - ExtensionsValidator.performDefaultValidations.get(extensionName); - if (performDefault === false) { - performDefaultValidation = false; - } - } - } - return { - allValid: allValid, - performDefaultValidation: performDefaultValidation, - }; - } - -} diff --git a/src/validation/extensions/BoundingVolumeS2Validator.ts b/src/validation/extensions/BoundingVolumeS2Validator.ts index fc853444..da84df69 100644 --- a/src/validation/extensions/BoundingVolumeS2Validator.ts +++ b/src/validation/extensions/BoundingVolumeS2Validator.ts @@ -7,29 +7,39 @@ import { RootPropertyValidator } from "../RootPropertyValidator"; import { SemanticValidationIssues } from "../../issues/SemanticValidationIssues"; import { BoundingVolumeS2ValidationIssues } from "./BoundingVolumeS2ValidationIssues"; +import { BoundingVolumeValidator } from "../BoundingVolumeValidator"; +import { ExtendedObjectsValidators } from "../ExtendedObjectsValidators"; /** - * A class for the validation of `3DTILES_bounding_volume_S2` extension objects + * A class for the validation of bounding volumes that contain + * `3DTILES_bounding_volume_S2` extension objects * * @private */ export class BoundingVolumeS2Validator implements Validator { /** - * Performs the validation to ensure that the given object is a - * valid `3DTILES_bounding_volume_S2` object. + * Performs the validation of a `BoundungVolume` object that + * contains a `3DTILES_bounding_volume_S2` extension object. * * @param path The path for `ValidationIssue` instances - * @param object The object to validate + * @param boundingVolume The object to validate * @param context The `ValidationContext` that any issues will be added to * @returns Whether the object was valid */ async validateObject( path: string, - object: any, + boundingVolume: any, context: ValidationContext ): Promise { // Make sure that the given value is an object - if (!BasicValidator.validateObject(path, "object", object, context)) { + if ( + !BasicValidator.validateObject( + path, + "boundingVolume", + boundingVolume, + context + ) + ) { return false; } @@ -39,13 +49,109 @@ export class BoundingVolumeS2Validator implements Validator { if ( !RootPropertyValidator.validateRootProperty( path, - "object", - object, + "boundingVolume", + boundingVolume, + context + ) + ) { + result = false; + } + + // Perform the validation of the object in view of the + // extensions that it may contain + if ( + !ExtendedObjectsValidators.validateExtendedObject( + path, + boundingVolume, context ) ) { result = false; } + // If there was an extension validator that overrides the + // default validation, then skip the remaining validation. + if (ExtendedObjectsValidators.hasOverride(boundingVolume)) { + return result; + } + + // Validate the box + const box = boundingVolume.box; + const boxPath = path + "/box"; + if (defined(box)) { + if ( + !BoundingVolumeValidator.validateBoundingBox(boxPath, box!, context) + ) { + result = false; + } + } + + // Validate the region + const region = boundingVolume.region; + const regionPath = path + "/region"; + if (defined(region)) { + if ( + !BoundingVolumeValidator.validateBoundingRegion( + regionPath, + region!, + context + ) + ) { + result = false; + } + } + + // Validate the sphere + const sphere = boundingVolume.sphere; + const spherePath = path + "/sphere"; + if (defined(sphere)) { + if ( + !BoundingVolumeValidator.validateBoundingSphere( + spherePath, + sphere!, + context + ) + ) { + result = false; + } + } + + // If there is a 3DTILES_bounding_volume_S2 extension, + // perform the corresponding object + const extensions = boundingVolume.extensions; + if (defined(extensions)) { + const key = "3DTILES_bounding_volume_S2"; + const s2 = extensions[key]; + const s2Path = path + "/" + key; + if ( + !BoundingVolumeS2Validator.validateBoundingVolumeS2(s2Path, s2, context) + ) { + result = false; + } + } + + return result; + } + + /** + * Performs the validation to ensure that the given object is a + * valid `3DTILES_bounding_volume_S2` object. + * + * @param path The path for `ValidationIssue` instances + * @param object The object to validate + * @param context The `ValidationContext` that any issues will be added to + * @returns Whether the object was valid + */ + static validateBoundingVolumeS2( + path: string, + object: any, + context: ValidationContext + ): boolean { + // Make sure that the given value is an object + if (!BasicValidator.validateObject(path, "object", object, context)) { + return false; + } + + let result = true; // Validate the token const token = object.token; @@ -117,6 +223,12 @@ export class BoundingVolumeS2Validator implements Validator { return result; } + /** + * Peforms a basic validation that the given string is a valid S2 cell token + * + * @param token The token + * @returns Whether the token is valid + */ private static isValidToken(token: string): boolean { // According to cesium/Source/Core/S2Cell.js if (!/^[0-9a-fA-F]{1,16}$/.test(token)) { From 5264738374e9b2b26cb3fc73aa9f8d2c575d234f Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Tue, 11 Oct 2022 21:03:12 +0200 Subject: [PATCH 05/22] Formatting --- src/issues/SemanticValidationIssues.ts | 3 +- src/tileFormats/B3dmValidator.ts | 7 ++- src/tileFormats/CmptValidator.ts | 7 ++- src/tileFormats/GltfValidator.ts | 1 - src/tileFormats/I3dmValidator.ts | 18 +++--- src/tileFormats/PntsValidator.ts | 7 ++- src/validation/ContentDataValidator.ts | 36 +++++++++-- src/validation/ContentValidator.ts | 11 ++-- src/validation/ExtendedObjectsValidators.ts | 60 ++++++++----------- src/validation/SubtreeValidator.ts | 4 +- src/validation/TileValidator.ts | 23 +++---- src/validation/TilesetTraversingValidator.ts | 13 +++- src/validation/TilesetValidator.ts | 1 - src/validation/Validator.ts | 6 +- .../BoundingVolumeS2ValidationIssues.ts | 1 - 15 files changed, 113 insertions(+), 85 deletions(-) diff --git a/src/issues/SemanticValidationIssues.ts b/src/issues/SemanticValidationIssues.ts index 41d4c9e7..b5cbb6ea 100644 --- a/src/issues/SemanticValidationIssues.ts +++ b/src/issues/SemanticValidationIssues.ts @@ -341,8 +341,7 @@ export class SemanticValidationIssues { const type = "EXTENSION_NOT_SUPPORTED"; const severity = ValidationIssueSeverity.WARNING; const message = - `The extension '${extensionName}' was used, but ` + - `is not supported`; + `The extension '${extensionName}' was used, but ` + `is not supported`; const issue = new ValidationIssue(type, path, message, severity); return issue; } diff --git a/src/tileFormats/B3dmValidator.ts b/src/tileFormats/B3dmValidator.ts index c90c4fd3..375c891e 100644 --- a/src/tileFormats/B3dmValidator.ts +++ b/src/tileFormats/B3dmValidator.ts @@ -34,7 +34,6 @@ const featureTableSemantics = { * given as a Buffer. */ export class B3dmValidator implements Validator { - async validateObject( uri: string, input: Buffer, @@ -45,7 +44,11 @@ export class B3dmValidator implements Validator { // will be stored as the 'internal issues' of a // single content validation issue. const derivedContext = context.derive("."); - const result = await this.validateObjectInternal(uri, input, derivedContext); + const result = await this.validateObjectInternal( + uri, + input, + derivedContext + ); const derivedResult = derivedContext.getResult(); const issue = ContentValidationIssues.createFrom(uri, derivedResult); if (issue) { diff --git a/src/tileFormats/CmptValidator.ts b/src/tileFormats/CmptValidator.ts index 9156c572..64aa7f8d 100644 --- a/src/tileFormats/CmptValidator.ts +++ b/src/tileFormats/CmptValidator.ts @@ -18,7 +18,6 @@ import { BinaryValidationIssues } from "../issues/BinaryValidationIssues"; * given as a Buffer. */ export class CmptValidator implements Validator { - async validateObject( uri: string, input: Buffer, @@ -29,7 +28,11 @@ export class CmptValidator implements Validator { // will be stored as the 'internal issues' of a // single content validation issue. const derivedContext = context.derive("."); - const result = await this.validateObjectInternal(uri, input, derivedContext); + const result = await this.validateObjectInternal( + uri, + input, + derivedContext + ); const derivedResult = derivedContext.getResult(); const issue = ContentValidationIssues.createFrom(uri, derivedResult); if (issue) { diff --git a/src/tileFormats/GltfValidator.ts b/src/tileFormats/GltfValidator.ts index 13e2b352..ebbf7334 100644 --- a/src/tileFormats/GltfValidator.ts +++ b/src/tileFormats/GltfValidator.ts @@ -16,7 +16,6 @@ const validator = require("gltf-validator"); * in a Buffer. */ export class GltfValidator implements Validator { - /** * Creates a `ValidationIssue` object for the given 'message' object * that appears in the output of the glTF validator. diff --git a/src/tileFormats/I3dmValidator.ts b/src/tileFormats/I3dmValidator.ts index 3bd1547c..3a7ea8eb 100644 --- a/src/tileFormats/I3dmValidator.ts +++ b/src/tileFormats/I3dmValidator.ts @@ -96,7 +96,6 @@ const featureTableSemantics = { * given as a Buffer. */ export class I3dmValidator implements Validator { - async validateObject( uri: string, input: Buffer, @@ -107,7 +106,11 @@ export class I3dmValidator implements Validator { // will be stored as the 'internal issues' of a // single content validation issue. const derivedContext = context.derive("."); - const result = await this.validateObjectInternal(uri, input, derivedContext); + const result = await this.validateObjectInternal( + uri, + input, + derivedContext + ); const derivedResult = derivedContext.getResult(); const issue = ContentValidationIssues.createFrom(uri, derivedResult); if (issue) { @@ -274,7 +277,11 @@ export class I3dmValidator implements Validator { // If the GLB data was embdedded, validate it directly if (hasEmbeddedGlb) { const gltfValidator = new GltfValidator(); - const gltfResult = await gltfValidator.validateObject(uri, glbData, context); + const gltfResult = await gltfValidator.validateObject( + uri, + glbData, + context + ); if (!gltfResult) { result = false; } @@ -309,10 +316,7 @@ export class I3dmValidator implements Validator { result = false; } const derivedResult = derivedContext.getResult(); - const issue = ContentValidationIssues.createFrom( - uri, - derivedResult - ); + const issue = ContentValidationIssues.createFrom(uri, derivedResult); if (issue) { context.addIssue(issue); } diff --git a/src/tileFormats/PntsValidator.ts b/src/tileFormats/PntsValidator.ts index be288a95..506d58d3 100644 --- a/src/tileFormats/PntsValidator.ts +++ b/src/tileFormats/PntsValidator.ts @@ -94,7 +94,6 @@ const featureTableSemantics = { * given as a Buffer. */ export class PntsValidator implements Validator { - async validateObject( uri: string, input: Buffer, @@ -105,7 +104,11 @@ export class PntsValidator implements Validator { // will be stored as the 'internal issues' of a // single content validation issue. const derivedContext = context.derive("."); - const result = await this.validateObjectInternal(uri, input, derivedContext); + const result = await this.validateObjectInternal( + uri, + input, + derivedContext + ); const derivedResult = derivedContext.getResult(); const issue = ContentValidationIssues.createFrom(uri, derivedResult); if (issue) { diff --git a/src/validation/ContentDataValidator.ts b/src/validation/ContentDataValidator.ts index 7ddfd319..6fbce3b3 100644 --- a/src/validation/ContentDataValidator.ts +++ b/src/validation/ContentDataValidator.ts @@ -113,7 +113,11 @@ export class ContentDataValidator { if (isGlb) { console.log("Validating GLB: " + contentUri); const dataValidator = new GltfValidator(); - const result = await dataValidator.validateObject(contentUri, contentData, context); + const result = await dataValidator.validateObject( + contentUri, + contentData, + context + ); return result; } @@ -121,7 +125,11 @@ export class ContentDataValidator { if (isB3dm) { console.log("Validating B3DM: " + contentUri); const dataValidator = new B3dmValidator(); - const result = await dataValidator.validateObject(contentUri, contentData, context); + const result = await dataValidator.validateObject( + contentUri, + contentData, + context + ); return result; } @@ -129,7 +137,11 @@ export class ContentDataValidator { if (isI3dm) { console.log("Validating I3DM: " + contentUri); const dataValidator = new I3dmValidator(); - const result = await dataValidator.validateObject(contentUri, contentData, context); + const result = await dataValidator.validateObject( + contentUri, + contentData, + context + ); return result; } @@ -137,7 +149,11 @@ export class ContentDataValidator { if (isPnts) { console.log("Validating PNTS: " + contentUri); const dataValidator = new PntsValidator(); - const result = await dataValidator.validateObject(contentUri, contentData, context); + const result = await dataValidator.validateObject( + contentUri, + contentData, + context + ); return result; } @@ -145,7 +161,11 @@ export class ContentDataValidator { if (isCmpt) { console.log("Validating CMPT: " + contentUri); const dataValidator = new CmptValidator(); - const result = await dataValidator.validateObject(contentUri, contentData, context); + const result = await dataValidator.validateObject( + contentUri, + contentData, + context + ); return result; } @@ -277,7 +297,11 @@ export class ContentDataValidator { // Assume that it is a glTF: console.log("Validating glTF: " + contentUri); const gltfValidator = new GltfValidator(); - const result = await gltfValidator.validateObject(contentUri, contentData, context); + const result = await gltfValidator.validateObject( + contentUri, + contentData, + context + ); return result; } const path = contentPath; diff --git a/src/validation/ContentValidator.ts b/src/validation/ContentValidator.ts index 1a737689..61d88572 100644 --- a/src/validation/ContentValidator.ts +++ b/src/validation/ContentValidator.ts @@ -129,11 +129,12 @@ export class ContentValidator { const boundingVolume = content.boundingVolume; const boundingVolumePath = contentPath + "/boundingVolume"; if (defined(boundingVolume)) { - const boundingVolumeValid = await BoundingVolumeValidator.validateBoundingVolume( - boundingVolumePath, - boundingVolume!, - context - ); + const boundingVolumeValid = + await BoundingVolumeValidator.validateBoundingVolume( + boundingVolumePath, + boundingVolume!, + context + ); if (!boundingVolumeValid) { result = false; } diff --git a/src/validation/ExtendedObjectsValidators.ts b/src/validation/ExtendedObjectsValidators.ts index a393f896..8bdb9660 100644 --- a/src/validation/ExtendedObjectsValidators.ts +++ b/src/validation/ExtendedObjectsValidators.ts @@ -9,33 +9,29 @@ import { SemanticValidationIssues } from "../issues/SemanticValidationIssues"; /** * A class for managing the validation of objects that contain extensions. - * + * * It allows registering `Validator` objects for specific extension * names. The `validateExtendedObject` function will be called for * each `RootProperty` (i.e. for each object that may contain * extensions). When an object contains an extension with one - * of the registered names, then the respective validators will + * of the registered names, then the respective validators will * be applied to that object. */ export class ExtendedObjectsValidators { - /** * The mapping from extension names to the validators that * are used for objects that contain the respective extension. */ - static readonly extendedObjectValidators = new Map< - string, - Validator - >(); + static readonly extendedObjectValidators = new Map>(); static readonly overrides = new Map(); /** * Registers a validator for an object with the specified extension. - * - * When an object has the specified extension, then the given - * validator will be applied to this object. - * + * + * When an object has the specified extension, then the given + * validator will be applied to this object. + * * @param extensionName The name of the extension * @param extendedObjectValidator The `Validator` for the extended objects * @param override Whether the given validator should replace the @@ -50,32 +46,27 @@ export class ExtendedObjectsValidators { extensionName, extendedObjectValidator ); - ExtendedObjectsValidators.overrides.set( - extensionName, - override - ); - + ExtendedObjectsValidators.overrides.set(extensionName, override); } /** * Returns whether the default validation of the given object - * is overridden. This is the case when the object contains - * an extension which has been registered by calling the + * is overridden. This is the case when the object contains + * an extension which has been registered by calling the * `register` method, with the `override` flag being `true`. - * + * * @param rootProperty The `RootProperty` * @returns Whether the default validation is overridden * by one of the registered validators. */ - static hasOverride(rootProperty: RootProperty) : boolean { + static hasOverride(rootProperty: RootProperty): boolean { const extensions = rootProperty.extensions; if (!defined(extensions)) { return false; } const extensionNames = Object.keys(extensions!); for (const extensionName of extensionNames) { - const override = - ExtendedObjectsValidators.overrides.get(extensionName); + const override = ExtendedObjectsValidators.overrides.get(extensionName); if (override === true) { return true; } @@ -85,24 +76,24 @@ export class ExtendedObjectsValidators { /** * Perform the validation of the given (possibly extended) object. - * - * If the given object does not have extensions, then `true` will + * + * If the given object does not have extensions, then `true` will * be returned. - * + * * If there are extensions, then each of them will be examined: - * + * * If a `Validator` instance has been registered for one of the - * extensions (by calling the `register` method), then this + * extensions (by calling the `register` method), then this * validator will be applied to the given object. - * - * (If no `Validator` instance has been registered, then + * + * (If no `Validator` instance has been registered, then * a warning will be added to the given context, indicating * that the extension is not supported) - * - * If any of the registered validators returns `false`, then + * + * If any of the registered validators returns `false`, then * `false` will be returned. If all of them consider the object * to be valid, then `true` will be returned. - * + * * @param path The path for `ValidationIssue` instances * @param rootProperty The `RootProperty` that may contain extensions * @param context The `ValidationContext` @@ -113,7 +104,6 @@ export class ExtendedObjectsValidators { rootProperty: RootProperty, context: ValidationContext ): Promise { - // If there are no extensions, consider the object to be valid const extensions = rootProperty.extensions; if (!defined(extensions)) { @@ -128,7 +118,7 @@ export class ExtendedObjectsValidators { ExtendedObjectsValidators.extendedObjectValidators.get(extensionName); // If an extension was found, but no validator for - // that extension was registered, then issue a + // that extension was registered, then issue a // warning. if (!defined(extendedObjectValidator)) { const issue = SemanticValidationIssues.EXTENSION_NOT_SUPPORTED( @@ -137,7 +127,6 @@ export class ExtendedObjectsValidators { ); context.addIssue(issue); } else { - // Validate the object with the registered Validator const isValid = await extendedObjectValidator!.validateObject( path, @@ -151,5 +140,4 @@ export class ExtendedObjectsValidators { } return allValid; } - } diff --git a/src/validation/SubtreeValidator.ts b/src/validation/SubtreeValidator.ts index 4ae484e6..c2eb5cfd 100644 --- a/src/validation/SubtreeValidator.ts +++ b/src/validation/SubtreeValidator.ts @@ -38,7 +38,6 @@ import { RootPropertyValidator } from "./RootPropertyValidator"; * @private */ export class SubtreeValidator implements Validator { - /** * The `ValidationState` that carries information about * the metadata schema @@ -77,7 +76,7 @@ export class SubtreeValidator implements Validator { } /** - * Implementation of the `Validator` interface that performs the + * Implementation of the `Validator` interface that performs the * validation of the given buffer, which is supposed to * contain subtree data, either in binary form or as JSON. * @@ -123,7 +122,6 @@ export class SubtreeValidator implements Validator { input: Buffer, context: ValidationContext ): Promise { - // Validate the header length const headerByteLength = 24; if ( diff --git a/src/validation/TileValidator.ts b/src/validation/TileValidator.ts index e026c6a5..a883789e 100644 --- a/src/validation/TileValidator.ts +++ b/src/validation/TileValidator.ts @@ -78,11 +78,12 @@ export class TileValidator { const boundingVolume = tile.boundingVolume; const boundingVolumePath = tilePath + "/boundingVolume"; // The boundingVolume MUST be defined - const boundingVolumeValid = await BoundingVolumeValidator.validateBoundingVolume( - boundingVolumePath, - boundingVolume, - context - ); + const boundingVolumeValid = + await BoundingVolumeValidator.validateBoundingVolume( + boundingVolumePath, + boundingVolume, + context + ); if (!boundingVolumeValid) { result = false; } @@ -248,9 +249,7 @@ export class TileValidator { validationState, context ); - if ( - !simpleTileValid - ) { + if (!simpleTileValid) { result = false; } } @@ -290,9 +289,7 @@ export class TileValidator { validationState, context ); - if ( - !contentValid - ) { + if (!contentValid) { result = false; } } @@ -326,9 +323,7 @@ export class TileValidator { validationState, context ); - if ( - !contentValid - ) { + if (!contentValid) { result = false; } } diff --git a/src/validation/TilesetTraversingValidator.ts b/src/validation/TilesetTraversingValidator.ts index c379d598..0e0b7bfe 100644 --- a/src/validation/TilesetTraversingValidator.ts +++ b/src/validation/TilesetTraversingValidator.ts @@ -194,7 +194,12 @@ export class TilesetTraversingValidator { const tile = traversedTile.asTile(); // Validate the tile itself - const tileValid = await TileValidator.validateTile(path, tile, validationState, context); + const tileValid = await TileValidator.validateTile( + path, + tile, + validationState, + context + ); if (!tileValid) { return false; } @@ -302,7 +307,11 @@ export class TilesetTraversingValidator { implicitTiling, subtreeResourceResolver ); - const result = await subtreeValidator.validateObject(subtreeUri, subtreeData, context); + const result = await subtreeValidator.validateObject( + subtreeUri, + subtreeData, + context + ); return result; } diff --git a/src/validation/TilesetValidator.ts b/src/validation/TilesetValidator.ts index 27637c52..a1113b75 100644 --- a/src/validation/TilesetValidator.ts +++ b/src/validation/TilesetValidator.ts @@ -84,7 +84,6 @@ export class TilesetValidator implements Validator { tileset: Tileset, context: ValidationContext ): Promise { - // Make sure that the given value is an object if (!BasicValidator.validateObject(path, "tileset", tileset, context)) { return false; diff --git a/src/validation/Validator.ts b/src/validation/Validator.ts index 66308e39..38616454 100644 --- a/src/validation/Validator.ts +++ b/src/validation/Validator.ts @@ -14,5 +14,9 @@ export interface Validator { * @returns A promise that is fulfilled when the validation is finished * and indicates whether the object was valid or not. */ - validateObject(path: string, input: T, context: ValidationContext): Promise; + validateObject( + path: string, + input: T, + context: ValidationContext + ): Promise; } diff --git a/src/validation/extensions/BoundingVolumeS2ValidationIssues.ts b/src/validation/extensions/BoundingVolumeS2ValidationIssues.ts index 619f017e..e3bd0e72 100644 --- a/src/validation/extensions/BoundingVolumeS2ValidationIssues.ts +++ b/src/validation/extensions/BoundingVolumeS2ValidationIssues.ts @@ -8,5 +8,4 @@ export class BoundingVolumeS2ValidationIssues { const issue = new ValidationIssue(type, path, message, severity); return issue; } - } From 84b833d756cdd88c3e78d441d8b18e705f486059 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Tue, 11 Oct 2022 23:43:37 +0200 Subject: [PATCH 06/22] Draft for plugin mechanism of content data validation --- specs/TilesetValidationSpec.ts | 3 + src/ValidatorMain.ts | 5 +- src/io/ResourceTypes.ts | 41 ++--- src/main.ts | 3 + src/tileFormats/B3dmValidator.ts | 23 --- src/tileFormats/CmptValidator.ts | 23 --- src/tileFormats/GltfValidator.ts | 4 +- src/tileFormats/I3dmValidator.ts | 23 --- src/tileFormats/PntsValidator.ts | 23 --- src/validation/ContentData.ts | 55 ++++++ src/validation/ContentDataValidator.ts | 234 +++++++++++++++---------- src/validation/TilesetValidator.ts | 2 +- src/validation/Validators.ts | 44 +++++ 13 files changed, 257 insertions(+), 226 deletions(-) create mode 100644 src/validation/ContentData.ts diff --git a/specs/TilesetValidationSpec.ts b/specs/TilesetValidationSpec.ts index e4f6f3f7..069e35d5 100644 --- a/specs/TilesetValidationSpec.ts +++ b/specs/TilesetValidationSpec.ts @@ -1,4 +1,7 @@ import { Validators } from "../src/validation/Validators"; +import { ContentDataValidator } from "../src/validation/ContentDataValidator"; + +ContentDataValidator.registerDefaults(); describe("Tileset validation", function () { it("detects issues in assetTilesetVersionInvalidType", async function () { diff --git a/src/ValidatorMain.ts b/src/ValidatorMain.ts index 4f91b51d..bafcee78 100644 --- a/src/ValidatorMain.ts +++ b/src/ValidatorMain.ts @@ -9,11 +9,12 @@ import { writeUnchecked } from "./base/writeUnchecked"; import { ValidationState } from "./validation/ValidationState"; import { Validators } from "./validation/Validators"; +import { ExtendedObjectsValidators } from "./validation/ExtendedObjectsValidators"; + +import { BoundingVolumeS2Validator } from "./validation/extensions/BoundingVolumeS2Validator"; import { TileImplicitTiling } from "./structure/TileImplicitTiling"; import { Schema } from "./structure/Metadata/Schema"; -import { BoundingVolumeS2Validator } from "./validation/extensions/BoundingVolumeS2Validator"; -import { ExtendedObjectsValidators } from "./validation/ExtendedObjectsValidators"; /** * A class summarizing the command-line functions of the validator. diff --git a/src/io/ResourceTypes.ts b/src/io/ResourceTypes.ts index 4ecd53fc..df8281eb 100644 --- a/src/io/ResourceTypes.ts +++ b/src/io/ResourceTypes.ts @@ -1,6 +1,5 @@ /** - * Methods to determine resource types based on the magic - * bytes of buffer data. + * Methods to determine resource type from buffer data. */ export class ResourceTypes { static isGzipped(buffer: Buffer): boolean { @@ -10,6 +9,14 @@ export class ResourceTypes { return buffer[0] === 0x1f && buffer[1] === 0x8b; } + static getMagic(buffer: Buffer): string | undefined { + if (buffer.length < 4) { + return undefined; + } + const magic = buffer.toString("utf8", 0, 4); + return magic; + } + static startsWith(buffer: Buffer, magic: string) { if (buffer.length < magic.length) { return false; @@ -18,37 +25,9 @@ export class ResourceTypes { return actual === magic; } - static isB3dm(buffer: Buffer): boolean { - return ResourceTypes.startsWith(buffer, "b3dm"); - } - - static isI3dm(buffer: Buffer): boolean { - return ResourceTypes.startsWith(buffer, "i3dm"); - } - - static isPnts(buffer: Buffer): boolean { - return ResourceTypes.startsWith(buffer, "pnts"); - } - - static isCmpt(buffer: Buffer): boolean { - return ResourceTypes.startsWith(buffer, "cmpt"); - } - - static isGlb(buffer: Buffer): boolean { - return ResourceTypes.startsWith(buffer, "glTF"); - } - static isSubt(buffer: Buffer): boolean { return ResourceTypes.startsWith(buffer, "subt"); - } - - static isGeom(buffer: Buffer): boolean { - return ResourceTypes.startsWith(buffer, "geom"); - } - - static isVctr(buffer: Buffer): boolean { - return ResourceTypes.startsWith(buffer, "vctr"); - } + } static isProbablyJson(buffer: Buffer): boolean { for (let i = 0; i < buffer.length; i++) { diff --git a/src/main.ts b/src/main.ts index 59abc122..0118251b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,9 +1,12 @@ //eslint-disable-next-line const yargs = require("yargs/yargs"); +import { ContentDataValidator } from "./validation/ContentDataValidator"; import { ValidatorMain } from "./ValidatorMain"; ValidatorMain.registerExtensionValidators(); +ContentDataValidator.registerDefaults(); + const args = yargs(process.argv.slice(1)) .help("help") diff --git a/src/tileFormats/B3dmValidator.ts b/src/tileFormats/B3dmValidator.ts index 375c891e..a74740a1 100644 --- a/src/tileFormats/B3dmValidator.ts +++ b/src/tileFormats/B3dmValidator.ts @@ -38,29 +38,6 @@ export class B3dmValidator implements Validator { uri: string, input: Buffer, context: ValidationContext - ): Promise { - // Create a new context to collect the issues that are - // found in the data. If there are issues, then they - // will be stored as the 'internal issues' of a - // single content validation issue. - const derivedContext = context.derive("."); - const result = await this.validateObjectInternal( - uri, - input, - derivedContext - ); - const derivedResult = derivedContext.getResult(); - const issue = ContentValidationIssues.createFrom(uri, derivedResult); - if (issue) { - context.addIssue(issue); - } - return result; - } - - async validateObjectInternal( - uri: string, - input: Buffer, - context: ValidationContext ): Promise { const headerByteLength = 28; diff --git a/src/tileFormats/CmptValidator.ts b/src/tileFormats/CmptValidator.ts index 64aa7f8d..e245cec0 100644 --- a/src/tileFormats/CmptValidator.ts +++ b/src/tileFormats/CmptValidator.ts @@ -22,29 +22,6 @@ export class CmptValidator implements Validator { uri: string, input: Buffer, context: ValidationContext - ): Promise { - // Create a new context to collect the issues that are - // found in the data. If there are issues, then they - // will be stored as the 'internal issues' of a - // single content validation issue. - const derivedContext = context.derive("."); - const result = await this.validateObjectInternal( - uri, - input, - derivedContext - ); - const derivedResult = derivedContext.getResult(); - const issue = ContentValidationIssues.createFrom(uri, derivedResult); - if (issue) { - context.addIssue(issue); - } - return result; - } - - async validateObjectInternal( - uri: string, - input: Buffer, - context: ValidationContext ): Promise { const headerByteLength = 16; diff --git a/src/tileFormats/GltfValidator.ts b/src/tileFormats/GltfValidator.ts index ebbf7334..cf65d852 100644 --- a/src/tileFormats/GltfValidator.ts +++ b/src/tileFormats/GltfValidator.ts @@ -48,14 +48,12 @@ export class GltfValidator implements Validator { context: ValidationContext ): Promise { const resourceResolver = context.getResourceResolver(); - const baseDirectory = path.dirname(uri); - const gltfResourceResolver = resourceResolver.derive(baseDirectory); let gltfResult = undefined; try { gltfResult = await validator.validateBytes(input, { uri: uri, externalResourceFunction: (gltfUri: string) => { - const resolvedDataPromise = gltfResourceResolver.resolve(gltfUri); + const resolvedDataPromise = resourceResolver.resolve(gltfUri); return resolvedDataPromise.then((resolvedData) => { if (!defined(resolvedData)) { throw "Could not resolve data from " + gltfUri; diff --git a/src/tileFormats/I3dmValidator.ts b/src/tileFormats/I3dmValidator.ts index 3a7ea8eb..b5d439d3 100644 --- a/src/tileFormats/I3dmValidator.ts +++ b/src/tileFormats/I3dmValidator.ts @@ -100,29 +100,6 @@ export class I3dmValidator implements Validator { uri: string, input: Buffer, context: ValidationContext - ): Promise { - // Create a new context to collect the issues that are - // found in the data. If there are issues, then they - // will be stored as the 'internal issues' of a - // single content validation issue. - const derivedContext = context.derive("."); - const result = await this.validateObjectInternal( - uri, - input, - derivedContext - ); - const derivedResult = derivedContext.getResult(); - const issue = ContentValidationIssues.createFrom(uri, derivedResult); - if (issue) { - context.addIssue(issue); - } - return result; - } - - async validateObjectInternal( - uri: string, - input: Buffer, - context: ValidationContext ): Promise { const headerByteLength = 32; diff --git a/src/tileFormats/PntsValidator.ts b/src/tileFormats/PntsValidator.ts index 506d58d3..6b46b17c 100644 --- a/src/tileFormats/PntsValidator.ts +++ b/src/tileFormats/PntsValidator.ts @@ -98,29 +98,6 @@ export class PntsValidator implements Validator { uri: string, input: Buffer, context: ValidationContext - ): Promise { - // Create a new context to collect the issues that are - // found in the data. If there are issues, then they - // will be stored as the 'internal issues' of a - // single content validation issue. - const derivedContext = context.derive("."); - const result = await this.validateObjectInternal( - uri, - input, - derivedContext - ); - const derivedResult = derivedContext.getResult(); - const issue = ContentValidationIssues.createFrom(uri, derivedResult); - if (issue) { - context.addIssue(issue); - } - return result; - } - - async validateObjectInternal( - uri: string, - input: Buffer, - context: ValidationContext ): Promise { const headerByteLength = 28; diff --git a/src/validation/ContentData.ts b/src/validation/ContentData.ts new file mode 100644 index 00000000..c75d32c6 --- /dev/null +++ b/src/validation/ContentData.ts @@ -0,0 +1,55 @@ +import path from "path"; + +import { ResourceTypes } from "../io/ResourceTypes"; + +/** + * A class summarizing information about content data. + * + * @private + */ +export class ContentData { + private readonly _uri: string; + private readonly _extension: string; + private readonly _magic: string | undefined; + private readonly _data: Buffer; + private readonly _parsedObject: any; + + constructor(uri: string, data: Buffer, parsedObject: any) { + this._uri = uri; + this._extension = path.extname(uri).toLowerCase(); + this._magic = ResourceTypes.getMagic(data); + this._data = data; + this._parsedObject = parsedObject; + } + + get uri() : string { + return this._uri; + } + + /** + * Returns a string that consists of the first 4 bytes + * of the buffer data, or `undefined` if the buffer + * contained less than 4 bytes + */ + get magic() : string | undefined { + return this._magic; + } + + /** + * Returns the extension of the file/URI from which + * the buffer data was read, in lowercase, including + * the `.` dot. + */ + get extension() : string { + return this._extension; + } + + get data() : Buffer { + return this._data; + } + + get parsedObject() : any { + return this._parsedObject; + } + +} diff --git a/src/validation/ContentDataValidator.ts b/src/validation/ContentDataValidator.ts index 6fbce3b3..d8281423 100644 --- a/src/validation/ContentDataValidator.ts +++ b/src/validation/ContentDataValidator.ts @@ -18,6 +18,13 @@ import { Content } from "../structure/Content"; import { IoValidationIssues } from "../issues/IoValidationIssue"; import { ContentValidationIssues } from "../issues/ContentValidationIssues"; +import { Validator } from "./Validator"; +import { ContentData } from "./ContentData"; + +type Entry = { + predicate: (contentData: ContentData) => boolean; + dataValidator: Validator; +}; /** * A class for validation of the data that is pointed to by a `content.uri`. @@ -26,6 +33,94 @@ import { ContentValidationIssues } from "../issues/ContentValidationIssues"; * @private */ export class ContentDataValidator { + + static dataValidators: Entry[] = []; + + static registerDefaults() { + ContentDataValidator.registerByMagic("glTF", new GltfValidator()); + ContentDataValidator.registerByMagic("b3dm", new B3dmValidator()); + ContentDataValidator.registerByMagic("i3dm", new I3dmValidator()); + ContentDataValidator.registerByMagic("cmpt", new CmptValidator()); + ContentDataValidator.registerByMagic("pnts", new PntsValidator()); + ContentDataValidator.registerByMagic("geom", Validators.createEmpty("Skipping 'geom' validation")); + ContentDataValidator.registerByMagic("vctr", Validators.createEmpty("Skipping 'vctr' validation")); + ContentDataValidator.registerByExtension(".geojson", Validators.createEmpty("Skipping 'GeoJSON' validation")); + ContentDataValidator.registerTileset(); + ContentDataValidator.registerGltf(); + } + + static registerByMagic(magic: string, dataValidator: Validator) { + ContentDataValidator.registerByPredicate( + (contentData: ContentData) => contentData.magic === magic, + dataValidator + ); + } + + static registerByExtension(extension: string, dataValidator: Validator) { + ContentDataValidator.registerByPredicate( + (contentData: ContentData) => contentData.extension === extension.toLowerCase(), + dataValidator + ); + } + + static registerTileset() { + const predicate = (contentData: ContentData) => ContentDataValidator.isProbablyTileset(contentData); + const externalValidator = Validators.createDefaultTilesetValidator(); + const dataValidator = Validators.wrap(externalValidator); + ContentDataValidator.registerByPredicate(predicate, dataValidator); + } + + static registerGltf() { + const predicate = (contentData: ContentData) => ContentDataValidator.isProbablyGltf(contentData); + const dataValidator = new GltfValidator(); + ContentDataValidator.registerByPredicate(predicate, dataValidator); + } + + private static isProbablyTileset(contentData : ContentData) { + const parsedObject = contentData.parsedObject; + if (!defined(parsedObject)) { + return false; + } + if (!defined(parsedObject.asset)) { + return false; + } + return defined(parsedObject.geometricError) || defined(parsedObject.root); + } + + private static isProbablyGltf(contentData : ContentData) { + const parsedObject = contentData.parsedObject; + if (!defined(parsedObject)) { + return false; + } + if (!defined(parsedObject.asset)) { + return false; + } + return true; + } + + + static registerByPredicate( + predicate: (contentData: ContentData) => boolean, + dataValidator: Validator + ) { + const entry = { + predicate: predicate, + dataValidator: dataValidator, + }; + ContentDataValidator.dataValidators.push(entry); + } + + private static findDataValidator( + contentData: ContentData + ): Validator | undefined { + for (const entry of ContentDataValidator.dataValidators) { + if (entry.predicate(contentData)) { + return entry.dataValidator; + } + } + return undefined; + } + /** * Validate the actual data that is referred to by the URI in the * given content. @@ -96,123 +191,68 @@ export class ContentDataValidator { * * @param contentPath The path for the `ValidationIssue` instances. * @param contentUri The URI of the content - * @param contentData The buffer containing the actual content data + * @param contentDataBuffer The buffer containing the actual content data * @param context The `ValidationContext` * @returns A promise that resolves when the validation is finished */ private static async validateContentDataInternal( contentPath: string, contentUri: string, - contentData: Buffer, + contentDataBuffer: Buffer, context: ValidationContext ): Promise { - // Figure out the type of the content data and pass it - // to the responsible validator. - - const isGlb = ResourceTypes.isGlb(contentData); - if (isGlb) { - console.log("Validating GLB: " + contentUri); - const dataValidator = new GltfValidator(); - const result = await dataValidator.validateObject( - contentUri, - contentData, - context - ); - return result; - } - const isB3dm = ResourceTypes.isB3dm(contentData); - if (isB3dm) { - console.log("Validating B3DM: " + contentUri); - const dataValidator = new B3dmValidator(); - const result = await dataValidator.validateObject( - contentUri, - contentData, - context - ); - return result; - } - - const isI3dm = ResourceTypes.isI3dm(contentData); - if (isI3dm) { - console.log("Validating I3DM: " + contentUri); - const dataValidator = new I3dmValidator(); - const result = await dataValidator.validateObject( - contentUri, - contentData, - context - ); - return result; - } - - const isPnts = ResourceTypes.isPnts(contentData); - if (isPnts) { - console.log("Validating PNTS: " + contentUri); - const dataValidator = new PntsValidator(); - const result = await dataValidator.validateObject( - contentUri, - contentData, - context - ); - return result; - } - - const isCmpt = ResourceTypes.isCmpt(contentData); - if (isCmpt) { - console.log("Validating CMPT: " + contentUri); - const dataValidator = new CmptValidator(); - const result = await dataValidator.validateObject( - contentUri, - contentData, - context - ); - return result; + // If the data is probably JSON, try to parse it in any case, + // and bail out if it cannot be parsed + const isJson = ResourceTypes.isProbablyJson(contentDataBuffer); + let parsedObject = undefined; + if (isJson) { + try { + parsedObject = JSON.parse(contentDataBuffer.toString()); + } catch (error) { + const issue = IoValidationIssues.JSON_PARSE_ERROR(contentUri, "" + error); + context.addIssue(issue); + return false; + } } - const isGeom = ResourceTypes.isGeom(contentData); - if (isGeom) { - const message = `Skipping validation of apparent GEOM file: ${contentUri}`; + // Create the `ContentData`, and look up a + // matching content data validator + const contentData = new ContentData(contentUri, contentDataBuffer, parsedObject); + const dataValidator = ContentDataValidator.findDataValidator(contentData); + if (!defined(dataValidator)) { + const path = contentPath; + const message = + `Tile content ${contentPath} refers to URI ${contentUri}, ` + + `for which no tile content type could be determined`; const issue = ContentValidationIssues.CONTENT_VALIDATION_WARNING( - contentUri, + path, message ); context.addIssue(issue); return true; } - const isVctr = ResourceTypes.isVctr(contentData); - if (isVctr) { - const message = `Skipping validation of apparent VCTR file: ${contentUri}`; - const issue = ContentValidationIssues.CONTENT_VALIDATION_WARNING( - contentUri, - message - ); + // Create a new context to collect the issues that are + // found in the data. If there are issues, then they + // will be stored as the 'internal issues' of a + // single content validation issue. + const dirName = paths.dirname(contentData.uri); + const derivedContext = context.derive(dirName); + const result = await dataValidator!.validateObject( + contentUri, + contentDataBuffer, + derivedContext + ); + const derivedResult = derivedContext.getResult(); + const issue = ContentValidationIssues.createFrom( + contentUri, + derivedResult + ); + if (issue) { context.addIssue(issue); - return true; } - - // When there is no known magic value, then it may be JSON. - const isJson = ResourceTypes.isProbablyJson(contentData); - if (isJson) { - const result = await ContentDataValidator.validateJsonContentData( - contentPath, - contentUri, - contentData, - context - ); - return result; - } - - const path = contentPath; - const message = - `Tile content ${contentPath} refers to URI ${contentUri}, ` + - `for which no tile content type could be determined`; - const issue = ContentValidationIssues.CONTENT_VALIDATION_WARNING( - path, - message - ); - context.addIssue(issue); - return true; + return result; } /** diff --git a/src/validation/TilesetValidator.ts b/src/validation/TilesetValidator.ts index a1113b75..afc9e51a 100644 --- a/src/validation/TilesetValidator.ts +++ b/src/validation/TilesetValidator.ts @@ -48,7 +48,7 @@ export class TilesetValidator implements Validator { } /** - * Implementation of the `Validator` interface that just the + * Implementation of the `Validator` interface that just passes the * input to `validateTileset`. * * @param path The path for `ValidationIssue` instances diff --git a/src/validation/Validators.ts b/src/validation/Validators.ts index c9565b86..3b55863f 100644 --- a/src/validation/Validators.ts +++ b/src/validation/Validators.ts @@ -13,6 +13,8 @@ import { ValidationState } from "./ValidationState"; import { IoValidationIssues } from "../issues/IoValidationIssue"; import { TileImplicitTiling } from "../structure/TileImplicitTiling"; +import { Validator } from "./Validator"; +import { ContentValidationIssues } from "../issues/ContentValidationIssues"; /** * Utility methods related to `Validator` instances. @@ -136,4 +138,46 @@ export class Validators { } return context.getResult(); } + + static wrap(delegate: Validator): Validator { + return { + async validateObject( + inputPath: string, + input: Buffer, + context: ValidationContext + ): Promise { + try { + const object: T = JSON.parse(input.toString()); + const delegateResult = await delegate.validateObject( + "", + object, + context + ); + return delegateResult; + } catch (error) { + const message = `${error}`; + const issue = IoValidationIssues.JSON_PARSE_ERROR(inputPath, message); + context.addIssue(issue); + return false; + } + }, + }; + } + + static createEmpty(message: string): Validator { + return { + async validateObject( + inputPath: string, + input: T, + context: ValidationContext + ): Promise { + const issue = ContentValidationIssues.CONTENT_VALIDATION_WARNING( + inputPath, + message + ); + context.addIssue(issue); + return true; + }, + }; + } } From 4cc10b698313a9effaa3f5bfb4a5dea6f73c77b8 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 12 Oct 2022 14:15:03 +0200 Subject: [PATCH 07/22] Cleanups for content data validators --- specs/TilesetValidationSpec.ts | 4 +- src/io/ResourceTypes.ts | 18 +- src/main.ts | 4 +- src/tileFormats/CmptValidator.ts | 1 - src/tileFormats/GltfValidator.ts | 1 - src/validation/ContentData.ts | 13 +- src/validation/ContentDataEntry.ts | 25 +++ src/validation/ContentDataValidator.ts | 219 +----------------------- src/validation/ContentDataValidators.ts | 192 +++++++++++++++++++++ src/validation/Validators.ts | 31 +++- 10 files changed, 279 insertions(+), 229 deletions(-) create mode 100644 src/validation/ContentDataEntry.ts create mode 100644 src/validation/ContentDataValidators.ts diff --git a/specs/TilesetValidationSpec.ts b/specs/TilesetValidationSpec.ts index 069e35d5..7197bfd4 100644 --- a/specs/TilesetValidationSpec.ts +++ b/specs/TilesetValidationSpec.ts @@ -1,7 +1,7 @@ import { Validators } from "../src/validation/Validators"; -import { ContentDataValidator } from "../src/validation/ContentDataValidator"; +import { ContentDataValidators } from "../src/validation/ContentDataValidators"; -ContentDataValidator.registerDefaults(); +ContentDataValidators.registerDefaults(); describe("Tileset validation", function () { it("detects issues in assetTilesetVersionInvalidType", async function () { diff --git a/src/io/ResourceTypes.ts b/src/io/ResourceTypes.ts index df8281eb..1e133565 100644 --- a/src/io/ResourceTypes.ts +++ b/src/io/ResourceTypes.ts @@ -9,11 +9,19 @@ export class ResourceTypes { return buffer[0] === 0x1f && buffer[1] === 0x8b; } - static getMagic(buffer: Buffer): string | undefined { - if (buffer.length < 4) { - return undefined; - } - const magic = buffer.toString("utf8", 0, 4); + /** + * Returns the magic header of the given buffer, as a string. + * + * This is a string that consists of the first 4 bytes of + * the buffer data, or fewer bytes if the buffer has less + * than 4 bytes. + * + * @param buffer The buffer + * @returns + */ + static getMagic(buffer: Buffer): string { + const length = Math.min(buffer.length, 4); + const magic = buffer.toString("utf8", 0, length); return magic; } diff --git a/src/main.ts b/src/main.ts index 0118251b..d1688407 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,11 +1,11 @@ //eslint-disable-next-line const yargs = require("yargs/yargs"); -import { ContentDataValidator } from "./validation/ContentDataValidator"; +import { ContentDataValidators } from "./validation/ContentDataValidators"; import { ValidatorMain } from "./ValidatorMain"; ValidatorMain.registerExtensionValidators(); -ContentDataValidator.registerDefaults(); +ContentDataValidators.registerDefaults(); const args = yargs(process.argv.slice(1)) diff --git a/src/tileFormats/CmptValidator.ts b/src/tileFormats/CmptValidator.ts index e245cec0..5181ce64 100644 --- a/src/tileFormats/CmptValidator.ts +++ b/src/tileFormats/CmptValidator.ts @@ -10,7 +10,6 @@ import { PntsValidator } from "./PntsValidator"; import { B3dmValidator } from "./B3dmValidator"; import { TileFormatValidator } from "./TileFormatValidator"; -import { ContentValidationIssues } from "../issues/ContentValidationIssues"; import { BinaryValidationIssues } from "../issues/BinaryValidationIssues"; /** diff --git a/src/tileFormats/GltfValidator.ts b/src/tileFormats/GltfValidator.ts index cf65d852..68fe01a6 100644 --- a/src/tileFormats/GltfValidator.ts +++ b/src/tileFormats/GltfValidator.ts @@ -1,4 +1,3 @@ -import path from "path"; import { defined } from "../base/defined"; import { Validator } from "../validation/Validator"; diff --git a/src/validation/ContentData.ts b/src/validation/ContentData.ts index c75d32c6..8155a8ad 100644 --- a/src/validation/ContentData.ts +++ b/src/validation/ContentData.ts @@ -5,12 +5,17 @@ import { ResourceTypes } from "../io/ResourceTypes"; /** * A class summarizing information about content data. * + * This is only used in the `ContentDataValidator` and + * `ContentDataValidators` classes, to facilitate the + * lookup up validators for given content data, based + * on criteria like the file extension or magic header. + * * @private */ export class ContentData { private readonly _uri: string; private readonly _extension: string; - private readonly _magic: string | undefined; + private readonly _magic: string; private readonly _data: Buffer; private readonly _parsedObject: any; @@ -28,10 +33,10 @@ export class ContentData { /** * Returns a string that consists of the first 4 bytes - * of the buffer data, or `undefined` if the buffer - * contained less than 4 bytes + * of the buffer data (or fewer, if the buffer contains + * less than 4 bytes) */ - get magic() : string | undefined { + get magic() : string { return this._magic; } diff --git a/src/validation/ContentDataEntry.ts b/src/validation/ContentDataEntry.ts new file mode 100644 index 00000000..e887abd1 --- /dev/null +++ b/src/validation/ContentDataEntry.ts @@ -0,0 +1,25 @@ +import { Validator } from "./Validator"; +import { ContentData } from "./ContentData"; + +/** + * An entry of the registered content data validators, + * used in the `ContentDataValidators`. + * + * @private + */ +export type ContentDataEntry = { + + /** + * A predicate that determines - for a given `ContentData` - + * whether the `dataValidator` should be used to validate + * the content data. + */ + predicate: (contentData: ContentData) => boolean; + + /** + * The `Validator` that will be applied to the content + * data when the predicate matches a given `ContentData` + * object. + */ + dataValidator: Validator; +}; diff --git a/src/validation/ContentDataValidator.ts b/src/validation/ContentDataValidator.ts index d8281423..c8050d50 100644 --- a/src/validation/ContentDataValidator.ts +++ b/src/validation/ContentDataValidator.ts @@ -5,122 +5,22 @@ import { defined } from "../base/defined"; import { Uris } from "../io/Uris"; import { ResourceTypes } from "../io/ResourceTypes"; -import { Validators } from "./Validators"; import { ValidationContext } from "./ValidationContext"; - -import { B3dmValidator } from "../tileFormats/B3dmValidator"; -import { I3dmValidator } from "../tileFormats/I3dmValidator"; -import { PntsValidator } from "../tileFormats/PntsValidator"; -import { CmptValidator } from "../tileFormats/CmptValidator"; -import { GltfValidator } from "../tileFormats/GltfValidator"; +import { ContentData } from "./ContentData"; +import { ContentDataValidators } from "./ContentDataValidators"; import { Content } from "../structure/Content"; import { IoValidationIssues } from "../issues/IoValidationIssue"; import { ContentValidationIssues } from "../issues/ContentValidationIssues"; -import { Validator } from "./Validator"; -import { ContentData } from "./ContentData"; - -type Entry = { - predicate: (contentData: ContentData) => boolean; - dataValidator: Validator; -}; /** * A class for validation of the data that is pointed to by a `content.uri`. * - * * @private */ export class ContentDataValidator { - static dataValidators: Entry[] = []; - - static registerDefaults() { - ContentDataValidator.registerByMagic("glTF", new GltfValidator()); - ContentDataValidator.registerByMagic("b3dm", new B3dmValidator()); - ContentDataValidator.registerByMagic("i3dm", new I3dmValidator()); - ContentDataValidator.registerByMagic("cmpt", new CmptValidator()); - ContentDataValidator.registerByMagic("pnts", new PntsValidator()); - ContentDataValidator.registerByMagic("geom", Validators.createEmpty("Skipping 'geom' validation")); - ContentDataValidator.registerByMagic("vctr", Validators.createEmpty("Skipping 'vctr' validation")); - ContentDataValidator.registerByExtension(".geojson", Validators.createEmpty("Skipping 'GeoJSON' validation")); - ContentDataValidator.registerTileset(); - ContentDataValidator.registerGltf(); - } - - static registerByMagic(magic: string, dataValidator: Validator) { - ContentDataValidator.registerByPredicate( - (contentData: ContentData) => contentData.magic === magic, - dataValidator - ); - } - - static registerByExtension(extension: string, dataValidator: Validator) { - ContentDataValidator.registerByPredicate( - (contentData: ContentData) => contentData.extension === extension.toLowerCase(), - dataValidator - ); - } - - static registerTileset() { - const predicate = (contentData: ContentData) => ContentDataValidator.isProbablyTileset(contentData); - const externalValidator = Validators.createDefaultTilesetValidator(); - const dataValidator = Validators.wrap(externalValidator); - ContentDataValidator.registerByPredicate(predicate, dataValidator); - } - - static registerGltf() { - const predicate = (contentData: ContentData) => ContentDataValidator.isProbablyGltf(contentData); - const dataValidator = new GltfValidator(); - ContentDataValidator.registerByPredicate(predicate, dataValidator); - } - - private static isProbablyTileset(contentData : ContentData) { - const parsedObject = contentData.parsedObject; - if (!defined(parsedObject)) { - return false; - } - if (!defined(parsedObject.asset)) { - return false; - } - return defined(parsedObject.geometricError) || defined(parsedObject.root); - } - - private static isProbablyGltf(contentData : ContentData) { - const parsedObject = contentData.parsedObject; - if (!defined(parsedObject)) { - return false; - } - if (!defined(parsedObject.asset)) { - return false; - } - return true; - } - - - static registerByPredicate( - predicate: (contentData: ContentData) => boolean, - dataValidator: Validator - ) { - const entry = { - predicate: predicate, - dataValidator: dataValidator, - }; - ContentDataValidator.dataValidators.push(entry); - } - - private static findDataValidator( - contentData: ContentData - ): Validator | undefined { - for (const entry of ContentDataValidator.dataValidators) { - if (entry.predicate(contentData)) { - return entry.dataValidator; - } - } - return undefined; - } - /** * Validate the actual data that is referred to by the URI in the * given content. @@ -210,7 +110,8 @@ export class ContentDataValidator { try { parsedObject = JSON.parse(contentDataBuffer.toString()); } catch (error) { - const issue = IoValidationIssues.JSON_PARSE_ERROR(contentUri, "" + error); + const message = `${error}`; + const issue = IoValidationIssues.JSON_PARSE_ERROR(contentUri, message); context.addIssue(issue); return false; } @@ -219,12 +120,12 @@ export class ContentDataValidator { // Create the `ContentData`, and look up a // matching content data validator const contentData = new ContentData(contentUri, contentDataBuffer, parsedObject); - const dataValidator = ContentDataValidator.findDataValidator(contentData); + const dataValidator = ContentDataValidators.findContentDataValidator(contentData); if (!defined(dataValidator)) { const path = contentPath; const message = `Tile content ${contentPath} refers to URI ${contentUri}, ` + - `for which no tile content type could be determined`; + `for which no content type could be determined`; const issue = ContentValidationIssues.CONTENT_VALIDATION_WARNING( path, message @@ -233,10 +134,9 @@ export class ContentDataValidator { return true; } - // Create a new context to collect the issues that are - // found in the data. If there are issues, then they - // will be stored as the 'internal issues' of a - // single content validation issue. + // Create a new context to collect the issues that are found in + // the data. If there are issues, then they will be stored as + // the 'causes' of a single content validation issue. const dirName = paths.dirname(contentData.uri); const derivedContext = context.derive(dirName); const result = await dataValidator!.validateObject( @@ -254,105 +154,4 @@ export class ContentDataValidator { } return result; } - - /** - * Perform the validation of the given content data, which already - * has been determined to (probably) be JSON data. - * - * The method will try to figure out the actual data type using - * a few guesses, and try to validate the data. - * - * If the data causes validation issues, they will be summarized - * into a `CONTENT_VALIDATION_ERROR` or `CONTENT_VALIDATION_WARNING` - * that is added to the given context. - * - * If the data type cannot be determined, an `CONTENT_VALIDATION_WARNING` - * will be added to the given context. - * - * @param contentPath The path for the `ValidationIssue` instances. - * @param contentUri The URI of the content - * @param contentData The buffer containing the actual content data - * @param context The `ValidationContext` - * @returns A promise that resolves when the validation is finished - */ - private static async validateJsonContentData( - contentPath: string, - contentUri: string, - contentData: Buffer, - context: ValidationContext - ): Promise { - // If the data is probably JSON, try to parse it in any case, - // and bail out if it cannot be parsed - let parsedObject = undefined; - try { - parsedObject = JSON.parse(contentData.toString()); - } catch (error) { - const issue = IoValidationIssues.JSON_PARSE_ERROR(contentUri, "" + error); - context.addIssue(issue); - return false; - } - - // Try to rule out JSON files which will not be validated anyhow - const ext = paths.extname(contentUri).toLowerCase(); - if (ext === ".geojson") { - const message = `Skipping validation of apparent GeoJson file: ${contentUri}`; - const issue = ContentValidationIssues.CONTENT_VALIDATION_WARNING( - contentUri, - message - ); - context.addIssue(issue); - return true; - } - - // An 'asset' may indicate an external tileset or a glTF... - if (defined(parsedObject.asset)) { - // When there is a `geometricError` or a `root`, - // let's assume that it is an external tileset: - if (defined(parsedObject.geometricError) || defined(parsedObject.root)) { - console.log("Validating as external tileset: " + contentUri); - // Create a new context to collect the issues that are - // found in the data. If there are issues, then they - // will be stored as the 'internal issues' of a - // single content validation issue. - const dirName = paths.dirname(contentUri); - const derivedContext = context.derive(dirName); - const externalValidator = Validators.createDefaultTilesetValidator(); - const result = await externalValidator.validateObject( - contentUri, - parsedObject, - derivedContext - ); - const derivedResult = derivedContext.getResult(); - const issue = ContentValidationIssues.createFrom( - contentUri, - derivedResult - ); - if (issue) { - context.addIssue(issue); - } - return result; - } - - // The parsed object has an 'asset', but is no tileset. - // Assume that it is a glTF: - console.log("Validating glTF: " + contentUri); - const gltfValidator = new GltfValidator(); - const result = await gltfValidator.validateObject( - contentUri, - contentData, - context - ); - return result; - } - const path = contentPath; - const message = - `Tile content ${contentPath} refers to URI ${contentUri}, which ` + - `contains JSON data, but for which no type could be determined`; - const issue = ContentValidationIssues.CONTENT_VALIDATION_WARNING( - path, - message - ); - context.addIssue(issue); - return true; - } } diff --git a/src/validation/ContentDataValidators.ts b/src/validation/ContentDataValidators.ts new file mode 100644 index 00000000..dd6b9c52 --- /dev/null +++ b/src/validation/ContentDataValidators.ts @@ -0,0 +1,192 @@ +import { defined } from "../base/defined"; + +import { Validators } from "./Validators"; +import { Validator } from "./Validator"; +import { ContentData } from "./ContentData"; +import { ContentDataEntry } from "./ContentDataEntry"; + +import { B3dmValidator } from "../tileFormats/B3dmValidator"; +import { I3dmValidator } from "../tileFormats/I3dmValidator"; +import { PntsValidator } from "../tileFormats/PntsValidator"; +import { CmptValidator } from "../tileFormats/CmptValidator"; +import { GltfValidator } from "../tileFormats/GltfValidator"; + +import { Tileset } from "../structure/Tileset"; + +/** + * A class for managing `Validator` instances that are used for + * validating the data that is pointed to by a `content.uri`. + * + * The only public methods (for now) are `registerDefaults`, + * which registers all known content data validators, and + * `findContentDataValidator`, which returns the validator + * that should be used for a given `ContentData` object. + * + * @private + */ +export class ContentDataValidators { + + /** + * The list of validators that have been registered. + */ + private static readonly dataValidators: ContentDataEntry[] = []; + + /** + * Registers all default content data validators + */ + static registerDefaults() { + // The validators will be checked in the order in which they are + // registered. In the futhre, there might be a mechanism for + // 'overriding' a previously registered validator. + ContentDataValidators.registerByMagic("glTF", new GltfValidator()); + ContentDataValidators.registerByMagic("b3dm", new B3dmValidator()); + ContentDataValidators.registerByMagic("i3dm", new I3dmValidator()); + ContentDataValidators.registerByMagic("cmpt", new CmptValidator()); + ContentDataValidators.registerByMagic("pnts", new PntsValidator()); + ContentDataValidators.registerByMagic("geom", Validators.createContentValidationWarning("Skipping 'geom' validation")); + ContentDataValidators.registerByMagic("vctr", Validators.createContentValidationWarning("Skipping 'vctr' validation")); + ContentDataValidators.registerByExtension(".geojson", Validators.createContentValidationWarning("Skipping 'GeoJSON' validation")); + ContentDataValidators.registerTileset(); + ContentDataValidators.registerGltf(); + } + + /** + * Tries to find a data validator that can be used for validating + * the given content data. If no matching validator can be found, + * then `undefined` is returned. + * + * @param contentData The `ContentData` + * @returns The validator, or `undefined` + */ + static findContentDataValidator( + contentData: ContentData + ): Validator | undefined { + for (const entry of ContentDataValidators.dataValidators) { + if (entry.predicate(contentData)) { + return entry.dataValidator; + } + } + return undefined; + } + + /** + * Register a validator that should be used when the content + * data starts with the given magic string. + * + * (This string is currently assumed to have length 4, but + * this may have to be generalized in the future) + * + * @param magic The magic string + * @param dataValidator The data validator + */ + private static registerByMagic(magic: string, dataValidator: Validator) { + ContentDataValidators.registerByPredicate( + (contentData: ContentData) => contentData.magic === magic, + dataValidator + ); + } + + /** + * Register a validator that should be used when the content URI + * has the given file extension + * + * The file extension should include the `"."` dot, and the + * check for the file extension will be case INsensitive. + * + * @param extension The extension + * @param dataValidator The data validator + */ + private static registerByExtension(extension: string, dataValidator: Validator) { + ContentDataValidators.registerByPredicate( + (contentData: ContentData) => contentData.extension === extension.toLowerCase(), + dataValidator + ); + } + + /** + * Register the data validator for (external) tileset files. + * + * The condition of whether this validator is used for + * given content data is that it `isProbablyTileset`. + */ + private static registerTileset() { + const predicate = (contentData: ContentData) => ContentDataValidators.isProbablyTileset(contentData); + const externalValidator = Validators.createDefaultTilesetValidator(); + const dataValidator = Validators.parseFromBuffer(externalValidator); + ContentDataValidators.registerByPredicate(predicate, dataValidator); + } + + /** + * Register the data validator for glTF files. + * + * This refers to JSON files (not GLB files), and checks + * whether the object that is parsed from the JSON data + * is probably a glTF asset, as of `isProbablyGltf`. + */ + private static registerGltf() { + const predicate = (contentData: ContentData) => ContentDataValidators.isProbablyGltf(contentData); + const dataValidator = new GltfValidator(); + ContentDataValidators.registerByPredicate(predicate, dataValidator); + } + + /** + * Returns whether the given content data is probably a tileset. + * + * The exact conditions for this method returning `true` are + * intentionally not specified. + * + * @param contentData The content data + * @returns Whether the content data is probably a tileset + */ + private static isProbablyTileset(contentData : ContentData) { + const parsedObject = contentData.parsedObject; + if (!defined(parsedObject)) { + return false; + } + if (!defined(parsedObject.asset)) { + return false; + } + return defined(parsedObject.geometricError) || defined(parsedObject.root); + } + + /** + * Returns whether the given content data is probably a glTF + * (not a GLB, but a glTF JSON). + * + * The exact conditions for this method returning `true` are + * intentionally not specified. + * + * @param contentData The content data + * @returns Whether the content data is probably glTF + */ + private static isProbablyGltf(contentData : ContentData) { + const parsedObject = contentData.parsedObject; + if (!defined(parsedObject)) { + return false; + } + if (!defined(parsedObject.asset)) { + return false; + } + return true; + } + + /** + * Registers a data validator that will be used when a + * `ContentData` matches the given predicate. + * + * @param predicate The predicate + * @param dataValidator The data validator + */ + private static registerByPredicate( + predicate: (contentData: ContentData) => boolean, + dataValidator: Validator + ) { + const entry = { + predicate: predicate, + dataValidator: dataValidator, + }; + ContentDataValidators.dataValidators.push(entry); + } + + +} diff --git a/src/validation/Validators.ts b/src/validation/Validators.ts index 3b55863f..969defbc 100644 --- a/src/validation/Validators.ts +++ b/src/validation/Validators.ts @@ -139,7 +139,19 @@ export class Validators { return context.getResult(); } - static wrap(delegate: Validator): Validator { + /** + * Creates a validator for `Buffer` objects that parses an + * object of type `T` from the (JSON) string representation + * of the buffer contents, and applies the given delegate + * to the result. + * + * If the object cannot be parsed, a `JSON_PARSE_ERROR` + * will be added to the given context. + * + * @param delegate The delegate + * @returns The new validator + */ + static parseFromBuffer(delegate: Validator): Validator { return { async validateObject( inputPath: string, @@ -149,7 +161,7 @@ export class Validators { try { const object: T = JSON.parse(input.toString()); const delegateResult = await delegate.validateObject( - "", + inputPath, object, context ); @@ -164,11 +176,22 @@ export class Validators { }; } - static createEmpty(message: string): Validator { + /** + * Creates a `Validator` that only adds a `CONTENT_VALIDATION_WARNING` + * with the given message to the given context when it is called. + * + * This is used for "dummy" validators that handle content data types + * that are already anticipated (like VCTR or GEOM), but not validated + * explicitly. + * + * @param message The message for the warning + * @returns The new validator + */ + static createContentValidationWarning(message: string): Validator { return { async validateObject( inputPath: string, - input: T, + input: Buffer, context: ValidationContext ): Promise { const issue = ContentValidationIssues.CONTENT_VALIDATION_WARNING( From 2da4dcfd4d2b45dde0b6d73ec92c03f5b983fad9 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 12 Oct 2022 14:16:30 +0200 Subject: [PATCH 08/22] Formatting --- src/io/ResourceTypes.ts | 10 +-- src/main.ts | 1 - src/validation/ContentData.ts | 17 ++-- src/validation/ContentDataEntry.ts | 5 +- src/validation/ContentDataValidator.ts | 22 ++--- src/validation/ContentDataValidators.ts | 108 ++++++++++++++---------- src/validation/Validators.ts | 12 +-- 7 files changed, 94 insertions(+), 81 deletions(-) diff --git a/src/io/ResourceTypes.ts b/src/io/ResourceTypes.ts index 1e133565..83f69b58 100644 --- a/src/io/ResourceTypes.ts +++ b/src/io/ResourceTypes.ts @@ -11,13 +11,13 @@ export class ResourceTypes { /** * Returns the magic header of the given buffer, as a string. - * + * * This is a string that consists of the first 4 bytes of * the buffer data, or fewer bytes if the buffer has less - * than 4 bytes. - * + * than 4 bytes. + * * @param buffer The buffer - * @returns + * @returns The magic header */ static getMagic(buffer: Buffer): string { const length = Math.min(buffer.length, 4); @@ -35,7 +35,7 @@ export class ResourceTypes { static isSubt(buffer: Buffer): boolean { return ResourceTypes.startsWith(buffer, "subt"); - } + } static isProbablyJson(buffer: Buffer): boolean { for (let i = 0; i < buffer.length; i++) { diff --git a/src/main.ts b/src/main.ts index d1688407..53e972e0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,7 +7,6 @@ import { ValidatorMain } from "./ValidatorMain"; ValidatorMain.registerExtensionValidators(); ContentDataValidators.registerDefaults(); - const args = yargs(process.argv.slice(1)) .help("help") .alias("help", "h") diff --git a/src/validation/ContentData.ts b/src/validation/ContentData.ts index 8155a8ad..7c8e4071 100644 --- a/src/validation/ContentData.ts +++ b/src/validation/ContentData.ts @@ -4,12 +4,12 @@ import { ResourceTypes } from "../io/ResourceTypes"; /** * A class summarizing information about content data. - * + * * This is only used in the `ContentDataValidator` and * `ContentDataValidators` classes, to facilitate the * lookup up validators for given content data, based * on criteria like the file extension or magic header. - * + * * @private */ export class ContentData { @@ -26,8 +26,8 @@ export class ContentData { this._data = data; this._parsedObject = parsedObject; } - - get uri() : string { + + get uri(): string { return this._uri; } @@ -36,7 +36,7 @@ export class ContentData { * of the buffer data (or fewer, if the buffer contains * less than 4 bytes) */ - get magic() : string { + get magic(): string { return this._magic; } @@ -45,16 +45,15 @@ export class ContentData { * the buffer data was read, in lowercase, including * the `.` dot. */ - get extension() : string { + get extension(): string { return this._extension; } - get data() : Buffer { + get data(): Buffer { return this._data; } - get parsedObject() : any { + get parsedObject(): any { return this._parsedObject; } - } diff --git a/src/validation/ContentDataEntry.ts b/src/validation/ContentDataEntry.ts index e887abd1..f1230db2 100644 --- a/src/validation/ContentDataEntry.ts +++ b/src/validation/ContentDataEntry.ts @@ -4,11 +4,10 @@ import { ContentData } from "./ContentData"; /** * An entry of the registered content data validators, * used in the `ContentDataValidators`. - * + * * @private */ export type ContentDataEntry = { - /** * A predicate that determines - for a given `ContentData` - * whether the `dataValidator` should be used to validate @@ -17,7 +16,7 @@ export type ContentDataEntry = { predicate: (contentData: ContentData) => boolean; /** - * The `Validator` that will be applied to the content + * The `Validator` that will be applied to the content * data when the predicate matches a given `ContentData` * object. */ diff --git a/src/validation/ContentDataValidator.ts b/src/validation/ContentDataValidator.ts index c8050d50..b65ddad1 100644 --- a/src/validation/ContentDataValidator.ts +++ b/src/validation/ContentDataValidator.ts @@ -20,7 +20,6 @@ import { ContentValidationIssues } from "../issues/ContentValidationIssues"; * @private */ export class ContentDataValidator { - /** * Validate the actual data that is referred to by the URI in the * given content. @@ -101,7 +100,6 @@ export class ContentDataValidator { contentDataBuffer: Buffer, context: ValidationContext ): Promise { - // If the data is probably JSON, try to parse it in any case, // and bail out if it cannot be parsed const isJson = ResourceTypes.isProbablyJson(contentDataBuffer); @@ -117,10 +115,15 @@ export class ContentDataValidator { } } - // Create the `ContentData`, and look up a + // Create the `ContentData`, and look up a // matching content data validator - const contentData = new ContentData(contentUri, contentDataBuffer, parsedObject); - const dataValidator = ContentDataValidators.findContentDataValidator(contentData); + const contentData = new ContentData( + contentUri, + contentDataBuffer, + parsedObject + ); + const dataValidator = + ContentDataValidators.findContentDataValidator(contentData); if (!defined(dataValidator)) { const path = contentPath; const message = @@ -134,8 +137,8 @@ export class ContentDataValidator { return true; } - // Create a new context to collect the issues that are found in - // the data. If there are issues, then they will be stored as + // Create a new context to collect the issues that are found in + // the data. If there are issues, then they will be stored as // the 'causes' of a single content validation issue. const dirName = paths.dirname(contentData.uri); const derivedContext = context.derive(dirName); @@ -145,10 +148,7 @@ export class ContentDataValidator { derivedContext ); const derivedResult = derivedContext.getResult(); - const issue = ContentValidationIssues.createFrom( - contentUri, - derivedResult - ); + const issue = ContentValidationIssues.createFrom(contentUri, derivedResult); if (issue) { context.addIssue(issue); } diff --git a/src/validation/ContentDataValidators.ts b/src/validation/ContentDataValidators.ts index dd6b9c52..17a67843 100644 --- a/src/validation/ContentDataValidators.ts +++ b/src/validation/ContentDataValidators.ts @@ -14,18 +14,17 @@ import { GltfValidator } from "../tileFormats/GltfValidator"; import { Tileset } from "../structure/Tileset"; /** - * A class for managing `Validator` instances that are used for + * A class for managing `Validator` instances that are used for * validating the data that is pointed to by a `content.uri`. - * - * The only public methods (for now) are `registerDefaults`, - * which registers all known content data validators, and + * + * The only public methods (for now) are `registerDefaults`, + * which registers all known content data validators, and * `findContentDataValidator`, which returns the validator * that should be used for a given `ContentData` object. * * @private */ export class ContentDataValidators { - /** * The list of validators that have been registered. */ @@ -35,17 +34,26 @@ export class ContentDataValidators { * Registers all default content data validators */ static registerDefaults() { - // The validators will be checked in the order in which they are - // registered. In the futhre, there might be a mechanism for + // The validators will be checked in the order in which they are + // registered. In the futhre, there might be a mechanism for // 'overriding' a previously registered validator. ContentDataValidators.registerByMagic("glTF", new GltfValidator()); ContentDataValidators.registerByMagic("b3dm", new B3dmValidator()); ContentDataValidators.registerByMagic("i3dm", new I3dmValidator()); ContentDataValidators.registerByMagic("cmpt", new CmptValidator()); ContentDataValidators.registerByMagic("pnts", new PntsValidator()); - ContentDataValidators.registerByMagic("geom", Validators.createContentValidationWarning("Skipping 'geom' validation")); - ContentDataValidators.registerByMagic("vctr", Validators.createContentValidationWarning("Skipping 'vctr' validation")); - ContentDataValidators.registerByExtension(".geojson", Validators.createContentValidationWarning("Skipping 'GeoJSON' validation")); + ContentDataValidators.registerByMagic( + "geom", + Validators.createContentValidationWarning("Skipping 'geom' validation") + ); + ContentDataValidators.registerByMagic( + "vctr", + Validators.createContentValidationWarning("Skipping 'vctr' validation") + ); + ContentDataValidators.registerByExtension( + ".geojson", + Validators.createContentValidationWarning("Skipping 'GeoJSON' validation") + ); ContentDataValidators.registerTileset(); ContentDataValidators.registerGltf(); } @@ -53,8 +61,8 @@ export class ContentDataValidators { /** * Tries to find a data validator that can be used for validating * the given content data. If no matching validator can be found, - * then `undefined` is returned. - * + * then `undefined` is returned. + * * @param contentData The `ContentData` * @returns The validator, or `undefined` */ @@ -71,15 +79,18 @@ export class ContentDataValidators { /** * Register a validator that should be used when the content - * data starts with the given magic string. - * - * (This string is currently assumed to have length 4, but + * data starts with the given magic string. + * + * (This string is currently assumed to have length 4, but * this may have to be generalized in the future) - * + * * @param magic The magic string * @param dataValidator The data validator */ - private static registerByMagic(magic: string, dataValidator: Validator) { + private static registerByMagic( + magic: string, + dataValidator: Validator + ) { ContentDataValidators.registerByPredicate( (contentData: ContentData) => contentData.magic === magic, dataValidator @@ -89,56 +100,63 @@ export class ContentDataValidators { /** * Register a validator that should be used when the content URI * has the given file extension - * - * The file extension should include the `"."` dot, and the + * + * The file extension should include the `"."` dot, and the * check for the file extension will be case INsensitive. - * + * * @param extension The extension * @param dataValidator The data validator */ - private static registerByExtension(extension: string, dataValidator: Validator) { + private static registerByExtension( + extension: string, + dataValidator: Validator + ) { ContentDataValidators.registerByPredicate( - (contentData: ContentData) => contentData.extension === extension.toLowerCase(), + (contentData: ContentData) => + contentData.extension === extension.toLowerCase(), dataValidator ); } /** * Register the data validator for (external) tileset files. - * - * The condition of whether this validator is used for + * + * The condition of whether this validator is used for * given content data is that it `isProbablyTileset`. */ - private static registerTileset() { - const predicate = (contentData: ContentData) => ContentDataValidators.isProbablyTileset(contentData); + private static registerTileset() { + const predicate = (contentData: ContentData) => + ContentDataValidators.isProbablyTileset(contentData); const externalValidator = Validators.createDefaultTilesetValidator(); - const dataValidator = Validators.parseFromBuffer(externalValidator); + const dataValidator = + Validators.parseFromBuffer(externalValidator); ContentDataValidators.registerByPredicate(predicate, dataValidator); } /** - * Register the data validator for glTF files. - * + * Register the data validator for glTF files. + * * This refers to JSON files (not GLB files), and checks * whether the object that is parsed from the JSON data * is probably a glTF asset, as of `isProbablyGltf`. */ - private static registerGltf() { - const predicate = (contentData: ContentData) => ContentDataValidators.isProbablyGltf(contentData); + private static registerGltf() { + const predicate = (contentData: ContentData) => + ContentDataValidators.isProbablyGltf(contentData); const dataValidator = new GltfValidator(); ContentDataValidators.registerByPredicate(predicate, dataValidator); } /** * Returns whether the given content data is probably a tileset. - * - * The exact conditions for this method returning `true` are + * + * The exact conditions for this method returning `true` are * intentionally not specified. - * + * * @param contentData The content data * @returns Whether the content data is probably a tileset */ - private static isProbablyTileset(contentData : ContentData) { + private static isProbablyTileset(contentData: ContentData) { const parsedObject = contentData.parsedObject; if (!defined(parsedObject)) { return false; @@ -150,16 +168,16 @@ export class ContentDataValidators { } /** - * Returns whether the given content data is probably a glTF - * (not a GLB, but a glTF JSON). - * - * The exact conditions for this method returning `true` are + * Returns whether the given content data is probably a glTF + * (not a GLB, but a glTF JSON). + * + * The exact conditions for this method returning `true` are * intentionally not specified. - * + * * @param contentData The content data - * @returns Whether the content data is probably glTF + * @returns Whether the content data is probably glTF */ - private static isProbablyGltf(contentData : ContentData) { + private static isProbablyGltf(contentData: ContentData) { const parsedObject = contentData.parsedObject; if (!defined(parsedObject)) { return false; @@ -171,9 +189,9 @@ export class ContentDataValidators { } /** - * Registers a data validator that will be used when a + * Registers a data validator that will be used when a * `ContentData` matches the given predicate. - * + * * @param predicate The predicate * @param dataValidator The data validator */ @@ -187,6 +205,4 @@ export class ContentDataValidators { }; ContentDataValidators.dataValidators.push(entry); } - - } diff --git a/src/validation/Validators.ts b/src/validation/Validators.ts index 969defbc..e7b97bd1 100644 --- a/src/validation/Validators.ts +++ b/src/validation/Validators.ts @@ -140,15 +140,15 @@ export class Validators { } /** - * Creates a validator for `Buffer` objects that parses an + * Creates a validator for `Buffer` objects that parses an * object of type `T` from the (JSON) string representation * of the buffer contents, and applies the given delegate * to the result. - * + * * If the object cannot be parsed, a `JSON_PARSE_ERROR` * will be added to the given context. - * - * @param delegate The delegate + * + * @param delegate The delegate * @returns The new validator */ static parseFromBuffer(delegate: Validator): Validator { @@ -179,11 +179,11 @@ export class Validators { /** * Creates a `Validator` that only adds a `CONTENT_VALIDATION_WARNING` * with the given message to the given context when it is called. - * + * * This is used for "dummy" validators that handle content data types * that are already anticipated (like VCTR or GEOM), but not validated * explicitly. - * + * * @param message The message for the warning * @returns The new validator */ From 6923c24d029dc8719bb733f29fa7f336c85168ca Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 12 Oct 2022 14:21:10 +0200 Subject: [PATCH 09/22] Fix validation of extensions of S2 object --- .../extensions/BoundingVolumeS2Validator.ts | 54 +++++++++---------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/src/validation/extensions/BoundingVolumeS2Validator.ts b/src/validation/extensions/BoundingVolumeS2Validator.ts index da84df69..51bf8d5b 100644 --- a/src/validation/extensions/BoundingVolumeS2Validator.ts +++ b/src/validation/extensions/BoundingVolumeS2Validator.ts @@ -45,35 +45,6 @@ export class BoundingVolumeS2Validator implements Validator { let result = true; - // Validate the object as a RootProperty - if ( - !RootPropertyValidator.validateRootProperty( - path, - "boundingVolume", - boundingVolume, - context - ) - ) { - result = false; - } - - // Perform the validation of the object in view of the - // extensions that it may contain - if ( - !ExtendedObjectsValidators.validateExtendedObject( - path, - boundingVolume, - context - ) - ) { - result = false; - } - // If there was an extension validator that overrides the - // default validation, then skip the remaining validation. - if (ExtendedObjectsValidators.hasOverride(boundingVolume)) { - return result; - } - // Validate the box const box = boundingVolume.box; const boxPath = path + "/box"; @@ -153,6 +124,31 @@ export class BoundingVolumeS2Validator implements Validator { let result = true; + // Validate the object as a RootProperty + if ( + !RootPropertyValidator.validateRootProperty( + path, + "3DTILES_bounding_volume_S2", + object, + context + ) + ) { + result = false; + } + + // Perform the validation of the object in view of the + // extensions that it may contain + if ( + !ExtendedObjectsValidators.validateExtendedObject(path, object, context) + ) { + result = false; + } + // If there was an extension validator that overrides the + // default validation, then skip the remaining validation. + if (ExtendedObjectsValidators.hasOverride(object)) { + return result; + } + // Validate the token const token = object.token; const tokenPath = path + "/token"; From e349b0c046fed412bbf7391a6b182a56134f4bcd Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 12 Oct 2022 15:11:15 +0200 Subject: [PATCH 10/22] Do not stop traversal due to invalid content --- src/validation/TilesetTraversingValidator.ts | 67 ++++++++++++++------ 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/src/validation/TilesetTraversingValidator.ts b/src/validation/TilesetTraversingValidator.ts index 0e0b7bfe..395288f0 100644 --- a/src/validation/TilesetTraversingValidator.ts +++ b/src/validation/TilesetTraversingValidator.ts @@ -62,6 +62,32 @@ export class TilesetTraversingValidator { if (!isValid) { result = false; } + if (isValid) { + // If the traversed tile is generally valid, then + // validate its content + const contentValid = + await TilesetTraversingValidator.validateTraversedTileContent( + traversedTile, + context + ); + if (!contentValid) { + result = false; + } + // If the traversed tile is not the root tile, validate + // the consistency of the hierarchy + const parent = traversedTile.getParent(); + if (defined(parent)) { + const hierarchyValid = + TilesetTraversingValidator.validateTraversedTiles( + parent!, + traversedTile, + context + ); + if (!hierarchyValid) { + result = false; + } + } + } return isValid; }, depthFirst @@ -101,8 +127,11 @@ export class TilesetTraversingValidator { * Validates the given traversed tile. * * This will validate the tile that is represented with the given - * traversed tile, its contents, and the consistency of the given - * traversed tile and its parent (if present). + * traversed tile, so far that it ensures that it is a valid + * tile object and can be traversed further. + * + * It will not validate the tile content. This is done with + * `validateTraversedTileContent` * * @param traversedTile The `TraversedTile` * @param validationState The `ValidationState` @@ -203,6 +232,24 @@ export class TilesetTraversingValidator { if (!tileValid) { return false; } + return true; + } + + /** + * Validates the content in given traversed tile. + * + * This assumes that the given tile already has been determined to + * be basically valid, as of `validateTraversedTile`. + * + * @param traversedTile The `TraversedTile` + * @param context The `ValidationContext` + * @returns A promise that resolves when the validation is finished + */ + private static async validateTraversedTileContent( + traversedTile: TraversedTile, + context: ValidationContext + ): Promise { + const tile = traversedTile.asTile(); let result = true; @@ -239,22 +286,6 @@ export class TilesetTraversingValidator { } } } - - // If the traversed tile is not the root tile, validate - // the consistency of the hierarchy - const parent = traversedTile.getParent(); - if (defined(parent)) { - if ( - !TilesetTraversingValidator.validateTraversedTiles( - parent!, - traversedTile, - context - ) - ) { - result = false; - } - } - return result; } From 99d8cd51d93d2f153709b07427454652aa08d96c Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 12 Oct 2022 17:11:28 +0200 Subject: [PATCH 11/22] Allow skipping validation for certain content types --- src/validation/ContentDataValidators.ts | 42 +++++++++++++++++-------- src/validation/TilesetValidator.ts | 10 +++++- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/validation/ContentDataValidators.ts b/src/validation/ContentDataValidators.ts index 17a67843..b1e4e502 100644 --- a/src/validation/ContentDataValidators.ts +++ b/src/validation/ContentDataValidators.ts @@ -42,18 +42,31 @@ export class ContentDataValidators { ContentDataValidators.registerByMagic("i3dm", new I3dmValidator()); ContentDataValidators.registerByMagic("cmpt", new CmptValidator()); ContentDataValidators.registerByMagic("pnts", new PntsValidator()); - ContentDataValidators.registerByMagic( - "geom", - Validators.createContentValidationWarning("Skipping 'geom' validation") - ); - ContentDataValidators.registerByMagic( - "vctr", - Validators.createContentValidationWarning("Skipping 'vctr' validation") - ); - ContentDataValidators.registerByExtension( - ".geojson", - Validators.createContentValidationWarning("Skipping 'GeoJSON' validation") - ); + + // Certain content types are known to be encountered, + // but are not (yet) validated. These can either be + // ignored, or cause a warning. In the future, this + // should be configurable, probably even on a per-type + // basis, via the command line or a config file + const ignoreUnhandledContentTypes = true; + let geomValidator = Validators.createEmptyValidator(); + let vctrValidator = Validators.createEmptyValidator(); + let geojsonValidator = Validators.createEmptyValidator(); + if (!ignoreUnhandledContentTypes) { + geomValidator = Validators.createContentValidationWarning( + "Skipping 'geom' validation" + ); + vctrValidator = Validators.createContentValidationWarning( + "Skipping 'vctr' validation" + ); + geojsonValidator = Validators.createContentValidationWarning( + "Skipping 'geojson' validation" + ); + } + + ContentDataValidators.registerByMagic("geom", geomValidator); + ContentDataValidators.registerByMagic("vctr", vctrValidator); + ContentDataValidators.registerByExtension(".geojson", geojsonValidator); ContentDataValidators.registerTileset(); ContentDataValidators.registerGltf(); } @@ -177,7 +190,10 @@ export class ContentDataValidators { * @param contentData The content data * @returns Whether the content data is probably glTF */ - private static isProbablyGltf(contentData: ContentData) { + static isProbablyGltf(contentData: ContentData) { + if (ContentDataValidators.isProbablyTileset(contentData)) { + return false; + } const parsedObject = contentData.parsedObject; if (!defined(parsedObject)) { return false; diff --git a/src/validation/TilesetValidator.ts b/src/validation/TilesetValidator.ts index afc9e51a..c751859c 100644 --- a/src/validation/TilesetValidator.ts +++ b/src/validation/TilesetValidator.ts @@ -392,9 +392,17 @@ export class TilesetValidator implements Validator { } // Each extension that is found during the validation - // in the `RootPropertyValidator` also has to appear + // in the `RootPropertyValidator` or one of the + // content data validators also has to appear // in the 'extensionsUsed' const actualExtensionsFound = context.getExtensionsFound(); + + // TODO: A cleaner solution has to be found for this. See + // https://github.com/CesiumGS/3d-tiles-validator/issues/231 + if (tileset.asset.version === "1.1") { + actualExtensionsFound.delete("3DTILES_content_gltf"); + } + for (const extensionName of actualExtensionsFound) { if (!actualExtensionsUsed.has(extensionName)) { const issue = SemanticValidationIssues.EXTENSION_FOUND_BUT_NOT_USED( From 24ca56374a386c1e69f4ea3fe3f3407f94bc964e Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 12 Oct 2022 17:11:51 +0200 Subject: [PATCH 12/22] Track use of 3DTILES_content_gltf extension --- src/tileFormats/GltfValidator.ts | 2 ++ src/validation/Validators.ts | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/tileFormats/GltfValidator.ts b/src/tileFormats/GltfValidator.ts index 68fe01a6..f4449c51 100644 --- a/src/tileFormats/GltfValidator.ts +++ b/src/tileFormats/GltfValidator.ts @@ -46,6 +46,8 @@ export class GltfValidator implements Validator { input: Buffer, context: ValidationContext ): Promise { + context.addExtensionFound("3DTILES_content_gltf"); + const resourceResolver = context.getResourceResolver(); let gltfResult = undefined; try { diff --git a/src/validation/Validators.ts b/src/validation/Validators.ts index e7b97bd1..2b8f8a60 100644 --- a/src/validation/Validators.ts +++ b/src/validation/Validators.ts @@ -203,4 +203,24 @@ export class Validators { }, }; } + + /** + * Creates an empty validator that does nothing. + * + * This is used for "dummy" validators for content types that + * are ignored. + * + * @returns The new validator + */ + static createEmptyValidator(): Validator { + return { + async validateObject( + inputPath: string, + input: T, + context: ValidationContext + ): Promise { + return true; + }, + }; + } } From 788043bd91f05c502f338eb510b11af1b681deeb Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 12 Oct 2022 17:36:32 +0200 Subject: [PATCH 13/22] Basic tests for 3DTILES_content_glTF extension handling --- .../tilesets/extensionNotDeclared_1_0_glTF.json | 15 +++++++++++++++ .../tilesets/extensionNotNecessary_1_1_glTF.json | 15 +++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 specs/data/tilesets/extensionNotDeclared_1_0_glTF.json create mode 100644 specs/data/tilesets/extensionNotNecessary_1_1_glTF.json diff --git a/specs/data/tilesets/extensionNotDeclared_1_0_glTF.json b/specs/data/tilesets/extensionNotDeclared_1_0_glTF.json new file mode 100644 index 00000000..8c535be9 --- /dev/null +++ b/specs/data/tilesets/extensionNotDeclared_1_0_glTF.json @@ -0,0 +1,15 @@ +{ + "asset" : { + "version" : "1.0" + }, + "geometricError" : 2.0, + "root" : { + "boundingVolume" : { + "box" : [ 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "content": { + "uri": "tiles/glTF/Triangle/Triangle.gltf" + }, + "geometricError" : 1.0 + } +} \ No newline at end of file diff --git a/specs/data/tilesets/extensionNotNecessary_1_1_glTF.json b/specs/data/tilesets/extensionNotNecessary_1_1_glTF.json new file mode 100644 index 00000000..af536874 --- /dev/null +++ b/specs/data/tilesets/extensionNotNecessary_1_1_glTF.json @@ -0,0 +1,15 @@ +{ + "asset" : { + "version" : "1.1" + }, + "geometricError" : 2.0, + "root" : { + "boundingVolume" : { + "box" : [ 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "content": { + "uri": "tiles/glTF/Triangle/Triangle.gltf" + }, + "geometricError" : 1.0 + } +} \ No newline at end of file From af5387a870281af7733fced5de038682d6be44b1 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 12 Oct 2022 19:07:05 +0200 Subject: [PATCH 14/22] Properly detect use of glTF as extension --- src/io/ResourceTypes.ts | 4 ++++ src/tileFormats/GltfValidator.ts | 2 -- src/validation/ContentDataValidator.ts | 25 +++++++++++++++++++++++++ src/validation/TilesetValidator.ts | 6 +++--- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/io/ResourceTypes.ts b/src/io/ResourceTypes.ts index 83f69b58..5c98f633 100644 --- a/src/io/ResourceTypes.ts +++ b/src/io/ResourceTypes.ts @@ -37,6 +37,10 @@ export class ResourceTypes { return ResourceTypes.startsWith(buffer, "subt"); } + static isGlb(buffer: Buffer): boolean { + return ResourceTypes.startsWith(buffer, "glTF"); + } + static isProbablyJson(buffer: Buffer): boolean { for (let i = 0; i < buffer.length; i++) { const c = String.fromCharCode(buffer[i]); diff --git a/src/tileFormats/GltfValidator.ts b/src/tileFormats/GltfValidator.ts index f4449c51..68fe01a6 100644 --- a/src/tileFormats/GltfValidator.ts +++ b/src/tileFormats/GltfValidator.ts @@ -46,8 +46,6 @@ export class GltfValidator implements Validator { input: Buffer, context: ValidationContext ): Promise { - context.addExtensionFound("3DTILES_content_gltf"); - const resourceResolver = context.getResourceResolver(); let gltfResult = undefined; try { diff --git a/src/validation/ContentDataValidator.ts b/src/validation/ContentDataValidator.ts index b65ddad1..c4eb480c 100644 --- a/src/validation/ContentDataValidator.ts +++ b/src/validation/ContentDataValidator.ts @@ -137,6 +137,8 @@ export class ContentDataValidator { return true; } + ContentDataValidator.trackExtensionsFound(contentData, context); + // Create a new context to collect the issues that are found in // the data. If there are issues, then they will be stored as // the 'causes' of a single content validation issue. @@ -154,4 +156,27 @@ export class ContentDataValidator { } return result; } + + /** + * Track the extensions that are used, and which only refer to + * allowing certain content data types. + * + * When a certain content data type that requires an extension + * is encountered, then the respective extension will be added + * as a "used" extension to the given context. + * + * @param contentData The `ContentData` + * @param context The `ValidationContext` + */ + private static trackExtensionsFound( + contentData: ContentData, + context: ValidationContext + ) { + if ( + ResourceTypes.isGlb(contentData.data) || + ContentDataValidators.isProbablyGltf(contentData) + ) { + context.addExtensionFound("3DTILES_content_gltf"); + } + } } diff --git a/src/validation/TilesetValidator.ts b/src/validation/TilesetValidator.ts index c751859c..9952d8e6 100644 --- a/src/validation/TilesetValidator.ts +++ b/src/validation/TilesetValidator.ts @@ -392,14 +392,14 @@ export class TilesetValidator implements Validator { } // Each extension that is found during the validation - // in the `RootPropertyValidator` or one of the - // content data validators also has to appear + // in the `RootPropertyValidator` or the + // `ContentDataValidator` also has to appear // in the 'extensionsUsed' const actualExtensionsFound = context.getExtensionsFound(); // TODO: A cleaner solution has to be found for this. See // https://github.com/CesiumGS/3d-tiles-validator/issues/231 - if (tileset.asset.version === "1.1") { + if (tileset.asset?.version === "1.1") { actualExtensionsFound.delete("3DTILES_content_gltf"); } From e94970c3cf7bff4e5ee668fc09a1d399efd8d7a9 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 12 Oct 2022 19:07:23 +0200 Subject: [PATCH 15/22] Properly track extensions that are used in B3DM --- src/tileFormats/B3dmValidator.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/tileFormats/B3dmValidator.ts b/src/tileFormats/B3dmValidator.ts index a74740a1..181f231b 100644 --- a/src/tileFormats/B3dmValidator.ts +++ b/src/tileFormats/B3dmValidator.ts @@ -140,6 +140,13 @@ export class B3dmValidator implements Validator { return false; } + if (defined(batchTableJson.extensions)) { + const extensionNames = Object.keys(batchTableJson.extensions); + for (const extensionFound of extensionNames) { + context.addExtensionFound(extensionFound); + } + } + const gltfValidator = new GltfValidator(); const result = await gltfValidator.validateObject(uri, glbData, context); return result; From 9abc590cbcdb225f743f96aded1567a7aba29963 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 12 Oct 2022 20:38:45 +0200 Subject: [PATCH 16/22] Print a short summary when validating a directory --- src/ValidatorMain.ts | 28 ++++++++++++++++++++++++---- src/validation/ValidationResult.ts | 12 ++++++++++-- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/ValidatorMain.ts b/src/ValidatorMain.ts index bafcee78..06926fe7 100644 --- a/src/ValidatorMain.ts +++ b/src/ValidatorMain.ts @@ -15,6 +15,7 @@ import { BoundingVolumeS2Validator } from "./validation/extensions/BoundingVolum import { TileImplicitTiling } from "./structure/TileImplicitTiling"; import { Schema } from "./structure/Metadata/Schema"; +import { ValidationResult } from "./validation/ValidationResult"; /** * A class summarizing the command-line functions of the validator. @@ -30,7 +31,7 @@ export class ValidatorMain { static async validateTilesetFile( fileName: string, reportFileName: string | undefined - ): Promise { + ): Promise { console.log("Validating tileset " + fileName); const validationResult = await Validators.validateTilesetFile(fileName); if (defined(reportFileName)) { @@ -39,6 +40,7 @@ export class ValidatorMain { console.log("Validation result:"); console.log(validationResult.serialize()); } + return validationResult; } static async validateTilesetsDirectory( @@ -53,19 +55,35 @@ export class ValidatorMain { const ignoreCase = true; const matcher = globMatcher(globPattern, ignoreCase); const tilesetFiles = filterIterable(allFiles, matcher); + let numFiles = 0; + let numFilesWithErrors = 0; + let numFilesWithWarnings = 0; for (const tilesetFile of tilesetFiles) { let reportFileName = undefined; if (writeReports) { reportFileName = ValidatorMain.deriveReportFileName(tilesetFile); } - await ValidatorMain.validateTilesetFile(tilesetFile, reportFileName); + const validationResult = await ValidatorMain.validateTilesetFile( + tilesetFile, + reportFileName + ); + numFiles++; + if (validationResult.numErrors > 0) { + numFilesWithErrors++; + } + if (validationResult.numWarnings > 0) { + numFilesWithWarnings++; + } } + console.log("Validated " + numFiles + " files"); + console.log(" " + numFilesWithErrors + " files with errors"); + console.log(" " + numFilesWithWarnings + " files with warnings"); } static async validateSchemaFile( fileName: string, reportFileName: string | undefined - ): Promise { + ): Promise { console.log("Validating schema " + fileName); const validationResult = await Validators.validateSchemaFile(fileName); if (defined(reportFileName)) { @@ -74,6 +92,7 @@ export class ValidatorMain { console.log("Validation result:"); console.log(validationResult.serialize()); } + return validationResult; } static async validateSubtreeFile( @@ -81,7 +100,7 @@ export class ValidatorMain { validationState: ValidationState, implicitTiling: TileImplicitTiling | undefined, reportFileName: string | undefined - ): Promise { + ): Promise { console.log("Validating subtree " + fileName); const validationResult = await Validators.validateSubtreeFile( fileName, @@ -94,6 +113,7 @@ export class ValidatorMain { console.log("Validation result:"); console.log(validationResult.serialize()); } + return validationResult; } static async validateAllTilesetSpecFiles( diff --git a/src/validation/ValidationResult.ts b/src/validation/ValidationResult.ts index 1f2c7ca7..936ade12 100644 --- a/src/validation/ValidationResult.ts +++ b/src/validation/ValidationResult.ts @@ -40,6 +40,14 @@ export class ValidationResult { return this._issues.length; } + get numErrors(): number { + return this.count(ValidationIssueSeverity.ERROR); + } + + get numWarnings(): number { + return this.count(ValidationIssueSeverity.WARNING); + } + get(index: number): ValidationIssue { return this._issues[index]; } @@ -56,8 +64,8 @@ export class ValidationResult { toJson(): any { const issuesJson = this._issues.length > 0 ? this._issues.map((i) => i.toJson()) : undefined; - const numErrors = this.count(ValidationIssueSeverity.ERROR); - const numWarnings = this.count(ValidationIssueSeverity.WARNING); + const numErrors = this.numErrors; + const numWarnings = this.numWarnings; return { date: this._date, numErrors: numErrors, From 15b34fe837eceec1ce3fda58472070c9d384a6f0 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Thu, 13 Oct 2022 14:56:09 +0200 Subject: [PATCH 17/22] Create warnings for unhandled content types --- src/validation/ContentDataValidators.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/validation/ContentDataValidators.ts b/src/validation/ContentDataValidators.ts index b1e4e502..72321f61 100644 --- a/src/validation/ContentDataValidators.ts +++ b/src/validation/ContentDataValidators.ts @@ -35,7 +35,7 @@ export class ContentDataValidators { */ static registerDefaults() { // The validators will be checked in the order in which they are - // registered. In the futhre, there might be a mechanism for + // registered. In the future, there might be a mechanism for // 'overriding' a previously registered validator. ContentDataValidators.registerByMagic("glTF", new GltfValidator()); ContentDataValidators.registerByMagic("b3dm", new B3dmValidator()); @@ -48,7 +48,7 @@ export class ContentDataValidators { // ignored, or cause a warning. In the future, this // should be configurable, probably even on a per-type // basis, via the command line or a config file - const ignoreUnhandledContentTypes = true; + const ignoreUnhandledContentTypes = false; let geomValidator = Validators.createEmptyValidator(); let vctrValidator = Validators.createEmptyValidator(); let geojsonValidator = Validators.createEmptyValidator(); From f2c47992e82e2475ef2738e0b423225e6e74138e Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Thu, 13 Oct 2022 14:57:46 +0200 Subject: [PATCH 18/22] Linting fixes --- src/validation/Validators.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/validation/Validators.ts b/src/validation/Validators.ts index 2b8f8a60..5442ae2d 100644 --- a/src/validation/Validators.ts +++ b/src/validation/Validators.ts @@ -191,6 +191,7 @@ export class Validators { return { async validateObject( inputPath: string, + //eslint-disable-next-line @typescript-eslint/no-unused-vars input: Buffer, context: ValidationContext ): Promise { @@ -215,8 +216,11 @@ export class Validators { static createEmptyValidator(): Validator { return { async validateObject( + //eslint-disable-next-line @typescript-eslint/no-unused-vars inputPath: string, + //eslint-disable-next-line @typescript-eslint/no-unused-vars input: T, + //eslint-disable-next-line @typescript-eslint/no-unused-vars context: ValidationContext ): Promise { return true; From 0ac1eedd3f464d236cd6374b56cf5bd137520866 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Thu, 13 Oct 2022 15:00:35 +0200 Subject: [PATCH 19/22] An invalid S2 token is an error --- src/validation/extensions/BoundingVolumeS2ValidationIssues.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/validation/extensions/BoundingVolumeS2ValidationIssues.ts b/src/validation/extensions/BoundingVolumeS2ValidationIssues.ts index e3bd0e72..1203c7be 100644 --- a/src/validation/extensions/BoundingVolumeS2ValidationIssues.ts +++ b/src/validation/extensions/BoundingVolumeS2ValidationIssues.ts @@ -4,7 +4,7 @@ import { ValidationIssueSeverity } from "../../validation/ValidationIssueSeverit export class BoundingVolumeS2ValidationIssues { static S2_TOKEN_INVALID(path: string, message: string) { const type = "S2_TOKEN_INVALID"; - const severity = ValidationIssueSeverity.WARNING; + const severity = ValidationIssueSeverity.ERROR; const issue = new ValidationIssue(type, path, message, severity); return issue; } From c95c2e1964a4b47d644c4148762f58623e0ad283 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Thu, 13 Oct 2022 15:05:24 +0200 Subject: [PATCH 20/22] Minor cleanup: Make readonly what can be readonly --- src/validation/ValidationContext.ts | 4 ++-- src/validation/ValidationIssue.ts | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/validation/ValidationContext.ts b/src/validation/ValidationContext.ts index 87d861d6..4008fdcd 100644 --- a/src/validation/ValidationContext.ts +++ b/src/validation/ValidationContext.ts @@ -31,7 +31,7 @@ export class ValidationContext { /** * The `ValidationResult` that receives the `ValidationIssue` instances */ - private _result: ValidationResult; + private readonly _result: ValidationResult; /** * The set of extensions that have been found during the validation @@ -43,7 +43,7 @@ export class ValidationContext { * as URI strings into Buffer objects, relative to the directory * in which the validation started. */ - private _resourceResolver: ResourceResolver; + private readonly _resourceResolver: ResourceResolver; constructor(resourceResolver: ResourceResolver) { this._options = new ValidationOptions(); diff --git a/src/validation/ValidationIssue.ts b/src/validation/ValidationIssue.ts index 70e2771e..5f03a3d7 100644 --- a/src/validation/ValidationIssue.ts +++ b/src/validation/ValidationIssue.ts @@ -10,23 +10,23 @@ export class ValidationIssue { * type of the issue, in `UPPER_SNAKE_CASE`, describing what * caused the issue. */ - private _type: string; + private readonly _type: string; /** * The JSON path leading to the element that caused the issue. */ - private _path: string; + private readonly _path: string; /** * The human-readable message that describes the issue, preferably * with information that indicates how to resolve the issue. */ - private _message: string; + private readonly _message: string; /** * A severity level for the issue (e.g. WARNING or ERROR) */ - private _severity: ValidationIssueSeverity; + private readonly _severity: ValidationIssueSeverity; /** * Validation issues that are individual issues, which, as a whole, @@ -36,7 +36,7 @@ export class ValidationIssue { * validation of tile content, and which are combined into a * general `CONTENT_VALIDATION_ERROR`. */ - private _causes: ValidationIssue[]; + private readonly _causes: ValidationIssue[]; constructor( type: string, From ff10d9e81658f4660e4164b86afe6ecb60301fd3 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Fri, 14 Oct 2022 18:57:56 +0200 Subject: [PATCH 21/22] Apply extension validation to all RootProperty objects --- src/validation/AssetValidator.ts | 24 ++++++++++----- src/validation/ContentValidator.ts | 20 ++++++++++++- src/validation/ImplicitTilingValidator.ts | 18 ++++++++++++ src/validation/MetadataEntityValidator.ts | 18 ++++++++++++ src/validation/PropertiesValidator.ts | 29 ++++++++++++++----- src/validation/PropertyTableValidator.ts | 18 ++++++++++++ src/validation/SchemaClassValidator.ts | 18 ++++++++++++ src/validation/SchemaEnumValidator.ts | 18 ++++++++++++ src/validation/SchemaValidator.ts | 14 +++++++++ src/validation/StatisticsClassValidator.ts | 18 ++++++++++++ src/validation/StatisticsValidator.ts | 18 ++++++++++++ src/validation/SubtreeValidator.ts | 22 +++++++++++--- src/validation/TileValidator.ts | 16 +++++++++- src/validation/TilesetValidator.ts | 16 +++++++++- .../extensions/BoundingVolumeS2Validator.ts | 4 +-- 15 files changed, 248 insertions(+), 23 deletions(-) diff --git a/src/validation/AssetValidator.ts b/src/validation/AssetValidator.ts index 5db0394c..a59f3196 100644 --- a/src/validation/AssetValidator.ts +++ b/src/validation/AssetValidator.ts @@ -3,6 +3,7 @@ import { defined } from "../base/defined"; import { ValidationContext } from "./ValidationContext"; import { BasicValidator } from "./BasicValidator"; import { RootPropertyValidator } from "./RootPropertyValidator"; +import { ExtendedObjectsValidators } from "./ExtendedObjectsValidators"; import { Asset } from "../structure/Asset"; @@ -29,8 +30,9 @@ export class AssetValidator { * @returns Whether the object was valid */ static validateAsset(asset: Asset, context: ValidationContext): boolean { + const path = "/asset"; // Make sure that the given value is an object - if (!BasicValidator.validateObject("/asset", "asset", asset, context)) { + if (!BasicValidator.validateObject(path, "asset", asset, context)) { return false; } @@ -38,16 +40,24 @@ export class AssetValidator { // Validate the object as a RootProperty if ( - !RootPropertyValidator.validateRootProperty( - "/asset", - "asset", - asset, - context - ) + !RootPropertyValidator.validateRootProperty(path, "asset", asset, context) ) { result = false; } + // Perform the validation of the object in view of the + // extensions that it may contain + if ( + !ExtendedObjectsValidators.validateExtendedObject(path, asset, context) + ) { + result = false; + } + // If there was an extension validator that overrides the + // default validation, then skip the remaining validation. + if (ExtendedObjectsValidators.hasOverride(asset)) { + return result; + } + // Validate the version const version = asset.version; const versionPath = "/asset/version"; diff --git a/src/validation/ContentValidator.ts b/src/validation/ContentValidator.ts index 61d88572..c5d4a960 100644 --- a/src/validation/ContentValidator.ts +++ b/src/validation/ContentValidator.ts @@ -1,15 +1,16 @@ import { defined } from "../base/defined"; import { ValidationContext } from "./ValidationContext"; +import { ValidationState } from "./ValidationState"; import { BoundingVolumeValidator } from "./BoundingVolumeValidator"; import { BasicValidator } from "./BasicValidator"; import { RootPropertyValidator } from "./RootPropertyValidator"; import { MetadataEntityValidator } from "./MetadataEntityValidator"; +import { ExtendedObjectsValidators } from "./ExtendedObjectsValidators"; import { Content } from "../structure/Content"; import { StructureValidationIssues } from "../issues/StructureValidationIssues"; -import { ValidationState } from "./ValidationState"; /** * A class for validations related to `content` objects. @@ -59,6 +60,23 @@ export class ContentValidator { result = false; } + // Perform the validation of the object in view of the + // extensions that it may contain + if ( + !ExtendedObjectsValidators.validateExtendedObject( + contentPath, + content, + context + ) + ) { + result = false; + } + // If there was an extension validator that overrides the + // default validation, then skip the remaining validation. + if (ExtendedObjectsValidators.hasOverride(content)) { + return result; + } + // Validate the group const group = content.group; const groupPath = contentPath + "/group"; diff --git a/src/validation/ImplicitTilingValidator.ts b/src/validation/ImplicitTilingValidator.ts index 9969a94f..670419ac 100644 --- a/src/validation/ImplicitTilingValidator.ts +++ b/src/validation/ImplicitTilingValidator.ts @@ -2,6 +2,7 @@ import { ValidationContext } from "./ValidationContext"; import { BasicValidator } from "./BasicValidator"; import { TemplateUriValidator } from "./TemplateUriValidator"; import { RootPropertyValidator } from "./RootPropertyValidator"; +import { ExtendedObjectsValidators } from "./ExtendedObjectsValidators"; import { TileImplicitTiling } from "../structure/TileImplicitTiling"; @@ -56,6 +57,23 @@ export class ImplicitTilingValidator { result = false; } + // Perform the validation of the object in view of the + // extensions that it may contain + if ( + !ExtendedObjectsValidators.validateExtendedObject( + path, + implicitTiling, + context + ) + ) { + result = false; + } + // If there was an extension validator that overrides the + // default validation, then skip the remaining validation. + if (ExtendedObjectsValidators.hasOverride(implicitTiling)) { + return result; + } + // Validate the subdivisionScheme // The subdivisionSchemes MUST be defined // The subdivisionSchemes MUST be one of the valid values diff --git a/src/validation/MetadataEntityValidator.ts b/src/validation/MetadataEntityValidator.ts index 26b6b66d..7494ed30 100644 --- a/src/validation/MetadataEntityValidator.ts +++ b/src/validation/MetadataEntityValidator.ts @@ -6,6 +6,7 @@ import { BasicValidator } from "./BasicValidator"; import { MetadataStructureValidator } from "./MetadataStructureValidator"; import { MetadataValueValidator } from "./MetadataValueValidator"; import { RootPropertyValidator } from "./RootPropertyValidator"; +import { ExtendedObjectsValidators } from "./ExtendedObjectsValidators"; import { Schema } from "../structure/Metadata/Schema"; import { MetadataEntity } from "../structure/MetadataEntity"; @@ -54,6 +55,23 @@ export class MetadataEntityValidator { result = false; } + // Perform the validation of the object in view of the + // extensions that it may contain + if ( + !ExtendedObjectsValidators.validateExtendedObject( + path, + metadataEntity, + context + ) + ) { + result = false; + } + // If there was an extension validator that overrides the + // default validation, then skip the remaining validation. + if (ExtendedObjectsValidators.hasOverride(metadataEntity)) { + return result; + } + // Validate that the class and properties are structurally // valid and comply to the metadata schema const className = metadataEntity.class; diff --git a/src/validation/PropertiesValidator.ts b/src/validation/PropertiesValidator.ts index 03d1fde5..fce72d81 100644 --- a/src/validation/PropertiesValidator.ts +++ b/src/validation/PropertiesValidator.ts @@ -1,6 +1,7 @@ import { ValidationContext } from "./ValidationContext"; import { BasicValidator } from "./BasicValidator"; import { RootPropertyValidator } from "./RootPropertyValidator"; +import { ExtendedObjectsValidators } from "./ExtendedObjectsValidators"; import { Properties } from "../structure/Properties"; @@ -25,14 +26,11 @@ export class PropertiesValidator { properties: Properties, context: ValidationContext ): boolean { + const path = "/properties"; + // Make sure that the given value is an object if ( - !BasicValidator.validateObject( - "/properties", - "properties", - properties, - context - ) + !BasicValidator.validateObject(path, "properties", properties, context) ) { return false; } @@ -42,7 +40,7 @@ export class PropertiesValidator { // Validate the object as a RootProperty if ( !RootPropertyValidator.validateRootProperty( - "/properties", + path, "properties", properties, context @@ -51,6 +49,23 @@ export class PropertiesValidator { result = false; } + // Perform the validation of the object in view of the + // extensions that it may contain + if ( + !ExtendedObjectsValidators.validateExtendedObject( + path, + properties, + context + ) + ) { + result = false; + } + // If there was an extension validator that overrides the + // default validation, then skip the remaining validation. + if (ExtendedObjectsValidators.hasOverride(properties)) { + return result; + } + // Validate all entries of the properties dictionary for (const [key, value] of Object.entries(properties)) { // TODO Technically, the key should be validated to be in the batch table... diff --git a/src/validation/PropertyTableValidator.ts b/src/validation/PropertyTableValidator.ts index cb9530bf..74bfe619 100644 --- a/src/validation/PropertyTableValidator.ts +++ b/src/validation/PropertyTableValidator.ts @@ -4,6 +4,7 @@ import { defaultValue } from "../base/defaultValue"; import { ValidationContext } from "./ValidationContext"; import { BasicValidator } from "./BasicValidator"; import { RootPropertyValidator } from "./RootPropertyValidator"; +import { ExtendedObjectsValidators } from "./ExtendedObjectsValidators"; import { MetadataStructureValidator } from "./MetadataStructureValidator"; import { Schema } from "../structure/Metadata/Schema"; @@ -62,6 +63,23 @@ export class PropertyTableValidator { result = false; } + // Perform the validation of the object in view of the + // extensions that it may contain + if ( + !ExtendedObjectsValidators.validateExtendedObject( + path, + propertyTable, + context + ) + ) { + result = false; + } + // If there was an extension validator that overrides the + // default validation, then skip the remaining validation. + if (ExtendedObjectsValidators.hasOverride(propertyTable)) { + return result; + } + // Validate that the class and properties are structurally // valid and comply to the metadata schema const className = propertyTable.class; diff --git a/src/validation/SchemaClassValidator.ts b/src/validation/SchemaClassValidator.ts index 3de80cd9..0a756b07 100644 --- a/src/validation/SchemaClassValidator.ts +++ b/src/validation/SchemaClassValidator.ts @@ -5,6 +5,7 @@ import { BasicValidator } from "./BasicValidator"; import { RootPropertyValidator } from "./RootPropertyValidator"; import { ClassPropertyValidator } from "./ClassPropertyValidator"; import { ClassPropertySemanticsValidator } from "./ClassPropertySemanticsValidator"; +import { ExtendedObjectsValidators } from "./ExtendedObjectsValidators"; import { Schema } from "../structure/Metadata/Schema"; import { SchemaClass } from "../structure/Metadata/SchemaClass"; @@ -58,6 +59,23 @@ export class SchemaClassValidator { result = false; } + // Perform the validation of the object in view of the + // extensions that it may contain + if ( + !ExtendedObjectsValidators.validateExtendedObject( + schemaClassPath, + schemaClass, + context + ) + ) { + result = false; + } + // If there was an extension validator that overrides the + // default validation, then skip the remaining validation. + if (ExtendedObjectsValidators.hasOverride(schemaClass)) { + return result; + } + // Validate the name. // If the name is defined, it MUST be a string. if ( diff --git a/src/validation/SchemaEnumValidator.ts b/src/validation/SchemaEnumValidator.ts index cff4a1d8..293f3daa 100644 --- a/src/validation/SchemaEnumValidator.ts +++ b/src/validation/SchemaEnumValidator.ts @@ -3,6 +3,7 @@ import { defined } from "../base/defined"; import { ValidationContext } from "./ValidationContext"; import { BasicValidator } from "./BasicValidator"; import { RootPropertyValidator } from "./RootPropertyValidator"; +import { ExtendedObjectsValidators } from "./ExtendedObjectsValidators"; import { MetadataComponentTypes } from "../metadata/MetadataComponentTypes"; @@ -59,6 +60,23 @@ export class SchemaEnumValidator { result = false; } + // Perform the validation of the object in view of the + // extensions that it may contain + if ( + !ExtendedObjectsValidators.validateExtendedObject( + schemaEnumPath, + schemaEnum, + context + ) + ) { + result = false; + } + // If there was an extension validator that overrides the + // default validation, then skip the remaining validation. + if (ExtendedObjectsValidators.hasOverride(schemaEnum)) { + return result; + } + // Validate the name. // If the name is defined, it MUST be a string. if ( diff --git a/src/validation/SchemaValidator.ts b/src/validation/SchemaValidator.ts index 35ebaf8d..cdb41f7c 100644 --- a/src/validation/SchemaValidator.ts +++ b/src/validation/SchemaValidator.ts @@ -6,6 +6,7 @@ import { BasicValidator } from "./BasicValidator"; import { SchemaClassValidator } from "./SchemaClassValidator"; import { SchemaEnumValidator } from "./SchemaEnumValidator"; import { RootPropertyValidator } from "./RootPropertyValidator"; +import { ExtendedObjectsValidators } from "./ExtendedObjectsValidators"; import { Schema } from "../structure/Metadata/Schema"; @@ -96,6 +97,19 @@ export class SchemaValidator implements Validator { result = false; } + // Perform the validation of the object in view of the + // extensions that it may contain + if ( + !ExtendedObjectsValidators.validateExtendedObject(path, schema, context) + ) { + result = false; + } + // If there was an extension validator that overrides the + // default validation, then skip the remaining validation. + if (ExtendedObjectsValidators.hasOverride(schema)) { + return result; + } + // Validate the id const id = schema.id; const idPath = path + "/id"; diff --git a/src/validation/StatisticsClassValidator.ts b/src/validation/StatisticsClassValidator.ts index 89c9a315..89818fa1 100644 --- a/src/validation/StatisticsClassValidator.ts +++ b/src/validation/StatisticsClassValidator.ts @@ -3,6 +3,7 @@ import { defined } from "../base/defined"; import { ValidationContext } from "./ValidationContext"; import { BasicValidator } from "./BasicValidator"; import { RootPropertyValidator } from "./RootPropertyValidator"; +import { ExtendedObjectsValidators } from "./ExtendedObjectsValidators"; import { StatisticsClass } from "../structure/StatisticsClass"; import { Schema } from "../structure/Metadata/Schema"; @@ -62,6 +63,23 @@ export class StatisticsClassValidator { result = false; } + // Perform the validation of the object in view of the + // extensions that it may contain + if ( + !ExtendedObjectsValidators.validateExtendedObject( + classPath, + schema, + context + ) + ) { + result = false; + } + // If there was an extension validator that overrides the + // default validation, then skip the remaining validation. + if (ExtendedObjectsValidators.hasOverride(schema)) { + return result; + } + // Each class that appears in the statistics MUST be // one of the classes defined in the schema const schemaClasses: any = defined(schema.classes) ? schema.classes : {}; diff --git a/src/validation/StatisticsValidator.ts b/src/validation/StatisticsValidator.ts index d875f048..483e2c53 100644 --- a/src/validation/StatisticsValidator.ts +++ b/src/validation/StatisticsValidator.ts @@ -4,6 +4,7 @@ import { ValidationContext } from "./ValidationContext"; import { BasicValidator } from "./BasicValidator"; import { ValidationState } from "./ValidationState"; import { RootPropertyValidator } from "./RootPropertyValidator"; +import { ExtendedObjectsValidators } from "./ExtendedObjectsValidators"; import { Statistics } from "../structure/Statistics"; @@ -53,6 +54,23 @@ export class StatisticsValidator { result = false; } + // Perform the validation of the object in view of the + // extensions that it may contain + if ( + !ExtendedObjectsValidators.validateExtendedObject( + path, + statistics, + context + ) + ) { + result = false; + } + // If there was an extension validator that overrides the + // default validation, then skip the remaining validation. + if (ExtendedObjectsValidators.hasOverride(statistics)) { + return result; + } + // Validate the classes const classes = statistics.classes; const classesPath = path + "/classes"; diff --git a/src/validation/SubtreeValidator.ts b/src/validation/SubtreeValidator.ts index c2eb5cfd..697450c7 100644 --- a/src/validation/SubtreeValidator.ts +++ b/src/validation/SubtreeValidator.ts @@ -13,6 +13,8 @@ import { MetadataEntityValidator } from "./MetadataEntityValidator"; import { SubtreeConsistencyValidator } from "./SubtreeConsistencyValidator"; import { PropertyTableValidator } from "./PropertyTableValidator"; import { SubtreeInfoValidator } from "./SubtreeInfoValidator"; +import { RootPropertyValidator } from "./RootPropertyValidator"; +import { ExtendedObjectsValidators } from "./ExtendedObjectsValidators"; import { BufferObject } from "../structure/BufferObject"; import { Subtree } from "../structure/Subtree"; @@ -23,7 +25,6 @@ import { TileImplicitTiling } from "../structure/TileImplicitTiling"; import { JsonValidationIssues } from "../issues/JsonValidationIssues"; import { IoValidationIssues } from "../issues/IoValidationIssue"; import { StructureValidationIssues } from "../issues/StructureValidationIssues"; -import { RootPropertyValidator } from "./RootPropertyValidator"; /** * A class for validations related to `subtree` objects that have @@ -42,19 +43,19 @@ export class SubtreeValidator implements Validator { * The `ValidationState` that carries information about * the metadata schema */ - private _validationState: ValidationState; + private readonly _validationState: ValidationState; /** * The `TileImplicitTiling` object that carries information * about the expected structure of the subtree */ - private _implicitTiling: TileImplicitTiling | undefined; + private readonly _implicitTiling: TileImplicitTiling | undefined; /** * The `ResourceResolver` that will be used to resolve * buffer URIs */ - private _resourceResolver: ResourceResolver; + private readonly _resourceResolver: ResourceResolver; /** * Creates a new instance. @@ -296,6 +297,19 @@ export class SubtreeValidator implements Validator { result = false; } + // Perform the validation of the object in view of the + // extensions that it may contain + if ( + !ExtendedObjectsValidators.validateExtendedObject(path, subtree, context) + ) { + result = false; + } + // If there was an extension validator that overrides the + // default validation, then skip the remaining validation. + if (ExtendedObjectsValidators.hasOverride(subtree)) { + return result; + } + // Validate the structure of the given subtree object, // on the level of JSON validity const structureIsValid = this.validateSubtreeObject( diff --git a/src/validation/TileValidator.ts b/src/validation/TileValidator.ts index a883789e..becd6453 100644 --- a/src/validation/TileValidator.ts +++ b/src/validation/TileValidator.ts @@ -9,6 +9,8 @@ import { ImplicitTilingValidator } from "./ImplicitTilingValidator"; import { TransformValidator } from "./TransformValidator"; import { ValidationState } from "./ValidationState"; import { TemplateUriValidator } from "./TemplateUriValidator"; +import { RootPropertyValidator } from "./RootPropertyValidator"; +import { ExtendedObjectsValidators } from "./ExtendedObjectsValidators"; import { Tile } from "../structure/Tile"; import { TileImplicitTiling } from "../structure/TileImplicitTiling"; @@ -16,7 +18,6 @@ import { TileImplicitTiling } from "../structure/TileImplicitTiling"; import { JsonValidationIssues } from "../issues/JsonValidationIssues"; import { SemanticValidationIssues } from "../issues/SemanticValidationIssues"; import { StructureValidationIssues } from "../issues/StructureValidationIssues"; -import { RootPropertyValidator } from "./RootPropertyValidator"; /** * The valid values for the `refine` property @@ -74,6 +75,19 @@ export class TileValidator { result = false; } + // Perform the validation of the object in view of the + // extensions that it may contain + if ( + !ExtendedObjectsValidators.validateExtendedObject(tilePath, tile, context) + ) { + result = false; + } + // If there was an extension validator that overrides the + // default validation, then skip the remaining validation. + if (ExtendedObjectsValidators.hasOverride(tile)) { + return result; + } + // Validate the boundingVolume const boundingVolume = tile.boundingVolume; const boundingVolumePath = tilePath + "/boundingVolume"; diff --git a/src/validation/TilesetValidator.ts b/src/validation/TilesetValidator.ts index 9952d8e6..ff6bed84 100644 --- a/src/validation/TilesetValidator.ts +++ b/src/validation/TilesetValidator.ts @@ -10,6 +10,8 @@ import { MetadataEntityValidator } from "./MetadataEntityValidator"; import { AssetValidator } from "./AssetValidator"; import { SchemaValidator } from "./SchemaValidator"; import { TilesetTraversingValidator } from "./TilesetTraversingValidator"; +import { RootPropertyValidator } from "./RootPropertyValidator"; +import { ExtendedObjectsValidators } from "./ExtendedObjectsValidators"; import { Tileset } from "../structure/Tileset"; import { Schema } from "../structure/Metadata/Schema"; @@ -18,7 +20,6 @@ import { Group } from "../structure/Group"; import { IoValidationIssues } from "../issues/IoValidationIssue"; import { StructureValidationIssues } from "../issues/StructureValidationIssues"; import { JsonValidationIssues } from "../issues/JsonValidationIssues"; -import { RootPropertyValidator } from "./RootPropertyValidator"; import { SemanticValidationIssues } from "../issues/SemanticValidationIssues"; /** @@ -103,6 +104,19 @@ export class TilesetValidator implements Validator { result = false; } + // Perform the validation of the object in view of the + // extensions that it may contain + if ( + !ExtendedObjectsValidators.validateExtendedObject(path, tileset, context) + ) { + result = false; + } + // If there was an extension validator that overrides the + // default validation, then skip the remaining validation. + if (ExtendedObjectsValidators.hasOverride(tileset)) { + return result; + } + // The asset MUST be defined const asset = tileset.asset; if (!AssetValidator.validateAsset(asset, context)) { diff --git a/src/validation/extensions/BoundingVolumeS2Validator.ts b/src/validation/extensions/BoundingVolumeS2Validator.ts index 51bf8d5b..2cf6bb99 100644 --- a/src/validation/extensions/BoundingVolumeS2Validator.ts +++ b/src/validation/extensions/BoundingVolumeS2Validator.ts @@ -3,12 +3,12 @@ import { defined } from "../../base/defined"; import { Validator } from "../Validator"; import { ValidationContext } from "../ValidationContext"; import { BasicValidator } from "../BasicValidator"; +import { BoundingVolumeValidator } from "../BoundingVolumeValidator"; import { RootPropertyValidator } from "../RootPropertyValidator"; +import { ExtendedObjectsValidators } from "../ExtendedObjectsValidators"; import { SemanticValidationIssues } from "../../issues/SemanticValidationIssues"; import { BoundingVolumeS2ValidationIssues } from "./BoundingVolumeS2ValidationIssues"; -import { BoundingVolumeValidator } from "../BoundingVolumeValidator"; -import { ExtendedObjectsValidators } from "../ExtendedObjectsValidators"; /** * A class for the validation of bounding volumes that contain From 29f2dda3e8cedbc762789ded0fabb5e4d35e1f58 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Fri, 14 Oct 2022 19:09:23 +0200 Subject: [PATCH 22/22] Completed incomplete inlined comment --- src/validation/extensions/BoundingVolumeS2Validator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/validation/extensions/BoundingVolumeS2Validator.ts b/src/validation/extensions/BoundingVolumeS2Validator.ts index 2cf6bb99..359578b9 100644 --- a/src/validation/extensions/BoundingVolumeS2Validator.ts +++ b/src/validation/extensions/BoundingVolumeS2Validator.ts @@ -87,7 +87,7 @@ export class BoundingVolumeS2Validator implements Validator { } // If there is a 3DTILES_bounding_volume_S2 extension, - // perform the corresponding object + // perform the validation of the corresponding object const extensions = boundingVolume.extensions; if (defined(extensions)) { const key = "3DTILES_bounding_volume_S2";