From 6a97d070c91ec31bebde4807bd6ea704f3c013b1 Mon Sep 17 00:00:00 2001 From: Jason Ngo Date: Thu, 1 May 2025 23:22:01 -0400 Subject: [PATCH 1/6] Initial commit for port to Go --- .github/workflows/{dotnet.yml => go.yml} | 18 +- .gitignore | 389 ++--------------------- LICENSE | 4 +- ModlinksShaVerifier.csproj | 10 - ModlinksShaVerifier.csproj.DotSettings | 2 - go.mod | 3 + main.go | 163 ++++++++++ modlinks.go | 22 ++ 8 files changed, 221 insertions(+), 390 deletions(-) rename .github/workflows/{dotnet.yml => go.yml} (64%) delete mode 100755 ModlinksShaVerifier.csproj delete mode 100644 ModlinksShaVerifier.csproj.DotSettings create mode 100644 go.mod create mode 100644 main.go create mode 100644 modlinks.go diff --git a/.github/workflows/dotnet.yml b/.github/workflows/go.yml similarity index 64% rename from .github/workflows/dotnet.yml rename to .github/workflows/go.yml index 03dda9b..1739893 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/go.yml @@ -1,4 +1,4 @@ -name: .NET +name: Build Golang on: [push, pull_request] @@ -7,21 +7,15 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 8.0.x - - name: Restore dependencies - run: dotnet restore -r linux-x64 + - name: Setup Go + uses: actions/setup-go@v5 - name: Build - run: | - dotnet publish -r linux-x64 -p:PublishSingleFile=true -p:Configuration=Release --self-contained true - cd .. + run: go build - name: Upload Binary uses: actions/upload-artifact@v4 with: name: linux - path: bin/Release/net8.0/linux-x64/publish/ + path: ModlinksShaVerifier release: needs: [build] @@ -44,4 +38,4 @@ jobs: uses: softprops/action-gh-release@v2 with: files: | - ./linux.zip + ./linux.zip \ No newline at end of file diff --git a/.gitignore b/.gitignore index 38fece0..64ae7f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,364 +1,25 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -.idea - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Ww][Ii][Nn]32/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -[Ll]ogs/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# ASP.NET Scaffolding -ScaffoldingReadMe.txt - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Coverlet is a free, cross platform Code Coverage Tool -coverage*.json -coverage*.xml -coverage*.info - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ - -# Fody - auto-generated XML schema -FodyWeavers.xsd +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env \ No newline at end of file diff --git a/LICENSE b/LICENSE index ea2fbef..155ff6b 100755 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ -MIT License +# MIT License -Copyright (c) 2022 hk-modding +Copyright (c) 2025 hk-modding Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/ModlinksShaVerifier.csproj b/ModlinksShaVerifier.csproj deleted file mode 100755 index 0f2a571..0000000 --- a/ModlinksShaVerifier.csproj +++ /dev/null @@ -1,10 +0,0 @@ - - - Exe - net8.0 - disable - enable - True - latest - - diff --git a/ModlinksShaVerifier.csproj.DotSettings b/ModlinksShaVerifier.csproj.DotSettings deleted file mode 100644 index acd232e..0000000 --- a/ModlinksShaVerifier.csproj.DotSettings +++ /dev/null @@ -1,2 +0,0 @@ - - Preview \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..056f507 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module ModlinksShaVerifier + +go 1.24.2 diff --git a/main.go b/main.go new file mode 100644 index 0000000..737d124 --- /dev/null +++ b/main.go @@ -0,0 +1,163 @@ +package main + +import ( + "bufio" + "crypto/sha256" + "encoding/hex" + "encoding/xml" + "fmt" + "io" + "log" + "net/http" + "os" + "strings" + "time" +) + +func main() { + start := time.Now() + + args := os.Args + + if len(args) < 3 { + fmt.Print("Usage: go run main.go ") + return + } + + currentPath := args[1] + currentFile, err := os.Open(currentPath) + if err != nil { + fmt.Println("Error opening current file:", err) + return + } + defer currentFile.Close() + + incomingPath := args[2] + incomingFile, err := os.Open(incomingPath) + if err != nil { + fmt.Println("Error opening incoming file:", err) + return + } + defer incomingFile.Close() + + currentReader := bufio.NewReader(currentFile) + incomingReader := bufio.NewReader(incomingFile) + + var currentModlinks Modlinks + var incomingModlinks Modlinks + + err = xml.NewDecoder(currentReader).Decode(¤tModlinks) + if err != nil { + fmt.Println("Error decoding current file: ", err) + return + } + + err = xml.NewDecoder(incomingReader).Decode(&incomingModlinks) + if err != nil { + fmt.Println("Error decoding incoming file: ", err) + return + } + + mainChannel := make(chan bool) + + checkedManifests := make(map[string]Manifest) + for _, currentManifest := range currentModlinks.Manifests { + trimManifest(¤tManifest) + checkedManifests[currentManifest.Name] = currentManifest + } + + var checkManifestCount int + for _, incomingManifest := range incomingModlinks.Manifests { + trimManifest(&incomingManifest) + if checkedManifest, exists := checkedManifests[incomingManifest.Name]; exists { + if checkedManifest != incomingManifest { + go checkManifest(incomingManifest, mainChannel) + checkManifestCount++ + } + } else { + go checkManifest(incomingManifest, mainChannel) + checkManifestCount++ + } + } + + var resultCount int + for result := range mainChannel { + if !result { + log.Fatal("Not all checks were satisfied.") + } else if resultCount >= checkManifestCount { + break + } + resultCount++ + } + + fmt.Printf("Completed in %dms\n", time.Since(start).Milliseconds()) +} + +func trimManifest(manifest *Manifest) { + if manifest.Link != (Link{}) { + manifest.Link.URL = strings.TrimSpace(manifest.Link.URL) + } else if manifest.Links != (Links{}) { + if manifest.Links.Linux != (Link{}) { + manifest.Links.Linux.URL = strings.TrimSpace(manifest.Links.Linux.URL) + } + if manifest.Links.Mac != (Link{}) { + manifest.Links.Mac.URL = strings.TrimSpace(manifest.Links.Mac.URL) + } + if manifest.Links.Windows != (Link{}) { + manifest.Links.Windows.URL = strings.TrimSpace(manifest.Links.Windows.URL) + } + } +} + +func checkManifest(manifest Manifest, channel chan bool) { + fmt.Printf("Checking '%s'\n", manifest.Name) + + if manifest.Link != (Link{}) { + go checkLink(manifest.Name, manifest.Link, channel) + } else if manifest.Links != (Links{}) { + links := manifest.Links + + if links.Linux != (Link{}) { + go checkLink(manifest.Name, links.Linux, channel) + } + if links.Mac != (Link{}) { + go checkLink(manifest.Name, links.Mac, channel) + } + if links.Windows != (Link{}) { + go checkLink(manifest.Name, links.Windows, channel) + } + } else { + log.Fatalf("No links found for manifest '%s'\n", manifest.Name) + } +} + +func checkLink(manifestName string, link Link, channel chan bool) { + url := strings.TrimSpace(link.URL) + response, err := http.Get(url) + if err != nil { + fmt.Printf("Failed to fetch link at %s: %s", url, err) + channel <- false + return + } else if response.StatusCode != 200 { + fmt.Printf("Invalid status code %d", response.StatusCode) + return + } + + data, err := io.ReadAll(response.Body) + if err != nil { + fmt.Printf("Error reading response body: %s", err) + return + } + defer response.Body.Close() + + var sha = sha256.New() + sha.Write(data) + hash := hex.EncodeToString(sha.Sum(nil)) + + if strings.EqualFold(hash, link.SHA256) { + channel <- true + } else { + fmt.Printf("Hash mismatch if %s in link %s. Expected value from modlinks: %s, Actual value: %s", manifestName, url, link.SHA256, hash) + channel <- false + } +} diff --git a/modlinks.go b/modlinks.go new file mode 100644 index 0000000..8d99082 --- /dev/null +++ b/modlinks.go @@ -0,0 +1,22 @@ +package main + +type Modlinks struct { + Manifests []Manifest `xml:"Manifest"` +} + +type Manifest struct { + Name string `xml:"Name"` + Link Link `xml:"Link"` + Links Links `xml:"Links"` +} + +type Links struct { + Linux Link `xml:"Linux"` + Mac Link `xml:"Mac"` + Windows Link `xml:"Windows"` +} + +type Link struct { + SHA256 string `xml:"SHA256,attr"` + URL string `xml:",cdata"` +} From a19db57322bd0e3c4558932599b76051314ed215 Mon Sep 17 00:00:00 2001 From: Jason Ngo Date: Thu, 1 May 2025 23:29:48 -0400 Subject: [PATCH 2/6] Fix string formatting --- main.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/main.go b/main.go index 737d124..630caa2 100644 --- a/main.go +++ b/main.go @@ -20,14 +20,14 @@ func main() { args := os.Args if len(args) < 3 { - fmt.Print("Usage: go run main.go ") + fmt.Println("Usage: ./ModlinksShaVerifier ") return } currentPath := args[1] currentFile, err := os.Open(currentPath) if err != nil { - fmt.Println("Error opening current file:", err) + fmt.Println("Error opening current file: ", err) return } defer currentFile.Close() @@ -35,7 +35,7 @@ func main() { incomingPath := args[2] incomingFile, err := os.Open(incomingPath) if err != nil { - fmt.Println("Error opening incoming file:", err) + fmt.Println("Error opening incoming file: ", err) return } defer incomingFile.Close() @@ -135,17 +135,17 @@ func checkLink(manifestName string, link Link, channel chan bool) { url := strings.TrimSpace(link.URL) response, err := http.Get(url) if err != nil { - fmt.Printf("Failed to fetch link at %s: %s", url, err) + fmt.Printf("Failed to fetch link at %s: %s\n", url, err) channel <- false return } else if response.StatusCode != 200 { - fmt.Printf("Invalid status code %d", response.StatusCode) + fmt.Println("Invalid status code ", response.StatusCode) return } data, err := io.ReadAll(response.Body) if err != nil { - fmt.Printf("Error reading response body: %s", err) + fmt.Println("Error reading response body: ", err) return } defer response.Body.Close() @@ -157,7 +157,7 @@ func checkLink(manifestName string, link Link, channel chan bool) { if strings.EqualFold(hash, link.SHA256) { channel <- true } else { - fmt.Printf("Hash mismatch if %s in link %s. Expected value from modlinks: %s, Actual value: %s", manifestName, url, link.SHA256, hash) + fmt.Printf("Hash mismatch if %s in link %s. Expected value from modlinks: %s, Actual value: %s\n", manifestName, url, link.SHA256, hash) channel <- false } } From 03a424cc062b0462122f8c6ece05205199707bdb Mon Sep 17 00:00:00 2001 From: Jason Ngo Date: Fri, 2 May 2025 10:11:04 -0400 Subject: [PATCH 3/6] Print out number of mods checked --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 630caa2..8f80f51 100644 --- a/main.go +++ b/main.go @@ -90,7 +90,7 @@ func main() { resultCount++ } - fmt.Printf("Completed in %dms\n", time.Since(start).Milliseconds()) + fmt.Printf("Checked %d mods in %dms\n", resultCount, time.Since(start).Milliseconds()) } func trimManifest(manifest *Manifest) { From 2b83e783985f50eaa9177fe5ab3e7a6a6529f994 Mon Sep 17 00:00:00 2001 From: Jason Ngo Date: Fri, 2 May 2025 11:28:08 -0400 Subject: [PATCH 4/6] Remove restored C# files --- ModLinks.cs | 128 ---------------------------------------------------- Program.cs | 107 ------------------------------------------- 2 files changed, 235 deletions(-) delete mode 100644 ModLinks.cs delete mode 100755 Program.cs diff --git a/ModLinks.cs b/ModLinks.cs deleted file mode 100644 index 2694dfd..0000000 --- a/ModLinks.cs +++ /dev/null @@ -1,128 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; - -namespace ModlinksShaVerifier; - -public static class SerializationConstants -{ - public const string Namespace = "https://github.com/HollowKnight-Modding/HollowKnight.ModLinks/HollowKnight.ModManager"; -} - -[Serializable] -[XmlRoot(Namespace = SerializationConstants.Namespace)] -public record Manifest -{ - // Internally handle the Link/Links either-or divide - private Links? _links; - private Link? _link; - - public string Name { get; set; } = null!; - - [XmlElement] - public Link? Link - { - get => throw new NotImplementedException("This is only for XML Serialization!"); - set => _link = value; - } - - public Links Links - { - get => - _links ??= new Links - { - Windows = _link ?? throw new InvalidDataException(nameof(_link)) - }; - set => _links = value; - } - - [XmlArray("Dependencies")] - [XmlArrayItem("Dependency")] - public string[]? Dependencies { get; set; } - - public string Description { get; set; } = null!; - - public override string ToString() - { - return "{\n" - + $"\t{nameof(Name)}: {Name},\n" - + $"\t{nameof(Links)}: {(object?) _link ?? Links},\n" - + $"\t{nameof(Dependencies)}: {string.Join(", ", Dependencies ?? Array.Empty())},\n" - + $"\t{nameof(Description)}: {Description}\n" - + "}"; - } -} - -public class Links -{ - public Link? Windows; - public Link? Mac; - public Link? Linux; - - public override string ToString() - { - return "Links {" - + $"\t{nameof(Windows)} = {Windows},\n" - + $"\t{nameof(Mac)} = {Mac},\n" - + $"\t{nameof(Linux)} = {Linux}\n" - + "}"; - } - - public IEnumerable AsEnumerable() - { - if (Windows is not null) - yield return Windows; - if (Mac is not null) - yield return Mac; - if (Linux is not null) - yield return Linux; - } -} - -public class Link -{ - [XmlAttribute] - public string SHA256 = null!; - - [XmlText] - public string URL = null!; - - public override string ToString() - { - return $"[Link: {nameof(SHA256)} = {SHA256}, {nameof(URL)}: {URL}]"; - } -} - -[Serializable] -public class ApiManifest -{ - public Links Links { get; set; } - - // For serializer and nullability - public ApiManifest() => Links = null!; -} - -[XmlRoot(Namespace = SerializationConstants.Namespace)] -public class ApiLinks -{ - public ApiManifest Manifest { get; set; } = null!; -} - -[XmlRoot(Namespace = SerializationConstants.Namespace)] -public class ModLinks -{ - [XmlElement("Manifest")] - public Manifest[] Manifests { get; set; } = null!; - - public override string ToString() - { - return "ModLinks {[\n" - + string.Join("\n", Manifests.Select(x => x.ToString())) - + "]}"; - } -} \ No newline at end of file diff --git a/Program.cs b/Program.cs deleted file mode 100755 index 4f69da0..0000000 --- a/Program.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Xml; -using System.Security.Cryptography; -using System.Threading.Tasks; -using System.Xml.Serialization; - -namespace ModlinksShaVerifier -{ - internal static class Program - { - private static readonly HttpClient _Client = new(); - - private static string ShaToString(byte[] hash) - => BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); - - private static async Task CheckLink(Manifest m, Link link) - { - using var sha = SHA256.Create(); - - Stream stream; - - try - { - stream = await _Client.GetStreamAsync(link.URL); - } - catch (HttpRequestException e) - { - WriteError("Check", $"Request failed for {m.Name} - {link.URL}! {e.StatusCode}"); - return false; - } - - string shasum = ShaToString(await sha.ComputeHashAsync(stream)); - - if (shasum == link.SHA256.ToLowerInvariant()) - return true; - - WriteError("Check", $"Hash mismatch of {m.Name} in link {link.URL}. Expected value from modlinks: {link.SHA256}, Actual value: {shasum}"); - - return false; - } - - private static async Task CheckSingleSha(Manifest m) - { - Console.WriteLine($"Checking '{m.Name}'"); - - var res = await Task.WhenAll(m.Links.AsEnumerable().Select(x => CheckLink(m, x))); - - return res.All(x => x); - } - - internal static async Task Main(string[] args) - { - var sw = new Stopwatch(); - sw.Start(); - - if (args.Length != 1) - { - await Console.Error.WriteLineAsync("Usage: ModlinksShaVerifier [FILE]"); - return 1; - } - - var path = args[0]; - - if (!File.Exists(path)) - { - await Console.Error.WriteLineAsync($"Unable to access {path}! Does it exist?"); - return 1; - } - - var reader = XmlReader.Create(path, new XmlReaderSettings {Async = true}); - - var serializer = new XmlSerializer(typeof(Manifest)); - - List> checks = new(); - - while (await reader.ReadAsync()) - { - if (reader.NodeType != XmlNodeType.Element) - continue; - - if (reader.Name != "Manifest") - continue; - - var manifest = (Manifest?) serializer.Deserialize(reader) ?? throw new InvalidDataException(); - - checks.Add(CheckSingleSha(manifest)); - } - - var res = await Task.WhenAll(checks); - - sw.Stop(); - - Console.WriteLine($"Completed in {sw.ElapsedMilliseconds}ms."); - - // If they're not all correct, error. - return !res.All(x => x) ? 1 : 0; - } - - private static void WriteError(string title, string message) => - Console.WriteLine($"::error title={title}::{message}"); - } -} From 8ac0ec4e0a3d7fbac22eb5e472bfefd9866e699b Mon Sep 17 00:00:00 2001 From: Jason Ngo Date: Fri, 2 May 2025 13:34:46 -0400 Subject: [PATCH 5/6] - Do not exit immediately after a failed SHA check - Docstrings - Correct program exit paths --- main.go | 30 ++++++++++++++++++------------ modlinks.go | 25 +++++++++++++++++++------ 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/main.go b/main.go index 8f80f51..8772539 100644 --- a/main.go +++ b/main.go @@ -20,23 +20,20 @@ func main() { args := os.Args if len(args) < 3 { - fmt.Println("Usage: ./ModlinksShaVerifier ") - return + log.Fatalln("Usage: ./ModlinksShaVerifier ") } currentPath := args[1] currentFile, err := os.Open(currentPath) if err != nil { - fmt.Println("Error opening current file: ", err) - return + log.Fatalln("Error opening current file: ", err) } defer currentFile.Close() incomingPath := args[2] incomingFile, err := os.Open(incomingPath) if err != nil { - fmt.Println("Error opening incoming file: ", err) - return + log.Fatalln("Error opening incoming file: ", err) } defer incomingFile.Close() @@ -48,14 +45,12 @@ func main() { err = xml.NewDecoder(currentReader).Decode(¤tModlinks) if err != nil { - fmt.Println("Error decoding current file: ", err) - return + log.Fatalln("Error decoding current file: ", err) } err = xml.NewDecoder(incomingReader).Decode(&incomingModlinks) if err != nil { - fmt.Println("Error decoding incoming file: ", err) - return + log.Fatalln("Error decoding incoming file: ", err) } mainChannel := make(chan bool) @@ -81,18 +76,25 @@ func main() { } var resultCount int + var success bool = true for result := range mainChannel { if !result { - log.Fatal("Not all checks were satisfied.") - } else if resultCount >= checkManifestCount { + success = false + } + if resultCount >= checkManifestCount { break } resultCount++ } + if !success { + log.Fatalln("Not all checks were successful.") + } + fmt.Printf("Checked %d mods in %dms\n", resultCount, time.Since(start).Milliseconds()) } +// Trim any newlines existing in a mod manifest's link URLs. func trimManifest(manifest *Manifest) { if manifest.Link != (Link{}) { manifest.Link.URL = strings.TrimSpace(manifest.Link.URL) @@ -109,6 +111,7 @@ func trimManifest(manifest *Manifest) { } } +// Verify the SHA256 hashes of a single mod manifest's links. func checkManifest(manifest Manifest, channel chan bool) { fmt.Printf("Checking '%s'\n", manifest.Name) @@ -131,6 +134,7 @@ func checkManifest(manifest Manifest, channel chan bool) { } } +// Verify the SHA256 hash of a single mod manifest link. func checkLink(manifestName string, link Link, channel chan bool) { url := strings.TrimSpace(link.URL) response, err := http.Get(url) @@ -140,12 +144,14 @@ func checkLink(manifestName string, link Link, channel chan bool) { return } else if response.StatusCode != 200 { fmt.Println("Invalid status code ", response.StatusCode) + channel <- false return } data, err := io.ReadAll(response.Body) if err != nil { fmt.Println("Error reading response body: ", err) + channel <- false return } defer response.Body.Close() diff --git a/modlinks.go b/modlinks.go index 8d99082..84705db 100644 --- a/modlinks.go +++ b/modlinks.go @@ -1,22 +1,35 @@ package main +// Represents an entire Modlinks XML document. type Modlinks struct { + // An array of all manifests within the Modlinks document. Manifests []Manifest `xml:"Manifest"` } +// Represents a single mod manifest within a Modlinks document. type Manifest struct { - Name string `xml:"Name"` - Link Link `xml:"Link"` - Links Links `xml:"Links"` + // The name of the mod manifest. + Name string `xml:"Name"` + // A single link representing a cross-platform mod. + Link Link `xml:"Link"` + // A group of links representing each mod for the Linux, macOS, and Windows platform. + Links Links `xml:"Links"` } +// Represents a links structure in a mod manifest with Linux, macOS, and Windows links. type Links struct { - Linux Link `xml:"Linux"` - Mac Link `xml:"Mac"` + // The link to the Linux build. + Linux Link `xml:"Linux"` + // The link to the macOS build. + Mac Link `xml:"Mac"` + // The link to the Windows build. Windows Link `xml:"Windows"` } +// Represents a link structure in a mod manifest. type Link struct { + // The SHA256 hash proposed by the link tag. SHA256 string `xml:"SHA256,attr"` - URL string `xml:",cdata"` + // The URL to the shared .NET library or zip file whose SHA256 hash will be verified. + URL string `xml:",cdata"` } From 9f2e6a0eb3d2cce2d356d402b36820aee6bbfab6 Mon Sep 17 00:00:00 2001 From: Jason Ngo Date: Fri, 2 May 2025 13:42:01 -0400 Subject: [PATCH 6/6] Typo fixes --- main.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/main.go b/main.go index 8772539..f5a55d5 100644 --- a/main.go +++ b/main.go @@ -26,14 +26,14 @@ func main() { currentPath := args[1] currentFile, err := os.Open(currentPath) if err != nil { - log.Fatalln("Error opening current file: ", err) + log.Fatalln("Error opening current file:", err) } defer currentFile.Close() incomingPath := args[2] incomingFile, err := os.Open(incomingPath) if err != nil { - log.Fatalln("Error opening incoming file: ", err) + log.Fatalln("Error opening incoming file:", err) } defer incomingFile.Close() @@ -45,12 +45,12 @@ func main() { err = xml.NewDecoder(currentReader).Decode(¤tModlinks) if err != nil { - log.Fatalln("Error decoding current file: ", err) + log.Fatalln("Error decoding current file:", err) } err = xml.NewDecoder(incomingReader).Decode(&incomingModlinks) if err != nil { - log.Fatalln("Error decoding incoming file: ", err) + log.Fatalln("Error decoding incoming file:", err) } mainChannel := make(chan bool) @@ -150,7 +150,7 @@ func checkLink(manifestName string, link Link, channel chan bool) { data, err := io.ReadAll(response.Body) if err != nil { - fmt.Println("Error reading response body: ", err) + fmt.Println("Error reading response body:", err) channel <- false return } @@ -163,7 +163,7 @@ func checkLink(manifestName string, link Link, channel chan bool) { if strings.EqualFold(hash, link.SHA256) { channel <- true } else { - fmt.Printf("Hash mismatch if %s in link %s. Expected value from modlinks: %s, Actual value: %s\n", manifestName, url, link.SHA256, hash) + fmt.Printf("Hash mismatch of %s in link %s. Expected value from modlinks: %s, Actual value: %s\n", manifestName, url, link.SHA256, hash) channel <- false } }