Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
268 changes: 260 additions & 8 deletions dotnet/src/Devolutions.Pinget.Core.Tests/CoreTests.cs

Large diffs are not rendered by default.

39 changes: 33 additions & 6 deletions dotnet/src/Devolutions.Pinget.Core/InstalledPackages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ private static void CollectArpPackages(
var packageFamilyName = subkey.GetValue("PackageFamilyName") as string;
var productCode = subkey.GetValue("ProductCode") as string;
var upgradeCode = subkey.GetValue("UpgradeCode") as string;
var wingetPackageIdentifier = subkey.GetValue("WinGetPackageIdentifier") as string;
var wingetSourceIdentifier = subkey.GetValue("WinGetSourceIdentifier") as string;

var localId = $@"ARP\{scopeLabel}\{effectiveArch}\{subkeyName}";
// Honor the WinGet ARP signal so uninstall flows through the
Expand Down Expand Up @@ -246,6 +248,8 @@ private static void CollectArpPackages(
Scope = scopeLabel,
InstallerCategory = installerCategory,
InstallLocation = installLocation,
WinGetPackageIdentifier = wingetPackageIdentifier,
WinGetSourceIdentifier = wingetSourceIdentifier,
PackageFamilyNames = packageFamilyNames,
ProductCodes = productCodes,
UpgradeCodes = upgradeCodes,
Expand Down Expand Up @@ -279,16 +283,19 @@ private static void CollectAppModelPackages(
if (subkey is null)
continue;

var displayName = subkey.GetValue("DisplayName") as string;
if (string.IsNullOrWhiteSpace(displayName))
continue;

var installLocation = subkey.GetValue("PackageRootFolder") as string;
if (IsWindowsSystemPath(installLocation))
continue;

var parsed = ParseMsixFullName(subkeyName);
if (parsed is null) continue;
if (IsMsixSplitResourcePackage(parsed.Value.ResourceId)) continue;

var displayName = subkey.GetValue("DisplayName") as string;
if (string.IsNullOrWhiteSpace(displayName))
displayName = GetAppModelDisplayName(subkey);
if (string.IsNullOrWhiteSpace(displayName))
continue;

var localId = $@"MSIX\{subkeyName}";
var dedupKey = $"{localId}|{displayName.ToLowerInvariant()}|{parsed.Value.Version.ToLowerInvariant()}";
Expand Down Expand Up @@ -316,7 +323,25 @@ private static bool IsWindowsSystemPath(string? path)
return path.Trim().StartsWith(@"C:\Windows\", StringComparison.OrdinalIgnoreCase);
}

private static (string Version, string FamilyName)? ParseMsixFullName(string fullName)
[SupportedOSPlatform("windows")]
private static string? GetAppModelDisplayName(Microsoft.Win32.RegistryKey packageKey)
{
using var appKey = packageKey.OpenSubKey(@"App\Capabilities");
if (appKey is null) return null;

var description = appKey.GetValue("ApplicationDescription") as string;
if (!string.IsNullOrWhiteSpace(description))
return description;

var name = appKey.GetValue("ApplicationName") as string;
return string.IsNullOrWhiteSpace(name) ? null : name;
}

internal static bool IsMsixSplitResourcePackage(string? resourceId) =>
!string.IsNullOrWhiteSpace(resourceId)
&& resourceId.StartsWith("split.", StringComparison.OrdinalIgnoreCase);

internal static MsixPackageFullName? ParseMsixFullName(string fullName)
{
var parts = fullName.Split('_');
if (parts.Length < 5) return null;
Expand All @@ -335,6 +360,8 @@ private static (string Version, string FamilyName)? ParseMsixFullName(string ful
? $"{name}_{publisherHash}"
: $"{name}_{resourceId}_{publisherHash}";

return (version, familyName);
return new MsixPackageFullName(version, resourceId, familyName);
}

internal readonly record struct MsixPackageFullName(string Version, string ResourceId, string FamilyName);
}
12 changes: 11 additions & 1 deletion dotnet/src/Devolutions.Pinget.Core/Models.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ public enum PinType
Gating
}

public enum SourceMode
{
Auto,
Private,
SystemWingetMirror
}

public record SourceRecord
{
public required string Name { get; init; }
Expand All @@ -37,9 +44,10 @@ public record SourceRecord
public record RepositoryOptions
{
/// <summary>
/// Storage root for source state and caches. A null value uses the real Desktop App Installer / WinGet state on Windows.
/// Storage root for Pinget state and caches.
/// </summary>
public string? AppRoot { get; init; }
public SourceMode SourceMode { get; init; } = SourceMode.Auto;
public string UserAgent { get; init; } = "pinget-dotnet/0.1";
public Action<RepositoryWarning>? Diagnostics { get; init; }

Expand Down Expand Up @@ -651,6 +659,8 @@ internal record InstalledPackage
public string? Scope { get; init; }
public string? InstallerCategory { get; init; }
public string? InstallLocation { get; init; }
public string? WinGetPackageIdentifier { get; init; }
public string? WinGetSourceIdentifier { get; init; }
public List<string> PackageFamilyNames { get; init; } = [];
public List<string> ProductCodes { get; init; } = [];
public List<string> UpgradeCodes { get; init; } = [];
Expand Down
16 changes: 8 additions & 8 deletions dotnet/src/Devolutions.Pinget.Core/PreIndexedSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,22 @@ internal static class PreIndexedSource
{
private static readonly string[] MsixCandidates = ["source2.msix", "source.msix"];

public static string IndexPath(SourceRecord source, string? appRoot = null)
public static string IndexPath(SourceRecord source, string? appRoot = null, bool identifierKeyed = false)
{
var stateDir = SourceStoreManager.SourceStateDir(source, appRoot);
var stateDir = SourceStoreManager.SourceStateDir(source, appRoot, identifierKeyed);
return Path.Combine(stateDir, "index.db");
}

public static string Update(HttpClient client, SourceRecord source, string? appRoot = null)
public static string Update(HttpClient client, SourceRecord source, string? appRoot = null, bool identifierKeyed = false)
{
var stateDir = SourceStoreManager.SourceStateDir(source, appRoot);
var stateDir = SourceStoreManager.SourceStateDir(source, appRoot, identifierKeyed);
Directory.CreateDirectory(stateDir);

Exception? lastError = null;
foreach (var candidate in MsixCandidates)
{
var url = $"{source.Arg.TrimEnd('/')}/{candidate}";
var hasLocalIndex = File.Exists(IndexPath(source, appRoot));
var hasLocalIndex = File.Exists(IndexPath(source, appRoot, identifierKeyed));
try
{
if (hasLocalIndex && TrySkipUnchangedBySourceVersion(client, url, source, candidate, out var detail))
Expand Down Expand Up @@ -59,7 +59,7 @@ public static string Update(HttpClient client, SourceRecord source, string? appR
var indexEntry = archive.GetEntry("Public/index.db")
?? throw new InvalidOperationException($"No Public/index.db in {candidate}");

var indexPath = IndexPath(source, appRoot);
var indexPath = IndexPath(source, appRoot, identifierKeyed);
var tempIndexPath = Path.Combine(stateDir, $"index.{Guid.NewGuid():N}.tmp");
try
{
Expand Down Expand Up @@ -571,9 +571,9 @@ public static byte[] GetCachedSourceFile(
return bytes;
}

public static byte[] GetCachedSourceFileFromMsix(SourceRecord source, string relativePath, string? appRoot = null)
public static byte[] GetCachedSourceFileFromMsix(SourceRecord source, string relativePath, string? appRoot = null, bool identifierKeyed = false)
{
var stateDir = SourceStoreManager.SourceStateDir(source, appRoot);
var stateDir = SourceStoreManager.SourceStateDir(source, appRoot, identifierKeyed);
foreach (var candidate in MsixCandidates)
{
var msixPath = Path.Combine(stateDir, candidate);
Expand Down
Loading
Loading