Param([switch]$Help, [switch]$PreserveActiveProfile)

$ErrorActionPreference = "Stop"
Set-StrictMode -version 2

if ($Help) {
@"
$($MyInvocation.InvocationName) - Add Netmon parsers from current directory to
    current user's AutoProfile parser profile. If profile does not exist, create
    it.

Details:
- If there is a my_sparser.npl file in the current directory, this script
  will combine it into the AutoProfile.
- If there is no my_sparser.npl file, this script will add all .npl files in
  the current directory to AutoProfile. Watch out for double includes.
- Parsers added to the AutoProfile take precedence over parsers shipped with
  Netmon.
- Among AutoProfile parsers, parsers (directories) added most recently take
  precedence over older ones.
"@
    return
}

#
# Helper functions
#
function not { $savedargs = $args; $input | ? {$_ -notmatch $savedargs } }
function s { $savedargs = $args; if ($savedargs[0].gettype().isarray) { $savedargs = $savedargs[0] }; $input | %{ $_ -replace $savedargs } }
function g { $savedargs = $args; $input | ? {$_ -match $savedargs } }
function join($delim = "`n") { $arr = @($input | %{$_}); [string]::join($delim, $arr) }
function dot { $savedargs = $args; $input | % {$_.$savedargs} }
function split($delim) { $input | %{ ([string]$_).split($delim) } }
function insert { $args | %{$_}; $input }
function mkdir_force_wrapper($path) { Set-StrictMode -Off; mkdir -fo -path $path } # some older versions of Powershell require strictmode off for mkdir

function RetrieveSparserIncludes($sparser)
#
# Read the given parser file and returns an array of files it includes
# Throw an error if the sparser has unrecognized content
#
{
    $lines = @(gc $sparser)
    $nplregex = '^ *include "([\w-.]+\.npl)"$'
    $unrecognized = @($lines | not "^ *$" | not "^ *//" | not $nplregex)
    if ($unrecognized.count -ne 0) {
        write-error ("unrecognized lines in $sparser :",$unrecognized | join)
    }
    $lines | g $nplregex | s $nplregex "`$1"
}

#
# Constants
#
$AutoProfileGuidStr = [string]([guid]"6d414364-2ad9-4f64-b1b0-6dbd9e55657f")
$WindowsParsersGuidStr = [string]([guid]"FE3524BB-D1B3-41a4-BA6B-B05C3056B4D7")
$AutoProfileProfileName = "AutoProfile (generated)"

if (-not (Test-Path "HKLM:\software\microsoft\netmon3\ParserPackages\NetworkMonitor Parsers\Profiles\$WindowsParsersGuidStr")) {

    write-error "Windows Parsing profile not found. Do you have Netmon 3.4 installed? https://go.microsoft.com/fwlink/?LinkID=103158"
}

#
# Generate/update a combined my_sparser.npl to get NPLs from the current directory to be included in
# Netmon.
# Suppose we have 4 NPLs in the local directory:
#   inbox.npl            : Update to a parser that ships "inbox" with Netmon
#   my_sparser.npl       : One-liner: 'include "noninbox.npl"'
#   noninbox.npl         : A new parser, containing 'include "noninbox-private.npl"'
#   noninbox-private.npl : A new parser
# If there is another include directory with a my_sparser.npl, we need a combined my_sparser.npl.
# (The 2 main concepts at work here are: for filenames found in multiple include paths, such as
# my_sparser.npl and inbox.npl, Netmon ignores any but the highest-precedence matching file, based
# on include path precedence; and the same file cannot be included twice.)
#

$sparserdir = mkdir_force_wrapper "$env:userprofile/Documents/Network Monitor 3/Parsers/AutoProfile" | dot fullname
$sparserpath = "$sparserdir\my_sparser.npl"

if (-not (test-path $sparserpath)) {

    set-content $sparserpath "" -encoding ascii
}

$existingnpls = @(RetrieveSparserIncludes($sparserpath))

#
# Add only the NPLs that are referenced in the current directory's my_sparser.npl.
# If there is no local my_sparser.npl, include all local NPLs.
#
if (test-path my_sparser.npl) {

    $newnpls = @(RetrieveSparserIncludes my_sparser.npl)

} else {

    $newnpls = @(dir *.npl | dot name)
}

if (@(dir *.npl).count -eq 0) {

    write-error "No .npl files found in current directory, aborting"
}

#
# Remove duplicate NPLs
#
$unique = @($existingnpls + $newnpls | select -unique)

#
# Note, we are completely overwriting the existing autoprofile sparser, which will delete any
# manually added sparser comments
#
if ($unique.count -ne $existingnpls.count) {

    "[Re]writing $sparserpath`nto include $($unique.count - $existingnpls.count) additional parsers."
    $unique | %{"include `"$_`""} | insert "// Generated by $((gi $MyInvocation.InvocationName).fullname)" | set-content $sparserpath -encoding ascii

} else {

    "Already up to date: $sparserpath"
}

#
# Part 2: Create or update parser profile to include current dir and $sparserdir
#

$userprofiles = "HKCU:\software\microsoft\netmon3\ParserEngine\Profiles"
$sysprofiles = "HKLM:\software\microsoft\netmon3\ParserPackages\NetworkMonitor Parsers\Profiles"

#
# The only file needed to define the new profile is sparser.npl (whose contents are constant)
#
$profileDir = mkdir_force_wrapper "$env:userprofile/AppData/Roaming/Microsoft/Network Monitor 3/$AutoProfileGuidStr" | dot fullname
set-content $profileDir\sparser.npl @"
include "NetworkMonitor_Parsers_sparser.npl" // From NetworkMonitor Parsers
include "my_sparser.npl" // Private Parsers
"@

if (-not (Test-Path $userprofiles\$AutoProfileGuidStr)) {

    "Creating AutoProfile parser profile."
    mkdir_force_wrapper $userprofiles | out-null
    #
    # Base the new parser profile on one that ships with Netmon.
    # Windows parser profile is needed since it has some definitions that Windows parsers in the WDK
    # depend on.
    #
    copy $sysprofiles\$WindowsParsersGuidStr $userprofiles\$AutoProfileGuidStr 
}

#
# A new profile must have all the registry values we will set below.
#
$profileKey = "$userprofiles\$AutoProfileGuidStr"
sp $profileKey ID $AutoProfileGuidStr
sp $profileKey Name $AutoProfileProfileName
sp $profileKey Description "Parser profile generated by $((gi $MyInvocation.InvocationName).fullname) on $(get-date). Based on Windows Parsing profile plus WDK parsers."
sp $profileKey Type "Template Profile"
sp $profileKey ProfileDirectory $profileDir

#
# Compute IncludePath reg value. $profileDir must go at the beginning, then
# $sparserdir comes next so that the correct my_sparser.npl is picked up. Then add the current
# directory so it takes precedence over all other parsers. Finally, remove any duplicates in the
# IncludePath list in case any of these paths were already in it somewhere else.
#
$includepaths = @(gp $profileKey | dot IncludePath | split ";")
$newincludepaths = $includepaths | insert $profileDir, $sparserdir, (pwd).path | select -unique
"","Profile name: `"$AutoProfileProfileName`". Parser include paths:","",$newincludepaths
sp $profileKey IncludePath ($newincludepaths | join ";")

#
# Select our profile as current on behalf of user (if desired)
#
if (-not $PreserveActiveProfile) {
    sp $userprofiles/.. ActiveProfileID $AutoProfileGuidStr
    sp $userprofiles/.. IncludePath ($newincludepaths | join ";")
}


if ($newincludepaths.count -ne $includepaths.count -and
    @(get-process netmon -erroraction silentlycontinue).count -ne 0) {

    "!! You must restart Netmon for this change to take effect. !!"
}
