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 { a = Assembly.LoadFile(dllFile); } Version = a.GetName().Version; return a; } public byte[] CompileFromSource(Action 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); } }