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
19 changes: 17 additions & 2 deletions dotnet/src/Devolutions.Pinget.Core.Tests/CoreTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,32 @@ public void DefaultSources_ContainsWingetAndMsstore()
}

[Fact]
public void PackagedLayout_DefaultsToPackagedSettingsAndCachePaths()
public void PackagedLayout_PathsResolveFromPackagedAppRoot()
{
if (!OperatingSystem.IsWindows())
return;

var appRoot = SourceStoreManager.NormalizeAppRoot(null);
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var appRoot = Path.Combine(localAppData, "Packages", SourceStoreManager.PackagedFamilyName, "LocalState");
Assert.EndsWith(Path.Combine("Packages", SourceStoreManager.PackagedFamilyName, "LocalState"), appRoot, StringComparison.OrdinalIgnoreCase);
Assert.EndsWith(Path.Combine("Packages", SourceStoreManager.PackagedFamilyName, "LocalState", "settings.json"), SettingsStoreManager.UserSettingsPath(appRoot), StringComparison.OrdinalIgnoreCase);
Assert.EndsWith(Path.Combine("Packages", SourceStoreManager.PackagedFamilyName, "LocalState", "Microsoft", "Windows Package Manager"), SourceStoreManager.GetPackagedFileCacheRoot(appRoot), StringComparison.OrdinalIgnoreCase);
}

[Fact]
public void DefaultAppRootOutsidePackage_AvoidsPackagedLayout()
{
// Tests run without AppX package identity, so the default app root must
// not resolve to the WinGet packaged LocalState — that location requires
// a brokered/elevated writer for its secure-settings stream.
if (!OperatingSystem.IsWindows())
return;

var appRoot = SourceStoreManager.NormalizeAppRoot(null);
Assert.EndsWith(Path.Combine("Devolutions", "Pinget"), appRoot, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain(Path.Combine("Packages", SourceStoreManager.PackagedFamilyName), appRoot, StringComparison.OrdinalIgnoreCase);
}

[Fact]
public void PackagedSourceYaml_OverlaysDefaultsAndMetadata()
{
Expand Down
28 changes: 25 additions & 3 deletions dotnet/src/Devolutions.Pinget.Core/SourceStore.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Runtime.InteropServices;
using System.Security.Principal;
using System.Text.Json;
using System.Text.Json.Serialization;
Expand Down Expand Up @@ -55,9 +56,30 @@
return Path.GetFullPath(appRoot);

var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
return OperatingSystem.IsWindows()
? Path.Combine(localAppData, "Packages", PackagedFamilyName, "LocalState")
: Path.Combine(localAppData, "pinget");
if (!OperatingSystem.IsWindows())
return Path.Combine(localAppData, "pinget");

if (HasCurrentPackageIdentity())
return Path.Combine(localAppData, "Packages", PackagedFamilyName, "LocalState");

return Path.Combine(localAppData, "Devolutions", "Pinget");
}

[DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = false)]
private static extern int GetCurrentPackageFullName(ref uint packageFullNameLength, IntPtr packageFullName);

private static bool HasCurrentPackageIdentity()
{
if (!OperatingSystem.IsWindows())
return false;

// APPMODEL_ERROR_NO_PACKAGE; documented Win32 error returned when the
// calling process has no AppX package identity.
const int AppModelErrorNoPackage = 15700;

uint length = 0;
var rc = GetCurrentPackageFullName(ref length, IntPtr.Zero);
return rc != AppModelErrorNoPackage;
}

public static void EnsureAppDirs(string? appRoot = null)
Expand Down Expand Up @@ -240,7 +262,7 @@

private static void SavePackagedStore(string root, SourceStore store)
{
var serializer = new SerializerBuilder()

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

View workflow job for this annotation

GitHub Actions / .NET NativeAOT CLI (win-x64)

Using member 'YamlDotNet.Serialization.SerializerBuilder.SerializerBuilder()' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. This builder configures the serializer to use reflection which is not compatible with ahead-of-time compilation or assembly trimming. You need to use the code generator/analyzer to generate static code and use the 'StaticSerializerBuilder' object instead of this one.

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

View workflow job for this annotation

GitHub Actions / .NET NativeAOT CLI (win-arm64)

Using member 'YamlDotNet.Serialization.SerializerBuilder.SerializerBuilder()' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. This builder configures the serializer to use reflection which is not compatible with ahead-of-time compilation or assembly trimming. You need to use the code generator/analyzer to generate static code and use the 'StaticSerializerBuilder' object instead of this one.
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.Build();

Expand Down
2 changes: 1 addition & 1 deletion rust/crates/pinget-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ zip = "8.5.1"

[target.'cfg(windows)'.dependencies]
winreg = "0.55.0"
windows-sys = { version = "0.61.2", features = ["Win32_Foundation", "Win32_Security", "Win32_Security_Authorization", "Win32_System_Threading"] }
windows-sys = { version = "0.61.2", features = ["Win32_Foundation", "Win32_Security", "Win32_Security_Authorization", "Win32_Storage_Packaging_Appx", "Win32_System_Threading"] }
59 changes: 53 additions & 6 deletions rust/crates/pinget-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ use windows_sys::Win32::Security::Authorization::ConvertSidToStringSidW;
#[cfg(windows)]
use windows_sys::Win32::Security::{GetTokenInformation, TOKEN_QUERY, TOKEN_USER, TokenUser};
#[cfg(windows)]
use windows_sys::Win32::Storage::Packaging::Appx::GetCurrentPackageFullName;
#[cfg(windows)]
use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
#[cfg(windows)]
use winreg::{RegKey, enums::*};
Expand Down Expand Up @@ -2995,10 +2997,13 @@ fn default_app_root() -> Result<PathBuf> {

#[cfg(windows)]
{
Ok(local_app_data
.join("Packages")
.join(PACKAGED_FAMILY_NAME)
.join("LocalState"))
if has_current_package_identity() {
return Ok(local_app_data
.join("Packages")
.join(PACKAGED_FAMILY_NAME)
.join("LocalState"));
}
Ok(local_app_data.join("Devolutions").join("Pinget"))
}

#[cfg(not(windows))]
Expand All @@ -3007,6 +3012,19 @@ fn default_app_root() -> Result<PathBuf> {
}
}

#[cfg(windows)]
fn has_current_package_identity() -> bool {
// APPMODEL_ERROR_NO_PACKAGE; documented Win32 error returned when the calling
// process has no AppX package identity.
const APPMODEL_ERROR_NO_PACKAGE: u32 = 15700;

let mut length: u32 = 0;
// SAFETY: GetCurrentPackageFullName accepts a null name buffer when length is 0; it returns
// ERROR_INSUFFICIENT_BUFFER for packaged callers and APPMODEL_ERROR_NO_PACKAGE otherwise.
let rc = unsafe { GetCurrentPackageFullName(&mut length, std::ptr::null_mut()) };
rc != APPMODEL_ERROR_NO_PACKAGE
}

fn store_path(app_root: &Path) -> PathBuf {
app_root.join("sources.json")
}
Expand Down Expand Up @@ -6741,8 +6759,12 @@ mod tests {

#[cfg(windows)]
#[test]
fn packaged_layout_defaults_to_packaged_paths() {
let app_root = default_app_root().expect("default app root");
fn packaged_layout_paths_resolve_from_packaged_app_root() {
let local_app_data = dirs::data_local_dir().expect("LocalAppData");
let app_root = local_app_data
.join("Packages")
.join(PACKAGED_FAMILY_NAME)
.join("LocalState");
assert!(uses_packaged_layout(&app_root));
assert!(
user_settings_path(&app_root)
Expand All @@ -6761,6 +6783,31 @@ mod tests {
);
}

#[cfg(windows)]
#[test]
fn default_app_root_outside_package_avoids_packaged_layout() {
// Tests run without AppX package identity, so the default app root must not
// resolve to the WinGet packaged LocalState — that location requires a
// brokered/elevated writer for its secure-settings stream.
let prior = std::env::var_os("PINGET_APPROOT");
// SAFETY: tests in this module are not run concurrently with other env mutators.
unsafe { std::env::remove_var("PINGET_APPROOT") };

let result = default_app_root();

if let Some(prior) = prior {
// SAFETY: restoring the prior value before assertions panic, single-threaded test.
unsafe { std::env::set_var("PINGET_APPROOT", prior) };
}

let app_root = result.expect("default app root");
assert!(!uses_packaged_layout(&app_root), "got packaged layout for {app_root:?}");
assert!(
app_root.ends_with(Path::new("Devolutions").join("Pinget")),
"unexpected fallback app root: {app_root:?}"
);
}

#[test]
fn packaged_source_stream_overlays_defaults_and_metadata() {
let store = parse_packaged_source_store(
Expand Down
Loading