using System; using System.Collections.Generic; using System.Linq; using System.Text; using TARGET = SharpGLTF.IO.JsonSerializable; namespace SharpGLTF.Validation { /// /// Utility class used in the process of model validation. /// [System.Diagnostics.DebuggerStepThrough] public struct ValidationContext { #region constructor public ValidationContext(ValidationResult result, TARGET target) { _Result = result; _Target = target; } #endregion #region data private readonly TARGET _Target; private readonly ValidationResult _Result; #endregion #region properties public Schema2.ModelRoot Root => _Result.Root; public ValidationResult Result => _Result; public bool TryFix => Result.Mode == Validation.ValidationMode.TryFix; #endregion #region API public ValidationContext GetContext(TARGET target) { return _Result.GetContext(target); } public void AddSchemaError(ValueLocation location, string message) { AddSchemaError(location.ToString(_Target, message)); } public bool TryFixLinkOrError(ValueLocation location, string message) { if (TryFix) AddLinkWarning(location.ToString(_Target, message)); else AddLinkError(location.ToString(_Target, message)); return TryFix; } public bool TryFixDataOrError(ValueLocation location, string message) { if (TryFix) AddDataWarning(location.ToString(_Target, message)); else AddDataError(location.ToString(_Target, message)); return TryFix; } public void AddLinkError(ValueLocation location, string message) { AddLinkError(location.ToString(_Target, message)); } public void AddLinkWarning(String format, params object[] args) { AddLinkWarning(String.Format(format, args)); } public void AddDataError(ValueLocation location, string message) { AddDataError(location.ToString(_Target, message)); } public void AddDataWarning(ValueLocation location, string message) { AddDataWarning(location.ToString(_Target, message)); } public void AddSemanticWarning(String format, params object[] args) { AddSemanticWarning(String.Format(format, args)); } public void AddSemanticError(String format, params object[] args) { AddSemanticError(String.Format(format, args)); } public void AddLinkWarning(string message) { var ex = new LinkException(_Target, message); _Result.AddWarning(ex); } public void AddLinkError(string message) { var ex = new LinkException(_Target, message); _Result.AddError(ex); } public void AddSchemaError(string message) { var ex = new SchemaException(_Target, message); _Result.AddError(ex); } public void AddDataWarning(string message) { var ex = new DataException(_Target, message); _Result.AddWarning(ex); } public void AddDataError(string message) { var ex = new DataException(_Target, message); _Result.AddError(ex); } public void AddSemanticError(String message) { var ex = new SemanticException(_Target, message); _Result.AddError(ex); } public void AddSemanticWarning(String message) { var ex = new SemanticException(_Target, message); _Result.AddWarning(ex); } #endregion #region schema errors public bool CheckSchemaIsDefined(ValueLocation location, T value) where T : class { if (value != null) return true; AddSchemaError(location, "must be defined."); return false; } public bool CheckSchemaIsDefined(ValueLocation location, T? value) where T : struct { if (value.HasValue) return true; AddSchemaError(location, "must be defined."); return false; } public bool CheckSchemaNonNegative(ValueLocation location, int? value) { if ((value ?? 0) >= 0) return true; AddSchemaError(location, "must be a non-negative integer."); return false; } public void CheckSchemaIsInRange(ValueLocation location, T value, T minInclusive, T maxInclusive) where T : IComparable { if (value.CompareTo(minInclusive) == -1) AddSchemaError(location, $"is below minimum {minInclusive} value: {value}"); if (value.CompareTo(maxInclusive) == +1) AddSchemaError(location, $"is above maximum {maxInclusive} value: {value}"); } public void CheckSchemaIsMultipleOf(ValueLocation location, int value, int multiple) { if ((value % multiple) == 0) return; AddSchemaError(location, $"Value {value} is not a multiple of {multiple}."); } public void CheckSchemaIsJsonSerializable(ValueLocation location, Object value) { if (IO.JsonUtils.IsSerializable(value)) return; AddSchemaError(location, "Invalid JSON data."); } #pragma warning disable CA1054 // Uri parameters should not be strings public void CheckSchemaIsValidURI(ValueLocation location, string gltfURI) { if (string.IsNullOrEmpty(gltfURI)) return; if (gltfURI.StartsWith("data:", StringComparison.Ordinal)) { // check decoding return; } if (Uri.TryCreate(gltfURI, UriKind.Relative, out Uri xuri)) return; AddSchemaError(location, $"Invalid URI '{gltfURI}'."); } #pragma warning restore CA1054 // Uri parameters should not be strings #endregion #region semantic errors #endregion #region data errors public void CheckVertexIndex(ValueLocation location, UInt32 vertexIndex, UInt32 vertexCount, UInt32 vertexRestart) { if (vertexIndex == vertexRestart) { AddDataError(location, $"is a primitive restart value ({vertexIndex})"); return; } if (vertexIndex >= vertexCount) { AddDataError(location, $"has a value ({vertexIndex}) that exceeds number of available vertices ({vertexCount})"); return; } } public bool CheckIsFinite(ValueLocation location, System.Numerics.Vector2? value) { if (!value.HasValue) return true; if (value.Value._IsFinite()) return true; AddDataError(location, $"is NaN or Infinity."); return false; } public bool CheckIsFinite(ValueLocation location, System.Numerics.Vector3? value) { if (!value.HasValue) return true; if (value.Value._IsFinite()) return true; AddDataError(location, "is NaN or Infinity."); return false; } public bool CheckIsFinite(ValueLocation location, System.Numerics.Vector4? value) { if (!value.HasValue) return true; if (value.Value._IsFinite()) return true; AddDataError(location, "is NaN or Infinity."); return false; } public bool CheckIsFinite(ValueLocation location, System.Numerics.Quaternion? value) { if (!value.HasValue) return true; if (value.Value._IsFinite()) return true; AddDataError(location, "is NaN or Infinity."); return false; } public bool TryFixUnitLengthOrError(ValueLocation location, System.Numerics.Vector3? value) { if (!value.HasValue) return false; if (!CheckIsFinite(location, value)) return false; if (value.Value.IsValidNormal()) return false; return TryFixDataOrError(location, $"is not of unit length: {value.Value.Length()}."); } public bool TryFixTangentOrError(ValueLocation location, System.Numerics.Vector4 tangent) { if (TryFixUnitLengthOrError(location, new System.Numerics.Vector3(tangent.X, tangent.Y, tangent.Z))) return true; if (tangent.W == 1 || tangent.W == -1) return false; return TryFixDataOrError(location, $"has invalid value: {tangent.W}. Must be 1.0 or -1.0."); } public void CheckIsInRange(ValueLocation location, System.Numerics.Vector4 v, float minInclusive, float maxInclusive) { CheckIsInRange(location, v.X, minInclusive, maxInclusive); CheckIsInRange(location, v.Y, minInclusive, maxInclusive); CheckIsInRange(location, v.Z, minInclusive, maxInclusive); CheckIsInRange(location, v.W, minInclusive, maxInclusive); } public void CheckIsInRange(ValueLocation location, float value, float minInclusive, float maxInclusive) { if (value < minInclusive) AddDataError(location, $"is below minimum {minInclusive} value: {value}"); if (value > maxInclusive) AddDataError(location, $"is above maximum {maxInclusive} value: {value}"); } public bool CheckIsMatrix(ValueLocation location, System.Numerics.Matrix4x4? matrix) { if (matrix == null) return true; if (!matrix.Value._IsFinite()) { AddDataError(location, "is NaN or Infinity."); return false; } if (!System.Numerics.Matrix4x4.Decompose(matrix.Value, out System.Numerics.Vector3 s, out System.Numerics.Quaternion r, out System.Numerics.Vector3 t)) { AddDataError(location, "is not decomposable to TRS."); return false; } return true; } #endregion #region link errors public bool CheckArrayIndexAccess(ValueLocation location, int? index, IReadOnlyList array) { return CheckArrayRangeAccess(location, index, 1, array); } public bool CheckArrayRangeAccess(ValueLocation location, int? offset, int length, IReadOnlyList array) { if (!offset.HasValue) return true; if (!CheckSchemaNonNegative(location, offset)) return false; if (length <= 0) { AddSchemaError(location, "Invalid length"); return false; } if (array == null) { AddLinkError(location, $"Index {offset} exceeds the number of available items (null)."); return false; } if (offset > array.Count - length) { if (length == 1) AddLinkError(location, $"Index {offset} exceeds the number of available items ({array.Count})."); else AddLinkError(location, $"Index {offset}+{length} exceeds the number of available items ({array.Count})."); return false; } return true; } public bool CheckLinkMustBeAnyOf(ValueLocation location, T value, params T[] values) { if (values.Contains(value)) return true; var validValues = string.Join(" ", values); AddLinkError(location, $"value {value} is invalid. Must be one of {validValues}"); return false; } public bool CheckLinksInCollection(ValueLocation location, IEnumerable collection) where T : class { int idx = 0; if (collection == null) { AddLinkError(location, "Is NULL."); return false; } var uniqueInstances = new HashSet(); foreach (var v in collection) { if (v == null) { AddLinkError((location, idx), "Is NULL."); return false; } else if (uniqueInstances.Contains(v)) { AddSchemaError((location, idx), "Is duplicated."); return false; } uniqueInstances.Add(v); ++idx; } return true; } public void UnsupportedExtensionError(String message) { AddLinkError(message); } #endregion } public struct ValueLocation { public static implicit operator ValueLocation(int index) { return new ValueLocation(string.Empty, index); } public static implicit operator ValueLocation(int? index) { return new ValueLocation(string.Empty, index ?? 0); } public static implicit operator ValueLocation(string name) { return new ValueLocation(name); } public static implicit operator ValueLocation((string name, int index) tuple) { return new ValueLocation(tuple.name, tuple.index); } public static implicit operator ValueLocation((string name, int? index) tuple) { return new ValueLocation(tuple.name, tuple.index ?? 0); } public static implicit operator String(ValueLocation location) { return location.ToString(); } private ValueLocation(string name, int idx1 = -1) { _Name = name; _Index = idx1; } private readonly string _Name; private readonly int _Index; public override string ToString() { if (_Index >= 0) return $"{_Name}[{_Index}]"; return _Name; } public string ToString(TARGET target, string message) { return ToString(target) + " " + message; } public string ToString(TARGET target) { if (target == null) return this.ToString(); var name = target.GetType().Name; var pinfo = target.GetType().GetProperty("LogicalIndex"); if (pinfo != null) { var idx = pinfo.GetValue(target); name += $"[{idx}]"; } return name + this.ToString(); } } }