diff --git a/specs/BinaryBufferStructureSpec.ts b/specs/BinaryBufferStructureSpec.ts index b822505b..d5dc74a1 100644 --- a/specs/BinaryBufferStructureSpec.ts +++ b/specs/BinaryBufferStructureSpec.ts @@ -42,7 +42,8 @@ function performTestValidation( if ( BinaryBufferStructureValidator.validateBinaryBufferStructure( path, - binaryBufferStructure, + binaryBufferStructure.buffers, + binaryBufferStructure.bufferViews, firstBufferUriIsRequired, context ) diff --git a/specs/SubtreeValidationSpec.ts b/specs/SubtreeValidationSpec.ts new file mode 100644 index 00000000..52cd4506 --- /dev/null +++ b/specs/SubtreeValidationSpec.ts @@ -0,0 +1,392 @@ +import { Schema } from "3d-tiles-tools"; + +import { readJsonUnchecked } from "../src/base/readJsonUnchecked"; + +import { Validators } from "../src/validation/Validators"; +import { ValidatedElement } from "../src/validation/ValidatedElement"; +import { ValidationResult } from "../src/validation/ValidationResult"; + +/** + * Validate the specified subtree file from the `specs/data/subtrees` + * directory. + * + * Note that in order to validate a subtree file, the validator requires + * additional data elements. Namely, the `TileImplicitTiling` that + * defines the structure of the subtree, and a `Schema` (if the + * subtree contains metadata). + * + * This function will load this data from the additional files in + * the `specs/data/` directory that define the same structure for + * all subtree spec files. + * + * @param fileName - The subtree file name + * @returns A promise to the `ValidationResult` + */ +async function validateSpecSubtreeFile( + fileName: string +): Promise { + // The schema for the subtrees in the specs directory + const specSchema: Schema = await readJsonUnchecked( + "specs/data/schemas/validSchema.json" + ); + const specSchemaState: ValidatedElement = { + wasPresent: true, + validatedElement: specSchema, + }; + + // The `TileImplicitTiling` object that defines the + // structure of subtrees in the specs directory + const specImplicitTiling = await readJsonUnchecked( + "specs/data/subtrees/validSubtreeImplicitTiling.json.input" + ); + + const validationResult = await Validators.validateSubtreeFile( + fileName, + specSchemaState, + specImplicitTiling + ); + return validationResult; +} + +describe("Subtree validation", function () { + it("detects issues in binarySubtreeComputedLengthInvalid", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/binarySubtreeComputedLengthInvalid.subtree" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("BINARY_INVALID_LENGTH"); + }); + + it("detects issues in binarySubtreeInvalidBinaryByteLengthAlignment", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/binarySubtreeInvalidBinaryByteLengthAlignment.subtree" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("BINARY_INVALID_ALIGNMENT"); + }); + + it("detects issues in binarySubtreeInvalidJsonByteLengthAlignment", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/binarySubtreeInvalidJsonByteLengthAlignment.subtree" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("BINARY_INVALID_ALIGNMENT"); + }); + + it("detects issues in binarySubtreeInvalidMagic", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/binarySubtreeInvalidMagic.subtree" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("IO_ERROR"); + }); + + it("detects issues in binarySubtreeInvalidVersion", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/binarySubtreeInvalidVersion.subtree" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("BINARY_INVALID_VALUE"); + }); + + it("detects issues in binarySubtreeJsonInvalid", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/binarySubtreeJsonInvalid.subtree" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("JSON_PARSE_ERROR"); + }); + + it("detects no issues in binarySubtreeValid", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/binarySubtreeValid.subtree" + ); + expect(result.length).toEqual(0); + }); + + it("detects issues in subtreeBufferViewsWithoutBuffers", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreeBufferViewsWithoutBuffers.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("BUFFER_VIEWS_WITHOUT_BUFFERS"); + }); + + it("detects issues in subtreeChildSubtreeAvailabilityInvalidType", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreeChildSubtreeAvailabilityInvalidType.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("TYPE_MISMATCH"); + }); + + it("detects issues in subtreeChildSubtreeAvailabilityMissing", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreeChildSubtreeAvailabilityMissing.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("PROPERTY_MISSING"); + }); + + it("detects issues in subtreeContentAvailabilityInvalidLength", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreeContentAvailabilityInvalidLength.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("ARRAY_LENGTH_MISMATCH"); + }); + + it("detects issues in subtreeContentAvailabilityInvalidType", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreeContentAvailabilityInvalidType.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("TYPE_MISMATCH"); + }); + + it("detects issues in subtreeContentMetadataArrayElementInvalidType", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreeContentMetadataArrayElementInvalidType.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("ARRAY_ELEMENT_TYPE_MISMATCH"); + }); + + it("detects issues in subtreeContentMetadataArrayElementInvalidValueA", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreeContentMetadataArrayElementInvalidValueA.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("TYPE_MISMATCH"); + }); + + it("detects issues in subtreeContentMetadataArrayElementInvalidValueB", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreeContentMetadataArrayElementInvalidValueB.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("VALUE_NOT_IN_RANGE"); + }); + + it("detects issues in subtreeContentMetadataInvalidLength", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreeContentMetadataInvalidLength.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("ARRAY_LENGTH_MISMATCH"); + }); + + it("detects issues in subtreeContentMetadataInvalidType", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreeContentMetadataInvalidType.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("TYPE_MISMATCH"); + }); + + it("detects issues in subtreeContentMetadataWithoutPropertyTables", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreeContentMetadataWithoutPropertyTables.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("REQUIRED_VALUE_NOT_FOUND"); + }); + + it("detects issues in subtreePropertyTablesElementInvalidType", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreePropertyTablesElementInvalidType.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("ARRAY_ELEMENT_TYPE_MISMATCH"); + }); + + it("detects issues in subtreePropertyTablesInvalidLength", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreePropertyTablesInvalidLength.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("ARRAY_LENGTH_MISMATCH"); + }); + + it("detects issues in subtreePropertyTablesInvalidType", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreePropertyTablesInvalidType.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("TYPE_MISMATCH"); + }); + + it("detects issues in subtreeTileAvailabilityAvailableCountInvalid", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreeTileAvailabilityAvailableCountInvalid.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("SUBTREE_AVAILABILITY_INCONSISTENT"); + }); + + it("detects issues in subtreeTileAvailabilityAvailableCountMismatch", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreeTileAvailabilityAvailableCountMismatch.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("SUBTREE_AVAILABILITY_INCONSISTENT"); + }); + + it("detects issues in subtreeTileAvailabilityBitstreamAndConstant", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreeTileAvailabilityBitstreamAndConstant.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("ONE_OF_ERROR"); + }); + + it("detects issues in subtreeTileAvailabilityBitstreamInvalidType", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreeTileAvailabilityBitstreamInvalidType.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("TYPE_MISMATCH"); + }); + + it("detects issues in subtreeTileAvailabilityBitstreamInvalidValueA", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreeTileAvailabilityBitstreamInvalidValueA.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("TYPE_MISMATCH"); + }); + + it("detects issues in subtreeTileAvailabilityBitstreamInvalidValueB", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreeTileAvailabilityBitstreamInvalidValueB.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("VALUE_NOT_IN_RANGE"); + }); + + it("detects issues in subtreeTileAvailabilityBitstreamInvalidValueC", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreeTileAvailabilityBitstreamInvalidValueC.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("VALUE_NOT_IN_RANGE"); + }); + + it("detects issues in subtreeTileAvailabilityBitstreamLengthTooLarge", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreeTileAvailabilityBitstreamLengthTooLarge.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("SUBTREE_AVAILABILITY_INCONSISTENT"); + }); + + it("detects issues in subtreeTileAvailabilityBitstreamLengthTooSmall", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreeTileAvailabilityBitstreamLengthTooSmall.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("SUBTREE_AVAILABILITY_INCONSISTENT"); + }); + + it("detects issues in subtreeTileAvailabilityConstantInvalidType", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreeTileAvailabilityConstantInvalidType.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("VALUE_NOT_IN_LIST"); + }); + + it("detects issues in subtreeTileAvailabilityConstantInvalidValue", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreeTileAvailabilityConstantInvalidValue.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("VALUE_NOT_IN_LIST"); + }); + + it("detects issues in subtreeTileAvailabilityForParentMissingForAvailableTile", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreeTileAvailabilityForParentMissingForAvailableTile.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("SUBTREE_AVAILABILITY_INCONSISTENT"); + }); + + it("detects issues in subtreeTileAvailabilityInvalidType", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreeTileAvailabilityInvalidType.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("TYPE_MISMATCH"); + }); + + it("detects issues in subtreeTileAvailabilityMissing", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreeTileAvailabilityMissing.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("PROPERTY_MISSING"); + }); + + it("detects issues in subtreeTileAvailabilityMissingForAvailableContent", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreeTileAvailabilityMissingForAvailableContent.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("SUBTREE_AVAILABILITY_INCONSISTENT"); + }); + + it("detects issues in subtreeTileAvailabilityNeitherBitstreamNorConstant", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreeTileAvailabilityNeitherBitstreamNorConstant.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("ANY_OF_ERROR"); + }); + + it("detects issues in subtreeTileMetadataInvalidType", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreeTileMetadataInvalidType.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("TYPE_MISMATCH"); + }); + + it("detects issues in subtreeTileMetadataInvalidValueA", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreeTileMetadataInvalidValueA.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("TYPE_MISMATCH"); + }); + + it("detects issues in subtreeTileMetadataInvalidValueB", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/subtreeTileMetadataInvalidValueB.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("VALUE_NOT_IN_RANGE"); + }); + + it("detects no issues in validSubtree", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/validSubtree.json" + ); + expect(result.length).toEqual(0); + }); + + it("detects issues in validSubtreeBuffersWithoutBufferViews", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/validSubtreeBuffersWithoutBufferViews.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("BUFFERS_WITHOUT_BUFFER_VIEWS"); + }); + + it("detects no issues in validSubtreeNoBuffers", async function () { + const result = await validateSpecSubtreeFile( + "specs/data/subtrees/validSubtreeNoBuffers.json" + ); + expect(result.length).toEqual(0); + }); +}); diff --git a/specs/data/subtrees/subtreeBufferViewsWithoutBuffers.json b/specs/data/subtrees/subtreeBufferViewsWithoutBuffers.json new file mode 100644 index 00000000..26b8a864 --- /dev/null +++ b/specs/data/subtrees/subtreeBufferViewsWithoutBuffers.json @@ -0,0 +1,9 @@ +{ + "bufferViews": [ + { "buffer": 0, "byteOffset": 0, "byteLength": 3 }, + { "buffer": 0, "byteOffset": 8, "byteLength": 8 } + ], + "tileAvailability": { "bitstream": 0, "availableCount": 7 }, + "contentAvailability": [{ "availableCount": 0, "constant": 0 }], + "childSubtreeAvailability": { "bitstream": 1, "availableCount": 8 } +} diff --git a/specs/data/subtrees/validSubtreeBuffersWithoutBufferViews.json b/specs/data/subtrees/validSubtreeBuffersWithoutBufferViews.json new file mode 100644 index 00000000..e5d9f4f4 --- /dev/null +++ b/specs/data/subtrees/validSubtreeBuffersWithoutBufferViews.json @@ -0,0 +1,6 @@ +{ + "buffers": [{ "uri": "validBuffer.bin", "byteLength": 16 }], + "tileAvailability": { "constant": 1 }, + "contentAvailability": [{ "constant": 1 }], + "childSubtreeAvailability": { "constant": 0 } +} diff --git a/specs/data/subtrees/validSubtreeNoBuffers.json b/specs/data/subtrees/validSubtreeNoBuffers.json new file mode 100644 index 00000000..a9395543 --- /dev/null +++ b/specs/data/subtrees/validSubtreeNoBuffers.json @@ -0,0 +1,5 @@ +{ + "tileAvailability": { "constant": 1 }, + "contentAvailability": [{ "constant": 1 }], + "childSubtreeAvailability": { "constant": 0 } +} diff --git a/src/issues/SemanticValidationIssues.ts b/src/issues/SemanticValidationIssues.ts index cdcc98df..8ddf95b1 100644 --- a/src/issues/SemanticValidationIssues.ts +++ b/src/issues/SemanticValidationIssues.ts @@ -229,7 +229,7 @@ export class SemanticValidationIssues { * Indicates an inconsistency of buffers and buffer views. * * This mainly refers to the 'buffers' and 'bufferViews' of - * an implicit subtree. It may, for example, inciate that a + * an implicit subtree. It may, for example, indicate that a * buffer view does not fit into the buffer that it refers to. * * @param path - The path for the `ValidationIssue` @@ -243,6 +243,39 @@ export class SemanticValidationIssues { return issue; } + /** + * Indicates that a binary buffer structure (like the 'buffers' and + * 'bufferViews' of a subtree) contained `buffers`, but no `bufferViews`. + * + * This is only a WARNING, because it does not violate the specification, + * but is certainly not intended. + * + * @param path - The path for the `ValidationIssue` + * @returns The `ValidationIssue` + */ + static BUFFERS_WITHOUT_BUFFER_VIEWS(path: string) { + const type = "BUFFERS_WITHOUT_BUFFER_VIEWS"; + const message = "The object contained 'buffers' but no 'bufferViews'"; + const severity = ValidationIssueSeverity.WARNING; + const issue = new ValidationIssue(type, path, message, severity); + return issue; + } + + /** + * Indicates that a binary buffer structure (like the 'buffers' and + * 'bufferViews' of a subtree) contained `bufferViews`, but no `buffers` + * + * @param path - The path for the `ValidationIssue` + * @returns The `ValidationIssue` + */ + static BUFFER_VIEWS_WITHOUT_BUFFERS(path: string) { + const type = "BUFFER_VIEWS_WITHOUT_BUFFERS"; + const message = "The object contained 'bufferViews' but no 'buffers'"; + const severity = ValidationIssueSeverity.ERROR; + const issue = new ValidationIssue(type, path, message, severity); + return issue; + } + /** * Indicates an inconsistency in availability information. * diff --git a/src/validation/BinaryBufferStructureValidator.ts b/src/validation/BinaryBufferStructureValidator.ts index 052ec85a..8e1ea9ad 100644 --- a/src/validation/BinaryBufferStructureValidator.ts +++ b/src/validation/BinaryBufferStructureValidator.ts @@ -32,10 +32,11 @@ import { SemanticValidationIssues } from "../issues/SemanticValidationIssues"; */ export class BinaryBufferStructureValidator { /** - * Performs the validation of the given `BinaryBufferStructure` + * Performs the validation of the given elements of a `BinaryBufferStructure`. * * @param path - The path for `ValidationIssue` instances - * @param binaryBufferStructure - The `BinaryBufferStructure` object + * @param buffers - The buffers + * @param bufferViews - The buffer views * @param firstBufferUriIsRequired - If this is `false`, then the * first buffer may omit the `uri` property, namely when it refers * to a binary chunk, for example, of a binary `.subtree` file. @@ -44,14 +45,14 @@ export class BinaryBufferStructureValidator { */ static validateBinaryBufferStructure( path: string, - binaryBufferStructure: BinaryBufferStructure, + buffers: BufferObject[] | undefined, + bufferViews: BufferView[] | undefined, firstBufferUriIsRequired: boolean, context: ValidationContext ): boolean { let result = true; // Validate the buffers - const buffers = binaryBufferStructure.buffers; const buffersPath = path + "/buffers"; if (defined(buffers)) { // The buffers MUST be an array of at least 1 objects @@ -67,6 +68,7 @@ export class BinaryBufferStructureValidator { ) ) { result = false; + return result; } else { // Validate each buffer for (let i = 0; i < buffers.length; i++) { @@ -87,10 +89,34 @@ export class BinaryBufferStructureValidator { } } } + + // If the buffers are invalid, bail out early + if (!result) { + return false; + } + // Validate the bufferViews - const bufferViews = binaryBufferStructure.bufferViews; const bufferViewsPath = path + "/bufferViews"; - if (defined(bufferViews)) { + + if (!defined(bufferViews)) { + // When there are bufferViews, but (unused) buffers, issue a warning + if (defined(buffers)) { + const issue = + SemanticValidationIssues.BUFFERS_WITHOUT_BUFFER_VIEWS(path); + context.addIssue(issue); + } + } else { + // When there are bufferViews, but no buffers, issue an error + if (!defined(buffers)) { + const issue = + SemanticValidationIssues.BUFFER_VIEWS_WITHOUT_BUFFERS(path); + context.addIssue(issue); + return false; + } + + // Here, bufferViews and buffers are defined. + const numBuffers = buffers.length; + //The bufferViews MUST be an array of at least 1 objects if ( !BasicValidator.validateArray( @@ -114,6 +140,7 @@ export class BinaryBufferStructureValidator { bufferViewPath, "bufferViews/" + i, bufferView, + numBuffers, context ) ) { @@ -220,6 +247,7 @@ export class BinaryBufferStructureValidator { * @param path - The path for the `ValidationIssue` instances * @param name - The name of the object * @param bufferView - The `BufferView` object + * @param numBuffers - The number of `buffers` * @param context - The `ValidationContext` that any issues will be added to * @returns Whether the object was valid */ @@ -227,6 +255,7 @@ export class BinaryBufferStructureValidator { path: string, name: string, bufferView: BufferView, + numBuffers: number, context: ValidationContext ): boolean { // Make sure that the given value is an object @@ -239,6 +268,7 @@ export class BinaryBufferStructureValidator { // Validate the buffer // The buffer MUST be defined // The buffer MUST be an integer of at least 0 + // The buffer MUST be smaller than the number of buffers const buffer = bufferView.buffer; const bufferPath = path + "/buffer"; if ( @@ -248,7 +278,7 @@ export class BinaryBufferStructureValidator { buffer, 0, true, - undefined, + numBuffers, false, context ) @@ -346,38 +376,22 @@ export class BinaryBufferStructureValidator { const bufferView = bufferViews[i]; const bufferViewPath = path + "/bufferViews/" + i; - // The buffer (index) MUST be smaller than the number of buffers - if ( - !BasicValidator.validateIntegerRange( - bufferViewPath + "/buffer", - "buffer", - bufferView.buffer, - 0, - true, - buffers.length, - false, - context - ) - ) { - result = false; - } else { - const bufferViewEnd = bufferView.byteOffset + bufferView.byteLength; - const buffer = buffers[bufferView.buffer]; + const bufferViewEnd = bufferView.byteOffset + bufferView.byteLength; + const buffer = buffers[bufferView.buffer]; - // The end of the buffer view MUST be at most the buffer length - if (bufferViewEnd > buffer.byteLength) { - const message = - `The bufferView has an offset of ${bufferView.byteOffset} ` + - `and a length of ${bufferView.byteLength}, yielding ` + - `${bufferView.byteOffset + bufferView.byteLength}, but buffer ` + - `${bufferView.buffer} only has a length of ${buffer.byteLength}`; - const issue = SemanticValidationIssues.BUFFERS_INCONSISTENT( - bufferViewPath, - message - ); - context.addIssue(issue); - result = false; - } + // The end of the buffer view MUST be at most the buffer length + if (bufferViewEnd > buffer.byteLength) { + const message = + `The bufferView has an offset of ${bufferView.byteOffset} ` + + `and a length of ${bufferView.byteLength}, yielding ` + + `${bufferView.byteOffset + bufferView.byteLength}, but buffer ` + + `${bufferView.buffer} only has a length of ${buffer.byteLength}`; + const issue = SemanticValidationIssues.BUFFERS_INCONSISTENT( + bufferViewPath, + message + ); + context.addIssue(issue); + result = false; } } diff --git a/src/validation/SubtreeConsistencyValidator.ts b/src/validation/SubtreeConsistencyValidator.ts index 46c0c2ce..0eb3730b 100644 --- a/src/validation/SubtreeConsistencyValidator.ts +++ b/src/validation/SubtreeConsistencyValidator.ts @@ -24,7 +24,9 @@ import { BinaryBufferStructure } from "3d-tiles-tools"; * and availability data, referring to the information that is * given in the `TileImplicitTiling` structure. * - * They will **NOT** analyze the actual buffer data. + * They will **NOT** analyze the actual buffer data. If the subtree + * data is consistent, then the actual buffer data is validated based + * on the high-level (model) objects, using a `SubtreeInfoValidator`. * * @internal */ @@ -52,21 +54,25 @@ export class SubtreeConsistencyValidator { implicitTiling: TileImplicitTiling | undefined, context: ValidationContext ): boolean { - // Only if the buffers and buffer views have been valid - // on the JSON level, validate their consistency - // in terms of memory layout - const binaryBufferStructure: BinaryBufferStructure = { - buffers: subtree.buffers ?? [], - bufferViews: subtree.bufferViews ?? [], - }; - if ( - !BinaryBufferStructureValidator.validateBinaryBufferStructureConsistency( - path, - binaryBufferStructure, - context - ) - ) { - return false; + // The buffers and bufferViews are optional. If they are both defined, + // then the SubtreeValidator already validated them on the JSON level. + // Here, validate their consistency in terms of memory layout + const buffers = subtree.buffers; + const bufferViews = subtree.bufferViews; + if (defined(buffers) && defined(bufferViews)) { + const binaryBufferStructure: BinaryBufferStructure = { + buffers: buffers, + bufferViews: bufferViews, + }; + if ( + !BinaryBufferStructureValidator.validateBinaryBufferStructureConsistency( + path, + binaryBufferStructure, + context + ) + ) { + return false; + } } if (defined(implicitTiling)) { diff --git a/src/validation/SubtreeValidator.ts b/src/validation/SubtreeValidator.ts index a5c99f15..fce391ad 100644 --- a/src/validation/SubtreeValidator.ts +++ b/src/validation/SubtreeValidator.ts @@ -9,7 +9,6 @@ import { Availability } from "3d-tiles-tools"; import { TileImplicitTiling } from "3d-tiles-tools"; import { MetadataUtilities } from "3d-tiles-tools"; -import { BinaryBufferStructure } from "3d-tiles-tools"; import { BinarySubtreeData } from "3d-tiles-tools"; import { BinarySubtreeDataResolver } from "3d-tiles-tools"; import { BinaryPropertyTable } from "3d-tiles-tools"; @@ -27,13 +26,14 @@ import { ValidatedElement } from "./ValidatedElement"; import { BinaryBufferStructureValidator } from "./BinaryBufferStructureValidator"; +import { BinaryPropertyTableValidator } from "./metadata/BinaryPropertyTableValidator"; + import { MetadataEntityValidator } from "./metadata/MetadataEntityValidator"; import { PropertyTablesDefinitionValidator } from "./metadata/PropertyTablesDefinitionValidator"; import { JsonValidationIssues } from "../issues/JsonValidationIssues"; import { IoValidationIssues } from "../issues/IoValidationIssue"; import { StructureValidationIssues } from "../issues/StructureValidationIssues"; -import { BinaryPropertyTableValidator } from "./metadata/BinaryPropertyTableValidator"; /** * A class for validations related to `subtree` objects that have @@ -213,8 +213,10 @@ export class SubtreeValidator implements Validator { const jsonBuffer = input.subarray(jsonStartByteOffset, jsonEndByteOffset); // Try to parse the JSON + let subtree: Subtree; try { - Buffers.getJson(jsonBuffer); + const jsonString = jsonBuffer.toString(); + subtree = JSON.parse(jsonString); } catch (error) { const message = `Could not parse subtree JSON: ${error}`; const issue = IoValidationIssues.JSON_PARSE_ERROR(path, message); @@ -222,17 +224,28 @@ export class SubtreeValidator implements Validator { return false; } - const hasBinaryBuffer = binaryByteLength > 0; + // First, validate the basic JSON structure of the buffers + // and bufferViews. When they are invalid, then the binary + // subtree data cannot be resolved, and the subtree is + // considered to be invalid. + const firstBufferUriIsRequired = binaryByteLength === 0n; + const bufferStructureValid = + BinaryBufferStructureValidator.validateBinaryBufferStructure( + path, + subtree.buffers, + subtree.bufferViews, + firstBufferUriIsRequired, + context + ); + if (!bufferStructureValid) { + return false; + } + const binarySubtreeData = await BinarySubtreeDataResolver.resolveFromBuffer( input, this.resourceResolver ); - const result = this.validateSubtree( - path, - binarySubtreeData, - hasBinaryBuffer, - context - ); + const result = this.validateSubtree(path, binarySubtreeData, context); return result; } @@ -262,20 +275,34 @@ export class SubtreeValidator implements Validator { const inputString = input.toString(); const subtree: Subtree = JSON.parse(inputString); + // First, validate the basic JSON structure of the buffers + // and bufferViews. When they are invalid, then the binary + // subtree data cannot be resolved, and the subtree is + // considered to be invalid. + const firstBufferUriIsRequired = true; + const bufferStructureValid = + BinaryBufferStructureValidator.validateBinaryBufferStructure( + path, + subtree.buffers, + subtree.bufferViews, + firstBufferUriIsRequired, + context + ); + if (!bufferStructureValid) { + return false; + } const binarySubtreeData = await BinarySubtreeDataResolver.resolveFromJson( subtree, this.resourceResolver ); - const hasBinaryBuffer = false; const result = await this.validateSubtree( path, binarySubtreeData, - hasBinaryBuffer, context ); return result; } catch (error) { - //console.log(error); + console.log(error); const issue = IoValidationIssues.JSON_PARSE_ERROR(path, `${error}`); context.addIssue(issue); return false; @@ -287,9 +314,6 @@ export class SubtreeValidator implements Validator { * * @param path - The path for `ValidationIssue` instances * @param binarySubtreeData - The `BinarySubtreeData` object - * @param hasBinaryBuffer - Whether the subtree data has an (internal) - * binary buffer, meaning that the first `Buffer` object may omit - * the URI. * @param context - The `ValidationContext` * @returns A promise that resolves when the validation is finished * and indicates whether the object was valid or not. @@ -297,7 +321,6 @@ export class SubtreeValidator implements Validator { private async validateSubtree( path: string, binarySubtreeData: BinarySubtreeData, - hasBinaryBuffer: boolean, context: ValidationContext ): Promise { const subtree = binarySubtreeData.subtree; @@ -331,12 +354,7 @@ export class SubtreeValidator implements Validator { // Validate the structure of the given subtree object, // on the level of JSON validity - const structureIsValid = this.validateSubtreeObject( - path, - subtree, - hasBinaryBuffer, - context - ); + const structureIsValid = this.validateSubtreeObject(path, subtree, context); if (!structureIsValid) { result = false; return result; @@ -392,18 +410,15 @@ export class SubtreeValidator implements Validator { * * @param path - The path for `ValidationIssue` instances * @param subtree - The `Subtree` object - * @param hasBinaryBuffer - Whether the subtree has an associated - * binary buffer * @param context - The `ValidationContext` * @returns A promise that resolves when the validation is finished */ private validateSubtreeObject( path: string, subtree: Subtree, - hasBinaryBuffer: boolean, context: ValidationContext ): boolean { - if (!this.validateSubtreeBasic(path, subtree, hasBinaryBuffer, context)) { + if (!this.validateSubtreeBasic(path, subtree, context)) { return false; } if (!this.validateMetadata(path, subtree, context)) { @@ -428,7 +443,6 @@ export class SubtreeValidator implements Validator { private validateSubtreeBasic( path: string, subtree: Subtree, - hasBinaryBuffer: boolean, context: ValidationContext ): boolean { // Make sure that the given value is an object @@ -438,24 +452,6 @@ export class SubtreeValidator implements Validator { let result = true; - // Validate the binary buffer structure, i.e. the `buffers` - // and `bufferViews` - const binaryBufferStructure: BinaryBufferStructure = { - buffers: subtree.buffers ?? [], - bufferViews: subtree.bufferViews ?? [], - }; - const firstBufferUriIsRequired = !hasBinaryBuffer; - if ( - !BinaryBufferStructureValidator.validateBinaryBufferStructure( - path, - binaryBufferStructure, - firstBufferUriIsRequired, - context - ) - ) { - result = false; - } - // Validate the tileAvailability const tileAvailability = subtree.tileAvailability; const tileAvailabilityPath = path + "/tileAvailability";