// 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.Collections.Specialized;
using System.Linq;
using System.Reflection;
using Duplicati.Library.Interface;
namespace Duplicati.Library.Utility;
///
/// Helper to get the command line argument type attribute
///
public interface ICommandLineArgumentMapper
{
///
/// Get the command line argument type attribute
///
/// The member info to get the attribute from
/// The command line argument type attribute
CommandLineArgumentDescriptionAttribute? GetCommandLineArgumentDescription(MemberInfo mi);
}
///
/// Attribute to define command line argument types
///
public class CommandLineArgumentDescriptionAttribute
{
///
/// The type of the argument, using the property type as default
///
public CommandLineArgument.ArgumentType? Type { get; init; } = null;
///
/// The name of the argument, using the property name as default
///
public string? Name { get; init; } = null;
///
/// A short description of the argument, using the property name as default
///
public string? ShortDescription { get; init; } = null;
///
/// A long description of the argument, using the property name as default
///
public string? LongDescription { get; init; } = null;
///
/// The default value for the argument, using the property value as default
///
public string? DefaultValue { get; init; } = null;
///
/// The list of valid values for the argument
///
public string[]? ValueList { get; init; } = null;
///
/// The converter to use for the argument
///
public Func? Converter { get; init; } = null;
}
///
/// Implementation of commandline argument mapping
///
public static class CommandLineArgumentMapper
{
///
/// Helper method to detect slow-access properties
///
/// The property to read
/// The parent object to read from
/// A list of properties to exclude
/// The value of the property
private static object? GetValueGuarded(PropertyInfo p, object parent, HashSet? excludeDefaultValue)
{
object? value = null;
if (excludeDefaultValue != null && excludeDefaultValue.Contains(p.Name))
return value;
#if DEBUG
var start = DateTime.Now;
value = p.GetValue(parent);
var duration = DateTime.Now - start;
if (duration.TotalMilliseconds > 100)
Console.WriteLine($"Warning: {parent.GetType().FullName}.{p.Name} took {duration.TotalMilliseconds}ms to get value");
#else
value = p.GetValue(parent);
#endif
return value;
}
///
/// Extracts all primitive types from a class and maps them to command line arguments
///
/// The type to extract arguments from
/// The prefix to use for the arguments
/// A list of properties to exclude
/// A list of command line arguments
public static IEnumerable MapArguments(object obj, string prefix = "", HashSet? exclude = null, HashSet? excludeDefaultValue = null)
{
exclude ??= new HashSet();
foreach (var p in obj.GetType().GetProperties().Where(p => p.CanWrite && p.CanRead))
{
if (exclude.Contains(p.Name))
continue;
var customAttr = (obj as ICommandLineArgumentMapper)?.GetCommandLineArgumentDescription(p);
var name = (prefix + (customAttr?.Name ?? p.Name)).ToLowerInvariant();
var shortDescription = customAttr?.ShortDescription ?? p.Name;
var longDescription = customAttr?.LongDescription ?? p.Name;
var defaultValue = customAttr?.DefaultValue ?? GetValueGuarded(p, obj, excludeDefaultValue)?.ToString();
var argumentType = customAttr?.Type;
// Fully custom argument
if (customAttr != null && customAttr.Converter != null && argumentType != null)
{
yield return new CommandLineArgument(name, argumentType.Value, shortDescription, longDescription, defaultValue, null, customAttr.ValueList);
continue;
}
var propType = p.PropertyType;
if (Nullable.GetUnderlyingType(propType) != null)
propType = Nullable.GetUnderlyingType(propType);
if (propType == null)
continue;
if (propType == typeof(string))
yield return new CommandLineArgument(name, argumentType ?? CommandLineArgument.ArgumentType.String, shortDescription, longDescription, defaultValue);
else if (propType == typeof(int))
yield return new CommandLineArgument(name, argumentType ?? CommandLineArgument.ArgumentType.Integer, shortDescription, longDescription, defaultValue);
else if (propType == typeof(bool))
yield return new CommandLineArgument(name, argumentType ?? CommandLineArgument.ArgumentType.Boolean, shortDescription, longDescription, defaultValue);
else if (propType.IsEnum)
yield return new CommandLineArgument(name, argumentType ?? CommandLineArgument.ArgumentType.String, shortDescription, longDescription, defaultValue, null, customAttr?.ValueList ?? Enum.GetNames(propType));
else if (propType == typeof(Uri))
yield return new CommandLineArgument(name, argumentType ?? CommandLineArgument.ArgumentType.String, shortDescription, longDescription, defaultValue);
else if (propType == typeof(TimeSpan))
yield return new CommandLineArgument(name, argumentType ?? CommandLineArgument.ArgumentType.Timespan, shortDescription, longDescription, defaultValue);
else if (propType == typeof(DateTime))
yield return new CommandLineArgument(name, argumentType ?? CommandLineArgument.ArgumentType.DateTime, shortDescription, longDescription, defaultValue);
}
}
///
/// Applies the arguments to the object
///
/// The type of object to apply the arguments to
/// The object to apply the arguments to
/// The arguments to apply
/// The prefix to use for the arguments
/// The object with the arguments applied
public static T ApplyArguments(T obj, IReadOnlyDictionary args, string prefix = "")
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
foreach (var p in obj.GetType().GetProperties().Where(p => p.CanWrite && p.CanRead))
{
var customAttr = (obj as ICommandLineArgumentMapper)?.GetCommandLineArgumentDescription(p);
var name = (prefix + (customAttr?.Name ?? p.Name)).ToLowerInvariant();
var propType = p.PropertyType;
if (Nullable.GetUnderlyingType(propType) != null)
propType = Nullable.GetUnderlyingType(propType);
if (propType == null)
continue;
// Ignore case-sensitive properties of input dictionary
var value = args.Keys.FirstOrDefault(x => string.Equals(x, name, StringComparison.OrdinalIgnoreCase)) is string key ? args[key] : null;
if (value != null)
{
if (customAttr?.Converter != null)
p.SetValue(obj, customAttr.Converter(value));
else if (propType == typeof(string))
p.SetValue(obj, value);
else if (propType == typeof(int))
p.SetValue(obj, int.Parse(value));
else if (propType == typeof(bool))
p.SetValue(obj, bool.Parse(value));
else if (propType.IsEnum)
p.SetValue(obj, Enum.Parse(propType, value, true));
else if (propType == typeof(Uri))
p.SetValue(obj, new Uri(value));
else if (propType == typeof(TimeSpan))
p.SetValue(obj, Timeparser.ParseTimeSpan(value));
}
}
return obj;
}
///
/// Applies the arguments to the object
///
/// The type of object to apply the arguments to
/// The object to apply the arguments to
/// The arguments to apply
/// The prefix to use for the arguments
/// The object with the arguments applied
public static T ApplyArguments(T obj, NameValueCollection nameValueCollection, string prefix = "")
=> ApplyArguments(obj, nameValueCollection.AllKeys.Where(k => !string.IsNullOrWhiteSpace(k)).ToDictionary(k => k!, k => nameValueCollection[k]), prefix);
}