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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ version numbers, because that is what those releases were called.

## [Unreleased]

### Fixed: restore and snapshot integrity

- Verified successful manifest rows against their physical artifacts and kept unavailable modules out
of restore sets, preventing stale metadata from authorizing an unsafe restore.
- Read the selected app export directly from its prepared payload and exposed friendly source names in
the reinstall picker.
- Created a fresh, second-precision folder for every user backup and reserved collision suffixes
atomically so repeated or concurrent runs cannot reuse an existing restore point.

### Fixed: WPF Timeline interaction and accessibility

- Added mouse double-click activation alongside Enter for Timeline restore points, with duplicate-open
Expand Down
26 changes: 21 additions & 5 deletions src/WinRestoreKit.Application/AppRestore/AppRestoreService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,21 @@ internal enum AppRestoreProblemRouting

internal sealed class AppRestoreSource
{
internal AppRestoreSource(string path, string displayName, bool isSelectedRestoreSource)
internal AppRestoreSource(string path, string displayName, bool isSelectedRestoreSource,
bool isPreparedPayload)
{
Path = path ?? string.Empty;
DisplayName = displayName ?? string.Empty;
IsSelectedRestoreSource = isSelectedRestoreSource;
IsPreparedPayload = isPreparedPayload;
}

public string Path { get; }
public string DisplayName { get; }
public bool IsSelectedRestoreSource { get; }
internal bool IsPreparedPayload { get; }

public override string ToString() => DisplayName;
}

internal sealed class AppExport
Expand Down Expand Up @@ -88,14 +93,14 @@ internal static IReadOnlyList<AppRestoreSource> BuildSources(string selectedRest
IReadOnlyList<SnapshotEvent> snapshots)
{
var sources = new List<AppRestoreSource>();
AddDistinct(sources, selectedRestorePath, "Selected restore source", true);
AddDistinct(sources, selectedRestorePath, "Selected restore source", true, true);

foreach (SnapshotEvent snapshot in snapshots ?? Array.Empty<SnapshotEvent>())
{
if (snapshot == null || !snapshot.IsRestorable)
continue;

AddDistinct(sources, snapshot.CanonicalPath, snapshot.DisplayName, false);
AddDistinct(sources, snapshot.CanonicalPath, snapshot.DisplayName, false, false);
}

return Array.AsReadOnly(sources.ToArray());
Expand Down Expand Up @@ -123,6 +128,16 @@ internal static AppExport ReadFromSource(string sourcePath)
return ReadExport(AppStoreApps.ExportPathIn(scope.Path));
}

internal static AppExport ReadFromSourceEntry(AppRestoreSource source)
{
if (source == null)
return AppExport.Unreadable("No app backup source is selected.");

return source.IsPreparedPayload
? ReadExport(AppStoreApps.ExportPathIn(source.Path))
: ReadFromSource(source.Path);
}

internal static Task<AppRestoreOutcome> InstallAsync(IReadOnlyList<string> packageIdentifiers,
Func<bool> stopRequested) =>
InstallAsync(packageIdentifiers, id => Utils.RunWingetAsync(true, "install", "--id", id,
Expand Down Expand Up @@ -197,7 +212,7 @@ internal static AppRestoreOutcome ComposeOutcome(int requested, int attempted,
}

private static void AddDistinct(List<AppRestoreSource> sources, string path, string displayName,
bool isSelectedRestoreSource)
bool isSelectedRestoreSource, bool isPreparedPayload)
{
if (string.IsNullOrWhiteSpace(path))
return;
Expand All @@ -216,7 +231,8 @@ private static void AddDistinct(List<AppRestoreSource> sources, string path, str
StringComparison.OrdinalIgnoreCase)))
return;

sources.Add(new AppRestoreSource(canonicalPath, displayName, isSelectedRestoreSource));
sources.Add(new AppRestoreSource(canonicalPath, displayName, isSelectedRestoreSource,
isPreparedPayload));
}

private static AppExport ReadExport(string path)
Expand Down
4 changes: 4 additions & 0 deletions src/WinRestoreKit.Application/Comparison/ModuleComparison.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ internal ModuleComparison(BackupBase module, ComparisonState state,

public bool HasUsableArtifact { get; }

public bool CanRestore => HasUsableArtifact
&& State != ComparisonState.Unavailable
&& State != ComparisonState.NotCaptured;

public string ArtifactSummary { get; }

public string Reason { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,35 +161,38 @@ private static ModuleComparison CompareOne(BackupBase module, string payloadPath
if (IsManifestWithoutArtifact(entry))
return NotCapturedFromManifest(module, entry);

bool usableArtifact;
string artifactSummary;
if (entry?.State == BackupManifest.StateSucceeded)
bool manifestSucceeded = entry?.State == BackupManifest.StateSucceeded;
bool? probe;
try
{
usableArtifact = true;
artifactSummary = "The snapshot manifest records this module as captured.";
probe = module.HasArtifactIn(payloadPath);
}
else
catch (Exception ex)
{
bool? probe;
try
{
probe = module.HasArtifactIn(payloadPath);
}
catch (Exception ex)
{
LogHelper.Instance.LogMessage("Comparison artifact probe failed for " + module.Title
+ ": " + ex.Message);
return new ModuleComparison(module, ComparisonState.Unavailable, false,
"Artifact presence could not be determined.", ex.Message);
}
LogHelper.Instance.LogMessage("Comparison artifact probe failed for " + module.Title
+ ": " + ex.Message);
return new ModuleComparison(module, ComparisonState.Unavailable, false,
"Artifact presence could not be determined.", ex.Message);
}

if (probe == false)
{
string reason = manifestSucceeded
? "The manifest records this module as captured, but its restore artifact is missing."
: "The module proved that this snapshot has no restore artifact.";
return new ModuleComparison(module, ComparisonState.NotCaptured, false, reason, string.Empty);
}

if (probe == false)
bool usableArtifact;
string artifactSummary;
if (!probe.HasValue)
{
if (manifestSucceeded)
{
return new ModuleComparison(module, ComparisonState.NotCaptured, false,
"The module proved that this snapshot has no restore artifact.", string.Empty);
usableArtifact = true;
artifactSummary = "The manifest records this module as captured; the module has no physical artifact probe.";
}

if (!probe.HasValue)
else
{
if (manifest != null)
{
Expand All @@ -202,11 +205,13 @@ private static ModuleComparison CompareOne(BackupBase module, string payloadPath
usableArtifact = true;
artifactSummary = "No manifest is available and the module cannot disprove a legacy artifact.";
}
else
{
usableArtifact = true;
artifactSummary = "The module verified a captured artifact.";
}
}
else
{
usableArtifact = true;
artifactSummary = manifestSucceeded
? "The manifest and the module both verify a captured artifact."
: "The module verified a captured artifact.";
}

try
Expand Down
Loading
Loading