// 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); }