Initial commit
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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; } = [];
|
||||
}
|
||||
@@ -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; } = [];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; } = [];
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace RobotNet10.ScriptEditor.Helpers.Monaco.Languages;
|
||||
|
||||
public class SignatureHelpResult
|
||||
{
|
||||
public SignatureHelp Value { get; set; } = null!;
|
||||
}
|
||||
@@ -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; } = [];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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)!;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user