forked from dotnet/runtime
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add CBOR property-based tests (dotnet#39828)
* Add CBOR property-based tests * address feedback
- Loading branch information
1 parent
2b2eb5a
commit efd5565
Showing
10 changed files
with
673 additions
and
182 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
51 changes: 51 additions & 0 deletions
51
src/libraries/System.Formats.Cbor/tests/CborDocument/CborDocument.fs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
namespace System.Formats.Cbor.Tests.DataModel | ||
|
||
open System | ||
|
||
// -- | ||
// Models a CBOR document type using a discriminated union. | ||
// Discriminated unions have structural equality for free | ||
// and random instances can be generated using FsCheck. | ||
// | ||
// The type and serialization helpers are used by the | ||
// property tests in the main test project. | ||
// | ||
|
||
type CborDocument = | ||
// Major type 0 | ||
| UnsignedInteger of uint64 | ||
// Major type 1 | ||
| NegativeInteger of uint64 | ||
// Major type 2 | ||
| ByteString of byte[] | ||
| ByteStringIndefiniteLength of byte[][] | ||
// Major type 3 | ||
| TextString of string | ||
| TextStringIndefiniteLength of string[] | ||
// Major type 4 | ||
| Array of isDefiniteLength:bool * CborDocument[] | ||
// Major type 5 | ||
| Map of isDefiniteLength:bool * Map<CborDocument, CborDocument> | ||
// Major type 6 | ||
| Tag of tag:uint64 * CborDocument | ||
| SemanticValue of CborSemanticValue | ||
// Major type 7 | ||
| Double of double | ||
| SimpleValue of value:byte | ||
|
||
// Defines a set of semantic values drawing either from the CBOR spec | ||
// or invented here for the purposes of this test. | ||
and CborSemanticValue = | ||
| Null | ||
| Bool of bool | ||
| Int32 of int | ||
| Int64 of int64 | ||
| Half of Half | ||
| Single of single | ||
| DateTimeOffset of DateTimeOffset | ||
| UnixTimeSeconds of seconds:int64 | ||
| BigInt of bigint | ||
| Decimal of decimal |
185 changes: 185 additions & 0 deletions
185
src/libraries/System.Formats.Cbor/tests/CborDocument/CborDocumentSerializer.fs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
module rec System.Formats.Cbor.Tests.DataModel.CborDocumentSerializer | ||
|
||
open System | ||
open System.Formats.Cbor | ||
open System.Formats.Cbor.Tests.DataModel | ||
|
||
let createWriter (context : CborPropertyTestContext) = | ||
new CborWriter( | ||
conformanceMode = context.ConformanceMode, | ||
convertIndefiniteLengthEncodings = context.ConvertIndefiniteLengthItems, | ||
allowMultipleRootLevelValues = (context.RootDocuments.Length > 1)) | ||
|
||
let createReader (context : CborPropertyTestContext) (encoding : byte[]) = | ||
new CborReader( | ||
data = ReadOnlyMemory<byte>.op_Implicit encoding, | ||
conformanceMode = context.ConformanceMode, | ||
allowMultipleRootLevelValues = (context.RootDocuments.Length > 1)) | ||
|
||
let encode (context : CborPropertyTestContext) = | ||
let writer = createWriter context | ||
for doc in context.RootDocuments do | ||
write writer doc | ||
writer.Encode() | ||
|
||
let decode (context : CborPropertyTestContext) (data : byte[]) = | ||
let reader = createReader context data | ||
let values = new System.Collections.Generic.List<_>() | ||
while reader.PeekState() <> CborReaderState.Finished do | ||
values.Add(read reader) | ||
values.ToArray() | ||
|
||
let rec write (writer : CborWriter) (doc : CborDocument) = | ||
match doc with | ||
| UnsignedInteger i -> writer.WriteUInt64 i | ||
| NegativeInteger i -> writer.WriteCborNegativeIntegerRepresentation i | ||
| ByteString bs -> writer.WriteByteString bs | ||
| TextString ts -> writer.WriteTextString ts | ||
| ByteStringIndefiniteLength bss -> | ||
writer.WriteStartIndefiniteLengthByteString() | ||
for bs in bss do | ||
writer.WriteByteString bs | ||
writer.WriteEndIndefiniteLengthByteString() | ||
|
||
| TextStringIndefiniteLength tss -> | ||
writer.WriteStartIndefiniteLengthTextString() | ||
for ts in tss do | ||
writer.WriteTextString ts | ||
writer.WriteEndIndefiniteLengthTextString() | ||
|
||
| Array (isDefiniteLength, elements) -> | ||
writer.WriteStartArray (if isDefiniteLength then Nullable elements.Length else Nullable()) | ||
for elem in elements do | ||
write writer elem | ||
writer.WriteEndArray() | ||
|
||
| Map (isDefiniteLength, pairs) -> | ||
writer.WriteStartMap (if isDefiniteLength then Nullable pairs.Count else Nullable()) | ||
for kv in pairs do | ||
write writer kv.Key ; write writer kv.Value | ||
writer.WriteEndMap() | ||
|
||
| Tag (tag, nested) -> | ||
writer.WriteTag (LanguagePrimitives.EnumOfValue tag) | ||
write writer nested | ||
|
||
| Double d -> writer.WriteDouble d | ||
| SimpleValue v -> writer.WriteSimpleValue(LanguagePrimitives.EnumOfValue v) | ||
|
||
| SemanticValue value -> | ||
writer.WriteTag CborTagExt.SemValueGuard | ||
writeSemanticValue writer value | ||
|
||
let private writeSemanticValue (writer : CborWriter) (s : CborSemanticValue) = | ||
match s with | ||
| Null -> writer.WriteTag CborTagExt.Null; writer.WriteNull() | ||
| Bool b -> writer.WriteTag CborTagExt.Bool; writer.WriteBoolean b | ||
| Int32 i -> writer.WriteTag CborTagExt.Int32; writer.WriteInt32 i | ||
| Int64 i -> writer.WriteTag CborTagExt.Int64; writer.WriteInt64 i | ||
| Half f -> writer.WriteTag CborTagExt.Half; writer.WriteHalf f | ||
| Single f -> writer.WriteTag CborTagExt.Single; writer.WriteSingle f | ||
| DateTimeOffset d -> writer.WriteDateTimeOffset d | ||
| UnixTimeSeconds s -> writer.WriteUnixTimeSeconds s | ||
| BigInt i -> writer.WriteBigInteger i | ||
| Decimal d -> writer.WriteDecimal d | ||
|
||
let rec read (reader : CborReader) : CborDocument = | ||
match reader.PeekState() with | ||
| CborReaderState.UnsignedInteger -> UnsignedInteger(reader.ReadUInt64()) | ||
| CborReaderState.NegativeInteger -> NegativeInteger(reader.ReadCborNegativeIntegerRepresentation()) | ||
| CborReaderState.ByteString -> ByteString(reader.ReadByteString()) | ||
| CborReaderState.TextString -> TextString(reader.ReadTextString()) | ||
| CborReaderState.StartArray -> | ||
let length = reader.ReadStartArray() | ||
if length.HasValue then | ||
let results = Array.zeroCreate<CborDocument> length.Value | ||
for i = 0 to results.Length - 1 do | ||
results.[i] <- read reader | ||
|
||
reader.ReadEndArray() | ||
Array(true, results) | ||
else | ||
let results = new System.Collections.Generic.List<_>() | ||
while reader.PeekState() <> CborReaderState.EndArray do | ||
results.Add(read reader) | ||
|
||
reader.ReadEndArray() | ||
Array(false, results.ToArray()) | ||
|
||
| CborReaderState.StartIndefiniteLengthByteString -> | ||
reader.ReadStartIndefiniteLengthByteString() | ||
let chunks = new System.Collections.Generic.List<byte[]>() | ||
while reader.PeekState() <> CborReaderState.EndIndefiniteLengthByteString do | ||
chunks.Add(reader.ReadByteString()) | ||
reader.ReadEndIndefiniteLengthByteString() | ||
ByteStringIndefiniteLength(chunks.ToArray()) | ||
|
||
| CborReaderState.StartIndefiniteLengthTextString -> | ||
reader.ReadStartIndefiniteLengthTextString() | ||
let chunks = new System.Collections.Generic.List<string>() | ||
while reader.PeekState() <> CborReaderState.EndIndefiniteLengthTextString do | ||
chunks.Add(reader.ReadTextString()) | ||
reader.ReadEndIndefiniteLengthTextString() | ||
TextStringIndefiniteLength(chunks.ToArray()) | ||
|
||
| CborReaderState.StartMap -> | ||
let length = reader.ReadStartMap() | ||
if length.HasValue then | ||
let results = Array.zeroCreate<CborDocument * CborDocument> length.Value | ||
for i = 0 to results.Length - 1 do | ||
results.[i] <- (read reader, read reader) | ||
reader.ReadEndMap() | ||
Map(true, Map.ofArray results) | ||
else | ||
let results = new System.Collections.Generic.List<_>() | ||
while reader.PeekState() <> CborReaderState.EndMap do | ||
results.Add(read reader, read reader) | ||
reader.ReadEndMap() | ||
Map(false, Map.ofSeq results) | ||
|
||
| CborReaderState.Tag -> | ||
let tag = reader.ReadTag() | ||
if tag = CborTagExt.SemValueGuard then SemanticValue(readSemanticValue reader) | ||
else Tag (LanguagePrimitives.EnumToValue tag, read reader) | ||
|
||
| CborReaderState.HalfPrecisionFloat | ||
| CborReaderState.SinglePrecisionFloat | ||
| CborReaderState.DoublePrecisionFloat -> Double (reader.ReadDouble()) | ||
|
||
| CborReaderState.Null | ||
| CborReaderState.Boolean | ||
| CborReaderState.SimpleValue -> | ||
let value = reader.ReadSimpleValue() | ||
SimpleValue(LanguagePrimitives.EnumToValue value) | ||
|
||
| state -> failwithf "Unrecognized reader state %O" state | ||
|
||
let private readSemanticValue (reader : CborReader) : CborSemanticValue = | ||
match reader.PeekTag() with | ||
| CborTagExt.Null -> let _ = reader.ReadTag() in reader.ReadNull(); Null | ||
| CborTagExt.Bool -> let _ = reader.ReadTag() in Bool(reader.ReadBoolean()) | ||
| CborTagExt.Int32 -> let _ = reader.ReadTag() in Int32(reader.ReadInt32()) | ||
| CborTagExt.Int64 -> let _ = reader.ReadTag() in Int64(reader.ReadInt64()) | ||
| CborTagExt.Half -> let _ = reader.ReadTag() in Half(reader.ReadHalf()) | ||
| CborTagExt.Single -> let _ = reader.ReadTag() in Single(reader.ReadSingle()) | ||
| CborTag.DateTimeString -> DateTimeOffset(reader.ReadDateTimeOffset()) | ||
| CborTag.UnixTimeSeconds -> let dto = reader.ReadUnixTimeSeconds() in UnixTimeSeconds(dto.ToUnixTimeSeconds()) | ||
| CborTag.UnsignedBigNum | ||
| CborTag.NegativeBigNum -> BigInt(reader.ReadBigInteger()) | ||
| CborTag.DecimalFraction -> Decimal(reader.ReadDecimal()) | ||
| tag -> failwithf "Unrecognized tag %O" tag | ||
|
||
// defines a set of custom CBOR tags for semantic value encodings | ||
module private CborTagExt = | ||
|
||
let [<Literal>] SemValueGuard : CborTag = LanguagePrimitives.EnumOfValue 50015001uL | ||
|
||
let [<Literal>] Null : CborTag = LanguagePrimitives.EnumOfValue 5001uL | ||
let [<Literal>] Bool : CborTag = LanguagePrimitives.EnumOfValue 5002uL | ||
let [<Literal>] Int32 : CborTag = LanguagePrimitives.EnumOfValue 5003uL | ||
let [<Literal>] Int64 : CborTag = LanguagePrimitives.EnumOfValue 5004uL | ||
let [<Literal>] Half : CborTag = LanguagePrimitives.EnumOfValue 5005uL | ||
let [<Literal>] Single : CborTag = LanguagePrimitives.EnumOfValue 5006uL |
106 changes: 106 additions & 0 deletions
106
src/libraries/System.Formats.Cbor/tests/CborDocument/CborPropertyTestContext.fs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
namespace System.Formats.Cbor.Tests.DataModel | ||
|
||
open System.Formats.Cbor | ||
open System.Formats.Cbor.Tests.DataModel | ||
|
||
/// Randomly generated record containing parameters for a CBOR property-based test | ||
[<CLIMutable>] | ||
type CborPropertyTestContext = | ||
{ | ||
RootDocuments : CborDocument[] | ||
ConformanceMode : CborConformanceMode | ||
ConvertIndefiniteLengthItems : bool | ||
} | ||
|
||
module rec CborPropertyTestContextHelper = | ||
|
||
/// Identifies & transforms documents that might not be accepted under the supplied conformance mode | ||
let create (mode : CborConformanceMode) (convertIndefiniteLengthItems : bool) (docs : CborDocument[]) = | ||
{ | ||
RootDocuments = Array.map (normalize mode convertIndefiniteLengthItems) docs | ||
ConvertIndefiniteLengthItems = convertIndefiniteLengthItems | ||
ConformanceMode = mode | ||
} | ||
|
||
/// Gets the expected value that we would expected to see after a full serialization/deserialization roundtrip | ||
let getExpectedRoundtripValues (context : CborPropertyTestContext) = | ||
Array.map (getExpectedRoundtripValue context.ConvertIndefiniteLengthItems) context.RootDocuments | ||
|
||
/// Identifies & transforms documents that might not be accepted under the supplied conformance mode | ||
let private normalize (mode : CborConformanceMode) (convertIndefiniteLengthItems : bool) (doc : CborDocument) = | ||
// normalization can lead to collisions in conformance modes that require field uniqueness | ||
// mitigate by wrapping with a unique CBOR node | ||
let mutable counter = 19000uL | ||
let createUniqueWrapper (doc : CborDocument) = | ||
counter <- counter + 1uL | ||
Array(true, [|UnsignedInteger counter; doc|]) | ||
|
||
// not accepted by strict & canonical conformance modes | ||
let trimNonCanonicalSimpleValue doc = | ||
match doc with | ||
| SimpleValue value when value >= 24uy && value < 32uy -> createUniqueWrapper(SimpleValue 0uy) | ||
| _ -> doc | ||
|
||
// completely replace indefinite-length nodes in canonical conformance modes | ||
let trimIndefiniteLengthNode doc = | ||
match doc with | ||
| ByteStringIndefiniteLength bss -> createUniqueWrapper(ByteString (Array.concat bss)) | ||
| TextStringIndefiniteLength tss -> createUniqueWrapper(TextString (String.concat "" tss)) | ||
| Array(false, elems) -> createUniqueWrapper(Array(true, elems)) | ||
| Map(false, fields) -> createUniqueWrapper(Map(true, fields)) | ||
| _ -> doc | ||
|
||
// CTAP2 does not allow major type 6, at all | ||
let trimTagNode doc = | ||
match doc with | ||
| Tag (tag, doc) -> Array(true, [| UnsignedInteger (uint64 tag) ; doc|]) | ||
| SemanticValue _ -> createUniqueWrapper(UnsignedInteger 0uL) | ||
| _ -> doc | ||
|
||
match mode with | ||
| CborConformanceMode.Lax -> doc | ||
| CborConformanceMode.Strict | ||
| CborConformanceMode.Canonical -> map (trimNonCanonicalSimpleValue << trimIndefiniteLengthNode) doc | ||
| CborConformanceMode.Ctap2Canonical -> map (trimNonCanonicalSimpleValue << trimIndefiniteLengthNode << trimTagNode) doc | ||
| _ -> doc | ||
|
||
/// Gets the expected value that we would expected to see after a full serialization/deserialization roundtrip | ||
let private getExpectedRoundtripValue (convertIndefiniteLengthItems : bool) (doc : CborDocument) = | ||
if not convertIndefiniteLengthItems then doc else | ||
|
||
let trimIndefiniteLengthNode doc = | ||
match doc with | ||
| ByteStringIndefiniteLength bss -> ByteString (Array.concat bss) | ||
| TextStringIndefiniteLength tss -> TextString (String.concat "" tss) | ||
| Array(false, elems) -> Array(true, elems) | ||
| Map(false, fields) -> Map(true, fields) | ||
| _ -> doc | ||
|
||
map trimIndefiniteLengthNode doc | ||
|
||
/// Depth-first application of update function on a CBOR doc tree | ||
let rec private map (updater : CborDocument -> CborDocument) (doc : CborDocument) = | ||
match updater doc with | ||
| UnsignedInteger _ | ||
| NegativeInteger _ | ||
| ByteString _ | ||
| ByteStringIndefiniteLength _ | ||
| TextString _ | ||
| TextStringIndefiniteLength _ | ||
| SemanticValue _ | ||
| SimpleValue _ | ||
| Double _ as updated -> updated | ||
|
||
| Tag(tag, value) -> Tag(tag, map updater value) | ||
| Array(isDefiniteLength, elems) -> Array(isDefiniteLength, Array.map (map updater) elems) | ||
| Map(isDefiniteLength, fields) -> | ||
let updatedFields = | ||
fields | ||
|> Map.toSeq | ||
|> Seq.map (fun (k,v) -> map updater k, map updater v) | ||
|> Map.ofSeq | ||
|
||
Map(isDefiniteLength, updatedFields) |
16 changes: 16 additions & 0 deletions
16
...braries/System.Formats.Cbor/tests/CborDocument/System.Formats.Cbor.Tests.DataModel.fsproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFrameworks>$(NetCoreAppCurrent)</TargetFrameworks> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<Compile Include="CborDocument.fs" /> | ||
<Compile Include="CborPropertyTestContext.fs" /> | ||
<Compile Include="CborDocumentSerializer.fs" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\..\src\System.Formats.Cbor.csproj" /> | ||
</ItemGroup> | ||
</Project> |
Oops, something went wrong.