Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Schema.Test/GeneratedDataEditorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,22 @@ public void TestAMissingOrMisshapenValueThrows()
Assert.ThrowsExactly<InvalidOperationException>(() => SchemaDataValues.GetString(record, "Missing"));
}

[TestMethod]
public void TestANumberPastItsTypesRangeThrowsRatherThanReadingAsInfinity()
{
JsonObject record = new()
{
["Speed"] = JsonNode.Parse("1e39"),
["D"] = JsonNode.Parse("1e309"),
["Position"] = JsonNode.Parse("[1, 1e39, 2]"),
};

Assert.ThrowsExactly<InvalidOperationException>(() => SchemaDataValues.GetFloat(record, "Speed"));
Assert.ThrowsExactly<InvalidOperationException>(() => SchemaDataValues.GetDouble(record, "D"));
Assert.ThrowsExactly<InvalidOperationException>(() => SchemaDataValues.GetVector3(record, "Position"));
Assert.AreEqual(1e39, SchemaDataValues.GetDouble(record, "Speed"));
}

[TestMethod]
public void TestVectorsAndColoursRoundTrip()
{
Expand Down
56 changes: 56 additions & 0 deletions Schema.Test/SchemaDataValidatorTypeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,60 @@ public void ATypeWithNoStoredFormIsReportedAsUnchecked()
Assert.AreEqual(SchemaValidationSeverity.Warning, issues[0].Severity, type.DisplayName);
}
}

// ---- floating-point overflow -----------------------------------------------

[TestMethod]
[DataRow("1e39")]
[DataRow("-1e39")]
public void AFloatPastFloatsRangeIsAnError(string value)
{
Collection<SchemaValidationIssue> issues = Validate(new Float(), value);

Assert.HasCount(1, issues, string.Join("; ", issues));
Assert.AreEqual(SchemaValidationSeverity.Error, issues[0].Severity);
Assert.Contains("range of Float", issues[0].Message);
}

[TestMethod]
[DataRow("3.4028234663852886E+38")]
[DataRow("1e38")]
[DataRow("0")]
public void AFloatAtOrInsideFloatsRangeIsAccepted(string value) =>
Assert.HasCount(0, Validate(new Float(), value));

[TestMethod]
public void ADoublePastDoublesRangeIsAnError()
{
Assert.HasCount(1, Validate(new Double(), "1e309"));
Assert.HasCount(0, Validate(new Double(), "1e39"));
}

[TestMethod]
public void AFloatVectorComponentPastFloatsRangeIsAnError()
{
Collection<SchemaValidationIssue> issues = Validate(new Vector3(), "[1, 1e39, 2]");

Assert.HasCount(1, issues, string.Join("; ", issues));
Assert.AreEqual("data.Value[1]", issues[0].Path);
}

[TestMethod]
public void AColourComponentAndAQuantityPastFloatsRangeAreErrors()
{
Assert.HasCount(1, Validate(new ColorRGB(), "[0, 1e39, 0]"));
Assert.HasCount(1, Validate(Quantity("Mass"), "1e39"));
}

[TestMethod]
public void ASemanticTypeOverAFloatIsHeldToFloatsRange()
{
SchemaClass schemaClass = ClassWith((schema, c) =>
{
schema.AddSemanticType("Speed".As<SemanticTypeName>())!.SetUnderlyingType(new Float());
c.AddMember("Value".As<MemberName>())!.SetType(new Semantic { SemanticTypeName = "Speed".As<SemanticTypeName>() });
});

Assert.HasCount(1, SchemaDataValidator.ValidateData(schemaClass, """{ "Value": 1e39 }""", "data"));
}
}
21 changes: 19 additions & 2 deletions Schema/Models/SchemaDataValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ private static void ValidateValue(BaseType type, JsonElement value, string path,
break;

case Float or Double:
Expect(value, JsonValueKind.Number, path, type, issues);
ExpectNumber(value, type, path, type, issues);
break;

case Types.String:
Expand Down Expand Up @@ -399,7 +399,24 @@ private static void ExpectNumber(JsonElement value, BaseType storage, string pat
return;
}

Expect(value, JsonValueKind.Number, path, type, issues);
if (value.ValueKind != JsonValueKind.Number)
{
Expect(value, JsonValueKind.Number, path, type, issues);
return;
}

// A number past what the storage holds reads back as an infinity rather than failing, so
// "1e39" in a Float passes every reader and arrives as one. Refuse it here, as the
// integral types already are and as schema validation already refuses such a default.
bool finite = value.TryGetDouble(out double number) && double.IsFinite(number);
if (represented is Float && !(finite && Math.Abs(number) <= float.MaxValue))
{
issues.Add(Error(path, $"Expected a number within the range of Float (±{float.MaxValue.ToString("R", CultureInfo.InvariantCulture)}) for type '{type.DisplayName}' but found {value.GetRawText()}."));
}
else if (represented is Double && !finite)
{
issues.Add(Error(path, $"Expected a number within the range of Double (±{double.MaxValue.ToString("R", CultureInfo.InvariantCulture)}) for type '{type.DisplayName}' but found {value.GetRawText()}."));
}
}

private static void ValidateEnumValue(Types.Enum enumType, JsonElement value, string path, Collection<SchemaValidationIssue> issues)
Expand Down
7 changes: 4 additions & 3 deletions Schema/Runtime/SchemaDataValues.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ public static long GetLong(JsonObject record, string name) =>
/// <param name="name">The member's declared name.</param>
/// <returns>The value.</returns>
public static float GetFloat(JsonObject record, string name) =>
ReadNumber(record, name, static (JsonElement e, out float v) => e.TryGetSingle(out v));
ReadNumber(record, name, static (JsonElement e, out float v) => e.TryGetSingle(out v) && float.IsFinite(v));

/// <summary>Writes a <c>Float</c> member.</summary>
/// <param name="record">The record.</param>
Expand All @@ -128,7 +128,7 @@ public static float GetFloat(JsonObject record, string name) =>
/// <param name="name">The member's declared name.</param>
/// <returns>The value.</returns>
public static double GetDouble(JsonObject record, string name) =>
ReadNumber(record, name, static (JsonElement e, out double v) => e.TryGetDouble(out v));
ReadNumber(record, name, static (JsonElement e, out double v) => e.TryGetDouble(out v) && double.IsFinite(v));

/// <summary>Writes a <c>Double</c> member.</summary>
/// <param name="record">The record.</param>
Expand Down Expand Up @@ -342,7 +342,8 @@ private static float[] GetComponents(JsonObject record, string name, int count)
{
JsonNode? component = array[i];
if (component?.GetValueKind() != JsonValueKind.Number
|| !JsonSerializer.SerializeToElement(component).TryGetSingle(out components[i]))
|| !JsonSerializer.SerializeToElement(component).TryGetSingle(out components[i])
|| !float.IsFinite(components[i]))
{
throw Malformed(name, $"component {i} is not a number");
}
Expand Down
Loading