Initial commit

This commit is contained in:
2026-07-03 16:37:12 +07:00
commit 63b8c1ea8b
1931 changed files with 640587 additions and 0 deletions

View File

@@ -0,0 +1,443 @@
using BlazorMonaco.Languages;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.QuickInfo;
using Microsoft.CodeAnalysis.Tags;
using Microsoft.CodeAnalysis.Text;
using System.Collections.Immutable;
using System.Text;
using System.Text.RegularExpressions;
namespace RobotNet10.ScriptEditor.Helpers;
public static partial class AdhocWorkspaceHelper
{
[GeneratedRegex(@"<summary>\s*(.+?)\s*</summary>", RegexOptions.Singleline)]
private static partial Regex SummaryRegex();
[GeneratedRegex(@"\s+")]
private static partial Regex WhitespaceRegex();
private const int TriggerKind_Invoke = 1;
private const int TriggerKind_TriggerCharacter = 2;
private const int TriggerKind_TriggerForIncompleteCompletions = 3;
private static readonly Dictionary<string, CompletionItemKind> s_roslynTagToCompletionItemKind = new()
{
{ WellKnownTags.Public, CompletionItemKind.Keyword },
{ WellKnownTags.Protected, CompletionItemKind.Keyword },
{ WellKnownTags.Private, CompletionItemKind.Keyword },
{ WellKnownTags.Internal, CompletionItemKind.Keyword },
{ WellKnownTags.File, CompletionItemKind.File },
{ WellKnownTags.Project, CompletionItemKind.File },
{ WellKnownTags.Folder, CompletionItemKind.Folder },
{ WellKnownTags.Assembly, CompletionItemKind.File },
{ WellKnownTags.Class, CompletionItemKind.Class },
{ WellKnownTags.Constant, CompletionItemKind.Constant },
{ WellKnownTags.Delegate, CompletionItemKind.Function },
{ WellKnownTags.Enum, CompletionItemKind.Enum },
{ WellKnownTags.EnumMember, CompletionItemKind.EnumMember },
{ WellKnownTags.Event, CompletionItemKind.Event },
{ WellKnownTags.ExtensionMethod, CompletionItemKind.Method },
{ WellKnownTags.Field, CompletionItemKind.Field },
{ WellKnownTags.Interface, CompletionItemKind.Interface },
{ WellKnownTags.Intrinsic, CompletionItemKind.Text },
{ WellKnownTags.Keyword, CompletionItemKind.Keyword },
{ WellKnownTags.Label, CompletionItemKind.Text },
{ WellKnownTags.Local, CompletionItemKind.Variable },
{ WellKnownTags.Namespace, CompletionItemKind.Module },
{ WellKnownTags.Method, CompletionItemKind.Method },
{ WellKnownTags.Module, CompletionItemKind.Module },
{ WellKnownTags.Operator, CompletionItemKind.Operator },
{ WellKnownTags.Parameter, CompletionItemKind.Value },
{ WellKnownTags.Property, CompletionItemKind.Property },
{ WellKnownTags.RangeVariable, CompletionItemKind.Variable },
{ WellKnownTags.Reference, CompletionItemKind.Reference },
{ WellKnownTags.Structure, CompletionItemKind.Struct },
{ WellKnownTags.TypeParameter, CompletionItemKind.TypeParameter },
{ WellKnownTags.Snippet, CompletionItemKind.Snippet },
{ WellKnownTags.Error, CompletionItemKind.Text },
{ WellKnownTags.Warning, CompletionItemKind.Text },
};
private static CompletionTrigger GetCompletionTrigger(int kind, char? triggerCharacter, bool includeTriggerCharacter)
=> kind switch
{
TriggerKind_Invoke => CompletionTrigger.Invoke,
TriggerKind_TriggerCharacter when includeTriggerCharacter && triggerCharacter.HasValue
=> CompletionTrigger.CreateInsertionTrigger(triggerCharacter.Value),
_ => CompletionTrigger.Invoke,
};
private static ImmutableArray<char> BuildCommitCharacters(
Microsoft.CodeAnalysis.Completion.CompletionList completions,
ImmutableArray<CharacterSetModificationRule> characterRules,
ImmutableArray<char>.Builder triggerCharactersBuilder)
{
if (completions is null) return [];
triggerCharactersBuilder.Clear();
triggerCharactersBuilder.AddRange(completions.Rules.DefaultCommitCharacters);
foreach (var modifiedRule in characterRules)
{
switch (modifiedRule.Kind)
{
case CharacterSetModificationKind.Add:
triggerCharactersBuilder.AddRange(modifiedRule.Characters);
break;
case CharacterSetModificationKind.Remove:
for (int i = triggerCharactersBuilder.Count - 1; i >= 0; i--)
{
if (modifiedRule.Characters.Contains(triggerCharactersBuilder[i]))
{
triggerCharactersBuilder.RemoveAt(i);
}
}
break;
case CharacterSetModificationKind.Replace:
triggerCharactersBuilder.Clear();
triggerCharactersBuilder.AddRange(modifiedRule.Characters);
break;
}
}
if (completions.SuggestionModeItem is not null)
{
triggerCharactersBuilder.Remove(' ');
}
return triggerCharactersBuilder.ToImmutable();
}
private static CompletionItemKind GetCompletionItemKind(ImmutableArray<string> tags)
{
foreach (var tag in tags)
{
if (s_roslynTagToCompletionItemKind.TryGetValue(tag, out var itemKind))
{
return itemKind;
}
}
return CompletionItemKind.Text;
}
public static async Task<IEnumerable<BlazorMonaco.Languages.CompletionItem>> GetCompletionAsync(
this AdhocWorkspace workspace,
DocumentId documentId,
int line,
int column,
int kind,
char? triggerCharacter)
{
if (triggerCharacter == ' ') return [];
var document = workspace.CurrentSolution.GetDocument(documentId);
if (document is null) return [];
var sourceText = await document.GetTextAsync();
if (line < 0 || line >= sourceText.Lines.Count)
return [];
var lineObj = sourceText.Lines[line];
int maxColumn = lineObj.End - lineObj.Start;
if (column < 0)
return [];
if (column > maxColumn)
column = maxColumn;
var position = sourceText.Lines.GetPosition(new LinePosition(line, column));
if (position < 0 || position > sourceText.Length)
return [];
var completionService = CompletionService.GetService(document);
if (completionService == null) return [];
if (kind == TriggerKind_TriggerForIncompleteCompletions
&& !completionService.ShouldTriggerCompletion(
sourceText,
position,
GetCompletionTrigger(TriggerKind_TriggerCharacter, triggerCharacter, includeTriggerCharacter: true)))
{
return [];
}
Microsoft.CodeAnalysis.Completion.CompletionList? completionList = null;
try
{
completionList = await completionService.GetCompletionsAsync(
document,
position,
GetCompletionTrigger(kind - 1, triggerCharacter, includeTriggerCharacter: false));
}
catch
{
return [];
}
if (completionList is null || completionList.ItemsList.Count <= 0)
return [];
var typedSpan = completionService.GetDefaultCompletionListSpan(sourceText, position);
if (typedSpan.Start < 0 || typedSpan.End > sourceText.Length)
return [];
var typedText = sourceText.GetSubText(typedSpan).ToString();
LinePosition replacingSpanStart;
LinePosition replacingSpanEnd;
try
{
replacingSpanStart = sourceText.Lines.GetLinePosition(typedSpan.Start);
replacingSpanEnd = sourceText.Lines.GetLinePosition(typedSpan.End);
}
catch
{
return [];
}
if (!typedSpan.IsEmpty || triggerCharacter != '.')
{
if (replacingSpanStart.Line != replacingSpanEnd.Line
|| replacingSpanStart.Character > replacingSpanEnd.Character)
{
return [];
}
}
ImmutableArray<string> filteredItems = typedText != string.Empty
? [.. completionService.FilterItems(document, [.. completionList.ItemsList], typedText)
.Select(i => i.DisplayText)]
: [];
bool expectingImportedItems = workspace.Options.GetOption(
new PerLanguageOption<bool?>("CompletionOptions", "ShowItemsFromUnimportedNamespaces", defaultValue: null),
LanguageNames.CSharp) == true;
var completionsBuilder = new List<BlazorMonaco.Languages.CompletionItem>(completionList.ItemsList.Count);
var commitCharactersCache = new Dictionary<int, string[]>();
var range = new BlazorMonaco.Range
{
StartLineNumber = replacingSpanStart.Line + 1,
EndLineNumber = replacingSpanEnd.Line + 1,
StartColumn = replacingSpanStart.Character + 1,
EndColumn = replacingSpanEnd.Character + 1,
};
foreach (var completion in completionList.ItemsList)
{
string? insertText = completion.Properties.TryGetValue("InsertionText", out var propInsertText)
? propInsertText
: completion.DisplayText;
if (string.IsNullOrEmpty(insertText))
continue;
string documentation = "";
int rulesHash = GetRulesHash(completion.Rules.CommitCharacterRules);
if (!commitCharactersCache.TryGetValue(rulesHash, out var commitCharacters))
{
var localBuilder = ImmutableArray.CreateBuilder<char>(
completionList.Rules.DefaultCommitCharacters.Length);
var chars = BuildCommitCharacters(
completionList,
completion.Rules.CommitCharacterRules,
localBuilder);
commitCharacters = [.. chars.Select(c => c.ToString())];
commitCharactersCache[rulesHash] = commitCharacters;
}
char sortTextPrepend = '0';
CompletionItemInsertTextRule? insertTextRules = null;
if (completion.IsComplexTextEdit ||
(completion.Properties.ContainsKey("Provider") &&
completion.Properties["Provider"] == "SnippetCompletionProvider"))
{
insertTextRules = CompletionItemInsertTextRule.InsertAsSnippet;
}
completionsBuilder.Add(new BlazorMonaco.Languages.CompletionItem
{
LabelAsString = completion.DisplayTextPrefix + completion.DisplayText + completion.DisplayTextSuffix,
Kind = GetCompletionItemKind(completion.Tags),
DocumentationAsString = documentation,
InsertText = insertText,
RangeAsObject = range,
AdditionalTextEdits = [],
SortText = expectingImportedItems ? sortTextPrepend + completion.SortText : completion.SortText,
FilterText = completion.FilterText,
Detail = completion.InlineDescription,
Preselect = completion.Rules.MatchPriority == MatchPriority.Preselect
|| filteredItems.Contains(completion.DisplayText),
CommitCharacters = [.. commitCharacters],
Tags = [],
Command = null,
InsertTextRules = insertTextRules,
});
}
return completionsBuilder;
}
private static int GetRulesHash(ImmutableArray<CharacterSetModificationRule> rules)
{
if (rules.IsEmpty) return 0;
var hash = new HashCode();
foreach (var rule in rules)
{
hash.Add(rule.Kind);
hash.Add(rule.Characters.Length);
foreach (var c in rule.Characters)
{
hash.Add(c);
}
}
return hash.ToHashCode();
}
public static async Task<string?> GetQuickInfoAsync(
this AdhocWorkspace workspace,
DocumentId documentId,
int line,
int column)
{
var document = workspace.CurrentSolution.GetDocument(documentId);
if (document is null) return null;
var sourceText = await document.GetTextAsync();
var position = sourceText.Lines.GetPosition(new LinePosition(line, column));
var quickInfoService = QuickInfoService.GetService(document);
if (quickInfoService is null) return string.Empty;
var quickInfo = await quickInfoService.GetQuickInfoAsync(document, position);
if (quickInfo is null) return string.Empty;
var finalTextBuilder = new StringBuilder();
bool lastSectionHadLineBreak = true;
var description = quickInfo.Sections.FirstOrDefault(s => s.Kind == QuickInfoSectionKinds.Description);
if (description is not null)
{
finalTextBuilder.AppendSection(description, MarkdownFormat.AllTextAsCSharp, ref lastSectionHadLineBreak);
}
var summary = quickInfo.Sections.FirstOrDefault(s => s.Kind == QuickInfoSectionKinds.DocumentationComments);
if (summary is not null)
{
finalTextBuilder.AppendSection(summary, MarkdownFormat.Default, ref lastSectionHadLineBreak);
}
foreach (var section in quickInfo.Sections)
{
switch (section.Kind)
{
case QuickInfoSectionKinds.Description:
case QuickInfoSectionKinds.DocumentationComments:
continue;
case QuickInfoSectionKinds.TypeParameters:
finalTextBuilder.AppendSection(section, MarkdownFormat.AllTextAsCSharp, ref lastSectionHadLineBreak);
break;
case QuickInfoSectionKinds.AnonymousTypes:
finalTextBuilder.AppendSection(section, MarkdownFormat.FirstLineDefaultRestCSharp, ref lastSectionHadLineBreak);
break;
case "NullabilityAnalysis":
finalTextBuilder.AppendSection(section, MarkdownFormat.Italicize, ref lastSectionHadLineBreak);
break;
default:
finalTextBuilder.AppendSection(section, MarkdownFormat.Default, ref lastSectionHadLineBreak);
break;
}
}
try
{
var semanticModel = await document.GetSemanticModelAsync();
var syntaxTree = await document.GetSyntaxTreeAsync();
if (semanticModel is not null && syntaxTree is not null)
{
var root = await syntaxTree.GetRootAsync();
var node = root.FindToken(position).Parent;
while (node is not null)
{
var symbolInfo = semanticModel.GetSymbolInfo(node);
var symbol = symbolInfo.Symbol ?? semanticModel.GetDeclaredSymbol(node);
if (symbol is IMethodSymbol methodSymbol)
{
var containingType = methodSymbol.ContainingType;
if (containingType is not null)
{
var overloads = containingType.GetMembers(methodSymbol.Name)
.OfType<IMethodSymbol>()
.Where(m => m.MethodKind == methodSymbol.MethodKind)
.ToList();
if (overloads.Count > 1)
{
finalTextBuilder.AppendLine();
finalTextBuilder.AppendLine();
finalTextBuilder.AppendLine("---");
finalTextBuilder.AppendLine($"**Overloads ({overloads.Count}):**");
finalTextBuilder.AppendLine();
foreach (var overload in overloads)
{
finalTextBuilder.AppendLine("```csharp");
finalTextBuilder.AppendLine(overload.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
finalTextBuilder.AppendLine("```");
var xmlDoc = overload.GetDocumentationCommentXml();
if (!string.IsNullOrEmpty(xmlDoc))
{
var summaryMatch = SummaryRegex().Match(xmlDoc);
if (summaryMatch.Success)
{
var summaryText = summaryMatch.Groups[1].Value.Trim();
summaryText = WhitespaceRegex().Replace(summaryText, " ");
finalTextBuilder.AppendLine(summaryText);
}
}
finalTextBuilder.AppendLine();
}
}
}
break;
}
node = node.Parent;
}
}
}
catch
{
// Silently handle errors
}
return finalTextBuilder.ToString().Trim();
}
}

View File

@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace RobotNet10.ScriptEditor.Helpers.Code;
public class BlazorBootJson
{
public string MainAssemblyName { get; set; } = "";
[JsonPropertyName("resources")]
public BlazorResources Resources { get; set; } = new();
public bool CacheBootResources { get; set; }
public int DebugLevel { get; set; }
public string GlobalizationMode { get; set; } = "";
public Dictionary<string, object> Extensions { get; set; } = [];
}

View File

@@ -0,0 +1,20 @@
using System.Text.Json.Serialization;
namespace RobotNet10.ScriptEditor.Helpers.Code;
public class BlazorResources
{
public string Hash { get; set; } = "";
public Dictionary<string, string> Assembly { get; set; } = [];
[JsonPropertyName("fingerprinting")]
public Dictionary<string, string> Fingerprinting { get; set; } = [];
public Dictionary<string, string> WasmNative { get; set; } = [];
public Dictionary<string, string> CoreAssembly { get; set; } = [];
public Dictionary<string, string> Pdb { get; set; } = [];
public Dictionary<string, Dictionary<string, string>> SatelliteResources { get; set; } = [];
public Dictionary<string, string> JsModuleNative { get; set; } = [];
public Dictionary<string, string> JsModuleRuntime { get; set; } = [];
public Dictionary<string, string> LibraryInitializers { get; set; } = [];
public Dictionary<string, string> ModulesAfterConfigLoaded { get; set; } = [];
}

View File

@@ -0,0 +1,271 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.QuickInfo;
using System.Collections.Immutable;
using System.Text;
using System.Text.RegularExpressions;
namespace RobotNet10.ScriptEditor.Helpers;
public enum MarkdownFormat
{
Default,
Italicize,
FirstLineAsCSharp,
FirstLineDefaultRestCSharp,
AllTextAsCSharp
}
public static class MarkdownHelpers
{
private static readonly Regex EscapeRegex = new("([\\\\`\\*_\\{\\}\\[\\]\\(\\)#+\\-\\.!])", RegexOptions.Compiled);
private const string ContainerStart = "ContainerStart";
private const string ContainerEnd = "ContainerEnd";
public static string Escape(string markdown) => string.IsNullOrEmpty(markdown) ? string.Empty : EscapeRegex.Replace(markdown, "\\$1");
public static void AppendSection(this StringBuilder builder, QuickInfoSection section, MarkdownFormat format, ref bool lastLineBreak)
{
if (!lastLineBreak && section.TaggedParts.Length > 0 && section.TaggedParts[0].Tag != "LineBreak")
{
builder.Append("\n\n");
}
MarkdownHelpers.TaggedTextToMarkdown(section.TaggedParts, builder, "\n", format, out lastLineBreak);
}
public static void TaggedTextToMarkdown(ImmutableArray<TaggedText> taggedParts, StringBuilder stringBuilder, string newLine, MarkdownFormat markdownFormat, out bool endedWithLineBreak)
{
bool isInCodeBlock = false;
bool brokeLine = true;
bool afterFirstLine = false;
if (markdownFormat == MarkdownFormat.Italicize)
{
stringBuilder.Append('_');
}
int num = 0;
while (num < taggedParts.Length)
{
TaggedText taggedText = taggedParts[num];
bool flag;
if (brokeLine && markdownFormat != MarkdownFormat.Italicize)
{
brokeLine = false;
if (!afterFirstLine)
{
if (markdownFormat != MarkdownFormat.FirstLineAsCSharp)
{
goto IL_00a2;
}
flag = true;
}
else
{
if (markdownFormat != MarkdownFormat.FirstLineDefaultRestCSharp)
{
goto IL_00a2;
}
flag = true;
}
goto IL_00bf;
}
goto IL_0279;
IL_00a2:
flag = markdownFormat == MarkdownFormat.AllTextAsCSharp;
goto IL_00bf;
IL_0279:
switch (taggedText.Tag)
{
case "Text":
if (!isInCodeBlock)
{
addText(taggedText.Text);
break;
}
endBlock();
addText(taggedText.Text);
break;
case "Space":
if (isInCodeBlock)
{
if (indexIsTag(num + 1, ["Text"]))
{
endBlock();
}
addText(taggedText.Text);
break;
}
goto case "Punctuation";
case "Punctuation":
addText(taggedText.Text);
break;
case ContainerStart:
addNewline();
addText(taggedText.Text);
break;
case ContainerEnd:
addNewline();
break;
case "LineBreak":
if (stringBuilder.Length != 0 && !indexIsTag(num + 1, [ContainerStart, ContainerEnd]) && num + 1 != taggedParts.Length)
{
addNewline();
}
break;
default:
if (!isInCodeBlock)
{
isInCodeBlock = true;
stringBuilder.Append('`');
}
stringBuilder.Append(taggedText.Text);
brokeLine = false;
break;
}
num++;
continue;
IL_00bf:
bool flag2 = flag;
if (!flag2)
{
for (int j = num; j < taggedParts.Length; flag2 = true, j++)
{
switch (taggedParts[j].Tag)
{
case "Text":
flag2 = false;
break;
default:
continue;
case ContainerStart:
case ContainerEnd:
case "LineBreak":
break;
}
break;
}
}
else
{
flag2 = !indexIsTag(num,
[
ContainerStart,
ContainerEnd,
"LineBreak"
]);
}
if (flag2)
{
afterFirstLine = true;
stringBuilder.Append("```csharp");
stringBuilder.Append(newLine);
while (true)
{
if (num < taggedParts.Length)
{
taggedText = taggedParts[num];
if (taggedText.Tag == ContainerStart || taggedText.Tag == ContainerEnd || taggedText.Tag == "LineBreak")
{
stringBuilder.Append(newLine);
if (markdownFormat != MarkdownFormat.AllTextAsCSharp && markdownFormat != MarkdownFormat.FirstLineDefaultRestCSharp)
{
break;
}
}
else
{
stringBuilder.Append(taggedText.Text);
}
num++;
continue;
}
stringBuilder.Append(newLine);
stringBuilder.Append("```");
endedWithLineBreak = false;
return;
}
stringBuilder.Append("```");
}
goto IL_0279;
}
if (isInCodeBlock)
{
endBlock();
}
if (!brokeLine && markdownFormat == MarkdownFormat.Italicize)
{
stringBuilder.Append('_');
}
endedWithLineBreak = brokeLine;
void addNewline()
{
if (isInCodeBlock)
{
endBlock();
}
if (markdownFormat == MarkdownFormat.Italicize)
{
stringBuilder.Append('_');
}
stringBuilder.Append(newLine);
stringBuilder.Append(newLine);
brokeLine = true;
if (markdownFormat == MarkdownFormat.Italicize)
{
stringBuilder.Append('_');
}
}
void addText(string text)
{
brokeLine = false;
afterFirstLine = true;
if (!isInCodeBlock)
{
text = Escape(text);
}
stringBuilder.Append(text);
}
void endBlock()
{
stringBuilder.Append('`');
isInCodeBlock = false;
}
bool indexIsTag(int i, string[] tags)
{
if (i < taggedParts.Length)
{
return tags.Contains(taggedParts[i].Tag);
}
return false;
}
}
}

View File

@@ -0,0 +1,196 @@
using System.Text;
using System.Xml;
namespace RobotNet10.ScriptEditor.Helpers.Monaco.Documentation;
public class DocumentationComment(
string summaryText = "",
DocumentationItem[]? typeParamElements = null,
DocumentationItem[]? paramElements = null,
string returnsText = "",
string remarksText = "",
string exampleText = "",
string valueText = "",
DocumentationItem[]? exception = null)
{
public string SummaryText { get; } = summaryText;
public DocumentationItem[] TypeParamElements { get; } = typeParamElements ?? [];
public DocumentationItem[] ParamElements { get; } = paramElements ?? [];
public string ReturnsText { get; } = returnsText;
public string RemarksText { get; } = remarksText;
public string ExampleText { get; } = exampleText;
public string ValueText { get; } = valueText;
public DocumentationItem[] Exception { get; } = exception ?? [];
public static DocumentationComment? From(string xmlDocumentation, string lineEnding)
{
if (string.IsNullOrEmpty(xmlDocumentation))
return Empty;
var reader = new StringReader("<docroot>" + xmlDocumentation + "</docroot>");
var summaryText = new StringBuilder();
var typeParamElements = new List<DocumentationItemBuilder>();
var paramElements = new List<DocumentationItemBuilder>();
var returnsText = new StringBuilder();
var remarksText = new StringBuilder();
var exampleText = new StringBuilder();
var valueText = new StringBuilder();
var exception = new List<DocumentationItemBuilder>();
using (var xml = XmlReader.Create(reader))
{
try
{
xml.Read();
string? elementName = null;
StringBuilder? currentSectionBuilder = null;
do
{
if (xml.NodeType == XmlNodeType.Element)
{
elementName = xml.Name.ToLowerInvariant();
switch (elementName)
{
case "filterpriority":
xml.Skip();
break;
case "remarks":
currentSectionBuilder = remarksText;
break;
case "example":
currentSectionBuilder = exampleText;
break;
case "exception":
DocumentationItemBuilder exceptionInstance = new(GetCref(xml["cref"]).TrimEnd());
currentSectionBuilder = exceptionInstance.Documentation;
exception.Add(exceptionInstance);
break;
case "returns":
currentSectionBuilder = returnsText;
break;
case "summary":
currentSectionBuilder = summaryText;
break;
case "see":
if (currentSectionBuilder is null) continue;
currentSectionBuilder.Append(GetCref(xml["cref"]));
currentSectionBuilder.Append(xml["langword"]);
break;
case "seealso":
if (currentSectionBuilder is null) continue;
currentSectionBuilder.Append("See also: ");
currentSectionBuilder.Append(GetCref(xml["cref"]));
break;
case "paramref":
if (currentSectionBuilder is null) continue;
currentSectionBuilder.Append(xml["name"]);
currentSectionBuilder.Append(' ');
break;
case "param":
DocumentationItemBuilder paramInstance = new(TrimMultiLineString(xml["name"] ?? "", lineEnding));
currentSectionBuilder = paramInstance.Documentation;
paramElements.Add(paramInstance);
break;
case "typeparamref":
if (currentSectionBuilder is null) continue;
currentSectionBuilder.Append(xml["name"]);
currentSectionBuilder.Append(' ');
break;
case "typeparam":
DocumentationItemBuilder typeParamInstance = new(TrimMultiLineString(xml["name"] ?? "", lineEnding));
currentSectionBuilder = typeParamInstance.Documentation;
typeParamElements.Add(typeParamInstance);
break;
case "value":
currentSectionBuilder = valueText;
break;
case "br":
case "para":
if (currentSectionBuilder is null) continue;
currentSectionBuilder.Append(lineEnding);
break;
}
}
else if (xml.NodeType == XmlNodeType.Text && currentSectionBuilder != null)
{
if (elementName == "code")
{
currentSectionBuilder.Append(xml.Value);
}
else
{
currentSectionBuilder.Append(TrimMultiLineString(xml.Value, lineEnding));
}
}
} while (xml.Read());
}
catch (Exception)
{
return null;
}
}
return new DocumentationComment(
summaryText.ToString(),
[.. typeParamElements.Select(s => s.ConvertToDocumentedObject())],
[.. paramElements.Select(s => s.ConvertToDocumentedObject())],
returnsText.ToString(),
remarksText.ToString(),
exampleText.ToString(),
valueText.ToString(),
[.. exception.Select(s => s.ConvertToDocumentedObject())]);
}
private static string TrimMultiLineString(string input, string lineEnding)
{
var lines = input.Split(separator, StringSplitOptions.RemoveEmptyEntries);
return string.Join(lineEnding, lines.Select(l => TrimStartRetainingSingleLeadingSpace(l)));
}
private static string GetCref(string? cref)
{
if (cref == null || cref.Trim().Length == 0)
{
return "";
}
if (cref.Length < 2)
{
return cref;
}
if (cref.Substring(1, 1) == ":")
{
return string.Concat(cref.AsSpan(2, cref.Length - 2), " ");
}
return cref + " ";
}
private static string TrimStartRetainingSingleLeadingSpace(string input)
{
if (string.IsNullOrWhiteSpace(input))
return string.Empty;
if (!char.IsWhiteSpace(input[0]))
return input;
return $" {input.TrimStart()}";
}
public string GetParameterText(string name)
=> Array.Find(ParamElements, parameter => parameter.Name == name)?.Documentation ?? string.Empty;
public string GetTypeParameterText(string name)
=> Array.Find(TypeParamElements, typeParam => typeParam.Name == name)?.Documentation ?? string.Empty;
public static readonly DocumentationComment Empty = new();
private static readonly string[] separator = ["\n", "\r\n"];
}
class DocumentationItemBuilder(string name)
{
public string Name { get; set; } = name;
public StringBuilder Documentation { get; set; } = new StringBuilder();
public DocumentationItem ConvertToDocumentedObject()
{
return new DocumentationItem(Name, Documentation.ToString());
}
}

View File

@@ -0,0 +1,7 @@
namespace RobotNet10.ScriptEditor.Helpers.Monaco.Documentation;
public class DocumentationItem(string name, string documentation)
{
public string Name { get; } = name;
public string Documentation { get; } = documentation;
}

View File

@@ -0,0 +1,171 @@
using Microsoft.CodeAnalysis;
using RobotNet10.ScriptEditor.Helpers.Monaco.Documentation;
using System.Text;
using System.Xml;
namespace RobotNet10.ScriptEditor.Helpers.Monaco;
public class DocumentationConverter
{/// <summary>
/// Converts the xml documentation string into a plain text string.
/// </summary>
public static string ConvertDocumentation(string xmlDocumentation, string lineEnding)
{
if (string.IsNullOrEmpty(xmlDocumentation))
return string.Empty;
var reader = new StringReader("<docroot>" + xmlDocumentation + "</docroot>");
using var xml = XmlReader.Create(reader);
var ret = new StringBuilder();
try
{
xml.Read();
string? elementName = null;
do
{
if (xml.NodeType == XmlNodeType.Element)
{
elementName = xml.Name.ToLowerInvariant();
switch (elementName)
{
case "filterpriority":
xml.Skip();
break;
case "remarks":
ret.Append(lineEnding);
ret.Append("Remarks:");
ret.Append(lineEnding);
break;
case "example":
ret.Append(lineEnding);
ret.Append("Example:");
ret.Append(lineEnding);
break;
case "exception":
ret.Append(lineEnding);
ret.Append(GetCref(xml["cref"]).TrimEnd());
ret.Append(": ");
break;
case "returns":
ret.Append(lineEnding);
ret.Append("Returns: ");
break;
case "see":
ret.Append(GetCref(xml["cref"]));
ret.Append(xml["langword"]);
break;
case "seealso":
ret.Append(lineEnding);
ret.Append("See also: ");
ret.Append(GetCref(xml["cref"]));
break;
case "paramref":
ret.Append(xml["name"]);
ret.Append(' ');
break;
case "typeparam":
ret.Append(lineEnding);
ret.Append('<');
ret.Append(TrimMultiLineString(xml["name"], lineEnding));
ret.Append(">: ");
break;
case "param":
ret.Append(lineEnding);
ret.Append(TrimMultiLineString(xml["name"], lineEnding));
ret.Append(": ");
break;
case "value":
ret.Append(lineEnding);
ret.Append("Value: ");
ret.Append(lineEnding);
break;
case "br":
case "para":
ret.Append(lineEnding);
break;
}
}
else if (xml.NodeType == XmlNodeType.Text)
{
if (elementName == "code")
{
ret.Append(xml.Value);
}
else
{
ret.Append(TrimMultiLineString(xml.Value, lineEnding));
}
}
} while (xml.Read());
}
catch (Exception)
{
return xmlDocumentation;
}
return ret.ToString();
}
private static readonly string[] separator = ["\n", "\r\n"];
private static string TrimMultiLineString(string? input, string lineEnding)
{
if (string.IsNullOrEmpty(input)) return "";
var lines = input.Split(separator, StringSplitOptions.RemoveEmptyEntries);
return string.Join(lineEnding, lines.Select(l => l.TrimStart()));
}
private static string GetCref(string? cref)
{
if (cref == null || cref.Trim().Length == 0)
{
return "";
}
if (cref.Length < 2)
{
return cref;
}
if (cref.Substring(1, 1) == ":")
{
return cref[2..] + " ";
}
return cref + " ";
}
public static DocumentationComment? GetStructuredDocumentation(string? xmlDocumentation, string lineEnding)
{
if (string.IsNullOrEmpty(xmlDocumentation)) return null;
return DocumentationComment.From(xmlDocumentation, lineEnding);
}
public static DocumentationComment? GetStructuredDocumentation(ISymbol symbol, string lineEnding = "\n")
{
return symbol switch
{
IParameterSymbol parameter => new DocumentationComment(summaryText: GetParameterDocumentation(parameter, lineEnding) ?? ""),
ITypeParameterSymbol typeParam => new DocumentationComment(summaryText: GetTypeParameterDocumentation(typeParam, lineEnding) ?? ""),
IAliasSymbol alias => new DocumentationComment(summaryText: GetAliasDocumentation(alias, lineEnding) ?? ""),
_ => GetStructuredDocumentation(symbol.GetDocumentationCommentXml(), lineEnding),
};
}
private static string? GetParameterDocumentation(IParameterSymbol parameter, string lineEnding = "\n")
{
var contaningSymbolDef = parameter.ContainingSymbol.OriginalDefinition;
return GetStructuredDocumentation(contaningSymbolDef.GetDocumentationCommentXml(), lineEnding)
?.GetParameterText(parameter.Name);
}
private static string? GetTypeParameterDocumentation(ITypeParameterSymbol typeParam, string lineEnding = "\n")
{
var contaningSymbol = typeParam.ContainingSymbol;
return GetStructuredDocumentation(contaningSymbol.GetDocumentationCommentXml(), lineEnding)
?.GetTypeParameterText(typeParam.Name);
}
private static string? GetAliasDocumentation(IAliasSymbol alias, string lineEnding = "\n")
{
return GetStructuredDocumentation(alias.Target.GetDocumentationCommentXml(), lineEnding)?.SummaryText;
}
}

View File

@@ -0,0 +1,34 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace RobotNet10.ScriptEditor.Helpers.Monaco;
public class InvocationContext
{
public SemanticModel SemanticModel { get; }
public int Position { get; }
public SyntaxNode Receiver { get; }
public IEnumerable<TypeInfo> ArgumentTypes { get; }
public IEnumerable<SyntaxToken> Separators { get; }
public bool IsInStaticContext { get; }
public InvocationContext(SemanticModel semModel, int position, SyntaxNode receiver, ArgumentListSyntax argList, bool isStatic)
{
SemanticModel = semModel;
Position = position;
Receiver = receiver;
ArgumentTypes = argList.Arguments.Select(argument => semModel.GetTypeInfo(argument.Expression));
Separators = argList.Arguments.GetSeparators();
IsInStaticContext = isStatic;
}
public InvocationContext(SemanticModel semModel, int position, SyntaxNode receiver, AttributeArgumentListSyntax argList, bool isStatic)
{
SemanticModel = semModel;
Position = position;
Receiver = receiver;
ArgumentTypes = argList.Arguments.Select(argument => semModel.GetTypeInfo(argument.Expression));
Separators = argList.Arguments.GetSeparators();
IsInStaticContext = isStatic;
}
}

View File

@@ -0,0 +1,9 @@
using BlazorMonaco;
namespace RobotNet10.ScriptEditor.Helpers.Monaco.Languages;
public class ParameterInformation
{
public string Label { get; set; } = "";
public MarkdownString? Documentation { get; set; }
}

View File

@@ -0,0 +1,8 @@
namespace RobotNet10.ScriptEditor.Helpers.Monaco.Languages;
public class SignatureHelp
{
public int ActiveParameter { get; set; }
public int ActiveSignature { get; set; }
public SignatureInformation[] Signatures { get; set; } = [];
}

View File

@@ -0,0 +1,6 @@
namespace RobotNet10.ScriptEditor.Helpers.Monaco.Languages;
public class SignatureHelpResult
{
public SignatureHelp Value { get; set; } = null!;
}

View File

@@ -0,0 +1,11 @@
using BlazorMonaco;
namespace RobotNet10.ScriptEditor.Helpers.Monaco.Languages;
public class SignatureInformation
{
public int? ActiveParameter { get; set; }
public MarkdownString? Documentation { get; set; }
public string Label { get; set; } = "";
public ParameterInformation[] Parameters { get; set; } = [];
}

View File

@@ -0,0 +1,319 @@
using BlazorMonaco;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using RobotNet10.ScriptEditor.Helpers.Monaco;
using RobotNet10.ScriptEditor.Helpers.Monaco.Languages;
namespace RobotNet10.ScriptEditor.Helpers;
public static class SignatureHelpExtensions
{
public static async Task<SignatureHelpResult?> GetSignatureHelpAsync(this Document document, int line, int column)
{
var invocation = await GetInvocation(document, line, column);
if (invocation is null) return null;
var response = new SignatureHelp();
foreach (var comma in invocation.Separators)
{
if (comma.Span.Start > invocation.Position)
{
break;
}
response.ActiveParameter += 1;
}
var signaturesSet = new HashSet<SignatureInformation>();
var bestScore = int.MinValue;
SignatureInformation? bestScoredItem = null;
var types = invocation.ArgumentTypes;
ISymbol? throughSymbol = null;
ISymbol? throughType = null;
var methodGroup = invocation.SemanticModel.GetMemberGroup(invocation.Receiver).OfType<IMethodSymbol>();
if (invocation.Receiver is MemberAccessExpressionSyntax syntax)
{
var throughExpression = syntax.Expression;
throughSymbol = invocation.SemanticModel.GetSpeculativeSymbolInfo(invocation.Position, throughExpression, SpeculativeBindingOption.BindAsExpression).Symbol;
throughType = invocation.SemanticModel.GetSpeculativeTypeInfo(invocation.Position, throughExpression, SpeculativeBindingOption.BindAsTypeOrNamespace).Type;
var includeInstance = throughSymbol != null && throughSymbol is not ITypeSymbol ||
throughExpression is LiteralExpressionSyntax ||
throughExpression is TypeOfExpressionSyntax;
var includeStatic = throughSymbol is INamedTypeSymbol || throughType != null;
methodGroup = methodGroup.Where(m => m.IsStatic && includeStatic || !m.IsStatic && includeInstance);
}
else if (invocation.Receiver is SimpleNameSyntax && invocation.IsInStaticContext)
{
methodGroup = methodGroup.Where(m => m.IsStatic || m.MethodKind == MethodKind.LocalFunction);
}
foreach (var methodOverload in methodGroup)
{
var signature = BuildSignature(methodOverload);
signaturesSet.Add(signature);
var score = InvocationScore(methodOverload, types);
if (score > bestScore)
{
bestScore = score;
bestScoredItem = signature;
}
}
var signaturesList = signaturesSet.ToList();
response.Signatures = [.. signaturesList];
if (bestScoredItem == null)
{
response.ActiveSignature = -1;
}
else
{
response.ActiveSignature = signaturesList.IndexOf((SignatureInformation)bestScoredItem);
}
return new SignatureHelpResult()
{
Value = response,
};
}
private static async Task<InvocationContext?> GetInvocation(Document document, int line, int column)
{
var sourceText = await document.GetTextAsync();
var position = sourceText.Lines.GetPosition(new LinePosition(line, column));
var tree = await document.GetSyntaxTreeAsync();
if (tree is null) return null;
var root = await tree.GetRootAsync();
if (root is null) return null;
try
{
var node = root.FindToken(position).Parent;
// Walk up until we find a node that we're interested in.
while (node != null)
{
if (node is InvocationExpressionSyntax invocation && invocation.ArgumentList.Span.Contains(position))
{
var semanticModel = await document.GetSemanticModelAsync();
return semanticModel is null ? null : new InvocationContext(semanticModel, position, invocation.Expression, invocation.ArgumentList, invocation.IsInStaticContext());
}
if (node is BaseObjectCreationExpressionSyntax objectCreation && (objectCreation.ArgumentList?.Span.Contains(position) ?? false))
{
var semanticModel = await document.GetSemanticModelAsync();
return semanticModel is null ? null : new InvocationContext(semanticModel, position, objectCreation, objectCreation.ArgumentList, objectCreation.IsInStaticContext());
}
if (node is AttributeSyntax attributeSyntax && (attributeSyntax.ArgumentList?.Span.Contains(position) ?? false))
{
var semanticModel = await document.GetSemanticModelAsync();
return semanticModel is null ? null : new InvocationContext(semanticModel, position, attributeSyntax, attributeSyntax.ArgumentList, attributeSyntax.IsInStaticContext());
}
node = node.Parent;
}
}
catch { }
return null;
}
private static int InvocationScore(IMethodSymbol symbol, IEnumerable<TypeInfo> types)
{
var parameters = symbol.Parameters;
if (parameters.Length < types.Count())
{
return int.MinValue;
}
var score = 0;
var invocationEnum = types.GetEnumerator();
var definitionEnum = parameters.GetEnumerator();
while (invocationEnum.MoveNext() && definitionEnum.MoveNext())
{
if (invocationEnum.Current.ConvertedType == null)
{
// 1 point for having a parameter
score += 1;
}
else if (SymbolEqualityComparer.Default.Equals(invocationEnum.Current.ConvertedType, definitionEnum.Current.Type))
{
// 2 points for having a parameter and being
// the same type
score += 2;
}
}
return score;
}
private static SignatureInformation BuildSignature(IMethodSymbol symbol)
{
var StructuredDocumentation = DocumentationConverter.GetStructuredDocumentation(symbol);
return new SignatureInformation
{
Documentation = new MarkdownString()
{
Value = StructuredDocumentation?.SummaryText ?? "",
},
Label = symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
Parameters = [..symbol.Parameters.Select(parameter => new ParameterInformation()
{
Label = parameter.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
Documentation = new MarkdownString()
{
Value = StructuredDocumentation?.GetParameterText(parameter.Name) ?? string.Empty,
},
})],
ActiveParameter = null,
};
}
private static bool IsInStaticContext(this SyntaxNode node)
{
// this/base calls are always static.
if (node.FirstAncestorOrSelf<ConstructorInitializerSyntax>() != null)
{
return true;
}
var memberDeclaration = node.FirstAncestorOrSelf<MemberDeclarationSyntax>();
if (memberDeclaration == null)
{
return false;
}
switch (memberDeclaration.Kind())
{
case SyntaxKind.MethodDeclaration:
case SyntaxKind.ConstructorDeclaration:
case SyntaxKind.EventDeclaration:
case SyntaxKind.IndexerDeclaration:
return GetModifiers(memberDeclaration).Any(SyntaxKind.StaticKeyword);
case SyntaxKind.PropertyDeclaration:
return GetModifiers(memberDeclaration).Any(SyntaxKind.StaticKeyword) ||
node.IsFoundUnder((PropertyDeclarationSyntax p) => p.Initializer);
case SyntaxKind.FieldDeclaration:
case SyntaxKind.EventFieldDeclaration:
// Inside a field one can only access static members of a type (unless it's top-level).
return !memberDeclaration.Parent.IsKind(SyntaxKind.CompilationUnit);
case SyntaxKind.DestructorDeclaration:
return false;
}
// Global statements are not a static context.
if (node.FirstAncestorOrSelf<GlobalStatementSyntax>() != null)
{
return false;
}
// any other location is considered static
return true;
}
private static SyntaxTokenList GetModifiers(SyntaxNode member)
{
if (member != null)
{
switch (member.Kind())
{
case SyntaxKind.EnumDeclaration:
return ((EnumDeclarationSyntax)member).Modifiers;
case SyntaxKind.ClassDeclaration:
case SyntaxKind.InterfaceDeclaration:
case SyntaxKind.StructDeclaration:
return ((TypeDeclarationSyntax)member).Modifiers;
case SyntaxKind.DelegateDeclaration:
return ((DelegateDeclarationSyntax)member).Modifiers;
case SyntaxKind.FieldDeclaration:
return ((FieldDeclarationSyntax)member).Modifiers;
case SyntaxKind.EventFieldDeclaration:
return ((EventFieldDeclarationSyntax)member).Modifiers;
case SyntaxKind.ConstructorDeclaration:
return ((ConstructorDeclarationSyntax)member).Modifiers;
case SyntaxKind.DestructorDeclaration:
return ((DestructorDeclarationSyntax)member).Modifiers;
case SyntaxKind.PropertyDeclaration:
return ((PropertyDeclarationSyntax)member).Modifiers;
case SyntaxKind.EventDeclaration:
return ((EventDeclarationSyntax)member).Modifiers;
case SyntaxKind.IndexerDeclaration:
return ((IndexerDeclarationSyntax)member).Modifiers;
case SyntaxKind.OperatorDeclaration:
return ((OperatorDeclarationSyntax)member).Modifiers;
case SyntaxKind.ConversionOperatorDeclaration:
return ((ConversionOperatorDeclarationSyntax)member).Modifiers;
case SyntaxKind.MethodDeclaration:
return ((MethodDeclarationSyntax)member).Modifiers;
case SyntaxKind.GetAccessorDeclaration:
case SyntaxKind.SetAccessorDeclaration:
case SyntaxKind.AddAccessorDeclaration:
case SyntaxKind.RemoveAccessorDeclaration:
return ((AccessorDeclarationSyntax)member).Modifiers;
}
}
return default;
}
private static bool IsFoundUnder<TParent>(this SyntaxNode node, Func<TParent, SyntaxNode?> childGetter)
where TParent : SyntaxNode
{
var ancestor = node.GetAncestor<TParent>();
if (ancestor == null)
{
return false;
}
var child = childGetter(ancestor);
// See if node passes through child on the way up to ancestor.
return node.GetAncestorsOrThis<SyntaxNode>().Contains(child);
}
private static TNode? GetAncestor<TNode>(this SyntaxNode node)
where TNode : SyntaxNode
{
var current = node.Parent;
while (current != null)
{
if (current is TNode tNode)
{
return tNode;
}
current = current.GetParent();
}
return null;
}
private static IEnumerable<TNode> GetAncestorsOrThis<TNode>(this SyntaxNode node)
where TNode : SyntaxNode
{
var current = node;
while (current != null)
{
if (current is TNode tNode)
{
yield return tNode;
}
current = current.GetParent();
}
}
private static SyntaxNode? GetParent(this SyntaxNode node)
{
return node is IStructuredTriviaSyntax trivia ? trivia.ParentTrivia.Token.Parent : node.Parent;
}
}

View File

@@ -0,0 +1,296 @@
using System.Runtime.InteropServices;
namespace RobotNet10.ScriptEditor.Helpers.Webcil;
internal static class Constants
{
internal const ushort IMAGE_FILE_MACHINE_I386 = 0x014c;
internal const ushort IMAGE_FILE_MACHINE_IA64 = 0x0200;
internal const ushort IMAGE_FILE_MACHINE_AMD64 = 0x8664;
}
/// <summary>
/// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_data_directory
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct IMAGE_DATA_DIRECTORY
{
public uint VirtualAddress; // DWORD VirtualAddress
public uint Size; // DWORD Size
}
/// <summary>
/// https://www.nirsoft.net/kernel_struct/vista/IMAGE_DOS_HEADER.html
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct IMAGE_DOS_HEADER
{
public ushort MagicNumber; // e_magic - Magic number (The value “MZ” are the initials of the PE designer Mark Zbikowski)
public ushort BytesOnLastPageOfFile; // e_cblp - Bytes on last page of file
public ushort PagesInFile; // e_cp - Pages in file
public ushort Relocations; // e_crlc - Relocations
public ushort SizeOfHeaderInParagraphs; // e_cparhdr - Size of header in paragraphs
public ushort MinimumExtraParagraphs; // e_minalloc - Minimum extra paragraphs needed
public ushort MaximumExtraParagraphs; // e_maxalloc - Maximum extra paragraphs needed
public ushort InitialSS; // e_ss - Initial (relative) SS value
public ushort InitialSP; // e_sp - Initial SP value
public ushort Checksum; // e_csum - Checksum
public ushort InitialIP; // e_ip - Initial IP value
public ushort InitialCS; // e_cs - Initial (relative) CS value
public ushort AddressOfRelocationTable; // e_lfarlc - File address of relocation table
public ushort OverlayNumber; // e_ovno - Overlay number
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
public ushort[] ReservedWords1; // e_res - Reserved words
public ushort OEMIdentifier; // e_oemid - OEM identifier (for e_oeminfo)
public ushort OEMInformation; // e_oeminfo - OEM information; e_oemid specific
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
public ushort[] ReservedWords2; // e_res2 - Reserved words
public int FileAddressOfNewExeHeader; // e_lfanew - File address of new exe header
}
/// <summary>
/// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_file_header
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct IMAGE_FILE_HEADER
{
/// <summary>
/// The architecture type of the computer.
/// An image file can only be run on the specified computer or a system that emulates the specified computer.
/// </summary>
public ushort Machine;
/// <summary>
/// The number of sections.
/// This indicates the size of the section table, which immediately follows the headers.
/// Note that the Windows loader limits the number of sections to 96.
/// </summary>
public ushort NumberOfSections;
/// <summary>
/// The low 32 bits of the time stamp of the image.
/// This represents the date and time the image was created by the linker.
/// The value is represented in the number of seconds elapsed since midnight (00:00:00), January 1, 1970, Universal Coordinated Time, according to the system clock.
/// </summary>
public uint TimeDateStamp;
public uint PointerToSymbolTable;
public uint NumberOfSymbols;
public ushort SizeOfOptionalHeader;
public ushort Characteristics;
}
/// <summary>
/// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_nt_headers32
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct IMAGE_NT_HEADERS32
{
public uint Signature; // DWORD Signature
public IMAGE_FILE_HEADER FileHeader; // IMAGE_FILE_HEADER FileHeader
public IMAGE_OPTIONAL_HEADER32 OptionalHeader; // IMAGE_OPTIONAL_HEADER32 OptionalHeader
}
/// <summary>
/// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_nt_headers64
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct IMAGE_NT_HEADERS64
{
public uint Signature; // DWORD Signature
public IMAGE_FILE_HEADER FileHeader; // IMAGE_FILE_HEADER FileHeader
public IMAGE_OPTIONAL_HEADER64 OptionalHeader; // IMAGE_OPTIONAL_HEADER32 OptionalHeader
}
/// <summary>
/// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_optional_header32
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct IMAGE_OPTIONAL_HEADER32
{
public ushort Magic;
public byte MajorLinkerVersion;
public byte MinorLinkerVersion;
public uint SizeOfCode;
public uint SizeOfInitializedData;
public uint SizeOfUninitializedData;
/// <summary>
/// A pointer to the entry point function, relative to the image base address.
/// For executable files, this is the starting address.
/// For device drivers, this is the address of the initialization function.
/// The entry point function is optional for DLLs.
/// When no entry point is present, this member is zero.
/// </summary>
public uint AddressOfEntryPoint;
/// <summary>
/// A pointer to the beginning of the code section, relative to the image base.
/// </summary>
public uint BaseOfCode;
/// <summary>
/// A pointer to the beginning of the data section, relative to the image base.
/// </summary>
public uint BaseOfData;
/// <summary>
/// The preferred address of the first byte of the image when it is loaded in memory.
/// This value is a multiple of 64K bytes.
/// The default value for DLLs is 0x10000000.
/// The default value for applications is 0x00400000, except on Windows CE where it is 0x00010000.
/// </summary>
public uint ImageBase;
public uint SectionAlignment;
public uint FileAlignment;
public ushort MajorOperatingSystemVersion;
public ushort MinorOperatingSystemVersion;
public ushort MajorImageVersion;
public ushort MinorImageVersion;
public ushort MajorSubsystemVersion;
public ushort MinorSubsystemVersion;
public uint Win32VersionValue;
/// <summary>
/// The size of the image, in bytes, including all headers. Must be a multiple of SectionAlignment.
/// </summary>
public uint SizeOfImage;
/// <summary>
/// The combined size of the following items, rounded to a multiple of the value specified in the FileAlignment member.
/// - e_lfanew member of IMAGE_DOS_HEADER
/// - 4 byte signature
/// - size of IMAGE_FILE_HEADER
/// - size of optional header
/// - size of all section headers
/// </summary>
public uint SizeOfHeaders;
public uint CheckSum;
public ushort Subsystem;
public ushort DllCharacteristics;
public uint SizeOfStackReserve;
public uint SizeOfStackCommit;
public uint SizeOfHeapReserve;
public uint SizeOfHeapCommit;
public uint LoaderFlags;
/// <summary>
/// The number of directory entries in the remainder of the optional header. Each entry describes a location and size.
/// </summary>
public uint NumberOfRvaAndSizes;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x10)]
public IMAGE_DATA_DIRECTORY[] DataDirectory;
}
/// <summary>
/// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_optional_header64
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct IMAGE_OPTIONAL_HEADER64
{
public ushort Magic;
public byte MajorLinkerVersion;
public byte MinorLinkerVersion;
public uint SizeOfCode;
public uint SizeOfInitializedData;
public uint SizeOfUninitializedData;
public uint AddressOfEntryPoint;
public uint BaseOfCode;
public ulong ImageBase;
public uint SectionAlignment;
public uint FileAlignment;
public ushort MajorOperatingSystemVersion;
public ushort MinorOperatingSystemVersion;
public ushort MajorImageVersion;
public ushort MinorImageVersion;
public ushort MajorSubsystemVersion;
public ushort MinorSubsystemVersion;
public uint Win32VersionValue;
public uint SizeOfImage;
public uint SizeOfHeaders;
public uint CheckSum;
public ushort Subsystem;
public ushort DllCharacteristics;
public ulong SizeOfStackReserve;
public ulong SizeOfStackCommit;
public ulong SizeOfHeapReserve;
public ulong SizeOfHeapCommit;
public uint LoaderFlags;
public uint NumberOfRvaAndSizes;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x10)]
public IMAGE_DATA_DIRECTORY[] DataDirectory;
}
/// <summary>
/// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_section_header
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct IMAGE_SECTION_HEADER
{
/// <summary>
/// An 8-byte, null-padded UTF-8 string.
/// There is no terminating null character if the string is exactly eight characters long.
/// For longer names, this member contains a forward slash (/) followed by an ASCII representation of a double number that is an offset into the string table.
/// Executable images do not use a string table and do not support section names longer than eight characters.
/// </summary>
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
public byte[] Name;
public UnionType Misc;
/// <summary>
/// The address of the first byte of the section when loaded into memory, relative to the image base.
/// For object files, this is the address of the first byte before relocation is applied.
/// </summary>
public uint VirtualAddress;
/// <summary>
/// The size of the initialized data on disk, in bytes.
/// This value must be a multiple of the FileAlignment member of the IMAGE_OPTIONAL_HEADER structure.
/// If this value is less than the VirtualSize member, the remainder of the section is filled with zeroes.
/// If the section contains only uninitialized data, the member is zero.
/// </summary>
public uint SizeOfRawData;
/// <summary>
/// A file pointer to the first page within the COFF file.
/// This value must be a multiple of the FileAlignment member of the IMAGE_OPTIONAL_HEADER structure.
/// If a section contains only uninitialized data, set this member is zero.
/// </summary>
public uint PointerToRawData;
public uint PointerToRelocations;
public uint PointerToLinenumbers;
public ushort NumberOfRelocations;
public ushort NumberOfLinenumbers;
public uint Characteristics;
[StructLayout(LayoutKind.Explicit)]
public struct UnionType
{
[FieldOffset(0)]
public uint PhysicalAddress;
/// <summary>
/// The total size of the section when loaded into memory, in bytes. If this value is greater than the SizeOfRawData member, the section is filled with zeroes.
/// This field is valid only for executable images and should be set to 0 for object files.
/// </summary>
[FieldOffset(0)]
public uint VirtualSize;
}
}

View File

@@ -0,0 +1,31 @@
using System.Runtime.InteropServices;
namespace RobotNet10.ScriptEditor.Helpers.Webcil;
internal static class StreamExtensions
{
internal static void WriteStruct<T>(this Stream stream, T structData) where T : struct
{
var bytes = StructToBytes(structData);
stream.Write(bytes);
}
private static byte[] StructToBytes<T>(T structData) where T : struct
{
int size = Marshal.SizeOf(structData);
byte[] byteArray = new byte[size];
nint ptr = Marshal.AllocHGlobal(size);
try
{
Marshal.StructureToPtr(structData, ptr, false);
Marshal.Copy(ptr, byteArray, 0, size);
}
finally
{
Marshal.FreeHGlobal(ptr);
}
return byteArray;
}
}

View File

@@ -0,0 +1,154 @@
namespace RobotNet10.ScriptEditor.Helpers.Webcil;
internal class WasmWebcilUnwrapper : IAsyncDisposable
{
private readonly Stream _wasmStream;
private MemoryStream? _cachedStream;
public WasmWebcilUnwrapper(Stream wasmStream)
{
_wasmStream = wasmStream;
}
public async Task WriteUnwrappedAsync(Stream outputStream)
{
// Cache the stream content to MemoryStream for synchronous BinaryReader operations
if (_cachedStream == null)
{
_cachedStream = new MemoryStream();
await _wasmStream.CopyToAsync(_cachedStream);
_cachedStream.Position = 0; // Reset to beginning for validation
}
// Validate prefix from cached stream
ValidateWasmPrefix(_cachedStream);
// Skip prefix and read data section
var prefix = WasmWebcilWrapper.GetPrefix();
_cachedStream.Position = prefix.Length;
using var reader = new BinaryReader(_cachedStream, System.Text.Encoding.UTF8, leaveOpen: true);
var bytes = ReadDataSection(reader);
await outputStream.WriteAsync(bytes);
}
private void ValidateWasmPrefix(Stream stream)
{
var originalPosition = stream.Position;
try
{
// Create a byte array matching the length of the prefix.
var prefix = WasmWebcilWrapper.GetPrefix();
var buffer = new byte[prefix.Length];
stream.Position = 0;
int bytesRead = stream.Read(buffer, 0, buffer.Length);
if (bytesRead < buffer.Length)
{
throw new InvalidOperationException("Unable to read Wasm prefix.");
}
// Compare the read prefix with the expected one.
if (!buffer.SequenceEqual(prefix))
{
throw new InvalidOperationException("Invalid Wasm prefix.");
}
}
finally
{
stream.Position = originalPosition;
}
}
private static void SkipSection(BinaryReader reader)
{
var size = ULEB128Decode(reader);
reader.BaseStream.Seek(size, SeekOrigin.Current);
}
private static byte[] ReadDataSection(BinaryReader reader)
{
// Skip until we find the data section, which contains the Webcil payload.
byte[] buffer = new byte[1];
while (true)
{
// Read the Data section
var dataRead = reader.Read(buffer, 0, 1);
if (dataRead == 0)
{
throw new InvalidOperationException("Unable to read Data Section.");
}
// Check the Data section (ID = 11)
if (buffer[0] == 11)
{
break;
}
// Skip other sections by reading and ignoring their content.
SkipSection(reader);
}
// Read and ignore the size of the data section.
ULEB128Decode(reader);
// Read the number of segments.
int segmentsCount = (int)ULEB128Decode(reader);
int lastSegment = segmentsCount - 1;
for (int segmentIndex = 0; segmentIndex < segmentsCount; segmentIndex++)
{
// Ignore segmentType (1 = passive segment)
var segmentType = reader.Read(buffer, 0, 1);
if (segmentType != 1)
{
throw new InvalidOperationException($"Unexpected segment code for segment {segmentIndex}.");
}
// Read the segment size.
var segmentSize = ULEB128Decode(reader);
// The actual Webcil payload is expected to be in the last segment.
if (segmentIndex == lastSegment)
{
return reader.ReadBytes((int)segmentSize);
}
// Skip other segments.
reader.BaseStream.Seek(segmentSize, SeekOrigin.Current);
}
throw new Exception("Unable to read DataSection.");
}
/// <summary>
/// Decodes a variable-length quantity (VLQ) encoded as unsigned LEB128.
/// LEB128 (Little Endian Base 128) is used to encode integers in a variable number of bytes.
/// The method reads bytes from the provided binary reader and decodes them into an unsigned integer.
/// </summary>
/// <param name="reader">The binary reader from which to read the ULEB128 encoded data.</param>
/// <returns>The decoded unsigned integer from the ULEB128 encoded data.</returns>
private static uint ULEB128Decode(BinaryReader reader)
{
uint result = 0;
int shift = 0;
byte byteValue;
do
{
byteValue = reader.ReadByte();
uint byteAsUInt = byteValue & 0x7Fu;
result |= byteAsUInt << shift;
shift += 7;
} while ((byteValue & 0x80) != 0);
return result;
}
public async ValueTask DisposeAsync()
{
if (_cachedStream != null)
{
await _cachedStream.DisposeAsync();
}
await _wasmStream.DisposeAsync();
}
}

View File

@@ -0,0 +1,22 @@
using System.Reflection;
namespace RobotNet10.ScriptEditor.Helpers.Webcil;
public class WasmWebcilWrapper
{
private static readonly FieldInfo FieldInfoPrefix = typeof(WebcilWasmWrapper).GetField("s_wasmWrapperPrefix", BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.GetField)!;
public static byte[] GetPrefix()
{
#if NET7_0_OR_GREATER
return GetPrefixValue<ReadOnlyMemory<byte>>().ToArray();
#else
return GetPrefixValue<byte[]>();
#endif
}
private static T GetPrefixValue<T>()
{
return (T)FieldInfoPrefix.GetValue(null)!;
}
}

View File

@@ -0,0 +1,53 @@
using System.Runtime.InteropServices;
namespace RobotNet10.ScriptEditor.Helpers.Webcil;
public class Webcil
{
/// <summary>
/// The header of a WebCIL file.
/// </summary>
///
/// <remarks>
/// The header is a subset of the PE, COFF and CLI headers that are needed by the mono runtime to load managed assemblies.
/// </remarks>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public unsafe struct WebcilHeader
{
public fixed byte id[4]; // 'W' 'b' 'I' 'L'
// 4 bytes
public ushort version_major; // 0
public ushort version_minor; // 0
// 8 bytes
public ushort coff_sections;
public ushort reserved0; // 0
// 12 bytes
public uint pe_cli_header_rva;
public uint pe_cli_header_size;
// 20 bytes
public uint pe_debug_rva;
public uint pe_debug_size;
// 28 bytes
}
/// <summary>
/// This is the Webcil analog of System.Reflection.PortableExecutable.SectionHeader, but with fewer fields
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct WebcilSectionHeader
{
public readonly int VirtualSize;
public readonly int VirtualAddress;
public readonly int SizeOfRawData;
public readonly int PointerToRawData;
public WebcilSectionHeader(int virtualSize, int virtualAddress, int sizeOfRawData, int pointerToRawData)
{
VirtualSize = virtualSize;
VirtualAddress = virtualAddress;
SizeOfRawData = sizeOfRawData;
PointerToRawData = pointerToRawData;
}
}
}

View File

@@ -0,0 +1,7 @@
namespace RobotNet10.ScriptEditor.Helpers.Webcil;
internal static unsafe class WebcilConstants
{
public const int WC_VERSION_MAJOR = 0;
public const int WC_VERSION_MINOR = 0;
}

View File

@@ -0,0 +1,370 @@
using System.Buffers.Binary;
using System.Collections.Immutable;
using System.Reflection.PortableExecutable;
namespace RobotNet10.ScriptEditor.Helpers.Webcil;
/// <summary>
/// Reads a .NET assembly in a normal PE COFF file and writes it out as a Webcil file
/// </summary>
public class WebcilConverter
{
// Interesting stuff we've learned about the input PE file
public record PEFileInfo(
// The sections in the PE file
ImmutableArray<SectionHeader> SectionHeaders,
// The location of the debug directory entries
DirectoryEntry DebugTableDirectory,
// The file offset of the sections, following the section directory
FilePosition SectionStart,
// The debug directory entries
ImmutableArray<DebugDirectoryEntry> DebugDirectoryEntries
);
// Intersting stuff we know about the webcil file we're writing
public record WCFileInfo(
// The header of the webcil file
Webcil.WebcilHeader Header,
// The section directory of the webcil file
ImmutableArray<Webcil.WebcilSectionHeader> SectionHeaders,
// The file offset of the sections, following the section directory
FilePosition SectionStart
);
private readonly string _inputPath;
private readonly string _outputPath;
private string InputPath => _inputPath;
public bool WrapInWebAssembly { get; set; } = true;
private WebcilConverter(string inputPath, string outputPath)
{
_inputPath = inputPath;
_outputPath = outputPath;
}
public static WebcilConverter FromPortableExecutable(string inputPath, string outputPath)
=> new WebcilConverter(inputPath, outputPath);
public void ConvertToWebcil()
{
using var inputStream = File.Open(_inputPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
PEFileInfo peInfo;
WCFileInfo wcInfo;
using (var peReader = new PEReader(inputStream, PEStreamOptions.LeaveOpen))
{
GatherInfo(peReader, out wcInfo, out peInfo);
}
using var outputStream = File.Open(_outputPath, FileMode.Create, FileAccess.Write);
if (!WrapInWebAssembly)
{
WriteConversionTo(outputStream, inputStream, peInfo, wcInfo);
}
else
{
// if wrapping in WASM, write the webcil payload to memory because we need to discover the length
// webcil is about the same size as the PE file
using var memoryStream = new MemoryStream(checked((int)inputStream.Length));
WriteConversionTo(memoryStream, inputStream, peInfo, wcInfo);
memoryStream.Flush();
var wrapper = new WebcilWasmWrapper(memoryStream);
memoryStream.Seek(0, SeekOrigin.Begin);
wrapper.WriteWasmWrappedWebcil(outputStream);
}
}
public void WriteConversionTo(Stream outputStream, FileStream inputStream, PEFileInfo peInfo, WCFileInfo wcInfo)
{
WriteHeader(outputStream, wcInfo.Header);
WriteSectionHeaders(outputStream, wcInfo.SectionHeaders);
CopySections(outputStream, inputStream, peInfo.SectionHeaders);
if (wcInfo.Header.pe_debug_size != 0 && wcInfo.Header.pe_debug_rva != 0)
{
var wcDebugDirectoryEntries = FixupDebugDirectoryEntries(peInfo, wcInfo);
OverwriteDebugDirectoryEntries(outputStream, wcInfo, wcDebugDirectoryEntries);
}
}
public record struct FilePosition(int Position)
{
public static implicit operator FilePosition(int position) => new(position);
public static FilePosition operator +(FilePosition left, int right) => new(left.Position + right);
}
private static unsafe int SizeOfHeader()
{
return sizeof(Webcil.WebcilHeader);
}
public unsafe void GatherInfo(PEReader peReader, out WCFileInfo wcInfo, out PEFileInfo peInfo)
{
var headers = peReader.PEHeaders;
var peHeader = headers.PEHeader!;
var coffHeader = headers.CoffHeader!;
var sections = headers.SectionHeaders;
Webcil.WebcilHeader header;
header.id[0] = (byte)'W';
header.id[1] = (byte)'b';
header.id[2] = (byte)'I';
header.id[3] = (byte)'L';
header.version_major = WebcilConstants.WC_VERSION_MAJOR;
header.version_minor = WebcilConstants.WC_VERSION_MINOR;
header.coff_sections = (ushort)coffHeader.NumberOfSections;
header.reserved0 = 0;
header.pe_cli_header_rva = (uint)peHeader.CorHeaderTableDirectory.RelativeVirtualAddress;
header.pe_cli_header_size = (uint)peHeader.CorHeaderTableDirectory.Size;
header.pe_debug_rva = (uint)peHeader.DebugTableDirectory.RelativeVirtualAddress;
header.pe_debug_size = (uint)peHeader.DebugTableDirectory.Size;
// current logical position in the output file
FilePosition pos = SizeOfHeader();
// position of the current section in the output file
// initially it's after all the section headers
FilePosition curSectionPos = pos + sizeof(Webcil.WebcilSectionHeader) * coffHeader.NumberOfSections;
// The first WC section is immediately after the section directory
FilePosition firstWCSection = curSectionPos;
FilePosition firstPESection = 0;
ImmutableArray<Webcil.WebcilSectionHeader>.Builder headerBuilder = ImmutableArray.CreateBuilder<Webcil.WebcilSectionHeader>(coffHeader.NumberOfSections);
foreach (var sectionHeader in sections)
{
// The first section is the one with the lowest file offset
if (firstPESection.Position == 0)
{
firstPESection = sectionHeader.PointerToRawData;
}
else
{
firstPESection = Math.Min(firstPESection.Position, sectionHeader.PointerToRawData);
}
var newHeader = new Webcil.WebcilSectionHeader
(
virtualSize: sectionHeader.VirtualSize,
virtualAddress: sectionHeader.VirtualAddress,
sizeOfRawData: sectionHeader.SizeOfRawData,
pointerToRawData: curSectionPos.Position
);
pos += sizeof(Webcil.WebcilSectionHeader);
curSectionPos += sectionHeader.SizeOfRawData;
headerBuilder.Add(newHeader);
}
ImmutableArray<DebugDirectoryEntry> debugDirectoryEntries = peReader.ReadDebugDirectory();
peInfo = new PEFileInfo(SectionHeaders: sections,
DebugTableDirectory: peHeader.DebugTableDirectory,
SectionStart: firstPESection,
DebugDirectoryEntries: debugDirectoryEntries);
wcInfo = new WCFileInfo(Header: header,
SectionHeaders: headerBuilder.MoveToImmutable(),
SectionStart: firstWCSection);
}
private static void WriteHeader(Stream s, Webcil.WebcilHeader webcilHeader)
{
if (!BitConverter.IsLittleEndian)
{
webcilHeader.version_major = BinaryPrimitives.ReverseEndianness(webcilHeader.version_major);
webcilHeader.version_minor = BinaryPrimitives.ReverseEndianness(webcilHeader.version_minor);
webcilHeader.coff_sections = BinaryPrimitives.ReverseEndianness(webcilHeader.coff_sections);
webcilHeader.pe_cli_header_rva = BinaryPrimitives.ReverseEndianness(webcilHeader.pe_cli_header_rva);
webcilHeader.pe_cli_header_size = BinaryPrimitives.ReverseEndianness(webcilHeader.pe_cli_header_size);
webcilHeader.pe_debug_rva = BinaryPrimitives.ReverseEndianness(webcilHeader.pe_debug_rva);
webcilHeader.pe_debug_size = BinaryPrimitives.ReverseEndianness(webcilHeader.pe_debug_size);
}
WriteStructure(s, webcilHeader);
}
private static void WriteSectionHeaders(Stream s, ImmutableArray<Webcil.WebcilSectionHeader> sectionsHeaders)
{
foreach (var sectionHeader in sectionsHeaders)
{
WriteSectionHeader(s, sectionHeader);
}
}
private static void WriteSectionHeader(Stream s, Webcil.WebcilSectionHeader sectionHeader)
{
if (!BitConverter.IsLittleEndian)
{
sectionHeader = new Webcil.WebcilSectionHeader
(
virtualSize: BinaryPrimitives.ReverseEndianness(sectionHeader.VirtualSize),
virtualAddress: BinaryPrimitives.ReverseEndianness(sectionHeader.VirtualAddress),
sizeOfRawData: BinaryPrimitives.ReverseEndianness(sectionHeader.SizeOfRawData),
pointerToRawData: BinaryPrimitives.ReverseEndianness(sectionHeader.PointerToRawData)
);
}
WriteStructure(s, sectionHeader);
}
#if NETCOREAPP2_1_OR_GREATER
private static void WriteStructure<T>(Stream s, T structure)
where T : unmanaged
{
unsafe
{
byte* p = (byte*)&structure;
s.Write(new ReadOnlySpan<byte>(p, sizeof(T)));
}
}
#else
private static void WriteStructure<T>(Stream s, T structure)
where T : unmanaged
{
int size = Marshal.SizeOf<T>();
byte[] buffer = new byte[size];
IntPtr ptr = IntPtr.Zero;
try
{
ptr = Marshal.AllocHGlobal(size);
Marshal.StructureToPtr(structure, ptr, false);
Marshal.Copy(ptr, buffer, 0, size);
}
finally
{
Marshal.FreeHGlobal(ptr);
}
s.Write(buffer, 0, size);
}
#endif
private static void CopySections(Stream outStream, FileStream inputStream, ImmutableArray<SectionHeader> peSections)
{
// endianness: ok, we're just copying from one stream to another
foreach (var peHeader in peSections)
{
var buffer = new byte[peHeader.SizeOfRawData];
inputStream.Seek(peHeader.PointerToRawData, SeekOrigin.Begin);
ReadExactly(inputStream, buffer);
outStream.Write(buffer, 0, buffer.Length);
}
}
#if NETCOREAPP2_1_OR_GREATER && !NET6_0
private static void ReadExactly(FileStream s, Span<byte> buffer)
{
s.ReadExactly(buffer);
}
#else
private static void ReadExactly(FileStream s, byte[] buffer)
{
int offset = 0;
while (offset < buffer.Length)
{
int read = s.Read(buffer, offset, buffer.Length - offset);
if (read == 0)
throw new EndOfStreamException();
offset += read;
}
}
#endif
private static FilePosition GetPositionOfRelativeVirtualAddress(ImmutableArray<Webcil.WebcilSectionHeader> wcSections, uint relativeVirtualAddress)
{
foreach (var section in wcSections)
{
if (relativeVirtualAddress >= section.VirtualAddress && relativeVirtualAddress < section.VirtualAddress + section.VirtualSize)
{
FilePosition pos = section.PointerToRawData + ((int)relativeVirtualAddress - section.VirtualAddress);
return pos;
}
}
throw new InvalidOperationException("relative virtual address not in any section");
}
// Given a physical file offset, return the section and the offset within the section.
private (Webcil.WebcilSectionHeader section, int offset) GetSectionFromFileOffset(ImmutableArray<Webcil.WebcilSectionHeader> peSections, FilePosition fileOffset)
{
foreach (var section in peSections)
{
if (fileOffset.Position >= section.PointerToRawData && fileOffset.Position < section.PointerToRawData + section.SizeOfRawData)
{
return (section, fileOffset.Position - section.PointerToRawData);
}
}
throw new InvalidOperationException($"file offset not in any section (Webcil) for {InputPath}");
}
private void GetSectionFromFileOffset(ImmutableArray<SectionHeader> sections, FilePosition fileOffset)
{
foreach (var section in sections)
{
if (fileOffset.Position >= section.PointerToRawData && fileOffset.Position < section.PointerToRawData + section.SizeOfRawData)
{
return;
}
}
throw new InvalidOperationException($"file offset {fileOffset.Position} not in any section (PE) for {InputPath}");
}
// Make a new set of debug directory entries that
// have their data pointers adjusted to be relative to the start of the webcil file.
// This is necessary because the debug directory entires in the PE file are relative to the start of the PE file,
// and a PE header is bigger than a webcil header.
private ImmutableArray<DebugDirectoryEntry> FixupDebugDirectoryEntries(PEFileInfo peInfo, WCFileInfo wcInfo)
{
int dataPointerAdjustment = peInfo.SectionStart.Position - wcInfo.SectionStart.Position;
ImmutableArray<DebugDirectoryEntry> entries = peInfo.DebugDirectoryEntries;
ImmutableArray<DebugDirectoryEntry>.Builder newEntries = ImmutableArray.CreateBuilder<DebugDirectoryEntry>(entries.Length);
foreach (var entry in entries)
{
DebugDirectoryEntry newEntry;
if (entry.Type == DebugDirectoryEntryType.Reproducible || entry.DataPointer == 0 || entry.DataSize == 0)
{
// this entry doesn't have an associated data pointer, so just copy it
newEntry = entry;
}
else
{
// the "DataPointer" field is a file offset in the PE file, adjust the entry wit the corresponding offset in the Webcil file
var newDataPointer = entry.DataPointer - dataPointerAdjustment;
newEntry = new DebugDirectoryEntry(entry.Stamp, entry.MajorVersion, entry.MinorVersion, entry.Type, entry.DataSize, entry.DataRelativeVirtualAddress, newDataPointer);
GetSectionFromFileOffset(peInfo.SectionHeaders, entry.DataPointer);
// validate that the new entry is in some section
GetSectionFromFileOffset(wcInfo.SectionHeaders, newDataPointer);
}
newEntries.Add(newEntry);
}
return newEntries.MoveToImmutable();
}
private static void OverwriteDebugDirectoryEntries(Stream s, WCFileInfo wcInfo, ImmutableArray<DebugDirectoryEntry> entries)
{
FilePosition debugDirectoryPos = GetPositionOfRelativeVirtualAddress(wcInfo.SectionHeaders, wcInfo.Header.pe_debug_rva);
using var writer = new BinaryWriter(s, System.Text.Encoding.UTF8, leaveOpen: true);
writer.Seek(debugDirectoryPos.Position, SeekOrigin.Begin);
foreach (var entry in entries)
{
WriteDebugDirectoryEntry(writer, entry);
}
// TODO check that we overwrite with the same size as the original
// restore the stream position
writer.Seek(0, SeekOrigin.End);
}
private static void WriteDebugDirectoryEntry(BinaryWriter writer, DebugDirectoryEntry entry)
{
writer.Write((uint)0); // Characteristics
writer.Write(entry.Stamp);
writer.Write(entry.MajorVersion);
writer.Write(entry.MinorVersion);
writer.Write((uint)entry.Type);
writer.Write(entry.DataSize);
writer.Write(entry.DataRelativeVirtualAddress);
writer.Write(entry.DataPointer);
}
}

View File

@@ -0,0 +1,379 @@
using System.Buffers.Binary;
using System.Collections.Immutable;
using System.Runtime.InteropServices;
using Microsoft.CodeAnalysis;
namespace RobotNet10.ScriptEditor.Helpers.Webcil;
internal static class WebcilConverterUtil
{
private static readonly byte[] SectionHeaderText = { 0x2E, 0x74, 0x65, 0x78, 0x74, 0x00, 0x00, 0x00 }; // .text
private static readonly byte[] SectionHeaderRsRc = { 0x2E, 0x72, 0x73, 0x72, 0x63, 0x00, 0x00, 0x00 }; // .rsrc
private static readonly byte[] SectionHeaderReloc = { 0x2E, 0x72, 0x65, 0x6C, 0x6F, 0x63, 0x00, 0x00 }; // .reloc
private static readonly byte[] MSDOS =
{
0x0E, 0x1F, 0xBA, 0x0E, 0x00, 0xB4, 0x09, 0xCD, 0x21, 0xB8, 0x01, 0x4C, 0xCD, 0x21, 0x54, 0x68,
0x69, 0x73, 0x20, 0x70, 0x72, 0x6F, 0x67, 0x72, 0x61, 0x6D, 0x20, 0x63, 0x61, 0x6E, 0x6E, 0x6F,
0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x75, 0x6E, 0x20, 0x69, 0x6E, 0x20, 0x44, 0x4F, 0x53, 0x20,
0x6D, 0x6F, 0x64, 0x65, 0x2E, 0x0D, 0x0D, 0x0A, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
private static readonly ushort[] DOSReservedWords1 = { 0, 0, 0, 0 };
private static readonly ushort[] DOSReservedWords2 = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
private static readonly DateTime Epoch = new(1970, 1, 1);
private static readonly int SizeofDOSHeader = Marshal.SizeOf<IMAGE_DOS_HEADER>(); // 64
private static readonly int SizeofFileHeader = Marshal.SizeOf<IMAGE_FILE_HEADER>();
private static readonly int SizeofMSDOS = MSDOS.Length; // 64
private static readonly int SizeofNTHeaders = Marshal.SizeOf<IMAGE_NT_HEADERS32>(); // 248
private static readonly int SizeofOptionalHeader = Marshal.SizeOf<IMAGE_OPTIONAL_HEADER32>();
private static readonly int SizeofSectionHeader = Marshal.SizeOf<IMAGE_SECTION_HEADER>(); // 40
private const uint FileAlignment = 0x0200;
private const uint SectionAlignment = 0x2000;
/// <summary>
/// Convert a Portable Executable file into a Webcil file.
/// </summary>
/// <param name="inputPath">The input path for the PE file.</param>
/// <param name="outputPath">The output path for the Webcil file.</param>
/// <param name="wrapInWebAssembly">The Webcil should be wrapped in Wasm [default value is <c>true</c>].</param>
public static void ConvertToWebcil(string inputPath, string outputPath, bool wrapInWebAssembly = true)
{
var webcilConverter = WebcilConverter.FromPortableExecutable(inputPath, outputPath);
webcilConverter.WrapInWebAssembly = wrapInWebAssembly;
webcilConverter.ConvertToWebcil();
}
/// <summary>
/// Convert a Webcil stream into a Portable Executable which can be used to create a valid <see cref="MetadataReference"/>.
/// </summary>
/// <param name="inputStream">The input sStream.</param>
/// <param name="wrappedInWebAssembly">The Webcil is wrapped in Wasm [default value is <c>true</c>].</param>
/// <returns>A byte[] Portable Executable</returns>
public static async Task<byte[]> ConvertFromWebcilAsync(Stream inputStream, bool wrappedInWebAssembly = true)
{
Stream webcilStream;
if (wrappedInWebAssembly)
{
await using var unwrapper = new WasmWebcilUnwrapper(inputStream);
webcilStream = new MemoryStream();
await unwrapper.WriteUnwrappedAsync(webcilStream);
webcilStream.Flush();
webcilStream.Seek(0, SeekOrigin.Begin);
}
else
{
webcilStream = inputStream;
}
// These are Webcil variables
var webcilHeader = ReadHeader(webcilStream);
var webcilSectionHeaders = ReadSectionHeaders(webcilStream, webcilHeader.coff_sections);
var webcilSectionHeadersCount = webcilSectionHeaders.Length;
var webcilSectionHeadersSizeOfRawData = (uint)webcilSectionHeaders.Sum(x => x.SizeOfRawData);
// These are PE (Portable Executable) variables
int sectionStart = SizeofDOSHeader + SizeofMSDOS + SizeofNTHeaders + webcilSectionHeadersCount * SizeofSectionHeader; // 496
int sectionStartRounded = sectionStart.RoundToNearest();
var extraBytesAfterSections = new byte[sectionStartRounded - sectionStart];
var pointerToRawDataFirstSectionHeader = webcilSectionHeaders[0].PointerToRawData;
var pointerToRawDataOffsetBetweenWebcilAndPE = sectionStartRounded - pointerToRawDataFirstSectionHeader;
using var peStream = new MemoryStream();
var DOSHeader = new IMAGE_DOS_HEADER
{
MagicNumber = 0x5A4D,
BytesOnLastPageOfFile = 0x90,
PagesInFile = 3,
Relocations = 0,
SizeOfHeaderInParagraphs = 4,
MinimumExtraParagraphs = 0,
MaximumExtraParagraphs = 0xFFFF,
InitialSS = 0,
InitialSP = 0xB8,
Checksum = 0,
InitialIP = 0,
InitialCS = 0,
AddressOfRelocationTable = 0x40,
OverlayNumber = 0,
ReservedWords1 = DOSReservedWords1,
OEMIdentifier = 0,
OEMInformation = 0,
ReservedWords2 = DOSReservedWords2,
FileAddressOfNewExeHeader = 0x80
};
peStream.WriteStruct(DOSHeader);
peStream.Write(MSDOS);
var IMAGE_NT_HEADERS32 = new IMAGE_NT_HEADERS32
{
Signature = 0x4550, // 'PE'
FileHeader = new IMAGE_FILE_HEADER
{
Machine = Constants.IMAGE_FILE_MACHINE_I386,
NumberOfSections = 3,
TimeDateStamp = GetImageTimestamp(),
PointerToSymbolTable = 0,
NumberOfSymbols = 0,
SizeOfOptionalHeader = 0x00E0,
Characteristics = 0x0022
},
OptionalHeader = new IMAGE_OPTIONAL_HEADER32
{
Magic = 0x010B, // Signature/Magic - Represents PE32 for 32-bit (0x10b) and PE32+ for 64-bit (0x20B)
MajorLinkerVersion = 0x30,
MinorLinkerVersion = 0,
SizeOfCode = (uint)webcilSectionHeaders[0].SizeOfRawData,
SizeOfInitializedData = (uint)(webcilSectionHeaders[1].SizeOfRawData + webcilSectionHeaders[2].SizeOfRawData),
SizeOfUninitializedData = 0,
AddressOfEntryPoint = 0, // This can be set to 0
BaseOfCode = 0x2000,
BaseOfData = 0xA000,
ImageBase = 0x400000, // The default value for applications is 0x00400000
SectionAlignment = SectionAlignment,
FileAlignment = FileAlignment,
MajorOperatingSystemVersion = 4,
MinorOperatingSystemVersion = 0,
MajorImageVersion = 0,
MinorImageVersion = 0,
MajorSubsystemVersion = 4,
MinorSubsystemVersion = 0,
Win32VersionValue = 0,
SizeOfImage = webcilSectionHeadersSizeOfRawData.RoundToNearest(SectionAlignment),
SizeOfHeaders = GetSizeOfHeaders(DOSHeader, webcilSectionHeadersCount),
CheckSum = 0,
Subsystem = 3, // IMAGE_SUBSYSTEM_WINDOWS_CUI
DllCharacteristics = 0x8560,
SizeOfStackReserve = 0x100000,
SizeOfStackCommit = 0x1000,
SizeOfHeapReserve = 0x100000,
SizeOfHeapCommit = 0x1000,
LoaderFlags = 0,
NumberOfRvaAndSizes = 0x10,
DataDirectory = new IMAGE_DATA_DIRECTORY[]
{
new() { Size = 0x0000, VirtualAddress = 0x0000 }, // IMAGE_DIRECTORY_ENTRY_EXPORT
new() { Size = 0x0000, VirtualAddress = 0x0000 }, // IMAGE_DIRECTORY_ENTRY_IMPORT (can be 0)
new() { Size = (uint) webcilSectionHeaders[1].VirtualSize, VirtualAddress = (uint) webcilSectionHeaders[1].VirtualAddress }, // IMAGE_DIRECTORY_ENTRY_RESOURCE
new() { Size = 0x0000, VirtualAddress = 0x0000 }, // IMAGE_DIRECTORY_ENTRY_EXCEPTION
new() { Size = 0x0000, VirtualAddress = 0x0000 }, // IMAGE_DIRECTORY_ENTRY_SECURITY
new() { Size = (uint) webcilSectionHeaders[2].VirtualSize, VirtualAddress = (uint) webcilSectionHeaders[2].VirtualAddress }, // IMAGE_DIRECTORY_ENTRY_BASERELOC
new() { Size = 0x0000, VirtualAddress = 0x0000 }, // IMAGE_DIRECTORY_ENTRY_DEBUG (can be 0)
new() { Size = 0x0000, VirtualAddress = 0x0000 }, // IMAGE_DIRECTORY_ENTRY_ARCHITECTURE
new() { Size = 0x0000, VirtualAddress = 0x0000 }, // IMAGE_DIRECTORY_ENTRY_GLOBALPTR
new() { Size = 0x0000, VirtualAddress = 0x0000 }, // IMAGE_DIRECTORY_ENTRY_TLS
new() { Size = 0x0000, VirtualAddress = 0x0000 }, // IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG
new() { Size = 0x0000, VirtualAddress = 0x0000 }, // IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT
new() { Size = 0x0008, VirtualAddress = (uint) webcilSectionHeaders[0].VirtualAddress }, // IMAGE_DIRECTORY_ENTRY_IAT
new() { Size = 0x0000, VirtualAddress = 0x0000 }, // IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT
new() { Size = 0x0048, VirtualAddress = (uint) webcilSectionHeaders[0].VirtualAddress + 8 }, // TODO ??? IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR
new() { Size = 0x0000, VirtualAddress = 0x0000 } // ?
}
}
};
peStream.WriteStruct(IMAGE_NT_HEADERS32);
var textSectionHeader = new IMAGE_SECTION_HEADER
{
Name = SectionHeaderText,
Misc = new IMAGE_SECTION_HEADER.UnionType { VirtualSize = (uint)webcilSectionHeaders[0].VirtualSize },
VirtualAddress = (uint)webcilSectionHeaders[0].VirtualAddress,
SizeOfRawData = (uint)webcilSectionHeaders[0].SizeOfRawData,
PointerToRawData = webcilSectionHeaders[0].GetCorrectedPointerToRawData(pointerToRawDataOffsetBetweenWebcilAndPE),
Characteristics = 0x60000020
};
peStream.WriteStruct(textSectionHeader);
var rsrcSectionHeader = new IMAGE_SECTION_HEADER
{
Name = SectionHeaderRsRc,
Misc = new IMAGE_SECTION_HEADER.UnionType { VirtualSize = (uint)webcilSectionHeaders[1].VirtualSize },
VirtualAddress = (uint)webcilSectionHeaders[1].VirtualAddress,
SizeOfRawData = (uint)webcilSectionHeaders[1].SizeOfRawData,
PointerToRawData = webcilSectionHeaders[1].GetCorrectedPointerToRawData(pointerToRawDataOffsetBetweenWebcilAndPE),
Characteristics = 0x40000040
};
peStream.WriteStruct(rsrcSectionHeader);
var relocSectionHeader = new IMAGE_SECTION_HEADER
{
Name = SectionHeaderReloc,
Misc = new IMAGE_SECTION_HEADER.UnionType { VirtualSize = (uint)webcilSectionHeaders[2].VirtualSize },
VirtualAddress = (uint)webcilSectionHeaders[2].VirtualAddress,
SizeOfRawData = (uint)webcilSectionHeaders[2].SizeOfRawData,
PointerToRawData = webcilSectionHeaders[2].GetCorrectedPointerToRawData(pointerToRawDataOffsetBetweenWebcilAndPE),
Characteristics = 0x42000040
};
peStream.WriteStruct(relocSectionHeader);
if (extraBytesAfterSections.Length > 0)
{
peStream.Write(extraBytesAfterSections);
}
// Just copy all data
foreach (var webcilSectionHeader in webcilSectionHeaders)
{
var buffer = new byte[webcilSectionHeader.SizeOfRawData];
webcilStream.Seek(webcilSectionHeader.PointerToRawData, SeekOrigin.Begin);
ReadExactly(webcilStream, buffer);
peStream.Write(buffer, 0, buffer.Length);
}
peStream.Flush();
peStream.Seek(0, SeekOrigin.Begin);
return peStream.ToArray();
}
private static Webcil.WebcilHeader ReadHeader(Stream webcilStream)
{
var webcilHeader = ReadStructure<Webcil.WebcilHeader>(webcilStream);
if (!BitConverter.IsLittleEndian)
{
webcilHeader.version_major = BinaryPrimitives.ReverseEndianness(webcilHeader.version_major);
webcilHeader.version_minor = BinaryPrimitives.ReverseEndianness(webcilHeader.version_minor);
webcilHeader.coff_sections = BinaryPrimitives.ReverseEndianness(webcilHeader.coff_sections);
webcilHeader.pe_cli_header_rva = BinaryPrimitives.ReverseEndianness(webcilHeader.pe_cli_header_rva);
webcilHeader.pe_cli_header_size = BinaryPrimitives.ReverseEndianness(webcilHeader.pe_cli_header_size);
webcilHeader.pe_debug_rva = BinaryPrimitives.ReverseEndianness(webcilHeader.pe_debug_rva);
webcilHeader.pe_debug_size = BinaryPrimitives.ReverseEndianness(webcilHeader.pe_debug_size);
}
return webcilHeader;
}
private static ImmutableArray<Webcil.WebcilSectionHeader> ReadSectionHeaders(Stream webcilStream, int sectionsHeaders)
{
var result = new List<Webcil.WebcilSectionHeader>();
for (int i = 0; i < sectionsHeaders; i++)
{
result.Add(ReadSectionHeader(webcilStream));
}
return ImmutableArray.Create(result.ToArray());
}
private static Webcil.WebcilSectionHeader ReadSectionHeader(Stream webcilStream)
{
var sectionHeader = ReadStructure<Webcil.WebcilSectionHeader>(webcilStream);
if (!BitConverter.IsLittleEndian)
{
sectionHeader = new Webcil.WebcilSectionHeader
(
virtualSize: BinaryPrimitives.ReverseEndianness(sectionHeader.VirtualSize),
virtualAddress: BinaryPrimitives.ReverseEndianness(sectionHeader.VirtualAddress),
sizeOfRawData: BinaryPrimitives.ReverseEndianness(sectionHeader.SizeOfRawData),
pointerToRawData: BinaryPrimitives.ReverseEndianness(sectionHeader.PointerToRawData)
);
}
return sectionHeader;
}
private static uint GetSizeOfHeaders(IMAGE_DOS_HEADER IMAGE_DOS_HEADER, int numSectionHeaders)
{
var soh = IMAGE_DOS_HEADER.FileAddressOfNewExeHeader + // e_lfanew member of IMAGE_DOS_HEADER
sizeof(uint) + // 4 byte signature
SizeofFileHeader +
SizeofOptionalHeader + // size of optional header
numSectionHeaders * SizeofSectionHeader // size of all section headers
;
return (uint)soh.RoundToNearest();
}
/// <summary>
/// The low 32 bits of the time stamp of the image.
/// This represents the date and time the image was created by the linker.
/// The value is represented in the number of seconds elapsed since midnight (00:00:00), January 1, 1970, Universal Coordinated Time, according to the system clock.
/// </summary>
private static uint GetImageTimestamp()
{
// Calculate the total seconds since Unix epoch
var totalSeconds = (DateTime.UtcNow - Epoch).Ticks / TimeSpan.TicksPerSecond;
// Convert to uint (low 32 bits)
return (uint)totalSeconds;
}
internal static int RoundToNearest(this int number, int nearest = 512)
{
int remainder = number % nearest;
int halfNearest = nearest / 2;
return remainder >= halfNearest ? number + nearest - remainder : number - remainder;
}
internal static uint RoundToNearest(this uint number, uint nearest = 512)
{
uint remainder = number % nearest;
uint halfNearest = nearest / 2;
return remainder >= halfNearest ? number + nearest - remainder : number - remainder;
}
#if NETCOREAPP2_1_OR_GREATER
private static T ReadStructure<T>(Stream s) where T : unmanaged
{
T structure = default;
unsafe
{
byte* p = (byte*)&structure;
Span<byte> buffer = new Span<byte>(p, sizeof(T));
int read = s.Read(buffer);
if (read != sizeof(T))
{
throw new InvalidOperationException("Couldn't read the full structure from the stream.");
}
}
return structure;
}
#else
private static T ReadStructure<T>(Stream s) where T : unmanaged
{
int size = Marshal.SizeOf<T>();
byte[] buffer = new byte[size];
s.Read(buffer, 0, size);
IntPtr ptr = IntPtr.Zero;
try
{
ptr = Marshal.AllocHGlobal(size);
Marshal.Copy(buffer, 0, ptr, size);
return Marshal.PtrToStructure<T>(ptr);
}
finally
{
Marshal.FreeHGlobal(ptr);
}
}
#endif
#if NETCOREAPP2_1_OR_GREATER && !NET6_0
private static void ReadExactly(Stream s, Span<byte> buffer)
{
s.ReadExactly(buffer);
}
#else
private static void ReadExactly(Stream s, byte[] buffer)
{
int offset = 0;
while (offset < buffer.Length)
{
int read = s.Read(buffer, offset, buffer.Length - offset);
if (read == 0)
{
throw new EndOfStreamException();
}
offset += read;
}
}
#endif
}

View File

@@ -0,0 +1,9 @@
namespace RobotNet10.ScriptEditor.Helpers.Webcil;
internal static class WebcilSectionHeaderExtensions
{
internal static uint GetCorrectedPointerToRawData(this Webcil.WebcilSectionHeader webcilSectionHeader, int offset)
{
return (uint) (webcilSectionHeader.PointerToRawData + offset);
}
}

View File

@@ -0,0 +1,222 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
//
// Emits a simple WebAssembly wrapper module around a given webcil payload.
//
// The entire wasm module is going to be unchanging, except for the data section which has 2 passive
// segments. segment 0 is 4 bytes and contains the length of the webcil payload. segment 1 is of a
// variable size and contains the webcil payload.
//
// The unchanging parts are stored as a "prefix" and "suffix" which contain the bytes for the following
// WAT module, split into the parts that come before the data section, and the bytes that come after:
//
// (module
// (data "\0\00\00\00") ;; data segment 0: payload size as a 4 byte LE uint32
// (data "webcil Payload\cc") ;; data segment 1: webcil payload
// (memory (import "webcil" "memory") 1)
// (global (export "webcilVersion") i32 (i32.const 0))
// (func (export "getWebcilSize") (param $destPtr i32) (result)
// local.get $destPtr
// i32.const 0
// i32.const 4
// memory.init 0)
// (func (export "getWebcilPayload") (param $d i32) (param $n i32) (result)
// local.get $d
// i32.const 0
// local.get $n
// memory.init 1))
namespace RobotNet10.ScriptEditor.Helpers.Webcil;
public class WebcilWasmWrapper
{
private readonly Stream _webcilPayloadStream;
private readonly uint _webcilPayloadSize;
public WebcilWasmWrapper(Stream webcilPayloadStream)
{
_webcilPayloadStream = webcilPayloadStream;
long len = webcilPayloadStream.Length;
if (len > (long)uint.MaxValue)
throw new InvalidOperationException("webcil payload too large");
_webcilPayloadSize = (uint)len;
}
public void WriteWasmWrappedWebcil(Stream outputStream)
{
WriteWasmHeader(outputStream);
using (var writer = new BinaryWriter(outputStream, System.Text.Encoding.UTF8, leaveOpen: true))
{
WriteDataSection(writer);
}
WriteWasmSuffix(outputStream);
}
//
// Everything from the above wat module before the data section
//
// extracted by wasm-reader -s wrapper.wasm
private static
#if NET7_0_OR_GREATER
ReadOnlyMemory<byte>
#else
byte[]
#endif
s_wasmWrapperPrefix = new byte[] {
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x0a, 0x02, 0x60, 0x01, 0x7f, 0x00, 0x60, 0x02, 0x7f, 0x7f, 0x00, 0x02, 0x12, 0x01, 0x06, 0x77, 0x65, 0x62, 0x63, 0x69, 0x6c, 0x06, 0x6d,
0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, 0x01, 0x03, 0x03, 0x02, 0x00, 0x01, 0x06, 0x0b, 0x02, 0x7f, 0x00, 0x41, 0x00, 0x0b, 0x7f, 0x00, 0x41, 0x00, 0x0b, 0x07, 0x41, 0x04, 0x0d, 0x77, 0x65,
0x62, 0x63, 0x69, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x03, 0x00, 0x0a, 0x77, 0x65, 0x62, 0x63, 0x69, 0x6c, 0x53, 0x69, 0x7a, 0x65, 0x03, 0x01, 0x0d, 0x67, 0x65, 0x74, 0x57, 0x65,
0x62, 0x63, 0x69, 0x6c, 0x53, 0x69, 0x7a, 0x65, 0x00, 0x00, 0x10, 0x67, 0x65, 0x74, 0x57, 0x65, 0x62, 0x63, 0x69, 0x6c, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x00, 0x01, 0x0c, 0x01, 0x02,
0x0a, 0x1b, 0x02, 0x0c, 0x00, 0x20, 0x00, 0x41, 0x00, 0x41, 0x04, 0xfc, 0x08, 0x00, 0x00, 0x0b, 0x0c, 0x00, 0x20, 0x00, 0x41, 0x00, 0x20, 0x01, 0xfc, 0x08, 0x01, 0x00, 0x0b,
};
//
// Everything from the above wat module after the data section
//
// extracted by wasm-reader -s wrapper.wasm
private static
#if NET7_0_OR_GREATER
ReadOnlyMemory<byte>
#else
byte[]
#endif
s_wasmWrapperSuffix = new byte[] {
0x00, 0x1b, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x02, 0x14, 0x02, 0x00, 0x01, 0x00, 0x07, 0x64, 0x65, 0x73, 0x74, 0x50, 0x74, 0x72, 0x01, 0x02, 0x00, 0x01, 0x64, 0x01, 0x01, 0x6e,
};
private static void WriteWasmHeader(Stream outputStream)
{
#if NET7_0_OR_GREATER
outputStream.Write(s_wasmWrapperPrefix.Span);
#else
outputStream.Write(s_wasmWrapperPrefix, 0, s_wasmWrapperPrefix.Length);
#endif
}
private static void WriteWasmSuffix(Stream outputStream)
{
#if NET7_0_OR_GREATER
outputStream.Write(s_wasmWrapperSuffix.Span);
#else
outputStream.Write(s_wasmWrapperSuffix, 0, s_wasmWrapperSuffix.Length);
#endif
}
// 1 byte to encode "passive" data segment
private const uint SegmentCodeSize = 1;
// Align the payload start to a 4-byte boundary within the wrapper. If the runtime reads the
// payload directly, instead of by instantiatng the wasm module, we don't want the WebAssembly
// prefix to push some of the values inside the image to odd byte offsets as the runtime assumes
// the image will be aligned.
//
// There are requirements in ECMA-335 (Section II.25.4) that fat method headers and method data
// sections be 4-byte aligned.
private const uint WebcilPayloadInternalAlignment = 4;
private void WriteDataSection(BinaryWriter writer)
{
uint dataSectionSize = 0;
// uleb128 encoding of number of segments
dataSectionSize += 1; // there's always 2 segments which encodes to 1 byte
// compute the segment 0 size:
// segment 0 has 1 byte segment code, 1 byte of size and at least 4 bytes of payload
uint segment0MinimumSize = SegmentCodeSize + 1 + 4;
dataSectionSize += segment0MinimumSize;
// encode webcil size as a uleb128
byte[] ulebWebcilPayloadSize = ULEB128Encode(_webcilPayloadSize);
// compute the segment 1 size:
// segment 1 has 1 byte segment code, a uleb128 encoding of the webcilPayloadSize, and the payload
// don't count the size of the payload yet
checked
{
dataSectionSize += SegmentCodeSize + (uint)ulebWebcilPayloadSize.Length;
}
// at this point the data section size includes everything except the data section code, the data section size and the webcil payload itself
// and any extra padding that we may want to add to segment 0.
// So we can compute the offset of the payload within the wasm module.
byte[] putativeULEBDataSectionSize = ULEB128Encode(dataSectionSize + _webcilPayloadSize);
uint payloadOffset = (uint)s_wasmWrapperPrefix.Length + 1 + (uint)putativeULEBDataSectionSize.Length + dataSectionSize ;
uint paddingSize = PadTo(payloadOffset, WebcilPayloadInternalAlignment);
if (paddingSize > 0)
{
checked
{
dataSectionSize += paddingSize;
}
}
checked
{
dataSectionSize += _webcilPayloadSize;
}
byte[] ulebSectionSize = ULEB128Encode(dataSectionSize);
if (putativeULEBDataSectionSize.Length != ulebSectionSize.Length)
throw new InvalidOperationException ("adding padding would cause data section's encoded length to chane"); // TODO: fixme: there's upto one extra byte to encode the section length - take away a padding byte.
writer.Write((byte)11); // section Data
writer.Write(ulebSectionSize, 0, ulebSectionSize.Length);
writer.Write((byte)2); // number of segments
// write segment 0
writer.Write((byte)1); // passive segment
if (paddingSize + 4 > 127) {
throw new InvalidOperationException ("padding would cause segment 0 to need a multi-byte ULEB128 size encoding");
}
writer.Write((byte)(4 + paddingSize)); // segment size: 4 plus any padding
writer.Write((uint)_webcilPayloadSize); // payload is an unsigned 32 bit number
for (int i = 0; i < paddingSize; i++)
writer.Write((byte)0);
// write segment 1
writer.Write((byte)1); // passive segment
writer.Write(ulebWebcilPayloadSize, 0, ulebWebcilPayloadSize.Length); // segment size: _webcilPayloadSize
if (writer.BaseStream.Position % WebcilPayloadInternalAlignment != 0) {
throw new Exception ($"predited offset {payloadOffset}, actual position {writer.BaseStream.Position}");
}
_webcilPayloadStream.CopyTo(writer.BaseStream); // payload is the entire webcil content
}
private static byte[] ULEB128Encode(uint value)
{
uint n = value;
int len = 0;
do
{
n >>= 7;
len++;
} while (n != 0);
byte[] arr = new byte[len];
int i = 0;
n = value;
do
{
byte b = (byte)(n & 0x7f);
n >>= 7;
if (n != 0)
b |= 0x80;
arr[i++] = b;
} while (n != 0);
return arr;
}
private static uint PadTo (uint value, uint align)
{
uint newValue = AlignTo(value, align);
return newValue - value;
}
private static uint AlignTo (uint value, uint align)
{
return (value + (align - 1)) & ~(align - 1);
}
}