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
147 changes: 147 additions & 0 deletions dotnet/src/Devolutions.Pinget.Core.Tests/CoreTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,153 @@ public void PackagedSourceYaml_OverlaysDefaultsAndMetadata()
Assert.Equal(DateTimeOffset.FromUnixTimeSeconds(1700000000).UtcDateTime, corp.LastUpdate);
}

[Fact]
public void SystemWingetExport_ParsesJsonLines()
{
const string output = """
{"Arg":"https://api.contoso.test/feed","Data":"","Explicit":false,"Identifier":"api.contoso.test","Name":"contoso","TrustLevel":["Trusted"],"Type":"Microsoft.Rest"}
{"Arg":"https://cdn.contoso.test/cache","Data":"Contoso.Source_8wekyb3d8bbwe","Explicit":true,"Identifier":"Contoso.Source_8wekyb3d8bbwe","Name":"contoso-cache","TrustLevel":["Trusted","StoreOrigin"],"Type":"Microsoft.PreIndexed.Package"}
""";

var sources = SystemWingetSourceStore.ParseExport(output);

Assert.Equal(["contoso", "contoso-cache"], sources.Select(source => source.Name).ToArray());
var rest = sources[0];
Assert.Equal(SourceKind.Rest, rest.Kind);
Assert.Equal("https://api.contoso.test/feed", rest.Arg);
Assert.Equal("api.contoso.test", rest.Identifier);
Assert.Equal("Trusted", rest.TrustLevel);
Assert.False(rest.Explicit);

var preIndexed = sources[1];
Assert.Equal(SourceKind.PreIndexed, preIndexed.Kind);
Assert.Equal("Contoso.Source_8wekyb3d8bbwe", preIndexed.Identifier);
Assert.True(preIndexed.Explicit);
}

[Fact]
public void SystemWingetExport_ParsesWrappedSources()
{
const string output = """
{
"Sources": [
{
"Name": "corp",
"Type": "Microsoft.Rest",
"Arg": "https://packages.contoso.test/api",
"Identifier": "Contoso.Rest",
"TrustLevel": "Trusted",
"Explicit": true,
"Priority": 9
}
]
}
""";

var source = Assert.Single(SystemWingetSourceStore.ParseExport(output));
Assert.Equal("corp", source.Name);
Assert.Equal(SourceKind.Rest, source.Kind);
Assert.Equal("Contoso.Rest", source.Identifier);
Assert.Equal("Trusted", source.TrustLevel);
Assert.True(source.Explicit);
Assert.Equal(9, source.Priority);
}

[Fact]
public void SystemWingetSourceStore_DetectsSecureSettingsStub()
{
Assert.True(SystemWingetSourceStore.IsSecureSettingsStub("SHA256: d52f7aa273206e81585b714fc627ecb6f6e17b6aeba7b28025124da0d25db334"));
Assert.False(SystemWingetSourceStore.IsSecureSettingsStub("Sources:\n - Name: winget\n"));
}

[Fact]
public void SystemWingetSourceStore_BuildsAddArguments()
{
var args = SystemWingetSourceStore.BuildAddArguments(
"corp",
"https://packages.contoso.test/api",
SourceKind.Rest,
"Trusted",
explicitSource: true);

Assert.Equal(
[
"source",
"add",
"--name",
"corp",
"--arg",
"https://packages.contoso.test/api",
"--type",
"Microsoft.Rest",
"--disable-interactivity",
"--trust-level",
"trusted",
"--explicit"
], args);
}

[Fact]
public void SystemWingetSourceStore_LoadUsesExportCommand()
{
var originalRunner = SystemWingetSourceStore.CommandRunner;
try
{
IReadOnlyList<string>? capturedArgs = null;
SystemWingetSourceStore.CommandRunner = args =>
{
capturedArgs = args.ToArray();
return new WingetCommandResult(
0,
"""
{"Arg":"https://api.contoso.test/feed","Data":"","Explicit":false,"Identifier":"api.contoso.test","Name":"contoso","TrustLevel":["Trusted"],"Type":"Microsoft.Rest"}
""",
"");
};

var store = SystemWingetSourceStore.Load();

Assert.Equal(SystemWingetSourceStore.BuildExportArguments(), capturedArgs);
Assert.Equal("contoso", Assert.Single(store.Sources).Name);
}
finally
{
SystemWingetSourceStore.CommandRunner = originalRunner;
}
}

[Fact]
public void PackagedSecureSettingsStub_DelegatesToSystemWingetExport()
{
var originalRunner = SystemWingetSourceStore.CommandRunner;
try
{
IReadOnlyList<string>? capturedArgs = null;
SystemWingetSourceStore.CommandRunner = args =>
{
capturedArgs = args.ToArray();
return new WingetCommandResult(
0,
"""
{"Arg":"https://api.contoso.test/feed","Data":"","Explicit":false,"Identifier":"api.contoso.test","Name":"contoso","TrustLevel":["Trusted"],"Type":"Microsoft.Rest"}
""",
"");
};

var store = SourceStoreManager.LoadPackagedStoreFromStreams(
"SHA256: d52f7aa273206e81585b714fc627ecb6f6e17b6aeba7b28025124da0d25db334",
null);

Assert.NotNull(store);
Assert.Equal(SystemWingetSourceStore.BuildExportArguments(), capturedArgs);
Assert.Equal("contoso", Assert.Single(store!.Sources).Name);
}
finally
{
SystemWingetSourceStore.CommandRunner = originalRunner;
}
}

[Fact]
public void RepositoryOpen_UsesCustomAppRoot()
{
Expand Down
76 changes: 72 additions & 4 deletions dotnet/src/Devolutions.Pinget.Core/Repository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ public class Repository : IDisposable

private readonly string _appRoot;
private readonly HttpClient _client;
private readonly bool _useSystemWingetSources;
private SourceStore _store;

private Repository(string appRoot, HttpClient client, SourceStore store)
private Repository(string appRoot, HttpClient client, SourceStore store, bool useSystemWingetSources)
{
_appRoot = appRoot;
_client = client;
_store = store;
_useSystemWingetSources = useSystemWingetSources;
}

/// <summary>
Expand All @@ -42,10 +44,11 @@ public static Repository Open(RepositoryOptions? options = null)
EnsureSqliteNativeLibraryLoaded();
var appRoot = SourceStoreManager.NormalizeAppRoot(options.AppRoot ?? Environment.GetEnvironmentVariable(AppRootEnvironmentVariable));
SourceStoreManager.EnsureAppDirs(appRoot);
var useSystemWingetSources = SourceStoreManager.UsesSystemWingetSourceCommands(appRoot);
var store = SourceStoreManager.Load(appRoot);
var client = new HttpClient();
client.DefaultRequestHeaders.UserAgent.ParseAdd(options.UserAgent);
return new Repository(appRoot, client, store);
return new Repository(appRoot, client, store, useSystemWingetSources);
}

internal static IEnumerable<string> GetSqliteNativeLibraryCandidates(string assemblyDirectory)
Expand Down Expand Up @@ -137,10 +140,21 @@ public void SetRequestHeader(string name, string value)

// ── Source management ──

public List<SourceRecord> ListSources() => _store.Sources.ToList();
public List<SourceRecord> ListSources()
{
RefreshSystemWingetSources();
return _store.Sources.ToList();
}

public void AddSource(string name, string arg, SourceKind kind, string trustLevel = "None", bool explicitSource = false, int priority = 0)
{
if (_useSystemWingetSources)
{
SystemWingetSourceStore.AddSource(name, arg, kind, NormalizeSourceTrustLevel(trustLevel), explicitSource);
RefreshSystemWingetSources();
return;
}

if (_store.Sources.Any(s => string.Equals(s.Name, name, StringComparison.OrdinalIgnoreCase)))
throw new InvalidOperationException($"A source with name '{name}' already exists.");
if (_store.Sources.Any(s => s.Arg == arg))
Expand All @@ -161,6 +175,9 @@ public void AddSource(string name, string arg, SourceKind kind, string trustLeve

public void EditSource(string name, bool? explicitSource = null, string? trustLevel = null)
{
if (_useSystemWingetSources)
throw new InvalidOperationException("Editing system WinGet sources is not supported by winget source commands.");

var source = _store.Sources.FirstOrDefault(s => string.Equals(s.Name, name, StringComparison.OrdinalIgnoreCase))
?? throw new InvalidOperationException($"Source '{name}' not found.");

Expand All @@ -175,6 +192,13 @@ public void EditSource(string name, bool? explicitSource = null, string? trustLe

public void RemoveSource(string name)
{
if (_useSystemWingetSources)
{
SystemWingetSourceStore.RemoveSource(name);
RefreshSystemWingetSources();
return;
}

var source = _store.Sources.FirstOrDefault(s => s.Name == name)
?? throw new InvalidOperationException($"Source '{name}' not found.");

Expand All @@ -187,6 +211,13 @@ public void RemoveSource(string name)

public void ResetSource(string name)
{
if (_useSystemWingetSources)
{
SystemWingetSourceStore.ResetSource(name);
RefreshSystemWingetSources();
return;
}

var index = _store.Sources.FindIndex(s => string.Equals(s.Name, name, StringComparison.OrdinalIgnoreCase));
if (index < 0)
throw new InvalidOperationException($"Source '{name}' not found.");
Expand Down Expand Up @@ -214,6 +245,13 @@ public void ResetSource(string name)

public void ResetSources()
{
if (_useSystemWingetSources)
{
SystemWingetSourceStore.ResetSources();
RefreshSystemWingetSources();
return;
}

foreach (var source in _store.Sources)
{
var stateDir = SourceStoreManager.SourceStateDir(source, _appRoot);
Expand Down Expand Up @@ -297,6 +335,9 @@ public void EnsureSettingsFiles()

public List<SourceUpdateResult> UpdateSources(string? sourceName = null)
{
if (_useSystemWingetSources)
return UpdateSystemWingetSources(sourceName);

var indexes = ResolveSourceIndexes(sourceName);
var results = new List<SourceUpdateResult>();

Expand Down Expand Up @@ -1022,7 +1063,8 @@ private SqliteConnection OpenPreindexedConnection(int sourceIndex)
{
PreIndexedSource.Update(_client, source, _appRoot);
source.LastUpdate = DateTime.UtcNow;
SourceStoreManager.Save(_store, _appRoot);
if (!_useSystemWingetSources)
SourceStoreManager.Save(_store, _appRoot);
}

var conn = new SqliteConnection($"Data Source={indexPath};Mode=ReadOnly");
Expand Down Expand Up @@ -2013,6 +2055,32 @@ private List<int> ResolveSourceIndexes(string? sourceName)
throw new InvalidOperationException($"Source '{sourceName}' not found.");
}

private void RefreshSystemWingetSources()
{
if (_useSystemWingetSources)
_store = SystemWingetSourceStore.Load();
}

private List<SourceUpdateResult> UpdateSystemWingetSources(string? sourceName)
{
RefreshSystemWingetSources();
var indexes = ResolveSourceIndexes(sourceName);
var selectedNames = indexes.Select(index => _store.Sources[index].Name).ToList();
var detail = SystemWingetSourceStore.UpdateSources(sourceName);
RefreshSystemWingetSources();

return selectedNames
.Select(name => _store.Sources.FirstOrDefault(source => source.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
.Where(source => source is not null)
.Select(source => new SourceUpdateResult
{
Name = source!.Name,
Kind = source.Kind,
Detail = detail,
})
.ToList();
}

// ── Scoring ──

private static int SearchMatchSortScore(SearchMatch match, PackageQuery query)
Expand Down
27 changes: 26 additions & 1 deletion dotnet/src/Devolutions.Pinget.Core/SourceStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,26 @@
return JsonSerializer.Deserialize(json, SourceStoreContext.Default.SourceStore) ?? SourceStore.Default();
}

internal static bool UsesSystemWingetSourceCommands(string? appRoot)
{
if (!OperatingSystem.IsWindows())
return false;

var root = NormalizeAppRoot(appRoot);
if (!UsesPackagedLayout(root))
return false;

var userSourcesPath = GetPackagedUserSourcesPath(root);
var userSourcesYaml = File.Exists(userSourcesPath) ? File.ReadAllText(userSourcesPath) : null;
return SystemWingetSourceStore.IsSecureSettingsStub(userSourcesYaml);
}

public static void Save(SourceStore store, string? appRoot = null)
{
var root = NormalizeAppRoot(appRoot);
if (UsesSystemWingetSourceCommands(root))
throw new InvalidOperationException("System WinGet source settings must be modified through winget.");

if (UsesPackagedLayout(root))
{
SavePackagedStore(root, store);
Expand Down Expand Up @@ -203,14 +220,22 @@
return store;
}

internal static SourceStore? LoadPackagedStoreFromStreams(string? userSourcesYaml, string? metadataYaml)
{
if (SystemWingetSourceStore.IsSecureSettingsStub(userSourcesYaml))
return SystemWingetSourceStore.Load();

return ParsePackagedSourceStore(userSourcesYaml, metadataYaml);
}

private static SourceStore? LoadPackagedStore(string root)
{
var userSourcesPath = GetPackagedUserSourcesPath(root);
var metadataPath = Path.Combine(root, PackagedMetadataFileName);

var userSourcesYaml = File.Exists(userSourcesPath) ? File.ReadAllText(userSourcesPath) : null;
var metadataYaml = File.Exists(metadataPath) ? File.ReadAllText(metadataPath) : null;
return ParsePackagedSourceStore(userSourcesYaml, metadataYaml);
return LoadPackagedStoreFromStreams(userSourcesYaml, metadataYaml);
}

private static void SavePackagedStore(string root, SourceStore store)
Expand Down Expand Up @@ -276,7 +301,7 @@
private static string GetPackagedSecureSettingsRoot()
{
var programData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
var sid = WindowsIdentity.GetCurrent().User?.Value ?? "UnknownSid";

Check warning on line 304 in dotnet/src/Devolutions.Pinget.Core/SourceStore.cs

View workflow job for this annotation

GitHub Actions / Dotnet Build

This call site is reachable on all platforms. 'SecurityIdentifier.Value' is only supported on: 'windows'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416)

Check warning on line 304 in dotnet/src/Devolutions.Pinget.Core/SourceStore.cs

View workflow job for this annotation

GitHub Actions / Dotnet Build

This call site is reachable on all platforms. 'WindowsIdentity.GetCurrent()' is only supported on: 'windows'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416)

Check warning on line 304 in dotnet/src/Devolutions.Pinget.Core/SourceStore.cs

View workflow job for this annotation

GitHub Actions / Dotnet Build

This call site is reachable on all platforms. 'WindowsIdentity.User' is only supported on: 'windows'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416)

Check warning on line 304 in dotnet/src/Devolutions.Pinget.Core/SourceStore.cs

View workflow job for this annotation

GitHub Actions / Dotnet Build

This call site is reachable on all platforms. 'SecurityIdentifier.Value' is only supported on: 'windows'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416)

Check warning on line 304 in dotnet/src/Devolutions.Pinget.Core/SourceStore.cs

View workflow job for this annotation

GitHub Actions / Dotnet Build

This call site is reachable on all platforms. 'WindowsIdentity.GetCurrent()' is only supported on: 'windows'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416)

Check warning on line 304 in dotnet/src/Devolutions.Pinget.Core/SourceStore.cs

View workflow job for this annotation

GitHub Actions / Dotnet Build

This call site is reachable on all platforms. 'WindowsIdentity.User' is only supported on: 'windows'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416)

Check warning on line 304 in dotnet/src/Devolutions.Pinget.Core/SourceStore.cs

View workflow job for this annotation

GitHub Actions / Dotnet Build

This call site is reachable on all platforms. 'SecurityIdentifier.Value' is only supported on: 'windows'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416)

Check warning on line 304 in dotnet/src/Devolutions.Pinget.Core/SourceStore.cs

View workflow job for this annotation

GitHub Actions / Dotnet Build

This call site is reachable on all platforms. 'WindowsIdentity.GetCurrent()' is only supported on: 'windows'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416)

Check warning on line 304 in dotnet/src/Devolutions.Pinget.Core/SourceStore.cs

View workflow job for this annotation

GitHub Actions / Dotnet Build

This call site is reachable on all platforms. 'WindowsIdentity.User' is only supported on: 'windows'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416)

Check warning on line 304 in dotnet/src/Devolutions.Pinget.Core/SourceStore.cs

View workflow job for this annotation

GitHub Actions / Dotnet Build

This call site is reachable on all platforms. 'SecurityIdentifier.Value' is only supported on: 'windows'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416)
return Path.Combine(programData, "Microsoft", "WinGet", sid, "settings", "pkg", PackagedName);
}

Expand Down
Loading