Files
se-launcher/PluginLoader/Data/GitHubPlugin.cs

168 lines
4.8 KiB
C#

using System.IO.Compression;
using System.Reflection;
using System.Text;
using System.Xml.Serialization;
using PluginLoader.Compiler;
using PluginLoader.Network;
using ProtoBuf;
using Sandbox.Graphics.GUI;
namespace PluginLoader.Data;
[ProtoContract]
public class GitHubPlugin : PluginData
{
private const string pluginFile = "plugin.dll";
private const string commitHashFile = "commit.sha1";
private string cacheDir, assemblyName;
public GitHubPlugin()
{
Status = PluginStatus.None;
}
public override string Source => "GitHub";
[ProtoMember(1)] public string Commit { get; set; }
[ProtoMember(2)]
[XmlArray]
[XmlArrayItem("Directory")]
public string[] SourceDirectories { get; set; }
public void Init(string mainDirectory)
{
var nameArgs = Id.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
if (nameArgs.Length != 2)
throw new("Invalid GitHub name: " + Id);
if (SourceDirectories != null)
for (var i = SourceDirectories.Length - 1; i >= 0; i--)
{
var path = SourceDirectories[i].Replace('\\', '/').TrimStart('/');
if (path.Length == 0)
{
SourceDirectories.RemoveAtFast(i);
continue;
}
if (path[path.Length - 1] != '/')
path += '/';
SourceDirectories[i] = path;
}
assemblyName = MakeSafeString(nameArgs[1]);
cacheDir = Path.Combine(mainDirectory, "GitHub", nameArgs[0], nameArgs[1]);
}
private string MakeSafeString(string s)
{
var sb = new StringBuilder();
foreach (var ch in s)
if (char.IsLetterOrDigit(ch))
sb.Append(ch);
else
sb.Append('_');
return sb.ToString();
}
public override Assembly? GetAssembly()
{
if (!Directory.Exists(cacheDir))
Directory.CreateDirectory(cacheDir);
Assembly? a;
var dllFile = Path.Combine(cacheDir, pluginFile);
var commitFile = Path.Combine(cacheDir, commitHashFile);
if (!File.Exists(dllFile) || !File.Exists(commitFile) || File.ReadAllText(commitFile) != Commit)
{
var lbl = Main.Instance.Splash;
lbl.SetText($"Downloading '{FriendlyName}'");
var data = CompileFromSource(x => lbl.SetBarValue(x));
File.WriteAllBytes(dllFile, data);
File.WriteAllText(commitFile, Commit);
Status = PluginStatus.Updated;
lbl.SetText($"Compiled '{FriendlyName}'");
a = Assembly.Load(data);
}
else
{
try
{
a = Assembly.LoadFile(dllFile);
}
catch
{
LogFile.WriteLine($"Error loading {dllFile}, deleting file");
File.Delete(dllFile);
throw;
}
}
Version = a.GetName().Version;
return a;
}
public byte[] CompileFromSource(Action<float> callback = null)
{
var compiler = new RoslynCompiler();
using (var s = GitHub.DownloadRepo(Id, Commit, out var fileName))
using (var zip = new ZipArchive(s))
{
callback?.Invoke(0);
for (var i = 0; i < zip.Entries.Count; i++)
{
var entry = zip.Entries[i];
CompileFromSource(compiler, entry);
callback?.Invoke(i / (float)zip.Entries.Count);
}
callback?.Invoke(1);
}
return compiler.Compile(assemblyName + '_' + Path.GetRandomFileName(), out _);
}
private void CompileFromSource(RoslynCompiler compiler, ZipArchiveEntry entry)
{
if (AllowedZipPath(entry.FullName))
using (var entryStream = entry.Open())
{
compiler.Load(entryStream, entry.FullName);
}
}
private bool AllowedZipPath(string path)
{
if (!path.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
return false;
if (SourceDirectories == null || SourceDirectories.Length == 0)
return true;
path = RemoveRoot(path); // Make the base of the path the root of the repository
foreach (var dir in SourceDirectories)
if (path.StartsWith(dir, StringComparison.Ordinal))
return true;
return false;
}
private string RemoveRoot(string path)
{
path = path.Replace('\\', '/').TrimStart('/');
var index = path.IndexOf('/');
if (index >= 0 && index + 1 < path.Length)
return path.Substring(index + 1);
return path;
}
public override void Show()
{
MyGuiSandbox.OpenUrl("https://github.com/" + Id, UrlOpenMode.SteamOrExternalWithConfirm);
}
}