Skip to content

Commit

Permalink
Merge pull request #10 from hlaueriksson/checksums
Browse files Browse the repository at this point in the history
PackageChecksumRules
  • Loading branch information
hlaueriksson authored Dec 18, 2024
2 parents 6a21e3f + 2b8f121 commit 5738a6f
Show file tree
Hide file tree
Showing 9 changed files with 211 additions and 23 deletions.
42 changes: 40 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Arguments:
Example:

```cmd
ptrun-lint https://github.com/hlaueriksson/Community.PowerToys.Run.Plugin.Update
ptrun-lint https://github.com/hlaueriksson/Community.PowerToys.Run.Plugin.Install
```

During linting, GitHub release assets are downloaded to `%LocalAppData%\Temp`.
Expand Down Expand Up @@ -74,7 +74,6 @@ Logs are written to a file, `ptrun-lint.log`, in the same directory as the tool
| `PTRUN1202` | Release notes should be valid (`<package>`) |
| | Release notes missing |
| | Package missing |
| | Hash "`<hash>`" missing |
| `PTRUN1301` | Package should be valid (`<package>`) |
| | Package missing |
| | Filename does not match "`<name>-<version>-<platform>.zip`" convention |
Expand All @@ -83,6 +82,10 @@ Logs are written to a file, `ptrun-lint.log`, in the same directory as the tool
| | Plugin folder missing |
| | Metadata "`plugin.json`" missing |
| | Assembly "`.dll`" missing |
| `PTRUN1303` | Package checksum should be valid (`<package>`) |
| | Release notes missing |
| | Package missing |
| | Hash "`<hash>`" missing |
| `PTRUN1401` | Plugin metadata should be valid (`<package>`) |
| | Package missing |
| | Repository missing |
Expand Down Expand Up @@ -114,6 +117,41 @@ Logs are written to a file, `ptrun-lint.log`, in the same directory as the tool
| | Target platform should be "`windows`" |
| | Main.PluginID does not match metadata (`plugin.json`) ID |

## Package Checksum

The rule `PTRUN1303` can fail with:

- Hash "`<hash>`" missing

This can be fixed by including:

- Installer hashes in the release notes
- A checksums file in the release assets

Generate markup snippets with installer hashes:

- Run the `releasenotes.ps1` script from [Community.PowerToys.Run.Plugin.Templates](https://github.com/hlaueriksson/Community.PowerToys.Run.Plugin.Templates)

Example:

| Filename | SHA256 Hash
| --- | ---
| `Install-0.1.0-arm64.zip` | `CB03EF645F24248F9618AF18E82D7BA7127F209AB1BE77701FC6183FDC952037`
| `Install-0.1.0-x64.zip` | `E0DB7B8A98D0891B97AAF85A68D340EDDCDF09389537BA64401CF8D786746EC5`

Create a checksums file and attach it to the release as an asset:

- Filename: `checksums.txt`
- Method: `SHA256`

Example:

```txt
CB03EF645F24248F9618AF18E82D7BA7127F209AB1BE77701FC6183FDC952037 Install-0.1.0-arm64.zip
E0DB7B8A98D0891B97AAF85A68D340EDDCDF09389537BA64401CF8D786746EC5 Install-0.1.0-x64.zip
```

A `checksum.txt` can be generated automatically in a GitHub workflow using the [wangzuo/action-release-checksums action](https://github.com/wangzuo/action-release-checksums).
## Disclaimer

This is not an official Microsoft PowerToys tool.
5 changes: 5 additions & 0 deletions src/Community.PowerToys.Run.Plugin.Lint/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ public static bool IsZip(this Asset asset)
(asset?.name != null && asset.name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase));
}

public static bool IsChecksumsFile(this Asset asset)
{
return asset?.name != null && asset.name == "checksums.txt";
}

public static bool HasValidTargetFramework(this Package package)
{
return package.AssemblyAttributeValue(typeof(TargetFrameworkAttribute)) == ".NETCoreApp,Version=v8.0";
Expand Down
6 changes: 6 additions & 0 deletions src/Community.PowerToys.Run.Plugin.Lint/Models.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,9 @@ public void Dispose()

public override string ToString() => Asset.name;
}

public class Checksum(string hash, string name)
{
public string Hash { get; } = hash;
public string Name { get; } = name;
}
33 changes: 33 additions & 0 deletions src/Community.PowerToys.Run.Plugin.Lint/ReleaseHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace Community.PowerToys.Run.Plugin.Lint;
public interface IReleaseHandler
{
Task<Package[]> GetPackagesAsync();
Task<Checksum[]> GetChecksumsAsync();
}

public sealed class ReleaseHandler(Release? release, ILogger logger) : IReleaseHandler, IDisposable
Expand Down Expand Up @@ -35,6 +36,38 @@ public async Task<Package[]> GetPackagesAsync()
return [.. result];
}

public async Task<Checksum[]> GetChecksumsAsync()
{
if (release == null)
{
return [];
}

var result = new List<Checksum>();
using var client = new HttpClient();

var asset = release.assets.FirstOrDefault(x => x.IsChecksumsFile());

if (asset == null)
{
return [];
}

var content = await client.GetStringAsync(asset.browser_download_url);

foreach (var line in content.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries))
{
var tokens = line.Split([' '], 2, StringSplitOptions.RemoveEmptyEntries);

if (tokens.Length == 2)
{
result.Add(new Checksum(tokens[0], tokens[1]));
}
}

return [.. result];
}

public void Dispose()
{
if (release == null) return;
Expand Down
50 changes: 40 additions & 10 deletions src/Community.PowerToys.Run.Plugin.Lint/Rules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,16 +159,6 @@ public IEnumerable<string> Validate()
yield return "Package missing";
yield break;
}

var hash = Hash();
if (!release.body.Contains(hash, StringComparison.OrdinalIgnoreCase)) yield return $"Hash {hash.ToQuote()} missing";

string Hash()
{
using var algorithm = SHA256.Create();
package.FileStream.Position = 0; // rewind
return BitConverter.ToString(algorithm.ComputeHash(package.FileStream)).Replace("-", string.Empty, StringComparison.Ordinal);
}
}
}

Expand Down Expand Up @@ -217,6 +207,46 @@ public IEnumerable<string> Validate()
}
}

public class PackageChecksumRules(Release release, Package package, Checksum[] checksums) : IRule
{
public int Id => 1303;
public string Description => $"Package checksum should be valid {package.ToString().ToFilename()}";

public IEnumerable<string> Validate()
{
if (release?.body == null)
{
yield return "Release notes missing";
yield break;
}

if (package?.FileStream == null)
{
yield return "Package missing";
yield break;
}

if (package?.Asset?.name == null)
{
yield return "Package missing";
yield break;
}

var hash = Hash();
var validReleaseNotes = release.body.Contains(hash, StringComparison.OrdinalIgnoreCase);
var validChecksumsFile = checksums?.Any(x => x.Hash == hash && x.Name.Contains(package.Asset.name, StringComparison.OrdinalIgnoreCase)) == true;

if (!validReleaseNotes && !validChecksumsFile) yield return $"Hash {hash.ToQuote()} missing";

string Hash()
{
using var algorithm = SHA256.Create();
package.FileStream.Position = 0; // rewind
return BitConverter.ToString(algorithm.ComputeHash(package.FileStream)).Replace("-", string.Empty, StringComparison.Ordinal);
}
}
}

public partial class PluginMetadataRules(Package package, Repository repository, User? user) : IRule
{
public int Id => 1401;
Expand Down
7 changes: 4 additions & 3 deletions src/Community.PowerToys.Run.Plugin.Lint/Worker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,6 @@ public async Task<int> RunAsync()
var readme = await client.GetReadmeAsync();
var release = await client.GetLatestReleaseAsync();

var handler = new ReleaseHandler(release, logger);
var packages = await handler.GetPackagesAsync();

rules =
[
new RepoDetailsRules(repository!),
Expand All @@ -53,6 +50,9 @@ public async Task<int> RunAsync()

Validate(rules);

var handler = new ReleaseHandler(release, logger);
var packages = await handler.GetPackagesAsync();
var checksums = await handler.GetChecksumsAsync();
var user = await client.GetUserAsync();

foreach (var package in packages)
Expand All @@ -62,6 +62,7 @@ public async Task<int> RunAsync()
new ReleaseNotesRules(release!, package),
new PackageRules(package),
new PackageContentRules(package),
new PackageChecksumRules(release!, package, checksums),
new PluginDependenciesRules(package),
new PluginMetadataRules(package, repository!, user),
new AssemblyRules(package),
Expand Down
10 changes: 10 additions & 0 deletions tests/Community.PowerToys.Run.Plugin.Lint.Tests/ExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ public void IsZip_should_determine_if_Asset_is_zip_file()
((Asset)null!).IsZip().Should().BeFalse();
}

[Test]
public void IsChecksumsFile_should_determine_if_Asset_is_checksums_file()
{
new Asset { name = "checksums.txt" }.IsChecksumsFile().Should().BeTrue();
new Asset { name = "checksums.zip" }.IsChecksumsFile().Should().BeFalse();
new Asset { name = "" }.IsChecksumsFile().Should().BeFalse();
new Asset().IsChecksumsFile().Should().BeFalse();
((Asset)null!).IsChecksumsFile().Should().BeFalse();
}

[Test]
public void HasValidTargetFramework_should_validate_Assembly()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,41 @@ public async Task GetPackagesAsync_should_return_no_packages_when_asset_has_wron
var result = await subject.GetPackagesAsync();
result.Should().BeEmpty();
}

[Test]
public async Task GetChecksumsAsync_should_return_no_checksums_when_release_is_null()
{
var subject = new ReleaseHandler(null, NullLogger.Instance);
var result = await subject.GetChecksumsAsync();
result.Should().BeEmpty();
}

[Test]
public async Task GetChecksumsAsync_should_return_no_checksums_when_file_is_missing()
{
var subject = new ReleaseHandler(new() { assets = [] }, NullLogger.Instance);
var result = await subject.GetChecksumsAsync();
result.Should().BeEmpty();
}

[Test]
public async Task GetChecksumsAsync_should_return_checksums()
{
var release = new Release
{
assets =
[
new Asset
{
browser_download_url = "https://github.com/neilenns/DiscordTimestamp/releases/download/v1.1.1/checksums.txt",
name = "checksums.txt",
}
],
};

var subject = new ReleaseHandler(release, NullLogger.Instance);
var result = await subject.GetChecksumsAsync();
result.Should().NotBeEmpty();
}
}
}
45 changes: 37 additions & 8 deletions tests/Community.PowerToys.Run.Plugin.Lint.Tests/RulesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,14 +138,6 @@ public void ReleaseNotesRules_should_validate_Release_and_Package()

package.Load();
subject = new ReleaseNotesRules(release, package);
subject.Validate().Clean().Should().BeEquivalentTo(
"Hash \"17AE81ECE0F201AE364047B63BDB052931614EDD49D860E562B277E6D3FA806A\" missing");

release = new Release
{
body = "17AE81ECE0F201AE364047B63BDB052931614EDD49D860E562B277E6D3FA806A",
};
subject = new ReleaseNotesRules(release, package);
subject.Validate().Clean().Should().BeEmpty();
}

Expand Down Expand Up @@ -208,6 +200,43 @@ public void PackageContentRules_should_validate_Package()
subject.Validate().Clean().Should().BeEmpty();
}

[Test]
public void PackageChecksumRules_should_validate_Package()
{
var subject = new PackageChecksumRules(null!, new(new(), @"..\..\..\Packages\Valid-0.82.1-x64.zip"), []);
subject.Validate().Clean().Should().BeEquivalentTo(
"Release notes missing");

subject = new PackageChecksumRules(new(), new(new(), @"..\..\..\Packages\Valid-0.82.1-x64.zip"), []);
subject.Validate().Clean().Should().BeEquivalentTo(
"Release notes missing");

subject = new PackageChecksumRules(new() { body = "" }, new(new(), @"..\..\..\Packages\Valid-0.82.1-x64.zip"), []);
subject.Validate().Clean().Should().BeEquivalentTo(
"Package missing");

var package = new Package(new() { name = "Valid-0.82.1-x64.zip" }, @"..\..\..\Packages\Valid-0.82.1-x64.zip");
package.Load();
subject = new PackageChecksumRules(new() { body = "" }, package, []);
subject.Validate().Clean().Should().BeEquivalentTo(
"Hash \"17AE81ECE0F201AE364047B63BDB052931614EDD49D860E562B277E6D3FA806A\" missing");

subject = new PackageChecksumRules(new() { body = "" }, package, null!);
subject.Validate().Clean().Should().BeEquivalentTo(
"Hash \"17AE81ECE0F201AE364047B63BDB052931614EDD49D860E562B277E6D3FA806A\" missing");

var release = new Release
{
body = "17AE81ECE0F201AE364047B63BDB052931614EDD49D860E562B277E6D3FA806A",
};
subject = new PackageChecksumRules(release, package, []);
subject.Validate().Clean().Should().BeEmpty();

var checksum = new Checksum("17AE81ECE0F201AE364047B63BDB052931614EDD49D860E562B277E6D3FA806A", "Valid-0.82.1-x64.zip");
subject = new PackageChecksumRules(new() { body = "" }, package, [checksum]);
subject.Validate().Clean().Should().BeEmpty();
}

[Test]
public void PluginMetadataRules_should_validate_Package()
{
Expand Down

0 comments on commit 5738a6f

Please sign in to comment.