// 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 Duplicati.Library.Interface; namespace Duplicati.Library.Utility; /// /// Helper class for validating command line arguments /// public static class CommandLineArgumentValidator { /// /// The log tag for this class /// private static readonly string LOGTAG = Logging.Log.LogTagFromType(typeof(CommandLineArgumentValidator)); /// /// Validates arguments against the supported commands /// /// The arguments supported< public static void ValidateArguments(IEnumerable arguments, IReadOnlyDictionary options, IReadOnlySet knownDuplicateOptions, IReadOnlySet IgnoredOptions) { var allOptionsGrouped = arguments .SelectMany(option => (option.Aliases ?? []).Prepend(option.Name).Select(name => (Name: name, Option: option))) .GroupBy(x => x.Name, StringComparer.OrdinalIgnoreCase); #if DEBUG foreach (var n in allOptionsGrouped.Where(x => !knownDuplicateOptions.Contains(x.Key) && x.Count() > 1)) Logging.Log.WriteErrorMessage(LOGTAG, "DuplicateOption", null, $"Duplicate option: {n.Key}"); #endif var allOptions = allOptionsGrouped .ToDictionary(x => x.Key, x => x.First().Option, StringComparer.OrdinalIgnoreCase); foreach (var (key, value) in options.Where(x => !IgnoredOptions.Contains(x.Key))) { if (!allOptions.TryGetValue(key, out var option) || option == null) { Logging.Log.WriteWarningMessage(LOGTAG, "UnknownOption", null, $"Unknown option supplied: {key}"); continue; } var validationMessage = ValidateOptionValue(option, key, value); if (!string.IsNullOrWhiteSpace(validationMessage)) Logging.Log.WriteWarningMessage(LOGTAG, "OptionValidationError", null, validationMessage); } } /// /// Checks if the value passed to an option is actually valid. /// /// The argument being validated /// The name of the option to validate /// The value to check /// Null if no errors are found, an error message otherwise public static string? ValidateOptionValue(ICommandLineArgument arg, string optionname, string? value) { if (arg.Type == CommandLineArgument.ArgumentType.Enumeration) { var found = false; foreach (var v in arg.ValidValues ?? []) if (string.Equals(v, value, StringComparison.OrdinalIgnoreCase)) { found = true; break; } if (!found) return Strings.CommandLineArgumentValidator.UnsupportedEnumerationValue(optionname, value, arg.ValidValues ?? new string[0]); } else if (arg.Type == CommandLineArgument.ArgumentType.Flags) { var validatedAllFlags = false; var flags = (value ?? string.Empty).ToLowerInvariant().Split(new[] { "," }, StringSplitOptions.None).Select(flag => flag.Trim()).Distinct(); var validFlags = arg.ValidValues ?? []; foreach (var flag in flags) { if (!validFlags.Any(validFlag => string.Equals(validFlag, flag, StringComparison.OrdinalIgnoreCase))) { validatedAllFlags = false; break; } validatedAllFlags = true; } if (!validatedAllFlags) return Strings.CommandLineArgumentValidator.UnsupportedFlagsValue(optionname, value, validFlags); } else if (arg.Type == CommandLineArgument.ArgumentType.Boolean) { if (!string.IsNullOrEmpty(value) && Utility.ParseBool(value, true) != Utility.ParseBool(value, false)) return Strings.CommandLineArgumentValidator.UnsupportedBooleanValue(optionname, value); } else if (arg.Type == CommandLineArgument.ArgumentType.Integer) { if (!long.TryParse(value, out _)) return Strings.CommandLineArgumentValidator.UnsupportedIntegerValue(optionname, value); } else if (arg.Type == CommandLineArgument.ArgumentType.Path) { if (string.IsNullOrWhiteSpace(value)) return Strings.CommandLineArgumentValidator.UnsupportedPathValue(optionname, value); foreach (var p in value.Split(Path.DirectorySeparatorChar)) if (p.IndexOfAny(Path.GetInvalidPathChars()) >= 0) return Strings.CommandLineArgumentValidator.UnsupportedPathValue(optionname, p); } else if (arg.Type == CommandLineArgument.ArgumentType.Size) { try { Sizeparser.ParseSize(value); } catch { return Strings.CommandLineArgumentValidator.UnsupportedSizeValue(optionname, value); } if (!string.IsNullOrWhiteSpace(value) && char.IsDigit(value.Last())) return Strings.CommandLineArgumentValidator.NonQualifiedSizeValue(optionname, value); } else if (arg.Type == CommandLineArgument.ArgumentType.Timespan) { try { Timeparser.ParseTimeSpan(value); } catch { return Strings.CommandLineArgumentValidator.UnsupportedTimeValue(optionname, value); } } else if (arg.Type == CommandLineArgument.ArgumentType.DateTime) { try { Timeparser.ParseTimeInterval(value, DateTime.Now); } catch { return Strings.CommandLineArgumentValidator.UnsupportedTimeValue(optionname, value); } } return null; } }