Public/Import-ModuleFromFlatTextAndManifest.ps1
|
<# .SYNOPSIS Recreate a module folder structure and files from a JSON manifest and a flat text file generated by Export-ModuleToFlatTextAndManifest. .DESCRIPTION Import-ModuleFromFlatTextAndManifest takes a manifest JSON and a flat text file, and reconstructs the original folder structure and files under a specified output directory. It supports manifests with module root and item details, and will create all directories and files as described in the manifest. File contents are restored exactly as exported, including the UTF-8 byte order mark when the manifest records one (Utf8Bom). For manifests from earlier versions, which do not record it, files containing non-ASCII characters are written with a BOM so Windows PowerShell 5.1 reads them correctly. Manifests created on Windows (with \ separators) can be imported on any platform. .PARAMETER ManifestPath Path to the JSON manifest file (e.g., modulename_manifest.json). .PARAMETER FlatTextPath Path to the flat text file (e.g., modulename_flat.txt). .PARAMETER OutputPath Directory where the module folder structure will be recreated. Created if it does not exist. .EXAMPLE Import-ModuleFromFlatTextAndManifest -ManifestPath 'C:\temp\tcs.intune.export\tcs.intune_manifest.json' -FlatTextPath 'C:\temp\tcs.intune.export\tcs.intune_flat.txt' -OutputPath 'C:\temp\tcs.intune.imported' Recreates the module folder structure under C:\temp\tcs.intune.imported\tcs.intune using the manifest and flat file produced by Export-ModuleToFlatTextAndManifest. .INPUTS None. All parameters must be supplied by name or position. .OUTPUTS None. Creates directories and files under OutputPath; use -Verbose to confirm the target path. .LINK Export-ModuleToFlatTextAndManifest .NOTES Author: TheCodeSaiyan Date: 2025-09-11 Compatible with PowerShell Constrained Language Mode (string parsing only; no [regex] or RegexOptions). In Constrained Language Mode on Windows PowerShell 5.1, files are always written with a UTF-8 BOM. #> function Import-ModuleFromFlatTextAndManifest { [CmdletBinding()] [OutputType([void])] param( [Parameter(Mandatory = $true)] [string]$ManifestPath, [Parameter(Mandatory = $true)] [string]$FlatTextPath, [Parameter(Mandatory = $true)] [string]$OutputPath ) $TelemetryArgs = @{ ModuleName = $MyInvocation.MyCommand.Module.Name ModuleVersion = [string]$MyInvocation.MyCommand.Module.Version CommandName = $MyInvocation.MyCommand.Name ExecutionID = [guid]::NewGuid().ToString() } Invoke-TelemetryCollection @TelemetryArgs -Stage Start -ClearTimer try { if (-not (Test-Path -Path $ManifestPath -PathType Leaf)) { throw "ManifestPath '$ManifestPath' does not exist." } if (-not (Test-Path -Path $FlatTextPath -PathType Leaf)) { throw "FlatTextPath '$FlatTextPath' does not exist." } if (-not (Test-Path -Path $OutputPath)) { $null = New-Item -ItemType Directory -Path $OutputPath -Force -ErrorAction Stop } $manifestObj = Get-Content -Path $ManifestPath -Raw -Encoding UTF8 -ErrorAction Stop | ConvertFrom-Json $flatText = Get-Content -Path $FlatTextPath -Raw -Encoding UTF8 -ErrorAction Stop if ($null -eq $flatText) { $flatText = '' } $moduleRoot = $manifestObj.ModuleRoot if ([string]::IsNullOrWhiteSpace($moduleRoot) -or $moduleRoot -match '[\\/]' -or $moduleRoot -eq '..') { throw "The manifest '$ManifestPath' has an invalid ModuleRoot '$moduleRoot'." } $items = @($manifestObj.Items) $moduleParentPath = Join-Path -Path $OutputPath -ChildPath $moduleRoot if (-not (Test-Path -Path $moduleParentPath)) { $null = New-Item -ItemType Directory -Path $moduleParentPath -Force -ErrorAction Stop } # Relative paths may use either separator; refuse any that would leave the module folder $toLocalPath = { param([string]$RelativePath) $segments = @($RelativePath -split '[\\/]' | Where-Object { $_ -ne '' -and $_ -ne '.' }) if ($segments -contains '..' -or $RelativePath -match '^([A-Za-z]:|[\\/])') { throw "Unsafe relative path '$RelativePath'." } $localPath = $moduleParentPath foreach ($segment in $segments) { $localPath = Join-Path -Path $localPath -ChildPath $segment } $localPath } # Create all directories first foreach ($item in $items | Where-Object { $_.ItemType -eq 'Directory' }) { try { $dirPath = & $toLocalPath $item.RelativePath if (-not (Test-Path -Path $dirPath)) { $null = New-Item -ItemType Directory -Path $dirPath -Force -ErrorAction Stop } } catch { Write-Warning "Failed to create directory: $($item.RelativePath). Error: $($_.Exception.Message)" } } # Then create all files (using string parsing only for Constrained Language Mode) $startTag = '###FILE-START:' $endTag = '###FILE-END:' foreach ($item in $items | Where-Object { $_.ItemType -eq 'File' }) { try { $relPath = $item.RelativePath $startMarker = $startTag + $relPath + '###' $endMarker = $endTag + $relPath + '###' $startIdx = $flatText.IndexOf($startMarker) $endIdx = if ($startIdx -ge 0) { $flatText.IndexOf($endMarker, $startIdx) } else { -1 } if ($startIdx -lt 0 -or $endIdx -lt $startIdx) { Write-Warning "Section for $relPath not found in flat text." continue } $contentStart = $startIdx + $startMarker.Length $content = $flatText.Substring($contentStart, $endIdx - $contentStart) # Remove exactly the line break written after the start marker and before the end marker if ($content.StartsWith("`r`n")) { $content = $content.Substring(2) } elseif ($content.StartsWith("`n")) { $content = $content.Substring(1) } if ($content.EndsWith("`r`n")) { $content = $content.Substring(0, $content.Length - 2) } elseif ($content.EndsWith("`n")) { $content = $content.Substring(0, $content.Length - 1) } $targetPath = & $toLocalPath $relPath $targetDir = Split-Path -Path $targetPath -Parent if (-not (Test-Path -Path $targetDir)) { $null = New-Item -ItemType Directory -Path $targetDir -Force -ErrorAction Stop } # Restore the byte order mark recorded by the export. Older manifests do not record it: non-ASCII # files then get a BOM so Windows PowerShell 5.1 reads them as UTF-8. if ($item.PSObject.Properties.Name -contains 'Utf8Bom') { $writeBom = [bool]$item.Utf8Bom } else { $writeBom = $content -match '[^\x00-\x7F]' } Write-Utf8TextFile -Path $targetPath -Content $content -Utf8Bom:$writeBom } catch { Write-Warning "Failed to create file: $($item.RelativePath). Error: $($_.Exception.Message)" } } Write-Verbose "Module files regenerated under $moduleParentPath." Invoke-TelemetryCollection @TelemetryArgs -Stage End } catch { Invoke-TelemetryCollection @TelemetryArgs -Stage End -Failed $true -Exception $_ throw } } |