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, and 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).
#>

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
                }
                Set-Content -Path $targetPath -Value $content -Encoding UTF8 -NoNewline -ErrorAction Stop
            }
            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
    }
}