// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using Duplicati.Library.Interface;
using Duplicati.Library.Utility;
namespace Duplicati.Library.Snapshots.Windows;
///
/// Class for implementing runtime-loading of Windows-only components
///
public static class WindowsShimLoader
{
///
/// The lock for thread-safe access to the type dictionary
///
private static readonly object _loadedTypesLock = new object();
///
/// Cache of types already loaded
///
private static readonly Dictionary _loadedTypes = new Dictionary();
///
/// Cached reference to the assembly we are loading from
///
private static Assembly? _loadedAssembly;
///
/// Custom load context to deal with assemblies not being listed in deps.json
///
private sealed class ModulesLoadContext : AssemblyLoadContext
{
///
/// The resolver
///
private readonly AssemblyDependencyResolver _resolver;
///
/// Assemblies that are already loading in the main context
///
private readonly HashSet _knownAssemblies;
///
/// Creates a new custom loading context
///
/// The path to the assembly to load
public ModulesLoadContext(string mainAssemblyPath)
: base("WinModules", isCollectible: false)
{
_resolver = new AssemblyDependencyResolver(mainAssemblyPath);
_knownAssemblies = new HashSet(
AppDomain.CurrentDomain.GetAssemblies()
.Select(a => a.GetName().Name)
.WhereNotNull(),
StringComparer.OrdinalIgnoreCase); ;
}
///
protected override Assembly? Load(AssemblyName name)
{
// Use existing assembly
if (name.Name != null && _knownAssemblies.Contains(name.Name))
return null;
var path = _resolver.ResolveAssemblyToPath(name);
return path is null ? null : LoadFromAssemblyPath(path);
}
///
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
var path = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
return path is null ? IntPtr.Zero : LoadUnmanagedDllFromPath(path);
}
}
///
/// Loads a type using reflection
///
/// The base type or interface to cast the result to
/// The name of the class to load
/// Any constructor arguments
/// The loaded type
private static T LoadWithReflection(string classname, params object[] args) where T : class
{
const string AssemblyName = "Duplicati.Library.WindowsModules";
const string Namespace = AssemblyName;
try
{
Type? type;
lock (_loadedTypesLock)
type = _loadedTypes.GetValueOrDefault(classname, null!);
if (type == null)
{
// Lock while loading assembly so we only load once
lock (_loadedTypesLock)
{
if (_loadedAssembly == null)
{
var path = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? "", AssemblyName + ".dll");
var alc = new ModulesLoadContext(path);
_loadedAssembly = alc.LoadFromAssemblyPath(path);
}
}
type = _loadedAssembly.GetType($"{Namespace}.{classname}", throwOnError: true, ignoreCase: false)
?? throw new ArgumentException($"Failed to load {Namespace}.{classname}, {AssemblyName}");
lock (_loadedTypesLock)
_loadedTypes[classname] = type;
}
return (T)(Activator.CreateInstance(type, args) ?? throw new ArgumentException($"Failed to load {Namespace}.{classname}, {AssemblyName}"));
}
catch (Exception ex)
{
throw new NotImplementedException($"Failed to load Windows Component {classname}: {ex.Message}", ex);
}
}
///
/// Creates a new disposable SeBackupPrivilegeScope that reatins the scope until disposed
///
/// A disposable instance
public static IDisposable NewSeBackupPrivilegeScope()
=> LoadWithReflection("SeBackupPrivilegeScope");
///
/// Creates a new PowerModeProvider that can notify of suspend/resume events
///
/// A new PowerModeProvider
public static IPowerModeProvider NewPowerModeProvider()
=> LoadWithReflection("PowerManagementModule");
///
/// Creates a new BackupDataStream for reading data with BackupRead
///
/// The path to wrap
/// A new BackupDataStream
public static Stream NewBackupDataStream(string path)
=> LoadWithReflection("BackupDataStream", path);
///
/// Loads the chosen snapshot provider
///
/// The provider to load
/// The snapshot provider
public static ISnapshotProvider GetSnapshotProvider(WindowsSnapshotProvider provider)
=> provider switch
{
// To simplify things, we have AlphaVSS in the shim loader,
// even though it is not loaded by reflection
WindowsSnapshotProvider.AlphaVSS => new AlphaVssBackup(),
WindowsSnapshotProvider.Vanara => LoadWithReflection("VanaraVssBackup"),
WindowsSnapshotProvider.Wmic => LoadWithReflection("WmicVssBackup"),
_ => throw new ArgumentException($"Invalid provider: {provider}")
};
}