fixed path bug

This commit is contained in:
2026-06-10 14:30:10 -05:00
parent a691828f1c
commit d04c9b2058
5 changed files with 94 additions and 9 deletions
+1 -1
View File
@@ -53,7 +53,7 @@ the example manifest (`examples/Covenant-SetupSampleApp-install.toml`).
- `InstallManifest` — Declarative TOML contract: directories, files, registry, shortcuts, scripts, purge spec
- `Journal` / `JournalAction` — Serialized record of every mutation for deterministic rollback
- `MutationTracker` trait — Extensibility point (MVP uses `DeclaredTracker`; future: `ObservedTracker` for ETW-based capture)
- `PathResolver` — Resolves `{ProgramFilesX64}`, `{LocalAppData}`, `{Desktop}` tokens via `SHGetKnownFolderPath`
- `PathResolver` — Resolves `{ProgramFilesX64}`, `{ProgramFilesX86}`, `{LocalAppData}`, `{Desktop}` tokens via `SHGetKnownFolderPath`
- `Logger` — Dual-mode output: structured JSON (`--json` flag) for IPC or human-readable text
**Elevation:** Manifest/journal is scanned for `HKLM` registry or ProgramFiles paths to determine if admin is needed. Auto-relaunches via `ShellExecuteW` with `runas` when `--elevate` flag is set. Exit code 33 signals elevation required.
+21 -4
View File
@@ -16,8 +16,8 @@ public class MainViewModelTests
Assert.Empty(viewModel.PurgePaths);
Assert.Equal("Covenant-SetupSampleApp-install.toml", viewModel.ExpectedManifestFileName);
Assert.Contains("app_name = 'Covenant-Setup Sample App'", viewModel.TomlPreview);
Assert.Contains(@"{LocalAppData}\CovenantSetupSample", viewModel.TomlPreview);
Assert.Contains(@"{Desktop}\CovenantSetupSample.lnk", viewModel.TomlPreview);
Assert.Contains(@"{ProgramFilesX64}\CovenantSetupSample", viewModel.TomlPreview);
Assert.Contains(@"{Desktop}\sample_app.lnk", viewModel.TomlPreview);
Assert.DoesNotContain(@"{Desktop}\Covenant-Setup Sample App.lnk", viewModel.TomlPreview, StringComparison.Ordinal);
Assert.Contains(@"payload\sample_app.cmd", viewModel.TomlPreview);
}
@@ -168,9 +168,9 @@ public class MainViewModelTests
var defaultDocument = viewModel.BuildDocument();
var defaultShortcut = Assert.Single(defaultDocument.Shortcuts);
Assert.Equal(@"{Desktop}\RenamedApp.lnk", defaultShortcut.Path);
Assert.Equal(@"{Desktop}\renamed.lnk", defaultShortcut.Path);
Assert.Equal(@"{ProgramFilesX64}\RenamedApp\bin\renamed.exe", defaultShortcut.Target);
Assert.Equal(@"{ProgramFilesX64}\RenamedApp", defaultShortcut.WorkingDirectory);
Assert.Equal(@"{ProgramFilesX64}\RenamedApp\bin", defaultShortcut.WorkingDirectory);
Assert.Equal("Launch Renamed App", defaultShortcut.Description);
viewModel.ShortcutDescription = "Start Renamed App";
@@ -219,4 +219,21 @@ public class MainViewModelTests
Assert.True(viewModel.HasValidationErrors);
Assert.False(viewModel.CanPackage);
}
[Fact]
public void Placeholders_update_with_install_root_token()
{
var viewModel = new MainViewModel(() => null)
{
InstallRootToken = "{ProgramFilesX64}"
};
Assert.Equal(@"{ProgramFilesX64}\Vendor\App", viewModel.DirectoryPlaceholder);
Assert.Equal(@"{ProgramFilesX64}\Vendor\App\bin\app.exe", viewModel.FileDestinationPlaceholder);
viewModel.InstallRootToken = "{LocalAppData}";
Assert.Equal(@"{LocalAppData}\Vendor\App", viewModel.DirectoryPlaceholder);
Assert.Equal(@"{LocalAppData}\Vendor\App\bin\app.exe", viewModel.FileDestinationPlaceholder);
}
}
+6
View File
@@ -58,6 +58,10 @@ internal sealed partial class MainViewModel : INotifyPropertyChanged
set => SetProperty(ref _installRootToken, value);
}
public string DirectoryPlaceholder => $"{InstallRootToken}\\Vendor\\App";
public string FileDestinationPlaceholder => $"{InstallRootToken}\\Vendor\\App\\bin\\app.exe";
public string ApplicationFolder
{
get => _applicationFolder;
@@ -414,6 +418,8 @@ internal sealed partial class MainViewModel : INotifyPropertyChanged
OnPropertyChanged(nameof(ShortcutWorkingDirectory));
OnPropertyChanged(nameof(PurgePathsPreview));
OnPropertyChanged(nameof(PurgeRegistryBranchesPreview));
OnPropertyChanged(nameof(DirectoryPlaceholder));
OnPropertyChanged(nameof(FileDestinationPlaceholder));
}
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+32 -4
View File
@@ -313,7 +313,14 @@ internal sealed class MainWindow : Window
private FrameworkElement BuildDirectoriesSection()
{
var pathBox = new TextBox { PlaceholderText = @"{LocalAppData}\Vendor\App" };
var pathBox = new TextBox();
pathBox.SetBinding(
TextBox.PlaceholderTextProperty,
new Binding
{
Path = new PropertyPath(nameof(MainViewModel.DirectoryPlaceholder)),
Mode = BindingMode.OneWay
});
var rows = RemovableRows(_viewModel.Directories);
var addButton = new Button { Content = "Add Path", MinWidth = 88 };
addButton.Click += (_, _) =>
@@ -331,7 +338,14 @@ internal sealed class MainWindow : Window
private FrameworkElement BuildFilesSection()
{
var sourceBox = new TextBox { PlaceholderText = @"payload\app.exe" };
var destinationBox = new TextBox { PlaceholderText = @"{LocalAppData}\Vendor\App\bin\app.exe" };
var destinationBox = new TextBox();
destinationBox.SetBinding(
TextBox.PlaceholderTextProperty,
new Binding
{
Path = new PropertyPath(nameof(MainViewModel.FileDestinationPlaceholder)),
Mode = BindingMode.OneWay
});
var rows = RemovableRows(_viewModel.Files);
var addButton = new Button { Content = "Add", MinWidth = 72 };
addButton.Click += async (_, _) =>
@@ -358,7 +372,14 @@ internal sealed class MainWindow : Window
{
var keyBox = new TextBox { PlaceholderText = @"HKCU\Software\VendorApp" };
var nameBox = new TextBox { PlaceholderText = "InstallRoot" };
var valueBox = new TextBox { PlaceholderText = @"{LocalAppData}\Vendor\App" };
var valueBox = new TextBox();
valueBox.SetBinding(
TextBox.PlaceholderTextProperty,
new Binding
{
Path = new PropertyPath(nameof(MainViewModel.DirectoryPlaceholder)),
Mode = BindingMode.OneWay
});
var rows = RemovableRows(_viewModel.Registry);
var addButton = new Button { Content = "Add", MinWidth = 72 };
addButton.Click += async (_, _) =>
@@ -403,7 +424,14 @@ internal sealed class MainWindow : Window
Height = 76,
PlaceholderText = "-ExecutionPolicy"
};
var workingDirectoryBox = new TextBox { PlaceholderText = @"{LocalAppData}\Vendor\App" };
var workingDirectoryBox = new TextBox();
workingDirectoryBox.SetBinding(
TextBox.PlaceholderTextProperty,
new Binding
{
Path = new PropertyPath(nameof(MainViewModel.DirectoryPlaceholder)),
Mode = BindingMode.OneWay
});
var rows = RemovableRows(_viewModel.Scripts);
var addButton = new Button { Content = "Add", MinWidth = 72 };
addButton.Click += async (_, _) =>
+34
View File
@@ -36,6 +36,7 @@ use windows::core::{Interface, PCWSTR, PWSTR, w};
pub struct PathResolver {
pub program_files_x64: PathBuf,
pub program_files_x86: PathBuf,
pub local_app_data: PathBuf,
pub desktop: PathBuf,
admin_roots: Vec<String>,
@@ -50,6 +51,7 @@ impl PathResolver {
build_admin_roots(&[&program_files_x64, &program_files_x86, &windows_dir]);
Ok(Self {
program_files_x64,
program_files_x86,
local_app_data: known_folder(&FOLDERID_LocalAppData, logger)?,
desktop: known_folder(&FOLDERID_Desktop, logger)?,
admin_roots,
@@ -63,6 +65,10 @@ impl PathResolver {
"{ProgramFilesX64}",
&self.program_files_x64.to_string_lossy(),
)
.replace(
"{ProgramFilesX86}",
&self.program_files_x86.to_string_lossy(),
)
.replace("{LocalAppData}", &self.local_app_data.to_string_lossy())
.replace("{Desktop}", &self.desktop.to_string_lossy()),
)
@@ -80,6 +86,7 @@ impl PathResolver {
let admin_roots = build_admin_roots(&roots.iter().collect::<Vec<_>>());
Self {
program_files_x64: PathBuf::new(),
program_files_x86: PathBuf::new(),
local_app_data: PathBuf::new(),
desktop: PathBuf::new(),
admin_roots,
@@ -631,6 +638,33 @@ mod tests {
])
}
#[test]
fn resolve_replaces_every_known_folder_token() {
let r = PathResolver {
program_files_x64: PathBuf::from("C:\\Program Files"),
program_files_x86: PathBuf::from("C:\\Program Files (x86)"),
local_app_data: PathBuf::from("C:\\Users\\alice\\AppData\\Local"),
desktop: PathBuf::from("C:\\Users\\alice\\Desktop"),
admin_roots: Vec::new(),
};
assert_eq!(
r.resolve("{ProgramFilesX64}\\App\\bin"),
PathBuf::from("C:\\Program Files\\App\\bin")
);
assert_eq!(
r.resolve("{ProgramFilesX86}\\Vendor\\app.exe"),
PathBuf::from("C:\\Program Files (x86)\\Vendor\\app.exe")
);
assert_eq!(
r.resolve("{LocalAppData}\\Vendor"),
PathBuf::from("C:\\Users\\alice\\AppData\\Local\\Vendor")
);
assert_eq!(
r.resolve("{Desktop}\\App.lnk"),
PathBuf::from("C:\\Users\\alice\\Desktop\\App.lnk")
);
}
#[test]
fn requires_admin_matches_subpaths() {
let r = resolver();