Skip to content
Draft
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
211 changes: 211 additions & 0 deletions Ubytec.Tests/HighLevelParserDeclarationDispatchTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
using Ubytec.Language.AST;
using Xunit;
using static Ubytec.Language.Syntax.TypeSystem.Types;

namespace Ubytec.Tests;

public class HighLevelParserDeclarationDispatchTests
{
[Fact]
public void FuncAndActionDeclarationsParseAcrossScopes()
{
const string source = """
module (name:"parser-entry", version:"0.1", author:"Ubytec")
{
func ModuleFunc()
{
return
}

action ModuleAction()
{
return
}

global
{
func GlobalFunc()
{
return
}

action GlobalAction()
{
return
}
}

local
{
func LocalFunc()
{
return
}

action LocalAction()
{
return
}
}

class Holder
{
func ClassFunc()
{
return
}

action ClassAction()
{
return
}
}
}
""";

var module = SourceCompiler.CompileSemantic(source, processImage: false).Module;

Assert.Equal("ModuleFunc", Assert.Single(module.Functions).Name);
Assert.Equal("ModuleAction", Assert.Single(module.Actions).Name);

Assert.True(module.GlobalContext.HasValue);
Assert.Equal("GlobalFunc", Assert.Single(module.GlobalContext.Value.Functions).Name);
Assert.Equal("GlobalAction", Assert.Single(module.GlobalContext.Value.Actions).Name);

Assert.True(module.LocalContext.HasValue);
Assert.Equal("LocalFunc", Assert.Single(module.LocalContext.Value.Functions).Name);
Assert.Equal("LocalAction", Assert.Single(module.LocalContext.Value.Actions).Name);

var holder = Assert.Single(module.Classes);
Assert.Equal("Holder", holder.Name);
Assert.Equal("ClassFunc", Assert.Single(holder.Functions).Name);
Assert.Equal("ClassAction", Assert.Single(holder.Actions).Name);
}

[Fact]
public void DispatcherConsumesKeywordsAndPreservesModifiers()
{
const string source = """
module (name:"parser-modifiers", version:"0.1", author:"Ubytec")
{
public func Visible()
{
return
}

private action Hidden()
{
return
}

public class Holder
{
protected func Member()
{
return
}
}
}
""";

var module = SourceCompiler.CompileSemantic(source, processImage: false).Module;

var visible = Assert.Single(module.Functions);
Assert.Equal("Visible", visible.Name);
Assert.True(visible.Modifiers.HasFlag(TypeModifiers.Public));

var hidden = Assert.Single(module.Actions);
Assert.Equal("Hidden", hidden.Name);
Assert.True(hidden.Modifiers.HasFlag(TypeModifiers.Private));

var holder = Assert.Single(module.Classes);
Assert.True(holder.Modifiers.HasFlag(TypeModifiers.Public));
var member = Assert.Single(holder.Functions);
Assert.True(member.Modifiers.HasFlag(TypeModifiers.Protected));
}

[Fact]
public void RecordFunctionUsesDeclarationKeywordRatherThanCallKeyword()
{
const string source = """
module (name:"parser-record", version:"0.1", author:"Ubytec")
{
record Packet
{
func Decode()
{
return
}

action Mark()
{
return
}
}
}
""";

var module = SourceCompiler.CompileSemantic(source, processImage: false).Module;

var record = Assert.Single(module.Records);
Assert.Equal("Packet", record.Name);
Assert.Equal("Decode", Assert.Single(record.Functions).Name);
Assert.Equal("Mark", Assert.Single(record.Actions).Name);
}

[Fact]
public void KeywordDispatchParsesFieldStructEnumAndNestedTypeDeclarations()
{
const string source = """
module (name:"parser-declarations", version:"0.1", author:"Ubytec")
{
field t_uint64 Counter

struct Pair
{
field t_uint64 Left
field t_uint64 Right

func Touch()
{
return
}
}

enum State
{
Idle,
Ready
}

class Outer
{
class Inner
{
func Run()
{
return
}
}
}
}
""";

var module = SourceCompiler.CompileSemantic(source, processImage: false).Module;

Assert.Equal("Counter", Assert.Single(module.Fields).Name);

var pair = Assert.Single(module.Structs);
Assert.Equal("Pair", pair.Name);
Assert.Equal(2, pair.Fields.Length);
Assert.Equal("Touch", Assert.Single(pair.Functions).Name);

var state = Assert.Single(module.Enums);
Assert.Equal("State", state.Name);

var outer = Assert.Single(module.Classes);
var inner = Assert.Single(outer.Classes);
Assert.Equal("Inner", inner.Name);
Assert.Equal("Run", Assert.Single(inner.Functions).Name);
}
}
80 changes: 80 additions & 0 deletions Ubytec.Tests/LexicalParserContractTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using Ubytec.Language.AST;
using Xunit;
using static Ubytec.Language.Syntax.TypeSystem.Types;

namespace Ubytec.Tests;

public class LexicalParserContractTests
{
[Fact]
public void MalformedDeclarationFailsAtFirstRequiredToken()
{
const string source = """
module (name:"strict-consume", version:"0.1", author:"Ubytec")
{
func
{
}
}
""";

var error = Assert.Throws<FormatException>(() =>
SourceCompiler.CompileSemantic(source, processImage: false));

Assert.Contains("Expected", error.Message, StringComparison.Ordinal);
Assert.Contains("found '{'", error.Message, StringComparison.Ordinal);
}

[Fact]
public void FieldNamesAreRecognizedAfterEverySupportedNullableArrayShape()
{
const string source = """
module (name:"typed-names", version:"0.1", author:"Ubytec")
{
field t_int32 Scalar
field t_int32? NullableScalar
field [t_int32] Array
field [t_int32]? NullableArray
field [t_int32?] NullableItems
field [t_int32?]? NullableArrayAndItems
}
""";

var module = SourceCompiler.CompileSemantic(source, processImage: false).Module;

Assert.Equal(
["Scalar", "NullableScalar", "Array", "NullableArray", "NullableItems", "NullableArrayAndItems"],
module.Fields.Select(field => field.Name).ToArray());
}

[Fact]
public void GlobalContextAndGlobalModifierRemainDistinctProductions()
{
const string source = """
module (name:"global-dispatch", version:"0.1", author:"Ubytec")
{
global func Flagged()
{
return
}

global
{
func Contextual()
{
return
}
}
}
""";

var module = SourceCompiler.CompileSemantic(source, processImage: false).Module;

var flagged = Assert.Single(module.Functions);
Assert.Equal("Flagged", flagged.Name);
Assert.True((flagged.Modifiers & TypeModifiers.Global) != 0);

var globalContext = Assert.IsType<Ubytec.Language.HighLevel.GlobalContext>(module.GlobalContext);
Assert.Equal("Contextual", Assert.Single(globalContext.Functions).Name);
}
}
73 changes: 73 additions & 0 deletions Ubytec.Tests/PortableBinaryVerifierFrameAttackTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System.Buffers.Binary;
using Ubytec.Language.AST;
using Ubytec.Language.Portable;
using Xunit;

namespace Ubytec.Tests;

public class PortableBinaryVerifierFrameAttackTests
{
[Fact]
public void IndependentVerifierRejectsVarRetargetedFromLocalToArgument()
{
const string source = """
module (name:"binary-var-attack", version:"0.1", author:"Ubytec")
{
func Main(t_int64 value) -> t_int64
{
local
{
t_int64 scratch 0
}
load @value
return
}
}
""";

SourceSemanticCompilation compilation = SourceCompiler.CompileSemantic(
source,
processImage: false);
byte[] bytes = PortableModuleFormat.Encode(
compilation.Module,
contractManifest: null,
includeIndexedExecutable: true,
portableContracts: compilation.PortableContracts);

var (codeOffset, codeLength) = FindSection(bytes, PortableModuleSectionKind.IndexedCode);
Span<byte> code = bytes.AsSpan(codeOffset, codeLength);

Assert.True(code.Length >= 13, "Expected canonical local VAR initialization.");
Assert.Equal(0x10, code[0]);
Assert.Equal(1u, BinaryPrimitives.ReadUInt32LittleEndian(code.Slice(1, 4)));

// Retarget the canonical local initialization to BindingIndex 0, which is the argument.
// The source frontend never produced this program; only the raw UBCM bytes are malicious.
BinaryPrimitives.WriteUInt32LittleEndian(code.Slice(1, 4), 0);

InvalidDataException error = Assert.Throws<InvalidDataException>(() =>
PortableBinaryVerifier.Verify(bytes));

Assert.Contains("VAR cannot initialize an argument", error.Message, StringComparison.Ordinal);
}

private static (int Offset, int Length) FindSection(
byte[] bytes,
PortableModuleSectionKind target)
{
uint count = BinaryPrimitives.ReadUInt32LittleEndian(bytes.AsSpan(8, 4));
int cursor = 12;
for (uint index = 0; index < count; index++)
{
ushort kind = BinaryPrimitives.ReadUInt16LittleEndian(bytes.AsSpan(cursor, 2));
ulong rawLength = BinaryPrimitives.ReadUInt64LittleEndian(bytes.AsSpan(cursor + 4, 8));
cursor += 12;
int length = checked((int)rawLength);
if (kind == (ushort)target)
return (cursor, length);
cursor += length;
}

throw new InvalidOperationException($"Section {target} was not found.");
}
}
Loading
Loading