-
Notifications
You must be signed in to change notification settings - Fork 10
/
l0g-101086.psm1
2264 lines (1876 loc) · 75.1 KB
/
l0g-101086.psm1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# SPDX-License-Identifier: BSD-3-Clause
# Copyright 2018 Jacob Keller. All rights reserved.
# vim: et:ts=4:sw=4
# This module contains several functions which are shared between the scripts
# related to uploading and formatting GW2 ArcDPS log files. It contains some
# general purpose utility functions, as well as functions related to managing
# the configuration file
<#
.Synopsis
Tests whether a path exists
.Description
Tests wither a given path exists. It is safe to pass a $null value to this
function, as it will return $false in that case.
.Parameter Path
The path to test
#>
Function X-Test-Path {
[CmdletBinding()]
param([Parameter(Mandatory)][string]$path)
try {
return Test-Path $path.trim()
} catch {
return $false
}
}
<#
.Synopsis
Print output to the log file, or console.
.Description
Print output to the log file if it has been specified. Otherwise, output
will be displayed to the screen.
.Parameter string
The string to log
#>
Function Log-Output {
[CmdletBinding()]
param([Parameter(Mandatory)][string]$string)
if ($script:logfile) {
Write-Output $string | Out-File -Append $script:logfile
} else {
Write-Output $string
}
}
<#
.Synopsis
Return the configuration file name
.Description
Return the configuration file. If $env:L0G_101086_CONFIG_FILE has been set,
then use this file. Otherwise, use the default "l0g-101086-config.json" filename
#>
Function Get-Config-File {
[CmdletBinding()]
param()
if ($env:L0G_101086_CONFIG_FILE) {
return $env:L0G_101086_CONFIG_FILE
} else {
return "l0g-101086-config.json"
}
}
<#
.Synopsis
Print output to the log file and the console.
.Description
Print output to the log file if it has been specified.
Also display output to the console screen as well.
.Parameter string
The string to log
#>
Function Log-And-Write-Output {
[CmdletBinding()]
param([Parameter(Mandatory)][string]$string)
if ($script:logfile) {
Write-Output $string | Out-File -Append $script:logfile
}
Write-Output $string
}
<#
.Synopsis
Set the log file used by Log-Output
.Description
Set the log file used by the Log-Output function. If the log file has been
set, then Log-Output will log to the file. Otherwise it will log to the screen.
To clear the log file, set it to $null
.Parameter file
The file name to set for the log file (or $null to clear it)
#>
Function Set-Logfile {
[CmdletBinding()]
param([Parameter(Mandatory)][string]$file)
$script:logfile = $file
}
<#
.Synopsis
Convert UTC time to the local timezone
.Description
Take a UTC date time object containing a UTC time and convert it to the
local time zone
.Parameter Time
The UTC time value to convert
#>
Function ConvertFrom-UTC {
[CmdletBinding()]
param([Parameter(Mandatory)][DateTime]$time)
[TimeZone]::CurrentTimeZone.ToLocalTime($time)
}
<#
.Synopsis
Convert a Unix timestamp to a DateTime object
.Description
Given a Unix timestamp (integer containing seconds since the Unix Epoch),
convert it to a DateTime object representing the same time.
.Parameter UnixDate
The Unix timestamp to convert
#>
Function ConvertFrom-UnixDate {
[CmdletBinding()]
param([Parameter(Mandatory)][int]$UnixDate)
ConvertFrom-UTC ([DateTime]'1/1/1970').AddSeconds($UnixDate)
}
<#
.Synopsis
Convert DateTime object into a Unix timestamp
.Description
Given a DateTime object, convert it to an integer representing seconds since
the Unix Epoch.
.Parameter Date
The DateTime object to convert
#>
Function ConvertTo-UnixDate {
[CmdletBinding()]
param([Parameter(Mandatory)][DateTime]$Date)
$UnixEpoch = [DateTime]'1/1/1970'
(New-TimeSpan -Start $UnixEpoch -End $Date).TotalSeconds
}
<#
.Synopsis
Check if an EVTC filename is expected to be compressed
.Description
Return $true if the filename matches known compressed EVTC file extensions,
false otherwise.
Similar to ExtensionIs-EVTC, but checks for only compressed filenames
.Parameter filename
The filename to check the extension of
#>
Function ExtensionIs-CompressedEVTC {
[CmdletBinding()]
param([Parameter(Mandatory)][string]$filename)
return ($filename -Like "*.evtc.zip" -or $filename -Like "*.zevtc")
}
<#
.Synopsis
Check if an EVTC filename is expected to be compressed
.Description
Return $true if the filename matches known uncompressed EVTC extension
Similar to ExtensionIs-CompressedEVTC, but checks for only uncompressed filenames
.Parameter filename
The filename to check the extension of
#>
Function ExtensionIs-UncompressedEVTC {
[CmdletBinding()]
param([Parameter(Mandatory)][string]$filename)
return ($filename -Like "*.evtc")
}
<#
.Synopsis
Check if a filename extension is for a (un)compressed EVTC file
.Description
Return $true if the given filename matches one of the known EVTC file
extensions for compressed or uncompressed EVTC log files.
.Parameter filename
The filename to check the extension of
#>
Function ExtensionIs-EVTC {
[CmdletBinding()]
param([Parameter(Mandatory)][string]$filename)
return ((ExtensionIs-UncompressedEVTC $filename) -or (ExtensionIs-CompressedEVTC $filename))
}
<#
.Synopsis
Given the EVTC file name, determine the uncompressed EVTC name
.Description
Determine the uncompressed name of the EVTC file, based on the file name.
.Parameter filename
The EVTC file to determine the uncompressed name of
#>
Function Get-UncompressedEVTC-Name {
[CmdletBinding()]
param([Parameter(Mandatory)][string]$filename)
if ($filename -Like "*.evtc") {
# This filename is already correct, so just strip the directory
return [io.path]::GetFileName($filename)
} elseif ($filename -Like "*.evtc.zip") {
# We have two extensions, so only remove the first one
return [io.path]::GetFileNameWithoutExtension($filename)
} elseif ($filename -Like "*.zevtc") {
# Strip the ".zevtc", and add back ".evtc"
$name = [io.path]::GetFileNameWithoutExtension($filename)
return "${name}.evtc"
} else {
throw "${filename} has an unrecognized extension"
}
}
<#
.Synopsis
Check simpleArcParse version to ensure it is compatible with the script
.Description
Given the version string reported by simpleArcParse, check if it is expected
to be compatible with this version of the script.
A simpleArcParse version is considered compatible if it has the correct major
and minor version. The patch version is ignored for these purposes. This assumes
that the versioning of simpleArcParse follows the Semantic Versioning outlined at
https://semver.org/
As of v1.2.0, the first version to have 3 digits, this should be the case.
Returns $true if the version is compatible, $false otherwise
.Parameter version
The version string, as reported by "(& $simple_arc_parse version)"
#>
Function Check-SimpleArcParse-Version {
[CmdletBinding()]
param([Parameter(Mandatory)][string]$version)
$expected_major_ver = 2
$expected_minor_ver = 4
$expected_patch_ver = 1
$expected_version = "v${expected_major_ver}.${expected_minor_ver}.${expected_patch_ver}"
if ($version -eq "") {
Write-Host "Unable to determine the version of simpleArcParse"
Write-Host "Please use the $expected_version release of simpleArcParse"
return $false
}
$found = $version -match 'v(\d+)\.(\d+)\.(\d+)'
if (-not $found) {
Write-Host "simpleArcParse version '${verison}' doesn't make any sense"
Write-Host "Please use the $expected_version release of simpleArcParse"
return $false
}
# Extract the actual major.minor.patch numbers
$actual_major_ver = [int]($matches[1])
$actual_minor_ver = [int]($matches[2])
$actual_patch_ver = [int]($matches[3])
# The major version is bumped when there are incompatibilities between the scripts
# and the simpleArcParse output. If the major versions are not an exact match,
# then assume we cannot possibly work.
if ($actual_major_ver -ne $expected_major_ver) {
Write-Host "simpleArcParse has major version ${actual_major_ver}, but we expected major version ${expected_major_ver}"
Write-Host "Please upgrade to the $expected_version release of simpleArcParse"
return $false
}
# Ok, we know the major version is an exact match, check the minor version
# If the minor version is *less* than the required minor version, we cannot run as we will miss a newly added feature
if ($actual_minor_ver -lt $expected_minor_ver) {
Log-And-Write-Output "simpleArcParse has minor version ${actual_minor_ver}, but we expected at least minor version ${expected_minor_ver}"
Log-And-Write-Output "Please upgrade to the $expected_version release of simpleArcParse"
return $false
} elseif ($actual_minor_ver -gt $expected_minor_ver) {
# Log non-fatal messages to the output file instead of the console
Log-And-Write-Output "simpleArcParse $version is newer than the expected $expected_version"
return $true
}
# At this point, we know that the minor version is an exact match too. Check the patch version to log a warning only
if ($actual_patch_ver -lt $expected_patch_ver) {
Log-And-Write-Output "You are using simpleArcParse ${version}, but ${expected_version} has been released, with possible bug fixes. You may want to upgrade."
} elseif ($actual_patch_ver -gt $expected_patch_ver) {
Log-And-Write-Output "simpleArcParse $version is newer than the expected $expected_version"
}
return $true
}
<#
.Synopsis
Returns the NoteProperties of a PSCustomObject
.Description
Given a PSCustomObject, return the names of each NoteProperty in the object
.Parameter obj
The PSCustomObject to match
#>
Function Keys {
[CmdletBinding()]
param([Parameter(Mandatory)][PSCustomObject]$obj)
return @($obj | Get-Member -MemberType NoteProperty | % Name)
}
<#
.Description
Configuration fields which are valid for a v2 configuration file. Anything
not listed here will be excluded from the generated $config object. If one
of the fields has an incorrect type, configuration will fail to be validated.
Fields which are common to many versions of the configuration file are stored
in $commonConfigurationFields
#>
$v2ValidGuildFields =
@(
@{
# The name of this guild
name="name"
type=[string]
}
@{
# Priority for determining which guild ran an encounter if there are
# conflicts. Lower numbers win ties.
name="priority"
type=[int]
}
@{
# Minimum number of players required for an encounter to be considered
# a guild run. 0 indicates any encounter can be considered if there is
# no better guild available
name="threshold"
type=[int]
}
@{
# The discord webhook URL for this guild
name="webhook_url"
type=[string]
}
@{
# URL to a thumbnail image for this guild
name="thumbnail"
type=[string]
}
@{
# Set this to true if this guild should be considered for fractal
# challenge motes. If set to false, fractals will never be posted
# to this guild.
name="fractals"
type=[bool]
}
@{
# Set this to true if the guild should be considered for raid encounters.
# If set to false, raid encounters will never be posted to this guild.
# Defaults to true if not specified
name="raids"
type=[bool]
optional=$true
default=$true
}
@{
# Set this to true if the guild should be considered for posting training
# golem encounters. If set to false, golem encounters will never be posted to
# this guild. Defaults to false if not specified.
name="golems"
type=[bool]
optional=$true
default=$false
}
@{
# Set of gw2 account names associated with this guild, mapped to
# their discord account ids. Used as the primary mechanism to determine
# which guild the encounter was run by, as well as for posting player pings
# to the discord webhook.
name="discord_map"
type=[PSCustomObject]
}
@{
# Determines how the list of players is displayed.
# "none" disables showing any gw2 accounts or discord pings
# "discord_only" will show only discord mapped names. Other accounts will not be displayed
# "accounts_only" will show the list using only account names, without discord pings
# "discord_if_possible" will show the discord map if possible, and the account name otherwise
name="show_players"
type=[string]
validStrings=@("none", "discord_only", "accounts_only", "discord_if_possible")
default="discord_if_possible"
}
@{
# Set this to any extra text you want to prefix the player account list. For example
# you can set it to "\u003c@526255792958078987\u003e" to add an @here ping
name="prefix_players_text"
type=[string]
optional=$true
}
@{
# emoji IDs used to provide pictures for each boss. Due to limitations of
# the webhook API, we can't use normal image URLs, but only emojis
# Each boss can have one emoji associated. If the map is empty for that boss
# then only the boss name will appear, without any emoji icon.
name="emoji_map"
type=[PSCustomObject]
}
@{
# If set to true, format-encounters will publish every post to this guilds
# discord. If unset or if set to false, only the encounters which match
# this guild will be published to the guild's discord.
name="everything"
type=[bool]
optional=$true
default=$false
}
@{
# If set to true, format-encounters will show the approximate duration that
# the encounter took as part of the link line. If set to false, this duration
# will not be displayed. Defaults to true.
name="show_duration"
type=[bool]
optional=$true
default=$true
}
@{
# If set, configures which encounters are published to this guild.
# "none" causes no encounters to be published for this guild
# "failed" causes only failed encounters to be published
# "successful" causes only successful encounters to be published
# "all" causes all encounters to be published.
# The default is "successful"
#
# When setting this to "all", you may want to ensure that
# "upload_dps_report" is set to "all" so that dps.report links exist
# for all encounters.
name="publish_encounters"
type=[string]
validStrings=@("none", "failed", "successful", "all")
default="successful"
}
)
<#
.Description
Configuration fields which are valid for the v2 configuration format.
If path is set, then the configuration will allow exchanging %UserProfile%
for the current $env:USERPROFILE value
If validFields is set to an array if fields, then the subfield will be
recursively validated. If arrayFields is set, then the field will be treated as
an array of objects and each object in the array will be recursively validated.
Path, validFields, and arrayFields are mutually exclusive
#>
$v2ConfigurationFields =
@(
@{
# Version indicating the format of the configuration file
name="config_version"
type=[int]
}
@{
# Setting debug_mode to true will modify some script behaviors
name="debug_mode"
type=[bool]
}
@{
# Setting experimental_arcdps will cause update-arcdps.ps1 to
# download the experimental version of ArcDPS instead of the
# stable version
name="experimental_arcdps"
type=[bool]
}
@{
# Path to the EVTC combat logs generated by ArcDPS
name="arcdps_logs"
type=[string]
path=$true
}
@{
# Path to a folder for storing the JSON we send to a discord webhook
# Intended for debugging if the logs do not format correctly. If
# this is set to a non-existent directory, then the discord webhooks
# will not be saved.
name="discord_json_data"
type=[string]
path=$true
}
@{
# Path to folder to store extra data about local EVTC encounter files
# Will contain files in the JSON format which have data exracted
# from the EVTC log file using simpleArcParse.
name="extra_upload_data"
type=[string]
path=$true
}
@{
# Path to a file which stores the last time that we formatted logs to discord
# Used to ensure that we don't re-post old logs. Disabled if debug_mode is true
name="last_format_file"
type=[string]
path=$true
}
@{
# Path to file to store the last time that we uploaded logs to dps.report
# This is *not* disabled when debug_mode is true, because we don't want to spam
# the uploads of old encounters.
name="last_upload_file"
type=[string]
path=$true
}
@{
# Path to the compiled binary for the simpleArcParse program
name="simple_arc_parse_path"
type=[string]
path=$true
}
@{
# Path to a file which logs actions and data generated while uploading logs
name="upload_log_file"
type=[string]
path=$true
}
@{
# Path to a file which logs actions and data generated while formatting to discord
name="format_encounters_log"
type=[string]
path=$true
}
@{
# Path to the GW2 installation directory
name="guildwars2_path"
type=[string]
path=$true
}
@{
# Path to Launch Buddy program (used by launcher.ps1)
name="launchbuddy_path"
type=[string]
path=$true
}
@{
# Path to a folder which holds backups of DLLs for arcdps, and related plugins
name="dll_backup_path"
type=[string]
path=$true
}
@{
# Path to the RestSharp DLL used for contacting dps.report
name="restsharp_path"
type=[string]
path=$true
}
@{
# An API token used by dps.report. Not currently required by dps.report but
# may be used in a future API update to allow searching for previously uploaded
# logs.
name="dps_report_token"
type=[string]
}
@{
# dps.report allows using alternative generators besides raid heros. This parameter
# is used to configure the generator used by the site, and must match a valid value
# from their API. Currently "rh" means RaidHeros, "ei" means EliteInsights, and
# leaving it blank will use the current default generator.
name="dps_report_generator"
type=[string]
}
@{
# If set, configures whether and how to upload to dps.report
# "no" disables uploading to dps.report entirely
# "successful" causes only successful encounters to be uploaded
# "all" causes all encounters to be uploaded.
# The default is "successful"
name="upload_dps_report"
type=[string]
validStrings=@("no", "successful", "all")
alternativeStrings=@{"none"="no"; "yes"="all"}
default="successful"
}
@{
# If set, specifies how long ago to pretend the last run of a script
# was, if no "last time" file exists. If git is accessible in your $PATH,
# then any approxidate format will work, such as "5 minutes ago" or
# "3 days ago". If git is NOT available in your $PATH, then it must
# be specified using "hours ago" only.
name="initial_last_event_time"
type=[string]
default="48 hours ago"
optional=$true
approxidate=$true
}
@{
name="guilds"
type=[Object[]]
arrayFields=$v2ValidGuildFields
}
)
<#
.Synopsis
Check if we have access to git
.Description
Check if git is available as a command we can run. Return true if it is
and false otherwise.
#>
Function Check-For-Git {
if (Get-Command "git" -ErrorAction SilentlyContinue) {
return $true
} else {
return $false
}
}
<#
.Synopsis
Parse an approxidate string into a DateTime object using git
.Description
Convert a string representing an approximate date into a DateTime object.
Use the git-config interface to enable the more advanced formats which
are supported within git as "approxidates"
This enables converting strings such as "5 hours ago" or "3 years ago",
or similar.
If the string cannot be parsed, then return $null
.Parameter approxidate
The approxidate string to convert
.Returns a DateTime object or $null
#>
Function Convert-Git-Approxidate-String {
[CmdletBinding()]
param([Parameter(Mandatory)][string]$approxidate)
# We have git, so use the git-config system
$seconds = (& git -c garbage.timestamp=`"${approxidate}`" config --type=expiry-date --get garbage.timestamp 2>$null)
if ($seconds -eq $null) {
return $null
}
return (ConvertFrom-UnixDate $seconds)
}
<#
.Synopsis
Parse a basic approxidate string into a DateTime object
.Description
Convert a string representing an approximate date into a DateTime object.
This function is used when git is not available in the path. It currently
only supports a very limited subset of the available formats that git does.
In any case, if we're unable to parse the string, return $null
.Parameter approxidate
The approxidate string to convert
.Returns a DateTime object or $null
#>
Function Convert-Basic-Approxidate-String {
[CmdletBinding()]
param([Parameter(Mandatory)][string]$approxidate)
$timespan=0
if ([timespan]::TryParseExact($approxidate, "%h' hours ago'", $null, [ref]$timespan)) {
return ((Get-Date) - $timespan)
} else {
return $null
}
}
<#
.Synopsis
Parse an approxidate string into a DateTime object
.Description
Convert the string representing an approximate date into a DateTime object.
If we have git available, use this to parse the date in the full git
approxidate format, which supports dates, and relative human readable
date time strings such as "4 hours ago" or "3 days ago".
If we do not have git available, then limit the support to only "hours ago"
format for simplicity.
In any case, if we're unable to parse the string, return $null
.Parameter approxidate
The approxidate string to convert
.Returns a DateTime
#>
Function Convert-Approxidate-String {
[CmdletBinding()]
param([Parameter(Mandatory)][string]$approxidate)
if (Check-For-Git) {
return (Convert-Git-Approxidate-String $approxidate)
} else {
return (Convert-Basic-Approxidate-String $approxidate)
}
}
<#
.Synopsis
Validate that a string can be parsed as an approximate date
.Description
Determine if a string represents a valid approximate date. If we have
the git program in our path, then we will use git-config to handle this via
the --expiry-date logic exposed by git-config.
If we do not have git, then only a small subset of "approxidate" formats will
be supported.
.Parameter approxidate
The string to check for being an approximate date string
.Returns true if the string is a valid approximate date, false otherwise.
#>
Function Validate-Approxidate-String {
[CmdletBinding()]
param([Parameter(Mandatory)][string]$approxidate)
if ((Convert-Approxidate-String $approxidate) -eq $null) {
return $false
} else {
return $true
}
}
<#
.Description
An enumeration defining methods for converting path-like fields
This enumeration defines the methods of converting path-like strings, which
support reading %UserProfile% as the $env:UserProfile environment variable.
FromUserProfile will allow converting the %UserProfile% string to the
UserProfile environment variable when reading the config in from disk.
ToUserProfile will allow converting the value of the UserProfile environment
variable into %UserProfile% when writing back out to disk.
#>
Add-Type -TypeDefinition @"
public enum PathConversion
{
FromUserProfile,
ToUserProfile,
}
"@
<#
.Synopsis
Validate fields of an object
.Description
Given a set of field definitions, validate that the given object has fields
of the correct type, possibly recursively.
Return the object on success, with updated path data if necessary. Unknown fields
will be removed from the returned object.
Return $null if the object has invalid fields or is missing required fields.
.Parameter object
The object to validate
.Parameter fields
The field definition
.Parameter RequiredFields
Specifies which fiels are required to exist. If a required field is missing, an error is
generated.
.Parameter conversion using the PathConversion enum
Optional parameter specifying how to convert path-like configuration values. The
default mode is to convert from %UserProfile% to the environment value for UserProfile
#>
Function Validate-Object-Fields {
[CmdletBinding()]
param([Parameter(Mandatory)][PSCustomObject]$Object,
[Parameter(Mandatory)][array]$Fields,
[Parameter(Mandatory)][AllowEmptyCollection()][array]$RequiredFields,
[PathConversion]$conversion = [PathConversion]::FromUserProfile)
# Make sure all the required parameters are actually valid
ForEach ($parameter in $RequiredFields) {
if ($parameter -notin ($Fields | ForEach-Object { $_.name })) {
Read-Host -Prompt "BUG: $parameter is not a valid parameter. Press enter to exit"
exit
}
}
# Select only the known properties, ignoring unknown properties
$Object = $Object | Select-Object -Property ($Fields | ForEach-Object { $_.name } | where { $Object."$_" -ne $null })
$invalid = $false
foreach ($field in $Fields) {
# Make sure required parameters are available
if (-not (Get-Member -InputObject $Object -Name $field.name)) {
# optional fields with a default value are never required. If not present, set their default value
if ($field.optional -or $field.default) {
$Object | Add-Member -Name $field.name -Value $field.default -MemberType NoteProperty
} elseif ($field.name -in $RequiredFields) {
Write-Host "$($field.name) is a required parameter for this script."
$invalid = $true
}
continue
}
# Make sure that the field has the expected type
if ($Object."$($field.name)" -isnot $field.type) {
Write-Host "$($field.name) has an unexpected type [$($Object."$($field.name)".GetType().name)]"
$invalid = $true
continue;
}
if ($field.path) {
# Handle %UserProfile% in path fields
switch ($conversion) {
"FromUserProfile" {
$Object."$($field.name)" = $Object."$($field.name)".replace("%UserProfile%", $env:USERPROFILE)
}
"ToUserProfile" {
$Object."$($field.name)" = $Object."$($field.name)".replace($env:USERPROFILE, "%UserProfile%")
}
}
} elseif ($field.approxidate) {
$approxidate = $Object."$($field.name)"
if (-not (Validate-Approxidate-String $approxidate)) {
Write-Host "$approxidate isn't a valid approximate time string"
if (-not (Check-For-Git)) {
Write-Host "Ensuring git is in your path allows parsing more approximate timestamps"
}
$invalid = $true
}
} elseif ($field.validFields) {
# Recursively validate subfields. All fields not explicitly marked "optional" must be present
$Object."$($field.name)" = Validate-Object-Fields $Object."$($field.name)" $field.validFields ($field.validFields | where { -not ( $_.optional -eq $true ) } | ForEach-Object { $_.name } )
} elseif ($field.arrayFields) {
# Recursively validate subfields of an array of objects. All fields not explicitly marked "optional" must be present
$ValidatedSubObjects = @()
$arrayObjectInvalid = $false
ForEach ($SubObject in $Object."$($field.name)") {
$SubObject = Validate-Object-Fields $SubObject $field.arrayFields ($field.arrayFields | where { -not ( $_.optional -eq $true ) } | ForEach-Object { $_.name } )
if (-not $SubObject) {
$arrayObjectInvalid = $true
break;
}
$ValidatedSubObjects += $SubObject
}
# If any of the sub fields was invalid, the whole array is invalid
if ($arrayObjectInvalid) {
$Object."$($field.name)" = $null
} else {
$Object."$($field.name)" = $ValidatedSubObjects
}
} elseif ($field.validStrings) {
# First, canonicalize strings
$fieldname = $field.name
$raw_value = $Object."$fieldname"
if ($field.alternativeStrings -and $field.alternativeStrings.Contains($raw_value)) {
$value = $field.alternativeStrings[$raw_value]
# Update the Object value to match the canonical representation
$Object."$fieldname" = $value
} else {
$value = $raw_value
}
if (-not $field.validStrings.Contains($value)) {
Write-Host "${raw_value} is not a valid value for $fieldname"
$invalid = $true
}
}
# If the subfield is now null, then the recursive validation failed, and this whole field is invalid
if ($Object."$($field.name)" -eq $null) {
$invalid = $true
}
}
if ($invalid) {
Read-Host -Prompt "Configuration file has invalid parameters. Press enter to exit"
return
}
return $Object
}
<#
.Synopsis
Validate a configuration object to make sure it has correct fields
.Description
Take a $config object, and verify that it has valid parameters with the expected
information and types. Return the $config object on success (with updated path names)
Return $null if the $config object is not valid.
.Parameter config
The configuration object to validate
.Parameter version
The expected configuration version, used to ensure that the config object matches
the configuration version used by the script requesting it.
.Parameter RequiredParameters
The parameters that are required by the invoking script
.Parameter conversion using the PathConversion enum
Optional parameter specifying how to convert path-like configuration values. The
default mode is to convert from %UserProfile% to the environment value for UserProfile
#>
Function Validate-Configuration {
[CmdletBinding()]
param([Parameter(Mandatory)][PSCustomObject]$config,
[Parameter(Mandatory)][int]$version,
[Parameter(Mandatory)][AllowEmptyCollection()][array]$RequiredParameters,
[PathConversion]$conversion = [PathConversion]::FromUserProfile)
if ($version -eq 1) {
Read-Host -Prompt "Loading and validating version 1 of the configuration file is no longer suppported. Press enter to exit"
exit
} elseif ($version -eq 2) {
$configurationFields = $v2ConfigurationFields
} else {
Read-Host -Prompt "BUG: configuration validation does not support version ${version}. Press enter to exit"
exit
}
# Make sure that the configuration file actually matches the version
# required by the script.
#
# Scripts should be resilient against new parameters not being configured.
if ($config.config_version -ne $version) {
Read-Host -Prompt "This script only knows how to understand config_version=${version}. Press enter to exit"
return
}
$config = Validate-Object-Fields $config $configurationFields $RequiredParameters $conversion
return $config
}
<#
.Synopsis
Load the configuration file and return a configuration object
.Description
Load the specified configuration file and return a valid configuration
object. Will ignore unknown fields in the configuration JSON, and will
convert magic path strings in path-like fields