diff --git a/specs/TilesetValidationSpec.ts b/specs/TilesetValidationSpec.ts index 8c952221..7197bfd4 100644 --- a/specs/TilesetValidationSpec.ts +++ b/specs/TilesetValidationSpec.ts @@ -1,4 +1,7 @@ import { Validators } from "../src/validation/Validators"; +import { ContentDataValidators } from "../src/validation/ContentDataValidators"; + +ContentDataValidators.registerDefaults(); describe("Tileset validation", function () { it("detects issues in assetTilesetVersionInvalidType", async function () { @@ -729,8 +732,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 +814,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/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/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/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/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/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 diff --git a/src/ValidatorMain.ts b/src/ValidatorMain.ts index febf281e..06926fe7 100644 --- a/src/ValidatorMain.ts +++ b/src/ValidatorMain.ts @@ -9,9 +9,13 @@ 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 { ValidationResult } from "./validation/ValidationResult"; /** * A class summarizing the command-line functions of the validator. @@ -27,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)) { @@ -36,6 +40,7 @@ export class ValidatorMain { console.log("Validation result:"); console.log(validationResult.serialize()); } + return validationResult; } static async validateTilesetsDirectory( @@ -50,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)) { @@ -71,6 +92,7 @@ export class ValidatorMain { console.log("Validation result:"); console.log(validationResult.serialize()); } + return validationResult; } static async validateSubtreeFile( @@ -78,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, @@ -91,6 +113,7 @@ export class ValidatorMain { console.log("Validation result:"); console.log(validationResult.serialize()); } + return validationResult; } static async validateAllTilesetSpecFiles( @@ -191,6 +214,22 @@ export class ValidatorMain { ); } + /** + * Register the validators for known extensions + */ + static registerExtensionValidators() { + // Register the validator for 3DTILES_bounding_volume_S2 + { + const s2Validator = new BoundingVolumeS2Validator(); + const override = true; + ExtendedObjectsValidators.register( + "3DTILES_bounding_volume_S2", + s2Validator, + override + ); + } + } + /** * 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/io/ResourceTypes.ts b/src/io/ResourceTypes.ts index 4ecd53fc..5c98f633 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,22 @@ export class ResourceTypes { return buffer[0] === 0x1f && buffer[1] === 0x8b; } + /** + * 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 The magic header + */ + static getMagic(buffer: Buffer): string { + const length = Math.min(buffer.length, 4); + const magic = buffer.toString("utf8", 0, length); + return magic; + } + static startsWith(buffer: Buffer, magic: string) { if (buffer.length < magic.length) { return false; @@ -18,36 +33,12 @@ 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 isGlb(buffer: Buffer): boolean { + return ResourceTypes.startsWith(buffer, "glTF"); } static isProbablyJson(buffer: Buffer): boolean { diff --git a/src/issues/SemanticValidationIssues.ts b/src/issues/SemanticValidationIssues.ts index af8ee350..b5cbb6ea 100644 --- a/src/issues/SemanticValidationIssues.ts +++ b/src/issues/SemanticValidationIssues.ts @@ -337,7 +337,14 @@ 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..53e972e0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,12 @@ //eslint-disable-next-line const yargs = require("yargs/yargs"); +import { ContentDataValidators } from "./validation/ContentDataValidators"; import { ValidatorMain } from "./ValidatorMain"; +ValidatorMain.registerExtensionValidators(); +ContentDataValidators.registerDefaults(); + const args = yargs(process.argv.slice(1)) .help("help") .alias("help", "h") diff --git a/src/tileFormats/B3dmValidator.ts b/src/tileFormats/B3dmValidator.ts index 7377e75b..181f231b 100644 --- a/src/tileFormats/B3dmValidator.ts +++ b/src/tileFormats/B3dmValidator.ts @@ -34,31 +34,8 @@ const featureTableSemantics = { * given as a Buffer. */ export class B3dmValidator implements Validator { - private _uri: string; - - constructor(uri: string) { - this._uri = uri; - } - async validateObject( - 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(input, derivedContext); - const derivedResult = derivedContext.getResult(); - const issue = ContentValidationIssues.createFrom(this._uri, derivedResult); - if (issue) { - context.addIssue(issue); - } - return result; - } - - async validateObjectInternal( + uri: string, input: Buffer, context: ValidationContext ): Promise { @@ -66,7 +43,7 @@ export class B3dmValidator implements Validator { if ( !TileFormatValidator.validateHeader( - this._uri, + uri, input, headerByteLength, "b3dm", @@ -94,7 +71,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 +81,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 +106,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 +119,7 @@ export class B3dmValidator implements Validator { ); if (defined(featureTableMessage)) { const issue = ContentValidationIssues.CONTENT_JSON_INVALID( - this._uri, + uri, featureTableMessage! ); context.addIssue(issue); @@ -156,15 +133,22 @@ 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); + 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; } } diff --git a/src/tileFormats/CmptValidator.ts b/src/tileFormats/CmptValidator.ts index 1b8338d3..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"; /** @@ -18,31 +17,8 @@ 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( - 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(input, derivedContext); - const derivedResult = derivedContext.getResult(); - const issue = ContentValidationIssues.createFrom(this._uri, derivedResult); - if (issue) { - context.addIssue(issue); - } - return result; - } - - async validateObjectInternal( + uri: string, input: Buffer, context: ValidationContext ): Promise { @@ -50,7 +26,7 @@ export class CmptValidator implements Validator { if ( !TileFormatValidator.validateHeader( - this._uri, + uri, input, headerByteLength, "cmpt", @@ -68,13 +44,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 +63,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 +73,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 +83,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 +93,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 +104,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..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"; @@ -16,14 +15,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 * that appears in the output of the glTF validator. @@ -51,18 +42,17 @@ 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; 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; @@ -72,10 +62,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..b5d439d3 100644 --- a/src/tileFormats/I3dmValidator.ts +++ b/src/tileFormats/I3dmValidator.ts @@ -96,31 +96,8 @@ const featureTableSemantics = { * given as a Buffer. */ export class I3dmValidator implements Validator { - private _uri: string; - - constructor(uri: string) { - this._uri = uri; - } - async validateObject( - 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(input, derivedContext); - const derivedResult = derivedContext.getResult(); - const issue = ContentValidationIssues.createFrom(this._uri, derivedResult); - if (issue) { - context.addIssue(issue); - } - return result; - } - - async validateObjectInternal( + uri: string, input: Buffer, context: ValidationContext ): Promise { @@ -128,7 +105,7 @@ export class I3dmValidator implements Validator { if ( !TileFormatValidator.validateHeader( - this._uri, + uri, input, headerByteLength, "i3dm", @@ -142,7 +119,7 @@ export class I3dmValidator implements Validator { if (gltfFormat > 1) { const issue = BinaryValidationIssues.BINARY_INVALID_VALUE( - this._uri, + uri, "gltfFormat", "<=1", gltfFormat @@ -153,7 +130,7 @@ export class I3dmValidator implements Validator { const hasEmbeddedGlb = gltfFormat === 1; const binaryTableData = TileFormatValidator.extractBinaryTableData( - this._uri, + uri, input, headerByteLength, hasEmbeddedGlb, @@ -174,7 +151,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 +161,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 +172,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 +183,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 +194,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 +205,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 +217,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 +230,7 @@ export class I3dmValidator implements Validator { ); if (defined(featureTableMessage)) { const issue = ContentValidationIssues.CONTENT_JSON_INVALID( - this._uri, + uri, featureTableMessage! ); context.addIssue(issue); @@ -267,7 +244,7 @@ export class I3dmValidator implements Validator { ); if (defined(batchTableMessage)) { const issue = ContentValidationIssues.CONTENT_JSON_INVALID( - this._uri, + uri, batchTableMessage! ); context.addIssue(issue); @@ -276,8 +253,12 @@ 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 +271,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 +283,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 ); @@ -311,10 +293,7 @@ export class I3dmValidator implements Validator { result = false; } const derivedResult = derivedContext.getResult(); - const issue = ContentValidationIssues.createFrom( - this._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 98dd6ffb..6b46b17c 100644 --- a/src/tileFormats/PntsValidator.ts +++ b/src/tileFormats/PntsValidator.ts @@ -94,31 +94,8 @@ const featureTableSemantics = { * given as a Buffer. */ export class PntsValidator implements Validator { - private _uri: string; - - constructor(uri: string) { - this._uri = uri; - } - async validateObject( - 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(input, derivedContext); - const derivedResult = derivedContext.getResult(); - const issue = ContentValidationIssues.createFrom(this._uri, derivedResult); - if (issue) { - context.addIssue(issue); - } - return result; - } - - async validateObjectInternal( + uri: string, input: Buffer, context: ValidationContext ): Promise { @@ -126,7 +103,7 @@ export class PntsValidator implements Validator { if ( !TileFormatValidator.validateHeader( - this._uri, + uri, input, headerByteLength, "pnts", @@ -137,7 +114,7 @@ export class PntsValidator implements Validator { } const binaryTableData = TileFormatValidator.extractBinaryTableData( - this._uri, + uri, input, headerByteLength, false, @@ -158,7 +135,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 +146,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 +158,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 +169,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 +180,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 +188,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 +206,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 +221,7 @@ export class PntsValidator implements Validator { ); if (defined(featureTableMessage)) { const issue = ContentValidationIssues.CONTENT_JSON_INVALID( - this._uri, + uri, featureTableMessage! ); context.addIssue(issue); @@ -258,7 +235,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/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/BoundingVolumeValidator.ts b/src/validation/BoundingVolumeValidator.ts index 363ec7c3..71498459 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 { ExtendedObjectsValidators } from "./ExtendedObjectsValidators"; 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,62 @@ export class BoundingVolumeValidator { result = false; } + // Perform the validation of the object in view of the + // extensions that it may contain + if ( + !ExtendedObjectsValidators.validateExtendedObject( + boundingVolumePath, + 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; + } + 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 { + // 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; const region = boundingVolume.region; const sphere = boundingVolume.sphere; @@ -122,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 @@ -155,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 @@ -202,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/ContentData.ts b/src/validation/ContentData.ts new file mode 100644 index 00000000..7c8e4071 --- /dev/null +++ b/src/validation/ContentData.ts @@ -0,0 +1,59 @@ +import path from "path"; + +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; + 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 fewer, if the buffer contains + * less than 4 bytes) + */ + get magic(): string { + 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/ContentDataEntry.ts b/src/validation/ContentDataEntry.ts new file mode 100644 index 00000000..f1230db2 --- /dev/null +++ b/src/validation/ContentDataEntry.ts @@ -0,0 +1,24 @@ +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 b4b31e15..c4eb480c 100644 --- a/src/validation/ContentDataValidator.ts +++ b/src/validation/ContentDataValidator.ts @@ -5,14 +5,9 @@ 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"; @@ -22,7 +17,6 @@ import { ContentValidationIssues } from "../issues/ContentValidationIssues"; /** * A class for validation of the data that is pointed to by a `content.uri`. * - * * @private */ export class ContentDataValidator { @@ -96,198 +90,93 @@ 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(contentUri); - const result = await dataValidator.validateObject(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); - 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); - 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); - 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); - 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 message = `${error}`; + const issue = IoValidationIssues.JSON_PARSE_ERROR(contentUri, message); + 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 = + ContentDataValidators.findContentDataValidator(contentData); + if (!defined(dataValidator)) { + const path = contentPath; + const message = + `Tile content ${contentPath} refers to URI ${contentUri}, ` + + `for which no 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 - ); + 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. + 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; } /** - * 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. + * Track the extensions that are used, and which only refer to + * allowing certain content data types. * - * If the data type cannot be determined, an `CONTENT_VALIDATION_WARNING` - * will be added to the given context. + * 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 contentPath The path for the `ValidationIssue` instances. - * @param contentUri The URI of the content - * @param contentData The buffer containing the actual content data + * @param contentData The `ContentData` * @param context The `ValidationContext` - * @returns A promise that resolves when the validation is finished */ - private static async validateJsonContentData( - contentPath: string, - contentUri: string, - contentData: Buffer, + private static trackExtensionsFound( + contentData: ContentData, 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; + ) { + if ( + ResourceTypes.isGlb(contentData.data) || + ContentDataValidators.isProbablyGltf(contentData) + ) { + context.addExtensionFound("3DTILES_content_gltf"); } - - // 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( - 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(contentUri); - const result = await gltfValidator.validateObject(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..72321f61 --- /dev/null +++ b/src/validation/ContentDataValidators.ts @@ -0,0 +1,224 @@ +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 future, 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()); + + // 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 = false; + 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(); + } + + /** + * 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 + */ + static isProbablyGltf(contentData: ContentData) { + if (ContentDataValidators.isProbablyTileset(contentData)) { + return false; + } + 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/ContentValidator.ts b/src/validation/ContentValidator.ts index 05e6de0e..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. @@ -32,12 +33,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) @@ -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"; @@ -129,13 +147,13 @@ export class ContentValidator { const boundingVolume = content.boundingVolume; const boundingVolumePath = contentPath + "/boundingVolume"; if (defined(boundingVolume)) { - if ( - !BoundingVolumeValidator.validateBoundingVolume( + const boundingVolumeValid = + await BoundingVolumeValidator.validateBoundingVolume( boundingVolumePath, boundingVolume!, context - ) - ) { + ); + if (!boundingVolumeValid) { result = false; } } diff --git a/src/validation/ExtendedObjectsValidators.ts b/src/validation/ExtendedObjectsValidators.ts new file mode 100644 index 00000000..8bdb9660 --- /dev/null +++ b/src/validation/ExtendedObjectsValidators.ts @@ -0,0 +1,143 @@ +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>(); + + 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/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 827612c9..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"; @@ -32,7 +33,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 +53,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); } /** @@ -95,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 bb73ade5..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 @@ -38,39 +39,27 @@ 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 * 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. * - * 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,17 +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; if ( @@ -223,7 +212,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 +235,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); @@ -307,9 +297,23 @@ 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( + path, subtree, hasBinaryBuffer, context @@ -356,6 +360,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 +368,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/TileValidator.ts b/src/validation/TileValidator.ts index 1f6f4392..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 @@ -49,12 +50,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; @@ -74,17 +75,30 @@ 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"; // The boundingVolume MUST be defined - if ( - !BoundingVolumeValidator.validateBoundingVolume( + const boundingVolumeValid = + await BoundingVolumeValidator.validateBoundingVolume( boundingVolumePath, boundingVolume, context - ) - ) { + ); + if (!boundingVolumeValid) { result = false; } @@ -243,14 +257,13 @@ export class TileValidator { result = false; } } else { - if ( - !TileValidator.validateSimpleTile( - tilePath, - tile, - validationState, - context - ) - ) { + const simpleTileValid = await TileValidator.validateSimpleTile( + tilePath, + tile, + validationState, + context + ); + if (!simpleTileValid) { result = false; } } @@ -269,12 +282,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,14 +297,13 @@ export class TileValidator { const content = tile.content; const contentPath = tilePath + "/content"; if (defined(content)) { - if ( - !ContentValidator.validateContent( - contentPath, - content!, - validationState, - context - ) - ) { + const contentValid = await ContentValidator.validateContent( + contentPath, + content!, + validationState, + context + ); + if (!contentValid) { result = false; } } @@ -319,14 +331,13 @@ export class TileValidator { for (let index = 0; index < contents!.length; index++) { const contentsElementPath = contentsPath + "/" + index; const contentsElement = contents![index]; - if ( - !ContentValidator.validateContent( - contentsElementPath, - contentsElement, - validationState, - context - ) - ) { + const contentValid = await ContentValidator.validateContent( + contentsElementPath, + contentsElement, + validationState, + context + ); + if (!contentValid) { result = false; } } diff --git a/src/validation/TilesetTraversingValidator.ts b/src/validation/TilesetTraversingValidator.ts index 15bf6890..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` @@ -194,9 +223,33 @@ 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; } + 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; @@ -233,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; } @@ -297,12 +334,15 @@ 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..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"; /** @@ -39,7 +40,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); @@ -48,19 +49,21 @@ 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 * @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,17 +74,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)) { return false; @@ -101,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)) { @@ -390,9 +406,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 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") { + actualExtensionsFound.delete("3DTILES_content_gltf"); + } + for (const extensionName of actualExtensionsFound) { if (!actualExtensionsUsed.has(extensionName)) { const issue = SemanticValidationIssues.EXTENSION_FOUND_BUT_NOT_USED( 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, 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, diff --git a/src/validation/Validator.ts b/src/validation/Validator.ts index 81978d3b..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(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..5442ae2d 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. @@ -96,7 +98,6 @@ export class Validators { const resourceResolver = ResourceResolvers.createFileResourceResolver(directory); const validator = new SubtreeValidator( - uri, validationState, implicitTiling, resourceResolver @@ -133,8 +134,97 @@ 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(); } + + /** + * 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, + input: Buffer, + context: ValidationContext + ): Promise { + try { + const object: T = JSON.parse(input.toString()); + const delegateResult = await delegate.validateObject( + inputPath, + object, + context + ); + return delegateResult; + } catch (error) { + const message = `${error}`; + const issue = IoValidationIssues.JSON_PARSE_ERROR(inputPath, message); + context.addIssue(issue); + return false; + } + }, + }; + } + + /** + * 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, + //eslint-disable-next-line @typescript-eslint/no-unused-vars + input: Buffer, + context: ValidationContext + ): Promise { + const issue = ContentValidationIssues.CONTENT_VALIDATION_WARNING( + inputPath, + message + ); + context.addIssue(issue); + return true; + }, + }; + } + + /** + * 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( + //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; + }, + }; + } } diff --git a/src/validation/extensions/BoundingVolumeS2ValidationIssues.ts b/src/validation/extensions/BoundingVolumeS2ValidationIssues.ts new file mode 100644 index 00000000..1203c7be --- /dev/null +++ b/src/validation/extensions/BoundingVolumeS2ValidationIssues.ts @@ -0,0 +1,11 @@ +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.ERROR; + 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..359578b9 --- /dev/null +++ b/src/validation/extensions/BoundingVolumeS2Validator.ts @@ -0,0 +1,237 @@ +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"; + +/** + * 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 of a `BoundungVolume` object that + * contains a `3DTILES_bounding_volume_S2` extension object. + * + * @param path The path for `ValidationIssue` instances + * @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, + boundingVolume: any, + context: ValidationContext + ): Promise { + // Make sure that the given value is an object + if ( + !BasicValidator.validateObject( + path, + "boundingVolume", + boundingVolume, + context + ) + ) { + return false; + } + + let result = true; + + // 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 validation of 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 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"; + // 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; + } + + /** + * 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)) { + return false; + } + // Further constraints could be added here (e.g. that + // the first digit is only a value in [0,5] ...) + return true; + } +}