// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Management.Automation;
using System.Management.Automation.Language;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
namespace Microsoft.PowerShell.Commands
{
///
/// JsonObject class.
///
[SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Justification = "Preferring Json over JSON")]
public static class JsonObject
{
#region HelperTypes
///
/// Context for convert-to-json operation.
///
public readonly struct ConvertToJsonContext
{
///
/// Gets the maximum depth for walking the object graph.
///
public readonly int MaxDepth;
///
/// Gets the cancellation token.
///
public readonly CancellationToken CancellationToken;
///
/// Gets the StringEscapeHandling setting.
///
public readonly StringEscapeHandling StringEscapeHandling;
///
/// Gets the EnumsAsStrings setting.
///
public readonly bool EnumsAsStrings;
///
/// Gets the CompressOutput setting.
///
public readonly bool CompressOutput;
///
/// Gets the target cmdlet that is doing the convert-to-json operation.
///
public readonly PSCmdlet Cmdlet;
///
/// Initializes a new instance of the struct.
///
/// The maximum depth to visit the object.
/// Indicates whether to use enum names for the JSON conversion.
/// Indicates whether to get the compressed output.
public ConvertToJsonContext(int maxDepth, bool enumsAsStrings, bool compressOutput)
: this(maxDepth, enumsAsStrings, compressOutput, StringEscapeHandling.Default, targetCmdlet: null, CancellationToken.None)
{
}
///
/// Initializes a new instance of the struct.
///
/// The maximum depth to visit the object.
/// Indicates whether to use enum names for the JSON conversion.
/// Indicates whether to get the compressed output.
/// Specifies how strings are escaped when writing JSON text.
/// Specifies the cmdlet that is calling this method.
/// Specifies the cancellation token for cancelling the operation.
public ConvertToJsonContext(
int maxDepth,
bool enumsAsStrings,
bool compressOutput,
StringEscapeHandling stringEscapeHandling,
PSCmdlet targetCmdlet,
CancellationToken cancellationToken)
{
this.MaxDepth = maxDepth;
this.CancellationToken = cancellationToken;
this.StringEscapeHandling = stringEscapeHandling;
this.EnumsAsStrings = enumsAsStrings;
this.CompressOutput = compressOutput;
this.Cmdlet = targetCmdlet;
}
}
private sealed class DuplicateMemberHashSet : HashSet
{
public DuplicateMemberHashSet(int capacity)
: base(capacity, StringComparer.OrdinalIgnoreCase)
{
}
}
#endregion HelperTypes
#region ConvertFromJson
///
/// Convert a Json string back to an object of type PSObject.
///
/// The json text to convert.
/// An error record if the conversion failed.
/// A PSObject.
[SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Justification = "Preferring Json over JSON")]
public static object ConvertFromJson(string input, out ErrorRecord error)
{
return ConvertFromJson(input, returnHashtable: false, out error);
}
///
/// Convert a Json string back to an object of type or
/// depending on parameter .
///
/// The json text to convert.
/// True if the result should be returned as a
/// instead of a
/// An error record if the conversion failed.
/// A or a
/// if the parameter is true.
[SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Justification = "Preferring Json over JSON")]
public static object ConvertFromJson(string input, bool returnHashtable, out ErrorRecord error)
{
return ConvertFromJson(input, returnHashtable, maxDepth: 1024, out error);
}
///
/// Convert a JSON string back to an object of type or
/// depending on parameter .
///
/// The JSON text to convert.
/// True if the result should be returned as a
/// instead of a .
/// The max depth allowed when deserializing the json input. Set to null for no maximum.
/// An error record if the conversion failed.
/// A or a
/// if the parameter is true.
[SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", Justification = "Preferring Json over JSON")]
public static object ConvertFromJson(string input, bool returnHashtable, int? maxDepth, out ErrorRecord error)
{
if (input == null)
{
throw new ArgumentNullException(nameof(input));
}
error = null;
try
{
// JsonConvert.DeserializeObject does not throw an exception when an invalid Json array is passed.
// This issue is being tracked by https://github.com/JamesNK/Newtonsoft.Json/issues/1930.
// To work around this, we need to identify when input is a Json array, and then try to parse it via JArray.Parse().
// If input starts with '[' (ignoring white spaces).
if (Regex.Match(input, @"^\s*\[").Success)
{
// JArray.Parse() will throw a JsonException if the array is invalid.
// This will be caught by the catch block below, and then throw an
// ArgumentException - this is done to have same behavior as the JavaScriptSerializer.
JArray.Parse(input);
// Please note that if the Json array is valid, we don't do anything,
// we just continue the deserialization.
}
var obj = JsonConvert.DeserializeObject(
input,
new JsonSerializerSettings
{
// This TypeNameHandling setting is required to be secure.
TypeNameHandling = TypeNameHandling.None,
MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
MaxDepth = maxDepth
});
switch (obj)
{
case JObject dictionary:
// JObject is a IDictionary
return returnHashtable
? PopulateHashTableFromJDictionary(dictionary, out error)
: PopulateFromJDictionary(dictionary, new DuplicateMemberHashSet(dictionary.Count), out error);
case JArray list:
return returnHashtable
? PopulateHashTableFromJArray(list, out error)
: PopulateFromJArray(list, out error);
default:
return obj;
}
}
catch (JsonException je)
{
var msg = string.Format(CultureInfo.CurrentCulture, WebCmdletStrings.JsonDeserializationFailed, je.Message);
// the same as JavaScriptSerializer does
throw new ArgumentException(msg, je);
}
}
// This function is a clone of PopulateFromDictionary using JObject as an input.
private static PSObject PopulateFromJDictionary(JObject entries, DuplicateMemberHashSet memberHashTracker, out ErrorRecord error)
{
error = null;
var result = new PSObject(entries.Count);
foreach (var entry in entries)
{
if (string.IsNullOrEmpty(entry.Key))
{
var errorMsg = string.Format(CultureInfo.CurrentCulture, WebCmdletStrings.EmptyKeyInJsonString);
error = new ErrorRecord(
new InvalidOperationException(errorMsg),
"EmptyKeyInJsonString",
ErrorCategory.InvalidOperation,
null);
return null;
}
// Case sensitive duplicates should normally not occur since JsonConvert.DeserializeObject
// does not throw when encountering duplicates and just uses the last entry.
if (memberHashTracker.TryGetValue(entry.Key, out var maybePropertyName)
&& string.Equals(entry.Key, maybePropertyName, StringComparison.Ordinal))
{
var errorMsg = string.Format(CultureInfo.CurrentCulture, WebCmdletStrings.DuplicateKeysInJsonString, entry.Key);
error = new ErrorRecord(
new InvalidOperationException(errorMsg),
"DuplicateKeysInJsonString",
ErrorCategory.InvalidOperation,
null);
return null;
}
// Compare case insensitive to tell the user to use the -AsHashTable option instead.
// This is because PSObject cannot have keys with different casing.
if (memberHashTracker.TryGetValue(entry.Key, out var propertyName))
{
var errorMsg = string.Format(CultureInfo.CurrentCulture, WebCmdletStrings.KeysWithDifferentCasingInJsonString, propertyName, entry.Key);
error = new ErrorRecord(
new InvalidOperationException(errorMsg),
"KeysWithDifferentCasingInJsonString",
ErrorCategory.InvalidOperation,
null);
return null;
}
// Array
switch (entry.Value)
{
case JArray list:
{
var listResult = PopulateFromJArray(list, out error);
if (error != null)
{
return null;
}
result.Properties.Add(new PSNoteProperty(entry.Key, listResult));
break;
}
case JObject dic:
{
// Dictionary
var dicResult = PopulateFromJDictionary(dic, new DuplicateMemberHashSet(dic.Count), out error);
if (error != null)
{
return null;
}
result.Properties.Add(new PSNoteProperty(entry.Key, dicResult));
break;
}
case JValue value:
{
result.Properties.Add(new PSNoteProperty(entry.Key, value.Value));
break;
}
}
memberHashTracker.Add(entry.Key);
}
return result;
}
// This function is a clone of PopulateFromList using JArray as input.
private static ICollection