diff --git a/dotnet/src/Devolutions.Pinget.Cli/Program.cs b/dotnet/src/Devolutions.Pinget.Cli/Program.cs index 3527dad..29dc923 100644 --- a/dotnet/src/Devolutions.Pinget.Cli/Program.cs +++ b/dotnet/src/Devolutions.Pinget.Cli/Program.cs @@ -6,7 +6,7 @@ using Devolutions.Pinget.Cli; using Devolutions.Pinget.Core; -const string Version = "0.12.0"; +const string Version = "0.13.0"; const string UpgradeUnsupportedWarning = "Upgrading packages is not supported on this platform; no changes were made."; if (args.Length == 1 && (string.Equals(args[0], "--version", StringComparison.OrdinalIgnoreCase) || string.Equals(args[0], "-v", StringComparison.OrdinalIgnoreCase))) diff --git a/dotnet/src/Devolutions.Pinget.Core.Tests/CoreTests.cs b/dotnet/src/Devolutions.Pinget.Core.Tests/CoreTests.cs index f546fe9..fd30891 100644 --- a/dotnet/src/Devolutions.Pinget.Core.Tests/CoreTests.cs +++ b/dotnet/src/Devolutions.Pinget.Core.Tests/CoreTests.cs @@ -3658,6 +3658,99 @@ public void Show_PreindexedStaleIndex_RefreshesBeforeLatestSelection() } } + [Fact] + public void Show_Preindexed_CachesSourceFilesInsideTheConfiguredAppRoot() + { + const string packageId = "Test.AppRootScopedCachePackage"; + var catalog = PreindexedCatalogFixture.Create(packageId, "1.0.0"); + + using var server = new TestPreindexedSourceServer(catalog.MsixBytes, catalog.Files); + var appRoot = TestPaths.CreateTempAppRoot(); + try + { + using var repo = Repository.Open(new RepositoryOptions + { + AppRoot = appRoot, + PreIndexedSourceAutoUpdateInterval = TimeSpan.FromDays(1), + }); + ReplaceSources(repo, ("test", server.Url, SourceKind.PreIndexed)); + WritePreindexedIndex(appRoot, repo, "test", catalog.IndexBytes); + + var result = repo.ShowManifest(new PackageQuery + { + Id = packageId, + Exact = true, + Source = "test", + }); + + Assert.Equal("1.0.0", result.PackageVersion); + Assert.NotEmpty(result.CachedFiles); + Assert.All(result.CachedFiles, file => Assert.StartsWith( + Path.GetFullPath(appRoot), + Path.GetFullPath(file), + StringComparison.OrdinalIgnoreCase)); + + var cacheRoot = Path.Combine(appRoot, "Microsoft", "Windows Package Manager"); + Assert.True(Directory.Exists(cacheRoot)); + var cached = Directory.GetFiles(cacheRoot, "*", SearchOption.AllDirectories); + Assert.Contains(cached, file => + file.Contains(Path.Combine("V2_M", "test"), StringComparison.OrdinalIgnoreCase)); + Assert.Contains(cached, file => + file.Contains(Path.Combine("V2_PVD", "test"), StringComparison.OrdinalIgnoreCase)); + } + finally + { + TestPaths.DeleteAppRoot(appRoot); + } + } + + [Fact] + public void Show_PreindexedUnsafeIdentifier_SanitizesTheCacheSegmentInsideTheAppRoot() + { + const string packageId = "Test.UnsafeIdentifierCachePackage"; + const string sourceName = "c:foo"; + var catalog = PreindexedCatalogFixture.Create(packageId, "1.0.0"); + + using var server = new TestPreindexedSourceServer(catalog.MsixBytes, catalog.Files); + var appRoot = TestPaths.CreateTempAppRoot(); + try + { + using var repo = Repository.Open(new RepositoryOptions + { + AppRoot = appRoot, + PreIndexedSourceAutoUpdateInterval = TimeSpan.FromDays(1), + }); + ReplaceSources(repo, (sourceName, server.Url, SourceKind.PreIndexed)); + WritePreindexedIndex(appRoot, repo, sourceName, catalog.IndexBytes); + + var result = repo.ShowManifest(new PackageQuery + { + Id = packageId, + Exact = true, + Source = sourceName, + }); + + Assert.Equal("1.0.0", result.PackageVersion); + Assert.NotEmpty(result.CachedFiles); + Assert.All(result.CachedFiles, file => Assert.StartsWith( + Path.GetFullPath(appRoot), + Path.GetFullPath(file), + StringComparison.OrdinalIgnoreCase)); + + var cacheRoot = Path.Combine(appRoot, "Microsoft", "Windows Package Manager"); + Assert.True(Directory.Exists(cacheRoot)); + var cached = Directory.GetFiles(cacheRoot, "*", SearchOption.AllDirectories); + Assert.Contains(cached, file => + file.Contains(Path.Combine("V2_M", "c_foo"), StringComparison.OrdinalIgnoreCase)); + Assert.Contains(cached, file => + file.Contains(Path.Combine("V2_PVD", "c_foo"), StringComparison.OrdinalIgnoreCase)); + } + finally + { + TestPaths.DeleteAppRoot(appRoot); + } + } + [Fact] public void Show_PreindexedStaleIndex_NotModifiedKeepsLocalIndex() { diff --git a/dotnet/src/Devolutions.Pinget.Core/PreIndexedSource.cs b/dotnet/src/Devolutions.Pinget.Core/PreIndexedSource.cs index 019c092..c4e4e8e 100644 --- a/dotnet/src/Devolutions.Pinget.Core/PreIndexedSource.cs +++ b/dotnet/src/Devolutions.Pinget.Core/PreIndexedSource.cs @@ -378,7 +378,7 @@ public static string ResolveV1RelativePath(SqliteConnection conn, long? pathPart // V2 version data: fetch versionData.mszyml from CDN, decompress CK+deflate → YAML public static (List Entries, string VersionDataFile) LoadV2VersionData( - HttpClient client, SqliteConnection conn, SourceRecord source, long packageRowid, string packageHash, string? appRoot = null) + HttpClient client, SqliteConnection conn, SourceRecord source, long packageRowid, string packageHash, string? appRoot) { // Resolve package id from packages table using var idCmd = conn.CreateCommand(); @@ -390,13 +390,13 @@ public static (List Entries, string VersionDataFile) LoadV2V var hashPrefix = packageHash.Length >= 8 ? packageHash[..8].ToLowerInvariant() : packageHash.ToLowerInvariant(); var relativePath = $"packages/{packageId}/{hashPrefix}/versionData.mszyml"; - var bytes = GetCachedSourceFile(client, "V2_PVD", source, relativePath, packageHash); + var bytes = GetCachedSourceFile(client, "V2_PVD", source, relativePath, packageHash, appRoot); var yaml = DecompressMszyml(bytes); var doc = ParseV2VersionDataDocument(yaml) ?? throw new InvalidOperationException("Failed to parse versionData.mszyml"); - var cacheDir = TempCachePath("V2_PVD", source.Identifier); + var cacheDir = TempCachePath(appRoot, "V2_PVD", source.Identifier); return (doc.Versions, Path.Combine(cacheDir, relativePath.Replace('/', '\\'))); } @@ -541,10 +541,10 @@ private static void SkipNode(IParser parser) } public static byte[] GetCachedSourceFile( - HttpClient client, string bucket, SourceRecord source, string relativePath, string? expectedHash) + HttpClient client, string bucket, SourceRecord source, string relativePath, string? expectedHash, string? appRoot) { var normalizedRelative = relativePath.Replace('\\', '/'); - var cacheDir = TempCachePath(bucket, source.Identifier); + var cacheDir = TempCachePath(appRoot, bucket, source.Identifier); var cachePath = Path.Combine(cacheDir, normalizedRelative.Replace('/', '\\')); // Check cache first @@ -596,18 +596,12 @@ public static byte[] GetCachedSourceFileFromMsix(SourceRecord source, string rel throw new InvalidOperationException($"Source file '{relativePath}' not found in MSIX for {source.Name}"); } - private static string TempCachePath(string bucket, string sourceIdentifier) + private static string TempCachePath(string? appRoot, string bucket, string sourceIdentifier) { - if (OperatingSystem.IsWindows()) - { - return Path.Combine( - SourceStoreManager.GetPackagedFileCacheRoot(null), - bucket, - sourceIdentifier); - } - - var tempDir = Path.GetTempPath(); - return Path.Combine(tempDir, "pinget", bucket, sourceIdentifier); + return Path.Combine( + SourceStoreManager.GetPackagedFileCacheRoot(appRoot), + bucket, + SourceStoreManager.SanitizePathSegment(sourceIdentifier)); } private static bool HashMatches(string? expected, byte[] data) diff --git a/dotnet/src/Devolutions.Pinget.Core/Repository.cs b/dotnet/src/Devolutions.Pinget.Core/Repository.cs index 8190f4f..4e9b659 100644 --- a/dotnet/src/Devolutions.Pinget.Core/Repository.cs +++ b/dotnet/src/Devolutions.Pinget.Core/Repository.cs @@ -1944,7 +1944,7 @@ private List VersionsFromV2(int sourceIndex, long packageRowid, stri var versions = PreIndexedSource.QueryV1Versions(conn, packageRowid); var selected = SelectV1Version(versions, query.Version, query.Channel, display.Id, source.Name); var relativePath = PreIndexedSource.ResolveV1RelativePath(conn, selected.PathPart); - var bytes = PreIndexedSource.GetCachedSourceFile(_client, "V1_M", source, relativePath, selected.ManifestHash); + var bytes = PreIndexedSource.GetCachedSourceFile(_client, "V1_M", source, relativePath, selected.ManifestHash, _appRoot); var manifest = ParseYamlManifest(bytes); var structuredDocument = ParseYamlManifestDocuments(bytes); manifest = manifest with { Version = selected.Version, Channel = selected.Channel }; @@ -1993,7 +1993,7 @@ private List VersionsFromV2(int sourceIndex, long packageRowid, stri var source = _store.Sources[sourceIndex]; var (entries, vdFile) = PreIndexedSource.LoadV2VersionData(_client, conn, source, packageRowid, packageHash, _appRoot); var selected = SelectV2Version(entries, query.Version, query.Channel, display.Id, source.Name); - var bytes = PreIndexedSource.GetCachedSourceFile(_client, "V2_M", source, selected.ManifestRelativePath, selected.ManifestHash); + var bytes = PreIndexedSource.GetCachedSourceFile(_client, "V2_M", source, selected.ManifestRelativePath, selected.ManifestHash, _appRoot); var manifest = ParseYamlManifest(bytes); var structuredDocument = ParseYamlManifestDocuments(bytes); manifest = manifest with { Version = selected.Version }; @@ -3524,7 +3524,7 @@ private List EnrichCorrelatedViaIndex(List installed, try { var bytes = PreIndexedSource.GetCachedSourceFile( - _client, "V2_M", source, latest.ManifestRelativePath, latest.ManifestHash); + _client, "V2_M", source, latest.ManifestRelativePath, latest.ManifestHash, _appRoot); var manifest = ParseYamlManifest(bytes); installed[idx].CorrelatedRequiresExplicitUpgrade = manifest.RequireExplicitUpgrade; installed[idx].CorrelatedLacksCompatibleInstaller = !ManifestHasCompatibleInstaller(manifest); diff --git a/dotnet/src/Devolutions.Pinget.Core/SourceStore.cs b/dotnet/src/Devolutions.Pinget.Core/SourceStore.cs index 79130b7..6ec9e76 100644 --- a/dotnet/src/Devolutions.Pinget.Core/SourceStore.cs +++ b/dotnet/src/Devolutions.Pinget.Core/SourceStore.cs @@ -642,6 +642,6 @@ private static void AddYamlKeyValue(Dictionary yamlSource, stri return value; } - private static string SanitizePathSegment(string value) => string.Concat(value.Select(c => + internal static string SanitizePathSegment(string value) => string.Concat(value.Select(c => @"\/:*?""<>|".Contains(c) ? '_' : c)); } diff --git a/dotnet/src/Devolutions.Pinget.PowerShell.Cmdlets/ModuleFiles/Devolutions.Pinget.Client.psd1 b/dotnet/src/Devolutions.Pinget.PowerShell.Cmdlets/ModuleFiles/Devolutions.Pinget.Client.psd1 index ef64ee3..ee2cb4e 100644 --- a/dotnet/src/Devolutions.Pinget.PowerShell.Cmdlets/ModuleFiles/Devolutions.Pinget.Client.psd1 +++ b/dotnet/src/Devolutions.Pinget.PowerShell.Cmdlets/ModuleFiles/Devolutions.Pinget.Client.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'Devolutions.Pinget.Client.psm1' - ModuleVersion = '0.12.0' + ModuleVersion = '0.13.0' CompatiblePSEditions = @('Desktop', 'Core') GUID = 'c6d1b5f2-5ccd-4771-9480-25caad7c58bd' Author = 'Devolutions' diff --git a/dotnet/src/Devolutions.Pinget.PowerShell.Engine/PowerShellEngineVersion.cs b/dotnet/src/Devolutions.Pinget.PowerShell.Engine/PowerShellEngineVersion.cs index 08bee35..1c6722f 100644 --- a/dotnet/src/Devolutions.Pinget.PowerShell.Engine/PowerShellEngineVersion.cs +++ b/dotnet/src/Devolutions.Pinget.PowerShell.Engine/PowerShellEngineVersion.cs @@ -2,5 +2,5 @@ namespace Devolutions.Pinget.PowerShell.Engine; public static class PowerShellEngineVersion { - public const string Current = "0.12.0"; + public const string Current = "0.13.0"; } diff --git a/nuget/Devolutions.Pinget.Cli.DotNet/Devolutions.Pinget.Cli.DotNet.csproj b/nuget/Devolutions.Pinget.Cli.DotNet/Devolutions.Pinget.Cli.DotNet.csproj index 24f4153..2c31e6e 100644 --- a/nuget/Devolutions.Pinget.Cli.DotNet/Devolutions.Pinget.Cli.DotNet.csproj +++ b/nuget/Devolutions.Pinget.Cli.DotNet/Devolutions.Pinget.Cli.DotNet.csproj @@ -1,7 +1,7 @@ - 0.12.0 + 0.13.0 Devolutions Inc. Devolutions Devolutions.Pinget.Cli.DotNet diff --git a/nuget/Devolutions.Pinget.Cli.Rust/Devolutions.Pinget.Cli.Rust.csproj b/nuget/Devolutions.Pinget.Cli.Rust/Devolutions.Pinget.Cli.Rust.csproj index db90fa9..751e292 100644 --- a/nuget/Devolutions.Pinget.Cli.Rust/Devolutions.Pinget.Cli.Rust.csproj +++ b/nuget/Devolutions.Pinget.Cli.Rust/Devolutions.Pinget.Cli.Rust.csproj @@ -1,7 +1,7 @@ - 0.12.0 + 0.13.0 Devolutions Inc. Devolutions Devolutions.Pinget.Cli.Rust diff --git a/rust/crates/pinget-cli/Cargo.toml b/rust/crates/pinget-cli/Cargo.toml index 7a21679..1acbe9a 100644 --- a/rust/crates/pinget-cli/Cargo.toml +++ b/rust/crates/pinget-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pinget-cli" -version = "0.12.0" +version = "0.13.0" edition = "2024" [lints] @@ -13,7 +13,7 @@ path = "src/main.rs" [dependencies] anyhow = "1.0.102" clap = { version = "4.6.1", features = ["derive"] } -pinget-core = { version = "0.12.0", path = "../pinget-core" } +pinget-core = { version = "0.13.0", path = "../pinget-core" } chrono = "0.4.44" dirs = "6.0" jsonschema = { version = "0.30", default-features = false, features = ["resolve-file"] } diff --git a/rust/crates/pinget-com/Cargo.toml b/rust/crates/pinget-com/Cargo.toml index f7f6193..a7bcfc6 100644 --- a/rust/crates/pinget-com/Cargo.toml +++ b/rust/crates/pinget-com/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pinget-com" -version = "0.12.0" +version = "0.13.0" edition = "2024" description = "Windows-only native COM bridge for Pinget backed by pinget-core." license = "MIT" diff --git a/rust/crates/pinget-core/Cargo.toml b/rust/crates/pinget-core/Cargo.toml index 339b8ae..f71e7b8 100644 --- a/rust/crates/pinget-core/Cargo.toml +++ b/rust/crates/pinget-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pinget-core" -version = "0.12.0" +version = "0.13.0" edition = "2024" description = "Pure Rust Pinget core library that works directly with source caches, REST endpoints, and installed package state without COM." license = "MIT" diff --git a/rust/crates/pinget-core/src/lib.rs b/rust/crates/pinget-core/src/lib.rs index bcadfac..86e0bee 100644 --- a/rust/crates/pinget-core/src/lib.rs +++ b/rust/crates/pinget-core/src/lib.rs @@ -6331,15 +6331,7 @@ fn packaged_file_cache_root(app_root: &Path) -> PathBuf { } fn cache_root(app_root: &Path) -> PathBuf { - if uses_packaged_layout(app_root) { - packaged_file_cache_root(app_root) - } else { - default_cache_root_fallback() - } -} - -fn default_cache_root_fallback() -> PathBuf { - std::env::temp_dir().join("cache") + packaged_file_cache_root(app_root) } fn packaged_source_type(kind: SourceKind) -> &'static str { @@ -11321,6 +11313,14 @@ mod tests { app_root.join("sources").join("winget_test") ); assert_eq!(pins_db_path(&app_root), app_root.join("pins.db")); + assert_eq!( + temp_cache_path(&app_root, "V1_M", &source.identifier), + app_root + .join("Microsoft") + .join("Windows Package Manager") + .join("V1_M") + .join("Test.Source") + ); } #[cfg(windows)]