winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

UserSettingsCommand.cs (10188B)


      1 // -----------------------------------------------------------------------------
      2 // <copyright file="UserSettingsCommand.cs" company="Microsoft Corporation">
      3 //     Copyright (c) Microsoft Corporation. Licensed under the MIT License.
      4 // </copyright>
      5 // -----------------------------------------------------------------------------
      6 
      7 namespace Microsoft.WinGet.Client.Engine.Commands
      8 {
      9     using System;
     10     using System.Collections;
     11     using System.IO;
     12     using System.Linq;
     13     using System.Management.Automation;
     14     using Microsoft.WinGet.Client.Engine.Commands.Common;
     15     using Microsoft.WinGet.Client.Engine.Common;
     16     using Microsoft.WinGet.Client.Engine.Exceptions;
     17     using Microsoft.WinGet.Client.Engine.Helpers;
     18     using Microsoft.WinGet.Common.Command;
     19     using Newtonsoft.Json;
     20     using Newtonsoft.Json.Linq;
     21 
     22     /// <summary>
     23     /// Class used by the user settings cmdlets.
     24     /// </summary>
     25     public sealed class UserSettingsCommand : BaseCommand
     26     {
     27         private const string SchemaKey = "$schema";
     28         private const string SchemaValue = "https://aka.ms/winget-settings.schema.json";
     29 
     30         private static string? winGetSettingsFilePath;
     31 
     32         /// <summary>
     33         /// Initializes a new instance of the <see cref="UserSettingsCommand"/> class.
     34         /// </summary>
     35         /// <param name="psCmdlet">PSCmdlet.</param>
     36         public UserSettingsCommand(PSCmdlet psCmdlet)
     37             : base(psCmdlet)
     38         {
     39             // Doing it in the static constructor will show the user running in system context:
     40             // The type initializer for 'Microsoft.WinGet.Client.Engine.Commands.UserSettingsCommand' threw an exception.
     41             // Here would be "The specified method is not supported."
     42             if (winGetSettingsFilePath == null)
     43             {
     44                 var wingetCliWrapper = new WingetCLIWrapper();
     45                 var settingsResult = wingetCliWrapper.RunCommand(this, "settings", "export");
     46 
     47                 // Read the user settings file property.
     48                 var userSettingsFile = Utilities.ConvertToHashtable(settingsResult.StdOut)["userSettingsFile"] ?? throw new ArgumentNullException("userSettingsFile");
     49                 winGetSettingsFilePath = (string)userSettingsFile;
     50             }
     51         }
     52 
     53         /// <summary>
     54         /// Get-WinGetUserSetting.
     55         /// </summary>
     56         public void Get()
     57         {
     58             this.Write(StreamType.Object, this.GetLocalSettingsAsHashtable());
     59         }
     60 
     61         /// <summary>
     62         /// Test-WinGetUserSetting.
     63         /// </summary>
     64         /// <param name="userSettings">Input user settings.</param>
     65         /// <param name="ignoreNotSet">Ignore comparing settings that are not part of the input.</param>
     66         public void Test(Hashtable userSettings, bool ignoreNotSet)
     67         {
     68             this.Write(StreamType.Object, this.CompareUserSettings(userSettings, ignoreNotSet));
     69         }
     70 
     71         /// <summary>
     72         /// Set-WinGetUserSetting.
     73         /// </summary>
     74         /// <param name="userSettings">Input user settings.</param>
     75         /// <param name="merge">Merge the current user settings and the input settings.</param>
     76         public void Set(Hashtable userSettings, bool merge)
     77         {
     78             var newSettings = HashtableToJObject(userSettings);
     79 
     80             // Merge settings.
     81             if (merge)
     82             {
     83                 var currentSettings = this.LocalSettingsFileToJObject();
     84 
     85                 // To make the input settings triumph, they need to be merged into the existing settings.
     86                 currentSettings.Merge(newSettings, new JsonMergeSettings
     87                 {
     88                     MergeArrayHandling = MergeArrayHandling.Union,
     89                     MergeNullValueHandling = MergeNullValueHandling.Ignore,
     90                 });
     91 
     92                 newSettings = currentSettings;
     93             }
     94 
     95             // Add schema if not there.
     96             if (!newSettings.ContainsKey(SchemaKey))
     97             {
     98                 newSettings.Add(SchemaKey, SchemaValue);
     99             }
    100 
    101             var orderedSettings = this.CreateAlphabeticallyOrderedJObject(newSettings);
    102 
    103             // Write settings.
    104             var settingsJson = orderedSettings.ToString(Formatting.Indented);
    105             File.WriteAllText(
    106                 winGetSettingsFilePath!,
    107                 settingsJson);
    108 
    109             this.Write(StreamType.Object, Utilities.ConvertToHashtable(settingsJson));
    110         }
    111 
    112         private static JObject HashtableToJObject(Hashtable hashtable)
    113         {
    114             return (JObject)JToken.FromObject(hashtable);
    115         }
    116 
    117         private Hashtable GetLocalSettingsAsHashtable()
    118         {
    119             var content = File.Exists(winGetSettingsFilePath) ?
    120                 File.ReadAllText(winGetSettingsFilePath) :
    121                 string.Empty;
    122 
    123             return Utilities.ConvertToHashtable(content);
    124         }
    125 
    126         private JObject LocalSettingsFileToJObject()
    127         {
    128             try
    129             {
    130                 return File.Exists(winGetSettingsFilePath) ?
    131                     JObject.Parse(File.ReadAllText(winGetSettingsFilePath)) :
    132                     new JObject();
    133             }
    134             catch (JsonReaderException e)
    135             {
    136                 this.Write(StreamType.Verbose, e.Message);
    137                 throw new UserSettingsReadException(e);
    138             }
    139         }
    140 
    141         private bool CompareUserSettings(Hashtable userSettings, bool ignoreNotSet)
    142         {
    143             try
    144             {
    145                 var currentSettings = this.LocalSettingsFileToJObject();
    146                 var newSettings = HashtableToJObject(userSettings);
    147 
    148                 // Don't fail because of the schema.
    149                 if (currentSettings.ContainsKey(SchemaKey))
    150                 {
    151                     currentSettings.Remove(SchemaKey);
    152                 }
    153 
    154                 if (newSettings.ContainsKey(SchemaKey))
    155                 {
    156                     newSettings.Remove(SchemaKey);
    157                 }
    158 
    159                 if (ignoreNotSet)
    160                 {
    161                     return this.PartialDeepEquals(newSettings, currentSettings);
    162                 }
    163 
    164                 return JToken.DeepEquals(newSettings, currentSettings);
    165             }
    166             catch (Exception e)
    167             {
    168                 this.Write(StreamType.Verbose, e.Message);
    169                 return false;
    170             }
    171         }
    172 
    173         /// <summary>
    174         /// Partially compares json. All properties and values of json must exist and have the same value
    175         /// as otherJson.
    176         /// This doesn't support deep JArray object comparison, but we don't have arrays of type object so far :).
    177         /// </summary>
    178         /// <param name="json">Main json.</param>
    179         /// <param name="otherJson">otherJson.</param>
    180         /// <returns>True is otherJson partially contains json.</returns>
    181         private bool PartialDeepEquals(JToken json, JToken? otherJson)
    182         {
    183             if (JToken.DeepEquals(json, otherJson))
    184             {
    185                 return true;
    186             }
    187 
    188             if (otherJson == null)
    189             {
    190                 return false;
    191             }
    192 
    193             // If they are a JValue (string, integer, date, etc) or they are a JArray and DeepEquals fails then not equal.
    194             if ((json is JValue && otherJson is JValue) ||
    195                 (json is JArray && otherJson is JArray))
    196             {
    197                 this.Write(
    198                     StreamType.Verbose,
    199                     $"'{json.ToString(Formatting.None)}' != '{otherJson.ToString(Formatting.None)}'");
    200                 return false;
    201             }
    202 
    203             // If its not the same type then don't bother.
    204             if (json.Type != otherJson.Type)
    205             {
    206                 this.Write(
    207                     StreamType.Verbose,
    208                     $"Mismatch types '{json.ToString(Formatting.None)}' '{otherJson.ToString(Formatting.None)}'");
    209                 return false;
    210             }
    211 
    212             // Look deeply.
    213             if (json.Type == JTokenType.Object)
    214             {
    215                 var jObject = (JObject)json;
    216                 var otherJObject = (JObject)otherJson;
    217 
    218                 var properties = jObject.Properties();
    219                 foreach (var property in properties)
    220                 {
    221                     // If the property is not there then give up.
    222                     if (!otherJObject.ContainsKey(property.Name))
    223                     {
    224                         this.Write(StreamType.Verbose, $"{property.Name} not found.");
    225                         return false;
    226                     }
    227 
    228                     if (!this.PartialDeepEquals(property.Value, otherJObject.GetValue(property.Name)))
    229                     {
    230                         // Found inequality within a property. We are done.
    231                         return false;
    232                     }
    233                 }
    234             }
    235 
    236             return true;
    237         }
    238 
    239         /// <summary>
    240         /// Helper method to order alphabetically properties. Newtonsoft doesn't have a nice way
    241         /// to do it via a custom JsonConverter.
    242         /// </summary>
    243         /// <param name="jObject">JObject.</param>
    244         /// <returns>New ordered JObject.</returns>
    245         private JObject CreateAlphabeticallyOrderedJObject(JObject jObject)
    246         {
    247             JObject newJObject = new ();
    248             var orderedProperties = jObject.Properties().OrderBy(p => p.Name, StringComparer.Ordinal);
    249             foreach (var property in orderedProperties)
    250             {
    251                 if (property.Value.Type == JTokenType.Object)
    252                 {
    253                     newJObject.Add(
    254                         property.Name,
    255                         this.CreateAlphabeticallyOrderedJObject((JObject)property.Value));
    256                 }
    257                 else
    258                 {
    259                     newJObject.Add(property);
    260                 }
    261             }
    262 
    263             return newJObject;
    264         }
    265     }
    266 }