diff --git a/src/functions/private/ConvertFrom-LuaTable.ps1 b/src/functions/private/ConvertFrom-LuaTable.ps1 index b3b3b63..ad34c66 100644 --- a/src/functions/private/ConvertFrom-LuaTable.ps1 +++ b/src/functions/private/ConvertFrom-LuaTable.ps1 @@ -22,7 +22,11 @@ # Maximum allowed nesting depth. [Parameter()] - [int] $MaxDepth = 1024 + [int] $MaxDepth = 1024, + + # Skip strict Lua grammar validation. Warnings are emitted instead of terminating errors. + [Parameter()] + [switch] $SkipValidation ) begin {} @@ -33,6 +37,7 @@ $script:luaAsPSCustomObject = $AsPSCustomObject.IsPresent $script:luaMaxDepth = $MaxDepth $script:luaCurrentDepth = 0 + $script:luaSkipValidation = $SkipValidation.IsPresent Skip-LuaWhitespace @@ -81,6 +86,14 @@ } if ($assignmentDetected) { + # Lua 5.4 reserved words per §3.1 + $reservedWords = @( + 'and', 'break', 'do', 'else', 'elseif', 'end', + 'false', 'for', 'function', 'goto', 'if', 'in', + 'local', 'nil', 'not', 'or', 'repeat', 'return', + 'then', 'true', 'until', 'while' + ) + # Parse one or more assignment statements into an ordered dictionary $assignments = [ordered]@{} while ($script:luaPos -lt $script:luaString.Length) { @@ -100,6 +113,15 @@ } $varName = $script:luaString.Substring($identStart, $script:luaPos - $identStart) + # Lua grammar: variable names cannot be reserved words (§3.1) + if ($varName -cin $reservedWords) { + if ($script:luaSkipValidation) { + Write-Warning "Reserved word '$varName' used as a variable name at position $identStart." + } else { + throw "Reserved word '$varName' cannot be used as a variable name at position $identStart." + } + } + Skip-LuaWhitespace # Expect '=' diff --git a/src/functions/private/ConvertTo-LuaTable.ps1 b/src/functions/private/ConvertTo-LuaTable.ps1 index 984d7af..39660c5 100644 --- a/src/functions/private/ConvertTo-LuaTable.ps1 +++ b/src/functions/private/ConvertTo-LuaTable.ps1 @@ -58,7 +58,7 @@ # Enum handling if ($InputObject -is [enum]) { if ($EnumsAsStrings) { - $escaped = $InputObject.ToString() -replace '\\', '\\\\' -replace '"', '\"' + $escaped = $InputObject.ToString() -replace '\\', '\\' -replace '"', '\"' return "`"$escaped`"" } $underlyingType = [System.Enum]::GetUnderlyingType($InputObject.GetType()) diff --git a/src/functions/private/Read-LuaTable.ps1 b/src/functions/private/Read-LuaTable.ps1 index 0ceadc1..f94b6e4 100644 --- a/src/functions/private/Read-LuaTable.ps1 +++ b/src/functions/private/Read-LuaTable.ps1 @@ -13,6 +13,14 @@ begin {} process { + # Lua 5.4 reserved words per §3.1 + $reservedWords = @( + 'and', 'break', 'do', 'else', 'elseif', 'end', + 'false', 'for', 'function', 'goto', 'if', 'in', + 'local', 'nil', 'not', 'or', 'repeat', 'return', + 'then', 'true', 'until', 'while' + ) + $script:luaCurrentDepth++ if ($script:luaCurrentDepth -gt $script:luaMaxDepth) { throw "Maximum nesting depth ($($script:luaMaxDepth)) exceeded." @@ -78,6 +86,14 @@ if ($script:luaPos -lt $script:luaString.Length -and $script:luaString[$script:luaPos] -eq '=') { + # Lua grammar: Name cannot be a reserved word (§3.1) + if ($ident -cin $reservedWords) { + if ($script:luaSkipValidation) { + Write-Warning "Reserved word '$ident' used as a bare identifier key at position $identStart." + } else { + throw "Reserved word '$ident' cannot be used as a bare identifier key. Use bracket notation: [`"$ident`"] = value." + } + } # Key = value pair $script:luaPos++ # skip = Skip-LuaWhitespace diff --git a/src/functions/public/Lua/ConvertFrom-Lua.ps1 b/src/functions/public/Lua/ConvertFrom-Lua.ps1 index dbabba9..1702322 100644 --- a/src/functions/public/Lua/ConvertFrom-Lua.ps1 +++ b/src/functions/public/Lua/ConvertFrom-Lua.ps1 @@ -77,13 +77,23 @@ # Output arrays as a single object instead of enumerating elements through the pipeline. [Parameter()] - [switch] $NoEnumerate + [switch] $NoEnumerate, + + # Skip strict Lua grammar validation (e.g., reserved words as bare keys). Warnings are emitted instead of errors. + [Parameter()] + [switch] $SkipValidation ) begin {} process { - $result = ConvertFrom-LuaTable -InputString $InputObject -AsPSCustomObject:(-not $AsHashtable) -MaxDepth $Depth + $convertParams = @{ + InputString = $InputObject + AsPSCustomObject = -not $AsHashtable + MaxDepth = $Depth + SkipValidation = $SkipValidation.IsPresent + } + $result = ConvertFrom-LuaTable @convertParams if ($NoEnumerate -and $result -is [System.Array]) { Write-Output -InputObject $result -NoEnumerate } else { diff --git a/tests/Lua.Tests.ps1 b/tests/Lua.Tests.ps1 index 2ca23d7..063feb4 100644 --- a/tests/Lua.Tests.ps1 +++ b/tests/Lua.Tests.ps1 @@ -507,6 +507,68 @@ B = { val = 2 } It 'Throws on assignment with missing value' { { ConvertFrom-Lua -InputObject 'A = ' } | Should -Throw '*Unexpected end of input*' } + + It 'Throws on reserved word as bare table key' { + { ConvertFrom-Lua -InputObject '{ end = 1 }' } | Should -Throw '*Reserved word*' + } + + It 'Throws on reserved word as bare table key (while)' { + { ConvertFrom-Lua -InputObject '{ while = "loop" }' } | Should -Throw '*Reserved word*' + } + + It 'Allows reserved words in bracket notation' { + $result = ConvertFrom-Lua -InputObject '{ ["end"] = 1, ["while"] = 2 }' -AsHashtable + $result['end'] | Should -Be 1 + $result['while'] | Should -Be 2 + } + + It 'Allows capitalized reserved words as bare table keys (Lua keywords are case-sensitive)' { + $result = ConvertFrom-Lua -InputObject '{ End = 1, While = "loop" }' -AsHashtable + $result['End'] | Should -Be 1 + $result['While'] | Should -Be 'loop' + } + + It 'Allows capitalized reserved words as assignment variable names (Lua keywords are case-sensitive)' { + $result = ConvertFrom-Lua -InputObject 'End = 1' -AsHashtable + $result['End'] | Should -Be 1 + } + + It 'Throws on reserved word as assignment variable name' { + { ConvertFrom-Lua -InputObject 'end = 1' } | Should -Throw '*Reserved word*' + } + + It 'Throws on reserved word as assignment variable name (while as assignment)' { + { ConvertFrom-Lua -InputObject 'while = 42' } | Should -Throw '*Reserved word*' + } + + It 'SkipValidation allows reserved word as bare table key with warning' { + $result = ConvertFrom-Lua -InputObject '{ end = 1 }' -AsHashtable -SkipValidation 3>&1 + $warnings = $result | Where-Object { $_ -is [System.Management.Automation.WarningRecord] } + $output = $result | Where-Object { $_ -isnot [System.Management.Automation.WarningRecord] } + $warnings.Message | Should -BeLike '*Reserved word*end*' + $output['end'] | Should -Be 1 + } + + It 'SkipValidation allows reserved word as assignment variable name with warning' { + $result = ConvertFrom-Lua -InputObject 'end = 1' -AsHashtable -SkipValidation 3>&1 + $warnings = $result | Where-Object { $_ -is [System.Management.Automation.WarningRecord] } + $output = $result | Where-Object { $_ -isnot [System.Management.Automation.WarningRecord] } + $warnings.Message | Should -BeLike '*Reserved word*end*' + $output['end'] | Should -Be 1 + } + + It 'SkipValidation emits a warning for each reserved word occurrence' { + $result = ConvertFrom-Lua -InputObject '{ end = 1, while = 2, for = 3 }' -AsHashtable -SkipValidation 3>&1 + $warnings = @($result | Where-Object { $_ -is [System.Management.Automation.WarningRecord] }) + $output = $result | Where-Object { $_ -isnot [System.Management.Automation.WarningRecord] } + $warnings.Count | Should -Be 3 + $warnings[0].Message | Should -BeLike '*end*' + $warnings[1].Message | Should -BeLike '*while*' + $warnings[2].Message | Should -BeLike '*for*' + $output['end'] | Should -Be 1 + $output['while'] | Should -Be 2 + $output['for'] | Should -Be 3 + } } Context 'Pipeline input' {