diff --git a/Tools/RemoteSynchronization/Program.cs b/Tools/RemoteSynchronization/Program.cs index cbc988f1f..5ff4bae32 100644 --- a/Tools/RemoteSynchronization/Program.cs +++ b/Tools/RemoteSynchronization/Program.cs @@ -143,9 +143,7 @@ destination will be verified before being overwritten (if they seemingly match). private static async Task Run(Config config) { // Unpack and parse the multi token options - Dictionary global_options = config.GlobalOptions - .Select(x => x.Split("=")) - .ToDictionary(x => x[0], x => x[1]); + Dictionary global_options = ParseOptions(config.GlobalOptions); // Parse the log level var log_level_parsed = Enum.TryParse(config.LogLevel, true, out var log_level_enum); @@ -169,13 +167,8 @@ destination will be verified before being overwritten (if they seemingly match). // Start the logging scope using var _ = Duplicati.Library.Logging.Log.StartScope(multi_sink, log_level_enum); - Dictionary src_opts = config.SrcOptions - .Select(x => x.Split("=")) - .ToDictionary(x => x[0], x => string.Join("=", x.Skip(1))); - - Dictionary dst_opts = config.DstOptions - .Select(x => x.Split("=")) - .ToDictionary(x => x[0], x => string.Join("=", x.Skip(1))); + Dictionary src_opts = ParseOptions(config.SrcOptions); + Dictionary dst_opts = ParseOptions(config.DstOptions); // Merge the global options into the source and destination options. The global options will be overridden by the source and destination options. foreach (var x in global_options) @@ -529,6 +522,34 @@ destination will be verified before being overwritten (if they seemingly match). }; } + /// + /// Parses the options from a list of strings. + /// Each option should be in the format "key=value". If the value contains spaces, + /// it should be enclosed in quotes, e.g. "key=\"value with spaces\"". + /// + /// The list of string options to parse + /// A dictionary with the parsed options, where the key is the option name and the value is the option value. + /// If an option was not parsed correctly. + private static Dictionary ParseOptions(IEnumerable options) + { + var result = options + .Select(x => x.Split('=')) + .ToDictionary(x => x[0], x => string.Join("=", x.Skip(1))); + + // Double check that the options are valid by reconstructing them from the dictionary + foreach (var opt in result.Select(x => $"{x.Key}={x.Value}")) + { + if (!options.Contains(opt)) + { + Duplicati.Library.Logging.Log.WriteErrorMessage(LOGTAG, "rsync", null, + "The source option '{0}' is not valid. Please check the syntax.", opt); + throw new ArgumentException($"The source option '{opt}' has not been parsed correctly."); + } + } + + return result; + } + /// /// Prepares the lists of files to copy, delete and verify. /// The files to copy are the files that are not in the destination, have a different size or have a more recent modification date.