Skip to content

Commit

Permalink
Handle large integers and PSCustomObjects
Browse files Browse the repository at this point in the history
This change fixes the way powershell-yaml deals with two particular
types: really large numbers and PSCustomObjects.

When an integer larger than what an int64 could handle was deserialized,
powershell-yaml would convert it to scientific notation. This is default
behavior in powershell when you try to cast a number larger than
[int64]::MaxValue to an [int64]. This change attempts to cast all numbers
to [BigInteger] and then try to fit that value into the smallest possible
type. If that is not possible, we leave it as [BigInteger].

This change also adds a custom type converter that properly serializes a
[BigInteger] back to yaml.

Another type we've had issues with is the PSCustomObject type. This change
adds a custom type converter for that as well and removes the hack we had
to do to cast the PSCustomObject to a dictionary before we serialize it.

A final change made here was in the way the assembly we ship is built.
The code was separated into a proper project and is built using the dotnet
sdk.

Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
  • Loading branch information
gabriel-samfira committed Aug 26, 2024
1 parent 55fed1c commit 7d1df5f
Show file tree
Hide file tree
Showing 26 changed files with 7,383 additions and 10,819 deletions.
11 changes: 9 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jobs:
name: Pester tests on powershell.exe
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [windows-2019, windows-2022]

Expand All @@ -27,7 +28,7 @@ jobs:
run: |
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module Assert -ErrorAction Stop -MaximumVersion 0.9.6 -Force
Install-Module Pester -ErrorAction Stop -Force
Install-Module Pester -ErrorAction Stop -MaximumVersion 5.6.1 -Force
- name: Run tests
shell: powershell
run: |
Expand All @@ -37,6 +38,7 @@ jobs:
name: Pester tests
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04, ubuntu-20.04, macos-12, windows-2019, windows-2022]

Expand All @@ -47,8 +49,13 @@ jobs:
run: |
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module Assert -ErrorAction Stop -MaximumVersion 0.9.6 -Force
Install-Module Pester -ErrorAction Stop -Force
Install-Module Pester -ErrorAction Stop -MaximumVersion 5.6.1 -Force
- name: Run tests
shell: pwsh
run: |
Remove-Module Pester -ErrorAction SilentlyContinue
Import-Module pester -Version 5.6.1
$PSVersionTable
Invoke-Pester
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
src/obj
src/bin
78 changes: 78 additions & 0 deletions Tests/powershell-yaml.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,84 @@ bools:
}
}

Describe 'Numbers are parsed as the smallest type possible' {
BeforeAll {
$global:value = @'
bigInt: 99999999999999999999999999999999999
int32: 2147483647
int64: 9223372036854775807
'@
}

It 'Should be a BigInt' {
$result = ConvertFrom-Yaml -Yaml $value
$result.bigInt | Should -BeOfType System.Numerics.BigInteger
}

It 'Should be of proper type and value' {
$result = ConvertFrom-Yaml -Yaml $value
$result.bigInt | Should -Be ([System.Numerics.BigInteger]::Parse("99999999999999999999999999999999999"))
$result.int32 | Should -Be ([int32]2147483647)
$result.int64 | Should -Be ([int64]9223372036854775807)
}
}

Describe 'PSCustomObjects' {
Context 'Classes with PSCustomObjects' {
It 'Should serialise as a hash' {
$nestedPsO = [PSCustomObject]@{
Nested = 'NestedValue'
}
$PsO = [PSCustomObject]@{
Name = 'Value'
Nested = $nestedPsO
}

class TestClass {
[PSCustomObject]$PsO
[string]$Ok
}
$Class = [TestClass]@{
PsO = $PsO
Ok = 'aye'
}
$asYaml = ConvertTo-Yaml $Class
$result = ConvertFrom-Yaml -Yaml $asYaml -Ordered
[System.Collections.Specialized.OrderedDictionary]$ret = [System.Collections.Specialized.OrderedDictionary]::new()
$ret["PsO"] = [System.Collections.Specialized.OrderedDictionary]::new()
$ret["PsO"]["Name"] = "Value"
$ret["PsO"]["Nested"] = [System.Collections.Specialized.OrderedDictionary]::new()
$ret["PsO"]["Nested"]["Nested"] = "NestedValue"
$ret["Ok"] = "aye"
Assert-Equivalent -Options $compareStrictly -Expected $ret -Actual $result
}
}

Context 'PSCustomObject with a single property' {
BeforeAll {
$global:value = [PSCustomObject]@{key="value"}
}
It 'Should serialise as a hash' {
$result = ConvertTo-Yaml $value
$result | Should -Be "key: value$([Environment]::NewLine)"
}
}
Context 'PSCustomObject with multiple properties' {
BeforeAll {
$global:value = [PSCustomObject]@{key1="value1"; key2="value2"}
}
It 'Should serialise as a hash' {
$result = ConvertTo-Yaml $value
$result | Should -Be "key1: value1$([Environment]::NewLine)key2: value2$([Environment]::NewLine)"
}
It 'Should deserialise as a hash' {
$asYaml = ConvertTo-Yaml $value
$result = ConvertFrom-Yaml -Yaml $asYaml -Ordered
Assert-Equivalent -Options $compareStrictly -Expected @{key1="value1"; key2="value2"} -Actual ([hashtable]$result)
}
}
}

Describe 'StringQuotingEmitter' {
BeforeAll {
$oldYamlPkgUrl = 'https://www.nuget.org/api/v2/package/YamlDotNet/11.2.1'
Expand Down
117 changes: 6 additions & 111 deletions build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -14,119 +14,14 @@
#

$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$source = @"
using System;
using System.Text.RegularExpressions;
using YamlDotNet;
using YamlDotNet.Core;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.EventEmitters;
public class StringQuotingEmitter: ChainedEventEmitter {
// Patterns from https://yaml.org/spec/1.2/spec.html#id2804356
private static Regex quotedRegex = new Regex(@`"^(\~|null|true|false|on|off|yes|no|y|n|[-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?|[-+]?(\.inf))?$`", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public StringQuotingEmitter(IEventEmitter next): base(next) {}

public override void Emit(ScalarEventInfo eventInfo, IEmitter emitter) {
var typeCode = eventInfo.Source.Value != null
? Type.GetTypeCode(eventInfo.Source.Type)
: TypeCode.Empty;
dotnet build --configuration Release $here/src/

switch (typeCode) {
case TypeCode.Char:
if (Char.IsDigit((char)eventInfo.Source.Value)) {
eventInfo.Style = ScalarStyle.DoubleQuoted;
}
break;
case TypeCode.String:
var val = eventInfo.Source.Value.ToString();
if (quotedRegex.IsMatch(val))
{
eventInfo.Style = ScalarStyle.DoubleQuoted;
} else if (val.IndexOf('\n') > -1) {
eventInfo.Style = ScalarStyle.Literal;
}
break;
}
$destinations = @("netstandard2.1", "net47")

base.Emit(eventInfo, emitter);
}
foreach ($item in $destinations) {
$src = Join-Path $here "src" "bin" "Release" $item "serializer.dll"
$dst = Join-Path $here "lib" $item "StringQuotingEmitter.dll"

public static SerializerBuilder Add(SerializerBuilder builder) {
return builder.WithEventEmitter(next => new StringQuotingEmitter(next));
}
Copy-Item -Force $src $dst
}
"@


function Invoke-LoadInContext {
param(
[string]$assemblyPath,
[string]$loadContextName
)

$loadContext = [System.Runtime.Loader.AssemblyLoadContext]::New($loadContextName, $true)
$assemblies = $loadContext.LoadFromAssemblyPath($assemblyPath)

return @{ "yaml"= $assemblies }
}

function Invoke-LoadInGlobalContext {
param(
[string]$assemblyPath
)
$assemblies = [Reflection.Assembly]::LoadFrom($assemblyPath)
return @{ "yaml"= $assemblies }
}


function Invoke-LoadAssembly {
$libDir = Join-Path $here "lib"
$assemblies = @{
"core" = Join-Path $libDir "netstandard2.1\YamlDotNet.dll";
"net45" = Join-Path $libDir "net45\YamlDotNet.dll";
"net35" = Join-Path $libDir "net35\YamlDotNet.dll";
}

if ($PSVersionTable.Keys -contains "PSEdition") {
if ($PSVersionTable.PSEdition -eq "Core") {
return (Invoke-LoadInContext -assemblyPath $assemblies["core"] -loadContextName "powershellyaml")
} elseif ($PSVersionTable.PSVersion.Major -gt 5.1) {
return (Invoke-LoadInContext -assemblyPath $assemblies["net45"] -loadContextName "powershellyaml")
} elseif ($PSVersionTable.PSVersion.Major -ge 4) {
return (Invoke-LoadInGlobalContext $assemblies["net45"])
} else {
return (Invoke-LoadInGlobalContext $assemblies["net35"])
}
} else { # Powershell 4.0 and lower do not know "PSEdition" yet
return (Invoke-LoadInGlobalContext $assemblies["net35"])
}
}

$assemblies = Invoke-LoadAssembly
$yamlDotNetAssembly = $assemblies["yaml"]


if (!([System.Management.Automation.PSTypeName]'StringQuotingEmitter').Type) {
$referenceList = @($yamlDotNetAssembly.Location,[Text.RegularExpressions.Regex].Assembly.Location)
if ($PSVersionTable.PSEdition -eq "Core") {
$referenceList += [IO.Directory]::GetFiles([IO.Path]::Combine($PSHOME, 'ref'), 'netstandard.dll', [IO.SearchOption]::TopDirectoryOnly)
$destinations = @("lib/netstandard2.1")
} else {
$referenceList += 'System.Runtime.dll'
$destinations = @("lib/net45", "lib/net35")
}
}

$destinations = @("lib/netstandard2.1", "lib/net45", "lib/net35")

foreach ($target in $destinations) {
$targetPath = Join-Path $here $target
$file = Join-Path $targetPath "StringQuotingEmitter.dll"
if (!(Test-Path $file)) {
if ($PSVersionTable.PSEdition -eq "Core") {
Add-Type -TypeDefinition $source -ReferencedAssemblies $referenceList -Language CSharp -CompilerOptions "-nowarn:1701" -OutputAssembly $file
} else {
Add-Type -TypeDefinition $source -ReferencedAssemblies $referenceList -Language CSharp -OutputAssembly $file
}
}
}
19 changes: 0 additions & 19 deletions lib/net35/LICENSE-libyaml

This file was deleted.

Binary file removed lib/net35/StringQuotingEmitter.dll
Binary file not shown.
Binary file removed lib/net35/YamlDotNet.dll
Binary file not shown.
Loading

0 comments on commit 7d1df5f

Please sign in to comment.