fixed path bug
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user