Skip to content
Draft
42 changes: 39 additions & 3 deletions specs/gltfExtensions/validateGltf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,34 @@ import path from "path";
import { ResourceResolvers } from "3d-tiles-tools";

import { ValidationContext } from "../../src/validation/ValidationContext";
import { ValidationResult } from "../../src/validation/ValidationResult";

import { GltfDataReader } from "../../src/validation/gltf/GltfDataReader";
import { GltfExtensionValidators } from "../../src/validation/gltf/GltfExtensionValidators";

export async function validateGltf(gltfFileName: string) {
import { IoValidationIssues } from "../../src/issues/IoValidationIssue";

/**
* An INTERNAL utility function that is only intended to be used in the
* specs: It ONLY performs the glTF validation that is implemented as
* part of the 3D Tiles Validator.
*
* It will NOT perform a full validation with the glTF-Validator. It assumes
* that the part of the glTF file that is checked with the glTF-Validator
* is already valid.
*
* This is intended for the use in the specs, where the validation result
* is checked to contain the validation issues that are generated by the
* 3D Tiles Validator implementation, without the distraction from the
* glTF-Validator issues.
*
*
* @param gltfFileName - The glTF file name
* @returns The validation result
*/
export async function validateGltf(
gltfFileName: string
): Promise<ValidationResult> {
fs.readFileSync(gltfFileName);

const directory = path.dirname(gltfFileName);
Expand All @@ -16,12 +40,24 @@ export async function validateGltf(gltfFileName: string) {
ResourceResolvers.createFileResourceResolver(directory);
const context = new ValidationContext(directory, resourceResolver);
const gltfFileData = await resourceResolver.resolveData(fileName);
if (gltfFileData) {
await GltfExtensionValidators.validateGltfExtensions(
if (!gltfFileData) {
const message = `Could not read glTF for specs`;
const issue = IoValidationIssues.IO_ERROR(gltfFileName, message);
context.addIssue(issue);
} else {
// If this fails, and issue will be added to the context:
const gltfData = await GltfDataReader.readGltfData(
gltfFileName,
gltfFileData,
context
);
if (gltfData) {
await GltfExtensionValidators.validateGltfExtensions(
gltfFileName,
gltfData,
context
);
}
}
const validationResult = context.getResult();
return validationResult;
Expand Down
15 changes: 15 additions & 0 deletions src/issues/ValidationIssues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,19 @@ export class ValidationIssues {
const issue = new ValidationIssue(type, path, message, severity);
return issue;
}

/**
* An issue that just summarizes information about the validation
* process or result.
*
* @param path - The path for the `ValidationIssue`
* @param message - The message for the `ValidationIssue`
* @returns The `ValidationIssue`
*/
static VALIDATION_INFO(path: string, message: string) {
const type = "VALIDATION_INFO";
const severity = ValidationIssueSeverity.INFO;
const issue = new ValidationIssue(type, path, message, severity);
return issue;
}
}
95 changes: 79 additions & 16 deletions src/tileFormats/GltfValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { ValidationIssue } from "../validation/ValidationIssue";

import { ContentValidationIssues } from "../issues/ContentValidationIssues";
import { GltfExtensionValidators } from "../validation/gltf/GltfExtensionValidators";
import { ValidationIssueSeverity } from "../validation/ValidationIssueSeverity";
import { GltfDataReader } from "../validation/gltf/GltfDataReader";

// eslint-disable-next-line @typescript-eslint/no-var-requires
const validator = require("gltf-validator");
Expand Down Expand Up @@ -126,19 +128,59 @@ export class GltfValidator implements Validator<Buffer> {
return false;
}

// Convert all messages from the glTF validator into ValidationIssue
// objects that act as the "causes" of the content validation issue
// that may be about to be created
const allCauses: ValidationIssue[] = [];
const gltfMessages = gltfResult.issues?.messages ?? [];
for (const gltfMessage of gltfMessages) {
//console.log(gltfMessage);
const cause =
GltfValidator.createValidationIssueFromGltfMessage(gltfMessage);
allCauses.push(cause);
}

// Read the glTF data
const gltfData = await GltfDataReader.readGltfData(uri, input, context);
if (!gltfData) {
// Issue was already added to context
return false;
}

// Process the list of causes, possibly filtering out the ones that
// are known to be obsolete due to the validation that is performed
// by validators that are part of the 3D Tiles Validator
const causes = await GltfExtensionValidators.processCauses(
uri,
gltfData,
allCauses
);

// The number of errors/warnings/infos is determined based on
// the filtered issues.
const numErrors = GltfValidator.countIssueSeverities(
causes,
ValidationIssueSeverity.ERROR
);
const numWarnings = GltfValidator.countIssueSeverities(
causes,
ValidationIssueSeverity.WARNING
);
const numInfos = GltfValidator.countIssueSeverities(
causes,
ValidationIssueSeverity.INFO
);

// If there are any errors, then summarize ALL issues from the glTF
// validation as 'internal issues' in a CONTENT_VALIDATION_ERROR
if (gltfResult.issues.numErrors > 0) {
if (numErrors > 0) {
const path = uri;
const message = `Content ${uri} caused validation errors`;
const issue = ContentValidationIssues.CONTENT_VALIDATION_ERROR(
path,
message
);
for (const gltfMessage of gltfResult.issues.messages) {
//console.log(gltfMessage);
const cause =
GltfValidator.createValidationIssueFromGltfMessage(gltfMessage);
for (const cause of causes) {
issue.addCause(cause);
}
context.addIssue(issue);
Expand All @@ -150,22 +192,19 @@ export class GltfValidator implements Validator<Buffer> {
// If there are any warnings, then summarize them in a
// CONTENT_VALIDATION_WARNING, but still consider the
// object to be valid.
if (gltfResult.issues.numWarnings > 0) {
if (numWarnings > 0) {
const path = uri;
const message = `Content ${uri} caused validation warnings`;
const issue = ContentValidationIssues.CONTENT_VALIDATION_WARNING(
path,
message
);

for (const gltfMessage of gltfResult.issues.messages) {
//console.log(gltfMessage);
const cause =
GltfValidator.createValidationIssueFromGltfMessage(gltfMessage);
for (const cause of causes) {
issue.addCause(cause);
}
context.addIssue(issue);
} else if (gltfResult.issues.numInfos > 0) {
} else if (numInfos > 0) {
// If there are no warnings, but infos, then summarize them in a
// CONTENT_VALIDATION_INFO, but still consider the
// object to be valid.
Expand All @@ -177,22 +216,46 @@ export class GltfValidator implements Validator<Buffer> {
message
);

for (const gltfMessage of gltfResult.issues.messages) {
const cause =
GltfValidator.createValidationIssueFromGltfMessage(gltfMessage);
for (const cause of causes) {
issue.addCause(cause);
}
context.addIssue(issue);
}

// When the glTF itself is considered to be valid, then perform
// the validation of the Cesium glTF metadata extensions
// the validation of the glTF extensions that are implemented
// as part of the 3D Tiles Validator
const extensionsValid =
await GltfExtensionValidators.validateGltfExtensions(uri, input, context);
await GltfExtensionValidators.validateGltfExtensions(
uri,
gltfData,
context
);
if (!extensionsValid) {
return false;
}

return true;
}

/**
* Counts and returns the number of issues in the given array that have
* the given severity.
*
* @param issues - The issues
* @param severity - The severity
* @returns The number of issues with the given severity
*/
private static countIssueSeverities(
issues: ValidationIssue[],
severity: ValidationIssueSeverity
): number {
let count = 0;
for (const issue of issues) {
if (issue.severity === severity) {
count++;
}
}
return count;
}
}
146 changes: 146 additions & 0 deletions src/validation/gltf/GltfExtensionIssues.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { ValidationIssue } from "../ValidationIssue";

/**
* Functions for implementing filters on lists of validation issues,
* to filter out issues that are obsolete due to glTF validation
* steps that are performed by the 3D Tiles validator.
*/
export class GltfExtensionIssues {
/**
* Returns whether the given issue is a message about a potentially
* unused object from the given top-level category, and this object
* is actually used according to the given used indices.
*
* For example, when the given issue contains
* - message: "This object may be unused."
* - path: "/textures/1"
* and the given usedIndices does contain '1', then this method
* returns true.
*
* @param issue - The validation issue
* @param topLevelName - The top level name, e.g. "textures" or "bufferViews"
* @param usedIndices - The indices that are actually used
* @returns Whether the issue is obsolete
*/
static isObsoleteIssueAboutUnusedObject(
issue: ValidationIssue,
topLevelName: string,
usedIndices: number[]
) {
const message = issue.message;
const path = issue.path;
if (!message.startsWith("This object may be unused.")) {
return false;
}
const prefix = "/" + topLevelName + "/";
if (!path.startsWith(prefix)) {
return;
}
const index = GltfExtensionIssues.extractIndex(topLevelName, path);
if (index === undefined) {
// This is making some assumptions about the path that
// is generated by the glTF validator. Just print a
// warning for the case that this ever breaks.
console.warn(
"Could not extract " + topLevelName + " index from path: " + path
);
return false;
}
const isUsed = usedIndices.includes(index);
return isUsed;
}

/**
* Tries to extract the index of the top-level element that is
* indicated by the given JSON path.
*
* The given path is a JSON path that is generated by the glTF validator,
* for example
* "/images/4/mimeType"
*
* This function will omit the prefix, and (if present) the part
* starting with the subsequent slash, and return the resulting
* element ("4" in this example) as a number, if it is in fact
* a number.
*
* If any aspect of this parsing does not succeed, then <code>undefined</code>
* is returned. Yes, this could be solved with a 1.5MB "JSON path" dependency.
* Let's be pragmatic for now.
*
* @param topLevelName - The name of the top-level type (e.g. 'images'
* or 'accessors')
* @param path - The path as generated by the glTF validator
* @returns The index
*/
static extractIndex(topLevelName: string, path: string): number | undefined {
const prefix = "/" + topLevelName + "/";
if (!path.startsWith(prefix)) {
return undefined;
}
let s = path.substring(prefix.length);
const slashIndex = s.indexOf("/");
if (slashIndex !== -1) {
s = s.substring(0, slashIndex);
}
if (GltfExtensionIssues.isNonnegativeInteger(s)) {
return Number(s);
}
return undefined;
}

/**
* Returns whether the given string is likely a nonnegative integer.
*
* @param s - The string
* @returns The result
*/
private static isNonnegativeInteger(s: string): boolean {
return /^\d+$/.test(s);
}

/**
* Returns whether the given validation issue is an issue that just
* reports that the extension with the given name is not supported
* by the glTF validator.
*
* @param issue - The validation issue
* @param extensionName - The extension name
* @returns The result
*/
static isIssueAboutUnsupportedExtension(
issue: ValidationIssue,
extensionName: string
): boolean {
const extensionNameInMessage =
GltfExtensionIssues.extractUnsupportedExtensionName(issue.message);
return extensionNameInMessage === extensionName;
}

/**
* Tries to extract an extension name from the message that is generated by
* the glTF Validator when it encounters an extension that is not supported.
*
* This makes assumptions about the structure of the message, and that it
* is (verbatim) the message as it is generated by the glTF validator.
*
* @param message - The validation message
* @returns The unsupported extension name, or `undefined` if the message
* does not indicate an unsupported extension
*/
private static extractUnsupportedExtensionName(
message: string
): string | undefined {
// Example:
// "Cannot validate an extension as it is not supported by the validator: 'EXT_mesh_features'.",
const prefix =
"Cannot validate an extension as it is not supported by the validator: '";
if (message.startsWith(prefix)) {
const extensionName = message.substring(
prefix.length,
message.length - 2
);
return extensionName;
}
return undefined;
}
}
Loading