@bcility/al-performance-mcp 2.1.2 → 2.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +35 -27
- package/server.py +2131 -6
package/server.py
CHANGED
|
@@ -1,6 +1,2131 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
b'IyEvdXNyL2Jpbi9lbnYgcHl0aG9uMwoiIiIKQUwgUGVyZm9ybWFuY2UgTUNQIFNlcnZlcgo9PT09PT09PT09PT09PT09PT09PT09PT09Ck1DUCBzZXJ2ZXIgdGhhdCBzY2FucyBBTCBzb3VyY2UgZmlsZXMgZm9yIHBlcmZvcm1hbmNlIGFudGktcGF0dGVybnMKYW5kIGFwcGxpZXMgb3B0aW1pemVkIGZpeGVzIOKAlCBiYXNlZCBvbiB0aGUgQkMgVGVjaERheXMgMjAyNiBXb3Jrc2hvcAooNDQgZXhlcmNpc2VzIGNvdmVyaW5nIHJlYWwtd29ybGQgQUwgcGVyZm9ybWFuY2UgcGF0dGVybnMpLgoKVG9vbHMgZXhwb3NlZDoKICBzY2FuX2FsX3dvcmtzcGFjZSAgICDigJQgc2NhbiBhIGZvbGRlciBmb3IgYWxsIHBlcmZvcm1hbmNlIGlzc3VlcwogIGZpeF9hbF9maWxlICAgICAgICAgIOKAlCBhcHBseSBhbGwgZml4ZXMgdG8gYSBzaW5nbGUgQUwgZmlsZQogIGZpeF9hbF93b3Jrc3BhY2UgICAgIOKAlCBhcHBseSBhbGwgZml4ZXMgdG8gZXZlcnkgQUwgZmlsZSBpbiBhIGZvbGRlcgogIGxpc3RfcGF0dGVybnMgICAgICAgIOKAlCBsaXN0IGFsbCBrbm93biBwYXR0ZXJucyB3aXRoIGRlc2NyaXB0aW9ucwogIGV4cGxhaW5fcGF0dGVybiAgICAgIOKAlCBleHBsYWluIGEgc2luZ2xlIHBhdHRlcm4gaW4gZGVwdGgKClVzYWdlIHdpdGggQ2xhdWRlIERlc2t0b3AgLyBWUyBDb2RlIENvcGlsb3Q6CiAgQWRkIHRvIG1jcC5qc29uIChzZWUgbWNwLmpzb24gaW4gdGhpcyBmb2xkZXIpCiIiIgoKaW1wb3J0IHJlCmltcG9ydCBqc29uCmZyb20gcGF0aGxpYiBpbXBvcnQgUGF0aApmcm9tIHR5cGluZyBpbXBvcnQgQW55Cgpmcm9tIG1jcC5zZXJ2ZXIuZmFzdG1jcCBpbXBvcnQgRmFzdE1DUAoKbWNwID0gRmFzdE1DUCgiQUwgUGVyZm9ybWFuY2UgQW5hbHl6ZXIiKQoKIyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KIyBQYXR0ZXJuIHJlZ2lzdHJ5CiMgRWFjaCBlbnRyeToKIyAgIGlkICAgICAgICAgIOKAlCBzaG9ydCBpZGVudGlmaWVyIHVzZWQgaW4gZmluZGluZ3MKIyAgIGdyb3VwICAgICAgIOKAlCBjYXRlZ29yeSBvZiBpc3N1ZQojICAgc2V2ZXJpdHkgICAg4oCUIEhJR0ggLyBNRURJVU0gLyBMT1cKIyAgIHRpdGxlICAgICAgIOKAlCBvbmUtbGluZSBkZXNjcmlwdGlvbgojICAgZGVzY3JpcHRpb24g4oCUIGZ1bGwgZXhwbGFuYXRpb24KIyAgIGV4ZXJjaXNlcyAgIOKAlCB3aGljaCB3b3Jrc2hvcCBleGVyY2lzZXMgY292ZXIgdGhpcwojICAgZGV0ZWN0b3IgICAg4oCUIGNhbGxhYmxlKHRleHQpIC0+IGxpc3RbZGljdChsaW5lLCBzbmlwcGV0KV0KIyAgIGZpeGVyICAgICAgIOKAlCBjYWxsYWJsZSh0ZXh0KSAtPiBzdHIgIChyZXR1cm5zIGZpeGVkIHRleHQpCiMgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCgpQQVRURVJOUzogbGlzdFtkaWN0XSA9IFtdCgoKZGVmIF9yZWdpc3RlcihpZCwgZ3JvdXAsIHNldmVyaXR5LCB0aXRsZSwgZGVzY3JpcHRpb24sIGV4ZXJjaXNlcyk6CiAgICAiIiJEZWNvcmF0b3IgZmFjdG9yeSB0aGF0IHJlZ2lzdGVycyBhIHBhdHRlcm4gd2l0aCBpdHMgZGV0ZWN0b3IgYW5kIGZpeGVyLiIiIgogICAgZGVmIGRlY29yYXRvcihjbHMpOgogICAgICAgIFBBVFRFUk5TLmFwcGVuZCh7CiAgICAgICAgICAgICJpZCI6IGlkLAogICAgICAgICAgICAiZ3JvdXAiOiBncm91cCwKICAgICAgICAgICAgInNldmVyaXR5Ijogc2V2ZXJpdHksCiAgICAgICAgICAgICJ0aXRsZSI6IHRpdGxlLAogICAgICAgICAgICAiZGVzY3JpcHRpb24iOiBkZXNjcmlwdGlvbiwKICAgICAgICAgICAgImV4ZXJjaXNlcyI6IGV4ZXJjaXNlcywKICAgICAgICAgICAgImRldGVjdG9yIjogY2xzLmRldGVjdCwKICAgICAgICAgICAgImZpeGVyIjogY2xzLmZpeCwKICAgICAgICB9KQogICAgICAgIHJldHVybiBjbHMKICAgIHJldHVybiBkZWNvcmF0b3IKCgpkZWYgX2ZpbmRfbGluZSh0ZXh0OiBzdHIsIGNoYXJfb2Zmc2V0OiBpbnQpIC0+IGludDoKICAgIHJldHVybiB0ZXh0WzpjaGFyX29mZnNldF0uY291bnQoJ1xuJykgKyAxCgoKZGVmIF9maW5kaW5ncyh0ZXh0OiBzdHIsIHBhdHRlcm46IHJlLlBhdHRlcm4sIG1lc3NhZ2VfZm49Tm9uZSkgLT4gbGlzdFtkaWN0XToKICAgIG91dCA9IFtdCiAgICBmb3IgbSBpbiBwYXR0ZXJuLmZpbmRpdGVyKHRleHQpOgogICAgICAgIGxpbmUgPSBfZmluZF9saW5lKHRleHQsIG0uc3RhcnQoKSkKICAgICAgICBzbmlwcGV0ID0gbS5ncm91cCgwKS5zdHJpcCgpWzoxMjBdCiAgICAgICAgb3V0LmFwcGVuZCh7ImxpbmUiOiBsaW5lLCAic25pcHBldCI6IHNuaXBwZXQsICJtZXNzYWdlIjogbWVzc2FnZV9mbihtKSBpZiBtZXNzYWdlX2ZuIGVsc2UgIiJ9KQogICAgcmV0dXJuIG91dAoKCiMg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACiMgUEFUVEVSTiAwMSDigJQgTWlzc2luZyBTZXRMb2FkRmllbGRzIGJlZm9yZSBGaW5kU2V0IC8gRmluZAojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgApAX3JlZ2lzdGVyKAogICAgaWQ9Ik1JU1NJTkdfU0VUTE9BREZJRUxEUyIsCiAgICBncm91cD0iRGF0YSBUcmFuc2ZlciIsCiAgICBzZXZlcml0eT0iSElHSCIsCiAgICB0aXRsZT0iRmluZFNldC9GaW5kIHdpdGhvdXQgU2V0TG9hZEZpZWxkcyIsCiAgICBkZXNjcmlwdGlvbj0oJ0N1c3RvbWVyLkZpbmRTZXQoKSB3aXRob3V0IFNldExvYWRGaWVsZHMgZ2VuZXJhdGVzIGEgU0VMRUNUIHdpdGggQUxMIDEwMCsgZmllbGRzIOKAlCBpbmNsdWRpbmcgZXZlcnkgdGFibGUgZXh0ZW5zaW9uIGNvbHVtbiBqb2luZWQgaW4uIE9uIGVhY2ggcm93LCB0aGUgTlNUIGRlc2VyaWFsaXNlcyBhIG1hc3NpdmUgYnVmZmVyIGl0IGRvZXNuXCd0IG5lZWQuXG5cblNRTCBJTVBBQ1Q6IFNFTEVDVCBcXCIxOFxcIi5cXCJ0aW1lc3RhbXBcXCIsXFwiMThcXCIuXFwiTm9fXFwiLFxcIjE4XFwiLlxcIk5hbWVcXCIuLi4gKGFsbCAxMDArIGNvbHVtbnMpICsgSk9JTiBleHRlbnNpb24gdGFibGVzLlxuV2l0aCAxMCAwMDAgY3VzdG9tZXJzIHRoYXRcJ3MgZ2lnYWJ5dGVzIG9mIG5lZWRsZXNzbHkgdHJhbnNmZXJyZWQgZGF0YS5cblxuQkFEOiAgaWYgQ3VzdG9tZXIuRmluZFNldCgpIHRoZW5cbkdPT0Q6IEN1c3RvbWVyLlNldExvYWRGaWVsZHMoXFwiTm8uXFwiLCBOYW1lLCBcXCJDcmVkaXQgTGltaXQgKExDWSlcXCIpO1xuICAgICAgaWYgQ3VzdG9tZXIuRmluZFNldCgpIHRoZW5cblxuSElOVDogTG9vayBhdCB0aGUgU0VMRUNUIGluIFNRTCBQcm9maWxlci4gQ291bnQgaG93IG1hbnkgY29sdW1ucyBhcmUgZmV0Y2hlZC4gVGhlbiBhZGQgU2V0TG9hZEZpZWxkcyBhbmQgY29tcGFyZS4nKSwKICAgIGV4ZXJjaXNlcz1bMSwgMiwgMzNdLAopCmNsYXNzIFBhdHRlcm5NaXNzaW5nU2V0TG9hZEZpZWxkczoKICAgICMgRGV0ZWN0OiBGaW5kU2V0L0ZpbmQgb24gYSByZWNvcmQgdmFyaWFibGUgTk9UIGltbWVkaWF0ZWx5IHByZWNlZGVkICh3aXRoaW4gMyBsaW5lcykgYnkgU2V0TG9hZEZpZWxkcwogICAgX0ZJTkQgPSByZS5jb21waWxlKHInKFx3KylcLihGaW5kU2V0fEZpbmRGaXJzdHxGaW5kTGFzdHxGaW5kXCgpJywgcmUuTVVMVElMSU5FKQoKICAgIEBzdGF0aWNtZXRob2QKICAgIGRlZiBkZXRlY3QodGV4dDogc3RyKSAtPiBsaXN0W2RpY3RdOgogICAgICAgIGZpbmRpbmdzID0gW10KICAgICAgICBsaW5lcyA9IHRleHQuc3BsaXRsaW5lcygpCiAgICAgICAgZm9yIGksIGxpbmUgaW4gZW51bWVyYXRlKGxpbmVzKToKICAgICAgICAgICAgbSA9IHJlLnNlYXJjaChyJyhcdyspXC4oRmluZFNldHxGaW5kRmlyc3R8RmluZExhc3R8RmluZFwoKScsIGxpbmUpCiAgICAgICAgICAgIGlmIG5vdCBtOgogICAgICAgICAgICAgICAgY29udGludWUKICAgICAgICAgICAgdmFyID0gbS5ncm91cCgxKQogICAgICAgICAgICBpZiB2YXIubG93ZXIoKSBpbiAoJ3RydWUnLCAnZmFsc2UnLCAncmVzdWx0JywgJ3JlYycpOgogICAgICAgICAgICAgICAgY29udGludWUKICAgICAgICAgICAgIyBDaGVjayAzIGxpbmVzIGFib3ZlIGZvciBTZXRMb2FkRmllbGRzIG9uIHNhbWUgdmFyCiAgICAgICAgICAgIGNvbnRleHQgPSAnXG4nLmpvaW4obGluZXNbbWF4KDAsIGktMyk6aV0pCiAgICAgICAgICAgIGlmIGYne3Zhcn0uU2V0TG9hZEZpZWxkcycgbm90IGluIGNvbnRleHQgYW5kIGYne3Zhcn0uU2V0QXV0b0NhbGNGaWVsZHMnIG5vdCBpbiBjb250ZXh0OgogICAgICAgICAgICAgICAgIyBFeGNsdWRlIGlmIGluIGEgY29tbWVudAogICAgICAgICAgICAgICAgc3RyaXBwZWQgPSBsaW5lLnN0cmlwKCkKICAgICAgICAgICAgICAgIGlmIHN0cmlwcGVkLnN0YXJ0c3dpdGgoJy8vJyk6CiAgICAgICAgICAgICAgICAgICAgY29udGludWUKICAgICAgICAgICAgICAgIGZpbmRpbmdzLmFwcGVuZCh7CiAgICAgICAgICAgICAgICAgICAgImxpbmUiOiBpICsgMSwKICAgICAgICAgICAgICAgICAgICAic25pcHBldCI6IGxpbmUuc3RyaXAoKVs6MTIwXSwKICAgICAgICAgICAgICAgICAgICAibWVzc2FnZSI6IGYiJ3t2YXJ9LnttLmdyb3VwKDIpfScgY2FsbGVkIHdpdGhvdXQgU2V0TG9hZEZpZWxkcyIKICAgICAgICAgICAgICAgIH0pCiAgICAgICAgcmV0dXJuIGZpbmRpbmdzCgogICAgQHN0YXRpY21ldGhvZAogICAgZGVmIGZpeCh0ZXh0OiBzdHIpIC0+IHN0cjoKICAgICAgICAjIFRoaXMgcGF0dGVybiByZXF1aXJlcyBjb250ZXh0LXNwZWNpZmljIGZpeGVzICh3aGljaCBmaWVsZHMgdG8gbG9hZCkKICAgICAgICAjIFdlIGFubm90YXRlIHJhdGhlciB0aGFuIGJsaW5kbHkgZml4CiAgICAgICAgcmV0dXJuIHRleHQKCgojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgAojIFBBVFRFUk4gMDIg4oCUIEZpbmQoJy0nKSBidWZmZXItc2l6ZS0xIGFudGktcGF0dGVybgojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgApAX3JlZ2lzdGVyKAogICAgaWQ9IkZJTkRfREFTSF9CVUZGRVJfT05FIiwKICAgIGdyb3VwPSJEYXRhIFRyYW5zZmVyIiwKICAgIHNldmVyaXR5PSJISUdIIiwKICAgIHRpdGxlPSJGaW5kKCctJykgd2l0aCBidWZmZXIgc2l6ZSAxICh1c2UgRmluZFNldCBpbnN0ZWFkKSIsCiAgICBkZXNjcmlwdGlvbj0oIkZpbmQoJy0nKSBwcmUtZmV0Y2hlcyBvbmx5IDEgcmVjb3JkIGF0IGEgdGltZSAocmVhZCBidWZmZXIgPSAxKS4gRWFjaCBjYWxsIHRvIE5leHQoKSBmaXJlcyBhIG5ldyBTUUwgcXVlcnk6IFNFTEVDVCBUT1AgNTAgLi4uXG5Gb3IgMSAwMDAgZW50cmllcyB0aGF0IGlzIDIwIHJvdW5kLXRyaXBzIGluc3RlYWQgb2YgMSB3aXRoIEZpbmRTZXQuIEZpbmQoJy0nKSBhbHNvIGlnbm9yZXMgYW55IHByZXZpb3VzIFNldEN1cnJlbnRLZXkgY2FsbC5cblxuU1FMIElNUEFDVDogU0VMRUNUIFRPUCA1MCAuLi4gKHJlcGVhdGVkIHBlciBOZXh0KCkgYmF0Y2gpXG52cyBGaW5kU2V0OiBzaW5nbGUgU0VMRUNUIHRoYXQgcHJlLWZldGNoZXMgcm93cyBlZmZpY2llbnRseS5cblxuQkFEOiAgaWYgUmVjb3JkLkZpbmQoJy0nKSB0aGVuIHJlcGVhdCAuLi4gdW50aWwgUmVjb3JkLk5leHQoKSA9IDA7XG5HT09EOiBpZiBSZWNvcmQuRmluZFNldCgpIHRoZW4gcmVwZWF0IC4uLiB1bnRpbCBSZWNvcmQuTmV4dCgpID0gMDtcblxuSElOVDogQ29tcGFyZSB0aGUgbnVtYmVyIG9mIFNRTCBxdWVyaWVzIGZvciBGaW5kKCctJykgdnMgRmluZFNldCBvbiAxIDAwMCByb3dzLiBDaGVjayB0aGUgUmVhZHMgY29sdW1uIGluIFByb2ZpbGVyLiIpLAogICAgZXhlcmNpc2VzPVsyLCAyNF0sCikKY2xhc3MgUGF0dGVybkZpbmREYXNoOgogICAgX1JFID0gcmUuY29tcGlsZShyIihcdyspXC5GaW5kXCgnLSdcKSIsIHJlLk1VTFRJTElORSkKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZGV0ZWN0KHRleHQ6IHN0cikgLT4gbGlzdFtkaWN0XToKICAgICAgICByZXR1cm4gX2ZpbmRpbmdzKHRleHQsIFBhdHRlcm5GaW5kRGFzaC5fUkUsCiAgICAgICAgICAgICAgICAgICAgICAgICBsYW1iZGEgbTogZiJVc2UgRmluZFNldCgpIGluc3RlYWQgb2Yge20uZ3JvdXAoMSl9LkZpbmQoJy0nKSIpCgogICAgQHN0YXRpY21ldGhvZAogICAgZGVmIGZpeCh0ZXh0OiBzdHIpIC0+IHN0cjoKICAgICAgICByZXR1cm4gUGF0dGVybkZpbmREYXNoLl9SRS5zdWIobGFtYmRhIG06IGYie20uZ3JvdXAoMSl9LkZpbmRTZXQoKSIsIHRleHQpCgoKIyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKIyBQQVRURVJOIDAzIOKAlCBDYWxjRmllbGRzIGluc2lkZSBhIGxvb3AKIyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKQF9yZWdpc3RlcigKICAgIGlkPSJDQUxDRklFTERTX0lOX0xPT1AiLAogICAgZ3JvdXA9IkZsb3dGaWVsZHMiLAogICAgc2V2ZXJpdHk9IkhJR0giLAogICAgdGl0bGU9IkNhbGNGaWVsZHMgaW5zaWRlIGEgcmVwZWF0Li51bnRpbCBsb29wIiwKICAgIGRlc2NyaXB0aW9uPSgnQ2FsbGluZyBDYWxjRmllbGRzKEJhbGFuY2UpIGluc2lkZSBhIEZpbmRTZXQgbG9vcCBmaXJlcyBvbmUgZXh0cmEgU1FMIHN1Yi1xdWVyeSBwZXIgaXRlcmF0aW9uIHRvIGNvbXB1dGUgdGhlIEZsb3dGaWVsZCBhZ2dyZWdhdGUuIEZvciA1MDAgdmVuZG9ycyB0aGF0IGlzIDUwMCBleHRyYSBTUUwgcm91bmQtdHJpcHMganVzdCBmb3IgQmFsYW5jZS4gU2V0QXV0b0NhbGNGaWVsZHMgbWVyZ2VzIHRoZSBGbG93RmllbGQgaW50byB0aGUgbWFpbiBTRUxFQ1QuXG5cblNRTCBJTVBBQ1Q6IFBlciByb3c6IFNFTEVDVCBTVU0oLi4uKSBGUk9NIFxcIkRldGFpbGVkIFZlbmRvciBMZWRnLiBFbnRyeVxcIiAuLi4gPSBOIGFkZGl0aW9uYWwgcXVlcmllcyBpbnNpZGUgdGhlIGxvb3AuXG5cbkJBRDogIHJlcGVhdCBDdXN0b21lci5DYWxjRmllbGRzKEJhbGFuY2UpOyB1bnRpbCBDdXN0b21lci5OZXh0KCkgPSAwO1xuR09PRDogQ3VzdG9tZXIuU2V0QXV0b0NhbGNGaWVsZHMoQmFsYW5jZSk7XG4gICAgICBpZiBDdXN0b21lci5GaW5kU2V0KCkgdGhlbiByZXBlYXQgLi4uIHVudGlsIEN1c3RvbWVyLk5leHQoKSA9IDA7XG5cbkhJTlQ6IENvdW50IHRoZSBleHRyYSBTUUwgcXVlcmllcyBwZXIgbG9vcCBpdGVyYXRpb24gd2l0aCBDYWxjRmllbGRzIHZzIEF1dG9DYWxjRmllbGRzLiBEdXJhdGlvbiBkaWZmZXJlbmNlIGdyb3dzIHdpdGggcmVjb3JkIGNvdW50LicpLAogICAgZXhlcmNpc2VzPVszXSwKKQpjbGFzcyBQYXR0ZXJuQ2FsY0ZpZWxkc0luTG9vcDoKICAgIF9SRSA9IHJlLmNvbXBpbGUocideXHMrXHcrXC5DYWxjRmllbGRzXChbXildK1wpOycsIHJlLk1VTFRJTElORSkKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZGV0ZWN0KHRleHQ6IHN0cikgLT4gbGlzdFtkaWN0XToKICAgICAgICBmaW5kaW5ncyA9IFtdCiAgICAgICAgbGluZXMgPSB0ZXh0LnNwbGl0bGluZXMoKQogICAgICAgIGluX2xvb3AgPSBGYWxzZQogICAgICAgIGRlcHRoID0gMAogICAgICAgIGZvciBpLCBsaW5lIGluIGVudW1lcmF0ZShsaW5lcyk6CiAgICAgICAgICAgIHN0cmlwcGVkID0gbGluZS5zdHJpcCgpCiAgICAgICAgICAgIGlmIHN0cmlwcGVkLmxvd2VyKCkgPT0gJ3JlcGVhdCc6CiAgICAgICAgICAgICAgICBpbl9sb29wID0gVHJ1ZQogICAgICAgICAgICAgICAgZGVwdGggKz0gMQogICAgICAgICAgICBpZiByZS5tYXRjaChyJ3VudGlsXHMrXHcrXC5OZXh0XChcKScsIHN0cmlwcGVkLCByZS5JR05PUkVDQVNFKToKICAgICAgICAgICAgICAgIGRlcHRoIC09IDEKICAgICAgICAgICAgICAgIGlmIGRlcHRoIDw9IDA6CiAgICAgICAgICAgICAgICAgICAgaW5fbG9vcCA9IEZhbHNlCiAgICAgICAgICAgIGlmIGluX2xvb3AgYW5kIHJlLnNlYXJjaChyJ1x3K1wuQ2FsY0ZpZWxkc1woJywgbGluZSk6CiAgICAgICAgICAgICAgICBpZiBub3Qgc3RyaXBwZWQuc3RhcnRzd2l0aCgnLy8nKToKICAgICAgICAgICAgICAgICAgICBmaW5kaW5ncy5hcHBlbmQoewogICAgICAgICAgICAgICAgICAgICAgICAibGluZSI6IGkgKyAxLAogICAgICAgICAgICAgICAgICAgICAgICAic25pcHBldCI6IGxpbmUuc3RyaXAoKVs6MTIwXSwKICAgICAgICAgICAgICAgICAgICAgICAgIm1lc3NhZ2UiOiAiQ2FsY0ZpZWxkcygpIGluc2lkZSBsb29wIOKAlCB1c2UgU2V0QXV0b0NhbGNGaWVsZHMoKSBiZWZvcmUgRmluZFNldCgpIgogICAgICAgICAgICAgICAgICAgIH0pCiAgICAgICAgcmV0dXJuIGZpbmRpbmdzCgogICAgQHN0YXRpY21ldGhvZAogICAgZGVmIGZpeCh0ZXh0OiBzdHIpIC0+IHN0cjoKICAgICAgICByZXR1cm4gdGV4dCAgIyBDb250ZXh0LXNwZWNpZmljOyBuZWVkcyB0aGUgdmFyaWFibGUgbmFtZQoKCiMg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACiMgUEFUVEVSTiAwNCDigJQgUmVjb3JkIHBhcmFtZXRlciBwYXNzZWQgYnkgdmFsdWUgKG1pc3NpbmcgdmFyKQojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgApAX3JlZ2lzdGVyKAogICAgaWQ9IlJFQ09SRF9CWV9WQUxVRSIsCiAgICBncm91cD0iTWVtb3J5IC8gQ29waWVzIiwKICAgIHNldmVyaXR5PSJNRURJVU0iLAogICAgdGl0bGU9IlJlY29yZCBwYXJhbWV0ZXIgcGFzc2VkIGJ5IHZhbHVlIGluc3RlYWQgb2YgdmFyIiwKICAgIGRlc2NyaXB0aW9uPSgnV2l0aG91dCBWQVIsIEFMIGNvcGllcyB0aGUgZW50aXJlIFJlY29yZCBpbnN0YW5jZSBpbnRvIGEgbmV3IG1lbW9yeSBibG9jazogYWxsIGZpZWxkIHZhbHVlcywgZmlsdGVyIHN0YXRlLCBjdXJzb3IgcG9zaXRpb24uIExhcmdlIHJlY29yZHMgKEN1c3RvbWVyLCBJdGVtKSBjb3BpZWQgb24gZXZlcnkgY2FsbC4gV2l0aCBtYW55IG5lc3RlZCBjYWxscyB0aGlzIGNhdXNlcyBzaWduaWZpY2FudCBHQyBwcmVzc3VyZSBvbiBOU1QuXG5cblNRTCBJTVBBQ1Q6IE5vIFNRTCBjaGFuZ2Ug4oCUIG92ZXJoZWFkIGlzIGluIE5TVCBtZW1vcnkgYWxsb2NhdGlvbiAmIGNvcHkuXG5cbkJBRDogIGxvY2FsIHByb2NlZHVyZSBGb28oQ3VzdG9tZXI6IFJlY29yZCBDdXN0b21lcilcbkdPT0Q6IGxvY2FsIHByb2NlZHVyZSBGb28odmFyIEN1c3RvbWVyOiBSZWNvcmQgQ3VzdG9tZXIpXG5cbkhJTlQ6IE5vIFNRTCBkaWZmZXJlbmNlIOKAlCBtZWFzdXJlIE5TVCB0aHJvdWdocHV0IGFuZCBhbGxvY2F0aW9uIGNvdW50ZXJzLicpLAogICAgZXhlcmNpc2VzPVs0XSwKKQpjbGFzcyBQYXR0ZXJuUmVjb3JkQnlWYWx1ZToKICAgIF9SRSA9IHJlLmNvbXBpbGUoCiAgICAgICAgcicobG9jYWxccytwcm9jZWR1cmV8cHJvY2VkdXJlKVxzK1x3K1woKD86W14pXSosXHMqKT8oXHcrKTpccypSZWNvcmRccytbXjspXStcKScsCiAgICAgICAgcmUuTVVMVElMSU5FCiAgICApCgogICAgQHN0YXRpY21ldGhvZAogICAgZGVmIGRldGVjdCh0ZXh0OiBzdHIpIC0+IGxpc3RbZGljdF06CiAgICAgICAgZmluZGluZ3MgPSBbXQogICAgICAgIGZvciBtIGluIFBhdHRlcm5SZWNvcmRCeVZhbHVlLl9SRS5maW5kaXRlcih0ZXh0KToKICAgICAgICAgICAgbGluZV90ZXh0ID0gdGV4dFttYXgoMCwgbS5zdGFydCgpLTIwMCk6bS5lbmQoKV0KICAgICAgICAgICAgaWYgJyh2YXIgJyBpbiBtLmdyb3VwKDApIG9yIHJlLnNlYXJjaChyJ1xidmFyXHMrXHcrOlxzKlJlY29yZCcsIG0uZ3JvdXAoMCkpOgogICAgICAgICAgICAgICAgY29udGludWUKICAgICAgICAgICAgaWYgbS5ncm91cCgyKSA9PSAndmFyJzoKICAgICAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgICAgIGxpbmUgPSBfZmluZF9saW5lKHRleHQsIG0uc3RhcnQoKSkKICAgICAgICAgICAgZmluZGluZ3MuYXBwZW5kKHsKICAgICAgICAgICAgICAgICJsaW5lIjogbGluZSwKICAgICAgICAgICAgICAgICJzbmlwcGV0IjogbS5ncm91cCgwKVs6MTIwXSwKICAgICAgICAgICAgICAgICJtZXNzYWdlIjogIlJlY29yZCBwYXJhbWV0ZXIgd2l0aG91dCAndmFyJyDigJQgY3JlYXRlcyBhIGZ1bGwgY29weSBvbiBlYWNoIGNhbGwiCiAgICAgICAgICAgIH0pCiAgICAgICAgcmV0dXJuIGZpbmRpbmdzCgogICAgQHN0YXRpY21ldGhvZAogICAgZGVmIGZpeCh0ZXh0OiBzdHIpIC0+IHN0cjoKICAgICAgICByZXR1cm4gdGV4dAoKCiMg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACiMgUEFUVEVSTiAwNSDigJQgTG9vcCBzdW0gaW5zdGVhZCBvZiBDYWxjU3VtcwojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgApAX3JlZ2lzdGVyKAogICAgaWQ9IkxPT1BfU1VNX1ZTX0NBTENTVU1TIiwKICAgIGdyb3VwPSJBZ2dyZWdhdGlvbiIsCiAgICBzZXZlcml0eT0iSElHSCIsCiAgICB0aXRsZT0iTWFudWFsIHN1bW1hdGlvbiBsb29wIGluc3RlYWQgb2YgQ2FsY1N1bXMiLAogICAgZGVzY3JpcHRpb249KCdTdW1taW5nIGEgZmllbGQgYnkgaXRlcmF0aW5nIGFsbCByb3dzIGluIEFMOiBmZXRjaGVzIGV2ZXJ5IHJvdyBmcm9tIFNRTCwgZGVzZXJpYWxpc2VzIGl0IGluIE5TVCwgYWNjdW11bGF0ZXMgaW4gYSBsb2NhbCB2YXJpYWJsZS4gQ2FsY1N1bXMoQW1vdW50KSB0cmFuc2xhdGVzIHRvIGEgU1FMIFNVTSgpIGFnZ3JlZ2F0ZSDigJQgY29tcHV0ZWQgYXQgdGhlIGRhdGFiYXNlLCByZXR1cm5zIGEgc2luZ2xlIHJvdy5cblxuU1FMIElNUEFDVDogTWFudWFsIGxvb3A6IFNFTEVDVCBhbGwgcm93cywgcHJvY2VzcyBOIHJvd3MgaW4gTlNUXG5DYWxjU3VtczogICAgU0VMRUNUIFNVTShcXCJBbW91bnRcXCIpIC4uLiDigJQgcmV0dXJucyAxIHJvdy5cblxuQkFEOiAgcmVwZWF0IFRvdGFsICs9IFJlY29yZC5BbW91bnQ7IHVudGlsIFJlY29yZC5OZXh0KCkgPSAwO1xuR09PRDogUmVjb3JkLkNhbGNTdW1zKEFtb3VudCk7IFRvdGFsIDo9IFJlY29yZC5BbW91bnQ7XG5cbkhJTlQ6IENhbGNTdW1zIHJlc3VsdDogMSBTUUwgcm93IHJldHVybmVkLiBMb29wIHJlc3VsdDogTiBTUUwgcm93cyBmZXRjaGVkIGFuZCBzdW1tZWQuIENoZWNrIFJlYWRzOiBDYWxjU3VtcyA9IDEsIGxvb3AgPSBOLicpLAogICAgZXhlcmNpc2VzPVs1XSwKKQpjbGFzcyBQYXR0ZXJuTG9vcFN1bToKICAgIF9SRSA9IHJlLmNvbXBpbGUocicoXHcrKVxzKlwrPVxzKlx3K1wuXHcrOycsIHJlLk1VTFRJTElORSkKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZGV0ZWN0KHRleHQ6IHN0cikgLT4gbGlzdFtkaWN0XToKICAgICAgICBmaW5kaW5ncyA9IFtdCiAgICAgICAgbGluZXMgPSB0ZXh0LnNwbGl0bGluZXMoKQogICAgICAgIGluX2xvb3AgPSBGYWxzZQogICAgICAgIGZvciBpLCBsaW5lIGluIGVudW1lcmF0ZShsaW5lcyk6CiAgICAgICAgICAgIHN0cmlwcGVkID0gbGluZS5zdHJpcCgpCiAgICAgICAgICAgIGlmIHN0cmlwcGVkLmxvd2VyKCkgPT0gJ3JlcGVhdCc6CiAgICAgICAgICAgICAgICBpbl9sb29wID0gVHJ1ZQogICAgICAgICAgICBpZiByZS5tYXRjaChyJ3VudGlsXHMrXHcrXC5OZXh0XChcKScsIHN0cmlwcGVkLCByZS5JR05PUkVDQVNFKToKICAgICAgICAgICAgICAgIGluX2xvb3AgPSBGYWxzZQogICAgICAgICAgICBpZiBpbl9sb29wIGFuZCBQYXR0ZXJuTG9vcFN1bS5fUkUuc2VhcmNoKGxpbmUpIGFuZCBub3Qgc3RyaXBwZWQuc3RhcnRzd2l0aCgnLy8nKToKICAgICAgICAgICAgICAgIGZpbmRpbmdzLmFwcGVuZCh7CiAgICAgICAgICAgICAgICAgICAgImxpbmUiOiBpICsgMSwKICAgICAgICAgICAgICAgICAgICAic25pcHBldCI6IGxpbmUuc3RyaXAoKVs6MTIwXSwKICAgICAgICAgICAgICAgICAgICAibWVzc2FnZSI6ICJNYW51YWwgKz0gYWNjdW11bGF0aW9uIGluIGxvb3Ag4oCUIGNvbnNpZGVyIENhbGNTdW1zKCkgaWYgc3VtbWluZyBhIHNpbmdsZSBmaWVsZCIKICAgICAgICAgICAgICAgIH0pCiAgICAgICAgcmV0dXJuIGZpbmRpbmdzCgogICAgQHN0YXRpY21ldGhvZAogICAgZGVmIGZpeCh0ZXh0OiBzdHIpIC0+IHN0cjoKICAgICAgICByZXR1cm4gdGV4dAoKCiMg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACiMgUEFUVEVSTiAwNiDigJQgU3RyaW5nIGNvbmNhdGVuYXRpb24gKz0gaW4gbG9vcCAodXNlIFRleHRCdWlsZGVyKQojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgApAX3JlZ2lzdGVyKAogICAgaWQ9IlNUUklOR19DT05DQVRfSU5fTE9PUCIsCiAgICBncm91cD0iTWVtb3J5IC8gQ29waWVzIiwKICAgIHNldmVyaXR5PSJISUdIIiwKICAgIHRpdGxlPSJUZXh0ICs9IHN0cmluZyBjb25jYXRlbmF0aW9uIGluIGxvb3AgKE8obsKyKSkg4oCUIHVzZSBUZXh0QnVpbGRlciIsCiAgICBkZXNjcmlwdGlvbj0oIkFMJ3MgVGV4dCB0eXBlIGlzIGEgdmFsdWUgdHlwZS4gRWFjaCArPSBjcmVhdGVzIGEgYnJhbmQtbmV3IHN0cmluZyBvYmplY3QgaW4gTlNUIG1lbW9yeSDigJQgb2xkIHN0cmluZ3MgYmVjb21lIEdDIGdhcmJhZ2UgaW1tZWRpYXRlbHkuIEZvciBOIGl0ZXJhdGlvbnM6IE8oTsKyKSBtZW1vcnkgYWxsb2NhdGlvbnMgYW5kIGNvcGllcy4gVGV4dEJ1aWxkZXIgd3JhcHMgLk5FVCBTdHJpbmdCdWlsZGVyOiBPKE4pIHRpbWUsIG5vIGludGVybWVkaWF0ZSBjb3BpZXMuXG5cblNRTCBJTVBBQ1Q6IE5vIFNRTCBpbXBhY3Qg4oCUIHB1cmUgTlNUIG1lbW9yeSBwcmVzc3VyZSAmIEdDIG92ZXJoZWFkLlxuXG5CQUQ6ICBDc3ZDb250ZW50ICs9IEJ1aWxkRGV0YWlsTGluZSguLi4pO1xuR09PRDogdmFyIEJ1aWxkZXI6IFRleHRCdWlsZGVyO1xuICAgICAgQnVpbGRlci5BcHBlbmQoQnVpbGREZXRhaWxMaW5lKC4uLikpO1xuICAgICAgUmVzdWx0IDo9IEJ1aWxkZXIuVG9UZXh0KCk7IC8vIFNpbmdsZSBhbGxvY2F0aW9uIGF0IGVuZFxuXG5ISU5UOiBUaGUgR0MgcHJlc3N1cmUgaXMgaW52aXNpYmxlIGluIFNRTCBQcm9maWxlciBidXQgc2hvd3MgdXAgaW4gTlNUIG1lbW9yeSBjb3VudGVycy4iKSwKICAgIGV4ZXJjaXNlcz1bNl0sCikKY2xhc3MgUGF0dGVyblN0cmluZ0NvbmNhdEluTG9vcDoKICAgIF9SRSA9IHJlLmNvbXBpbGUocicoXHcrKVxzKlwrPVxzKig/IVtcZC5dKScsIHJlLk1VTFRJTElORSkKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZGV0ZWN0KHRleHQ6IHN0cikgLT4gbGlzdFtkaWN0XToKICAgICAgICBmaW5kaW5ncyA9IFtdCiAgICAgICAgbGluZXMgPSB0ZXh0LnNwbGl0bGluZXMoKQogICAgICAgIGluX2xvb3AgPSBGYWxzZQogICAgICAgIGZvciBpLCBsaW5lIGluIGVudW1lcmF0ZShsaW5lcyk6CiAgICAgICAgICAgIHN0cmlwcGVkID0gbGluZS5zdHJpcCgpCiAgICAgICAgICAgIGlmIHN0cmlwcGVkLmxvd2VyKCkgPT0gJ3JlcGVhdCcgb3IgcmUuc2VhcmNoKHInXGJmb3JlYWNoXGInLCBzdHJpcHBlZCwgcmUuSUdOT1JFQ0FTRSk6CiAgICAgICAgICAgICAgICBpbl9sb29wID0gVHJ1ZQogICAgICAgICAgICBpZiByZS5tYXRjaChyJ3VudGlsXHMrXHcrXC5OZXh0XChcKScsIHN0cmlwcGVkLCByZS5JR05PUkVDQVNFKSBvciBzdHJpcHBlZCA9PSAnZW5kOyc6CiAgICAgICAgICAgICAgICBpbl9sb29wID0gRmFsc2UKICAgICAgICAgICAgaWYgaW5fbG9vcDoKICAgICAgICAgICAgICAgIG0gPSBQYXR0ZXJuU3RyaW5nQ29uY2F0SW5Mb29wLl9SRS5zZWFyY2gobGluZSkKICAgICAgICAgICAgICAgIGlmIG0gYW5kIG5vdCBzdHJpcHBlZC5zdGFydHN3aXRoKCcvLycpOgogICAgICAgICAgICAgICAgICAgIHZhciA9IG0uZ3JvdXAoMSkKICAgICAgICAgICAgICAgICAgICAjIExvb2sgYmFjayB0byBzZWUgaWYgaXQncyBkZWNsYXJlZCBhcyBUZXh0CiAgICAgICAgICAgICAgICAgICAgY29udGV4dCA9ICdcbicuam9pbihsaW5lc1ttYXgoMCwgaS0zMCk6aV0pCiAgICAgICAgICAgICAgICAgICAgaWYgcmUuc2VhcmNoKHJmJ1xie3JlLmVzY2FwZSh2YXIpfVxzKjpccypUZXh0XGInLCBjb250ZXh0KToKICAgICAgICAgICAgICAgICAgICAgICAgZmluZGluZ3MuYXBwZW5kKHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJsaW5lIjogaSArIDEsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAic25pcHBldCI6IGxpbmUuc3RyaXAoKVs6MTIwXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJtZXNzYWdlIjogZiJUZXh0ICs9IGluIGxvb3AgY3JlYXRlcyBPKG7CsikgYWxsb2NhdGlvbnMg4oCUIHVzZSBUZXh0QnVpbGRlciIKICAgICAgICAgICAgICAgICAgICAgICAgfSkKICAgICAgICByZXR1cm4gZmluZGluZ3MKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZml4KHRleHQ6IHN0cikgLT4gc3RyOgogICAgICAgIHJldHVybiB0ZXh0CgoKIyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKIyBQQVRURVJOIDA3IOKAlCBGaW5kU2V0IHdpdGhvdXQgKHRydWUpIHdoZW4gbW9kaWZ5aW5nIHZpYSBjdXJzb3IKIyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKQF9yZWdpc3RlcigKICAgIGlkPSJGSU5EU0VUX01PRElGWV9OT19UUlVFIiwKICAgIGdyb3VwPSJMb2NraW5nIiwKICAgIHNldmVyaXR5PSJNRURJVU0iLAogICAgdGl0bGU9IkZpbmRTZXQoKSB3aXRob3V0ICh0cnVlKSB3aGVuIE1vZGlmeSBpcyBjYWxsZWQgb24gY3Vyc29yIHZhcmlhYmxlIiwKICAgIGRlc2NyaXB0aW9uPSgnRmluZFNldCB1c2VzIFJlYWRVbmNvbW1pdHRlZCBpc29sYXRpb24gYW5kIHByZS1mZXRjaGVzIHJvd3MgaW4gYmF0Y2hlcy4gQ2FsbGluZyBNb2RpZnkgaW5zaWRlIHRoZSBsb29wIGJvZHkgY2FuIGludmFsaWRhdGUgdGhlIGN1cnNvciBwb3NpdGlvbi4gUmVjb3JkcyBtYXkgYmUgdmlzaXRlZCB0d2ljZSwgc2tpcHBlZCBlbnRpcmVseSwgb3IgY2F1c2UgZGVhZGxvY2tzLiBUaGUgY3Vyc29yIGlzIG5vdCBkZXNpZ25lZCB0byBzdXJ2aXZlIGNvbmN1cnJlbnQgRE1MIG9uIHRoZSBzYW1lIHJvd3MuXG5cblNRTCBJTVBBQ1Q6IFNFTEVDVCBUT1AgNTAgLi4uIChSZWFkVW5jb21taXR0ZWQpIHRoZW4gVVBEQVRFIGluc2lkZSDigJQgY3Vyc29yIGluc3RhYmlsaXR5LlxuXG5CQUQ6ICBpZiBSZWNvcmQuRmluZFNldCgpIHRoZW4gcmVwZWF0IFJlY29yZC5GaWVsZCA6PSAuLi47IFJlY29yZC5Nb2RpZnkoKTsgLi4uXG5HT09EOiBpZiBSZWNvcmQuRmluZFNldCh0cnVlKSB0aGVuIHJlcGVhdCBSZWNvcmQuRmllbGQgOj0gLi4uOyBSZWNvcmQuTW9kaWZ5KCk7IC4uLlxuXG5ISU5UOiBVc2UgU1FMIFByb2ZpbGVyIHRvIHdhdGNoIHdoaWNoIHJlY29yZHMgZ2V0IHVwZGF0ZWQuIFJ1biB0aGUgYW50aS1wYXR0ZXJuIHR3aWNlIOKAlCBzb21lIHJlY29yZHMgbWF5IGJlIHVwZGF0ZWQgdHdpY2Ugb3Igc2tpcHBlZC4nKSwKICAgIGV4ZXJjaXNlcz1bN10sCikKY2xhc3MgUGF0dGVybkZpbmRTZXROb0ZvclVwZGF0ZToKICAgIF9SRSA9IHJlLmNvbXBpbGUocicoXHcrKVwuRmluZFNldFwoXCknLCByZS5NVUxUSUxJTkUpCgogICAgQHN0YXRpY21ldGhvZAogICAgZGVmIGRldGVjdCh0ZXh0OiBzdHIpIC0+IGxpc3RbZGljdF06CiAgICAgICAgZmluZGluZ3MgPSBbXQogICAgICAgIGxpbmVzID0gdGV4dC5zcGxpdGxpbmVzKCkKICAgICAgICBmb3IgaSwgbGluZSBpbiBlbnVtZXJhdGUobGluZXMpOgogICAgICAgICAgICBtID0gUGF0dGVybkZpbmRTZXROb0ZvclVwZGF0ZS5fUkUuc2VhcmNoKGxpbmUpCiAgICAgICAgICAgIGlmIG5vdCBtIG9yIGxpbmUuc3RyaXAoKS5zdGFydHN3aXRoKCcvLycpOgogICAgICAgICAgICAgICAgY29udGludWUKICAgICAgICAgICAgdmFyID0gbS5ncm91cCgxKQogICAgICAgICAgICAjIExvb2sgYWhlYWQgZm9yIE1vZGlmeSBvbiB0aGUgc2FtZSB2YXIKICAgICAgICAgICAgYWhlYWQgPSAnXG4nLmpvaW4obGluZXNbaTptaW4obGVuKGxpbmVzKSwgaSsxNSldKQogICAgICAgICAgICBpZiByZS5zZWFyY2gocmYnXGJ7cmUuZXNjYXBlKHZhcil9XC5Nb2RpZnlcKCcsIGFoZWFkKToKICAgICAgICAgICAgICAgIGZpbmRpbmdzLmFwcGVuZCh7CiAgICAgICAgICAgICAgICAgICAgImxpbmUiOiBpICsgMSwKICAgICAgICAgICAgICAgICAgICAic25pcHBldCI6IGxpbmUuc3RyaXAoKVs6MTIwXSwKICAgICAgICAgICAgICAgICAgICAibWVzc2FnZSI6IGYiJ3t2YXJ9LkZpbmRTZXQoKScgYnV0ICd7dmFyfS5Nb2RpZnkoKScgZm91bmQgaW4gbG9vcCDigJQgdXNlIEZpbmRTZXQodHJ1ZSkiCiAgICAgICAgICAgICAgICB9KQogICAgICAgIHJldHVybiBmaW5kaW5ncwoKICAgIEBzdGF0aWNtZXRob2QKICAgIGRlZiBmaXgodGV4dDogc3RyKSAtPiBzdHI6CiAgICAgICAgIyBPbmx5IGZpeCB3aGVuIE1vZGlmeSBmb2xsb3dzIGluIHNhbWUgbG9vcCDigJQgY29uc2VydmF0aXZlIGFwcHJvYWNoCiAgICAgICAgZGVmIHJlcGxhY2VfaWZfbW9kaWZ5X2ZvbGxvd3MobSk6CiAgICAgICAgICAgIHBvcyA9IG0uZW5kKCkKICAgICAgICAgICAgYWhlYWQgPSB0ZXh0W3Bvczpwb3MrNTAwXQogICAgICAgICAgICB2YXIgPSBtLmdyb3VwKDEpCiAgICAgICAgICAgIGlmIHJlLnNlYXJjaChyZidcYntyZS5lc2NhcGUodmFyKX1cLk1vZGlmeVwoJywgYWhlYWQpOgogICAgICAgICAgICAgICAgcmV0dXJuIGYie3Zhcn0uRmluZFNldCh0cnVlKSIKICAgICAgICAgICAgcmV0dXJuIG0uZ3JvdXAoMCkKICAgICAgICByZXR1cm4gcmUuc3ViKHInKFx3KylcLkZpbmRTZXRcKFwpJywgcmVwbGFjZV9pZl9tb2RpZnlfZm9sbG93cywgdGV4dCkKCgojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgAojIFBBVFRFUk4gMDgg4oCUIERlbGV0ZSBpbnNpZGUgYSBsb29wICh1c2UgRGVsZXRlQWxsKQojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgApAX3JlZ2lzdGVyKAogICAgaWQ9IkRFTEVURV9JTl9MT09QIiwKICAgIGdyb3VwPSJCdWxrIE9wZXJhdGlvbnMiLAogICAgc2V2ZXJpdHk9IkhJR0giLAogICAgdGl0bGU9IlJlY29yZC5EZWxldGUoKSBpbnNpZGUgYSBsb29wIOKAlCB1c2UgRGVsZXRlQWxsKCkiLAogICAgZGVzY3JpcHRpb249KCdTYW1lIGN1cnNvciBpbnN0YWJpbGl0eSBhcyBNb2RpZnkg4oCUIERlbGV0ZSBkdXJpbmcgRmluZFNldCBjYW4gY2F1c2UgdGhlIHJlYWQgY3Vyc29yIHRvIHNraXAgb3IgcmV2aXNpdCByZWNvcmRzLiBEZWxldGVBbGwgaXMgYXRvbWljIGFuZCB1c2VzIGEgc2luZ2xlIERFTEVURSBGUk9NIHN0YXRlbWVudC4gSWYgc2VsZWN0aXZlIGRlbGV0aW9uIGlzIG5lZWRlZCwgY29sbGVjdCBrZXlzIGZpcnN0LCBkZWxldGUgYWZ0ZXIgbG9vcC5cblxuU1FMIElNUEFDVDogU0VMRUNUIFRPUCA1MCAoY3Vyc29yKSArIERFTEVURSBwZXIgcm93IOKAlCBjdXJzb3IgY29ycnVwdGlvbiByaXNrLlxuRGVsZXRlQWxsOiAxIERFTEVURSBGUk9NIHN0YXRlbWVudCB2cyBOIGluZGl2aWR1YWwgREVMRVRFcy5cblxuQkFEOiAgaWYgUmVjb3JkLkZpbmRTZXQoKSB0aGVuIHJlcGVhdCBSZWNvcmQuRGVsZXRlKCk7IHVudGlsIFJlY29yZC5OZXh0KCkgPSAwO1xuR09PRDogUmVjb3JkLlNldFJhbmdlKEFjdGl2ZSwgZmFsc2UpOyBSZWNvcmQuRGVsZXRlQWxsKGZhbHNlKTtcblxuSElOVDogQ29tcGFyZSB0aGUgbnVtYmVyIG9mIERFTEVURSBzdGF0ZW1lbnRzIGluIFByb2ZpbGVyOiBEZWxldGVBbGwgPSAxIFNRTCBERUxFVEU7IGxvb3AgPSBOIERFTEVURXMuJyksCiAgICBleGVyY2lzZXM9WzhdLAopCmNsYXNzIFBhdHRlcm5EZWxldGVJbkxvb3A6CiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZGV0ZWN0KHRleHQ6IHN0cikgLT4gbGlzdFtkaWN0XToKICAgICAgICBmaW5kaW5ncyA9IFtdCiAgICAgICAgbGluZXMgPSB0ZXh0LnNwbGl0bGluZXMoKQogICAgICAgIGluX2xvb3AgPSBGYWxzZQogICAgICAgIGxvb3BfdmFyID0gTm9uZQogICAgICAgIGZvciBpLCBsaW5lIGluIGVudW1lcmF0ZShsaW5lcyk6CiAgICAgICAgICAgIHN0cmlwcGVkID0gbGluZS5zdHJpcCgpCiAgICAgICAgICAgIG0gPSByZS5zZWFyY2gocicoXHcrKVwuRmluZFNldCcsIGxpbmUpCiAgICAgICAgICAgIGlmIG0gYW5kIG5vdCBzdHJpcHBlZC5zdGFydHN3aXRoKCcvLycpOgogICAgICAgICAgICAgICAgaW5fbG9vcCA9IFRydWUKICAgICAgICAgICAgICAgIGxvb3BfdmFyID0gbS5ncm91cCgxKQogICAgICAgICAgICBpZiBpbl9sb29wIGFuZCBsb29wX3ZhciBhbmQgcmUuc2VhcmNoKHJmJ1xie3JlLmVzY2FwZShsb29wX3Zhcil9XC5EZWxldGVcKCcsIGxpbmUpOgogICAgICAgICAgICAgICAgaWYgbm90IHN0cmlwcGVkLnN0YXJ0c3dpdGgoJy8vJyk6CiAgICAgICAgICAgICAgICAgICAgZmluZGluZ3MuYXBwZW5kKHsKICAgICAgICAgICAgICAgICAgICAgICAgImxpbmUiOiBpICsgMSwKICAgICAgICAgICAgICAgICAgICAgICAgInNuaXBwZXQiOiBsaW5lLnN0cmlwKClbOjEyMF0sCiAgICAgICAgICAgICAgICAgICAgICAgICJtZXNzYWdlIjogZiIne2xvb3BfdmFyfS5EZWxldGUoKScgaW5zaWRlIEZpbmRTZXQgbG9vcCDigJQgdXNlICd7bG9vcF92YXJ9LkRlbGV0ZUFsbChmYWxzZSknIgogICAgICAgICAgICAgICAgICAgIH0pCiAgICAgICAgICAgIGlmIHJlLm1hdGNoKHIndW50aWxccytcdytcLk5leHRcKFwpJywgc3RyaXBwZWQsIHJlLklHTk9SRUNBU0UpOgogICAgICAgICAgICAgICAgaW5fbG9vcCA9IEZhbHNlCiAgICAgICAgICAgICAgICBsb29wX3ZhciA9IE5vbmUKICAgICAgICByZXR1cm4gZmluZGluZ3MKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZml4KHRleHQ6IHN0cikgLT4gc3RyOgogICAgICAgIHJldHVybiB0ZXh0CgoKIyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKIyBQQVRURVJOIDA5IOKAlCBDb3VudCgpIDw+IDAgaW5zdGVhZCBvZiBub3QgSXNFbXB0eSgpCiMg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACkBfcmVnaXN0ZXIoCiAgICBpZD0iQ09VTlRfTk9UX1pFUk8iLAogICAgZ3JvdXA9IkV4aXN0ZW5jZSBDaGVja3MiLAogICAgc2V2ZXJpdHk9Ik1FRElVTSIsCiAgICB0aXRsZT0iQ291bnQoKSA8PiAwIC8gQ291bnQoKSA+IDAg4oCUIHVzZSBJc0VtcHR5KCkgaW5zdGVhZCIsCiAgICBkZXNjcmlwdGlvbj0oJ0NvdW50KCkgZ2VuZXJhdGVzIFNFTEVDVCBDT1VOVCgqKSDigJQgdGhlIGRhdGFiYXNlIG11c3QgY291bnQgQUxMIG1hdGNoaW5nIHJvd3MgYmVmb3JlIHJldHVybmluZy4gT24gbGFyZ2UgdGFibGVzIHRoaXMgaXMgZXhwZW5zaXZlLiBJc0VtcHR5KCkgZ2VuZXJhdGVzIElGIEVYSVNUUyAoU0VMRUNUIFRPUCAxIE5VTEwgLi4uKSDigJQgc3RvcHMgYXQgdGhlIGZpcnN0IG1hdGNoLiBPKDEpIHZzIE8oTikuXG5cblNRTCBJTVBBQ1Q6IENvdW50KCk6ICAgU0VMRUNUIENPVU5UKCopIEZST00gLi4uIOKGkiBzY2FucyBlbnRpcmUgdGFibGVcbklzRW1wdHkoKTogSUYgRVhJU1RTIChTRUxFQ1QgVE9QIDEgTlVMTCBGUk9NIC4uLikg4oaSIHN0b3BzIGltbWVkaWF0ZWx5LlxuXG5CQUQ6ICBleGl0KFJlY29yZC5Db3VudCgpIDw+IDApO1xuR09PRDogZXhpdChub3QgUmVjb3JkLklzRW1wdHkoKSk7XG5cbkhJTlQ6IENvbXBhcmUgU1FMIFByb2ZpbGVyIG91dHB1dCBmb3IgYm90aC4gQ291bnQoKSBhbHdheXMgcmVhZHMgYWxsIHJvd3MgZmlyc3QuIElzRW1wdHkgc3RvcHMgYXQgdGhlIHZlcnkgZmlyc3QgbWF0Y2guJyksCiAgICBleGVyY2lzZXM9WzksIDEwLCAxMV0sCikKY2xhc3MgUGF0dGVybkNvdW50Tm90WmVybzoKICAgIF9SRSA9IHJlLmNvbXBpbGUocicoXHcrKVwuQ291bnRcKFwpXHMqKD86PD58Pnw9KVxzKjAnLCByZS5NVUxUSUxJTkUpCgogICAgQHN0YXRpY21ldGhvZAogICAgZGVmIGRldGVjdCh0ZXh0OiBzdHIpIC0+IGxpc3RbZGljdF06CiAgICAgICAgcmV0dXJuIF9maW5kaW5ncyh0ZXh0LCBQYXR0ZXJuQ291bnROb3RaZXJvLl9SRSwKICAgICAgICAgICAgICAgICAgICAgICAgIGxhbWJkYSBtOiBmIid7bS5ncm91cCgxKX0uQ291bnQoKSAhPSAwJyDigJQgdXNlIElzRW1wdHkoKSBmb3IgTygxKSBleGlzdGVuY2UgY2hlY2siKQoKICAgIEBzdGF0aWNtZXRob2QKICAgIGRlZiBmaXgodGV4dDogc3RyKSAtPiBzdHI6CiAgICAgICAgZGVmIHJlcGxhY2UobSk6CiAgICAgICAgICAgIHZhciA9IG0uZ3JvdXAoMSkKICAgICAgICAgICAgb3AgPSBtLmdyb3VwKDApLnNwbGl0KCdDb3VudCgpJylbMV0uc3RyaXAoKS5zcGxpdCgnICcpWzBdCiAgICAgICAgICAgIGlmIG9wIGluICgnPD4nLCAnPicpOgogICAgICAgICAgICAgICAgcmV0dXJuIGYnbm90IHt2YXJ9LklzRW1wdHkoKScKICAgICAgICAgICAgaWYgb3AgPT0gJz0nOgogICAgICAgICAgICAgICAgcmV0dXJuIGYne3Zhcn0uSXNFbXB0eSgpJwogICAgICAgICAgICByZXR1cm4gbS5ncm91cCgwKQogICAgICAgIHJldHVybiBQYXR0ZXJuQ291bnROb3RaZXJvLl9SRS5zdWIocmVwbGFjZSwgdGV4dCkKCgojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgAojIFBBVFRFUk4gMTAg4oCUIFJlZHVuZGFudCBJc0VtcHR5IGJlZm9yZSBGaW5kU2V0IChkb3VibGUgc2NhbikKIyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKQF9yZWdpc3RlcigKICAgIGlkPSJJU0VNUFRZX0JFRk9SRV9GSU5EU0VUIiwKICAgIGdyb3VwPSJFeGlzdGVuY2UgQ2hlY2tzIiwKICAgIHNldmVyaXR5PSJNRURJVU0iLAogICAgdGl0bGU9IlJlZHVuZGFudCBJc0VtcHR5KCkgYmVmb3JlIEZpbmRTZXQoKSAodHdvIFNRTCBzY2FucyBpbnN0ZWFkIG9mIG9uZSkiLAogICAgZGVzY3JpcHRpb249KCdDaGVja2luZyBJc0VtcHR5KCkgaW1tZWRpYXRlbHkgYmVmb3JlIEZpbmRTZXQgZmlyZXMgVFdPIFNRTCBxdWVyaWVzOiAxKSBJRiBFWElTVFMgKFNFTEVDVCBUT1AgMSBOVUxMIC4uLikg4oCUIHRoZSBJc0VtcHR5IGNoZWNrOyAyKSBTRUxFQ1QgVE9QIDUwIC4uLiDigJQgdGhlIEZpbmRTZXQuIEZpbmRTZXQgYWxyZWFkeSByZXR1cm5zIGZhbHNlIHdoZW4gbm8gcm93cyBtYXRjaCDigJQgdGhlIElzRW1wdHkgaXMgd2FzdGUuXG5cblNRTCBJTVBBQ1Q6IFF1ZXJ5IDE6IElGIEVYSVNUUyAoU0VMRUNUIFRPUCAxIE5VTEwgLi4uKS4gUXVlcnkgMjogU0VMRUNUIFRPUCA1MCAuLi4g4oaSIHJlZHVuZGFudC5cblxuQkFEOiAgaWYgbm90IFJlY29yZC5Jc0VtcHR5KCkgdGhlblxuICAgICAgICAgIGlmIFJlY29yZC5GaW5kU2V0KCkgdGhlbiAuLi5cbkdPT0Q6IGlmIFJlY29yZC5GaW5kU2V0KCkgdGhlbiAuLi5cblxuSElOVDogQ291bnQgdGhlIFNRTCBxdWVyaWVzIGluIFByb2ZpbGVyOiBJc0VtcHR5ICsgRmluZFNldCA9IDI7IGp1c3QgRmluZFNldCA9IDEuIFRoZSBJc0VtcHR5IHJlc3VsdCBpcyBpZ25vcmVkIGJ5IEZpbmRTZXQgYW55d2F5LicpLAogICAgZXhlcmNpc2VzPVsxMV0sCikKY2xhc3MgUGF0dGVybklzRW1wdHlCZWZvcmVGaW5kU2V0OgogICAgQHN0YXRpY21ldGhvZAogICAgZGVmIGRldGVjdCh0ZXh0OiBzdHIpIC0+IGxpc3RbZGljdF06CiAgICAgICAgZmluZGluZ3MgPSBbXQogICAgICAgIGxpbmVzID0gdGV4dC5zcGxpdGxpbmVzKCkKICAgICAgICBmb3IgaSwgbGluZSBpbiBlbnVtZXJhdGUobGluZXMpOgogICAgICAgICAgICBpZiByZS5zZWFyY2gocidub3RccytcdytcLklzRW1wdHlcKFwpJywgbGluZSkgYW5kIG5vdCBsaW5lLnN0cmlwKCkuc3RhcnRzd2l0aCgnLy8nKToKICAgICAgICAgICAgICAgICMgQ2hlY2sgbmV4dCAyIGxpbmVzIGZvciBGaW5kU2V0CiAgICAgICAgICAgICAgICBhaGVhZCA9ICdcbicuam9pbihsaW5lc1tpOm1pbihsZW4obGluZXMpLCBpKzMpXSkKICAgICAgICAgICAgICAgIGlmIHJlLnNlYXJjaChyJ1wuRmluZFNldFwoJywgYWhlYWQpOgogICAgICAgICAgICAgICAgICAgIGZpbmRpbmdzLmFwcGVuZCh7CiAgICAgICAgICAgICAgICAgICAgICAgICJsaW5lIjogaSArIDEsCiAgICAgICAgICAgICAgICAgICAgICAgICJzbmlwcGV0IjogbGluZS5zdHJpcCgpWzoxMjBdLAogICAgICAgICAgICAgICAgICAgICAgICAibWVzc2FnZSI6ICJSZWR1bmRhbnQgSXNFbXB0eSgpIGd1YXJkIGJlZm9yZSBGaW5kU2V0KCkg4oCUIEZpbmRTZXQoKSByZXR1cm5zIGZhbHNlIHdoZW4gZW1wdHkiCiAgICAgICAgICAgICAgICAgICAgfSkKICAgICAgICByZXR1cm4gZmluZGluZ3MKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZml4KHRleHQ6IHN0cikgLT4gc3RyOgogICAgICAgIHJldHVybiByZS5zdWIoCiAgICAgICAgICAgIHInWyBcdF0raWYgbm90IChcdyspXC5Jc0VtcHR5XChcKSB0aGVuXHMqXG4oWyBcdF0rKWlmIFwxXC5GaW5kU2V0XCgnLAogICAgICAgICAgICByJ1wyaWYgXDEuRmluZFNldCgnLAogICAgICAgICAgICB0ZXh0CiAgICAgICAgKQoKCiMg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACiMgUEFUVEVSTiAxMSDigJQgTWlzc2luZyAndGVtcG9yYXJ5JyBrZXl3b3JkIG9uIHRlbXAgdGFibGUgdmFyaWFibGUKIyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKQF9yZWdpc3RlcigKICAgIGlkPSJNSVNTSU5HX1RFTVBPUkFSWSIsCiAgICBncm91cD0iTWVtb3J5IC8gQ29waWVzIiwKICAgIHNldmVyaXR5PSJISUdIIiwKICAgIHRpdGxlPSJUZW1wIHRhYmxlIHZhcmlhYmxlIG1pc3NpbmcgJ3RlbXBvcmFyeScga2V5d29yZCIsCiAgICBkZXNjcmlwdGlvbj0oJ0Egbm9uLXRlbXBvcmFyeSByZWNvcmQgc2VuZHMgZXZlcnkgSW5zZXJ0L0dldC9EZWxldGUgdG8gU1FMIFNlcnZlci4gVXNpbmcgYSByZWd1bGFyIHRhYmxlIGFzIGFuIGluLW1lbW9yeSBzY3JhdGNocGFkIGNhdXNlcyB1bm5lY2Vzc2FyeSBTUUwgcm91bmQtdHJpcHMsIHRyYW5zYWN0aW9uIGxvZyB3cml0ZXMsIGFuZCBsb2NrIGNvbnRlbnRpb24uIERlY2xhcmluZyB0aGUgdmFyaWFibGUgXCd0ZW1wb3JhcnlcJyBrZWVwcyBhbGwgb3BlcmF0aW9ucyBpbiBOU1QgbWVtb3J5LlxuXG5TUUwgSU1QQUNUOiBXaXRob3V0IHRlbXBvcmFyeTogSU5TRVJUIElOVE8gLyBTRUxFQ1QgLyBERUxFVEUg4oCUIGZ1bGwgU1FMIGZvciBlYWNoIG9wLlxuV2l0aCB0ZW1wb3Jhcnk6IEFsbCBvcGVyYXRpb25zIGluIE5TVCBtZW1vcnkg4oCUIHplcm8gU1FMIHJvdW5kLXRyaXBzLlxuXG5CQUQ6ICBUZW1wRGF0YTogUmVjb3JkIFxcIldvcmtzaG9wIERhdGFcXCI7XG5HT09EOiBUZW1wRGF0YTogUmVjb3JkIFxcIldvcmtzaG9wIERhdGFcXCIgdGVtcG9yYXJ5O1xuXG5ISU5UOiBJbnNlcnQgMSAwMDAgcm93cyB3aXRoIGFuZCB3aXRob3V0IFwndGVtcG9yYXJ5XCcuIFdpdGhvdXQ6IHdhdGNoIDEgMDAwIElOU0VSVHMgaW4gU1FMIFByb2ZpbGVyLiBXaXRoIHRlbXBvcmFyeTogemVybyBTUUwgYWN0aXZpdHkuJyksCiAgICBleGVyY2lzZXM9WzEyXSwKKQpjbGFzcyBQYXR0ZXJuTWlzc2luZ1RlbXBvcmFyeToKICAgIEBzdGF0aWNtZXRob2QKICAgIGRlZiBkZXRlY3QodGV4dDogc3RyKSAtPiBsaXN0W2RpY3RdOgogICAgICAgIGZpbmRpbmdzID0gW10KICAgICAgICBsaW5lcyA9IHRleHQuc3BsaXRsaW5lcygpCiAgICAgICAgZm9yIGksIGxpbmUgaW4gZW51bWVyYXRlKGxpbmVzKToKICAgICAgICAgICAgIyBWYXJpYWJsZSBkZWNsYXJlZCB3aXRoIFRlbXAgcHJlZml4IGJ1dCBubyAndGVtcG9yYXJ5JyBrZXl3b3JkCiAgICAgICAgICAgIGlmIHJlLnNlYXJjaChyJ1xiVGVtcFx3K1xzKjpccypSZWNvcmRccysnLCBsaW5lKSBhbmQgJ3RlbXBvcmFyeScgbm90IGluIGxpbmUgYW5kIG5vdCBsaW5lLnN0cmlwKCkuc3RhcnRzd2l0aCgnLy8nKToKICAgICAgICAgICAgICAgIGZpbmRpbmdzLmFwcGVuZCh7CiAgICAgICAgICAgICAgICAgICAgImxpbmUiOiBpICsgMSwKICAgICAgICAgICAgICAgICAgICAic25pcHBldCI6IGxpbmUuc3RyaXAoKVs6MTIwXSwKICAgICAgICAgICAgICAgICAgICAibWVzc2FnZSI6ICJWYXJpYWJsZSB3aXRoICdUZW1wJyBwcmVmaXggaXMgbWlzc2luZyAndGVtcG9yYXJ5JyBrZXl3b3JkIgogICAgICAgICAgICAgICAgfSkKICAgICAgICByZXR1cm4gZmluZGluZ3MKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZml4KHRleHQ6IHN0cikgLT4gc3RyOgogICAgICAgIHJldHVybiByZS5zdWIoCiAgICAgICAgICAgIHInKFRlbXBcdytccyo6XHMqUmVjb3JkXHMrW147XSspOyg/IVxzKi8vLip0ZW1wb3JhcnkpJywKICAgICAgICAgICAgbGFtYmRhIG06IG0uZ3JvdXAoMCkucnN0cmlwKCc7JykgKyAnIHRlbXBvcmFyeTsnIGlmICd0ZW1wb3JhcnknIG5vdCBpbiBtLmdyb3VwKDApIGVsc2UgbS5ncm91cCgwKSwKICAgICAgICAgICAgdGV4dAogICAgICAgICkKCgojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgAojIFBBVFRFUk4gMTIg4oCUIFRlbXAgdGFibGUgdXNlZCBhcyBEaWN0aW9uYXJ5ICh1c2UgRGljdGlvbmFyeSB0eXBlKQojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgApAX3JlZ2lzdGVyKAogICAgaWQ9IlRFTVBfVEFCTEVfQVNfRElDVCIsCiAgICBncm91cD0iTWVtb3J5IC8gQ29waWVzIiwKICAgIHNldmVyaXR5PSJNRURJVU0iLAogICAgdGl0bGU9IlRlbXAgUmVjb3JkIHRhYmxlIHVzZWQgYXMgYSBrZXktdmFsdWUgbG9va3VwICh1c2UgRGljdGlvbmFyeSBpbnN0ZWFkKSIsCiAgICBkZXNjcmlwdGlvbj0oIkV2ZW4gYSAndGVtcG9yYXJ5JyB0YWJsZSBjYXJyaWVzIHJlY29yZC1sYXllciBvdmVyaGVhZDoga2V5IGluZGV4ZXMsIHRyYW5zYWN0aW9uIHN0YXRlIHRyYWNraW5nLCBzY2hlbWEgdmFsaWRhdGlvbiBvbiBldmVyeSBvcGVyYXRpb24uIEZvciBzaW1wbGUga2V54oaSdmFsdWUgbWFwcGluZyAoZS5nLiBDdXN0b21lck5vIOKGkiBTYWxlc3BlcnNvbkNvZGUpLCB0aGUgYnVpbHQtaW4gRGljdGlvbmFyeSBvZiBbSywgVl0gdHlwZSBoYXMgemVybyBEQiBvdmVyaGVhZC5cblxuU1FMIElNUEFDVDogVGVtcCB0YWJsZToga2V5IGluZGV4IG1haW50ZW5hbmNlLCByZWNvcmQgc3RydWN0IG92ZXJoZWFkIHBlciBlbnRyeS5cbkRpY3Rpb25hcnk6IGRpcmVjdCBoYXNoIG1hcCBsb29rdXAg4oCUIE8oMSkgR2V0L1NldCwgbm8gcmVjb3JkIG92ZXJoZWFkLlxuXG5CQUQ6ICBUZW1wQ3VzdG9tZXI6IFJlY29yZCBDdXN0b21lciB0ZW1wb3Jhcnk7IFRlbXBDdXN0b21lci5JbnNlcnQoKTsgVGVtcEN1c3RvbWVyLkdldCguLi4pO1xuR09PRDogUm91dGluZ01hcDogRGljdGlvbmFyeSBvZiBbQ29kZVsyMF0sIENvZGVbMjBdXTtcbiAgICAgIFJvdXRpbmdNYXAuU2V0KC4uLiwgLi4uKTsgaWYgUm91dGluZ01hcC5Db250YWluc0tleSguLi4pIHRoZW4gLi4uXG5cbkhJTlQ6IENvbXBhcmUgR2V0L1NldCBvcGVyYXRpb25zIG9uIHRlbXAgdGFibGUgdnMgRGljdGlvbmFyeS4gTWVhc3VyZSB0aW1lIGZvciAxMCAwMDAgbG9va3VwcyBlYWNoIHdheS4iKSwKICAgIGV4ZXJjaXNlcz1bMTNdLAopCmNsYXNzIFBhdHRlcm5UZW1wVGFibGVBc0RpY3Q6CiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZGV0ZWN0KHRleHQ6IHN0cikgLT4gbGlzdFtkaWN0XToKICAgICAgICBmaW5kaW5ncyA9IFtdCiAgICAgICAgbGluZXMgPSB0ZXh0LnNwbGl0bGluZXMoKQogICAgICAgIGZvciBpLCBsaW5lIGluIGVudW1lcmF0ZShsaW5lcyk6CiAgICAgICAgICAgIG0gPSByZS5zZWFyY2gocicoVGVtcFx3Kylccyo6XHMqUmVjb3JkXHMrXFMrXHMrdGVtcG9yYXJ5JywgbGluZSkKICAgICAgICAgICAgaWYgbm90IG06CiAgICAgICAgICAgICAgICBjb250aW51ZQogICAgICAgICAgICB2YXIgPSBtLmdyb3VwKDEpCiAgICAgICAgICAgICMgTG9vayBhaGVhZCBmb3IgYm90aCBJbnNlcnQgYW5kIEdldCBvbiBzYW1lIHZhciDigJQgY2xhc3NpYyBkaWN0IHBhdHRlcm4KICAgICAgICAgICAgcmVzdCA9ICdcbicuam9pbihsaW5lc1tpOm1pbihsZW4obGluZXMpLCBpKzYwKV0pCiAgICAgICAgICAgIGlmIHJlLnNlYXJjaChyZidcYntyZS5lc2NhcGUodmFyKX1cLkluc2VydFwoJywgcmVzdCkgYW5kIHJlLnNlYXJjaChyZidcYntyZS5lc2NhcGUodmFyKX1cLkdldFwoJywgcmVzdCk6CiAgICAgICAgICAgICAgICBmaW5kaW5ncy5hcHBlbmQoewogICAgICAgICAgICAgICAgICAgICJsaW5lIjogaSArIDEsCiAgICAgICAgICAgICAgICAgICAgInNuaXBwZXQiOiBsaW5lLnN0cmlwKClbOjEyMF0sCiAgICAgICAgICAgICAgICAgICAgIm1lc3NhZ2UiOiBmIid7dmFyfScgdXNlZCBhcyBrZXktdmFsdWUgc3RvcmUgKEluc2VydCtHZXQgcGF0dGVybikg4oCUIHByZWZlciBEaWN0aW9uYXJ5IG9mIFsuLi5dIgogICAgICAgICAgICAgICAgfSkKICAgICAgICByZXR1cm4gZmluZGluZ3MKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZml4KHRleHQ6IHN0cikgLT4gc3RyOgogICAgICAgIHJldHVybiB0ZXh0CgoKIyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKIyBQQVRURVJOIDEzIOKAlCBGaW5kRmlyc3QoKSB1c2VkIGZvciBsb29wIGl0ZXJhdGlvbgojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgApAX3JlZ2lzdGVyKAogICAgaWQ9IkZJTkRGSVJTVF9JTl9MT09QIiwKICAgIGdyb3VwPSJEYXRhIFRyYW5zZmVyIiwKICAgIHNldmVyaXR5PSJISUdIIiwKICAgIHRpdGxlPSJGaW5kRmlyc3QoKSB1c2VkIGZvciBtdWx0aS1yb3cgaXRlcmF0aW9uIChidWZmZXIgc2l6ZSAxKSIsCiAgICBkZXNjcmlwdGlvbj0oJ0ZpbmRGaXJzdCgpIGdlbmVyYXRlcyBTRUxFQ1QgVE9QIDEg4oCUIGZldGNoZXMgZXhhY3RseSBvbmUgcmVjb3JkLiBXaGVuIHVzZWQgdG8gc3RhcnQgYSBsb29wIChjYWxsaW5nIE5leHQoKSByZXBlYXRlZGx5KSwgZXZlcnkgTmV4dCgpIGNhbGwgdGhhdCBleGhhdXN0cyB0aGUgYnVmZmVyIGZpcmVzIGEgbmV3IFNFTEVDVCBUT1AgNTAuIEZpbmRTZXQoKSBwcmUtZmV0Y2hlcyA1MCByb3dzIGltbWVkaWF0ZWx5IOKAlCBmYXIgZmV3ZXIgU1FMIHF1ZXJpZXMuXG5cblNRTCBJTVBBQ1Q6IEZpbmRGaXJzdDogU0VMRUNUIFRPUCAxIC4uLiB0aGVuIFNFTEVDVCBUT1AgNTAgcGVyIGJhdGNoIGluIE5leHQoKS5cbkZpbmRTZXQ6ICAgU2luZ2xlIFNFTEVDVCB0aGF0IGVzdGFibGlzaGVzIHRoZSBmdWxsIHJlYWQgY3Vyc29yLlxuXG5CQUQ6ICBpZiBSZWNvcmQuRmluZEZpcnN0KCkgdGhlbiByZXBlYXQgLi4uIHVudGlsIFJlY29yZC5OZXh0KCkgPSAwO1xuR09PRDogaWYgUmVjb3JkLkZpbmRTZXQoKSB0aGVuIHJlcGVhdCAuLi4gdW50aWwgUmVjb3JkLk5leHQoKSA9IDA7XG5cbkhJTlQ6IFRyYWNlIFNRTCBxdWVyaWVzIGZvciBhIDUwMC1yb3cgbG9vcCB3aXRoIEZpbmRGaXJzdC4gRmluZEZpcnN0IGZpcmVzIFNFTEVDVCBUT1AgMSBmaXJzdCwgdGhlbiBOZXh0IGJhdGNoZXMuIEZpbmRTZXQgZmlyZXMgb25lIFNFTEVDVCBhbmQgaGFuZGxlcyBiYXRjaGluZyBpbnRlcm5hbGx5LicpLAogICAgZXhlcmNpc2VzPVsxNF0sCikKY2xhc3MgUGF0dGVybkZpbmRGaXJzdEluTG9vcDoKICAgIEBzdGF0aWNtZXRob2QKICAgIGRlZiBkZXRlY3QodGV4dDogc3RyKSAtPiBsaXN0W2RpY3RdOgogICAgICAgIGZpbmRpbmdzID0gW10KICAgICAgICBsaW5lcyA9IHRleHQuc3BsaXRsaW5lcygpCiAgICAgICAgZm9yIGksIGxpbmUgaW4gZW51bWVyYXRlKGxpbmVzKToKICAgICAgICAgICAgbSA9IHJlLnNlYXJjaChyJyhcdyspXC5GaW5kRmlyc3RcKFwpJywgbGluZSkKICAgICAgICAgICAgaWYgbm90IG0gb3IgbGluZS5zdHJpcCgpLnN0YXJ0c3dpdGgoJy8vJyk6CiAgICAgICAgICAgICAgICBjb250aW51ZQogICAgICAgICAgICB2YXIgPSBtLmdyb3VwKDEpCiAgICAgICAgICAgICMgTG9vayBhaGVhZCBmb3IgTmV4dCgpIG9uIHNhbWUgdmFyCiAgICAgICAgICAgIGFoZWFkID0gJ1xuJy5qb2luKGxpbmVzW2k6bWluKGxlbihsaW5lcyksIGkrMjApXSkKICAgICAgICAgICAgaWYgcmUuc2VhcmNoKHJmJ1xie3JlLmVzY2FwZSh2YXIpfVwuTmV4dFwoXCknLCBhaGVhZCk6CiAgICAgICAgICAgICAgICBmaW5kaW5ncy5hcHBlbmQoewogICAgICAgICAgICAgICAgICAgICJsaW5lIjogaSArIDEsCiAgICAgICAgICAgICAgICAgICAgInNuaXBwZXQiOiBsaW5lLnN0cmlwKClbOjEyMF0sCiAgICAgICAgICAgICAgICAgICAgIm1lc3NhZ2UiOiBmIid7dmFyfS5GaW5kRmlyc3QoKScgZm9sbG93ZWQgYnkgTmV4dCgpIOKAlCB1c2UgRmluZFNldCgpIGZvciBtdWx0aS1yb3cgaXRlcmF0aW9uIgogICAgICAgICAgICAgICAgfSkKICAgICAgICByZXR1cm4gZmluZGluZ3MKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZml4KHRleHQ6IHN0cikgLT4gc3RyOgogICAgICAgIGRlZiByZXBsYWNlKG0pOgogICAgICAgICAgICB2YXIgPSBtLmdyb3VwKDEpCiAgICAgICAgICAgIHBvcyA9IG0uZW5kKCkKICAgICAgICAgICAgYWhlYWQgPSB0ZXh0W3Bvczpwb3MrMzAwXQogICAgICAgICAgICBpZiByZS5zZWFyY2gocmYnXGJ7cmUuZXNjYXBlKHZhcil9XC5OZXh0XChcKScsIGFoZWFkKToKICAgICAgICAgICAgICAgIHJldHVybiBmInt2YXJ9LkZpbmRTZXQoKSIKICAgICAgICAgICAgcmV0dXJuIG0uZ3JvdXAoMCkKICAgICAgICByZXR1cm4gcmUuc3ViKHInKFx3KylcLkZpbmRGaXJzdFwoXCknLCByZXBsYWNlLCB0ZXh0KQoKCiMg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACiMgUEFUVEVSTiAxNCDigJQgRmluZFNldCBndWFyZCBiZWZvcmUgTW9kaWZ5QWxsICh1bm5lY2Vzc2FyeSkKIyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKQF9yZWdpc3RlcigKICAgIGlkPSJGSU5EU0VUX0JFRk9SRV9NT0RJRllBTEwiLAogICAgZ3JvdXA9IkJ1bGsgT3BlcmF0aW9ucyIsCiAgICBzZXZlcml0eT0iTUVESVVNIiwKICAgIHRpdGxlPSJGaW5kU2V0KCkgZ3VhcmQgYmVmb3JlIE1vZGlmeUFsbCgpIOKAlCB1bm5lY2Vzc2FyeSAoZG91YmxlIHNjYW4pIiwKICAgIGRlc2NyaXB0aW9uPSgnQ2FsbGluZyBGaW5kU2V0KCkgaW1tZWRpYXRlbHkgYmVmb3JlIE1vZGlmeUFsbCgpIGZpcmVzIGFuIGV4dHJhIFNFTEVDVCBqdXN0IHRvIGNvbmZpcm0gcm93cyBleGlzdC4gTW9kaWZ5QWxsKCkgYWxyZWFkeSBoYW5kbGVzIGVtcHR5IHJlc3VsdCBzZXRzIGdyYWNlZnVsbHkg4oCUIGl0IGRvZXMgbm90aGluZyBpZiBubyByb3dzIG1hdGNoIHRoZSBjdXJyZW50IGZpbHRlcnMuIFRoZSBGaW5kU2V0IGlzIHB1cmUgb3ZlcmhlYWQgd2l0aCB6ZXJvIGZ1bmN0aW9uYWwgYmVuZWZpdC5cblxuU1FMIElNUEFDVDogRXh0cmEgcXVlcnk6IFNFTEVDVCBUT1AgNTAgLi4uIChvbmx5IHRvIGNoZWNrIGV4aXN0ZW5jZSkuXG5UaGVuOiBVUERBVEUgLi4uIFdIRVJFIC4uLiAoTW9kaWZ5QWxsIOKAlCBkb2VzIHRoaXMgYW55d2F5KS5cbldpdGggRmluZFNldDogMiBxdWVyaWVzIChTRUxFQ1QgKyBVUERBVEUpLiBXaXRob3V0OiAxIHF1ZXJ5IChVUERBVEUgb25seSkuXG5cbkJBRDogIGlmIFJlY29yZC5GaW5kU2V0KCkgdGhlbiBSZWNvcmQuTW9kaWZ5QWxsKC4uLik7XG5HT09EOiBSZWNvcmQuTW9kaWZ5QWxsKC4uLik7XG5cbkhJTlQ6IExvb2sgYXQgU1FMIGJlZm9yZSBNb2RpZnlBbGwgd2l0aCBhbmQgd2l0aG91dCBGaW5kU2V0LiBXaXRoIEZpbmRTZXQ6IDIgcXVlcmllcy4gV2l0aG91dDogMSBxdWVyeS4nKSwKICAgIGV4ZXJjaXNlcz1bMTVdLAopCmNsYXNzIFBhdHRlcm5GaW5kU2V0QmVmb3JlTW9kaWZ5QWxsOgogICAgX1JFID0gcmUuY29tcGlsZShyJ2lmXHMrKFx3KylcLkZpbmRTZXRcKFwpXHMrdGhlblxzK1xuP1xzK1wxXC5Nb2RpZnlBbGxcKCcsIHJlLk1VTFRJTElORSkKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZGV0ZWN0KHRleHQ6IHN0cikgLT4gbGlzdFtkaWN0XToKICAgICAgICByZXR1cm4gX2ZpbmRpbmdzKHRleHQsIFBhdHRlcm5GaW5kU2V0QmVmb3JlTW9kaWZ5QWxsLl9SRSwKICAgICAgICAgICAgICAgICAgICAgICAgIGxhbWJkYSBtOiBmIkZpbmRTZXQoKSBndWFyZCBiZWZvcmUgTW9kaWZ5QWxsKCkgb24gJ3ttLmdyb3VwKDEpfScgaXMgcmVkdW5kYW50IikKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZml4KHRleHQ6IHN0cikgLT4gc3RyOgogICAgICAgIHJldHVybiByZS5zdWIoCiAgICAgICAgICAgIHInaWZccysoXHcrKVwuRmluZFNldFwoXClccyt0aGVuXHMqXG4oXHMrKVwxXC5Nb2RpZnlBbGxcKCcsCiAgICAgICAgICAgIHInXDJcMS5Nb2RpZnlBbGwoJywKICAgICAgICAgICAgdGV4dAogICAgICAgICkKCgojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgAojIFBBVFRFUk4gMTUg4oCUIFNldEZpbHRlciBvbiBGbG93RmllbGQgKHVzZSBTZXRBdXRvQ2FsY0ZpZWxkcyArIEFMIGZpbHRlcikKIyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKQF9yZWdpc3RlcigKICAgIGlkPSJTRVRGSUxURVJfT05fRkxPV0ZJRUxEIiwKICAgIGdyb3VwPSJGbG93RmllbGRzIiwKICAgIHNldmVyaXR5PSJISUdIIiwKICAgIHRpdGxlPSJTZXRGaWx0ZXIgb24gRmxvd0ZpZWxkIGNhdXNlcyBjb3JyZWxhdGVkIHN1YnF1ZXJ5IHBlciByb3ciLAogICAgZGVzY3JpcHRpb249KCJTZXRGaWx0ZXIgb24gYSBGbG93RmllbGQgKGUuZy4gQmFsYW5jZSwgd2hpY2ggaXMgYSBDYWxjRmllbGQpIGNhbm5vdCBiZSBwdXNoZWQgdG8gdGhlIFNRTCBXSEVSRSBjbGF1c2Ug4oCUIGl0IGhhcyBubyBwaHlzaWNhbCBjb2x1bW4uIEJDIG11c3QgZmV0Y2ggQUxMIHJvd3MgbWF0Y2hpbmcgdGhlIG90aGVyIGZpbHRlcnMsIHRoZW4gY29tcHV0ZSB0aGUgRmxvd0ZpZWxkIGZvciBlYWNoIHJvdyBpbmRpdmlkdWFsbHkgYW5kIGZpbHRlciBpbiBBTC4gRnVsbCB0YWJsZSBzY2FuLlxuXG5TUUwgSU1QQUNUOiBTRUxFQ1QgYWxsIHJvd3MgLi4uIHRoZW4gcGVyIHJvdzogU0VMRUNUIFNVTSguLi4pIGZvciBGbG93RmllbGQuXG5TUUwgY2Fubm90IGZpbHRlciBvbiBhIGNvbXB1dGVkIHZhbHVlIGl0IGhhc24ndCBjYWxjdWxhdGVkIHlldC5cblxuQkFEOiAgQ3VzdG9tZXIuU2V0RmlsdGVyKEJhbGFuY2UsICc+JTEnLCAwKTsgaWYgQ3VzdG9tZXIuRmluZFNldCgpIHRoZW4gLi4uXG5HT09EOiBDdXN0b21lci5TZXRBdXRvQ2FsY0ZpZWxkcyhCYWxhbmNlKTtcbiAgICAgIGlmIEN1c3RvbWVyLkZpbmRTZXQoKSB0aGVuIHJlcGVhdCBpZiBDdXN0b21lci5CYWxhbmNlID4gMCB0aGVuIC4uLlxuXG5ISU5UOiBUcnkgU2V0RmlsdGVyKEJhbGFuY2UsICc+MTAwMCcpIOKAlCBjaGVjayB0aGUgU1FMIFdIRVJFLiBCYWxhbmNlIHdvbid0IGFwcGVhciBpbiBXSEVSRSDigJQgYWxsIHJvd3MgZmV0Y2hlZC4gQ29tcGFyZSB3aXRoIGZpbHRlcmluZyBvbiBhIG5vcm1hbCBpbmRleGVkIGRlY2ltYWwgZmllbGQuIiksCiAgICBleGVyY2lzZXM9WzE2XSwKKQpjbGFzcyBQYXR0ZXJuU2V0RmlsdGVyT25GbG93RmllbGQ6CiAgICAjIENvbW1vbiBGbG93RmllbGRzIGluIEJDCiAgICBfRkxPV0ZJRUxEUyA9IHsnQmFsYW5jZScsICdCYWxhbmNlIChMQ1kpJywgJ1NhbGVzIChMQ1kpJywgJ1B1cmNoYXNlcyAoTENZKScsCiAgICAgICAgICAgICAgICAgICAnT3V0c3RhbmRpbmcgT3JkZXJzJywgJ091dHN0YW5kaW5nIEludm9pY2VzJywgJ05ldCBDaGFuZ2UnfQogICAgX1JFID0gcmUuY29tcGlsZShyJyhcdyspXC5TZXRGaWx0ZXJcKChbXiwpXSspLCcsIHJlLk1VTFRJTElORSkKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZGV0ZWN0KHRleHQ6IHN0cikgLT4gbGlzdFtkaWN0XToKICAgICAgICBmaW5kaW5ncyA9IFtdCiAgICAgICAgZm9yIG0gaW4gUGF0dGVyblNldEZpbHRlck9uRmxvd0ZpZWxkLl9SRS5maW5kaXRlcih0ZXh0KToKICAgICAgICAgICAgZmllbGQgPSBtLmdyb3VwKDIpLnN0cmlwKCkuc3RyaXAoJyInKQogICAgICAgICAgICBpZiBmaWVsZCBpbiBQYXR0ZXJuU2V0RmlsdGVyT25GbG93RmllbGQuX0ZMT1dGSUVMRFM6CiAgICAgICAgICAgICAgICBsaW5lID0gX2ZpbmRfbGluZSh0ZXh0LCBtLnN0YXJ0KCkpCiAgICAgICAgICAgICAgICBmaW5kaW5ncy5hcHBlbmQoewogICAgICAgICAgICAgICAgICAgICJsaW5lIjogbGluZSwKICAgICAgICAgICAgICAgICAgICAic25pcHBldCI6IG0uZ3JvdXAoMClbOjEyMF0sCiAgICAgICAgICAgICAgICAgICAgIm1lc3NhZ2UiOiBmIlNldEZpbHRlciBvbiBGbG93RmllbGQgJ3tmaWVsZH0nIOKAlCB1c2UgU2V0QXV0b0NhbGNGaWVsZHMgKyBBTC1zaWRlIGZpbHRlciIKICAgICAgICAgICAgICAgIH0pCiAgICAgICAgcmV0dXJuIGZpbmRpbmdzCgogICAgQHN0YXRpY21ldGhvZAogICAgZGVmIGZpeCh0ZXh0OiBzdHIpIC0+IHN0cjoKICAgICAgICByZXR1cm4gdGV4dAoKCiMg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACiMgUEFUVEVSTiAxNiDigJQgT1IgLyBBTkQgYm90aCBzaWRlcyBhbHdheXMgZXZhbHVhdGVkICh1c2UgbmVzdGVkIElGKQojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgApAX3JlZ2lzdGVyKAogICAgaWQ9IkVBR0VSX0VWQUxVQVRJT05fT1JfQU5EIiwKICAgIGdyb3VwPSJTaG9ydC1DaXJjdWl0IEV2YWx1YXRpb24iLAogICAgc2V2ZXJpdHk9Ik1FRElVTSIsCiAgICB0aXRsZT0iQUwgJ29yJy8nYW5kJyBldmFsdWF0ZXMgYm90aCBzaWRlcyBlYWdlcmx5IOKAlCB1c2UgbmVzdGVkIElGIGZvciBleHBlbnNpdmUgY2FsbHMiLAogICAgZGVzY3JpcHRpb249KCdBTCBldmFsdWF0ZXMgQk9USCBzaWRlcyBvZiBPUi9BTkQgcmVnYXJkbGVzcyBvZiB0aGUgbGVmdCBzaWRlIHJlc3VsdC4gSWYgdGhlIGxlZnQgY29uZGl0aW9uIGNhbGxzIGFuIGV4cGVuc2l2ZSBmdW5jdGlvbiAoRmluZFNldCwgQ2FsY0ZpZWxkcyksIHRoZSByaWdodCBzaWRlIHN0aWxsIHJ1bnMgZXZlbiB3aGVuIGxlZnQgYWxyZWFkeSBkZXRlcm1pbmVzIHRoZSByZXN1bHQuIE5lc3RlZCBJRiBzdGF0ZW1lbnRzIHByb3ZpZGUgcHJvcGVyIHNob3J0LWNpcmN1aXQgYmVoYXZpb3IuXG5cblNRTCBJTVBBQ1Q6IE5vIFNRTCBjaGFuZ2Ug4oCUIGJvdGggZnVuY3Rpb24gY2FsbHMgZXhlY3V0ZSB1bmNvbmRpdGlvbmFsbHkuXG5XaXRoIG5lc3RlZCBJRjogZXhwZW5zaXZlIHJpZ2h0LXNpZGUgY2FsbCBza2lwcGVkIHdoZW4gbGVmdCBpcyB0cnVlL2ZhbHNlLlxuXG5CQUQ6ICBpZiBFeHBlbnNpdmVBKCkgb3IgRXhwZW5zaXZlQigpIHRoZW5cbkdPT0Q6IGlmIEV4cGVuc2l2ZUEoKSB0aGVuIFByb2Nlc3NSZXN1bHQoKVxuICAgICAgZWxzZSBpZiBFeHBlbnNpdmVCKCkgdGhlbiBQcm9jZXNzUmVzdWx0KCk7XG5cbkhJTlQ6IEFkZCB0cmFjZSBtZXNzYWdlcyBpbnNpZGUgZWFjaCBleHBlbnNpdmUgZnVuY3Rpb24uIENvdW50IGhvdyBtYW55IHRpbWVzIGVhY2ggaXMgY2FsbGVkIHdpdGggT1IgdnMgbmVzdGVkIElGLiBSaWdodC1zaWRlIGZ1bmN0aW9uIHNob3VsZCBub3QgcnVuIHdoZW4gbGVmdCBpcyB0cnVlLicpLAogICAgZXhlcmNpc2VzPVsxNywgMTgsIDM5XSwKKQpjbGFzcyBQYXR0ZXJuRWFnZXJFdmFsdWF0aW9uOgogICAgIyBIZXVyaXN0aWM6IGZ1bmN0aW9uIGNhbGxzIGFzIG9wZXJhbmRzIG9mIG9yL2FuZAogICAgX1JFID0gcmUuY29tcGlsZShyJ1xiKFx3K1woW14pXSpcKSlccysob3J8YW5kKVxzKyhcdytcKFteKV0qXCkpJywgcmUuTVVMVElMSU5FIHwgcmUuSUdOT1JFQ0FTRSkKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZGV0ZWN0KHRleHQ6IHN0cikgLT4gbGlzdFtkaWN0XToKICAgICAgICBmaW5kaW5ncyA9IFtdCiAgICAgICAgZm9yIG0gaW4gUGF0dGVybkVhZ2VyRXZhbHVhdGlvbi5fUkUuZmluZGl0ZXIodGV4dCk6CiAgICAgICAgICAgIGlmIHRleHRbbWF4KDAsIG0uc3RhcnQoKS0yKTptLnN0YXJ0KCldLnN0cmlwKCkuc3RhcnRzd2l0aCgnLy8nKToKICAgICAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgICAgIGxpbmUgPSBfZmluZF9saW5lKHRleHQsIG0uc3RhcnQoKSkKICAgICAgICAgICAgbGluZV90ZXh0ID0gdGV4dC5zcGxpdGxpbmVzKClbbGluZS0xXS5zdHJpcCgpCiAgICAgICAgICAgIGlmIGxpbmVfdGV4dC5zdGFydHN3aXRoKCcvLycpOgogICAgICAgICAgICAgICAgY29udGludWUKICAgICAgICAgICAgZmluZGluZ3MuYXBwZW5kKHsKICAgICAgICAgICAgICAgICJsaW5lIjogbGluZSwKICAgICAgICAgICAgICAgICJzbmlwcGV0IjogbS5ncm91cCgwKVs6MTIwXSwKICAgICAgICAgICAgICAgICJtZXNzYWdlIjogZiJFYWdlciBldmFsdWF0aW9uOiAne20uZ3JvdXAoMikudXBwZXIoKX0nIGFsd2F5cyBldmFsdWF0ZXMgYm90aCBzaWRlcyDigJQgdXNlIG5lc3RlZCBJRiBvciAnY2FzZSB0cnVlIG9mJyIKICAgICAgICAgICAgfSkKICAgICAgICByZXR1cm4gZmluZGluZ3MKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZml4KHRleHQ6IHN0cikgLT4gc3RyOgogICAgICAgIHJldHVybiB0ZXh0ICAjIFJlcXVpcmVzIHVuZGVyc3RhbmRpbmcgb2YgaW50ZW50CgoKIyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKIyBQQVRURVJOIDE3IOKAlCBTZXRSYW5nZS9maWx0ZXIgbXV0YXRpb24gaW5zaWRlIEZpbmRTZXQgbG9vcAojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgApAX3JlZ2lzdGVyKAogICAgaWQ9IkZJTFRFUl9NVVRBVElPTl9JTl9MT09QIiwKICAgIGdyb3VwPSJDdXJzb3IgU2FmZXR5IiwKICAgIHNldmVyaXR5PSJISUdIIiwKICAgIHRpdGxlPSJTZXRSYW5nZS9TZXRGaWx0ZXIgaW5zaWRlIEZpbmRTZXQgbG9vcCBjb3JydXB0cyBjdXJzb3IiLAogICAgZGVzY3JpcHRpb249KCJTZXR0aW5nIGEgbmV3IGZpbHRlciBpbnNpZGUgYSBGaW5kU2V0IGxvb3AgbW9kaWZpZXMgdGhlIHJlY29yZCB2YXJpYWJsZSdzIGZpbHRlciBzdGF0ZS4gTmV4dCgpIGNvbnRpbnVlcyBvbiB0aGUgT0xEIGN1cnNvciwgbm90IHRoZSBuZXcgZmlsdGVyLiBSZXN1bHQ6IHJlY29yZHMgbWF5IGJlIG1pc3NlZCwgd3JvbmcgZGF0YSByZXR1cm5lZCwgb3IgaW5maW5pdGUgbG9vcC4gVGhlIGN1cnNvciBwb3NpdGlvbiBiZWNvbWVzIHVuZGVmaW5lZCBhZnRlciBhIFNldFJhbmdlIG1pZC1sb29wLlxuXG5TUUwgSU1QQUNUOiBDdXJzb3IgZXN0YWJsaXNoZWQgYnkgRmluZFNldCwgdGhlbiBmaWx0ZXIgY2hhbmdlZCDihpIgTmV4dCgpIHVuZGVmaW5lZCBiZWhhdmlvci4gSGFyZCB0byByZXByb2R1Y2UgYnVncy5cblxuQkFEOiAgaWYgUmVjLkZpbmRTZXQoKSB0aGVuIHJlcGVhdCBSZWMuU2V0UmFuZ2UoRmllbGQsIFJlYy5GaWVsZCk7IHVudGlsIFJlYy5OZXh0KCk9MDtcbkdPT0Q6IENvbGxlY3Qga2V5cyBpbnRvIGEgTGlzdCBmaXJzdCwgdGhlbiBwcm9jZXNzIHdpdGggR2V0KCkuXG5cbkhJTlQ6IFNldCBhIGJyZWFrcG9pbnQgaW5zaWRlIHRoZSBsb29wIGFmdGVyIFNldFJhbmdlLiBXYXRjaCB3aGljaCByZWNvcmRzIE5leHQoKSByZXR1cm5zIGFmdGVyIHRoZSBmaWx0ZXIgY2hhbmdlLiIpLAogICAgZXhlcmNpc2VzPVsxOV0sCikKY2xhc3MgUGF0dGVybkZpbHRlck11dGF0aW9uSW5Mb29wOgogICAgQHN0YXRpY21ldGhvZAogICAgZGVmIGRldGVjdCh0ZXh0OiBzdHIpIC0+IGxpc3RbZGljdF06CiAgICAgICAgZmluZGluZ3MgPSBbXQogICAgICAgIGxpbmVzID0gdGV4dC5zcGxpdGxpbmVzKCkKICAgICAgICBpbl9sb29wID0gRmFsc2UKICAgICAgICBsb29wX3ZhciA9IE5vbmUKICAgICAgICBmb3IgaSwgbGluZSBpbiBlbnVtZXJhdGUobGluZXMpOgogICAgICAgICAgICBtID0gcmUuc2VhcmNoKHInKFx3KylcLkZpbmRTZXQnLCBsaW5lKQogICAgICAgICAgICBpZiBtIGFuZCBub3QgbGluZS5zdHJpcCgpLnN0YXJ0c3dpdGgoJy8vJyk6CiAgICAgICAgICAgICAgICBpbl9sb29wID0gVHJ1ZQogICAgICAgICAgICAgICAgbG9vcF92YXIgPSBtLmdyb3VwKDEpCiAgICAgICAgICAgIGlmIGluX2xvb3AgYW5kIGxvb3BfdmFyOgogICAgICAgICAgICAgICAgaWYgcmUuc2VhcmNoKHJmJ1xie3JlLmVzY2FwZShsb29wX3Zhcil9XC4oU2V0UmFuZ2V8U2V0RmlsdGVyKVwoJywgbGluZSkgYW5kIG5vdCBsaW5lLnN0cmlwKCkuc3RhcnRzd2l0aCgnLy8nKToKICAgICAgICAgICAgICAgICAgICBmaW5kaW5ncy5hcHBlbmQoewogICAgICAgICAgICAgICAgICAgICAgICAibGluZSI6IGkgKyAxLAogICAgICAgICAgICAgICAgICAgICAgICAic25pcHBldCI6IGxpbmUuc3RyaXAoKVs6MTIwXSwKICAgICAgICAgICAgICAgICAgICAgICAgIm1lc3NhZ2UiOiBmIid7bG9vcF92YXJ9LlNldFJhbmdlL1NldEZpbHRlcicgaW5zaWRlIEZpbmRTZXQgbG9vcCDigJQgY29ycnVwdHMgY3Vyc29yIgogICAgICAgICAgICAgICAgICAgIH0pCiAgICAgICAgICAgIGlmIHJlLm1hdGNoKHIndW50aWxccytcdytcLk5leHRcKFwpJywgbGluZS5zdHJpcCgpLCByZS5JR05PUkVDQVNFKToKICAgICAgICAgICAgICAgIGluX2xvb3AgPSBGYWxzZQogICAgICAgICAgICAgICAgbG9vcF92YXIgPSBOb25lCiAgICAgICAgcmV0dXJuIGZpbmRpbmdzCgogICAgQHN0YXRpY21ldGhvZAogICAgZGVmIGZpeCh0ZXh0OiBzdHIpIC0+IHN0cjoKICAgICAgICByZXR1cm4gdGV4dAoKCiMg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACiMgUEFUVEVSTiAxOCDigJQgRmluZFNldCBiZWZvcmUgRGVsZXRlQWxsICh1bm5lY2Vzc2FyeSBndWFyZCkKIyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKQF9yZWdpc3RlcigKICAgIGlkPSJGSU5EU0VUX0JFRk9SRV9ERUxFVEVBTEwiLAogICAgZ3JvdXA9IkJ1bGsgT3BlcmF0aW9ucyIsCiAgICBzZXZlcml0eT0iTUVESVVNIiwKICAgIHRpdGxlPSJGaW5kU2V0KCkgZ3VhcmQgYmVmb3JlIERlbGV0ZUFsbCgpIGlzIHJlZHVuZGFudCIsCiAgICBkZXNjcmlwdGlvbj0oJ1NhbWUgYW50aS1wYXR0ZXJuIGFzIEZpbmRTZXQgYmVmb3JlIE1vZGlmeUFsbCDigJQgY2FsbGluZyBGaW5kU2V0KCkgYmVmb3JlIERlbGV0ZUFsbCgpIGZpcmVzIGFuIGV4dHJhIFNFTEVDVCBqdXN0IHRvIHZlcmlmeSByb3dzIGV4aXN0LiBEZWxldGVBbGwoKSBhbHJlYWR5IGhhbmRsZXMgZW1wdHkgcmVzdWx0IHNldHMg4oCUIGl0IGRvZXMgbm90aGluZy5cblxuU1FMIElNUEFDVDogRXh0cmEgcXVlcnk6IFNFTEVDVCBUT1AgNTAgLi4uIChvbmx5IHRvIGNoZWNrIGV4aXN0ZW5jZSkuXG5UaGVuOiBERUxFVEUgRlJPTSAuLi4gV0hFUkUgLi4uIChEZWxldGVBbGwg4oCUIGRvZXMgdGhpcyBhbnl3YXkpLlxuRmluZFNldCBiZWZvcmUgRGVsZXRlQWxsID0gMiBTUUwgcXVlcmllcy4gSnVzdCBEZWxldGVBbGwgPSAxIFNRTCBERUxFVEUgc3RhdGVtZW50LlxuXG5CQUQ6ICBpZiBSZWNvcmQuRmluZFNldCgpIHRoZW4gUmVjb3JkLkRlbGV0ZUFsbCgpO1xuR09PRDogUmVjb3JkLkRlbGV0ZUFsbCgpO1xuXG5ISU5UOiBTYW1lIGFzIEZpbmRTZXQgYmVmb3JlIE1vZGlmeUFsbCBidXQgZm9yIERlbGV0ZUFsbC4gV2l0aCBGaW5kU2V0OiAyIHF1ZXJpZXMuIFdpdGhvdXQ6IDEgREVMRVRFIHN0YXRlbWVudC4nKSwKICAgIGV4ZXJjaXNlcz1bMjBdLAopCmNsYXNzIFBhdHRlcm5GaW5kU2V0QmVmb3JlRGVsZXRlQWxsOgogICAgX1JFID0gcmUuY29tcGlsZShyJ2lmXHMrKFx3KylcLkZpbmRTZXRcKFwpXHMrdGhlblxzKlxuP1xzKlwxXC5EZWxldGVBbGxcKCcsIHJlLk1VTFRJTElORSkKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZGV0ZWN0KHRleHQ6IHN0cikgLT4gbGlzdFtkaWN0XToKICAgICAgICByZXR1cm4gX2ZpbmRpbmdzKHRleHQsIFBhdHRlcm5GaW5kU2V0QmVmb3JlRGVsZXRlQWxsLl9SRSwKICAgICAgICAgICAgICAgICAgICAgICAgIGxhbWJkYSBtOiBmIkZpbmRTZXQoKSBndWFyZCBiZWZvcmUgRGVsZXRlQWxsKCkgb24gJ3ttLmdyb3VwKDEpfScgaXMgcmVkdW5kYW50IikKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZml4KHRleHQ6IHN0cikgLT4gc3RyOgogICAgICAgIHJldHVybiByZS5zdWIoCiAgICAgICAgICAgIHInaWZccysoXHcrKVwuRmluZFNldFwoXClccyt0aGVuXHMqXG4oXHMrKVwxXC5EZWxldGVBbGxcKCcsCiAgICAgICAgICAgIHInXDJcMS5EZWxldGVBbGwoJywKICAgICAgICAgICAgdGV4dAogICAgICAgICkKCgojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgAojIFBBVFRFUk4gMTkg4oCUIERlbGV0ZUFsbCB3aXRob3V0IElzRW1wdHkgZ3VhcmQgb24gbG9jay1zZW5zaXRpdmUgcmVjb3JkcwojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgApAX3JlZ2lzdGVyKAogICAgaWQ9IkRFTEVURUFMTF9OT19JU0VNUFRZX0dVQVJEIiwKICAgIGdyb3VwPSJMb2NraW5nIiwKICAgIHNldmVyaXR5PSJMT1ciLAogICAgdGl0bGU9IkRlbGV0ZUFsbCgpIHdpdGhvdXQgSXNFbXB0eSBndWFyZCBhY3F1aXJlcyB3cml0ZSBsb2NrIGV2ZW4gd2hlbiBlbXB0eSIsCiAgICBkZXNjcmlwdGlvbj0oJ0RlbGV0ZUFsbCgpIG9uIGFuIGVtcHR5IChvciBsaWtlbHktZW1wdHkpIHRhYmxlIHN0aWxsIGZpcmVzIGEgREVMRVRFIEZST00gLi4uIFNRTCBzdGF0ZW1lbnQg4oCUIGFjcXVpcmluZyBhIHdyaXRlIGxvY2sgZXZlbiBpZiB0aGVyZSBhcmUgemVybyByb3dzIHRvIGRlbGV0ZS4gVW5kZXIgaGlnaCBjb25jdXJyZW5jeSwgdW5uZWNlc3NhcnkgbG9jayBhY3F1aXNpdGlvbiBjYXVzZXMgY29udGVudGlvbi5cblxuU1FMIElNUEFDVDogREVMRVRFIEZST00gLi4uIFdIRVJFIC4uLiDihpIgYWNxdWlyZXMgd3JpdGUgbG9jayBldmVuIGlmIDAgcm93cyBhZmZlY3RlZC5cbklzRW1wdHkgZ3VhcmQ6IElGIEVYSVNUUyBjaGVjayBmaXJzdCDihpIgc2tpcCBERUxFVEUgaWYgZW1wdHkuXG5cbkNPTlNJREVSOiBpZiBub3QgUmVjb3JkLklzRW1wdHkoKSB0aGVuIFJlY29yZC5EZWxldGVBbGwoZmFsc2UpO1xuXG5ISU5UOiBDYWxsIERlbGV0ZUFsbCBvbiBhbiBlbXB0eSB0YWJsZSBpbiBTUUwgUHJvZmlsZXIuIFdpdGhvdXQgZ3VhcmQ6IERFTEVURSBmaXJlcyBldmVuIHdpdGggMCByb3dzLiBXaXRoIElzRW1wdHkgZ3VhcmQ6IERFTEVURSBza2lwcGVkIGVudGlyZWx5LicpLAogICAgZXhlcmNpc2VzPVsyMV0sCikKY2xhc3MgUGF0dGVybkRlbGV0ZUFsbE5vR3VhcmQ6CiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZGV0ZWN0KHRleHQ6IHN0cikgLT4gbGlzdFtkaWN0XToKICAgICAgICBmaW5kaW5ncyA9IFtdCiAgICAgICAgbGluZXMgPSB0ZXh0LnNwbGl0bGluZXMoKQogICAgICAgIGZvciBpLCBsaW5lIGluIGVudW1lcmF0ZShsaW5lcyk6CiAgICAgICAgICAgIG0gPSByZS5zZWFyY2gocicoXHcrKVwuRGVsZXRlQWxsXCgnLCBsaW5lKQogICAgICAgICAgICBpZiBub3QgbSBvciBsaW5lLnN0cmlwKCkuc3RhcnRzd2l0aCgnLy8nKToKICAgICAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgICAgIHZhciA9IG0uZ3JvdXAoMSkKICAgICAgICAgICAgIyBDaGVjayAyIGxpbmVzIGFib3ZlIGZvciBJc0VtcHR5IGd1YXJkIG9uIHNhbWUgdmFyCiAgICAgICAgICAgIGNvbnRleHQgPSAnXG4nLmpvaW4obGluZXNbbWF4KDAsIGktMik6aV0pCiAgICAgICAgICAgIGlmIGYne3Zhcn0uSXNFbXB0eScgbm90IGluIGNvbnRleHQ6CiAgICAgICAgICAgICAgICBmaW5kaW5ncy5hcHBlbmQoewogICAgICAgICAgICAgICAgICAgICJsaW5lIjogaSArIDEsCiAgICAgICAgICAgICAgICAgICAgInNuaXBwZXQiOiBsaW5lLnN0cmlwKClbOjEyMF0sCiAgICAgICAgICAgICAgICAgICAgIm1lc3NhZ2UiOiBmIkNvbnNpZGVyICdpZiBub3Qge3Zhcn0uSXNFbXB0eSgpIHRoZW4nIGd1YXJkIGJlZm9yZSBEZWxldGVBbGwoKSB0byBhdm9pZCBsb2NraW5nIG9uIGVtcHR5IHNldHMiCiAgICAgICAgICAgICAgICB9KQogICAgICAgIHJldHVybiBmaW5kaW5ncwoKICAgIEBzdGF0aWNtZXRob2QKICAgIGRlZiBmaXgodGV4dDogc3RyKSAtPiBzdHI6CiAgICAgICAgcmV0dXJuIHRleHQKCgojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgAojIFBBVFRFUk4gMjAg4oCUIENvdW50KCkgPSAxICJleGFjdGx5IG9uZSIgY2hlY2sgKHVzZSBGaW5kRmlyc3QgKyBOZXh0KQojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgApAX3JlZ2lzdGVyKAogICAgaWQ9IkNPVU5UX0VRVUFMU19PTkUiLAogICAgZ3JvdXA9IkV4aXN0ZW5jZSBDaGVja3MiLAogICAgc2V2ZXJpdHk9Ik1FRElVTSIsCiAgICB0aXRsZT0iQ291bnQoKSA9IDEgdG8gY2hlY2sgdW5pcXVlbmVzcyBzY2FucyBlbnRpcmUgc2V0IiwKICAgIGRlc2NyaXB0aW9uPSgiQ291bnQoKSA9IDEgcmVxdWlyZXMgdGhlIGRhdGFiYXNlIHRvIGNvdW50IEFMTCBtYXRjaGluZyByb3dzLiBUbyBjaGVjayAnZXhhY3RseSBvbmUgcmVjb3JkIGV4aXN0cycsIHlvdSBvbmx5IG5lZWQgdG8gZmV0Y2ggdHdvIHJvd3M6IEZpbmRGaXJzdCgpIGdldHMgdGhlIGZpcnN0LCBOZXh0KCkgPSAwIGNvbmZpcm1zIHRoZXJlIGlzIG5vIHNlY29uZC4gTWF4aW11bSAyIHJvdyByZWFkcy5cblxuU1FMIElNUEFDVDogQ291bnQoKSA9IDE6IFNFTEVDVCBDT1VOVCgqKSBGUk9NIC4uLiDihpIgZnVsbCB0YWJsZSBzY2FuLlxuRmluZEZpcnN0ICsgTmV4dDogU0VMRUNUIFRPUCAxIHRoZW4gU0VMRUNUIFRPUCAxIOKGkiBtYXggMiByb3dzIHJlYWQuXG5cbkJBRDogIGV4aXQoUmVjb3JkLkNvdW50KCkgPSAxKTtcbkdPT0Q6IGlmIFJlY29yZC5GaW5kRmlyc3QoKSB0aGVuIGV4aXQoUmVjb3JkLk5leHQoKSA9IDApXG4gICAgICBlbHNlIGV4aXQoZmFsc2UpO1xuXG5ISU5UOiBDb21wYXJlIFNRTCBmb3IgQ291bnQoKT0xIHZzIEZpbmRGaXJzdCtOZXh0IGFwcHJvYWNoLiBDb3VudCgpIHJlYWRzIGVudGlyZSBtYXRjaGluZyBzZXQuIEZpbmRGaXJzdCtOZXh0IHJlYWRzIGF0IG1vc3QgMiByb3dzLiIpLAogICAgZXhlcmNpc2VzPVsyMl0sCikKY2xhc3MgUGF0dGVybkNvdW50RXF1YWxzT25lOgogICAgX1JFID0gcmUuY29tcGlsZShyJyhcdyspXC5Db3VudFwoXClccyo9XHMqMScsIHJlLk1VTFRJTElORSkKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZGV0ZWN0KHRleHQ6IHN0cikgLT4gbGlzdFtkaWN0XToKICAgICAgICByZXR1cm4gX2ZpbmRpbmdzKHRleHQsIFBhdHRlcm5Db3VudEVxdWFsc09uZS5fUkUsCiAgICAgICAgICAgICAgICAgICAgICAgICBsYW1iZGEgbTogZiJDb3VudCgpID0gMSBzY2FucyBlbnRpcmUgc2V0IOKAlCB1c2UgRmluZEZpcnN0KCkgKyBOZXh0KCkgPSAwIikKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZml4KHRleHQ6IHN0cikgLT4gc3RyOgogICAgICAgIGRlZiByZXBsYWNlKG0pOgogICAgICAgICAgICB2YXIgPSBtLmdyb3VwKDEpCiAgICAgICAgICAgICMgTG9vayBhdCBjb250ZXh0IHRvIHNlZSBpZiBpdCdzIGluIGFuIGV4aXQoKQogICAgICAgICAgICByZXR1cm4gZid7dmFyfS5GaW5kRmlyc3QoKSBhbmQgKHt2YXJ9Lk5leHQoKSA9IDApJwogICAgICAgIHJldHVybiBQYXR0ZXJuQ291bnRFcXVhbHNPbmUuX1JFLnN1YihyZXBsYWNlLCB0ZXh0KQoKCiMg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACiMgUEFUVEVSTiAyMSDigJQgSW5zZXJ0LW9uLWNvbmZsaWN0IHBhdHRlcm4gKHRyeSBJbnNlcnQgZmlyc3QsIE1vZGlmeSBvbiBmYWlsKQojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgApAX3JlZ2lzdGVyKAogICAgaWQ9IklOU0VSVF9PTl9DT05GTElDVCIsCiAgICBncm91cD0iV3JpdGUgUGF0dGVybnMiLAogICAgc2V2ZXJpdHk9Ik1FRElVTSIsCiAgICB0aXRsZT0iSW5zZXJ0LW9uLWNvbmZsaWN0IGFudGktcGF0dGVybiAodHJ5IEluc2VydCwgTW9kaWZ5IG9uIGZhaWwpIiwKICAgIGRlc2NyaXB0aW9uPSgnVGhlIEluc2VydCBhbHdheXMgZmlyZXMuIE9uIGR1cGxpY2F0ZSBrZXksIFNRTCByYWlzZXMgYSBjb25zdHJhaW50IGVycm9yIHRoYXQgaXMgY2F1Z2h0IGFuZCB0cmlnZ2VycyBhIE1vZGlmeS4gRXJyb3IgaGFuZGxpbmcgaW4gU1FMIGlzIGV4dHJlbWVseSBleHBlbnNpdmUg4oCUIGVhY2ggY29uZmxpY3QgPSBhIHJvbGxlZC1iYWNrIHN0YXRlbWVudCArIGVycm9yIGV2ZW50LlxuXG5TUUwgSU1QQUNUOiBPbiBjb25mbGljdDogSU5TRVJUIChmYWlscykg4oaSIFNRTCBlcnJvciDihpIgZXJyb3IgaGFuZGxpbmcg4oaSIFVQREFURS5cbldpdGggR2V0OiBTRUxFQ1QgKDEgcm93KSDihpIgYnJhbmNoIOKGkiBJTlNFUlQgb3IgVVBEQVRFIChubyBlcnJvcnMpLlxuXG5CQUQ6ICBpZiBub3QgUmVjb3JkLkluc2VydChmYWxzZSkgdGhlbiBSZWNvcmQuTW9kaWZ5KGZhbHNlKTtcbkdPT0Q6IGlmIFJlY29yZC5HZXQoS2V5VmFsdWUpIHRoZW4gUmVjb3JkLk1vZGlmeShmYWxzZSlcbiAgICAgIGVsc2UgYmVnaW4gUmVjb3JkLkluaXQoKTsgUmVjb3JkLkluc2VydChmYWxzZSk7IGVuZDtcblxuSElOVDogT24gY29uZmxpY3Q6IElOU0VSVCBwYXRoIGZpcmVzIGEgU1FMIGVycm9yIGV2ZW50IOKAlCB2aXNpYmxlIGluIFByb2ZpbGVyLiBHZXQrYnJhbmNoOiBubyBlcnJvciBldmVudHMuJyksCiAgICBleGVyY2lzZXM9WzIzXSwKKQpjbGFzcyBQYXR0ZXJuSW5zZXJ0T25Db25mbGljdDoKICAgIF9SRSA9IHJlLmNvbXBpbGUocidpZiBub3QgXHcrXC5JbnNlcnRcKCg/OmZhbHNlfHRydWUpP1wpIHRoZW5ccytcdytcLk1vZGlmeVwoJywgcmUuTVVMVElMSU5FKQoKICAgIEBzdGF0aWNtZXRob2QKICAgIGRlZiBkZXRlY3QodGV4dDogc3RyKSAtPiBsaXN0W2RpY3RdOgogICAgICAgIHJldHVybiBfZmluZGluZ3ModGV4dCwgUGF0dGVybkluc2VydE9uQ29uZmxpY3QuX1JFLAogICAgICAgICAgICAgICAgICAgICAgICAgbGFtYmRhIG06ICJJbnNlcnQtb24tZmFpbCBwYXR0ZXJuIOKAlCB1c2UgR2V0KCkgdGhlbiBNb2RpZnkvSW5zZXJ0IGZvciBjbGFyaXR5IGFuZCBlZmZpY2llbmN5IikKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZml4KHRleHQ6IHN0cikgLT4gc3RyOgogICAgICAgIHJldHVybiB0ZXh0CgoKIyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKIyBQQVRURVJOIDIyIOKAlCBTZXRDdXJyZW50S2V5IG1pc3NpbmcgZm9yIHNvcnRlZCBpdGVyYXRpb24KIyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKQF9yZWdpc3RlcigKICAgIGlkPSJNSVNTSU5HX1NFVENVUlJFTlRLRVkiLAogICAgZ3JvdXA9IlNvcnQgLyBLZXlzIiwKICAgIHNldmVyaXR5PSJNRURJVU0iLAogICAgdGl0bGU9IkZpbmQoJy0nKSBvciBGaW5kU2V0IHdpdGhvdXQgU2V0Q3VycmVudEtleSB3aGVuIG9yZGVyIG1hdHRlcnMiLAogICAgZGVzY3JpcHRpb249KCdGaW5kKFwnLVwnKSBhbHdheXMgcmVhZHMgaW4gUFJJTUFSWSBLRVkgYXNjZW5kaW5nIG9yZGVyLCByZWdhcmRsZXNzIG9mIGFueSBTZXRDdXJyZW50S2V5IGNhbGwgbWFkZSBiZWZvcmUgaXQuIFRvIHJlYWQgZGF0YSBpbiBhIHNwZWNpZmljIHNvcnQgb3JkZXIsIHVzZSBGaW5kU2V0KCkgYWZ0ZXIgU2V0Q3VycmVudEtleSBhbmQgKGZvciBkZXNjZW5kaW5nKSBTZXRBc2NlbmRpbmcoZmFsc2UpLlxuXG5TUUwgSU1QQUNUOiBGaW5kKFwnLVwnKTogYWx3YXlzIE9SREVSIEJZIHByaW1hcnkga2V5IEFTQyDigJQgU2V0Q3VycmVudEtleSBpZ25vcmVkLlxuRmluZFNldCB3aXRoIFNldEFzY2VuZGluZzogT1JERVIgQlkgPGtleT4gREVTQyDigJQgY29ycmVjdCBvcmRlcmluZy5cblxuQkFEOiAgUmVjb3JkLlNldEN1cnJlbnRLZXkoXFwiUG9zdGluZyBEYXRlXFwiKTsgaWYgUmVjb3JkLkZpbmQoXCctXCcpIHRoZW4gLi4uXG5HT09EOiBSZWNvcmQuU2V0Q3VycmVudEtleShcXCJQb3N0aW5nIERhdGVcXCIpO1xuICAgICAgUmVjb3JkLlNldEFzY2VuZGluZyhcXCJQb3N0aW5nIERhdGVcXCIsIGZhbHNlKTtcbiAgICAgIGlmIFJlY29yZC5GaW5kU2V0KCkgdGhlbiAuLi5cblxuSElOVDogQ2hlY2sgT1JERVIgQlkgaW4gU1FMIGFmdGVyIEZpbmQoXCctXCcpIHdpdGggU2V0Q3VycmVudEtleS4gU2V0Q3VycmVudEtleSBpcyBpZ25vcmVkIOKAlCBPUkRFUiBCWSB1c2VzIHByaW1hcnkga2V5LicpLAogICAgZXhlcmNpc2VzPVsyNF0sCikKY2xhc3MgUGF0dGVybk1pc3NpbmdTZXRDdXJyZW50S2V5OgogICAgQHN0YXRpY21ldGhvZAogICAgZGVmIGRldGVjdCh0ZXh0OiBzdHIpIC0+IGxpc3RbZGljdF06CiAgICAgICAgcmV0dXJuIFtdICAjIENvdmVyZWQgYnkgRklORF9EQVNIX0JVRkZFUl9PTkU7IHRvbyBjb250ZXh0LXNwZWNpZmljIHRvIGRldGVjdCBnZW5lcmljYWxseQoKICAgIEBzdGF0aWNtZXRob2QKICAgIGRlZiBmaXgodGV4dDogc3RyKSAtPiBzdHI6CiAgICAgICAgcmV0dXJuIHRleHQKCgojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgAojIFBBVFRFUk4gMjMg4oCUIFRlbXAgdGFibGUgYXMgY29sbGVjdGlvbiAodXNlIExpc3QvRGljdGlvbmFyeSkKIyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKQF9yZWdpc3RlcigKICAgIGlkPSJURU1QX1RBQkxFX0FTX0NPTExFQ1RJT04iLAogICAgZ3JvdXA9Ik1lbW9yeSAvIENvcGllcyIsCiAgICBzZXZlcml0eT0iTUVESVVNIiwKICAgIHRpdGxlPSJUZW1wb3JhcnkgdGFibGUgdXNlZCBwdXJlbHkgYXMgYSBjb2xsZWN0aW9uICh1c2UgTGlzdCBvciBEaWN0aW9uYXJ5KSIsCiAgICBkZXNjcmlwdGlvbj0oJ1VzaW5nIGEgdGVtcG9yYXJ5IHRhYmxlIHB1cmVseSB0byBjb2xsZWN0IGRpc3RpbmN0IHNjYWxhciB2YWx1ZXMgKGRhdGVzLCBJRHMsIGNvZGVzKSBjYXJyaWVzIGZ1bGwgcmVjb3JkLWxheWVyIG92ZXJoZWFkOiBzY2hlbWEgdmFsaWRhdGlvbiwga2V5IGluZGV4IG1haW50ZW5hbmNlLCBJbml0L0luc2VydC9GaW5kU2V0IGNhbGxzLiBMaXN0IG9mIFtUXSBpcyBhIG5hdGl2ZSBBTCB0eXBlIHdpdGggemVybyByZWNvcmQgb3ZlcmhlYWQuXG5cblNRTCBJTVBBQ1Q6IFRlbXAgdGFibGUgSW5zZXJ0L0ZpbmRTZXQ6IHJlY29yZCBzdHJ1Y3Qgb3ZlcmhlYWQgcGVyIHZhbHVlLlxuTGlzdCBvZiBbRGF0ZV06IGRpcmVjdCBhcnJheSBhY2Nlc3MsIG5vIHNjaGVtYSwgbm8ga2V5IGxvb2t1cC5cblxuQkFEOiAgVGVtcERhdGVzOiBSZWNvcmQgXFwiV29ya3Nob3AgRGF0YVxcIiB0ZW1wb3Jhcnk7IFRlbXBEYXRlcy5cXCJQb3N0aW5nIERhdGVcXCIgOj0gLi4uOyBUZW1wRGF0ZXMuSW5zZXJ0KCk7XG5HT09EOiBEYXRlczogTGlzdCBvZiBbRGF0ZV07IGlmIG5vdCBEYXRlcy5Db250YWlucyhQb3N0aW5nRGF0ZSkgdGhlbiBEYXRlcy5BZGQoUG9zdGluZ0RhdGUpO1xuXG5ISU5UOiBDb21wYXJlIHRlbXAgdGFibGUgSW5zZXJ0L0ZpbmRTZXQgdnMgTGlzdC5BZGQvaXRlcmF0aW9uLiBUZW1wIHRhYmxlOiBzY2hlbWEgb3ZlcmhlYWQgZXZlbiBpbiBtZW1vcnkuIExpc3Q6IHB1cmUgTlNUIG1lbW9yeSBhcnJheS4nKSwKICAgIGV4ZXJjaXNlcz1bMjVdLAopCmNsYXNzIFBhdHRlcm5UZW1wVGFibGVBc0NvbGxlY3Rpb246CiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZGV0ZWN0KHRleHQ6IHN0cikgLT4gbGlzdFtkaWN0XToKICAgICAgICBmaW5kaW5ncyA9IFtdCiAgICAgICAgbGluZXMgPSB0ZXh0LnNwbGl0bGluZXMoKQogICAgICAgIGZvciBpLCBsaW5lIGluIGVudW1lcmF0ZShsaW5lcyk6CiAgICAgICAgICAgIG0gPSByZS5zZWFyY2gocicoVGVtcFx3Kylccyo6XHMqUmVjb3JkXHMrXFMrXHMrdGVtcG9yYXJ5JywgbGluZSkKICAgICAgICAgICAgaWYgbm90IG06CiAgICAgICAgICAgICAgICBjb250aW51ZQogICAgICAgICAgICB2YXIgPSBtLmdyb3VwKDEpCiAgICAgICAgICAgIHJlc3QgPSAnXG4nLmpvaW4obGluZXNbaTptaW4obGVuKGxpbmVzKSwgaSs4MCldKQogICAgICAgICAgICAjIFB1cmUgY29sbGVjdGlvbjogSW5zZXJ0IHdpdGhvdXQgcmVhZGluZyBiYWNrIG1lYW5pbmdmdWwgZmllbGRzCiAgICAgICAgICAgIGhhc19pbnNlcnQgPSBib29sKHJlLnNlYXJjaChyZidcYntyZS5lc2NhcGUodmFyKX1cLkluc2VydFwoJywgcmVzdCkpCiAgICAgICAgICAgIGhhc19nZXQgPSBib29sKHJlLnNlYXJjaChyZidcYntyZS5lc2NhcGUodmFyKX1cLkdldFwoJywgcmVzdCkpCiAgICAgICAgICAgIGhhc19zZXRyYW5nZSA9IGJvb2wocmUuc2VhcmNoKHJmJ1xie3JlLmVzY2FwZSh2YXIpfVwuU2V0UmFuZ2VcKCcsIHJlc3QpKQogICAgICAgICAgICBpZiBoYXNfaW5zZXJ0IGFuZCBoYXNfc2V0cmFuZ2UgYW5kIG5vdCBoYXNfZ2V0OgogICAgICAgICAgICAgICAgZmluZGluZ3MuYXBwZW5kKHsKICAgICAgICAgICAgICAgICAgICAibGluZSI6IGkgKyAxLAogICAgICAgICAgICAgICAgICAgICJzbmlwcGV0IjogbGluZS5zdHJpcCgpWzoxMjBdLAogICAgICAgICAgICAgICAgICAgICJtZXNzYWdlIjogZiIne3Zhcn0nIHVzZWQgYXMgY29sbGVjdGlvbiAoSW5zZXJ0K1NldFJhbmdlLCBubyBHZXQpIOKAlCB1c2UgTGlzdCBvZiBbLi4uXSBpbnN0ZWFkIgogICAgICAgICAgICAgICAgfSkKICAgICAgICByZXR1cm4gZmluZGluZ3MKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZml4KHRleHQ6IHN0cikgLT4gc3RyOgogICAgICAgIHJldHVybiB0ZXh0CgoKIyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKIyBQQVRURVJOIDI0IOKAlCBGaW5kTGFzdCAvIEZpbmRGaXJzdCBpbnNpZGUgYSBsb29wCiMg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACkBfcmVnaXN0ZXIoCiAgICBpZD0iRklORExBU1RfSU5fTE9PUCIsCiAgICBncm91cD0iRGF0YSBUcmFuc2ZlciIsCiAgICBzZXZlcml0eT0iSElHSCIsCiAgICB0aXRsZT0iRmluZExhc3QoKS9GaW5kRmlyc3QoKSBpbnNpZGUgYSBsb29wIChjYWxsIG9uY2UgYmVmb3JlIHRoZSBsb29wKSIsCiAgICBkZXNjcmlwdGlvbj0oJ0NhbGxpbmcgRmluZExhc3QoKSBpbnNpZGUgYSBsb29wIHRvIGRldGVybWluZSB0aGUgbmV4dCBFbnRyeSBOby4gZmlyZXMgU0VMRUNUIE1BWChcXCJFbnRyeSBOb19cXCIpIC4uLiBvciBPUkRFUiBCWSBERVNDIEZFVENIIDEgb24gRVZFUlkgaXRlcmF0aW9uLiBGb3IgNSAwMDAgbGluZXMgdGhhdCBpcyA1IDAwMCBTUUwgcm91bmQtdHJpcHMgZm9yIGEgY291bnRlci4gT25lIEZpbmRMYXN0KCkgYmVmb3JlIHRoZSBsb29wICsgYSBsb2NhbCBjb3VudGVyIGlzIE8oMSkuXG5cblNRTCBJTVBBQ1Q6IFBlciBpdGVyYXRpb246IFNFTEVDVCBUT1AgMSAuLi4gT1JERVIgQlkgXFwiRW50cnkgTm9fXFwiIERFU0MgPSBOIFNRTCBxdWVyaWVzLlxuXG5CQUQ6ICBmb3JlYWNoIExpbmUgaW4gSW1wb3J0U291cmNlIGRvIGJlZ2luIFJlY29yZC5GaW5kTGFzdCgpOyBOZXh0Tm8gOj0gUmVjb3JkLlxcIkVudHJ5IE5vLlxcIiArIDE7IC4uLlxuR09PRDogUmVjb3JkLkZpbmRMYXN0KCk7IE5leHRObyA6PSBSZWNvcmQuXFwiRW50cnkgTm8uXFwiICsgMTtcbiAgICAgIGZvcmVhY2ggTGluZSBpbiBJbXBvcnRTb3VyY2UgZG8gYmVnaW4gTmV4dE5vICs9IDE7IC4uLiBlbmQ7XG5cbkhJTlQ6IFRyYWNlIEZpbmRMYXN0KCkgY2FsbHMgaW4gdGhlIGxvb3Agd2l0aCBTUUwgUHJvZmlsZXIuIEVhY2ggaXRlcmF0aW9uIGZpcmVzIE9SREVSIEJZIERFU0MgRkVUQ0ggMS4gU2luZ2xlIEZpbmRMYXN0IGJlZm9yZSBsb29wOiAxIFNRTCBxdWVyeSB0b3RhbC4nKSwKICAgIGV4ZXJjaXNlcz1bMjZdLAopCmNsYXNzIFBhdHRlcm5GaW5kTGFzdEluTG9vcDoKICAgIEBzdGF0aWNtZXRob2QKICAgIGRlZiBkZXRlY3QodGV4dDogc3RyKSAtPiBsaXN0W2RpY3RdOgogICAgICAgIGZpbmRpbmdzID0gW10KICAgICAgICBsaW5lcyA9IHRleHQuc3BsaXRsaW5lcygpCiAgICAgICAgaW5fbG9vcCA9IEZhbHNlCiAgICAgICAgZm9yIGksIGxpbmUgaW4gZW51bWVyYXRlKGxpbmVzKToKICAgICAgICAgICAgc3RyaXBwZWQgPSBsaW5lLnN0cmlwKCkKICAgICAgICAgICAgaWYgcmUuc2VhcmNoKHInXGJmb3JlYWNoXGJ8XGJyZXBlYXRcYicsIHN0cmlwcGVkLCByZS5JR05PUkVDQVNFKToKICAgICAgICAgICAgICAgIGluX2xvb3AgPSBUcnVlCiAgICAgICAgICAgIGlmIHJlLm1hdGNoKHIndW50aWxccytcdytcLk5leHRcKFwpJywgc3RyaXBwZWQsIHJlLklHTk9SRUNBU0UpIG9yIChzdHJpcHBlZCA9PSAnZW5kOycgYW5kIGluX2xvb3ApOgogICAgICAgICAgICAgICAgaW5fbG9vcCA9IEZhbHNlCiAgICAgICAgICAgIGlmIGluX2xvb3AgYW5kIHJlLnNlYXJjaChyJ1x3K1wuKEZpbmRMYXN0fEZpbmRGaXJzdClcKFwpJywgbGluZSkgYW5kIG5vdCBzdHJpcHBlZC5zdGFydHN3aXRoKCcvLycpOgogICAgICAgICAgICAgICAgZmluZGluZ3MuYXBwZW5kKHsKICAgICAgICAgICAgICAgICAgICAibGluZSI6IGkgKyAxLAogICAgICAgICAgICAgICAgICAgICJzbmlwcGV0IjogbGluZS5zdHJpcCgpWzoxMjBdLAogICAgICAgICAgICAgICAgICAgICJtZXNzYWdlIjogIkZpbmRMYXN0L0ZpbmRGaXJzdCBpbnNpZGUgbG9vcCDigJQgY2FsbCBvbmNlIGJlZm9yZSB0aGUgbG9vcCBhbmQgY2FjaGUgdGhlIHJlc3VsdCIKICAgICAgICAgICAgICAgIH0pCiAgICAgICAgcmV0dXJuIGZpbmRpbmdzCgogICAgQHN0YXRpY21ldGhvZAogICAgZGVmIGZpeCh0ZXh0OiBzdHIpIC0+IHN0cjoKICAgICAgICByZXR1cm4gdGV4dAoKCiMg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACiMgUEFUVEVSTiAyNSDigJQgTG9ja1RhYmxlICsgRmluZExhc3QgZm9yIHNlcXVlbmNlIGdlbmVyYXRpb24gKHVzZSBOdW1iZXJTZXF1ZW5jZSkKIyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKQF9yZWdpc3RlcigKICAgIGlkPSJMT0NLVEFCTEVfRk9SX1NFUVVFTkNFIiwKICAgIGdyb3VwPSJMb2NraW5nIiwKICAgIHNldmVyaXR5PSJISUdIIiwKICAgIHRpdGxlPSJMb2NrVGFibGUgKyBGaW5kTGFzdCBmb3Igc2VxdWVuY2UgbnVtYmVycyDigJQgdXNlIE51bWJlclNlcXVlbmNlIiwKICAgIGRlc2NyaXB0aW9uPSgiTG9ja1RhYmxlKCkgYWNxdWlyZXMgYSBmdWxsIHRhYmxlLWxldmVsIGV4Y2x1c2l2ZSB3cml0ZSBsb2NrIGhlbGQgZm9yIHRoZSBlbnRpcmUgdHJhbnNhY3Rpb24uIEFueSBjb25jdXJyZW50IHNlc3Npb24gbmVlZGluZyB0aGlzIHRhYmxlIGJsb2Nrcy4gVW5kZXIgaGlnaC12b2x1bWUgcHJvY2Vzc2luZyAoaHVuZHJlZHMgb2Ygb3BlcmF0aW9ucyBwZXIgbWludXRlKSB0aGlzIGNyZWF0ZXMgbG9jayB3YWl0IHF1ZXVlcyBhbmQgZXZlbnR1YWwgdGltZW91dCBlcnJvcnMuXG5cblNRTCBJTVBBQ1Q6IExvY2tUYWJsZTogWC1sb2NrIG9uIGVudGlyZSB0YWJsZSBmb3IgdHJhbnNhY3Rpb24gZHVyYXRpb24uXG5OdW1iZXJTZXF1ZW5jZS5OZXh0KCk6IGF0b21pYyBjb3VudGVyIGF0IERCIGxldmVsIOKAlCBubyB0YWJsZSBsb2NrLlxuXG5CQUQ6ICBSZWNvcmQuTG9ja1RhYmxlKHRydWUpOyBpZiBSZWNvcmQuRmluZExhc3QoKSB0aGVuIE5leHRObyA6PSBSZWNvcmQuUEsgKyAxO1xuR09PRDogaWYgbm90IE51bWJlclNlcXVlbmNlLkV4aXN0cygnTVlfU0VRJykgdGhlbiBOdW1iZXJTZXF1ZW5jZS5JbnNlcnQoJ01ZX1NFUScsMSwxLGZhbHNlKTtcbiAgICAgIE5leHRObyA6PSBOdW1iZXJTZXF1ZW5jZS5OZXh0KCdNWV9TRVEnKTtcblxuSElOVDogTG9ja1RhYmxlOiB3YXRjaCBsb2NrIGV2ZW50cyBpbiBTUUwgUHJvZmlsZXIuIE51bWJlclNlcXVlbmNlOiBubyB0YWJsZSBsb2NrIOKAlCBhdG9taWMgc2VxdWVuY2UgaW5jcmVtZW50LiBDb21wYXJlIGNvbmN1cnJlbmN5OiBydW4gdHdvIHNlc3Npb25zIHNpbXVsdGFuZW91c2x5LiIpLAogICAgZXhlcmNpc2VzPVsyOF0sCikKY2xhc3MgUGF0dGVybkxvY2tUYWJsZUZvclNlcXVlbmNlOgogICAgQHN0YXRpY21ldGhvZAogICAgZGVmIGRldGVjdCh0ZXh0OiBzdHIpIC0+IGxpc3RbZGljdF06CiAgICAgICAgZmluZGluZ3MgPSBbXQogICAgICAgIGxpbmVzID0gdGV4dC5zcGxpdGxpbmVzKCkKICAgICAgICBmb3IgaSwgbGluZSBpbiBlbnVtZXJhdGUobGluZXMpOgogICAgICAgICAgICBpZiByZS5zZWFyY2gocidcdytcLkxvY2tUYWJsZVwoJywgbGluZSkgYW5kIG5vdCBsaW5lLnN0cmlwKCkuc3RhcnRzd2l0aCgnLy8nKToKICAgICAgICAgICAgICAgICMgTG9vayBhaGVhZCBmb3IgRmluZExhc3QgdG8gZ2V0IG5leHQga2V5CiAgICAgICAgICAgICAgICBhaGVhZCA9ICdcbicuam9pbihsaW5lc1tpOm1pbihsZW4obGluZXMpLCBpKzgpXSkKICAgICAgICAgICAgICAgIGlmIHJlLnNlYXJjaChyJ0ZpbmRMYXN0XChcKScsIGFoZWFkKSBhbmQgcmUuc2VhcmNoKHInRW50cnkgTm98TmV4dFNlcXxOZXh0Tm98TmV4dEtleScsIGFoZWFkLCByZS5JR05PUkVDQVNFKToKICAgICAgICAgICAgICAgICAgICBmaW5kaW5ncy5hcHBlbmQoewogICAgICAgICAgICAgICAgICAgICAgICAibGluZSI6IGkgKyAxLAogICAgICAgICAgICAgICAgICAgICAgICAic25pcHBldCI6IGxpbmUuc3RyaXAoKVs6MTIwXSwKICAgICAgICAgICAgICAgICAgICAgICAgIm1lc3NhZ2UiOiAiTG9ja1RhYmxlICsgRmluZExhc3QgZm9yIHNlcXVlbmNlIGdlbmVyYXRpb24g4oCUIHVzZSBOdW1iZXJTZXF1ZW5jZS5OZXh0KCkgaW5zdGVhZCIKICAgICAgICAgICAgICAgICAgICB9KQogICAgICAgIHJldHVybiBmaW5kaW5ncwoKICAgIEBzdGF0aWNtZXRob2QKICAgIGRlZiBmaXgodGV4dDogc3RyKSAtPiBzdHI6CiAgICAgICAgcmV0dXJuIHRleHQKCgojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgAojIFBBVFRFUk4gMjYg4oCUIFNldFJhbmdlIG9uIFBLICsgRmluZEZpcnN0IGluc3RlYWQgb2YgR2V0CiMg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACkBfcmVnaXN0ZXIoCiAgICBpZD0iU0VUUkFOR0VfRklORFNFVF9GT1JfR0VUIiwKICAgIGdyb3VwPSJEYXRhIFRyYW5zZmVyIiwKICAgIHNldmVyaXR5PSJNRURJVU0iLAogICAgdGl0bGU9IlNldFJhbmdlIG9uIFBLICsgRmluZEZpcnN0IGluc3RlYWQgb2YgZGlyZWN0IEdldCgpIiwKICAgIGRlc2NyaXB0aW9uPSgnU2V0UmFuZ2Ugb24gYSBwcmltYXJ5IGtleSBmaWVsZCBmb2xsb3dlZCBieSBGaW5kRmlyc3QoKSBnb2VzIHRocm91Z2ggdGhlIGZpbHRlcitzY2FuIHBhdGggaW5zdGVhZCBvZiB0aGUgb3B0aW1pemVkIHNpbmdsZS1yb3cgbG9va3VwLiBHZXQoKSB1c2VzIHRoZSBwcmltYXJ5IGtleSBpbmRleCBkaXJlY3RseSDigJQgbm8gY3Vyc29yIHNldHVwLCBoaXRzIHRoZSBjbHVzdGVyZWQgaW5kZXggZGlyZWN0bHkuXG5cblNRTCBJTVBBQ1Q6IFNldFJhbmdlK0ZpbmRGaXJzdDogU0VMRUNUIFRPUCA1MCAuLi4gV0hFUkUgXFwiTm9fXFwiPVggKGN1cnNvciBzZXR1cCBvdmVyaGVhZCkuXG5HZXQoKTogRGlyZWN0IGNsdXN0ZXJlZCBpbmRleCBwb2ludCBsb29rdXAg4oCUIG5vIGN1cnNvci4gTWVhc3VyYWJseSBmYXN0ZXIgaW4gaGlnaC1mcmVxdWVuY3kgbG9vcHMuXG5cbkJBRDogIEN1c3RvbWVyLlNldFJhbmdlKFxcIk5vLlxcIiwgQ3VzdG9tZXJObyk7IGlmIEN1c3RvbWVyLkZpbmRGaXJzdCgpIHRoZW4gLi4uXG5HT09EOiBpZiBDdXN0b21lci5HZXQoQ3VzdG9tZXJObykgdGhlbiAuLi5cblxuSElOVDogQ29tcGFyZSBTRUxFQ1QgVE9QIDUwIChTZXRSYW5nZStGaW5kRmlyc3QpIHZzIGRpcmVjdCBsb29rdXAuIEdldCgpIGhpdHMgY2x1c3RlcmVkIGluZGV4IGRpcmVjdGx5IOKAlCAxIHJlYWQsIG5vIGN1cnNvci4nKSwKICAgIGV4ZXJjaXNlcz1bMjldLAopCmNsYXNzIFBhdHRlcm5TZXRSYW5nZUZpbmRGaXJzdEZvckdldDoKICAgIEBzdGF0aWNtZXRob2QKICAgIGRlZiBkZXRlY3QodGV4dDogc3RyKSAtPiBsaXN0W2RpY3RdOgogICAgICAgIGZpbmRpbmdzID0gW10KICAgICAgICBsaW5lcyA9IHRleHQuc3BsaXRsaW5lcygpCiAgICAgICAgZm9yIGksIGxpbmUgaW4gZW51bWVyYXRlKGxpbmVzKToKICAgICAgICAgICAgbSA9IHJlLnNlYXJjaChyJyhcdyspXC5TZXRSYW5nZVwoIk5vXC4iLFxzKlx3K1wpJywgbGluZSkKICAgICAgICAgICAgaWYgbm90IG0gb3IgbGluZS5zdHJpcCgpLnN0YXJ0c3dpdGgoJy8vJyk6CiAgICAgICAgICAgICAgICBjb250aW51ZQogICAgICAgICAgICB2YXIgPSBtLmdyb3VwKDEpCiAgICAgICAgICAgIGFoZWFkID0gJ1xuJy5qb2luKGxpbmVzW2k6bWluKGxlbihsaW5lcyksIGkrMyldKQogICAgICAgICAgICBpZiByZS5zZWFyY2gocmYnXGJ7cmUuZXNjYXBlKHZhcil9XC5GaW5kRmlyc3RcKFwpJywgYWhlYWQpOgogICAgICAgICAgICAgICAgZmluZGluZ3MuYXBwZW5kKHsKICAgICAgICAgICAgICAgICAgICAibGluZSI6IGkgKyAxLAogICAgICAgICAgICAgICAgICAgICJzbmlwcGV0IjogbGluZS5zdHJpcCgpWzoxMjBdLAogICAgICAgICAgICAgICAgICAgICJtZXNzYWdlIjogZiIne3Zhcn0uU2V0UmFuZ2UoXCJOby5cIiwgLi4uKSArIEZpbmRGaXJzdCgpJyDigJQgdXNlICd7dmFyfS5HZXQoLi4uKScgZGlyZWN0bHkiCiAgICAgICAgICAgICAgICB9KQogICAgICAgIHJldHVybiBmaW5kaW5ncwoKICAgIEBzdGF0aWNtZXRob2QKICAgIGRlZiBmaXgodGV4dDogc3RyKSAtPiBzdHI6CiAgICAgICAgcmV0dXJuIHRleHQKCgojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgAojIFBBVFRFUk4gMjcg4oCUIFNpbGVudCBJbnNlcnQgZmFpbHVyZSAoaWYgSW5zZXJ0IHRoZW4pCiMg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACkBfcmVnaXN0ZXIoCiAgICBpZD0iU0lMRU5UX0lOU0VSVF9GQUlMVVJFIiwKICAgIGdyb3VwPSJXcml0ZSBQYXR0ZXJucyIsCiAgICBzZXZlcml0eT0iTUVESVVNIiwKICAgIHRpdGxlPSInaWYgUmVjb3JkLkluc2VydChmYWxzZSkgdGhlbicgc2lsZW50bHkgc3dhbGxvd3MgZHVwbGljYXRlLWtleSBlcnJvcnMiLAogICAgZGVzY3JpcHRpb249KCJJbiBBTCwgSW5zZXJ0KGZhbHNlKSBkb2VzIE5PVCBzdXBwcmVzcyB0aGUgU1FMIHVuaXF1ZSBjb25zdHJhaW50IGVycm9yIOKAlCBpdCBzdGlsbCBwcm9wYWdhdGVzLiBXcmFwcGluZyBpdCBpbiAnaWYgLi4uIHRoZW4nIGdpdmVzIGEgZmFsc2Ugc2Vuc2UgdGhhdCB0aGUgZmFpbHVyZSBpcyBoYW5kbGVkLiBJbiBwcmFjdGljZSB0aGUgcmVjb3JkIHNpbGVudGx5IGlzbid0IGNyZWF0ZWQgYW5kIHRoZSBjYWxsZXIgbmV2ZXIga25vd3MsIGxlYWRpbmcgdG8gZGF0YSBpbnRlZ3JpdHkgZ2Fwcy5cblxuU1FMIElNUEFDVDogU1FMIHVuaXF1ZSBjb25zdHJhaW50IGVycm9yIHN0aWxsIHJhaXNlZCDigJQgJ2lmIHRoZW4nIGRvZXNuJ3QgY2F0Y2ggU1FMIGVycm9ycy5cblNpbGVudCBmYWlsdXJlIOKGkiBtaXNzaW5nIHJlY29yZHMg4oaSIGludGVncml0eSBnYXAuXG5cbkJBRDogIGlmIFJlY29yZC5JbnNlcnQoZmFsc2UpIHRoZW4gTW92ZW1lbnRDb3VudCArPSAxO1xuR09PRDogUmVjb3JkLkluc2VydChmYWxzZSk7IE1vdmVtZW50Q291bnQgKz0gMTtcblxuSElOVDogSW5zZXJ0IGEgZHVwbGljYXRlIGtleSByb3cgd2l0aCAnaWYgSW5zZXJ0IHRoZW4nLiBDaGVjayBTUUwgUHJvZmlsZXIg4oCUIGRvZXMgdGhlIGVycm9yIGFwcGVhcj8gVGhlIHJlY29yZCBpcyBub3QgY3JlYXRlZCBidXQgbm8gQUwgZXJyb3IgcmFpc2VkLiIpLAogICAgZXhlcmNpc2VzPVszMF0sCikKY2xhc3MgUGF0dGVyblNpbGVudEluc2VydEZhaWx1cmU6CiAgICBfUkUgPSByZS5jb21waWxlKHInaWZccytcdytcLkluc2VydFwoKD86ZmFsc2V8dHJ1ZSlcKVxzK3RoZW4nLCByZS5NVUxUSUxJTkUpCgogICAgQHN0YXRpY21ldGhvZAogICAgZGVmIGRldGVjdCh0ZXh0OiBzdHIpIC0+IGxpc3RbZGljdF06CiAgICAgICAgcmV0dXJuIF9maW5kaW5ncyh0ZXh0LCBQYXR0ZXJuU2lsZW50SW5zZXJ0RmFpbHVyZS5fUkUsCiAgICAgICAgICAgICAgICAgICAgICAgICBsYW1iZGEgbTogIlNpbGVudCBJbnNlcnQg4oCUIGVycm9ycyB3b24ndCBzdXJmYWNlLiBVc2UgcGxhaW4gSW5zZXJ0KCkgb3IgaGFuZGxlIGV4cGxpY2l0bHkiKQoKICAgIEBzdGF0aWNtZXRob2QKICAgIGRlZiBmaXgodGV4dDogc3RyKSAtPiBzdHI6CiAgICAgICAgcmV0dXJuIHRleHQKCgojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgAojIFBBVFRFUk4gMjgg4oCUIExvY2tUYWJsZSB0b28gZWFybHkgKGJlZm9yZSByZWFkLW9ubHkgcGhhc2UpCiMg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACkBfcmVnaXN0ZXIoCiAgICBpZD0iTE9DS1RBQkxFX1RPT19FQVJMWSIsCiAgICBncm91cD0iTG9ja2luZyIsCiAgICBzZXZlcml0eT0iSElHSCIsCiAgICB0aXRsZT0iTG9ja1RhYmxlKCkgY2FsbGVkIGJlZm9yZSByZWFkIHBoYXNlIOKAlCBob2xkcyB3cml0ZSBsb2NrIGR1cmluZyByZWFkcyIsCiAgICBkZXNjcmlwdGlvbj0oJ0xvY2tUYWJsZSgpIGF0IHRoZSB0b3Agb2YgYSBwcm9jZWR1cmUgaG9sZHMgYW4gZXhjbHVzaXZlIGxvY2sgdGhyb3VnaCB0aGUgZW50aXJlIHZhbGlkYXRpb24gLyByZWFkIHBoYXNlIOKAlCB3aGljaCBtYXkgdGFrZSBzZWNvbmRzIG9uIGxhcmdlIGRhdGEuIEV2ZXJ5IG90aGVyIHNlc3Npb24gbmVlZGluZyB0aGlzIHRhYmxlIGlzIGJsb2NrZWQgZm9yIHRoZSBmdWxsIGR1cmF0aW9uLiBUaGUgbG9jayBpcyBvbmx5IG5lZWRlZCBhdCB0aGUgbW9tZW50IG9mIHRoZSBmaXJzdCB3cml0ZS5cblxuU1FMIElNUEFDVDogWC1sb2NrIGhlbGQgZnJvbSB0b3Agb2YgcHJvYyB0aHJvdWdoIGVudGlyZSByZWFkIHBoYXNlIChzZWNvbmRzKS5cbnZzLiBYLWxvY2sgYWNxdWlyZWQganVzdCBiZWZvcmUgZmlyc3QgTW9kaWZ5IChtaWxsaXNlY29uZHMpLlxuXG5CQUQ6ICBXb3Jrc2hvcERhdGEuTG9ja1RhYmxlKCk7IC4uLiBpZiBXb3Jrc2hvcERhdGEuRmluZFNldCgpIHRoZW4gW3JlYWQgbG9vcF0gLi4uXG5HT09EOiBbcmVhZCBsb29wIHdpdGhvdXQgbG9ja10gLi4uIFdvcmtzaG9wRGF0YS5Mb2NrVGFibGUoKTsgW3dyaXRlIHBoYXNlIG9ubHldXG5cbkhJTlQ6IFVzZSBBY3Rpdml0eSBNb25pdG9yIG9yIFByb2ZpbGVyIGxvY2sgZXZlbnRzLiBMb2NrVGFibGUgYXQgc3RhcnQ6IFgtbG9jayBoZWxkIGR1cmluZyBlbnRpcmUgcmVhZCBwaGFzZS4gTG9ja1RhYmxlIGF0IHdyaXRlIHBoYXNlOiBYLWxvY2sgaGVsZCBmb3IgbWlsbGlzZWNvbmRzIG9ubHkuJyksCiAgICBleGVyY2lzZXM9WzMxLCA0Ml0sCikKY2xhc3MgUGF0dGVybkxvY2tUYWJsZVRvb0Vhcmx5OgogICAgQHN0YXRpY21ldGhvZAogICAgZGVmIGRldGVjdCh0ZXh0OiBzdHIpIC0+IGxpc3RbZGljdF06CiAgICAgICAgZmluZGluZ3MgPSBbXQogICAgICAgIGxpbmVzID0gdGV4dC5zcGxpdGxpbmVzKCkKICAgICAgICBmb3IgaSwgbGluZSBpbiBlbnVtZXJhdGUobGluZXMpOgogICAgICAgICAgICBpZiByZS5zZWFyY2gocidcdytcLkxvY2tUYWJsZVwoJywgbGluZSkgYW5kIG5vdCBsaW5lLnN0cmlwKCkuc3RhcnRzd2l0aCgnLy8nKToKICAgICAgICAgICAgICAgICMgQ2hlY2sgaWYgRmluZFNldCBmb2xsb3dzIChyZWFkIHBoYXNlKQogICAgICAgICAgICAgICAgYWhlYWQgPSAnXG4nLmpvaW4obGluZXNbaTptaW4obGVuKGxpbmVzKSwgaSsxNSldKQogICAgICAgICAgICAgICAgaWYgcmUuc2VhcmNoKHInXC5GaW5kU2V0XChcKScsIGFoZWFkKToKICAgICAgICAgICAgICAgICAgICBmaW5kaW5ncy5hcHBlbmQoewogICAgICAgICAgICAgICAgICAgICAgICAibGluZSI6IGkgKyAxLAogICAgICAgICAgICAgICAgICAgICAgICAic25pcHBldCI6IGxpbmUuc3RyaXAoKVs6MTIwXSwKICAgICAgICAgICAgICAgICAgICAgICAgIm1lc3NhZ2UiOiAiTG9ja1RhYmxlKCkgYmVmb3JlIEZpbmRTZXQg4oCUIG1vdmUgbG9jayBjbG9zZXIgdG8gdGhlIHdyaXRlIG9wZXJhdGlvbiIKICAgICAgICAgICAgICAgICAgICB9KQogICAgICAgIHJldHVybiBmaW5kaW5ncwoKICAgIEBzdGF0aWNtZXRob2QKICAgIGRlZiBmaXgodGV4dDogc3RyKSAtPiBzdHI6CiAgICAgICAgcmV0dXJuIHRleHQKCgojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgAojIFBBVFRFUk4gMjkg4oCUIE1pc3NpbmcgUmVhZElzb2xhdGlvbiAoZGlydHkgcmVhZHMgc2FmZSA9IFJlYWRVbmNvbW1pdHRlZCkKIyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKQF9yZWdpc3RlcigKICAgIGlkPSJNSVNTSU5HX1JFQURfSVNPTEFUSU9OIiwKICAgIGdyb3VwPSJMb2NraW5nIiwKICAgIHNldmVyaXR5PSJNRURJVU0iLAogICAgdGl0bGU9IkZpbmRTZXQgb24gcmVwb3J0aW5nL3JlYWQtb25seSBxdWVyeSB3aXRob3V0IFJlYWRJc29sYXRpb24gPSBSZWFkVW5jb21taXR0ZWQiLAogICAgZGVzY3JpcHRpb249KCdSZWFkLW9ubHkgYW5hbHl0aWNzIEZpbmRTZXQgYWNxdWlyZXMgU2hhcmVkIChTKSBsb2NrcyBvbiBldmVyeSBmZXRjaGVkIHBhZ2UuIENvbmN1cnJlbnQgd2FyZWhvdXNlIHdyaXRlIHNlc3Npb25zIGhvbGQgRXhjbHVzaXZlIChYKSBsb2NrcyBvbiBzYW1lIHJvd3MuIFJlc3VsdDogYW5hbHl0aWNzIGJsb2NrcyBwb3N0aW5ncywgcG9zdGluZ3MgYmxvY2sgYW5hbHl0aWNzLiBPbiBhIDUwIDAwMC1yb3cgc2NhbiwgUy1sb2NrcyBhcmUgaGVsZCBmb3Igc2V2ZXJhbCBzZWNvbmRzLlxuXG5TUUwgSU1QQUNUOiBEZWZhdWx0IGlzb2xhdGlvbjogU2hhcmVkIChTKSBsb2NrcyBwZXIgcGFnZSBmZXRjaGVkLlxuUmVhZFVuY29tbWl0dGVkOiBOTyBsb2NrIGFjcXVpc2l0aW9uIOKAlCByZWFkZXJzIG5ldmVyIGJsb2NrIHdyaXRlcnMuXG5cbkdPT0Q6IFdvcmtzaG9wRGF0YS5SZWFkSXNvbGF0aW9uIDo9IElzb2xhdGlvbkxldmVsOjpSZWFkVW5jb21taXR0ZWQ7XG4gICAgICBpZiBXb3Jrc2hvcERhdGEuRmluZFNldCgpIHRoZW4gLi4uXG5cbkhJTlQ6IFdpdGhvdXQgUmVhZFVuY29tbWl0dGVkOiB3YXRjaCBTLWxvY2sgZXZlbnRzIHBlciBwYWdlIGluIFNRTCBQcm9maWxlci4gV2l0aCBSZWFkVW5jb21taXR0ZWQ6IHplcm8gUy1sb2NrIGV2ZW50cyDigJQgcmVhZGVycyBpbnZpc2libGUuIE5ldmVyIHVzZSBmb3IgZmluYW5jaWFsIHBvc3RpbmdzIG9yIHZhbGlkYXRpb24uJyksCiAgICBleGVyY2lzZXM9WzM0XSwKKQpjbGFzcyBQYXR0ZXJuTWlzc2luZ1JlYWRJc29sYXRpb246CiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZGV0ZWN0KHRleHQ6IHN0cikgLT4gbGlzdFtkaWN0XToKICAgICAgICBmaW5kaW5ncyA9IFtdCiAgICAgICAgbGluZXMgPSB0ZXh0LnNwbGl0bGluZXMoKQogICAgICAgIGZvciBpLCBsaW5lIGluIGVudW1lcmF0ZShsaW5lcyk6CiAgICAgICAgICAgIGlmIHJlLnNlYXJjaChyJ1x3K1wuRmluZFNldFwoXCknLCBsaW5lKSBhbmQgbm90IGxpbmUuc3RyaXAoKS5zdGFydHN3aXRoKCcvLycpOgogICAgICAgICAgICAgICAgIyBQcm9jZWR1cmUgbmFtZSBzdWdnZXN0cyByZXBvcnRpbmcKICAgICAgICAgICAgICAgIGNvbnRleHRfYWJvdmUgPSAnXG4nLmpvaW4obGluZXNbbWF4KDAsIGktMzApOmldKQogICAgICAgICAgICAgICAgaWYgcmUuc2VhcmNoKHInUmVwb3J0fERhc2hib2FyZHxFeHBvcnR8QnVpbGQuKlJlcG9ydHxTbmFwc2hvdCcsIGNvbnRleHRfYWJvdmUsIHJlLklHTk9SRUNBU0UpOgogICAgICAgICAgICAgICAgICAgIGlmICdSZWFkSXNvbGF0aW9uJyBub3QgaW4gY29udGV4dF9hYm92ZToKICAgICAgICAgICAgICAgICAgICAgICAgZmluZGluZ3MuYXBwZW5kKHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJsaW5lIjogaSArIDEsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAic25pcHBldCI6IGxpbmUuc3RyaXAoKVs6MTIwXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJtZXNzYWdlIjogIlJlcG9ydGluZyBGaW5kU2V0IHdpdGhvdXQgUmVhZElzb2xhdGlvbiA6PSBSZWFkVW5jb21taXR0ZWQg4oCUIG1heSBibG9jayBjb25jdXJyZW50IHdyaXRlcyIKICAgICAgICAgICAgICAgICAgICAgICAgfSkKICAgICAgICByZXR1cm4gZmluZGluZ3MKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZml4KHRleHQ6IHN0cikgLT4gc3RyOgogICAgICAgIHJldHVybiB0ZXh0CgoKIyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKIyBQQVRURVJOIDMwIOKAlCBSZWNvcmRSZWYvRmllbGRSZWYgd2hlbiB0eXBlZCBSZWNvcmQgaXMgc3VmZmljaWVudAojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgApAX3JlZ2lzdGVyKAogICAgaWQ9IlJFQ09SRFJFRl9XSEVOX1RZUEVEX1NVRkZJQ0lFTlQiLAogICAgZ3JvdXA9Ik1lbW9yeSAvIENvcGllcyIsCiAgICBzZXZlcml0eT0iTE9XIiwKICAgIHRpdGxlPSJSZWNvcmRSZWYvRmllbGRSZWYgdXNlZCB3aGVuIGEgdHlwZWQgUmVjb3JkIHZhcmlhYmxlIHdvdWxkIHN1ZmZpY2UiLAogICAgZGVzY3JpcHRpb249KCdSZWNvcmRSZWYgcHJvdmlkZXMgbGF0ZS1ib3VuZCwgcmVmbGVjdGlvbi1iYXNlZCB0YWJsZSBhY2Nlc3MuIEVhY2ggRmllbGQoKSBjYWxsIGRvZXMgYSBkaWN0aW9uYXJ5IGxvb2t1cCArIHR5cGUgYm94aW5nLiBTUUwgZ2VuZXJhdGlvbiBpcyBsZXNzIG9wdGltaXplZCB0aGFuIGZvciB0eXBlZCBSZWNvcmQgdmFyaWFibGVzLiBCZW5jaG1hcmtzIHNob3cgM+KAkzV4IHNsb3dlciByZWFkcyB0aGFuIHR5cGVkIFJlY29yZCBvbiBzYW1lIGRhdGEuXG5cblNRTCBJTVBBQ1Q6IFJlY29yZFJlZjogRmllbGQoKSBsb29rdXAgKyBib3hpbmcgb3ZlcmhlYWQgcGVyIGZpZWxkIHBlciByb3cuXG5UeXBlZCBSZWNvcmQ6IGRpcmVjdCBmaWVsZCBhY2Nlc3MsIGNvbXBpbGVyLW9wdGltaXplZCBTUUwuXG5cbkJBRDogIFJlY1JlZjogUmVjb3JkUmVmOyBSZWNSZWYuT3BlbihEYXRhYmFzZTo6XFwiV29ya3Nob3AgRGF0YVxcIik7IFJlY1JlZi5GaWVsZCgyKS5WYWx1ZSA6PSAuLi47XG5HT09EOiBXb3Jrc2hvcERhdGE6IFJlY29yZCBcXCJXb3Jrc2hvcCBEYXRhXFwiOyBXb3Jrc2hvcERhdGEuRGVzY3JpcHRpb24gOj0gLi4uO1xuXG5ISU5UOiBDb21wYXJlIFJlY29yZFJlZi5GaWVsZCgyKS5WYWx1ZSB2cyB0eXBlZCBXb3Jrc2hvcERhdGEuRGVzY3JpcHRpb24uIFJ1biBhIDEwIDAwMC1yb3cgbG9vcCB3aXRoIGVhY2gg4oCUIHRpbWUgdGhlIGRpZmZlcmVuY2UuIFJlY29yZFJlZiBpcyAz4oCTNXggc2xvd2VyLicpLAogICAgZXhlcmNpc2VzPVszNV0sCikKY2xhc3MgUGF0dGVyblJlY29yZFJlZlVubmVjZXNzYXJ5OgogICAgX1JFID0gcmUuY29tcGlsZShyJ1xiUmVjUmVmXHMqOlxzKlJlY29yZFJlZlxifFxiRmxkUmVmXHMqOlxzKkZpZWxkUmVmXGInLCByZS5NVUxUSUxJTkUpCgogICAgQHN0YXRpY21ldGhvZAogICAgZGVmIGRldGVjdCh0ZXh0OiBzdHIpIC0+IGxpc3RbZGljdF06CiAgICAgICAgcmV0dXJuIF9maW5kaW5ncyh0ZXh0LCBQYXR0ZXJuUmVjb3JkUmVmVW5uZWNlc3NhcnkuX1JFLAogICAgICAgICAgICAgICAgICAgICAgICAgbGFtYmRhIG06ICJSZWNvcmRSZWYvRmllbGRSZWYg4oCUIGNvbnNpZGVyIHR5cGVkIFJlY29yZCB2YXJpYWJsZSBpZiB0YWJsZSBpcyBrbm93biBhdCBjb21waWxlIHRpbWUiKQoKICAgIEBzdGF0aWNtZXRob2QKICAgIGRlZiBmaXgodGV4dDogc3RyKSAtPiBzdHI6CiAgICAgICAgcmV0dXJuIHRleHQKCgojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgAojIFBBVFRFUk4gMzEg4oCUIE1vZGlmeUFsbCB3aXRoIFJ1blRyaWdnZXI9dHJ1ZQojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgApAX3JlZ2lzdGVyKAogICAgaWQ9Ik1PRElGWUFMTF9SVU5UUklHR0VSX1RSVUUiLAogICAgZ3JvdXA9IkJ1bGsgT3BlcmF0aW9ucyIsCiAgICBzZXZlcml0eT0iSElHSCIsCiAgICB0aXRsZT0iTW9kaWZ5QWxsIHdpdGggUnVuVHJpZ2dlcj10cnVlIGZpcmVzIE9uTW9kaWZ5IHRyaWdnZXIgcGVyIHJvdyIsCiAgICBkZXNjcmlwdGlvbj0oJ01vZGlmeUFsbCgpIHdpdGggUnVuVHJpZ2dlcj10cnVlIGNoZWNrcyBmb3IgT25BZnRlck1vZGlmeSBzdWJzY3JpYmVycy4gSWYgQU5ZIHN1YnNjcmliZXIgaXMgYm91bmQsIE1vZGlmeUFsbCBmYWxscyBiYWNrIHRvIE4gaW5kaXZpZHVhbCBNb2RpZnkoKSBjYWxscyDigJQgb25lIHBlciByZWNvcmQg4oCUIGVhY2ggdHJpZ2dlcmluZyB0aGUgc3Vic2NyaWJlci4gMTAgMDAwIHJlY29yZHMgw5cgMSBNb2RpZnkoKSBjYWxsID0gMTAgMDAwIFNRTCBVUERBVEUgcm91bmQtdHJpcHMuXG5cblNRTCBJTVBBQ1Q6IFdpdGggUnVuVHJpZ2dlcj10cnVlICsgYWN0aXZlIHN1YnNjcmliZXI6IE4gw5cgVVBEQVRFIHBlciByb3cuXG5XaXRoIFJ1blRyaWdnZXI9ZmFsc2U6IDEgw5cgVVBEQVRFIC4uLiBXSEVSRSAuLi4gKHNpbmdsZSBTUUwgc3RhdGVtZW50KS5cblxuQkFEOiAgUmVjb3JkLk1vZGlmeUFsbChGaWVsZCwgVmFsdWUsIHRydWUpO1xuR09PRDogUmVjb3JkLk1vZGlmeUFsbChGaWVsZCwgVmFsdWUsIGZhbHNlKTtcblxuSElOVDogQmluZCB0aGUgc3Vic2NyaWJlciBhbmQgcnVuIE1vZGlmeUFsbCB3aXRoIFJ1blRyaWdnZXI9dHJ1ZS4gU1FMIFByb2ZpbGVyOiBjb3VudCBVUERBVEUgc3RhdGVtZW50cyDigJQgc2hvdWxkIGJlIE4gcm93cy4gQ2hhbmdlIHRvIFJ1blRyaWdnZXI9ZmFsc2U6IG5vdyBjb3VudCBpcyAxIFVQREFURSBzdGF0ZW1lbnQuJyksCiAgICBleGVyY2lzZXM9WzM3XSwKKQpjbGFzcyBQYXR0ZXJuTW9kaWZ5QWxsUnVuVHJpZ2dlcjoKICAgIF9SRSA9IHJlLmNvbXBpbGUocidcdytcLk1vZGlmeUFsbFwoW14pXSssXHMqdHJ1ZVwpJywgcmUuTVVMVElMSU5FKQoKICAgIEBzdGF0aWNtZXRob2QKICAgIGRlZiBkZXRlY3QodGV4dDogc3RyKSAtPiBsaXN0W2RpY3RdOgogICAgICAgIHJldHVybiBfZmluZGluZ3ModGV4dCwgUGF0dGVybk1vZGlmeUFsbFJ1blRyaWdnZXIuX1JFLAogICAgICAgICAgICAgICAgICAgICAgICAgbGFtYmRhIG06ICJNb2RpZnlBbGwgd2l0aCBSdW5UcmlnZ2VyPXRydWUgZmlyZXMgT25Nb2RpZnkgcGVyIHJvdyDigJQgdXNlIGZhbHNlIHVubGVzcyByZXF1aXJlZCIpCgogICAgQHN0YXRpY21ldGhvZAogICAgZGVmIGZpeCh0ZXh0OiBzdHIpIC0+IHN0cjoKICAgICAgICByZXR1cm4gcmUuc3ViKAogICAgICAgICAgICByJyhcdytcLk1vZGlmeUFsbFwoW14pXSssXHMqKXRydWUoXCkpJywKICAgICAgICAgICAgcidcMWZhbHNlXDInLAogICAgICAgICAgICB0ZXh0CiAgICAgICAgKQoKCiMg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACiMgUEFUVEVSTiAzMiDigJQgTWlzc2luZyBEYXRhYmFzZS5TZWxlY3RMYXRlc3RWZXJzaW9uIGluIG11bHRpLXBhc3MgbG9vcAojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgApAX3JlZ2lzdGVyKAogICAgaWQ9Ik1JU1NJTkdfU0VMRUNUTEFURVNUVkVSU0lPTiIsCiAgICBncm91cD0iU3RhbGUgUmVhZHMiLAogICAgc2V2ZXJpdHk9Ik1FRElVTSIsCiAgICB0aXRsZT0iTXVsdGktcGFzcyByZWFkIGxvb3Agd2l0aG91dCBEYXRhYmFzZS5TZWxlY3RMYXRlc3RWZXJzaW9uKCkiLAogICAgZGVzY3JpcHRpb249KCdCQyBOU1QgY2FjaGVzIFNRTCBxdWVyeSByZXN1bHRzIGluLXNlc3Npb24uIFJlcGVhdGVkIEZpbmRTZXQgY2FsbHMgd2l0aCB0aGUgc2FtZSBmaWx0ZXIgcmV0dXJuIENBQ0hFRCByb3dzIOKAlCBubyBTUUwgZXhlY3V0ZWQgYWZ0ZXIgcGFzcyAxLiBBIG11bHRpLXBhc3MgcGlwZWxpbmUgcmVhZGluZyB0aGUgc2FtZSBmaWx0ZXIgc2lsZW50bHkgcmV0dXJucyBzdGFsZSBkYXRhIGZyb20gcGFzcyAxIGZvciBwYXNzZXMgMuKAk04gaWYgZGF0YSBjaGFuZ2VkIGNvbmN1cnJlbnRseS5cblxuU1FMIElNUEFDVDogUGFzcyAxOiBTRUxFQ1QgLi4uIChyZWFsIFNRTCkuXG5QYXNzIDLigJNOOiBbTlNUIGNhY2hlIGhpdF0g4oaSIG5vIFNRTCwgbm8gYXdhcmVuZXNzIG9mIGNvbmN1cnJlbnQgdXBkYXRlcy5cblxuR09PRDogZm9yZWFjaCBQYXNzIGluIFBhc3NlcyBkbyBiZWdpblxuICAgICAgICAgIERhdGFiYXNlLlNlbGVjdExhdGVzdFZlcnNpb24oKTsgLy8gRmx1c2hlcyBub24tbG9ja2VkIHNlc3Npb24gY2FjaGVcbiAgICAgICAgICBpZiBSZWNvcmQuRmluZFNldCgpIHRoZW4gLi4uXG5cbkhJTlQ6IFJ1biB0aGUgcGlwZWxpbmUgdHdpY2U6IHdpdGggYW5kIHdpdGhvdXQgU2VsZWN0TGF0ZXN0VmVyc2lvbi4gV2l0aG91dCBmbHVzaDogMCBTUUwgcXVlcmllcyBmb3IgcGFzc2VzIDLigJNOIChjYWNoZSBoaXQg4oCUIHN0YWxlIGRhdGEpLicpLAogICAgZXhlcmNpc2VzPVszOF0sCikKY2xhc3MgUGF0dGVybk1pc3NpbmdTZWxlY3RMYXRlc3RWZXJzaW9uOgogICAgQHN0YXRpY21ldGhvZAogICAgZGVmIGRldGVjdCh0ZXh0OiBzdHIpIC0+IGxpc3RbZGljdF06CiAgICAgICAgcmV0dXJuIFtdICAjIFRvbyBjb250ZXh0LXNwZWNpZmljOyBjb3ZlcmVkIGJ5IEZJWCBjb21tZW50IGRldGVjdGlvbgoKICAgIEBzdGF0aWNtZXRob2QKICAgIGRlZiBmaXgodGV4dDogc3RyKSAtPiBzdHI6CiAgICAgICAgcmV0dXJuIHRleHQKCgojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgAojIFBBVFRFUk4gMzMg4oCUIE9SIGNoYWluIGluc3RlYWQgb2YgY2FzZSB0cnVlIG9mICgzKyBjb25kaXRpb25zKQojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgApAX3JlZ2lzdGVyKAogICAgaWQ9Ik9SX0NIQUlOX1VTRV9DQVNFX1RSVUUiLAogICAgZ3JvdXA9IlNob3J0LUNpcmN1aXQgRXZhbHVhdGlvbiIsCiAgICBzZXZlcml0eT0iTUVESVVNIiwKICAgIHRpdGxlPSIzKyBPUiBjb25kaXRpb25zIOKAlCB1c2UgJ2Nhc2UgdHJ1ZSBvZicgZm9yIHNob3J0LWNpcmN1aXQgZXZhbHVhdGlvbiIsCiAgICBkZXNjcmlwdGlvbj0oJ0FMIGV2YWx1YXRlcyBBTEwgb3BlcmFuZHMgb2Ygb3IgYmVmb3JlIHJldHVybmluZyDigJQgZXZlbiBpZiB0aGUgZmlyc3Qgb3BlcmFuZCBpcyBhbHJlYWR5IHRydWUuIFRocmVlIGV4cGVuc2l2ZSBlbGlnaWJpbGl0eSBjaGVja3Mgam9pbmVkIHdpdGggb3IgZWFjaCBleGVjdXRlIGEgQ2FsY1N1bXMgb3IgRmluZFNldCBxdWVyeSDigJQgYWxsIHRocmVlLCBldmVyeSB0aW1lLlxuXG5TUUwgSU1QQUNUOiAzIFNRTCBxdWVyaWVzIHBlciBjdXN0b21lcjogQ2FsY1N1bXMgKyBJc0VtcHR5ICsgQ291bnQuXG5FdmVuIHdoZW4gdGhlIGZpcnN0IGNoZWNrIGFscmVhZHkgcmV0dXJucyB0cnVlIOKAlCB3YXN0ZWQgMiBxdWVyaWVzLlxuY2FzZSB0cnVlIG9mIHdpdGggY29tbWEtc2VwYXJhdGVkIGNvbmRpdGlvbnMgc2hvcnQtY2lyY3VpdHMgYXQgZmlyc3QgdHJ1ZS5cblxuQkFEOiAgaWYgSXNPdmVyQ3JlZGl0TGltaXQoTm8pIG9yIEhhc0Jsb2NrZWRJdGVtcyhObykgb3IgSGFzT3ZlcmR1ZUludm9pY2VzKE5vKSB0aGVuIC4uLlxuR09PRDogY2FzZSB0cnVlIG9mXG4gICAgICAgICAgSXNPdmVyQ3JlZGl0TGltaXQoTm8pLCBIYXNCbG9ja2VkSXRlbXMoTm8pLCBIYXNPdmVyZHVlSW52b2ljZXMoTm8pOlxuICAgICAgICAgICAgICBleGl0KGZhbHNlKTtcbiAgICAgIGVuZDtcblxuSElOVDogQWRkIGEgY291bnRlciBpbnNpZGUgZWFjaCB2YWxpZGF0aW9uIGZ1bmN0aW9uLiBXaXRoIG9yOiBhbGwgdGhyZWUgY291bnRlcnMgaW5jcmVtZW50IGZvciBldmVyeSBjdXN0b21lci4gV2l0aCBjYXNlIHRydWUgb2Y6IG9ubHkgMeKAkzIgY291bnRlcnMgaW5jcmVtZW50IHdoZW4gZmlyc3QgaXMgdHJ1ZS4nKSwKICAgIGV4ZXJjaXNlcz1bMzldLAopCmNsYXNzIFBhdHRlcm5PckNoYWluQ2FzZVRydWU6CiAgICBfUkUgPSByZS5jb21waWxlKHInXGJpZlxiW147XStcYm9yXGJbXjtdK1xib3JcYlteO10rXGJ0aGVuXGInLCByZS5NVUxUSUxJTkUgfCByZS5JR05PUkVDQVNFKQoKICAgIEBzdGF0aWNtZXRob2QKICAgIGRlZiBkZXRlY3QodGV4dDogc3RyKSAtPiBsaXN0W2RpY3RdOgogICAgICAgIGZpbmRpbmdzID0gW10KICAgICAgICBmb3IgbSBpbiBQYXR0ZXJuT3JDaGFpbkNhc2VUcnVlLl9SRS5maW5kaXRlcih0ZXh0KToKICAgICAgICAgICAgbGluZSA9IF9maW5kX2xpbmUodGV4dCwgbS5zdGFydCgpKQogICAgICAgICAgICBsaW5lX3RleHQgPSB0ZXh0LnNwbGl0bGluZXMoKVtsaW5lLTFdLnN0cmlwKCkKICAgICAgICAgICAgaWYgbGluZV90ZXh0LnN0YXJ0c3dpdGgoJy8vJyk6CiAgICAgICAgICAgICAgICBjb250aW51ZQogICAgICAgICAgICAjIENvdW50IGZ1bmN0aW9uIGNhbGxzIGluIHRoZSBPUiBjaGFpbgogICAgICAgICAgICBpZiBsZW4ocmUuZmluZGFsbChyJ1x3K1woJywgbS5ncm91cCgwKSkpID49IDM6CiAgICAgICAgICAgICAgICBmaW5kaW5ncy5hcHBlbmQoewogICAgICAgICAgICAgICAgICAgICJsaW5lIjogbGluZSwKICAgICAgICAgICAgICAgICAgICAic25pcHBldCI6IG0uZ3JvdXAoMClbOjEyMF0sCiAgICAgICAgICAgICAgICAgICAgIm1lc3NhZ2UiOiAiMysgT1IgY29uZGl0aW9ucyB3aXRoIGZ1bmN0aW9uIGNhbGxzIOKAlCB1c2UgJ2Nhc2UgdHJ1ZSBvZicgZm9yIHNob3J0LWNpcmN1aXQiCiAgICAgICAgICAgICAgICB9KQogICAgICAgIHJldHVybiBmaW5kaW5ncwoKICAgIEBzdGF0aWNtZXRob2QKICAgIGRlZiBmaXgodGV4dDogc3RyKSAtPiBzdHI6CiAgICAgICAgcmV0dXJuIHRleHQKCgojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgAojIFBBVFRFUk4gMzQg4oCUIExvY2tUYWJsZSArIFJlYWRJc29sYXRpb24gVXBkTG9jayAoRXg0MikKIyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKQF9yZWdpc3RlcigKICAgIGlkPSJMT0NLVEFCTEVfVVNFX1VQRExPQ0siLAogICAgZ3JvdXA9IkxvY2tpbmciLAogICAgc2V2ZXJpdHk9Ik1FRElVTSIsCiAgICB0aXRsZT0iTG9ja1RhYmxlKCkg4oCUIGNvbnNpZGVyIFJlYWRJc29sYXRpb24gOj0gVXBkTG9jayArIEZpbmRTZXQodHJ1ZSkgaW5zdGVhZCIsCiAgICBkZXNjcmlwdGlvbj0oJ0xvY2tUYWJsZSgpIGFjcXVpcmVzIGEgdGFibGUtbGV2ZWwgRXhjbHVzaXZlIChYKSBsb2NrIG9uIHRoZSBFTlRJUkUgdGFibGUuIEV2ZXJ5IGNvbmN1cnJlbnQgc2Vzc2lvbiDigJQgcmVhZHMsIGFuYWx5dGljcywgb3JkZXIgZW50cnkg4oCUIGJsb2NrcyBmb3IgdGhlIGZ1bGwgdHJhbnNhY3Rpb24gZHVyYXRpb24uIEEgYmF0Y2ggdXBkYXRpbmcgMSAwMDAgcm93cyBjYW4gYmxvY2sgYWxsIG90aGVyIHNlc3Npb25zIGZvciBzZWNvbmRzLlxuXG5TUUwgSU1QQUNUOiBMb2NrVGFibGU6IFgtbG9jayBvbiBlbnRpcmUgdGFibGUg4oCUIGhlbGQgZm9yIGZ1bGwgdHJhbnNhY3Rpb24uXG5SZWFkSXNvbGF0aW9uIDo9IFVwZExvY2sgKyBGaW5kU2V0KHRydWUpOiBhY3F1aXJlcyB1cGRhdGUgbG9ja3Mgb25seSBvbiByb3dzIGFjdHVhbGx5IHJlYWQsIGFsbG93aW5nIGNvbmN1cnJlbnQgcmVhZGVycy5cblxuQkFEOiAgUmVjb3JkLkxvY2tUYWJsZSgpO1xuICAgICAgaWYgUmVjb3JkLkZpbmRTZXQoKSB0aGVuXG5HT09EOiBSZWNvcmQuUmVhZElzb2xhdGlvbiA6PSBJc29sYXRpb25MZXZlbDo6VXBkTG9jaztcbiAgICAgIGlmIFJlY29yZC5GaW5kU2V0KHRydWUpIHRoZW5cblxuSElOVDogT3BlbiB0d28gQkMgc2Vzc2lvbnMgc2ltdWx0YW5lb3VzbHkuIFNlc3Npb24gMTogcnVuIExvY2tUYWJsZS4gU2Vzc2lvbiAyOiB0cnkgYW55IHJlYWQg4oCUIGl0IGJsb2NrcyB1bnRpbCBTZXNzaW9uIDEgY29tbWl0cy4gV2l0aCBVcGRMb2NrOiBTZXNzaW9uIDIgcmVhZHMgZnJlZWx5LicpLAogICAgZXhlcmNpc2VzPVs0Ml0sCikKY2xhc3MgUGF0dGVybkxvY2tUYWJsZVVzZVVwZExvY2s6CiAgICBfUkUgPSByZS5jb21waWxlKHInKFx3KylcLkxvY2tUYWJsZVwoXCknLCByZS5NVUxUSUxJTkUpCgogICAgQHN0YXRpY21ldGhvZAogICAgZGVmIGRldGVjdCh0ZXh0OiBzdHIpIC0+IGxpc3RbZGljdF06CiAgICAgICAgcmV0dXJuIF9maW5kaW5ncyh0ZXh0LCBQYXR0ZXJuTG9ja1RhYmxlVXNlVXBkTG9jay5fUkUsCiAgICAgICAgICAgICAgICAgICAgICAgICBsYW1iZGEgbTogZiJDb25zaWRlciByZXBsYWNpbmcgJ3ttLmdyb3VwKDEpfS5Mb2NrVGFibGUoKScgd2l0aCBSZWFkSXNvbGF0aW9uIDo9IFVwZExvY2sgKyBGaW5kU2V0KHRydWUpIikKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZml4KHRleHQ6IHN0cikgLT4gc3RyOgogICAgICAgIGRlZiByZXBsYWNlKG0pOgogICAgICAgICAgICB2YXIgPSBtLmdyb3VwKDEpCiAgICAgICAgICAgIHBvcyA9IG0uZW5kKCkKICAgICAgICAgICAgYWhlYWQgPSB0ZXh0W3Bvczpwb3MrMjAwXQogICAgICAgICAgICBpZiByZS5zZWFyY2gocmYnXGJ7cmUuZXNjYXBlKHZhcil9XC5GaW5kU2V0XChcKScsIGFoZWFkKToKICAgICAgICAgICAgICAgIHJlcGxhY2VtZW50ID0gZid7dmFyfS5SZWFkSXNvbGF0aW9uIDo9IElzb2xhdGlvbkxldmVsOjpVcGRMb2NrJwogICAgICAgICAgICAgICAgIyBBbHNvIGZpeCB0aGUgRmluZFNldCBpbiBvbmUgcGFzcwogICAgICAgICAgICAgICAgcmV0dXJuIHJlcGxhY2VtZW50CiAgICAgICAgICAgIHJldHVybiBtLmdyb3VwKDApCiAgICAgICAgcmVzdWx0ID0gUGF0dGVybkxvY2tUYWJsZVVzZVVwZExvY2suX1JFLnN1YihyZXBsYWNlLCB0ZXh0KQogICAgICAgIHJlc3VsdCA9IHJlLnN1YihyJyhcdyspXC5SZWFkSXNvbGF0aW9uIDo9IElzb2xhdGlvbkxldmVsOjpVcGRMb2NrO1xzKlxuKFxzKylpZiBcMVwuRmluZFNldFwoXCknLAogICAgICAgICAgICAgICAgICAgICAgICByJ1wxLlJlYWRJc29sYXRpb24gOj0gSXNvbGF0aW9uTGV2ZWw6OlVwZExvY2s7XG5cMmlmIFwxLkZpbmRTZXQodHJ1ZSknLCByZXN1bHQpCiAgICAgICAgcmV0dXJuIHJlc3VsdAoKCiMg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACiMgUEFUVEVSTiAzNSDigJQgdHJ1ZSBpbiBbLi4uXSBjb25kaXRpb24gb3JkZXIgKGNoZWFwZXN0IGZpcnN0KQojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgApAX3JlZ2lzdGVyKAogICAgaWQ9IkNPTkRJVElPTl9PUkRFUl9UUlVFX0lOIiwKICAgIGdyb3VwPSJTaG9ydC1DaXJjdWl0IEV2YWx1YXRpb24iLAogICAgc2V2ZXJpdHk9IkxPVyIsCiAgICB0aXRsZT0iJ2lmIHRydWUgaW4gWy4uLl0nIOKAlCBwdXQgY2hlYXBlc3QgY29uZGl0aW9uIGZpcnN0IGZvciBiZXN0IHNob3J0LWNpcmN1aXQiLAogICAgZGVzY3JpcHRpb249KCJBTCdzIGluIFsuLi5dIG9wZXJhdG9yIERPRVMgc2hvcnQtY2lyY3VpdCDigJQgdW5saWtlIG9yL1x4MDduZC4gQnV0IHBsYWNpbmcgdGhlIG1vc3QgZXhwZW5zaXZlIGNoZWNrIEZJUlNUIGRlZmVhdHMgdGhlIGJlbmVmaXQ6IHRoZSBDYWxjU3VtcyBmaXJlcyBiZWZvcmUgdGhlIGNoZWFwIGJsYWNrbGlzdCBHZXQoKSBpcyBldmVuIHRlc3RlZC4gUnVsZTogYWx3YXlzIG9yZGVyIGNvbmRpdGlvbnMgY2hlYXBlc3QtZmlyc3QgaW4gaW4gWy4uLl0gbGlzdHMuXG5cblNRTCBJTVBBQ1Q6IEV4cGVuc2l2ZS1maXJzdDogQ2FsY1N1bXMgcnVucyBmb3IgfjEwMCUgb2YgY3VzdG9tZXJzLlxuQ2hlYXAtZmlyc3Q6IENhbGNTdW1zIHNraXBwZWQgZm9yIHRoZSB+MzUlIGNhdWdodCBieSBjaGVhcGVyIGNoZWNrcyBmaXJzdC5cblxuQkFEOiAgaWYgdHJ1ZSBpbiBbSGFzRXhjZWVkZWRBbm51YWxCdWRnZXQoTm8pLCBJc0JsYWNrbGlzdGVkKE5vKV0gdGhlbiAuLi5cbkdPT0Q6IGlmIHRydWUgaW4gW0lzQmxhY2tsaXN0ZWQoTm8pLCBIYXNFeGNlZWRlZEFubnVhbEJ1ZGdldChObyldIHRoZW4gLi4uXG5cbkhJTlQ6IEFkZCBhIE1lc3NhZ2UoKSBjb3VudGVyIGluc2lkZSBIYXNFeGNlZWRlZEFubnVhbEJ1ZGdldC4gUnVuIHdpdGggZXhwZW5zaXZlLWZpcnN0OiBjb3VudGVyID0gdG90YWwgY3VzdG9tZXIgY291bnQuIFJlb3JkZXIgdG8gY2hlYXAtZmlyc3Q6IGNvdW50ZXIgZHJvcHMgYnkgfjM1JS4iKSwKICAgIGV4ZXJjaXNlcz1bNDRdLAopCmNsYXNzIFBhdHRlcm5Db25kaXRpb25PcmRlclRydWVJbjoKICAgIF9SRSA9IHJlLmNvbXBpbGUocidpZiB0cnVlIGluIFxbJywgcmUuTVVMVElMSU5FIHwgcmUuSUdOT1JFQ0FTRSkKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZGV0ZWN0KHRleHQ6IHN0cikgLT4gbGlzdFtkaWN0XToKICAgICAgICByZXR1cm4gX2ZpbmRpbmdzKHRleHQsIFBhdHRlcm5Db25kaXRpb25PcmRlclRydWVJbi5fUkUsCiAgICAgICAgICAgICAgICAgICAgICAgICBsYW1iZGEgbTogIlJldmlldyBjb25kaXRpb24gb3JkZXI6IHB1dCBjaGVhcGVzdCBjaGVjayBmaXJzdCBmb3IgYmVzdCBzaG9ydC1jaXJjdWl0IHBlcmZvcm1hbmNlIikKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZml4KHRleHQ6IHN0cikgLT4gc3RyOgogICAgICAgIHJldHVybiB0ZXh0CgoKIyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKIyBQQVRURVJOIDM2IOKAlCBOKzEgbmVzdGVkIEZpbmRTZXQgaW5zaWRlIHJlcGVhdC4udW50aWwgbG9vcCAoRXggMjcgLyA0MykKIyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKQF9yZWdpc3RlcigKICAgIGlkPSJORVNURURfRklORFNFVF9OX1BMVVNfT05FIiwKICAgIGdyb3VwPSJEYXRhIFRyYW5zZmVyIiwKICAgIHNldmVyaXR5PSJISUdIIiwKICAgIHRpdGxlPSJOZXN0ZWQgRmluZFNldC9GaW5kRmlyc3QgaW5zaWRlIHJlcGVhdC4udW50aWwgbG9vcCAoTisxIHF1ZXJpZXMpIiwKICAgIGRlc2NyaXB0aW9uPSgKICAgICAgICAiRm9yIGVhY2ggcm93IGluIHRoZSBvdXRlciBGaW5kU2V0LCBhbiBpbm5lciBGaW5kU2V0IG9uIGEgZGlmZmVyZW50IHZhcmlhYmxlIGZpcmVzIGEgbmV3IFNRTCBxdWVyeS4gIgogICAgICAgICJXaXRoIDEgMDAwIG91dGVyIGl0ZW1zOiAxIG91dGVyIFNFTEVDVCArIDEgMDAwIGlubmVyIFNFTEVDVHMgPSAxIDAwMSBTUUwgcm91bmQtdHJpcHMuICIKICAgICAgICAiQSBRdWVyeSBvYmplY3Qgd2l0aCBHUk9VUCBCWSBkb2VzIHRoZSBzYW1lIHdvcmsgaW4gYSBzaW5nbGUgU1FMIHF1ZXJ5LlxuXG4iCiAgICAgICAgIlNRTCBJTVBBQ1Q6IDEgMDAwIGl0ZW1zIMOXIDEgaW5uZXIgcXVlcnkgPSAxIDAwMSB0b3RhbCBTUUwgcm91bmQtdHJpcHMuXG4iCiAgICAgICAgIlF1ZXJ5IG9iamVjdDogMSBTUUwgcXVlcnkgd2l0aCBHUk9VUCBCWSDigJQgcmVzdWx0IHNldCA9IE4gZ3JvdXBzIG9ubHkuXG5cbiIKICAgICAgICAiQkFEOiAgaWYgSXRlbXMuRmluZFNldCgpIHRoZW4gcmVwZWF0XG4iCiAgICAgICAgIiAgICAgICAgICBTYWxlc0xpbmVzLlNldFJhbmdlKFwiTm8uXCIsIEl0ZW1zLlwiTm8uXCIpO1xuIgogICAgICAgICIgICAgICAgICAgaWYgU2FsZXNMaW5lcy5GaW5kU2V0KCkgdGhlbiByZXBlYXQgLi4uIHVudGlsIFNhbGVzTGluZXMuTmV4dCgpID0gMDtcbiIKICAgICAgICAiICAgICAgdW50aWwgSXRlbXMuTmV4dCgpID0gMDtcbiIKICAgICAgICAiR09PRDogdmFyIEFuYWx5c2lzUXVlcnk6IFF1ZXJ5IFwiV29ya3Nob3AgSXRlbSBBbmFseXNpc1wiO1xuIgogICAgICAgICIgICAgICBpZiBBbmFseXNpc1F1ZXJ5Lk9wZW4oKSB0aGVuXG4iCiAgICAgICAgIiAgICAgICAgICB3aGlsZSBBbmFseXNpc1F1ZXJ5LlJlYWQoKSBkbyAuLi5cblxuIgogICAgICAgICJISU5UOiBDb3VudCBTUUwgcXVlcmllcyBpbiBQcm9maWxlciBmb3IgdGhlIG5lc3RlZCBsb29wLiBFYWNoIG91dGVyIHJvdyBmaXJlcyAxIGlubmVyIFNRTCBxdWVyeS4gIgogICAgICAgICJRdWVyeSBvYmplY3QgdmVyc2lvbjogMSBTUUwgcXVlcnkgd2l0aCBHUk9VUCBCWSByZWdhcmRsZXNzIG9mIGRhdGEgdm9sdW1lLiIKICAgICksCiAgICBleGVyY2lzZXM9WzI3LCA0M10sCikKY2xhc3MgUGF0dGVybk5lc3RlZEZpbmRTZXROUGx1c09uZToKICAgIEBzdGF0aWNtZXRob2QKICAgIGRlZiBkZXRlY3QodGV4dDogc3RyKSAtPiBsaXN0W2RpY3RdOgogICAgICAgIGZpbmRpbmdzID0gW10KICAgICAgICBsaW5lcyA9IHRleHQuc3BsaXRsaW5lcygpCiAgICAgICAgb3V0ZXJfdmFyOiBzdHIgfCBOb25lID0gTm9uZQogICAgICAgIGluX2xvb3AgPSBGYWxzZQoKICAgICAgICBmb3IgaSwgbGluZSBpbiBlbnVtZXJhdGUobGluZXMpOgogICAgICAgICAgICBzdHJpcHBlZCA9IGxpbmUuc3RyaXAoKQogICAgICAgICAgICBpZiBzdHJpcHBlZC5zdGFydHN3aXRoKCcvLycpOgogICAgICAgICAgICAgICAgY29udGludWUKCiAgICAgICAgICAgICMgVHJhY2sgb3V0ZXIgRmluZFNldCBiZWZvcmUgYSByZXBlYXQKICAgICAgICAgICAgbV9vdXRlciA9IHJlLnNlYXJjaChyJyhcdyspXC4oRmluZFNldHxGaW5kRmlyc3QpXCgnLCBsaW5lKQogICAgICAgICAgICBpZiBtX291dGVyIGFuZCBub3QgaW5fbG9vcDoKICAgICAgICAgICAgICAgIG91dGVyX3ZhciA9IG1fb3V0ZXIuZ3JvdXAoMSkKCiAgICAgICAgICAgIGlmIHJlLm1hdGNoKHInXnJlcGVhdFxiJywgc3RyaXBwZWQsIHJlLklHTk9SRUNBU0UpIGFuZCBvdXRlcl92YXI6CiAgICAgICAgICAgICAgICBpbl9sb29wID0gVHJ1ZQoKICAgICAgICAgICAgaWYgaW5fbG9vcDoKICAgICAgICAgICAgICAgIG1faW5uZXIgPSByZS5zZWFyY2gocicoXHcrKVwuKEZpbmRTZXR8RmluZEZpcnN0KVwoJywgbGluZSkKICAgICAgICAgICAgICAgIGlmIG1faW5uZXIgYW5kIG1faW5uZXIuZ3JvdXAoMSkgIT0gb3V0ZXJfdmFyOgogICAgICAgICAgICAgICAgICAgIGZpbmRpbmdzLmFwcGVuZCh7CiAgICAgICAgICAgICAgICAgICAgICAgICJsaW5lIjogaSArIDEsCiAgICAgICAgICAgICAgICAgICAgICAgICJzbmlwcGV0IjogbGluZS5zdHJpcCgpWzoxMjBdLAogICAgICAgICAgICAgICAgICAgICAgICAibWVzc2FnZSI6ICgKICAgICAgICAgICAgICAgICAgICAgICAgICAgIGYiTmVzdGVkIHttX2lubmVyLmdyb3VwKDIpfSgpIG9uICd7bV9pbm5lci5ncm91cCgxKX0nIGluc2lkZSBvdXRlciAiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBmIid7b3V0ZXJfdmFyfScgRmluZFNldCBsb29wIOKAlCBOKzEgU1FMIHF1ZXJpZXMuICIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJDb25zaWRlciBhIFF1ZXJ5IG9iamVjdCB3aXRoIEdST1VQIEJZLiIKICAgICAgICAgICAgICAgICAgICAgICAgKSwKICAgICAgICAgICAgICAgICAgICB9KQoKICAgICAgICAgICAgaWYgcmUubWF0Y2gocidedW50aWxccytcdytcLk5leHRcKFwpJywgc3RyaXBwZWQsIHJlLklHTk9SRUNBU0UpOgogICAgICAgICAgICAgICAgaW5fbG9vcCA9IEZhbHNlCiAgICAgICAgICAgICAgICBvdXRlcl92YXIgPSBOb25lCgogICAgICAgIHJldHVybiBmaW5kaW5ncwoKICAgIEBzdGF0aWNtZXRob2QKICAgIGRlZiBmaXgodGV4dDogc3RyKSAtPiBzdHI6CiAgICAgICAgcmV0dXJuIHRleHQgICMgU3RydWN0dXJhbCBjaGFuZ2UgcmVxdWlyZWQg4oCUIG5vIGF1dG8tZml4CgoKIyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKIyBQQVRURVJOIDM3IOKAlCBBdXRvSW5jcmVtZW50ID0gdHJ1ZSBkaXNhYmxlcyBTUUwgYmF0Y2ggSU5TRVJUIChFeCAzNikKIyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKQF9yZWdpc3RlcigKICAgIGlkPSJBVVRPSU5DUkVNRU5UX0RJU0FCTEVTX0JVTEtfSU5TRVJUIiwKICAgIGdyb3VwPSJCdWxrIE9wZXJhdGlvbnMiLAogICAgc2V2ZXJpdHk9IkhJR0giLAogICAgdGl0bGU9IkF1dG9JbmNyZW1lbnQgPSB0cnVlIG9uIFBLIGRpc2FibGVzIFNRTCBiYXRjaCBJTlNFUlQiLAogICAgZGVzY3JpcHRpb249KAogICAgICAgICJTUUwgU2VydmVyIElERU5USVRZIChBdXRvSW5jcmVtZW50ID0gdHJ1ZSkgcmVxdWlyZXMgU0NPUEVfSURFTlRJVFkoKSB0byBiZSByZXR1cm5lZCAiCiAgICAgICAgImFmdGVyIGVhY2ggSU5TRVJUIOKAlCB0aGlzIHByZXZlbnRzIGJhdGNoIElOU0VSVCBlbnRpcmVseS4gIgogICAgICAgICJGb3IgMTAgMDAwIGNvdW50IHNoZWV0IGxpbmVzOiAxMCAwMDAgaW5kaXZpZHVhbCBTUUwgSU5TRVJUcywgb25lIHBlciByb3cuICIKICAgICAgICAiTWFudWFsIFBLIGFzc2lnbm1lbnQgdmlhIE51bWJlclNlcXVlbmNlIGFsbG93cyBTUUwgU2VydmVyIHRvIGJhdGNoIHJvd3MgaW50byBvbmUgdHJpcC5cblxuIgogICAgICAgICJTUUwgSU1QQUNUOiBBdXRvSW5jcmVtZW50OiAxMCAwMDAgw5cgSU5TRVJUICgrIFNDT1BFX0lERU5USVRZIHBlciByb3cpLlxuIgogICAgICAgICJNYW51YWwgUEsgd2l0aCBOdW1iZXJTZXF1ZW5jZTogfjEwIGJhdGNoIElOU0VSVCBzdGF0ZW1lbnRzIOKAlCBvcmRlcnMgb2YgbWFnbml0dWRlIGZhc3Rlci5cblxuIgogICAgICAgICJCQUQ6ICBmaWVsZCgxOyBJZDsgSW50ZWdlcikgeyBBdXRvSW5jcmVtZW50ID0gdHJ1ZTsgfVxuIgogICAgICAgICJHT09EOiBmaWVsZCgxOyBJZDsgSW50ZWdlcikgeyB9XG4iCiAgICAgICAgIiAgICAgIC8vIEluIGNvZGU6XG4iCiAgICAgICAgIiAgICAgIEVudHJ5LklkIDo9IE51bWJlclNlcXVlbmNlLk5leHQoJ1dTX0NPVU5UX1NFUScpO1xuIgogICAgICAgICIgICAgICBFbnRyeS5JbnNlcnQoZmFsc2UpOyAvLyBCYXRjaGFibGUg4oCUIG5vIGlkZW50aXR5IHJldHVybiBuZWVkZWRcblxuIgogICAgICAgICJISU5UOiBJbnNlcnQgMTAgMDAwIHJvd3MgaW50byBib3RoIHRhYmxlcyBpbiBTUUwgUHJvZmlsZXIuICIKICAgICAgICAiQXV0b0luY3JlbWVudCB0YWJsZTogMTAgMDAwIGluZGl2aWR1YWwgSU5TRVJUIHN0YXRlbWVudHMuICIKICAgICAgICAiTWFudWFsIGtleSB0YWJsZTogfjEwIGJhdGNoIElOU0VSVCBzdGF0ZW1lbnRzIHRvdGFsLiIKICAgICksCiAgICBleGVyY2lzZXM9WzM2XSwKKQpjbGFzcyBQYXR0ZXJuQXV0b0luY3JlbWVudEJ1bGtJbnNlcnQ6CiAgICBfUkUgPSByZS5jb21waWxlKHInQXV0b0luY3JlbWVudFxzKj1ccyp0cnVlJywgcmUuSUdOT1JFQ0FTRSkKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgZGV0ZWN0KHRleHQ6IHN0cikgLT4gbGlzdFtkaWN0XToKICAgICAgICByZXR1cm4gX2ZpbmRpbmdzKAogICAgICAgICAgICB0ZXh0LAogICAgICAgICAgICBQYXR0ZXJuQXV0b0luY3JlbWVudEJ1bGtJbnNlcnQuX1JFLAogICAgICAgICAgICBsYW1iZGEgbTogKAogICAgICAgICAgICAgICAgIkF1dG9JbmNyZW1lbnQgPSB0cnVlIHJlcXVpcmVzIFNDT1BFX0lERU5USVRZKCkgcGVyIElOU0VSVCDigJQgcHJldmVudHMgU1FMIGJhdGNoIGluc2VydHMuICIKICAgICAgICAgICAgICAgICJSZW1vdmUgQXV0b0luY3JlbWVudCBhbmQgdXNlIE51bWJlclNlcXVlbmNlLk5leHQoKSBmb3IgUEsgYXNzaWdubWVudC4iCiAgICAgICAgICAgICksCiAgICAgICAgKQoKICAgIEBzdGF0aWNtZXRob2QKICAgIGRlZiBmaXgodGV4dDogc3RyKSAtPiBzdHI6CiAgICAgICAgcmV0dXJuIHRleHQgICMgUmVxdWlyZXMgdGFibGUgcmVkZXNpZ24gKyBjb2RlIGNoYW5nZSDigJQgbm8gYXV0by1maXgKCgojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgAojIFBBVFRFUk4gMzgg4oCUIEFMLXNpZGUgR1JPVVAgQlkgYWdncmVnYXRpb24gKEV4IDQzKQojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgApAX3JlZ2lzdGVyKAogICAgaWQ9IkFMX1NJREVfQUdHUkVHQVRJT04iLAogICAgZ3JvdXA9IkFnZ3JlZ2F0aW9uIiwKICAgIHNldmVyaXR5PSJISUdIIiwKICAgIHRpdGxlPSJBTC1zaWRlIEdST1VQIEJZIGFnZ3JlZ2F0aW9uIOKAlCBwdXNoIHRvIFNRTCB3aXRoIGEgUXVlcnkgb2JqZWN0IiwKICAgIGRlc2NyaXB0aW9uPSgKICAgICAgICAiTWFudWFsIEFMIGFnZ3JlZ2F0aW9uOiBGaW5kU2V0IGZldGNoZXMgQUxMIHJvd3MgaW50byBOU1QsIGFjY3VtdWxhdGVzIHN1bXMgcGVyIGdyb3VwIGluIEFMLiAiCiAgICAgICAgIkZvciAxMDAgMDAwIGVudHJpZXMg4oCUIDEwMCAwMDAgcm93cyB0cmFuc2ZlcnJlZCB0byBOU1QgbWVtb3J5LiAiCiAgICAgICAgIkEgUXVlcnkgb2JqZWN0IHdpdGggTWV0aG9kID0gU3VtIHB1c2hlcyBHUk9VUCBCWSBhbmQgU1VNKCkgdG8gU1FMIFNlcnZlci4gIgogICAgICAgICJTUUwgcmV0dXJucyBvbmx5IE4gcm93cyAob25lIHBlciBncm91cCkg4oCUIDk5IDk5MCByb3dzIG5ldmVyIGxlYXZlIHRoZSBkYXRhYmFzZS5cblxuIgogICAgICAgICJTUUwgSU1QQUNUOiBBTCBsb29wOiAgU0VMRUNUIGFsbCAxMDAgMDAwIHJvd3MsIFNVTSBpbiBOU1QgbWVtb3J5LlxuIgogICAgICAgICJRdWVyeTogICAgIFNFTEVDVCBMb2NhdGlvbiwgU1VNKEFtb3VudCkgR1JPVVAgQlkgTG9jYXRpb24g4oaSIDEwIHJvd3Mgb25seS5cblxuIgogICAgICAgICJCQUQ6ICBpZiBXb3Jrc2hvcERhdGEuRmluZFNldCgpIHRoZW4gcmVwZWF0XG4iCiAgICAgICAgIiAgICAgICAgICBUb3RhbEJ5TG9jYXRpb24uQWRkKFdvcmtzaG9wRGF0YS5cIkxvY2F0aW9uIENvZGVcIiwgV29ya3Nob3BEYXRhLkFtb3VudCk7XG4iCiAgICAgICAgIiAgICAgIHVudGlsIFdvcmtzaG9wRGF0YS5OZXh0KCkgPSAwO1xuIgogICAgICAgICJHT09EOiB2YXIgUmV2ZW51ZVF1ZXJ5OiBRdWVyeSBcIldTIExvY2F0aW9uIFJldmVudWVcIjtcbiIKICAgICAgICAiICAgICAgaWYgUmV2ZW51ZVF1ZXJ5Lk9wZW4oKSB0aGVuXG4iCiAgICAgICAgIiAgICAgICAgICB3aGlsZSBSZXZlbnVlUXVlcnkuUmVhZCgpIGRvXG4iCiAgICAgICAgIiAgICAgICAgICAgICAgUHJvY2Vzc1N1bW1hcnkoUmV2ZW51ZVF1ZXJ5LkxvY2F0aW9uX0NvZGUsIFJldmVudWVRdWVyeS5TdW1fTGluZV9BbW91bnQpO1xuXG4iCiAgICAgICAgIkhJTlQ6IFJ1biB0aGUgbWFudWFsIGxvb3Agb24gNTAgMDAwIHJvd3MuIENoZWNrIFNRTCBQcm9maWxlcjogY291bnQgU0VMRUNUIHJvd3MgcmV0dXJuZWQuICIKICAgICAgICAiVGhlbiBzd2l0Y2ggdG8gdGhlIFF1ZXJ5IHZlcnNpb24uIFByb2ZpbGVyIHNob3dzIDEgcXVlcnkgcmV0dXJuaW5nIG9ubHkgTiByb3dzLiAiCiAgICAgICAgIkR1cmF0aW9uIGRpZmZlcmVuY2UgaXMgZHJhbWF0aWMgYXQgc2NhbGUuIgogICAgKSwKICAgIGV4ZXJjaXNlcz1bNDNdLAopCmNsYXNzIFBhdHRlcm5BbFNpZGVBZ2dyZWdhdGlvbjoKICAgICMgRGV0ZWN0IEZpbmRTZXQgbG9vcCB0aGF0IGFjY3VtdWxhdGVzIGludG8gYSBEaWN0aW9uYXJ5IGJ5IGdyb3VwIGtleQogICAgX1JFID0gcmUuY29tcGlsZSgKICAgICAgICByJ0ZpbmRTZXRccypcKFteKV0qXCkuKj9yZXBlYXQuKj9cLkFkZFxzKlwoJywKICAgICAgICByZS5JR05PUkVDQVNFIHwgcmUuRE9UQUxMLAogICAgKQoKICAgIEBzdGF0aWNtZXRob2QKICAgIGRlZiBkZXRlY3QodGV4dDogc3RyKSAtPiBsaXN0W2RpY3RdOgogICAgICAgIGZpbmRpbmdzID0gW10KICAgICAgICBmb3IgbSBpbiBQYXR0ZXJuQWxTaWRlQWdncmVnYXRpb24uX1JFLmZpbmRpdGVyKHRleHQpOgogICAgICAgICAgICBsaW5lID0gX2ZpbmRfbGluZSh0ZXh0LCBtLnN0YXJ0KCkpCiAgICAgICAgICAgIHNuaXBwZXQgPSB0ZXh0W20uc3RhcnQoKTogbS5zdGFydCgpICsgODBdLnNwbGl0KCdcbicpWzBdLnN0cmlwKCkKICAgICAgICAgICAgZmluZGluZ3MuYXBwZW5kKHsKICAgICAgICAgICAgICAgICJsaW5lIjogbGluZSwKICAgICAgICAgICAgICAgICJtZXNzYWdlIjogKAogICAgICAgICAgICAgICAgICAgICJBTC1zaWRlIEdST1VQIEJZIGFnZ3JlZ2F0aW9uIOKAlCBmZXRjaGVzIGFsbCByb3dzIHRvIE5TVC4gIgogICAgICAgICAgICAgICAgICAgICJVc2UgYSBRdWVyeSBvYmplY3Qgd2l0aCBNZXRob2QgPSBTdW0gdG8gcHVzaCBHUk9VUCBCWSB0byBTUUwgU2VydmVyLiIKICAgICAgICAgICAgICAgICksCiAgICAgICAgICAgICAgICAic25pcHBldCI6IHNuaXBwZXQsCiAgICAgICAgICAgIH0pCiAgICAgICAgcmV0dXJuIGZpbmRpbmdzCgogICAgQHN0YXRpY21ldGhvZAogICAgZGVmIGZpeCh0ZXh0OiBzdHIpIC0+IHN0cjoKICAgICAgICByZXR1cm4gdGV4dCAgIyBTdHJ1Y3R1cmFsIGNoYW5nZSByZXF1aXJlZCDigJQgbm8gYXV0by1maXgKCgojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgAojIFV0aWxpdHkgaGVscGVycwojIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgAoKZGVmIF9zY2FuX2ZpbGUocGF0aDogUGF0aCkgLT4gbGlzdFtkaWN0XToKICAgICIiIlJ1biBhbGwgZGV0ZWN0b3JzIG9uIGEgc2luZ2xlIEFMIGZpbGUuIFJldHVybnMgbGlzdCBvZiBmaW5kaW5nIGRpY3RzLiIiIgogICAgdHJ5OgogICAgICAgIHRleHQgPSBwYXRoLnJlYWRfdGV4dChlbmNvZGluZz0ndXRmLTgtc2lnJykKICAgIGV4Y2VwdCBFeGNlcHRpb24gYXMgZToKICAgICAgICByZXR1cm4gW3sicGF0dGVybiI6ICJSRUFEX0VSUk9SIiwgImZpbGUiOiBzdHIocGF0aCksICJsaW5lIjogMCwgInNuaXBwZXQiOiBzdHIoZSksICJtZXNzYWdlIjogIkNvdWxkIG5vdCByZWFkIGZpbGUifV0KCiAgICBmaW5kaW5ncyA9IFtdCiAgICBmb3IgcCBpbiBQQVRURVJOUzoKICAgICAgICBmb3IgZiBpbiBwWyJkZXRlY3RvciJdKHRleHQpOgogICAgICAgICAgICBmaW5kaW5ncy5hcHBlbmQoewogICAgICAgICAgICAgICAgInBhdHRlcm4iOiBwWyJpZCJdLAogICAgICAgICAgICAgICAgInNldmVyaXR5IjogcFsic2V2ZXJpdHkiXSwKICAgICAgICAgICAgICAgICJ0aXRsZSI6IHBbInRpdGxlIl0sCiAgICAgICAgICAgICAgICAiZmlsZSI6IHN0cihwYXRoKSwKICAgICAgICAgICAgICAgICoqZgogICAgICAgICAgICB9KQogICAgcmV0dXJuIGZpbmRpbmdzCgoKZGVmIF9maXhfZmlsZShwYXRoOiBQYXRoKSAtPiB0dXBsZVtzdHIsIGxpc3Rbc3RyXV06CiAgICAiIiJBcHBseSBhbGwgZml4ZXJzIHRvIGEgc2luZ2xlIEFMIGZpbGUuIFJldHVybnMgKG5ld190ZXh0LCBsaXN0X29mX2FwcGxpZWRfZml4ZXMpLiIiIgogICAgdHJ5OgogICAgICAgIHRleHQgPSBwYXRoLnJlYWRfdGV4dChlbmNvZGluZz0ndXRmLTgtc2lnJykKICAgIGV4Y2VwdCBFeGNlcHRpb24gYXMgZToKICAgICAgICByZXR1cm4gIiIsIFtmIlJFQURfRVJST1I6IHtlfSJdCgogICAgYXBwbGllZCA9IFtdCiAgICBvcmlnaW5hbCA9IHRleHQKICAgIGZvciBwIGluIFBBVFRFUk5TOgogICAgICAgIG5ld190ZXh0ID0gcFsiZml4ZXIiXSh0ZXh0KQogICAgICAgIGlmIG5ld190ZXh0ICE9IHRleHQ6CiAgICAgICAgICAgIGFwcGxpZWQuYXBwZW5kKHBbImlkIl0pCiAgICAgICAgICAgIHRleHQgPSBuZXdfdGV4dAoKICAgIHJldHVybiB0ZXh0LCBhcHBsaWVkCgoKZGVmIF9hbF9maWxlcyhmb2xkZXI6IFBhdGgpIC0+IGxpc3RbUGF0aF06CiAgICByZXR1cm4gc29ydGVkKGZvbGRlci5yZ2xvYigiKi5hbCIpKQoKCiMg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACiMgTUNQIFRvb2xzCiMg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACgpAbWNwLnRvb2woKQpkZWYgbGlzdF9wYXR0ZXJucygpIC0+IHN0cjoKICAgICIiIgogICAgTGlzdCBhbGwga25vd24gQUwgcGVyZm9ybWFuY2UgYW50aS1wYXR0ZXJucyB3aXRoIElEcywgc2V2ZXJpdHksIGFuZCBzaG9ydCBkZXNjcmlwdGlvbnMuCiAgICBVc2UgdGhpcyB0byB1bmRlcnN0YW5kIHdoYXQgdGhlIGFuYWx5emVyIGNhbiBkZXRlY3QgYmVmb3JlIHJ1bm5pbmcgYSBzY2FuLgogICAgIiIiCiAgICBsaW5lcyA9IFsiIyBBTCBQZXJmb3JtYW5jZSBQYXR0ZXJuc1xuIl0KICAgIGdyb3VwczogZGljdFtzdHIsIGxpc3RdID0ge30KICAgIGZvciBwIGluIFBBVFRFUk5TOgogICAgICAgIGdyb3Vwcy5zZXRkZWZhdWx0KHBbImdyb3VwIl0sIFtdKS5hcHBlbmQocCkKCiAgICBmb3IgZ3JvdXAsIGl0ZW1zIGluIHNvcnRlZChncm91cHMuaXRlbXMoKSk6CiAgICAgICAgbGluZXMuYXBwZW5kKGYiXG4jIyB7Z3JvdXB9XG4iKQogICAgICAgIGZvciBwIGluIGl0ZW1zOgogICAgICAgICAgICBsaW5lcy5hcHBlbmQoZiItICoqW3twWydzZXZlcml0eSddfV0qKiBge3BbJ2lkJ119YCAgXG4gIHtwWyd0aXRsZSddfSAgXG4gIEV4ZXJjaXNlczoge3BbJ2V4ZXJjaXNlcyddfVxuIikKCiAgICByZXR1cm4gIlxuIi5qb2luKGxpbmVzKQoKCkBtY3AudG9vbCgpCmRlZiBleHBsYWluX3BhdHRlcm4ocGF0dGVybl9pZDogc3RyKSAtPiBzdHI6CiAgICAiIiIKICAgIEdldCBhIGZ1bGwgZXhwbGFuYXRpb24gb2YgYSBzcGVjaWZpYyBwZXJmb3JtYW5jZSBwYXR0ZXJuIGluY2x1ZGluZyBleGFtcGxlcyBhbmQgdGhlIGZpeC4KCiAgICBBcmdzOgogICAgICAgIHBhdHRlcm5faWQ6IFRoZSBwYXR0ZXJuIElEIChlLmcuICdNSVNTSU5HX1NFVExPQURGSUVMRFMnKS4gVXNlIGxpc3RfcGF0dGVybnMoKSB0byBzZWUgYWxsIElEcy4KICAgICIiIgogICAgZm9yIHAgaW4gUEFUVEVSTlM6CiAgICAgICAgaWYgcFsiaWQiXS51cHBlcigpID09IHBhdHRlcm5faWQudXBwZXIoKToKICAgICAgICAgICAgcmV0dXJuICgKICAgICAgICAgICAgICAgIGYiIyB7cFsndGl0bGUnXX1cblxuIgogICAgICAgICAgICAgICAgZiIqKklEOioqIGB7cFsnaWQnXX1gICBcbiIKICAgICAgICAgICAgICAgIGYiKipTZXZlcml0eToqKiB7cFsnc2V2ZXJpdHknXX0gIFxuIgogICAgICAgICAgICAgICAgZiIqKkNhdGVnb3J5OioqIHtwWydncm91cCddfSAgXG4iCiAgICAgICAgICAgICAgICBmIioqV29ya3Nob3AgRXhlcmNpc2VzOioqIHtwWydleGVyY2lzZXMnXX1cblxuIgogICAgICAgICAgICAgICAgZiJ7cFsnZGVzY3JpcHRpb24nXX0iCiAgICAgICAgICAgICkKICAgIGlkcyA9ICIsICIuam9pbihwWyJpZCJdIGZvciBwIGluIFBBVFRFUk5TKQogICAgcmV0dXJuIGYiUGF0dGVybiAne3BhdHRlcm5faWR9JyBub3QgZm91bmQuIEtub3duIElEczoge2lkc30iCgoKQG1jcC50b29sKCkKZGVmIHNjYW5fYWxfd29ya3NwYWNlKAogICAgZm9sZGVyX3BhdGg6IHN0ciwKICAgIHNldmVyaXR5X2ZpbHRlcjogc3RyID0gIkFMTCIsCiAgICBncm91cF9maWx0ZXI6IHN0ciA9ICJBTEwiLAopIC0+IHN0cjoKICAgICIiIgogICAgU2NhbiBhbGwgQUwgZmlsZXMgaW4gYSBmb2xkZXIgKHJlY3Vyc2l2ZWx5KSBmb3IgcGVyZm9ybWFuY2UgYW50aS1wYXR0ZXJucy4KICAgIFJldHVybnMgYSBzdHJ1Y3R1cmVkIHJlcG9ydCBvZiBmaW5kaW5ncyBncm91cGVkIGJ5IGZpbGUuCgogICAgQXJnczoKICAgICAgICBmb2xkZXJfcGF0aDogQWJzb2x1dGUgcGF0aCB0byB0aGUgZm9sZGVyIGNvbnRhaW5pbmcgQUwgc291cmNlIGZpbGVzLgogICAgICAgIHNldmVyaXR5X2ZpbHRlcjogRmlsdGVyIGJ5IHNldmVyaXR5OiBISUdILCBNRURJVU0sIExPVywgb3IgQUxMIChkZWZhdWx0IEFMTCkuCiAgICAgICAgZ3JvdXBfZmlsdGVyOiBGaWx0ZXIgYnkgcGF0dGVybiBncm91cCBuYW1lIG9yIEFMTCAoZGVmYXVsdCBBTEwpLgogICAgIiIiCiAgICBmb2xkZXIgPSBQYXRoKGZvbGRlcl9wYXRoKQogICAgaWYgbm90IGZvbGRlci5leGlzdHMoKToKICAgICAgICByZXR1cm4gZiJFUlJPUjogRm9sZGVyIG5vdCBmb3VuZDoge2ZvbGRlcl9wYXRofSIKCiAgICBmaWxlcyA9IF9hbF9maWxlcyhmb2xkZXIpCiAgICBpZiBub3QgZmlsZXM6CiAgICAgICAgcmV0dXJuIGYiTm8gLmFsIGZpbGVzIGZvdW5kIGluIHtmb2xkZXJfcGF0aH0iCgogICAgYWxsX2ZpbmRpbmdzOiBsaXN0W2RpY3RdID0gW10KICAgIGZvciBmIGluIGZpbGVzOgogICAgICAgIGFsbF9maW5kaW5ncy5leHRlbmQoX3NjYW5fZmlsZShmKSkKCiAgICAjIEFwcGx5IGZpbHRlcnMKICAgIGlmIHNldmVyaXR5X2ZpbHRlci51cHBlcigpICE9ICJBTEwiOgogICAgICAgIGFsbF9maW5kaW5ncyA9IFtmIGZvciBmIGluIGFsbF9maW5kaW5ncyBpZiBmWyJzZXZlcml0eSJdLnVwcGVyKCkgPT0gc2V2ZXJpdHlfZmlsdGVyLnVwcGVyKCldCiAgICBpZiBncm91cF9maWx0ZXIudXBwZXIoKSAhPSAiQUxMIjoKICAgICAgICBhbGxfZmluZGluZ3MgPSBbZiBmb3IgZiBpbiBhbGxfZmluZGluZ3MgaWYKICAgICAgICAgICAgICAgICAgICAgICAgYW55KHBbImdyb3VwIl0udXBwZXIoKSA9PSBncm91cF9maWx0ZXIudXBwZXIoKQogICAgICAgICAgICAgICAgICAgICAgICAgICAgZm9yIHAgaW4gUEFUVEVSTlMgaWYgcFsiaWQiXSA9PSBmWyJwYXR0ZXJuIl0pXQoKICAgIGlmIG5vdCBhbGxfZmluZGluZ3M6CiAgICAgICAgcmV0dXJuIGYi4pyFIE5vIGlzc3VlcyBmb3VuZCAoe2xlbihmaWxlcyl9IGZpbGVzIHNjYW5uZWQpLiIKCiAgICAjIEdyb3VwIGJ5IGZpbGUKICAgIGJ5X2ZpbGU6IGRpY3Rbc3RyLCBsaXN0XSA9IHt9CiAgICBmb3IgZiBpbiBhbGxfZmluZGluZ3M6CiAgICAgICAgYnlfZmlsZS5zZXRkZWZhdWx0KGZbImZpbGUiXSwgW10pLmFwcGVuZChmKQoKICAgIHNldmVyaXR5X29yZGVyID0geyJISUdIIjogMCwgIk1FRElVTSI6IDEsICJMT1ciOiAyfQogICAgdG90YWwgPSBsZW4oYWxsX2ZpbmRpbmdzKQogICAgaGlnaCA9IHN1bSgxIGZvciBmIGluIGFsbF9maW5kaW5ncyBpZiBmWyJzZXZlcml0eSJdID09ICJISUdIIikKICAgIG1lZGl1bSA9IHN1bSgxIGZvciBmIGluIGFsbF9maW5kaW5ncyBpZiBmWyJzZXZlcml0eSJdID09ICJNRURJVU0iKQogICAgbG93ID0gc3VtKDEgZm9yIGYgaW4gYWxsX2ZpbmRpbmdzIGlmIGZbInNldmVyaXR5Il0gPT0gIkxPVyIpCgogICAgbGluZXMgPSBbCiAgICAgICAgZiIjIEFMIFBlcmZvcm1hbmNlIFNjYW4gUmVzdWx0c1xuIiwKICAgICAgICBmIioqRm9sZGVyOioqIGB7Zm9sZGVyX3BhdGh9YCAgIiwKICAgICAgICBmIioqRmlsZXMgc2Nhbm5lZDoqKiB7bGVuKGZpbGVzKX0gICIsCiAgICAgICAgZiIqKklzc3VlcyBmb3VuZDoqKiB7dG90YWx9ICh7aGlnaH0gSElHSCwge21lZGl1bX0gTUVESVVNLCB7bG93fSBMT1cpXG4iLAogICAgXQoKICAgIGZvciBmaWxlcGF0aCwgZmluZGluZ3MgaW4gc29ydGVkKGJ5X2ZpbGUuaXRlbXMoKSk6CiAgICAgICAgcmVsID0gUGF0aChmaWxlcGF0aCkucmVsYXRpdmVfdG8oZm9sZGVyKSBpZiBQYXRoKGZpbGVwYXRoKS5pc19yZWxhdGl2ZV90byhmb2xkZXIpIGVsc2UgUGF0aChmaWxlcGF0aCkubmFtZQogICAgICAgIGxpbmVzLmFwcGVuZChmIlxuIyMgYHtyZWx9YFxuIikKICAgICAgICBmaW5kaW5nc19zb3J0ZWQgPSBzb3J0ZWQoZmluZGluZ3MsIGtleT1sYW1iZGEgeDogc2V2ZXJpdHlfb3JkZXIuZ2V0KHhbInNldmVyaXR5Il0sIDkpKQogICAgICAgIGZvciBmIGluIGZpbmRpbmdzX3NvcnRlZDoKICAgICAgICAgICAgc2V2X2ljb24gPSB7IkhJR0giOiAi8J+UtCIsICJNRURJVU0iOiAi8J+foSIsICJMT1ciOiAi8J+UtSJ9LmdldChmWyJzZXZlcml0eSJdLCAi4pqqIikKICAgICAgICAgICAgbGluZXMuYXBwZW5kKGYiLSB7c2V2X2ljb259ICoqTHtmWydsaW5lJ119KiogYHtmWydwYXR0ZXJuJ119YDoge2ZbJ21lc3NhZ2UnXX0iKQogICAgICAgICAgICBpZiBmLmdldCgic25pcHBldCIpOgogICAgICAgICAgICAgICAgbGluZXMuYXBwZW5kKGYiICBgYGBhbFxuICB7Zlsnc25pcHBldCddfVxuICBgYGAiKQoKICAgIHJldHVybiAiXG4iLmpvaW4obGluZXMpCgoKQG1jcC50b29sKCkKZGVmIGZpeF9hbF9maWxlKAogICAgZmlsZV9wYXRoOiBzdHIsCiAgICBkcnlfcnVuOiBib29sID0gVHJ1ZSwKKSAtPiBzdHI6CiAgICAiIiIKICAgIEFwcGx5IGFsbCBhdmFpbGFibGUgYXV0b21hdGljIGZpeGVzIHRvIGEgc2luZ2xlIEFMIGZpbGUuCgogICAgQXJnczoKICAgICAgICBmaWxlX3BhdGg6IEFic29sdXRlIHBhdGggdG8gdGhlIC5hbCBmaWxlIHRvIGZpeC4KICAgICAgICBkcnlfcnVuOiBJZiBUcnVlIChkZWZhdWx0KSwgc2hvdyB3aGF0IHdvdWxkIGNoYW5nZSB3aXRob3V0IHdyaXRpbmcuIFNldCBGYWxzZSB0byB3cml0ZS4KICAgICIiIgogICAgcGF0aCA9IFBhdGgoZmlsZV9wYXRoKQogICAgaWYgbm90IHBhdGguZXhpc3RzKCk6CiAgICAgICAgcmV0dXJuIGYiRVJST1I6IEZpbGUgbm90IGZvdW5kOiB7ZmlsZV9wYXRofSIKCiAgICBvcmlnaW5hbCA9IHBhdGgucmVhZF90ZXh0KGVuY29kaW5nPSd1dGYtOC1zaWcnKQogICAgbmV3X3RleHQsIGFwcGxpZWQgPSBfZml4X2ZpbGUocGF0aCkKCiAgICBpZiBub3QgYXBwbGllZDoKICAgICAgICByZXR1cm4gZiLinIUgTm8gYXV0b21hdGljIGZpeGVzIGFwcGxpY2FibGUgdG8gYHtwYXRoLm5hbWV9YC4iCgogICAgbGluZXMgPSBbZiIjIEZpeCBSZXBvcnQ6IGB7cGF0aC5uYW1lfWBcbiJdCiAgICBsaW5lcy5hcHBlbmQoZiIqKkZpeGVzIGFwcGxpZWQ6KiogeycsICcuam9pbihhcHBsaWVkKX1cbiIpCgogICAgaWYgZHJ5X3J1bjoKICAgICAgICBsaW5lcy5hcHBlbmQoIioqTW9kZToqKiBEUlkgUlVOIOKAlCBubyBmaWxlIHdyaXR0ZW4uIFNldCBkcnlfcnVuPUZhbHNlIHRvIGFwcGx5LlxuIikKICAgICAgICAjIFNob3cgZGlmZiBzdW1tYXJ5CiAgICAgICAgb3JpZ19saW5lcyA9IG9yaWdpbmFsLnNwbGl0bGluZXMoKQogICAgICAgIG5ld19saW5lcyA9IG5ld190ZXh0LnNwbGl0bGluZXMoKQogICAgICAgIGNoYW5nZXMgPSAwCiAgICAgICAgZm9yIGksIChvLCBuKSBpbiBlbnVtZXJhdGUoemlwKG9yaWdfbGluZXMsIG5ld19saW5lcykpOgogICAgICAgICAgICBpZiBvICE9IG46CiAgICAgICAgICAgICAgICBsaW5lcy5hcHBlbmQoZiJMe2krMX06IGB7by5zdHJpcCgpfWAg4oaSIGB7bi5zdHJpcCgpfWAiKQogICAgICAgICAgICAgICAgY2hhbmdlcyArPSAxCiAgICAgICAgICAgICAgICBpZiBjaGFuZ2VzID49IDIwOgogICAgICAgICAgICAgICAgICAgIGxpbmVzLmFwcGVuZChmIi4uLiAoYW5kIG1vcmUpIikKICAgICAgICAgICAgICAgICAgICBicmVhawogICAgZWxzZToKICAgICAgICBwYXRoLndyaXRlX3RleHQobmV3X3RleHQsIGVuY29kaW5nPSd1dGYtOC1zaWcnKQogICAgICAgIGxpbmVzLmFwcGVuZChmIuKchSBGaWxlIHdyaXR0ZW46IGB7ZmlsZV9wYXRofWAiKQoKICAgIHJldHVybiAiXG4iLmpvaW4obGluZXMpCgoKQG1jcC50b29sKCkKZGVmIGZpeF9hbF93b3Jrc3BhY2UoCiAgICBmb2xkZXJfcGF0aDogc3RyLAogICAgZHJ5X3J1bjogYm9vbCA9IFRydWUsCikgLT4gc3RyOgogICAgIiIiCiAgICBBcHBseSBhbGwgYXZhaWxhYmxlIGF1dG9tYXRpYyBmaXhlcyB0byBldmVyeSBBTCBmaWxlIGluIGEgZm9sZGVyIChyZWN1cnNpdmVseSkuCgogICAgQXJnczoKICAgICAgICBmb2xkZXJfcGF0aDogQWJzb2x1dGUgcGF0aCB0byB0aGUgZm9sZGVyIGNvbnRhaW5pbmcgQUwgc291cmNlIGZpbGVzLgogICAgICAgIGRyeV9ydW46IElmIFRydWUgKGRlZmF1bHQpLCBzaG93IHdoYXQgd291bGQgY2hhbmdlIHdpdGhvdXQgd3JpdGluZy4gU2V0IEZhbHNlIHRvIHdyaXRlLgogICAgIiIiCiAgICBmb2xkZXIgPSBQYXRoKGZvbGRlcl9wYXRoKQogICAgaWYgbm90IGZvbGRlci5leGlzdHMoKToKICAgICAgICByZXR1cm4gZiJFUlJPUjogRm9sZGVyIG5vdCBmb3VuZDoge2ZvbGRlcl9wYXRofSIKCiAgICBmaWxlcyA9IF9hbF9maWxlcyhmb2xkZXIpCiAgICBpZiBub3QgZmlsZXM6CiAgICAgICAgcmV0dXJuIGYiTm8gLmFsIGZpbGVzIGZvdW5kIGluIHtmb2xkZXJfcGF0aH0iCgogICAgcmVzdWx0cyA9IFtdCiAgICB0b3RhbF9maXhlcyA9IDAKICAgIGZvciBmIGluIGZpbGVzOgogICAgICAgIG9yaWdpbmFsID0gZi5yZWFkX3RleHQoZW5jb2Rpbmc9J3V0Zi04LXNpZycpCiAgICAgICAgbmV3X3RleHQsIGFwcGxpZWQgPSBfZml4X2ZpbGUoZikKICAgICAgICBpZiBhcHBsaWVkOgogICAgICAgICAgICB0b3RhbF9maXhlcyArPSBsZW4oYXBwbGllZCkKICAgICAgICAgICAgcmVzdWx0cy5hcHBlbmQoKGYsIGFwcGxpZWQsIG9yaWdpbmFsICE9IG5ld190ZXh0KSkKICAgICAgICAgICAgaWYgbm90IGRyeV9ydW4gYW5kIG9yaWdpbmFsICE9IG5ld190ZXh0OgogICAgICAgICAgICAgICAgZi53cml0ZV90ZXh0KG5ld190ZXh0LCBlbmNvZGluZz0ndXRmLTgtc2lnJykKCiAgICBpZiBub3QgcmVzdWx0czoKICAgICAgICByZXR1cm4gZiLinIUgTm8gYXV0b21hdGljIGZpeGVzIGFwcGxpY2FibGUgaW4ge2xlbihmaWxlcyl9IGZpbGVzLiIKCiAgICBtb2RlID0gIkRSWSBSVU4iIGlmIGRyeV9ydW4gZWxzZSAiQVBQTElFRCIKICAgIGxpbmVzID0gWwogICAgICAgIGYiIyBGaXggV29ya3NwYWNlIFJlcG9ydCBbe21vZGV9XVxuIiwKICAgICAgICBmIioqRm9sZGVyOioqIGB7Zm9sZGVyX3BhdGh9YCAgIiwKICAgICAgICBmIioqRmlsZXMgd2l0aCBmaXhlczoqKiB7bGVuKHJlc3VsdHMpfSBvZiB7bGVuKGZpbGVzKX0gICIsCiAgICAgICAgZiIqKlRvdGFsIGZpeCBvcGVyYXRpb25zOioqIHt0b3RhbF9maXhlc31cbiIsCiAgICBdCgogICAgZm9yIGYsIGFwcGxpZWQsIGNoYW5nZWQgaW4gcmVzdWx0czoKICAgICAgICByZWwgPSBmLnJlbGF0aXZlX3RvKGZvbGRlcikgaWYgZi5pc19yZWxhdGl2ZV90byhmb2xkZXIpIGVsc2UgZi5uYW1lCiAgICAgICAgc3RhdHVzID0gIuKchSB3cml0dGVuIiBpZiAobm90IGRyeV9ydW4gYW5kIGNoYW5nZWQpIGVsc2UgKCLwn5OdIHdvdWxkIGNoYW5nZSIgaWYgY2hhbmdlZCBlbHNlICLin7Mgbm8gY2hhbmdlIikKICAgICAgICBsaW5lcy5hcHBlbmQoZiItIGB7cmVsfWAgW3tzdGF0dXN9XTogeycsICcuam9pbihhcHBsaWVkKX0iKQoKICAgIGlmIGRyeV9ydW46CiAgICAgICAgbGluZXMuYXBwZW5kKCJcbj4gU2V0IGBkcnlfcnVuPUZhbHNlYCB0byBhcHBseSBhbGwgY2hhbmdlcy4iKQoKICAgIHJldHVybiAiXG4iLmpvaW4obGluZXMpCgoKQG1jcC50b29sKCkKZGVmIHNjYW5fYWxfY29kZSgKICAgIGFsX2NvZGU6IHN0ciwKICAgIGZpbGVfaGludDogc3RyID0gImlubGluZSIsCikgLT4gc3RyOgogICAgIiIiCiAgICBTY2FuIGEgc25pcHBldCBvZiBBTCBjb2RlIChwYXN0ZWQgaW5saW5lKSBmb3IgcGVyZm9ybWFuY2UgaXNzdWVzLgogICAgVXNlZnVsIGZvciBjaGVja2luZyBjb2RlIGJlZm9yZSBjb21taXR0aW5nIG9yIGR1cmluZyBjb2RlIHJldmlldy4KCiAgICBBcmdzOgogICAgICAgIGFsX2NvZGU6IFRoZSBBTCBjb2RlIHRleHQgdG8gYW5hbHl6ZS4KICAgICAgICBmaWxlX2hpbnQ6IE9wdGlvbmFsIGxhYmVsIGZvciB0aGUgY29kZSAoZS5nLiB0aGUgcHJvY2VkdXJlIG5hbWUpLgogICAgIiIiCiAgICBmaW5kaW5nczogbGlzdFtkaWN0XSA9IFtdCiAgICBmb3IgcCBpbiBQQVRURVJOUzoKICAgICAgICBmb3IgZiBpbiBwWyJkZXRlY3RvciJdKGFsX2NvZGUpOgogICAgICAgICAgICBmaW5kaW5ncy5hcHBlbmQoewogICAgICAgICAgICAgICAgInBhdHRlcm4iOiBwWyJpZCJdLAogICAgICAgICAgICAgICAgInNldmVyaXR5IjogcFsic2V2ZXJpdHkiXSwKICAgICAgICAgICAgICAgICJ0aXRsZSI6IHBbInRpdGxlIl0sCiAgICAgICAgICAgICAgICAiZmlsZSI6IGZpbGVfaGludCwKICAgICAgICAgICAgICAgICoqZgogICAgICAgICAgICB9KQoKICAgIGlmIG5vdCBmaW5kaW5nczoKICAgICAgICByZXR1cm4gIuKchSBObyBwZXJmb3JtYW5jZSBpc3N1ZXMgZGV0ZWN0ZWQgaW4gdGhlIHByb3ZpZGVkIGNvZGUuIgoKICAgIHNldmVyaXR5X29yZGVyID0geyJISUdIIjogMCwgIk1FRElVTSI6IDEsICJMT1ciOiAyfQogICAgZmluZGluZ3Nfc29ydGVkID0gc29ydGVkKGZpbmRpbmdzLCBrZXk9bGFtYmRhIHg6IHNldmVyaXR5X29yZGVyLmdldCh4WyJzZXZlcml0eSJdLCA5KSkKCiAgICBsaW5lcyA9IFtmIiMgUGVyZm9ybWFuY2UgSXNzdWVzIGluIGB7ZmlsZV9oaW50fWBcbiJdCiAgICBmb3IgZiBpbiBmaW5kaW5nc19zb3J0ZWQ6CiAgICAgICAgc2V2X2ljb24gPSB7IkhJR0giOiAi8J+UtCIsICJNRURJVU0iOiAi8J+foSIsICJMT1ciOiAi8J+UtSJ9LmdldChmWyJzZXZlcml0eSJdLCAi4pqqIikKICAgICAgICBsaW5lcy5hcHBlbmQoZiJ7c2V2X2ljb259ICoqTHtmWydsaW5lJ119KiogYHtmWydwYXR0ZXJuJ119YCDigJQge2ZbJ21lc3NhZ2UnXX0iKQogICAgICAgIGxpbmVzLmFwcGVuZChmIiAgPiB7ZlsndGl0bGUnXX0iKQogICAgICAgIGlmIGYuZ2V0KCJzbmlwcGV0Iik6CiAgICAgICAgICAgIGxpbmVzLmFwcGVuZChmIiAgYGBgYWxcbiAge2ZbJ3NuaXBwZXQnXX1cbiAgYGBgIikKICAgICAgICBsaW5lcy5hcHBlbmQoIiIpCgogICAgcmV0dXJuICJcbiIuam9pbihsaW5lcykKCgojIOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkOKVkAojIE9SQ0hFU1RSQVRJT04gTEFZRVIKIyDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZAKIwojIEFyY2hpdGVjdHVyZToKIyAgIGFuYWx5emVfYWxfcGVyZm9ybWFuY2UoZm9sZGVyKSAgICAgICAgICDihpAgbWFzdGVyIG9yY2hlc3RyYXRvcgojICAgICAgIGludGVybmFsbHkgZGVsZWdhdGVzIHRvIG9uZSBmdW5jdGlvbiBwZXIgcGF0dGVybiBncm91cCAoc3ViLWFnZW50cykKIyAgICAgICBhZ2dyZWdhdGVzIGFsbCByZXN1bHRzIGludG8gYSB1bmlmaWVkIHByaW9yaXR5IHJlcG9ydAojCiMgICBJbmRpdmlkdWFsIGdyb3VwIHN1Yi1hZ2VudCB0b29scyBhbHNvIGV4cG9zZWQgc28gdGhlIExMTSBjYW4gY2FsbAojICAgdGhlbSBpbmRlcGVuZGVudGx5IHdoZW4gdGhlIHVzZXIgYXNrcyBhYm91dCBhIHNwZWNpZmljIGNhdGVnb3J5LgojCiMg4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQ4pWQCgojIOKUgOKUgOKUgCBJbnRlcm5hbCBzdWItYWdlbnQgcnVubmVyIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgAoKZGVmIF9ydW5fZ3JvdXBfYWdlbnQoZ3JvdXBfbmFtZTogc3RyLCBmb2xkZXI6IFBhdGgpIC0+IGRpY3Q6CiAgICAiIiIKICAgIFJ1biBhbGwgZGV0ZWN0b3JzIGJlbG9uZ2luZyB0byBhIHNpbmdsZSBwYXR0ZXJuIGdyb3VwIGFnYWluc3QgZXZlcnkgQUwgZmlsZS4KICAgIFJldHVybnMgYSBzdHJ1Y3R1cmVkIHJlc3VsdCBkaWN0IOKAlCB0aGlzIGlzIHdoYXQgZWFjaCBzdWItYWdlbnQgcHJvZHVjZXMuCiAgICAiIiIKICAgIGdyb3VwX3BhdHRlcm5zID0gW3AgZm9yIHAgaW4gUEFUVEVSTlMgaWYgcFsiZ3JvdXAiXSA9PSBncm91cF9uYW1lXQogICAgZmlsZXMgPSBfYWxfZmlsZXMoZm9sZGVyKQoKICAgIGZpbmRpbmdzOiBsaXN0W2RpY3RdID0gW10KICAgIGZvciBmIGluIGZpbGVzOgogICAgICAgIHRyeToKICAgICAgICAgICAgdGV4dCA9IGYucmVhZF90ZXh0KGVuY29kaW5nPSJ1dGYtOC1zaWciKQogICAgICAgIGV4Y2VwdCBFeGNlcHRpb246CiAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgZm9yIHAgaW4gZ3JvdXBfcGF0dGVybnM6CiAgICAgICAgICAgIGZvciBmaW5kaW5nIGluIHBbImRldGVjdG9yIl0odGV4dCk6CiAgICAgICAgICAgICAgICBmaW5kaW5ncy5hcHBlbmQoewogICAgICAgICAgICAgICAgICAgICJwYXR0ZXJuIjogcFsiaWQiXSwKICAgICAgICAgICAgICAgICAgICAic2V2ZXJpdHkiOiBwWyJzZXZlcml0eSJdLAogICAgICAgICAgICAgICAgICAgICJ0aXRsZSI6IHBbInRpdGxlIl0sCiAgICAgICAgICAgICAgICAgICAgImZpbGUiOiBzdHIoZiksCiAgICAgICAgICAgICAgICAgICAgInJlbF9maWxlIjogc3RyKGYucmVsYXRpdmVfdG8oZm9sZGVyKSkgaWYgZi5pc19yZWxhdGl2ZV90byhmb2xkZXIpIGVsc2UgZi5uYW1lLAogICAgICAgICAgICAgICAgICAgICoqZmluZGluZwogICAgICAgICAgICAgICAgfSkKCiAgICBoaWdoICAgPSBbZiBmb3IgZiBpbiBmaW5kaW5ncyBpZiBmWyJzZXZlcml0eSJdID09ICJISUdIIl0KICAgIG1lZGl1bSA9IFtmIGZvciBmIGluIGZpbmRpbmdzIGlmIGZbInNldmVyaXR5Il0gPT0gIk1FRElVTSJdCiAgICBsb3cgICAgPSBbZiBmb3IgZiBpbiBmaW5kaW5ncyBpZiBmWyJzZXZlcml0eSJdID09ICJMT1ciXQoKICAgICMgQXV0by1maXhhYmxlIHBhdHRlcm5zIGluIHRoaXMgZ3JvdXAKICAgIGZpeGFibGUgPSBbcFsiaWQiXSBmb3IgcCBpbiBncm91cF9wYXR0ZXJucyBpZiBfaXNfcGF0dGVybl9hdXRvX2ZpeGFibGUocFsiaWQiXSldCgogICAgcmV0dXJuIHsKICAgICAgICAiZ3JvdXAiOiBncm91cF9uYW1lLAogICAgICAgICJwYXR0ZXJuc19jaGVja2VkIjogW3BbImlkIl0gZm9yIHAgaW4gZ3JvdXBfcGF0dGVybnNdLAogICAgICAgICJmaWxlc19zY2FubmVkIjogbGVuKGZpbGVzKSwKICAgICAgICAidG90YWwiOiBsZW4oZmluZGluZ3MpLAogICAgICAgICJoaWdoIjogbGVuKGhpZ2gpLAogICAgICAgICJtZWRpdW0iOiBsZW4obWVkaXVtKSwKICAgICAgICAibG93IjogbGVuKGxvdyksCiAgICAgICAgImZpeGFibGVfcGF0dGVybnMiOiBmaXhhYmxlLAogICAgICAgICJmaW5kaW5ncyI6IGZpbmRpbmdzLAogICAgfQoKCl9BVVRPX0ZJWEFCTEUgPSB7CiAgICAiRklORF9EQVNIX0JVRkZFUl9PTkUiLAogICAgIkNPVU5UX05PVF9aRVJPIiwKICAgICJDT1VOVF9FUVVBTFNfT05FIiwKICAgICJJU0VNUFRZX0JFRk9SRV9GSU5EU0VUIiwKICAgICJGSU5EU0VUX0JFRk9SRV9NT0RJRllBTEwiLAogICAgIkZJTkRTRVRfQkVGT1JFX0RFTEVURUFMTCIsCiAgICAiTU9ESUZZQUxMX1JVTlRSSUdHRVJfVFJVRSIsCiAgICAiTUlTU0lOR19URU1QT1JBUlkiLAogICAgIkZJTkRTRVRfTU9ESUZZX05PX1RSVUUiLAogICAgIkxPQ0tUQUJMRV9VU0VfVVBETE9DSyIsCiAgICAiRklOREZJUlNUX0lOX0xPT1AiLAp9CgpkZWYgX2lzX3BhdHRlcm5fYXV0b19maXhhYmxlKHBhdHRlcm5faWQ6IHN0cikgLT4gYm9vbDoKICAgIHJldHVybiBwYXR0ZXJuX2lkIGluIF9BVVRPX0ZJWEFCTEUKCgpkZWYgX2Zvcm1hdF9ncm91cF9yZXBvcnQocmVzdWx0OiBkaWN0LCBmb2xkZXI6IFBhdGgsIGRldGFpbF9sZXZlbDogc3RyID0gInN1bW1hcnkiKSAtPiBsaXN0W3N0cl06CiAgICAiIiJSZW5kZXIgb25lIGdyb3VwIHN1Yi1hZ2VudCByZXN1bHQgYXMgbWFya2Rvd24gbGluZXMuIiIiCiAgICBzZXZfaWNvbiA9ICLwn5S0IiBpZiByZXN1bHRbImhpZ2giXSA+IDAgZWxzZSAoIvCfn6EiIGlmIHJlc3VsdFsibWVkaXVtIl0gPiAwIGVsc2UgIuKchSIpCiAgICBsaW5lcyA9IFsKICAgICAgICBmIlxuIyMjIHtzZXZfaWNvbn0ge3Jlc3VsdFsnZ3JvdXAnXX0iLAogICAgICAgIGYiRm91bmQgKip7cmVzdWx0Wyd0b3RhbCddfSoqIGlzc3VlKHMpIOKAlCAiCiAgICAgICAgZiJ7cmVzdWx0WydoaWdoJ119IEhJR0gsIHtyZXN1bHRbJ21lZGl1bSddfSBNRURJVU0sIHtyZXN1bHRbJ2xvdyddfSBMT1ciLAogICAgXQogICAgaWYgcmVzdWx0WyJmaXhhYmxlX3BhdHRlcm5zIl06CiAgICAgICAgbGluZXMuYXBwZW5kKGYiQXV0by1maXhhYmxlIHBhdHRlcm5zOiBgeydgLCBgJy5qb2luKHJlc3VsdFsnZml4YWJsZV9wYXR0ZXJucyddKX1gIikKCiAgICBpZiBkZXRhaWxfbGV2ZWwgIT0gImZ1bGwiIG9yIHJlc3VsdFsidG90YWwiXSA9PSAwOgogICAgICAgIHJldHVybiBsaW5lcwoKICAgIHNldmVyaXR5X29yZGVyID0geyJISUdIIjogMCwgIk1FRElVTSI6IDEsICJMT1ciOiAyfQogICAgZmluZGluZ3Nfc29ydGVkID0gc29ydGVkKHJlc3VsdFsiZmluZGluZ3MiXSwga2V5PWxhbWJkYSB4OiBzZXZlcml0eV9vcmRlci5nZXQoeFsic2V2ZXJpdHkiXSwgOSkpCiAgICBzZWVuOiBzZXRbc3RyXSA9IHNldCgpCiAgICBmb3IgZiBpbiBmaW5kaW5nc19zb3J0ZWRbOjE1XTogICMgY2FwIHBlciBncm91cAogICAgICAgIGtleSA9IGYie2ZbJ3JlbF9maWxlJ119OntmWydsaW5lJ119OntmWydwYXR0ZXJuJ119IgogICAgICAgIGlmIGtleSBpbiBzZWVuOgogICAgICAgICAgICBjb250aW51ZQogICAgICAgIHNlZW4uYWRkKGtleSkKICAgICAgICBpY29uID0geyJISUdIIjogIvCflLQiLCAiTUVESVVNIjogIvCfn6EiLCAiTE9XIjogIvCflLUifS5nZXQoZlsic2V2ZXJpdHkiXSwgIuKaqiIpCiAgICAgICAgbGluZXMuYXBwZW5kKGYiICAtIHtpY29ufSBge2ZbJ3JlbF9maWxlJ119YCBMe2ZbJ2xpbmUnXX0gYHtmWydwYXR0ZXJuJ119YDoge2ZbJ21lc3NhZ2UnXX0iKQogICAgaWYgbGVuKHJlc3VsdFsiZmluZGluZ3MiXSkgPiAxNToKICAgICAgICBsaW5lcy5hcHBlbmQoZiIgIC0gXyjigKYge2xlbihyZXN1bHRbJ2ZpbmRpbmdzJ10pIC0gMTV9IG1vcmUg4oCUIHVzZSBncm91cCBzdWItYWdlbnQgZm9yIGZ1bGwgbGlzdClfIikKICAgIHJldHVybiBsaW5lcwoKCiMg4pSA4pSA4pSAIEdyb3VwIHN1Yi1hZ2VudCB0b29scyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKIyBFYWNoIGV4cG9zZWQgYXMgaXRzIG93biBNQ1AgdG9vbCBzbyB0aGUgTExNIGNhbiBjYWxsIHRoZW0gaW5kZXBlbmRlbnRseS4KCmRlZiBfZ3JvdXBfdG9vbF9pbXBsKGdyb3VwX25hbWU6IHN0ciwgZm9sZGVyX3BhdGg6IHN0cikgLT4gc3RyOgogICAgZm9sZGVyID0gUGF0aChmb2xkZXJfcGF0aCkKICAgIGlmIG5vdCBmb2xkZXIuZXhpc3RzKCk6CiAgICAgICAgcmV0dXJuIGYiRVJST1I6IEZvbGRlciBub3QgZm91bmQ6IHtmb2xkZXJfcGF0aH0iCiAgICByZXN1bHQgPSBfcnVuX2dyb3VwX2FnZW50KGdyb3VwX25hbWUsIGZvbGRlcikKICAgIGxpbmVzID0gW2YiIyBTdWItQWdlbnQ6IHtncm91cF9uYW1lfVxuIiwKICAgICAgICAgICAgIGYiKipGb2xkZXI6KiogYHtmb2xkZXJfcGF0aH1gICAiLAogICAgICAgICAgICAgZiIqKklzc3VlczoqKiB7cmVzdWx0Wyd0b3RhbCddfSAoe3Jlc3VsdFsnaGlnaCddfSBISUdILCB7cmVzdWx0WydtZWRpdW0nXX0gTUVESVVNLCB7cmVzdWx0Wydsb3cnXX0gTE9XKVxuIl0KICAgIGxpbmVzICs9IF9mb3JtYXRfZ3JvdXBfcmVwb3J0KHJlc3VsdCwgZm9sZGVyLCBkZXRhaWxfbGV2ZWw9ImZ1bGwiKQogICAgcmV0dXJuICJcbiIuam9pbihsaW5lcykKCgpAbWNwLnRvb2woKQpkZWYgc2Nhbl9kYXRhX3RyYW5zZmVyX2lzc3Vlcyhmb2xkZXJfcGF0aDogc3RyKSAtPiBzdHI6CiAgICAiIiIKICAgIFN1Yi1hZ2VudDogU2NhbiBmb3IgRGF0YSBUcmFuc2ZlciBhbnRpLXBhdHRlcm5zLgogICAgQ292ZXJzOiBtaXNzaW5nIFNldExvYWRGaWVsZHMsIEZpbmQoJy0nKSBidWZmZXItc2l6ZS0xLCBGaW5kRmlyc3QgaW4gbG9vcHMsIEZpbmRMYXN0IGluIGxvb3BzLAogICAgU2V0UmFuZ2UrRmluZEZpcnN0IGluc3RlYWQgb2YgR2V0LCBTZXRDdXJyZW50S2V5IG1pc3NpbmcuCiAgICAiIiIKICAgIHJldHVybiBfZ3JvdXBfdG9vbF9pbXBsKCJEYXRhIFRyYW5zZmVyIiwgZm9sZGVyX3BhdGgpCgoKQG1jcC50b29sKCkKZGVmIHNjYW5fZmxvd2ZpZWxkX2lzc3Vlcyhmb2xkZXJfcGF0aDogc3RyKSAtPiBzdHI6CiAgICAiIiIKICAgIFN1Yi1hZ2VudDogU2NhbiBmb3IgRmxvd0ZpZWxkIGFudGktcGF0dGVybnMuCiAgICBDb3ZlcnM6IENhbGNGaWVsZHMgaW5zaWRlIGxvb3BzLCBTZXRGaWx0ZXIgb24gRmxvd0ZpZWxkIChjb3JyZWxhdGVkIHN1YnF1ZXJ5KS4KICAgICIiIgogICAgcmV0dXJuIF9ncm91cF90b29sX2ltcGwoIkZsb3dGaWVsZHMiLCBmb2xkZXJfcGF0aCkKCgpAbWNwLnRvb2woKQpkZWYgc2Nhbl9hZ2dyZWdhdGlvbl9pc3N1ZXMoZm9sZGVyX3BhdGg6IHN0cikgLT4gc3RyOgogICAgIiIiCiAgICBTdWItYWdlbnQ6IFNjYW4gZm9yIEFnZ3JlZ2F0aW9uIGFudGktcGF0dGVybnMuCiAgICBDb3ZlcnM6IG1hbnVhbCBzdW1tYXRpb24gbG9vcHMgdGhhdCBzaG91bGQgdXNlIENhbGNTdW1zKCkuCiAgICAiIiIKICAgIHJldHVybiBfZ3JvdXBfdG9vbF9pbXBsKCJBZ2dyZWdhdGlvbiIsIGZvbGRlcl9wYXRoKQoKCkBtY3AudG9vbCgpCmRlZiBzY2FuX2J1bGtfb3BlcmF0aW9uX2lzc3Vlcyhmb2xkZXJfcGF0aDogc3RyKSAtPiBzdHI6CiAgICAiIiIKICAgIFN1Yi1hZ2VudDogU2NhbiBmb3IgQnVsayBPcGVyYXRpb24gYW50aS1wYXR0ZXJucy4KICAgIENvdmVyczogRGVsZXRlIGluIGxvb3AsIEZpbmRTZXQgYmVmb3JlIE1vZGlmeUFsbCwgRmluZFNldCBiZWZvcmUgRGVsZXRlQWxsLAogICAgTW9kaWZ5QWxsIHdpdGggUnVuVHJpZ2dlcj10cnVlLgogICAgIiIiCiAgICByZXR1cm4gX2dyb3VwX3Rvb2xfaW1wbCgiQnVsayBPcGVyYXRpb25zIiwgZm9sZGVyX3BhdGgpCgoKQG1jcC50b29sKCkKZGVmIHNjYW5fZXhpc3RlbmNlX2NoZWNrX2lzc3Vlcyhmb2xkZXJfcGF0aDogc3RyKSAtPiBzdHI6CiAgICAiIiIKICAgIFN1Yi1hZ2VudDogU2NhbiBmb3IgRXhpc3RlbmNlIENoZWNrIGFudGktcGF0dGVybnMuCiAgICBDb3ZlcnM6IENvdW50KCkgIT0gMCB2cyBJc0VtcHR5KCksIHJlZHVuZGFudCBJc0VtcHR5IGJlZm9yZSBGaW5kU2V0LCBDb3VudCgpID0gMSB1bmlxdWVuZXNzLgogICAgIiIiCiAgICByZXR1cm4gX2dyb3VwX3Rvb2xfaW1wbCgiRXhpc3RlbmNlIENoZWNrcyIsIGZvbGRlcl9wYXRoKQoKCkBtY3AudG9vbCgpCmRlZiBzY2FuX2xvY2tpbmdfaXNzdWVzKGZvbGRlcl9wYXRoOiBzdHIpIC0+IHN0cjoKICAgICIiIgogICAgU3ViLWFnZW50OiBTY2FuIGZvciBMb2NraW5nIGFudGktcGF0dGVybnMuCiAgICBDb3ZlcnM6IExvY2tUYWJsZSB0b28gZWFybHksIExvY2tUYWJsZSBmb3Igc2VxdWVuY2UgZ2VuZXJhdGlvbiwgRGVsZXRlQWxsIHdpdGhvdXQgSXNFbXB0eSBndWFyZCwKICAgIExvY2tUYWJsZSB2cyBSZWFkSXNvbGF0aW9uIFVwZExvY2suCiAgICAiIiIKICAgIHJldHVybiBfZ3JvdXBfdG9vbF9pbXBsKCJMb2NraW5nIiwgZm9sZGVyX3BhdGgpCgoKQG1jcC50b29sKCkKZGVmIHNjYW5fbWVtb3J5X2lzc3Vlcyhmb2xkZXJfcGF0aDogc3RyKSAtPiBzdHI6CiAgICAiIiIKICAgIFN1Yi1hZ2VudDogU2NhbiBmb3IgTWVtb3J5IC8gQ29weSBhbnRpLXBhdHRlcm5zLgogICAgQ292ZXJzOiBSZWNvcmQgcGFyYW1ldGVyIGJ5IHZhbHVlLCBtaXNzaW5nICd0ZW1wb3JhcnknIGtleXdvcmQsIHRlbXAgdGFibGUgdXNlZCBhcyBEaWN0aW9uYXJ5LAogICAgdGVtcCB0YWJsZSB1c2VkIGFzIGNvbGxlY3Rpb24sIFJlY29yZFJlZiB3aGVuIHR5cGVkIFJlY29yZCBzdWZmaWNlcy4KICAgICIiIgogICAgcmV0dXJuIF9ncm91cF90b29sX2ltcGwoIk1lbW9yeSAvIENvcGllcyIsIGZvbGRlcl9wYXRoKQoKCkBtY3AudG9vbCgpCmRlZiBzY2FuX3Nob3J0X2NpcmN1aXRfaXNzdWVzKGZvbGRlcl9wYXRoOiBzdHIpIC0+IHN0cjoKICAgICIiIgogICAgU3ViLWFnZW50OiBTY2FuIGZvciBTaG9ydC1DaXJjdWl0IEV2YWx1YXRpb24gYW50aS1wYXR0ZXJucy4KICAgIENvdmVyczogT1IvQU5EIGVhZ2VyIGV2YWx1YXRpb24sIDMrIE9SIGNvbmRpdGlvbnMgKHVzZSBjYXNlIHRydWUgb2YpLAogICAgJ2lmIHRydWUgaW4gWy4uLl0nIGNvbmRpdGlvbiBvcmRlcmluZy4KICAgICIiIgogICAgcmV0dXJuIF9ncm91cF90b29sX2ltcGwoIlNob3J0LUNpcmN1aXQgRXZhbHVhdGlvbiIsIGZvbGRlcl9wYXRoKQoKCkBtY3AudG9vbCgpCmRlZiBzY2FuX3dyaXRlX3BhdHRlcm5faXNzdWVzKGZvbGRlcl9wYXRoOiBzdHIpIC0+IHN0cjoKICAgICIiIgogICAgU3ViLWFnZW50OiBTY2FuIGZvciBXcml0ZSBQYXR0ZXJuIGFudGktcGF0dGVybnMuCiAgICBDb3ZlcnM6IEluc2VydC1vbi1jb25mbGljdCAodHJ5IEluc2VydCwgTW9kaWZ5IG9uIGZhaWwpLCBzaWxlbnQgSW5zZXJ0IGZhaWx1cmUuCiAgICAiIiIKICAgIHJldHVybiBfZ3JvdXBfdG9vbF9pbXBsKCJXcml0ZSBQYXR0ZXJucyIsIGZvbGRlcl9wYXRoKQoKCkBtY3AudG9vbCgpCmRlZiBzY2FuX2N1cnNvcl9zYWZldHlfaXNzdWVzKGZvbGRlcl9wYXRoOiBzdHIpIC0+IHN0cjoKICAgICIiIgogICAgU3ViLWFnZW50OiBTY2FuIGZvciBDdXJzb3IgU2FmZXR5IGFudGktcGF0dGVybnMuCiAgICBDb3ZlcnM6IFNldFJhbmdlL1NldEZpbHRlciBvbiBhY3RpdmUgRmluZFNldCBjdXJzb3IgKGNvcnJ1cHRzIGl0ZXJhdGlvbiksCiAgICBGaW5kU2V0KHRydWUpIG1pc3Npbmcgd2hlbiBNb2RpZnkgaXMgY2FsbGVkIG9uIGN1cnNvciB2YXJpYWJsZS4KICAgICIiIgogICAgIyBDb21iaW5lIEN1cnNvciBTYWZldHkgKyB0aGUgRmluZFNldCh0cnVlKSBwYXR0ZXJuCiAgICByZXR1cm4gX2dyb3VwX3Rvb2xfaW1wbCgiQ3Vyc29yIFNhZmV0eSIsIGZvbGRlcl9wYXRoKQoKCkBtY3AudG9vbCgpCmRlZiBzY2FuX3N0YWxlX3JlYWRfaXNzdWVzKGZvbGRlcl9wYXRoOiBzdHIpIC0+IHN0cjoKICAgICIiIgogICAgU3ViLWFnZW50OiBTY2FuIGZvciBTdGFsZSBSZWFkIGFudGktcGF0dGVybnMuCiAgICBDb3ZlcnM6IG1pc3NpbmcgUmVhZElzb2xhdGlvbiBvbiByZXBvcnRpbmcgcXVlcmllcywgbWlzc2luZyBTZWxlY3RMYXRlc3RWZXJzaW9uIGluIG11bHRpLXBhc3MgbG9vcHMuCiAgICAiIiIKICAgIHJldHVybiBfZ3JvdXBfdG9vbF9pbXBsKCJTdGFsZSBSZWFkcyIsIGZvbGRlcl9wYXRoKQoKCiMg4pSA4pSA4pSAIE1hc3RlciBvcmNoZXN0cmF0b3IgdG9vbCDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKCkBtY3AudG9vbCgpCmRlZiBhbmFseXplX2FsX3BlcmZvcm1hbmNlKAogICAgZm9sZGVyX3BhdGg6IHN0ciwKICAgIGluY2x1ZGVfYWN0aW9uX3BsYW46IGJvb2wgPSBUcnVlLAopIC0+IHN0cjoKICAgICIiIgogICAgTUFTVEVSIE9SQ0hFU1RSQVRPUiDigJQgcnVucyBhbGwgcGF0dGVybi1ncm91cCBzdWItYWdlbnRzIGluIHNlcXVlbmNlIGFuZCBwcm9kdWNlcwogICAgYSB1bmlmaWVkIHBlcmZvcm1hbmNlIHJlcG9ydCB3aXRoIGEgcHJpb3JpdGl6ZWQgYWN0aW9uIHBsYW4uCgogICAgVGhpcyBpcyB0aGUgcHJpbWFyeSBlbnRyeSBwb2ludC4gSXQ6CiAgICAgIDEuIERlbGVnYXRlcyB0byBlYWNoIHBhdHRlcm4tZ3JvdXAgc3ViLWFnZW50IChEYXRhIFRyYW5zZmVyLCBGbG93RmllbGRzLCBMb2NraW5nLCBldGMuKQogICAgICAyLiBBZ2dyZWdhdGVzIGFsbCBmaW5kaW5ncyBhY3Jvc3MgYWxsIGdyb3VwcwogICAgICAzLiBSYW5rcyBmaWxlcyBieSB0b3RhbCBpc3N1ZSBzZXZlcml0eSBzY29yZQogICAgICA0LiBQcm9kdWNlcyBhIHByaW9yaXRpemVkIGFjdGlvbiBwbGFuOiB3aGljaCBmaWxlcyB0byBmaXggZmlyc3QgYW5kIGhvdwogICAgICA1LiBMaXN0cyBhbGwgYXV0by1maXhhYmxlIHBhdHRlcm5zIHdpdGggdGhlIGZpeCBjb21tYW5kIHRvIHJ1bgoKICAgIEFyZ3M6CiAgICAgICAgZm9sZGVyX3BhdGg6IEFic29sdXRlIHBhdGggdG8gdGhlIEFMIHdvcmtzcGFjZSBmb2xkZXIgdG8gYW5hbHl6ZS4KICAgICAgICBpbmNsdWRlX2FjdGlvbl9wbGFuOiBJbmNsdWRlIHRoZSBwcmlvcml0aXplZCBmaXggcGxhbiAoZGVmYXVsdCBUcnVlKS4KICAgICIiIgogICAgZm9sZGVyID0gUGF0aChmb2xkZXJfcGF0aCkKICAgIGlmIG5vdCBmb2xkZXIuZXhpc3RzKCk6CiAgICAgICAgcmV0dXJuIGYiRVJST1I6IEZvbGRlciBub3QgZm91bmQ6IHtmb2xkZXJfcGF0aH0iCgogICAgZmlsZXMgPSBfYWxfZmlsZXMoZm9sZGVyKQogICAgaWYgbm90IGZpbGVzOgogICAgICAgIHJldHVybiBmIk5vIC5hbCBmaWxlcyBmb3VuZCBpbiB7Zm9sZGVyX3BhdGh9IgoKICAgICMg4pSA4pSAIFN0ZXAgMTogQ29sbGVjdCBhbGwgdW5pcXVlIGdyb3VwIG5hbWVzIGZyb20gdGhlIHBhdHRlcm4gcmVnaXN0cnkg4pSA4pSACiAgICBhbGxfZ3JvdXBzID0gc29ydGVkKHtwWyJncm91cCJdIGZvciBwIGluIFBBVFRFUk5TfSkKCiAgICAjIOKUgOKUgCBTdGVwIDI6IFJ1biBlYWNoIGdyb3VwIHN1Yi1hZ2VudCDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKICAgIGdyb3VwX3Jlc3VsdHM6IGxpc3RbZGljdF0gPSBbXQogICAgZm9yIGdyb3VwIGluIGFsbF9ncm91cHM6CiAgICAgICAgcmVzdWx0ID0gX3J1bl9ncm91cF9hZ2VudChncm91cCwgZm9sZGVyKQogICAgICAgIGdyb3VwX3Jlc3VsdHMuYXBwZW5kKHJlc3VsdCkKCiAgICAjIOKUgOKUgCBTdGVwIDM6IEFnZ3JlZ2F0ZSDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKICAgIGFsbF9maW5kaW5nczogbGlzdFtkaWN0XSA9IFtdCiAgICBmb3IgciBpbiBncm91cF9yZXN1bHRzOgogICAgICAgIGFsbF9maW5kaW5ncy5leHRlbmQoclsiZmluZGluZ3MiXSkKCiAgICB0b3RhbCAgID0gbGVuKGFsbF9maW5kaW5ncykKICAgIGhpZ2ggICAgPSBzdW0oMSBmb3IgZiBpbiBhbGxfZmluZGluZ3MgaWYgZlsic2V2ZXJpdHkiXSA9PSAiSElHSCIpCiAgICBtZWRpdW0gID0gc3VtKDEgZm9yIGYgaW4gYWxsX2ZpbmRpbmdzIGlmIGZbInNldmVyaXR5Il0gPT0gIk1FRElVTSIpCiAgICBsb3cgICAgID0gc3VtKDEgZm9yIGYgaW4gYWxsX2ZpbmRpbmdzIGlmIGZbInNldmVyaXR5Il0gPT0gIkxPVyIpCiAgICBzY29yZSAgID0gaGlnaCAqIDEwICsgbWVkaXVtICogMyArIGxvdyAgIyB3ZWlnaHRlZCBzZXZlcml0eSBzY29yZQoKICAgICMgRmlsZXMgcmFua2VkIGJ5IHNldmVyaXR5IHNjb3JlCiAgICBmaWxlX3Njb3JlczogZGljdFtzdHIsIGRpY3RdID0ge30KICAgIGZvciBmIGluIGFsbF9maW5kaW5nczoKICAgICAgICBmcCA9IGZbImZpbGUiXQogICAgICAgIGlmIGZwIG5vdCBpbiBmaWxlX3Njb3JlczoKICAgICAgICAgICAgZmlsZV9zY29yZXNbZnBdID0geyJoaWdoIjogMCwgIm1lZGl1bSI6IDAsICJsb3ciOiAwLCAicGF0dGVybnMiOiBzZXQoKSwgInJlbCI6IGZbInJlbF9maWxlIl19CiAgICAgICAgZmlsZV9zY29yZXNbZnBdW2ZbInNldmVyaXR5Il0ubG93ZXIoKV0gKz0gMQogICAgICAgIGZpbGVfc2NvcmVzW2ZwXVsicGF0dGVybnMiXS5hZGQoZlsicGF0dGVybiJdKQoKICAgIHJhbmtlZF9maWxlcyA9IHNvcnRlZCgKICAgICAgICBmaWxlX3Njb3Jlcy5pdGVtcygpLAogICAgICAgIGtleT1sYW1iZGEgeDogeFsxXVsiaGlnaCJdICogMTAgKyB4WzFdWyJtZWRpdW0iXSAqIDMgKyB4WzFdWyJsb3ciXSwKICAgICAgICByZXZlcnNlPVRydWUKICAgICkKCiAgICAjIEF1dG8tZml4YWJsZSBmaW5kaW5ncwogICAgZml4YWJsZV9maW5kaW5ncyA9IFtmIGZvciBmIGluIGFsbF9maW5kaW5ncyBpZiBfaXNfcGF0dGVybl9hdXRvX2ZpeGFibGUoZlsicGF0dGVybiJdKV0KICAgIGZpeGFibGVfY291bnQgPSBsZW4oZml4YWJsZV9maW5kaW5ncykKICAgIGZpeGFibGVfZmlsZXMgPSBzb3J0ZWQoe2ZbInJlbF9maWxlIl0gZm9yIGYgaW4gZml4YWJsZV9maW5kaW5nc30pCgogICAgIyDilIDilIAgU3RlcCA0OiBCdWlsZCByZXBvcnQg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACiAgICBsaW5lcyA9IFsKICAgICAgICAiIyDwn5SNIEFMIFBlcmZvcm1hbmNlIEFuYWx5c2lzIOKAlCBPcmNoZXN0cmF0b3IgUmVwb3J0XG4iLAogICAgICAgIGYiKipXb3Jrc3BhY2U6KiogYHtmb2xkZXJfcGF0aH1gICAiLAogICAgICAgIGYiKipGaWxlcyBzY2FubmVkOioqIHtsZW4oZmlsZXMpfSAgIiwKICAgICAgICBmIioqVG90YWwgaXNzdWVzOioqIHt0b3RhbH0gKPCflLQge2hpZ2h9IEhJR0gsIPCfn6Ege21lZGl1bX0gTUVESVVNLCDwn5S1IHtsb3d9IExPVykgICIsCiAgICAgICAgZiIqKlNldmVyaXR5IHNjb3JlOioqIHtzY29yZX0gICIsCiAgICAgICAgZiIqKkF1dG8tZml4YWJsZToqKiB7Zml4YWJsZV9jb3VudH0gaXNzdWVzIGluIHtsZW4oZml4YWJsZV9maWxlcyl9IGZpbGVzXG4iLAogICAgICAgICItLS1cbiIsCiAgICAgICAgIiMjIFN1Yi1BZ2VudCBSZXN1bHRzIGJ5IEdyb3VwXG4iLAogICAgICAgICJ8IEdyb3VwIHwgSElHSCB8IE1FRCB8IExPVyB8IFRvdGFsIHwiLAogICAgICAgICJ8LS0tLS0tLXwtLS0tLS18LS0tLS18LS0tLS18LS0tLS0tLXwiLAogICAgXQogICAgZm9yIHIgaW4gZ3JvdXBfcmVzdWx0czoKICAgICAgICBzdGF0dXMgPSAi8J+UtCIgaWYgclsiaGlnaCJdID4gMCBlbHNlICgi8J+foSIgaWYgclsibWVkaXVtIl0gPiAwIGVsc2UgIuKchSIpCiAgICAgICAgbGluZXMuYXBwZW5kKGYifCB7c3RhdHVzfSB7clsnZ3JvdXAnXX0gfCB7clsnaGlnaCddfSB8IHtyWydtZWRpdW0nXX0gfCB7clsnbG93J119IHwge3JbJ3RvdGFsJ119IHwiKQoKICAgICMg4pSA4pSAIFRvcCBvZmZlbmRpbmcgZmlsZXMg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACiAgICBsaW5lcyArPSBbIlxuLS0tXG4iLCAiIyMg8J+PhiBGaWxlcyBSYW5rZWQgYnkgU2V2ZXJpdHkgU2NvcmVcbiJdCiAgICBmb3IgaSwgKGZwLCBzYykgaW4gZW51bWVyYXRlKHJhbmtlZF9maWxlc1s6MTBdLCAxKToKICAgICAgICBmaWxlX3Njb3JlID0gc2NbImhpZ2giXSAqIDEwICsgc2NbIm1lZGl1bSJdICogMyArIHNjWyJsb3ciXQogICAgICAgIHBhdHRlcm5zX3N0ciA9ICIsICIuam9pbihmImB7cH1gIiBmb3IgcCBpbiBzb3J0ZWQoc2NbInBhdHRlcm5zIl0pKQogICAgICAgIGxpbmVzLmFwcGVuZCgKICAgICAgICAgICAgZiJ7aX0uICoqYHtzY1sncmVsJ119YCoqIOKAlCBzY29yZSB7ZmlsZV9zY29yZX0gIgogICAgICAgICAgICBmIijwn5S0e3NjWydoaWdoJ119IPCfn6F7c2NbJ21lZGl1bSddfSDwn5S1e3NjWydsb3cnXX0pICBcbiIKICAgICAgICAgICAgZiIgICBQYXR0ZXJuczoge3BhdHRlcm5zX3N0cn0iCiAgICAgICAgKQogICAgaWYgbGVuKHJhbmtlZF9maWxlcykgPiAxMDoKICAgICAgICBsaW5lcy5hcHBlbmQoZiJcbl/igKYgYW5kIHtsZW4ocmFua2VkX2ZpbGVzKSAtIDEwfSBtb3JlIGZpbGVzIHdpdGggaXNzdWVzLl8iKQoKICAgICMg4pSA4pSAIEFjdGlvbiBwbGFuIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgAogICAgaWYgaW5jbHVkZV9hY3Rpb25fcGxhbjoKICAgICAgICBsaW5lcyArPSBbIlxuLS0tXG4iLCAiIyMg8J+TiyBQcmlvcml0aXplZCBBY3Rpb24gUGxhblxuIl0KCiAgICAgICAgIyBQaGFzZSAxOiBBdXRvLWZpeGFibGUKICAgICAgICBpZiBmaXhhYmxlX2ZpbmRpbmdzOgogICAgICAgICAgICBsaW5lcy5hcHBlbmQoIiMjIyBQaGFzZSAxIOKAlCBSdW4gQXV0b21hdGljIEZpeGVzICh6ZXJvIG1hbnVhbCBlZmZvcnQpIikKICAgICAgICAgICAgbGluZXMuYXBwZW5kKGYiVGhlc2Uge2ZpeGFibGVfY291bnR9IGlzc3VlcyBjYW4gYmUgZml4ZWQgaW5zdGFudGx5IHdpdGggYGZpeF9hbF93b3Jrc3BhY2VgOiIpCiAgICAgICAgICAgIGZvciBwaWQgaW4gc29ydGVkKF9BVVRPX0ZJWEFCTEUpOgogICAgICAgICAgICAgICAgY291bnQgPSBzdW0oMSBmb3IgZiBpbiBhbGxfZmluZGluZ3MgaWYgZlsicGF0dGVybiJdID09IHBpZCkKICAgICAgICAgICAgICAgIGlmIGNvdW50ID4gMDoKICAgICAgICAgICAgICAgICAgICB0aXRsZSA9IG5leHQoKHBbInRpdGxlIl0gZm9yIHAgaW4gUEFUVEVSTlMgaWYgcFsiaWQiXSA9PSBwaWQpLCBwaWQpCiAgICAgICAgICAgICAgICAgICAgbGluZXMuYXBwZW5kKGYiICAtIGB7cGlkfWAgw5cge2NvdW50fSDigJQge3RpdGxlfSIpCiAgICAgICAgICAgIGxpbmVzLmFwcGVuZChmIlxuPiAqKlJ1bjoqKiBgZml4X2FsX3dvcmtzcGFjZShcIntmb2xkZXJfcGF0aH1cIiwgZHJ5X3J1bj1GYWxzZSlgIikKCiAgICAgICAgIyBQaGFzZSAyOiBIaWdoIHNldmVyaXR5IG1hbnVhbCBmaXhlcwogICAgICAgIGhpZ2hfbWFudWFsID0gW2YgZm9yIGYgaW4gYWxsX2ZpbmRpbmdzIGlmIGZbInNldmVyaXR5Il0gPT0gIkhJR0giIGFuZCBub3QgX2lzX3BhdHRlcm5fYXV0b19maXhhYmxlKGZbInBhdHRlcm4iXSldCiAgICAgICAgaWYgaGlnaF9tYW51YWw6CiAgICAgICAgICAgIGxpbmVzLmFwcGVuZCgiXG4jIyMgUGhhc2UgMiDigJQgSGlnaCBTZXZlcml0eSAobWFudWFsIGNvZGUgY2hhbmdlcyByZXF1aXJlZCkiKQogICAgICAgICAgICBieV9wYXR0ZXJuOiBkaWN0W3N0ciwgbGlzdF0gPSB7fQogICAgICAgICAgICBmb3IgZiBpbiBoaWdoX21hbnVhbDoKICAgICAgICAgICAgICAgIGJ5X3BhdHRlcm4uc2V0ZGVmYXVsdChmWyJwYXR0ZXJuIl0sIFtdKS5hcHBlbmQoZikKICAgICAgICAgICAgZm9yIHBpZCwgZmluZGluZ3MgaW4gc29ydGVkKGJ5X3BhdHRlcm4uaXRlbXMoKSwga2V5PWxhbWJkYSB4OiAtbGVuKHhbMV0pKToKICAgICAgICAgICAgICAgIHRpdGxlID0gbmV4dCgocFsidGl0bGUiXSBmb3IgcCBpbiBQQVRURVJOUyBpZiBwWyJpZCJdID09IHBpZCksIHBpZCkKICAgICAgICAgICAgICAgIGZpbGVzX2FmZmVjdGVkID0gc29ydGVkKHtmWyJyZWxfZmlsZSJdIGZvciBmIGluIGZpbmRpbmdzfSkKICAgICAgICAgICAgICAgIGxpbmVzLmFwcGVuZChmIlxuKipge3BpZH1gKiogw5cge2xlbihmaW5kaW5ncyl9IOKAlCB7dGl0bGV9IikKICAgICAgICAgICAgICAgIGxpbmVzLmFwcGVuZChmIiAgRmlsZXM6IHsnLCAnLmpvaW4oZidge2Z9YCcgZm9yIGYgaW4gZmlsZXNfYWZmZWN0ZWRbOjVdKX0iKQogICAgICAgICAgICAgICAgbGluZXMuYXBwZW5kKGYiICA+IFJ1biBgZXhwbGFpbl9wYXR0ZXJuKFwie3BpZH1cIilgIGZvciB0aGUgZml4IHBhdHRlcm4iKQoKICAgICAgICAjIFBoYXNlIDM6IE1lZGl1bQogICAgICAgIG1lZGl1bV9tYW51YWwgPSBbZiBmb3IgZiBpbiBhbGxfZmluZGluZ3MgaWYgZlsic2V2ZXJpdHkiXSA9PSAiTUVESVVNIiBhbmQgbm90IF9pc19wYXR0ZXJuX2F1dG9fZml4YWJsZShmWyJwYXR0ZXJuIl0pXQogICAgICAgIGlmIG1lZGl1bV9tYW51YWw6CiAgICAgICAgICAgIGJ5X3BhdHRlcm4yOiBkaWN0W3N0ciwgaW50XSA9IHt9CiAgICAgICAgICAgIGZvciBmIGluIG1lZGl1bV9tYW51YWw6CiAgICAgICAgICAgICAgICBieV9wYXR0ZXJuMltmWyJwYXR0ZXJuIl1dID0gYnlfcGF0dGVybjIuZ2V0KGZbInBhdHRlcm4iXSwgMCkgKyAxCiAgICAgICAgICAgIGxpbmVzLmFwcGVuZCgiXG4jIyMgUGhhc2UgMyDigJQgTWVkaXVtIFNldmVyaXR5IikKICAgICAgICAgICAgZm9yIHBpZCwgY291bnQgaW4gc29ydGVkKGJ5X3BhdHRlcm4yLml0ZW1zKCksIGtleT1sYW1iZGEgeDogLXhbMV0pOgogICAgICAgICAgICAgICAgdGl0bGUgPSBuZXh0KChwWyJ0aXRsZSJdIGZvciBwIGluIFBBVFRFUk5TIGlmIHBbImlkIl0gPT0gcGlkKSwgcGlkKQogICAgICAgICAgICAgICAgbGluZXMuYXBwZW5kKGYiICAtIGB7cGlkfWAgw5cge2NvdW50fSDigJQge3RpdGxlfSIpCgogICAgICAgICMgRHJpbGwtZG93biBoaW50cwogICAgICAgIGxpbmVzICs9IFsKICAgICAgICAgICAgIlxuLS0tXG4iLAogICAgICAgICAgICAiIyMg8J+UrCBEcmlsbCBEb3duIHdpdGggU3ViLUFnZW50c1xuIiwKICAgICAgICAgICAgIkNhbGwgdGhlc2UgdG9vbHMgZm9yIGEgZnVsbCBwZXItZmlsZSBicmVha2Rvd24gb2YgZWFjaCBjYXRlZ29yeTpcbiIsCiAgICAgICAgXQogICAgICAgIGdyb3VwX3Rvb2xfbWFwID0gewogICAgICAgICAgICAiRGF0YSBUcmFuc2ZlciI6ICAgICAgICAgICAgInNjYW5fZGF0YV90cmFuc2Zlcl9pc3N1ZXMiLAogICAgICAgICAgICAiRmxvd0ZpZWxkcyI6ICAgICAgICAgICAgICAgInNjYW5fZmxvd2ZpZWxkX2lzc3VlcyIsCiAgICAgICAgICAgICJBZ2dyZWdhdGlvbiI6ICAgICAgICAgICAgICAic2Nhbl9hZ2dyZWdhdGlvbl9pc3N1ZXMiLAogICAgICAgICAgICAiQnVsayBPcGVyYXRpb25zIjogICAgICAgICAgInNjYW5fYnVsa19vcGVyYXRpb25faXNzdWVzIiwKICAgICAgICAgICAgIkV4aXN0ZW5jZSBDaGVja3MiOiAgICAgICAgICJzY2FuX2V4aXN0ZW5jZV9jaGVja19pc3N1ZXMiLAogICAgICAgICAgICAiTG9ja2luZyI6ICAgICAgICAgICAgICAgICAgInNjYW5fbG9ja2luZ19pc3N1ZXMiLAogICAgICAgICAgICAiTWVtb3J5IC8gQ29waWVzIjogICAgICAgICAgInNjYW5fbWVtb3J5X2lzc3VlcyIsCiAgICAgICAgICAgICJTaG9ydC1DaXJjdWl0IEV2YWx1YXRpb24iOiAic2Nhbl9zaG9ydF9jaXJjdWl0X2lzc3VlcyIsCiAgICAgICAgICAgICJXcml0ZSBQYXR0ZXJucyI6ICAgICAgICAgICAic2Nhbl93cml0ZV9wYXR0ZXJuX2lzc3VlcyIsCiAgICAgICAgICAgICJDdXJzb3IgU2FmZXR5IjogICAgICAgICAgICAic2Nhbl9jdXJzb3Jfc2FmZXR5X2lzc3VlcyIsCiAgICAgICAgICAgICJTdGFsZSBSZWFkcyI6ICAgICAgICAgICAgICAic2Nhbl9zdGFsZV9yZWFkX2lzc3VlcyIsCiAgICAgICAgICAgICJTb3J0IC8gS2V5cyI6ICAgICAgICAgICAgICAic2Nhbl9kYXRhX3RyYW5zZmVyX2lzc3VlcyIsICAjIGNvdmVyZWQgYnkgZGF0YSB0cmFuc2ZlcgogICAgICAgIH0KICAgICAgICBmb3IgciBpbiBncm91cF9yZXN1bHRzOgogICAgICAgICAgICBpZiByWyJ0b3RhbCJdID4gMDoKICAgICAgICAgICAgICAgIHRvb2wgPSBncm91cF90b29sX21hcC5nZXQoclsiZ3JvdXAiXSwgInNjYW5fYWxfd29ya3NwYWNlIikKICAgICAgICAgICAgICAgIGxpbmVzLmFwcGVuZChmIi0gKip7clsnZ3JvdXAnXX0qKiAoe3JbJ3RvdGFsJ119IGlzc3Vlcykg4oaSIGB7dG9vbH0oXCJ7Zm9sZGVyX3BhdGh9XCIpYCIpCgogICAgcmV0dXJuICJcbiIuam9pbihsaW5lcykKCgppZiBfX25hbWVfXyA9PSAiX19tYWluX18iOgogICAgbWNwLnJ1bigpCg=='
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
AL Performance MCP Server
|
|
4
|
+
=========================
|
|
5
|
+
MCP server that scans AL source files for performance anti-patterns
|
|
6
|
+
and applies optimized fixes — based on the BC TechDays 2026 Workshop
|
|
7
|
+
(44 exercises covering real-world AL performance patterns).
|
|
8
|
+
|
|
9
|
+
Tools exposed:
|
|
10
|
+
scan_al_workspace — scan a folder for all performance issues
|
|
11
|
+
fix_al_file — apply all fixes to a single AL file
|
|
12
|
+
fix_al_workspace — apply all fixes to every AL file in a folder
|
|
13
|
+
list_patterns — list all known patterns with descriptions
|
|
14
|
+
explain_pattern — explain a single pattern in depth
|
|
15
|
+
|
|
16
|
+
Usage with Claude Desktop / VS Code Copilot:
|
|
17
|
+
Add to mcp.json (see mcp.json in this folder)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
import json
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from mcp.server.fastmcp import FastMCP
|
|
26
|
+
|
|
27
|
+
mcp = FastMCP("AL Performance Analyzer")
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Pattern registry
|
|
31
|
+
# Each entry:
|
|
32
|
+
# id — short identifier used in findings
|
|
33
|
+
# group — category of issue
|
|
34
|
+
# severity — HIGH / MEDIUM / LOW
|
|
35
|
+
# title — one-line description
|
|
36
|
+
# description — full explanation
|
|
37
|
+
# exercises — which workshop exercises cover this
|
|
38
|
+
# detector — callable(text) -> list[dict(line, snippet)]
|
|
39
|
+
# fixer — callable(text) -> str (returns fixed text)
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
PATTERNS: list[dict] = []
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _register(id, group, severity, title, description, exercises):
|
|
46
|
+
"""Decorator factory that registers a pattern with its detector and fixer."""
|
|
47
|
+
def decorator(cls):
|
|
48
|
+
PATTERNS.append({
|
|
49
|
+
"id": id,
|
|
50
|
+
"group": group,
|
|
51
|
+
"severity": severity,
|
|
52
|
+
"title": title,
|
|
53
|
+
"description": description,
|
|
54
|
+
"exercises": exercises,
|
|
55
|
+
"detector": cls.detect,
|
|
56
|
+
"fixer": cls.fix,
|
|
57
|
+
})
|
|
58
|
+
return cls
|
|
59
|
+
return decorator
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _find_line(text: str, char_offset: int) -> int:
|
|
63
|
+
return text[:char_offset].count('\n') + 1
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _findings(text: str, pattern: re.Pattern, message_fn=None) -> list[dict]:
|
|
67
|
+
out = []
|
|
68
|
+
for m in pattern.finditer(text):
|
|
69
|
+
line = _find_line(text, m.start())
|
|
70
|
+
snippet = m.group(0).strip()[:120]
|
|
71
|
+
out.append({"line": line, "snippet": snippet, "message": message_fn(m) if message_fn else ""})
|
|
72
|
+
return out
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
76
|
+
# PATTERN 01 — Missing SetLoadFields before FindSet / Find
|
|
77
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
78
|
+
@_register(
|
|
79
|
+
id="MISSING_SETLOADFIELDS",
|
|
80
|
+
group="Data Transfer",
|
|
81
|
+
severity="HIGH",
|
|
82
|
+
title="FindSet/Find without SetLoadFields",
|
|
83
|
+
description=('Customer.FindSet() without SetLoadFields generates a SELECT with ALL 100+ fields — including every table extension column joined in. On each row, the NST deserialises a massive buffer it doesn\'t need.\n\nSQL IMPACT: SELECT \\"18\\".\\"timestamp\\",\\"18\\".\\"No_\\",\\"18\\".\\"Name\\"... (all 100+ columns) + JOIN extension tables.\nWith 10 000 customers that\'s gigabytes of needlessly transferred data.\n\nBAD: if Customer.FindSet() then\nGOOD: Customer.SetLoadFields(\\"No.\\", Name, \\"Credit Limit (LCY)\\");\n if Customer.FindSet() then\n\nHINT: Look at the SELECT in SQL Profiler. Count how many columns are fetched. Then add SetLoadFields and compare.'),
|
|
84
|
+
exercises=[1, 2, 33],
|
|
85
|
+
)
|
|
86
|
+
class PatternMissingSetLoadFields:
|
|
87
|
+
# Detect: FindSet/Find on a record variable NOT immediately preceded (within 3 lines) by SetLoadFields
|
|
88
|
+
_FIND = re.compile(r'(\w+)\.(FindSet|FindFirst|FindLast|Find\()', re.MULTILINE)
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def detect(text: str) -> list[dict]:
|
|
92
|
+
findings = []
|
|
93
|
+
lines = text.splitlines()
|
|
94
|
+
for i, line in enumerate(lines):
|
|
95
|
+
m = re.search(r'(\w+)\.(FindSet|FindFirst|FindLast|Find\()', line)
|
|
96
|
+
if not m:
|
|
97
|
+
continue
|
|
98
|
+
var = m.group(1)
|
|
99
|
+
if var.lower() in ('true', 'false', 'result', 'rec'):
|
|
100
|
+
continue
|
|
101
|
+
# Check 3 lines above for SetLoadFields on same var
|
|
102
|
+
context = '\n'.join(lines[max(0, i-3):i])
|
|
103
|
+
if f'{var}.SetLoadFields' not in context and f'{var}.SetAutoCalcFields' not in context:
|
|
104
|
+
# Exclude if in a comment
|
|
105
|
+
stripped = line.strip()
|
|
106
|
+
if stripped.startswith('//'):
|
|
107
|
+
continue
|
|
108
|
+
findings.append({
|
|
109
|
+
"line": i + 1,
|
|
110
|
+
"snippet": line.strip()[:120],
|
|
111
|
+
"message": f"'{var}.{m.group(2)}' called without SetLoadFields"
|
|
112
|
+
})
|
|
113
|
+
return findings
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def fix(text: str) -> str:
|
|
117
|
+
# This pattern requires context-specific fixes (which fields to load)
|
|
118
|
+
# We annotate rather than blindly fix
|
|
119
|
+
return text
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
123
|
+
# PATTERN 02 — Find('-') buffer-size-1 anti-pattern
|
|
124
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
125
|
+
@_register(
|
|
126
|
+
id="FIND_DASH_BUFFER_ONE",
|
|
127
|
+
group="Data Transfer",
|
|
128
|
+
severity="HIGH",
|
|
129
|
+
title="Find('-') with buffer size 1 (use FindSet instead)",
|
|
130
|
+
description=("Find('-') pre-fetches only 1 record at a time (read buffer = 1). Each call to Next() fires a new SQL query: SELECT TOP 50 ...\nFor 1 000 entries that is 20 round-trips instead of 1 with FindSet. Find('-') also ignores any previous SetCurrentKey call.\n\nSQL IMPACT: SELECT TOP 50 ... (repeated per Next() batch)\nvs FindSet: single SELECT that pre-fetches rows efficiently.\n\nBAD: if Record.Find('-') then repeat ... until Record.Next() = 0;\nGOOD: if Record.FindSet() then repeat ... until Record.Next() = 0;\n\nHINT: Compare the number of SQL queries for Find('-') vs FindSet on 1 000 rows. Check the Reads column in Profiler."),
|
|
131
|
+
exercises=[2, 24],
|
|
132
|
+
)
|
|
133
|
+
class PatternFindDash:
|
|
134
|
+
_RE = re.compile(r"(\w+)\.Find\('-'\)", re.MULTILINE)
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def detect(text: str) -> list[dict]:
|
|
138
|
+
return _findings(text, PatternFindDash._RE,
|
|
139
|
+
lambda m: f"Use FindSet() instead of {m.group(1)}.Find('-')")
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def fix(text: str) -> str:
|
|
143
|
+
return PatternFindDash._RE.sub(lambda m: f"{m.group(1)}.FindSet()", text)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
147
|
+
# PATTERN 03 — CalcFields inside a loop
|
|
148
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
149
|
+
@_register(
|
|
150
|
+
id="CALCFIELDS_IN_LOOP",
|
|
151
|
+
group="FlowFields",
|
|
152
|
+
severity="HIGH",
|
|
153
|
+
title="CalcFields inside a repeat..until loop",
|
|
154
|
+
description=('Calling CalcFields(Balance) inside a FindSet loop fires one extra SQL sub-query per iteration to compute the FlowField aggregate. For 500 vendors that is 500 extra SQL round-trips just for Balance. SetAutoCalcFields merges the FlowField into the main SELECT.\n\nSQL IMPACT: Per row: SELECT SUM(...) FROM \\"Detailed Vendor Ledg. Entry\\" ... = N additional queries inside the loop.\n\nBAD: repeat Customer.CalcFields(Balance); until Customer.Next() = 0;\nGOOD: Customer.SetAutoCalcFields(Balance);\n if Customer.FindSet() then repeat ... until Customer.Next() = 0;\n\nHINT: Count the extra SQL queries per loop iteration with CalcFields vs AutoCalcFields. Duration difference grows with record count.'),
|
|
155
|
+
exercises=[3],
|
|
156
|
+
)
|
|
157
|
+
class PatternCalcFieldsInLoop:
|
|
158
|
+
_RE = re.compile(r'^\s+\w+\.CalcFields\([^)]+\);', re.MULTILINE)
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def detect(text: str) -> list[dict]:
|
|
162
|
+
findings = []
|
|
163
|
+
lines = text.splitlines()
|
|
164
|
+
in_loop = False
|
|
165
|
+
depth = 0
|
|
166
|
+
for i, line in enumerate(lines):
|
|
167
|
+
stripped = line.strip()
|
|
168
|
+
if stripped.lower() == 'repeat':
|
|
169
|
+
in_loop = True
|
|
170
|
+
depth += 1
|
|
171
|
+
if re.match(r'until\s+\w+\.Next\(\)', stripped, re.IGNORECASE):
|
|
172
|
+
depth -= 1
|
|
173
|
+
if depth <= 0:
|
|
174
|
+
in_loop = False
|
|
175
|
+
if in_loop and re.search(r'\w+\.CalcFields\(', line):
|
|
176
|
+
if not stripped.startswith('//'):
|
|
177
|
+
findings.append({
|
|
178
|
+
"line": i + 1,
|
|
179
|
+
"snippet": line.strip()[:120],
|
|
180
|
+
"message": "CalcFields() inside loop — use SetAutoCalcFields() before FindSet()"
|
|
181
|
+
})
|
|
182
|
+
return findings
|
|
183
|
+
|
|
184
|
+
@staticmethod
|
|
185
|
+
def fix(text: str) -> str:
|
|
186
|
+
return text # Context-specific; needs the variable name
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
190
|
+
# PATTERN 04 — Record parameter passed by value (missing var)
|
|
191
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
192
|
+
@_register(
|
|
193
|
+
id="RECORD_BY_VALUE",
|
|
194
|
+
group="Memory / Copies",
|
|
195
|
+
severity="MEDIUM",
|
|
196
|
+
title="Record parameter passed by value instead of var",
|
|
197
|
+
description=('Without VAR, AL copies the entire Record instance into a new memory block: all field values, filter state, cursor position. Large records (Customer, Item) copied on every call. With many nested calls this causes significant GC pressure on NST.\n\nSQL IMPACT: No SQL change — overhead is in NST memory allocation & copy.\n\nBAD: local procedure Foo(Customer: Record Customer)\nGOOD: local procedure Foo(var Customer: Record Customer)\n\nHINT: No SQL difference — measure NST throughput and allocation counters.'),
|
|
198
|
+
exercises=[4],
|
|
199
|
+
)
|
|
200
|
+
class PatternRecordByValue:
|
|
201
|
+
_RE = re.compile(
|
|
202
|
+
r'(local\s+procedure|procedure)\s+\w+\((?:[^)]*,\s*)?(\w+):\s*Record\s+[^;)]+\)',
|
|
203
|
+
re.MULTILINE
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
@staticmethod
|
|
207
|
+
def detect(text: str) -> list[dict]:
|
|
208
|
+
findings = []
|
|
209
|
+
for m in PatternRecordByValue._RE.finditer(text):
|
|
210
|
+
line_text = text[max(0, m.start()-200):m.end()]
|
|
211
|
+
if '(var ' in m.group(0) or re.search(r'\bvar\s+\w+:\s*Record', m.group(0)):
|
|
212
|
+
continue
|
|
213
|
+
if m.group(2) == 'var':
|
|
214
|
+
continue
|
|
215
|
+
line = _find_line(text, m.start())
|
|
216
|
+
findings.append({
|
|
217
|
+
"line": line,
|
|
218
|
+
"snippet": m.group(0)[:120],
|
|
219
|
+
"message": "Record parameter without 'var' — creates a full copy on each call"
|
|
220
|
+
})
|
|
221
|
+
return findings
|
|
222
|
+
|
|
223
|
+
@staticmethod
|
|
224
|
+
def fix(text: str) -> str:
|
|
225
|
+
return text
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
229
|
+
# PATTERN 05 — Loop sum instead of CalcSums
|
|
230
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
231
|
+
@_register(
|
|
232
|
+
id="LOOP_SUM_VS_CALCSUMS",
|
|
233
|
+
group="Aggregation",
|
|
234
|
+
severity="HIGH",
|
|
235
|
+
title="Manual summation loop instead of CalcSums",
|
|
236
|
+
description=('Summing a field by iterating all rows in AL: fetches every row from SQL, deserialises it in NST, accumulates in a local variable. CalcSums(Amount) translates to a SQL SUM() aggregate — computed at the database, returns a single row.\n\nSQL IMPACT: Manual loop: SELECT all rows, process N rows in NST\nCalcSums: SELECT SUM(\\"Amount\\") ... — returns 1 row.\n\nBAD: repeat Total += Record.Amount; until Record.Next() = 0;\nGOOD: Record.CalcSums(Amount); Total := Record.Amount;\n\nHINT: CalcSums result: 1 SQL row returned. Loop result: N SQL rows fetched and summed. Check Reads: CalcSums = 1, loop = N.'),
|
|
237
|
+
exercises=[5],
|
|
238
|
+
)
|
|
239
|
+
class PatternLoopSum:
|
|
240
|
+
_RE = re.compile(r'(\w+)\s*\+=\s*\w+\.\w+;', re.MULTILINE)
|
|
241
|
+
|
|
242
|
+
@staticmethod
|
|
243
|
+
def detect(text: str) -> list[dict]:
|
|
244
|
+
findings = []
|
|
245
|
+
lines = text.splitlines()
|
|
246
|
+
in_loop = False
|
|
247
|
+
for i, line in enumerate(lines):
|
|
248
|
+
stripped = line.strip()
|
|
249
|
+
if stripped.lower() == 'repeat':
|
|
250
|
+
in_loop = True
|
|
251
|
+
if re.match(r'until\s+\w+\.Next\(\)', stripped, re.IGNORECASE):
|
|
252
|
+
in_loop = False
|
|
253
|
+
if in_loop and PatternLoopSum._RE.search(line) and not stripped.startswith('//'):
|
|
254
|
+
findings.append({
|
|
255
|
+
"line": i + 1,
|
|
256
|
+
"snippet": line.strip()[:120],
|
|
257
|
+
"message": "Manual += accumulation in loop — consider CalcSums() if summing a single field"
|
|
258
|
+
})
|
|
259
|
+
return findings
|
|
260
|
+
|
|
261
|
+
@staticmethod
|
|
262
|
+
def fix(text: str) -> str:
|
|
263
|
+
return text
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
267
|
+
# PATTERN 06 — String concatenation += in loop (use TextBuilder)
|
|
268
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
269
|
+
@_register(
|
|
270
|
+
id="STRING_CONCAT_IN_LOOP",
|
|
271
|
+
group="Memory / Copies",
|
|
272
|
+
severity="HIGH",
|
|
273
|
+
title="Text += string concatenation in loop (O(n²)) — use TextBuilder",
|
|
274
|
+
description=("AL's Text type is a value type. Each += creates a brand-new string object in NST memory — old strings become GC garbage immediately. For N iterations: O(N²) memory allocations and copies. TextBuilder wraps .NET StringBuilder: O(N) time, no intermediate copies.\n\nSQL IMPACT: No SQL impact — pure NST memory pressure & GC overhead.\n\nBAD: CsvContent += BuildDetailLine(...);\nGOOD: var Builder: TextBuilder;\n Builder.Append(BuildDetailLine(...));\n Result := Builder.ToText(); // Single allocation at end\n\nHINT: The GC pressure is invisible in SQL Profiler but shows up in NST memory counters."),
|
|
275
|
+
exercises=[6],
|
|
276
|
+
)
|
|
277
|
+
class PatternStringConcatInLoop:
|
|
278
|
+
_RE = re.compile(r'(\w+)\s*\+=\s*(?![\d.])', re.MULTILINE)
|
|
279
|
+
|
|
280
|
+
@staticmethod
|
|
281
|
+
def detect(text: str) -> list[dict]:
|
|
282
|
+
findings = []
|
|
283
|
+
lines = text.splitlines()
|
|
284
|
+
in_loop = False
|
|
285
|
+
for i, line in enumerate(lines):
|
|
286
|
+
stripped = line.strip()
|
|
287
|
+
if stripped.lower() == 'repeat' or re.search(r'\bforeach\b', stripped, re.IGNORECASE):
|
|
288
|
+
in_loop = True
|
|
289
|
+
if re.match(r'until\s+\w+\.Next\(\)', stripped, re.IGNORECASE) or stripped == 'end;':
|
|
290
|
+
in_loop = False
|
|
291
|
+
if in_loop:
|
|
292
|
+
m = PatternStringConcatInLoop._RE.search(line)
|
|
293
|
+
if m and not stripped.startswith('//'):
|
|
294
|
+
var = m.group(1)
|
|
295
|
+
# Look back to see if it's declared as Text
|
|
296
|
+
context = '\n'.join(lines[max(0, i-30):i])
|
|
297
|
+
if re.search(rf'\b{re.escape(var)}\s*:\s*Text\b', context):
|
|
298
|
+
findings.append({
|
|
299
|
+
"line": i + 1,
|
|
300
|
+
"snippet": line.strip()[:120],
|
|
301
|
+
"message": f"Text += in loop creates O(n²) allocations — use TextBuilder"
|
|
302
|
+
})
|
|
303
|
+
return findings
|
|
304
|
+
|
|
305
|
+
@staticmethod
|
|
306
|
+
def fix(text: str) -> str:
|
|
307
|
+
return text
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
311
|
+
# PATTERN 07 — FindSet without (true) when modifying via cursor
|
|
312
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
313
|
+
@_register(
|
|
314
|
+
id="FINDSET_MODIFY_NO_TRUE",
|
|
315
|
+
group="Locking",
|
|
316
|
+
severity="MEDIUM",
|
|
317
|
+
title="FindSet() without (true) when Modify is called on cursor variable",
|
|
318
|
+
description=('FindSet uses ReadUncommitted isolation and pre-fetches rows in batches. Calling Modify inside the loop body can invalidate the cursor position. Records may be visited twice, skipped entirely, or cause deadlocks. The cursor is not designed to survive concurrent DML on the same rows.\n\nSQL IMPACT: SELECT TOP 50 ... (ReadUncommitted) then UPDATE inside — cursor instability.\n\nBAD: if Record.FindSet() then repeat Record.Field := ...; Record.Modify(); ...\nGOOD: if Record.FindSet(true) then repeat Record.Field := ...; Record.Modify(); ...\n\nHINT: Use SQL Profiler to watch which records get updated. Run the anti-pattern twice — some records may be updated twice or skipped.'),
|
|
319
|
+
exercises=[7],
|
|
320
|
+
)
|
|
321
|
+
class PatternFindSetNoForUpdate:
|
|
322
|
+
_RE = re.compile(r'(\w+)\.FindSet\(\)', re.MULTILINE)
|
|
323
|
+
|
|
324
|
+
@staticmethod
|
|
325
|
+
def detect(text: str) -> list[dict]:
|
|
326
|
+
findings = []
|
|
327
|
+
lines = text.splitlines()
|
|
328
|
+
for i, line in enumerate(lines):
|
|
329
|
+
m = PatternFindSetNoForUpdate._RE.search(line)
|
|
330
|
+
if not m or line.strip().startswith('//'):
|
|
331
|
+
continue
|
|
332
|
+
var = m.group(1)
|
|
333
|
+
# Look ahead for Modify on the same var
|
|
334
|
+
ahead = '\n'.join(lines[i:min(len(lines), i+15)])
|
|
335
|
+
if re.search(rf'\b{re.escape(var)}\.Modify\(', ahead):
|
|
336
|
+
findings.append({
|
|
337
|
+
"line": i + 1,
|
|
338
|
+
"snippet": line.strip()[:120],
|
|
339
|
+
"message": f"'{var}.FindSet()' but '{var}.Modify()' found in loop — use FindSet(true)"
|
|
340
|
+
})
|
|
341
|
+
return findings
|
|
342
|
+
|
|
343
|
+
@staticmethod
|
|
344
|
+
def fix(text: str) -> str:
|
|
345
|
+
# Only fix when Modify follows in same loop — conservative approach
|
|
346
|
+
def replace_if_modify_follows(m):
|
|
347
|
+
pos = m.end()
|
|
348
|
+
ahead = text[pos:pos+500]
|
|
349
|
+
var = m.group(1)
|
|
350
|
+
if re.search(rf'\b{re.escape(var)}\.Modify\(', ahead):
|
|
351
|
+
return f"{var}.FindSet(true)"
|
|
352
|
+
return m.group(0)
|
|
353
|
+
return re.sub(r'(\w+)\.FindSet\(\)', replace_if_modify_follows, text)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
357
|
+
# PATTERN 08 — Delete inside a loop (use DeleteAll)
|
|
358
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
359
|
+
@_register(
|
|
360
|
+
id="DELETE_IN_LOOP",
|
|
361
|
+
group="Bulk Operations",
|
|
362
|
+
severity="HIGH",
|
|
363
|
+
title="Record.Delete() inside a loop — use DeleteAll()",
|
|
364
|
+
description=('Same cursor instability as Modify — Delete during FindSet can cause the read cursor to skip or revisit records. DeleteAll is atomic and uses a single DELETE FROM statement. If selective deletion is needed, collect keys first, delete after loop.\n\nSQL IMPACT: SELECT TOP 50 (cursor) + DELETE per row — cursor corruption risk.\nDeleteAll: 1 DELETE FROM statement vs N individual DELETEs.\n\nBAD: if Record.FindSet() then repeat Record.Delete(); until Record.Next() = 0;\nGOOD: Record.SetRange(Active, false); Record.DeleteAll(false);\n\nHINT: Compare the number of DELETE statements in Profiler: DeleteAll = 1 SQL DELETE; loop = N DELETEs.'),
|
|
365
|
+
exercises=[8],
|
|
366
|
+
)
|
|
367
|
+
class PatternDeleteInLoop:
|
|
368
|
+
@staticmethod
|
|
369
|
+
def detect(text: str) -> list[dict]:
|
|
370
|
+
findings = []
|
|
371
|
+
lines = text.splitlines()
|
|
372
|
+
in_loop = False
|
|
373
|
+
loop_var = None
|
|
374
|
+
for i, line in enumerate(lines):
|
|
375
|
+
stripped = line.strip()
|
|
376
|
+
m = re.search(r'(\w+)\.FindSet', line)
|
|
377
|
+
if m and not stripped.startswith('//'):
|
|
378
|
+
in_loop = True
|
|
379
|
+
loop_var = m.group(1)
|
|
380
|
+
if in_loop and loop_var and re.search(rf'\b{re.escape(loop_var)}\.Delete\(', line):
|
|
381
|
+
if not stripped.startswith('//'):
|
|
382
|
+
findings.append({
|
|
383
|
+
"line": i + 1,
|
|
384
|
+
"snippet": line.strip()[:120],
|
|
385
|
+
"message": f"'{loop_var}.Delete()' inside FindSet loop — use '{loop_var}.DeleteAll(false)'"
|
|
386
|
+
})
|
|
387
|
+
if re.match(r'until\s+\w+\.Next\(\)', stripped, re.IGNORECASE):
|
|
388
|
+
in_loop = False
|
|
389
|
+
loop_var = None
|
|
390
|
+
return findings
|
|
391
|
+
|
|
392
|
+
@staticmethod
|
|
393
|
+
def fix(text: str) -> str:
|
|
394
|
+
return text
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
398
|
+
# PATTERN 09 — Count() <> 0 instead of not IsEmpty()
|
|
399
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
400
|
+
@_register(
|
|
401
|
+
id="COUNT_NOT_ZERO",
|
|
402
|
+
group="Existence Checks",
|
|
403
|
+
severity="MEDIUM",
|
|
404
|
+
title="Count() <> 0 / Count() > 0 — use IsEmpty() instead",
|
|
405
|
+
description=('Count() generates SELECT COUNT(*) — the database must count ALL matching rows before returning. On large tables this is expensive. IsEmpty() generates IF EXISTS (SELECT TOP 1 NULL ...) — stops at the first match. O(1) vs O(N).\n\nSQL IMPACT: Count(): SELECT COUNT(*) FROM ... → scans entire table\nIsEmpty(): IF EXISTS (SELECT TOP 1 NULL FROM ...) → stops immediately.\n\nBAD: exit(Record.Count() <> 0);\nGOOD: exit(not Record.IsEmpty());\n\nHINT: Compare SQL Profiler output for both. Count() always reads all rows first. IsEmpty stops at the very first match.'),
|
|
406
|
+
exercises=[9, 10, 11],
|
|
407
|
+
)
|
|
408
|
+
class PatternCountNotZero:
|
|
409
|
+
_RE = re.compile(r'(\w+)\.Count\(\)\s*(?:<>|>|=)\s*0', re.MULTILINE)
|
|
410
|
+
|
|
411
|
+
@staticmethod
|
|
412
|
+
def detect(text: str) -> list[dict]:
|
|
413
|
+
return _findings(text, PatternCountNotZero._RE,
|
|
414
|
+
lambda m: f"'{m.group(1)}.Count() != 0' — use IsEmpty() for O(1) existence check")
|
|
415
|
+
|
|
416
|
+
@staticmethod
|
|
417
|
+
def fix(text: str) -> str:
|
|
418
|
+
def replace(m):
|
|
419
|
+
var = m.group(1)
|
|
420
|
+
op = m.group(0).split('Count()')[1].strip().split(' ')[0]
|
|
421
|
+
if op in ('<>', '>'):
|
|
422
|
+
return f'not {var}.IsEmpty()'
|
|
423
|
+
if op == '=':
|
|
424
|
+
return f'{var}.IsEmpty()'
|
|
425
|
+
return m.group(0)
|
|
426
|
+
return PatternCountNotZero._RE.sub(replace, text)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
430
|
+
# PATTERN 10 — Redundant IsEmpty before FindSet (double scan)
|
|
431
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
432
|
+
@_register(
|
|
433
|
+
id="ISEMPTY_BEFORE_FINDSET",
|
|
434
|
+
group="Existence Checks",
|
|
435
|
+
severity="MEDIUM",
|
|
436
|
+
title="Redundant IsEmpty() before FindSet() (two SQL scans instead of one)",
|
|
437
|
+
description=('Checking IsEmpty() immediately before FindSet fires TWO SQL queries: 1) IF EXISTS (SELECT TOP 1 NULL ...) — the IsEmpty check; 2) SELECT TOP 50 ... — the FindSet. FindSet already returns false when no rows match — the IsEmpty is waste.\n\nSQL IMPACT: Query 1: IF EXISTS (SELECT TOP 1 NULL ...). Query 2: SELECT TOP 50 ... → redundant.\n\nBAD: if not Record.IsEmpty() then\n if Record.FindSet() then ...\nGOOD: if Record.FindSet() then ...\n\nHINT: Count the SQL queries in Profiler: IsEmpty + FindSet = 2; just FindSet = 1. The IsEmpty result is ignored by FindSet anyway.'),
|
|
438
|
+
exercises=[11],
|
|
439
|
+
)
|
|
440
|
+
class PatternIsEmptyBeforeFindSet:
|
|
441
|
+
@staticmethod
|
|
442
|
+
def detect(text: str) -> list[dict]:
|
|
443
|
+
findings = []
|
|
444
|
+
lines = text.splitlines()
|
|
445
|
+
for i, line in enumerate(lines):
|
|
446
|
+
if re.search(r'not\s+\w+\.IsEmpty\(\)', line) and not line.strip().startswith('//'):
|
|
447
|
+
# Check next 2 lines for FindSet
|
|
448
|
+
ahead = '\n'.join(lines[i:min(len(lines), i+3)])
|
|
449
|
+
if re.search(r'\.FindSet\(', ahead):
|
|
450
|
+
findings.append({
|
|
451
|
+
"line": i + 1,
|
|
452
|
+
"snippet": line.strip()[:120],
|
|
453
|
+
"message": "Redundant IsEmpty() guard before FindSet() — FindSet() returns false when empty"
|
|
454
|
+
})
|
|
455
|
+
return findings
|
|
456
|
+
|
|
457
|
+
@staticmethod
|
|
458
|
+
def fix(text: str) -> str:
|
|
459
|
+
return re.sub(
|
|
460
|
+
r'[ \t]+if not (\w+)\.IsEmpty\(\) then\s*\n([ \t]+)if \1\.FindSet\(',
|
|
461
|
+
r'\2if \1.FindSet(',
|
|
462
|
+
text
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
467
|
+
# PATTERN 11 — Missing 'temporary' keyword on temp table variable
|
|
468
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
469
|
+
@_register(
|
|
470
|
+
id="MISSING_TEMPORARY",
|
|
471
|
+
group="Memory / Copies",
|
|
472
|
+
severity="HIGH",
|
|
473
|
+
title="Temp table variable missing 'temporary' keyword",
|
|
474
|
+
description=('A non-temporary record sends every Insert/Get/Delete to SQL Server. Using a regular table as an in-memory scratchpad causes unnecessary SQL round-trips, transaction log writes, and lock contention. Declaring the variable \'temporary\' keeps all operations in NST memory.\n\nSQL IMPACT: Without temporary: INSERT INTO / SELECT / DELETE — full SQL for each op.\nWith temporary: All operations in NST memory — zero SQL round-trips.\n\nBAD: TempData: Record \\"Workshop Data\\";\nGOOD: TempData: Record \\"Workshop Data\\" temporary;\n\nHINT: Insert 1 000 rows with and without \'temporary\'. Without: watch 1 000 INSERTs in SQL Profiler. With temporary: zero SQL activity.'),
|
|
475
|
+
exercises=[12],
|
|
476
|
+
)
|
|
477
|
+
class PatternMissingTemporary:
|
|
478
|
+
@staticmethod
|
|
479
|
+
def detect(text: str) -> list[dict]:
|
|
480
|
+
findings = []
|
|
481
|
+
lines = text.splitlines()
|
|
482
|
+
for i, line in enumerate(lines):
|
|
483
|
+
# Variable declared with Temp prefix but no 'temporary' keyword
|
|
484
|
+
if re.search(r'\bTemp\w+\s*:\s*Record\s+', line) and 'temporary' not in line and not line.strip().startswith('//'):
|
|
485
|
+
findings.append({
|
|
486
|
+
"line": i + 1,
|
|
487
|
+
"snippet": line.strip()[:120],
|
|
488
|
+
"message": "Variable with 'Temp' prefix is missing 'temporary' keyword"
|
|
489
|
+
})
|
|
490
|
+
return findings
|
|
491
|
+
|
|
492
|
+
@staticmethod
|
|
493
|
+
def fix(text: str) -> str:
|
|
494
|
+
return re.sub(
|
|
495
|
+
r'(Temp\w+\s*:\s*Record\s+[^;]+);(?!\s*//.*temporary)',
|
|
496
|
+
lambda m: m.group(0).rstrip(';') + ' temporary;' if 'temporary' not in m.group(0) else m.group(0),
|
|
497
|
+
text
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
502
|
+
# PATTERN 12 — Temp table used as Dictionary (use Dictionary type)
|
|
503
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
504
|
+
@_register(
|
|
505
|
+
id="TEMP_TABLE_AS_DICT",
|
|
506
|
+
group="Memory / Copies",
|
|
507
|
+
severity="MEDIUM",
|
|
508
|
+
title="Temp Record table used as a key-value lookup (use Dictionary instead)",
|
|
509
|
+
description=("Even a 'temporary' table carries record-layer overhead: key indexes, transaction state tracking, schema validation on every operation. For simple key→value mapping (e.g. CustomerNo → SalespersonCode), the built-in Dictionary of [K, V] type has zero DB overhead.\n\nSQL IMPACT: Temp table: key index maintenance, record struct overhead per entry.\nDictionary: direct hash map lookup — O(1) Get/Set, no record overhead.\n\nBAD: TempCustomer: Record Customer temporary; TempCustomer.Insert(); TempCustomer.Get(...);\nGOOD: RoutingMap: Dictionary of [Code[20], Code[20]];\n RoutingMap.Set(..., ...); if RoutingMap.ContainsKey(...) then ...\n\nHINT: Compare Get/Set operations on temp table vs Dictionary. Measure time for 10 000 lookups each way."),
|
|
510
|
+
exercises=[13],
|
|
511
|
+
)
|
|
512
|
+
class PatternTempTableAsDict:
|
|
513
|
+
@staticmethod
|
|
514
|
+
def detect(text: str) -> list[dict]:
|
|
515
|
+
findings = []
|
|
516
|
+
lines = text.splitlines()
|
|
517
|
+
for i, line in enumerate(lines):
|
|
518
|
+
m = re.search(r'(Temp\w+)\s*:\s*Record\s+\S+\s+temporary', line)
|
|
519
|
+
if not m:
|
|
520
|
+
continue
|
|
521
|
+
var = m.group(1)
|
|
522
|
+
# Look ahead for both Insert and Get on same var — classic dict pattern
|
|
523
|
+
rest = '\n'.join(lines[i:min(len(lines), i+60)])
|
|
524
|
+
if re.search(rf'\b{re.escape(var)}\.Insert\(', rest) and re.search(rf'\b{re.escape(var)}\.Get\(', rest):
|
|
525
|
+
findings.append({
|
|
526
|
+
"line": i + 1,
|
|
527
|
+
"snippet": line.strip()[:120],
|
|
528
|
+
"message": f"'{var}' used as key-value store (Insert+Get pattern) — prefer Dictionary of [...]"
|
|
529
|
+
})
|
|
530
|
+
return findings
|
|
531
|
+
|
|
532
|
+
@staticmethod
|
|
533
|
+
def fix(text: str) -> str:
|
|
534
|
+
return text
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
538
|
+
# PATTERN 13 — FindFirst() used for loop iteration
|
|
539
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
540
|
+
@_register(
|
|
541
|
+
id="FINDFIRST_IN_LOOP",
|
|
542
|
+
group="Data Transfer",
|
|
543
|
+
severity="HIGH",
|
|
544
|
+
title="FindFirst() used for multi-row iteration (buffer size 1)",
|
|
545
|
+
description=('FindFirst() generates SELECT TOP 1 — fetches exactly one record. When used to start a loop (calling Next() repeatedly), every Next() call that exhausts the buffer fires a new SELECT TOP 50. FindSet() pre-fetches 50 rows immediately — far fewer SQL queries.\n\nSQL IMPACT: FindFirst: SELECT TOP 1 ... then SELECT TOP 50 per batch in Next().\nFindSet: Single SELECT that establishes the full read cursor.\n\nBAD: if Record.FindFirst() then repeat ... until Record.Next() = 0;\nGOOD: if Record.FindSet() then repeat ... until Record.Next() = 0;\n\nHINT: Trace SQL queries for a 500-row loop with FindFirst. FindFirst fires SELECT TOP 1 first, then Next batches. FindSet fires one SELECT and handles batching internally.'),
|
|
546
|
+
exercises=[14],
|
|
547
|
+
)
|
|
548
|
+
class PatternFindFirstInLoop:
|
|
549
|
+
@staticmethod
|
|
550
|
+
def detect(text: str) -> list[dict]:
|
|
551
|
+
findings = []
|
|
552
|
+
lines = text.splitlines()
|
|
553
|
+
for i, line in enumerate(lines):
|
|
554
|
+
m = re.search(r'(\w+)\.FindFirst\(\)', line)
|
|
555
|
+
if not m or line.strip().startswith('//'):
|
|
556
|
+
continue
|
|
557
|
+
var = m.group(1)
|
|
558
|
+
# Look ahead for Next() on same var
|
|
559
|
+
ahead = '\n'.join(lines[i:min(len(lines), i+20)])
|
|
560
|
+
if re.search(rf'\b{re.escape(var)}\.Next\(\)', ahead):
|
|
561
|
+
findings.append({
|
|
562
|
+
"line": i + 1,
|
|
563
|
+
"snippet": line.strip()[:120],
|
|
564
|
+
"message": f"'{var}.FindFirst()' followed by Next() — use FindSet() for multi-row iteration"
|
|
565
|
+
})
|
|
566
|
+
return findings
|
|
567
|
+
|
|
568
|
+
@staticmethod
|
|
569
|
+
def fix(text: str) -> str:
|
|
570
|
+
def replace(m):
|
|
571
|
+
var = m.group(1)
|
|
572
|
+
pos = m.end()
|
|
573
|
+
ahead = text[pos:pos+300]
|
|
574
|
+
if re.search(rf'\b{re.escape(var)}\.Next\(\)', ahead):
|
|
575
|
+
return f"{var}.FindSet()"
|
|
576
|
+
return m.group(0)
|
|
577
|
+
return re.sub(r'(\w+)\.FindFirst\(\)', replace, text)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
581
|
+
# PATTERN 14 — FindSet guard before ModifyAll (unnecessary)
|
|
582
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
583
|
+
@_register(
|
|
584
|
+
id="FINDSET_BEFORE_MODIFYALL",
|
|
585
|
+
group="Bulk Operations",
|
|
586
|
+
severity="MEDIUM",
|
|
587
|
+
title="FindSet() guard before ModifyAll() — unnecessary (double scan)",
|
|
588
|
+
description=('Calling FindSet() immediately before ModifyAll() fires an extra SELECT just to confirm rows exist. ModifyAll() already handles empty result sets gracefully — it does nothing if no rows match the current filters. The FindSet is pure overhead with zero functional benefit.\n\nSQL IMPACT: Extra query: SELECT TOP 50 ... (only to check existence).\nThen: UPDATE ... WHERE ... (ModifyAll — does this anyway).\nWith FindSet: 2 queries (SELECT + UPDATE). Without: 1 query (UPDATE only).\n\nBAD: if Record.FindSet() then Record.ModifyAll(...);\nGOOD: Record.ModifyAll(...);\n\nHINT: Look at SQL before ModifyAll with and without FindSet. With FindSet: 2 queries. Without: 1 query.'),
|
|
589
|
+
exercises=[15],
|
|
590
|
+
)
|
|
591
|
+
class PatternFindSetBeforeModifyAll:
|
|
592
|
+
_RE = re.compile(r'if\s+(\w+)\.FindSet\(\)\s+then\s+\n?\s+\1\.ModifyAll\(', re.MULTILINE)
|
|
593
|
+
|
|
594
|
+
@staticmethod
|
|
595
|
+
def detect(text: str) -> list[dict]:
|
|
596
|
+
return _findings(text, PatternFindSetBeforeModifyAll._RE,
|
|
597
|
+
lambda m: f"FindSet() guard before ModifyAll() on '{m.group(1)}' is redundant")
|
|
598
|
+
|
|
599
|
+
@staticmethod
|
|
600
|
+
def fix(text: str) -> str:
|
|
601
|
+
return re.sub(
|
|
602
|
+
r'if\s+(\w+)\.FindSet\(\)\s+then\s*\n(\s+)\1\.ModifyAll\(',
|
|
603
|
+
r'\2\1.ModifyAll(',
|
|
604
|
+
text
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
609
|
+
# PATTERN 15 — SetFilter on FlowField (use SetAutoCalcFields + AL filter)
|
|
610
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
611
|
+
@_register(
|
|
612
|
+
id="SETFILTER_ON_FLOWFIELD",
|
|
613
|
+
group="FlowFields",
|
|
614
|
+
severity="HIGH",
|
|
615
|
+
title="SetFilter on FlowField causes correlated subquery per row",
|
|
616
|
+
description=("SetFilter on a FlowField (e.g. Balance, which is a CalcField) cannot be pushed to the SQL WHERE clause — it has no physical column. BC must fetch ALL rows matching the other filters, then compute the FlowField for each row individually and filter in AL. Full table scan.\n\nSQL IMPACT: SELECT all rows ... then per row: SELECT SUM(...) for FlowField.\nSQL cannot filter on a computed value it hasn't calculated yet.\n\nBAD: Customer.SetFilter(Balance, '>%1', 0); if Customer.FindSet() then ...\nGOOD: Customer.SetAutoCalcFields(Balance);\n if Customer.FindSet() then repeat if Customer.Balance > 0 then ...\n\nHINT: Try SetFilter(Balance, '>1000') — check the SQL WHERE. Balance won't appear in WHERE — all rows fetched. Compare with filtering on a normal indexed decimal field."),
|
|
617
|
+
exercises=[16],
|
|
618
|
+
)
|
|
619
|
+
class PatternSetFilterOnFlowField:
|
|
620
|
+
# Common FlowFields in BC
|
|
621
|
+
_FLOWFIELDS = {'Balance', 'Balance (LCY)', 'Sales (LCY)', 'Purchases (LCY)',
|
|
622
|
+
'Outstanding Orders', 'Outstanding Invoices', 'Net Change'}
|
|
623
|
+
_RE = re.compile(r'(\w+)\.SetFilter\(([^,)]+),', re.MULTILINE)
|
|
624
|
+
|
|
625
|
+
@staticmethod
|
|
626
|
+
def detect(text: str) -> list[dict]:
|
|
627
|
+
findings = []
|
|
628
|
+
for m in PatternSetFilterOnFlowField._RE.finditer(text):
|
|
629
|
+
field = m.group(2).strip().strip('"')
|
|
630
|
+
if field in PatternSetFilterOnFlowField._FLOWFIELDS:
|
|
631
|
+
line = _find_line(text, m.start())
|
|
632
|
+
findings.append({
|
|
633
|
+
"line": line,
|
|
634
|
+
"snippet": m.group(0)[:120],
|
|
635
|
+
"message": f"SetFilter on FlowField '{field}' — use SetAutoCalcFields + AL-side filter"
|
|
636
|
+
})
|
|
637
|
+
return findings
|
|
638
|
+
|
|
639
|
+
@staticmethod
|
|
640
|
+
def fix(text: str) -> str:
|
|
641
|
+
return text
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
645
|
+
# PATTERN 16 — OR / AND both sides always evaluated (use nested IF)
|
|
646
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
647
|
+
@_register(
|
|
648
|
+
id="EAGER_EVALUATION_OR_AND",
|
|
649
|
+
group="Short-Circuit Evaluation",
|
|
650
|
+
severity="MEDIUM",
|
|
651
|
+
title="AL 'or'/'and' evaluates both sides eagerly — use nested IF for expensive calls",
|
|
652
|
+
description=('AL evaluates BOTH sides of OR/AND regardless of the left side result. If the left condition calls an expensive function (FindSet, CalcFields), the right side still runs even when left already determines the result. Nested IF statements provide proper short-circuit behavior.\n\nSQL IMPACT: No SQL change — both function calls execute unconditionally.\nWith nested IF: expensive right-side call skipped when left is true/false.\n\nBAD: if ExpensiveA() or ExpensiveB() then\nGOOD: if ExpensiveA() then ProcessResult()\n else if ExpensiveB() then ProcessResult();\n\nHINT: Add trace messages inside each expensive function. Count how many times each is called with OR vs nested IF. Right-side function should not run when left is true.'),
|
|
653
|
+
exercises=[17, 18, 39],
|
|
654
|
+
)
|
|
655
|
+
class PatternEagerEvaluation:
|
|
656
|
+
# Heuristic: function calls as operands of or/and
|
|
657
|
+
_RE = re.compile(r'\b(\w+\([^)]*\))\s+(or|and)\s+(\w+\([^)]*\))', re.MULTILINE | re.IGNORECASE)
|
|
658
|
+
|
|
659
|
+
@staticmethod
|
|
660
|
+
def detect(text: str) -> list[dict]:
|
|
661
|
+
findings = []
|
|
662
|
+
for m in PatternEagerEvaluation._RE.finditer(text):
|
|
663
|
+
if text[max(0, m.start()-2):m.start()].strip().startswith('//'):
|
|
664
|
+
continue
|
|
665
|
+
line = _find_line(text, m.start())
|
|
666
|
+
line_text = text.splitlines()[line-1].strip()
|
|
667
|
+
if line_text.startswith('//'):
|
|
668
|
+
continue
|
|
669
|
+
findings.append({
|
|
670
|
+
"line": line,
|
|
671
|
+
"snippet": m.group(0)[:120],
|
|
672
|
+
"message": f"Eager evaluation: '{m.group(2).upper()}' always evaluates both sides — use nested IF or 'case true of'"
|
|
673
|
+
})
|
|
674
|
+
return findings
|
|
675
|
+
|
|
676
|
+
@staticmethod
|
|
677
|
+
def fix(text: str) -> str:
|
|
678
|
+
return text # Requires understanding of intent
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
682
|
+
# PATTERN 17 — SetRange/filter mutation inside FindSet loop
|
|
683
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
684
|
+
@_register(
|
|
685
|
+
id="FILTER_MUTATION_IN_LOOP",
|
|
686
|
+
group="Cursor Safety",
|
|
687
|
+
severity="HIGH",
|
|
688
|
+
title="SetRange/SetFilter inside FindSet loop corrupts cursor",
|
|
689
|
+
description=("Setting a new filter inside a FindSet loop modifies the record variable's filter state. Next() continues on the OLD cursor, not the new filter. Result: records may be missed, wrong data returned, or infinite loop. The cursor position becomes undefined after a SetRange mid-loop.\n\nSQL IMPACT: Cursor established by FindSet, then filter changed → Next() undefined behavior. Hard to reproduce bugs.\n\nBAD: if Rec.FindSet() then repeat Rec.SetRange(Field, Rec.Field); until Rec.Next()=0;\nGOOD: Collect keys into a List first, then process with Get().\n\nHINT: Set a breakpoint inside the loop after SetRange. Watch which records Next() returns after the filter change."),
|
|
690
|
+
exercises=[19],
|
|
691
|
+
)
|
|
692
|
+
class PatternFilterMutationInLoop:
|
|
693
|
+
@staticmethod
|
|
694
|
+
def detect(text: str) -> list[dict]:
|
|
695
|
+
findings = []
|
|
696
|
+
lines = text.splitlines()
|
|
697
|
+
in_loop = False
|
|
698
|
+
loop_var = None
|
|
699
|
+
for i, line in enumerate(lines):
|
|
700
|
+
m = re.search(r'(\w+)\.FindSet', line)
|
|
701
|
+
if m and not line.strip().startswith('//'):
|
|
702
|
+
in_loop = True
|
|
703
|
+
loop_var = m.group(1)
|
|
704
|
+
if in_loop and loop_var:
|
|
705
|
+
if re.search(rf'\b{re.escape(loop_var)}\.(SetRange|SetFilter)\(', line) and not line.strip().startswith('//'):
|
|
706
|
+
findings.append({
|
|
707
|
+
"line": i + 1,
|
|
708
|
+
"snippet": line.strip()[:120],
|
|
709
|
+
"message": f"'{loop_var}.SetRange/SetFilter' inside FindSet loop — corrupts cursor"
|
|
710
|
+
})
|
|
711
|
+
if re.match(r'until\s+\w+\.Next\(\)', line.strip(), re.IGNORECASE):
|
|
712
|
+
in_loop = False
|
|
713
|
+
loop_var = None
|
|
714
|
+
return findings
|
|
715
|
+
|
|
716
|
+
@staticmethod
|
|
717
|
+
def fix(text: str) -> str:
|
|
718
|
+
return text
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
722
|
+
# PATTERN 18 — FindSet before DeleteAll (unnecessary guard)
|
|
723
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
724
|
+
@_register(
|
|
725
|
+
id="FINDSET_BEFORE_DELETEALL",
|
|
726
|
+
group="Bulk Operations",
|
|
727
|
+
severity="MEDIUM",
|
|
728
|
+
title="FindSet() guard before DeleteAll() is redundant",
|
|
729
|
+
description=('Same anti-pattern as FindSet before ModifyAll — calling FindSet() before DeleteAll() fires an extra SELECT just to verify rows exist. DeleteAll() already handles empty result sets — it does nothing.\n\nSQL IMPACT: Extra query: SELECT TOP 50 ... (only to check existence).\nThen: DELETE FROM ... WHERE ... (DeleteAll — does this anyway).\nFindSet before DeleteAll = 2 SQL queries. Just DeleteAll = 1 SQL DELETE statement.\n\nBAD: if Record.FindSet() then Record.DeleteAll();\nGOOD: Record.DeleteAll();\n\nHINT: Same as FindSet before ModifyAll but for DeleteAll. With FindSet: 2 queries. Without: 1 DELETE statement.'),
|
|
730
|
+
exercises=[20],
|
|
731
|
+
)
|
|
732
|
+
class PatternFindSetBeforeDeleteAll:
|
|
733
|
+
_RE = re.compile(r'if\s+(\w+)\.FindSet\(\)\s+then\s*\n?\s*\1\.DeleteAll\(', re.MULTILINE)
|
|
734
|
+
|
|
735
|
+
@staticmethod
|
|
736
|
+
def detect(text: str) -> list[dict]:
|
|
737
|
+
return _findings(text, PatternFindSetBeforeDeleteAll._RE,
|
|
738
|
+
lambda m: f"FindSet() guard before DeleteAll() on '{m.group(1)}' is redundant")
|
|
739
|
+
|
|
740
|
+
@staticmethod
|
|
741
|
+
def fix(text: str) -> str:
|
|
742
|
+
return re.sub(
|
|
743
|
+
r'if\s+(\w+)\.FindSet\(\)\s+then\s*\n(\s+)\1\.DeleteAll\(',
|
|
744
|
+
r'\2\1.DeleteAll(',
|
|
745
|
+
text
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
750
|
+
# PATTERN 19 — DeleteAll without IsEmpty guard on lock-sensitive records
|
|
751
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
752
|
+
@_register(
|
|
753
|
+
id="DELETEALL_NO_ISEMPTY_GUARD",
|
|
754
|
+
group="Locking",
|
|
755
|
+
severity="LOW",
|
|
756
|
+
title="DeleteAll() without IsEmpty guard acquires write lock even when empty",
|
|
757
|
+
description=('DeleteAll() on an empty (or likely-empty) table still fires a DELETE FROM ... SQL statement — acquiring a write lock even if there are zero rows to delete. Under high concurrency, unnecessary lock acquisition causes contention.\n\nSQL IMPACT: DELETE FROM ... WHERE ... → acquires write lock even if 0 rows affected.\nIsEmpty guard: IF EXISTS check first → skip DELETE if empty.\n\nCONSIDER: if not Record.IsEmpty() then Record.DeleteAll(false);\n\nHINT: Call DeleteAll on an empty table in SQL Profiler. Without guard: DELETE fires even with 0 rows. With IsEmpty guard: DELETE skipped entirely.'),
|
|
758
|
+
exercises=[21],
|
|
759
|
+
)
|
|
760
|
+
class PatternDeleteAllNoGuard:
|
|
761
|
+
@staticmethod
|
|
762
|
+
def detect(text: str) -> list[dict]:
|
|
763
|
+
findings = []
|
|
764
|
+
lines = text.splitlines()
|
|
765
|
+
for i, line in enumerate(lines):
|
|
766
|
+
m = re.search(r'(\w+)\.DeleteAll\(', line)
|
|
767
|
+
if not m or line.strip().startswith('//'):
|
|
768
|
+
continue
|
|
769
|
+
var = m.group(1)
|
|
770
|
+
# Check 2 lines above for IsEmpty guard on same var
|
|
771
|
+
context = '\n'.join(lines[max(0, i-2):i])
|
|
772
|
+
if f'{var}.IsEmpty' not in context:
|
|
773
|
+
findings.append({
|
|
774
|
+
"line": i + 1,
|
|
775
|
+
"snippet": line.strip()[:120],
|
|
776
|
+
"message": f"Consider 'if not {var}.IsEmpty() then' guard before DeleteAll() to avoid locking on empty sets"
|
|
777
|
+
})
|
|
778
|
+
return findings
|
|
779
|
+
|
|
780
|
+
@staticmethod
|
|
781
|
+
def fix(text: str) -> str:
|
|
782
|
+
return text
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
786
|
+
# PATTERN 20 — Count() = 1 "exactly one" check (use FindFirst + Next)
|
|
787
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
788
|
+
@_register(
|
|
789
|
+
id="COUNT_EQUALS_ONE",
|
|
790
|
+
group="Existence Checks",
|
|
791
|
+
severity="MEDIUM",
|
|
792
|
+
title="Count() = 1 to check uniqueness scans entire set",
|
|
793
|
+
description=("Count() = 1 requires the database to count ALL matching rows. To check 'exactly one record exists', you only need to fetch two rows: FindFirst() gets the first, Next() = 0 confirms there is no second. Maximum 2 row reads.\n\nSQL IMPACT: Count() = 1: SELECT COUNT(*) FROM ... → full table scan.\nFindFirst + Next: SELECT TOP 1 then SELECT TOP 1 → max 2 rows read.\n\nBAD: exit(Record.Count() = 1);\nGOOD: if Record.FindFirst() then exit(Record.Next() = 0)\n else exit(false);\n\nHINT: Compare SQL for Count()=1 vs FindFirst+Next approach. Count() reads entire matching set. FindFirst+Next reads at most 2 rows."),
|
|
794
|
+
exercises=[22],
|
|
795
|
+
)
|
|
796
|
+
class PatternCountEqualsOne:
|
|
797
|
+
_RE = re.compile(r'(\w+)\.Count\(\)\s*=\s*1', re.MULTILINE)
|
|
798
|
+
|
|
799
|
+
@staticmethod
|
|
800
|
+
def detect(text: str) -> list[dict]:
|
|
801
|
+
return _findings(text, PatternCountEqualsOne._RE,
|
|
802
|
+
lambda m: f"Count() = 1 scans entire set — use FindFirst() + Next() = 0")
|
|
803
|
+
|
|
804
|
+
@staticmethod
|
|
805
|
+
def fix(text: str) -> str:
|
|
806
|
+
def replace(m):
|
|
807
|
+
var = m.group(1)
|
|
808
|
+
# Look at context to see if it's in an exit()
|
|
809
|
+
return f'{var}.FindFirst() and ({var}.Next() = 0)'
|
|
810
|
+
return PatternCountEqualsOne._RE.sub(replace, text)
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
814
|
+
# PATTERN 21 — Insert-on-conflict pattern (try Insert first, Modify on fail)
|
|
815
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
816
|
+
@_register(
|
|
817
|
+
id="INSERT_ON_CONFLICT",
|
|
818
|
+
group="Write Patterns",
|
|
819
|
+
severity="MEDIUM",
|
|
820
|
+
title="Insert-on-conflict anti-pattern (try Insert, Modify on fail)",
|
|
821
|
+
description=('The Insert always fires. On duplicate key, SQL raises a constraint error that is caught and triggers a Modify. Error handling in SQL is extremely expensive — each conflict = a rolled-back statement + error event.\n\nSQL IMPACT: On conflict: INSERT (fails) → SQL error → error handling → UPDATE.\nWith Get: SELECT (1 row) → branch → INSERT or UPDATE (no errors).\n\nBAD: if not Record.Insert(false) then Record.Modify(false);\nGOOD: if Record.Get(KeyValue) then Record.Modify(false)\n else begin Record.Init(); Record.Insert(false); end;\n\nHINT: On conflict: INSERT path fires a SQL error event — visible in Profiler. Get+branch: no error events.'),
|
|
822
|
+
exercises=[23],
|
|
823
|
+
)
|
|
824
|
+
class PatternInsertOnConflict:
|
|
825
|
+
_RE = re.compile(r'if not \w+\.Insert\((?:false|true)?\) then\s+\w+\.Modify\(', re.MULTILINE)
|
|
826
|
+
|
|
827
|
+
@staticmethod
|
|
828
|
+
def detect(text: str) -> list[dict]:
|
|
829
|
+
return _findings(text, PatternInsertOnConflict._RE,
|
|
830
|
+
lambda m: "Insert-on-fail pattern — use Get() then Modify/Insert for clarity and efficiency")
|
|
831
|
+
|
|
832
|
+
@staticmethod
|
|
833
|
+
def fix(text: str) -> str:
|
|
834
|
+
return text
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
838
|
+
# PATTERN 22 — SetCurrentKey missing for sorted iteration
|
|
839
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
840
|
+
@_register(
|
|
841
|
+
id="MISSING_SETCURRENTKEY",
|
|
842
|
+
group="Sort / Keys",
|
|
843
|
+
severity="MEDIUM",
|
|
844
|
+
title="Find('-') or FindSet without SetCurrentKey when order matters",
|
|
845
|
+
description=('Find(\'-\') always reads in PRIMARY KEY ascending order, regardless of any SetCurrentKey call made before it. To read data in a specific sort order, use FindSet() after SetCurrentKey and (for descending) SetAscending(false).\n\nSQL IMPACT: Find(\'-\'): always ORDER BY primary key ASC — SetCurrentKey ignored.\nFindSet with SetAscending: ORDER BY <key> DESC — correct ordering.\n\nBAD: Record.SetCurrentKey(\\"Posting Date\\"); if Record.Find(\'-\') then ...\nGOOD: Record.SetCurrentKey(\\"Posting Date\\");\n Record.SetAscending(\\"Posting Date\\", false);\n if Record.FindSet() then ...\n\nHINT: Check ORDER BY in SQL after Find(\'-\') with SetCurrentKey. SetCurrentKey is ignored — ORDER BY uses primary key.'),
|
|
846
|
+
exercises=[24],
|
|
847
|
+
)
|
|
848
|
+
class PatternMissingSetCurrentKey:
|
|
849
|
+
@staticmethod
|
|
850
|
+
def detect(text: str) -> list[dict]:
|
|
851
|
+
return [] # Covered by FIND_DASH_BUFFER_ONE; too context-specific to detect generically
|
|
852
|
+
|
|
853
|
+
@staticmethod
|
|
854
|
+
def fix(text: str) -> str:
|
|
855
|
+
return text
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
859
|
+
# PATTERN 23 — Temp table as collection (use List/Dictionary)
|
|
860
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
861
|
+
@_register(
|
|
862
|
+
id="TEMP_TABLE_AS_COLLECTION",
|
|
863
|
+
group="Memory / Copies",
|
|
864
|
+
severity="MEDIUM",
|
|
865
|
+
title="Temporary table used purely as a collection (use List or Dictionary)",
|
|
866
|
+
description=('Using a temporary table purely to collect distinct scalar values (dates, IDs, codes) carries full record-layer overhead: schema validation, key index maintenance, Init/Insert/FindSet calls. List of [T] is a native AL type with zero record overhead.\n\nSQL IMPACT: Temp table Insert/FindSet: record struct overhead per value.\nList of [Date]: direct array access, no schema, no key lookup.\n\nBAD: TempDates: Record \\"Workshop Data\\" temporary; TempDates.\\"Posting Date\\" := ...; TempDates.Insert();\nGOOD: Dates: List of [Date]; if not Dates.Contains(PostingDate) then Dates.Add(PostingDate);\n\nHINT: Compare temp table Insert/FindSet vs List.Add/iteration. Temp table: schema overhead even in memory. List: pure NST memory array.'),
|
|
867
|
+
exercises=[25],
|
|
868
|
+
)
|
|
869
|
+
class PatternTempTableAsCollection:
|
|
870
|
+
@staticmethod
|
|
871
|
+
def detect(text: str) -> list[dict]:
|
|
872
|
+
findings = []
|
|
873
|
+
lines = text.splitlines()
|
|
874
|
+
for i, line in enumerate(lines):
|
|
875
|
+
m = re.search(r'(Temp\w+)\s*:\s*Record\s+\S+\s+temporary', line)
|
|
876
|
+
if not m:
|
|
877
|
+
continue
|
|
878
|
+
var = m.group(1)
|
|
879
|
+
rest = '\n'.join(lines[i:min(len(lines), i+80)])
|
|
880
|
+
# Pure collection: Insert without reading back meaningful fields
|
|
881
|
+
has_insert = bool(re.search(rf'\b{re.escape(var)}\.Insert\(', rest))
|
|
882
|
+
has_get = bool(re.search(rf'\b{re.escape(var)}\.Get\(', rest))
|
|
883
|
+
has_setrange = bool(re.search(rf'\b{re.escape(var)}\.SetRange\(', rest))
|
|
884
|
+
if has_insert and has_setrange and not has_get:
|
|
885
|
+
findings.append({
|
|
886
|
+
"line": i + 1,
|
|
887
|
+
"snippet": line.strip()[:120],
|
|
888
|
+
"message": f"'{var}' used as collection (Insert+SetRange, no Get) — use List of [...] instead"
|
|
889
|
+
})
|
|
890
|
+
return findings
|
|
891
|
+
|
|
892
|
+
@staticmethod
|
|
893
|
+
def fix(text: str) -> str:
|
|
894
|
+
return text
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
898
|
+
# PATTERN 24 — FindLast / FindFirst inside a loop
|
|
899
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
900
|
+
@_register(
|
|
901
|
+
id="FINDLAST_IN_LOOP",
|
|
902
|
+
group="Data Transfer",
|
|
903
|
+
severity="HIGH",
|
|
904
|
+
title="FindLast()/FindFirst() inside a loop (call once before the loop)",
|
|
905
|
+
description=('Calling FindLast() inside a loop to determine the next Entry No. fires SELECT MAX(\\"Entry No_\\") ... or ORDER BY DESC FETCH 1 on EVERY iteration. For 5 000 lines that is 5 000 SQL round-trips for a counter. One FindLast() before the loop + a local counter is O(1).\n\nSQL IMPACT: Per iteration: SELECT TOP 1 ... ORDER BY \\"Entry No_\\" DESC = N SQL queries.\n\nBAD: foreach Line in ImportSource do begin Record.FindLast(); NextNo := Record.\\"Entry No.\\" + 1; ...\nGOOD: Record.FindLast(); NextNo := Record.\\"Entry No.\\" + 1;\n foreach Line in ImportSource do begin NextNo += 1; ... end;\n\nHINT: Trace FindLast() calls in the loop with SQL Profiler. Each iteration fires ORDER BY DESC FETCH 1. Single FindLast before loop: 1 SQL query total.'),
|
|
906
|
+
exercises=[26],
|
|
907
|
+
)
|
|
908
|
+
class PatternFindLastInLoop:
|
|
909
|
+
@staticmethod
|
|
910
|
+
def detect(text: str) -> list[dict]:
|
|
911
|
+
findings = []
|
|
912
|
+
lines = text.splitlines()
|
|
913
|
+
in_loop = False
|
|
914
|
+
for i, line in enumerate(lines):
|
|
915
|
+
stripped = line.strip()
|
|
916
|
+
if re.search(r'\bforeach\b|\brepeat\b', stripped, re.IGNORECASE):
|
|
917
|
+
in_loop = True
|
|
918
|
+
if re.match(r'until\s+\w+\.Next\(\)', stripped, re.IGNORECASE) or (stripped == 'end;' and in_loop):
|
|
919
|
+
in_loop = False
|
|
920
|
+
if in_loop and re.search(r'\w+\.(FindLast|FindFirst)\(\)', line) and not stripped.startswith('//'):
|
|
921
|
+
findings.append({
|
|
922
|
+
"line": i + 1,
|
|
923
|
+
"snippet": line.strip()[:120],
|
|
924
|
+
"message": "FindLast/FindFirst inside loop — call once before the loop and cache the result"
|
|
925
|
+
})
|
|
926
|
+
return findings
|
|
927
|
+
|
|
928
|
+
@staticmethod
|
|
929
|
+
def fix(text: str) -> str:
|
|
930
|
+
return text
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
934
|
+
# PATTERN 25 — LockTable + FindLast for sequence generation (use NumberSequence)
|
|
935
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
936
|
+
@_register(
|
|
937
|
+
id="LOCKTABLE_FOR_SEQUENCE",
|
|
938
|
+
group="Locking",
|
|
939
|
+
severity="HIGH",
|
|
940
|
+
title="LockTable + FindLast for sequence numbers — use NumberSequence",
|
|
941
|
+
description=("LockTable() acquires a full table-level exclusive write lock held for the entire transaction. Any concurrent session needing this table blocks. Under high-volume processing (hundreds of operations per minute) this creates lock wait queues and eventual timeout errors.\n\nSQL IMPACT: LockTable: X-lock on entire table for transaction duration.\nNumberSequence.Next(): atomic counter at DB level — no table lock.\n\nBAD: Record.LockTable(true); if Record.FindLast() then NextNo := Record.PK + 1;\nGOOD: if not NumberSequence.Exists('MY_SEQ') then NumberSequence.Insert('MY_SEQ',1,1,false);\n NextNo := NumberSequence.Next('MY_SEQ');\n\nHINT: LockTable: watch lock events in SQL Profiler. NumberSequence: no table lock — atomic sequence increment. Compare concurrency: run two sessions simultaneously."),
|
|
942
|
+
exercises=[28],
|
|
943
|
+
)
|
|
944
|
+
class PatternLockTableForSequence:
|
|
945
|
+
@staticmethod
|
|
946
|
+
def detect(text: str) -> list[dict]:
|
|
947
|
+
findings = []
|
|
948
|
+
lines = text.splitlines()
|
|
949
|
+
for i, line in enumerate(lines):
|
|
950
|
+
if re.search(r'\w+\.LockTable\(', line) and not line.strip().startswith('//'):
|
|
951
|
+
# Look ahead for FindLast to get next key
|
|
952
|
+
ahead = '\n'.join(lines[i:min(len(lines), i+8)])
|
|
953
|
+
if re.search(r'FindLast\(\)', ahead) and re.search(r'Entry No|NextSeq|NextNo|NextKey', ahead, re.IGNORECASE):
|
|
954
|
+
findings.append({
|
|
955
|
+
"line": i + 1,
|
|
956
|
+
"snippet": line.strip()[:120],
|
|
957
|
+
"message": "LockTable + FindLast for sequence generation — use NumberSequence.Next() instead"
|
|
958
|
+
})
|
|
959
|
+
return findings
|
|
960
|
+
|
|
961
|
+
@staticmethod
|
|
962
|
+
def fix(text: str) -> str:
|
|
963
|
+
return text
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
967
|
+
# PATTERN 26 — SetRange on PK + FindFirst instead of Get
|
|
968
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
969
|
+
@_register(
|
|
970
|
+
id="SETRANGE_FINDSET_FOR_GET",
|
|
971
|
+
group="Data Transfer",
|
|
972
|
+
severity="MEDIUM",
|
|
973
|
+
title="SetRange on PK + FindFirst instead of direct Get()",
|
|
974
|
+
description=('SetRange on a primary key field followed by FindFirst() goes through the filter+scan path instead of the optimized single-row lookup. Get() uses the primary key index directly — no cursor setup, hits the clustered index directly.\n\nSQL IMPACT: SetRange+FindFirst: SELECT TOP 50 ... WHERE \\"No_\\"=X (cursor setup overhead).\nGet(): Direct clustered index point lookup — no cursor. Measurably faster in high-frequency loops.\n\nBAD: Customer.SetRange(\\"No.\\", CustomerNo); if Customer.FindFirst() then ...\nGOOD: if Customer.Get(CustomerNo) then ...\n\nHINT: Compare SELECT TOP 50 (SetRange+FindFirst) vs direct lookup. Get() hits clustered index directly — 1 read, no cursor.'),
|
|
975
|
+
exercises=[29],
|
|
976
|
+
)
|
|
977
|
+
class PatternSetRangeFindFirstForGet:
|
|
978
|
+
@staticmethod
|
|
979
|
+
def detect(text: str) -> list[dict]:
|
|
980
|
+
findings = []
|
|
981
|
+
lines = text.splitlines()
|
|
982
|
+
for i, line in enumerate(lines):
|
|
983
|
+
m = re.search(r'(\w+)\.SetRange\("No\.",\s*\w+\)', line)
|
|
984
|
+
if not m or line.strip().startswith('//'):
|
|
985
|
+
continue
|
|
986
|
+
var = m.group(1)
|
|
987
|
+
ahead = '\n'.join(lines[i:min(len(lines), i+3)])
|
|
988
|
+
if re.search(rf'\b{re.escape(var)}\.FindFirst\(\)', ahead):
|
|
989
|
+
findings.append({
|
|
990
|
+
"line": i + 1,
|
|
991
|
+
"snippet": line.strip()[:120],
|
|
992
|
+
"message": f"'{var}.SetRange(\"No.\", ...) + FindFirst()' — use '{var}.Get(...)' directly"
|
|
993
|
+
})
|
|
994
|
+
return findings
|
|
995
|
+
|
|
996
|
+
@staticmethod
|
|
997
|
+
def fix(text: str) -> str:
|
|
998
|
+
return text
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
1002
|
+
# PATTERN 27 — Silent Insert failure (if Insert then)
|
|
1003
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
1004
|
+
@_register(
|
|
1005
|
+
id="SILENT_INSERT_FAILURE",
|
|
1006
|
+
group="Write Patterns",
|
|
1007
|
+
severity="MEDIUM",
|
|
1008
|
+
title="'if Record.Insert(false) then' silently swallows duplicate-key errors",
|
|
1009
|
+
description=("In AL, Insert(false) does NOT suppress the SQL unique constraint error — it still propagates. Wrapping it in 'if ... then' gives a false sense that the failure is handled. In practice the record silently isn't created and the caller never knows, leading to data integrity gaps.\n\nSQL IMPACT: SQL unique constraint error still raised — 'if then' doesn't catch SQL errors.\nSilent failure → missing records → integrity gap.\n\nBAD: if Record.Insert(false) then MovementCount += 1;\nGOOD: Record.Insert(false); MovementCount += 1;\n\nHINT: Insert a duplicate key row with 'if Insert then'. Check SQL Profiler — does the error appear? The record is not created but no AL error raised."),
|
|
1010
|
+
exercises=[30],
|
|
1011
|
+
)
|
|
1012
|
+
class PatternSilentInsertFailure:
|
|
1013
|
+
_RE = re.compile(r'if\s+\w+\.Insert\((?:false|true)\)\s+then', re.MULTILINE)
|
|
1014
|
+
|
|
1015
|
+
@staticmethod
|
|
1016
|
+
def detect(text: str) -> list[dict]:
|
|
1017
|
+
return _findings(text, PatternSilentInsertFailure._RE,
|
|
1018
|
+
lambda m: "Silent Insert — errors won't surface. Use plain Insert() or handle explicitly")
|
|
1019
|
+
|
|
1020
|
+
@staticmethod
|
|
1021
|
+
def fix(text: str) -> str:
|
|
1022
|
+
return text
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
1026
|
+
# PATTERN 28 — LockTable too early (before read-only phase)
|
|
1027
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
1028
|
+
@_register(
|
|
1029
|
+
id="LOCKTABLE_TOO_EARLY",
|
|
1030
|
+
group="Locking",
|
|
1031
|
+
severity="HIGH",
|
|
1032
|
+
title="LockTable() called before read phase — holds write lock during reads",
|
|
1033
|
+
description=('LockTable() at the top of a procedure holds an exclusive lock through the entire validation / read phase — which may take seconds on large data. Every other session needing this table is blocked for the full duration. The lock is only needed at the moment of the first write.\n\nSQL IMPACT: X-lock held from top of proc through entire read phase (seconds).\nvs. X-lock acquired just before first Modify (milliseconds).\n\nBAD: WorkshopData.LockTable(); ... if WorkshopData.FindSet() then [read loop] ...\nGOOD: [read loop without lock] ... WorkshopData.LockTable(); [write phase only]\n\nHINT: Use Activity Monitor or Profiler lock events. LockTable at start: X-lock held during entire read phase. LockTable at write phase: X-lock held for milliseconds only.'),
|
|
1034
|
+
exercises=[31, 42],
|
|
1035
|
+
)
|
|
1036
|
+
class PatternLockTableTooEarly:
|
|
1037
|
+
@staticmethod
|
|
1038
|
+
def detect(text: str) -> list[dict]:
|
|
1039
|
+
findings = []
|
|
1040
|
+
lines = text.splitlines()
|
|
1041
|
+
for i, line in enumerate(lines):
|
|
1042
|
+
if re.search(r'\w+\.LockTable\(', line) and not line.strip().startswith('//'):
|
|
1043
|
+
# Check if FindSet follows (read phase)
|
|
1044
|
+
ahead = '\n'.join(lines[i:min(len(lines), i+15)])
|
|
1045
|
+
if re.search(r'\.FindSet\(\)', ahead):
|
|
1046
|
+
findings.append({
|
|
1047
|
+
"line": i + 1,
|
|
1048
|
+
"snippet": line.strip()[:120],
|
|
1049
|
+
"message": "LockTable() before FindSet — move lock closer to the write operation"
|
|
1050
|
+
})
|
|
1051
|
+
return findings
|
|
1052
|
+
|
|
1053
|
+
@staticmethod
|
|
1054
|
+
def fix(text: str) -> str:
|
|
1055
|
+
return text
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
1059
|
+
# PATTERN 29 — Missing ReadIsolation (dirty reads safe = ReadUncommitted)
|
|
1060
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
1061
|
+
@_register(
|
|
1062
|
+
id="MISSING_READ_ISOLATION",
|
|
1063
|
+
group="Locking",
|
|
1064
|
+
severity="MEDIUM",
|
|
1065
|
+
title="FindSet on reporting/read-only query without ReadIsolation = ReadUncommitted",
|
|
1066
|
+
description=('Read-only analytics FindSet acquires Shared (S) locks on every fetched page. Concurrent warehouse write sessions hold Exclusive (X) locks on same rows. Result: analytics blocks postings, postings block analytics. On a 50 000-row scan, S-locks are held for several seconds.\n\nSQL IMPACT: Default isolation: Shared (S) locks per page fetched.\nReadUncommitted: NO lock acquisition — readers never block writers.\n\nGOOD: WorkshopData.ReadIsolation := IsolationLevel::ReadUncommitted;\n if WorkshopData.FindSet() then ...\n\nHINT: Without ReadUncommitted: watch S-lock events per page in SQL Profiler. With ReadUncommitted: zero S-lock events — readers invisible. Never use for financial postings or validation.'),
|
|
1067
|
+
exercises=[34],
|
|
1068
|
+
)
|
|
1069
|
+
class PatternMissingReadIsolation:
|
|
1070
|
+
@staticmethod
|
|
1071
|
+
def detect(text: str) -> list[dict]:
|
|
1072
|
+
findings = []
|
|
1073
|
+
lines = text.splitlines()
|
|
1074
|
+
for i, line in enumerate(lines):
|
|
1075
|
+
if re.search(r'\w+\.FindSet\(\)', line) and not line.strip().startswith('//'):
|
|
1076
|
+
# Procedure name suggests reporting
|
|
1077
|
+
context_above = '\n'.join(lines[max(0, i-30):i])
|
|
1078
|
+
if re.search(r'Report|Dashboard|Export|Build.*Report|Snapshot', context_above, re.IGNORECASE):
|
|
1079
|
+
if 'ReadIsolation' not in context_above:
|
|
1080
|
+
findings.append({
|
|
1081
|
+
"line": i + 1,
|
|
1082
|
+
"snippet": line.strip()[:120],
|
|
1083
|
+
"message": "Reporting FindSet without ReadIsolation := ReadUncommitted — may block concurrent writes"
|
|
1084
|
+
})
|
|
1085
|
+
return findings
|
|
1086
|
+
|
|
1087
|
+
@staticmethod
|
|
1088
|
+
def fix(text: str) -> str:
|
|
1089
|
+
return text
|
|
1090
|
+
|
|
1091
|
+
|
|
1092
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
1093
|
+
# PATTERN 30 — RecordRef/FieldRef when typed Record is sufficient
|
|
1094
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
1095
|
+
@_register(
|
|
1096
|
+
id="RECORDREF_WHEN_TYPED_SUFFICIENT",
|
|
1097
|
+
group="Memory / Copies",
|
|
1098
|
+
severity="LOW",
|
|
1099
|
+
title="RecordRef/FieldRef used when a typed Record variable would suffice",
|
|
1100
|
+
description=('RecordRef provides late-bound, reflection-based table access. Each Field() call does a dictionary lookup + type boxing. SQL generation is less optimized than for typed Record variables. Benchmarks show 3–5x slower reads than typed Record on same data.\n\nSQL IMPACT: RecordRef: Field() lookup + boxing overhead per field per row.\nTyped Record: direct field access, compiler-optimized SQL.\n\nBAD: RecRef: RecordRef; RecRef.Open(Database::\\"Workshop Data\\"); RecRef.Field(2).Value := ...;\nGOOD: WorkshopData: Record \\"Workshop Data\\"; WorkshopData.Description := ...;\n\nHINT: Compare RecordRef.Field(2).Value vs typed WorkshopData.Description. Run a 10 000-row loop with each — time the difference. RecordRef is 3–5x slower.'),
|
|
1101
|
+
exercises=[35],
|
|
1102
|
+
)
|
|
1103
|
+
class PatternRecordRefUnnecessary:
|
|
1104
|
+
_RE = re.compile(r'\bRecRef\s*:\s*RecordRef\b|\bFldRef\s*:\s*FieldRef\b', re.MULTILINE)
|
|
1105
|
+
|
|
1106
|
+
@staticmethod
|
|
1107
|
+
def detect(text: str) -> list[dict]:
|
|
1108
|
+
return _findings(text, PatternRecordRefUnnecessary._RE,
|
|
1109
|
+
lambda m: "RecordRef/FieldRef — consider typed Record variable if table is known at compile time")
|
|
1110
|
+
|
|
1111
|
+
@staticmethod
|
|
1112
|
+
def fix(text: str) -> str:
|
|
1113
|
+
return text
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
1117
|
+
# PATTERN 31 — ModifyAll with RunTrigger=true
|
|
1118
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
1119
|
+
@_register(
|
|
1120
|
+
id="MODIFYALL_RUNTRIGGER_TRUE",
|
|
1121
|
+
group="Bulk Operations",
|
|
1122
|
+
severity="HIGH",
|
|
1123
|
+
title="ModifyAll with RunTrigger=true fires OnModify trigger per row",
|
|
1124
|
+
description=('ModifyAll() with RunTrigger=true checks for OnAfterModify subscribers. If ANY subscriber is bound, ModifyAll falls back to N individual Modify() calls — one per record — each triggering the subscriber. 10 000 records × 1 Modify() call = 10 000 SQL UPDATE round-trips.\n\nSQL IMPACT: With RunTrigger=true + active subscriber: N × UPDATE per row.\nWith RunTrigger=false: 1 × UPDATE ... WHERE ... (single SQL statement).\n\nBAD: Record.ModifyAll(Field, Value, true);\nGOOD: Record.ModifyAll(Field, Value, false);\n\nHINT: Bind the subscriber and run ModifyAll with RunTrigger=true. SQL Profiler: count UPDATE statements — should be N rows. Change to RunTrigger=false: now count is 1 UPDATE statement.'),
|
|
1125
|
+
exercises=[37],
|
|
1126
|
+
)
|
|
1127
|
+
class PatternModifyAllRunTrigger:
|
|
1128
|
+
_RE = re.compile(r'\w+\.ModifyAll\([^)]+,\s*true\)', re.MULTILINE)
|
|
1129
|
+
|
|
1130
|
+
@staticmethod
|
|
1131
|
+
def detect(text: str) -> list[dict]:
|
|
1132
|
+
return _findings(text, PatternModifyAllRunTrigger._RE,
|
|
1133
|
+
lambda m: "ModifyAll with RunTrigger=true fires OnModify per row — use false unless required")
|
|
1134
|
+
|
|
1135
|
+
@staticmethod
|
|
1136
|
+
def fix(text: str) -> str:
|
|
1137
|
+
return re.sub(
|
|
1138
|
+
r'(\w+\.ModifyAll\([^)]+,\s*)true(\))',
|
|
1139
|
+
r'\1false\2',
|
|
1140
|
+
text
|
|
1141
|
+
)
|
|
1142
|
+
|
|
1143
|
+
|
|
1144
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
1145
|
+
# PATTERN 32 — Missing Database.SelectLatestVersion in multi-pass loop
|
|
1146
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
1147
|
+
@_register(
|
|
1148
|
+
id="MISSING_SELECTLATESTVERSION",
|
|
1149
|
+
group="Stale Reads",
|
|
1150
|
+
severity="MEDIUM",
|
|
1151
|
+
title="Multi-pass read loop without Database.SelectLatestVersion()",
|
|
1152
|
+
description=('BC NST caches SQL query results in-session. Repeated FindSet calls with the same filter return CACHED rows — no SQL executed after pass 1. A multi-pass pipeline reading the same filter silently returns stale data from pass 1 for passes 2–N if data changed concurrently.\n\nSQL IMPACT: Pass 1: SELECT ... (real SQL).\nPass 2–N: [NST cache hit] → no SQL, no awareness of concurrent updates.\n\nGOOD: foreach Pass in Passes do begin\n Database.SelectLatestVersion(); // Flushes non-locked session cache\n if Record.FindSet() then ...\n\nHINT: Run the pipeline twice: with and without SelectLatestVersion. Without flush: 0 SQL queries for passes 2–N (cache hit — stale data).'),
|
|
1153
|
+
exercises=[38],
|
|
1154
|
+
)
|
|
1155
|
+
class PatternMissingSelectLatestVersion:
|
|
1156
|
+
@staticmethod
|
|
1157
|
+
def detect(text: str) -> list[dict]:
|
|
1158
|
+
return [] # Too context-specific; covered by FIX comment detection
|
|
1159
|
+
|
|
1160
|
+
@staticmethod
|
|
1161
|
+
def fix(text: str) -> str:
|
|
1162
|
+
return text
|
|
1163
|
+
|
|
1164
|
+
|
|
1165
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
1166
|
+
# PATTERN 33 — OR chain instead of case true of (3+ conditions)
|
|
1167
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
1168
|
+
@_register(
|
|
1169
|
+
id="OR_CHAIN_USE_CASE_TRUE",
|
|
1170
|
+
group="Short-Circuit Evaluation",
|
|
1171
|
+
severity="MEDIUM",
|
|
1172
|
+
title="3+ OR conditions — use 'case true of' for short-circuit evaluation",
|
|
1173
|
+
description=('AL evaluates ALL operands of or before returning — even if the first operand is already true. Three expensive eligibility checks joined with or each execute a CalcSums or FindSet query — all three, every time.\n\nSQL IMPACT: 3 SQL queries per customer: CalcSums + IsEmpty + Count.\nEven when the first check already returns true — wasted 2 queries.\ncase true of with comma-separated conditions short-circuits at first true.\n\nBAD: if IsOverCreditLimit(No) or HasBlockedItems(No) or HasOverdueInvoices(No) then ...\nGOOD: case true of\n IsOverCreditLimit(No), HasBlockedItems(No), HasOverdueInvoices(No):\n exit(false);\n end;\n\nHINT: Add a counter inside each validation function. With or: all three counters increment for every customer. With case true of: only 1–2 counters increment when first is true.'),
|
|
1174
|
+
exercises=[39],
|
|
1175
|
+
)
|
|
1176
|
+
class PatternOrChainCaseTrue:
|
|
1177
|
+
_RE = re.compile(r'\bif\b[^;]+\bor\b[^;]+\bor\b[^;]+\bthen\b', re.MULTILINE | re.IGNORECASE)
|
|
1178
|
+
|
|
1179
|
+
@staticmethod
|
|
1180
|
+
def detect(text: str) -> list[dict]:
|
|
1181
|
+
findings = []
|
|
1182
|
+
for m in PatternOrChainCaseTrue._RE.finditer(text):
|
|
1183
|
+
line = _find_line(text, m.start())
|
|
1184
|
+
line_text = text.splitlines()[line-1].strip()
|
|
1185
|
+
if line_text.startswith('//'):
|
|
1186
|
+
continue
|
|
1187
|
+
# Count function calls in the OR chain
|
|
1188
|
+
if len(re.findall(r'\w+\(', m.group(0))) >= 3:
|
|
1189
|
+
findings.append({
|
|
1190
|
+
"line": line,
|
|
1191
|
+
"snippet": m.group(0)[:120],
|
|
1192
|
+
"message": "3+ OR conditions with function calls — use 'case true of' for short-circuit"
|
|
1193
|
+
})
|
|
1194
|
+
return findings
|
|
1195
|
+
|
|
1196
|
+
@staticmethod
|
|
1197
|
+
def fix(text: str) -> str:
|
|
1198
|
+
return text
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
1202
|
+
# PATTERN 34 — LockTable + ReadIsolation UpdLock (Ex42)
|
|
1203
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
1204
|
+
@_register(
|
|
1205
|
+
id="LOCKTABLE_USE_UPDLOCK",
|
|
1206
|
+
group="Locking",
|
|
1207
|
+
severity="MEDIUM",
|
|
1208
|
+
title="LockTable() — consider ReadIsolation := UpdLock + FindSet(true) instead",
|
|
1209
|
+
description=('LockTable() acquires a table-level Exclusive (X) lock on the ENTIRE table. Every concurrent session — reads, analytics, order entry — blocks for the full transaction duration. A batch updating 1 000 rows can block all other sessions for seconds.\n\nSQL IMPACT: LockTable: X-lock on entire table — held for full transaction.\nReadIsolation := UpdLock + FindSet(true): acquires update locks only on rows actually read, allowing concurrent readers.\n\nBAD: Record.LockTable();\n if Record.FindSet() then\nGOOD: Record.ReadIsolation := IsolationLevel::UpdLock;\n if Record.FindSet(true) then\n\nHINT: Open two BC sessions simultaneously. Session 1: run LockTable. Session 2: try any read — it blocks until Session 1 commits. With UpdLock: Session 2 reads freely.'),
|
|
1210
|
+
exercises=[42],
|
|
1211
|
+
)
|
|
1212
|
+
class PatternLockTableUseUpdLock:
|
|
1213
|
+
_RE = re.compile(r'(\w+)\.LockTable\(\)', re.MULTILINE)
|
|
1214
|
+
|
|
1215
|
+
@staticmethod
|
|
1216
|
+
def detect(text: str) -> list[dict]:
|
|
1217
|
+
return _findings(text, PatternLockTableUseUpdLock._RE,
|
|
1218
|
+
lambda m: f"Consider replacing '{m.group(1)}.LockTable()' with ReadIsolation := UpdLock + FindSet(true)")
|
|
1219
|
+
|
|
1220
|
+
@staticmethod
|
|
1221
|
+
def fix(text: str) -> str:
|
|
1222
|
+
def replace(m):
|
|
1223
|
+
var = m.group(1)
|
|
1224
|
+
pos = m.end()
|
|
1225
|
+
ahead = text[pos:pos+200]
|
|
1226
|
+
if re.search(rf'\b{re.escape(var)}\.FindSet\(\)', ahead):
|
|
1227
|
+
replacement = f'{var}.ReadIsolation := IsolationLevel::UpdLock'
|
|
1228
|
+
# Also fix the FindSet in one pass
|
|
1229
|
+
return replacement
|
|
1230
|
+
return m.group(0)
|
|
1231
|
+
result = PatternLockTableUseUpdLock._RE.sub(replace, text)
|
|
1232
|
+
result = re.sub(r'(\w+)\.ReadIsolation := IsolationLevel::UpdLock;\s*\n(\s+)if \1\.FindSet\(\)',
|
|
1233
|
+
r'\1.ReadIsolation := IsolationLevel::UpdLock;\n\2if \1.FindSet(true)', result)
|
|
1234
|
+
return result
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
1238
|
+
# PATTERN 35 — true in [...] condition order (cheapest first)
|
|
1239
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
1240
|
+
@_register(
|
|
1241
|
+
id="CONDITION_ORDER_TRUE_IN",
|
|
1242
|
+
group="Short-Circuit Evaluation",
|
|
1243
|
+
severity="LOW",
|
|
1244
|
+
title="'if true in [...]' — put cheapest condition first for best short-circuit",
|
|
1245
|
+
description=("AL's in [...] operator DOES short-circuit — unlike or/\x07nd. But placing the most expensive check FIRST defeats the benefit: the CalcSums fires before the cheap blacklist Get() is even tested. Rule: always order conditions cheapest-first in in [...] lists.\n\nSQL IMPACT: Expensive-first: CalcSums runs for ~100% of customers.\nCheap-first: CalcSums skipped for the ~35% caught by cheaper checks first.\n\nBAD: if true in [HasExceededAnnualBudget(No), IsBlacklisted(No)] then ...\nGOOD: if true in [IsBlacklisted(No), HasExceededAnnualBudget(No)] then ...\n\nHINT: Add a Message() counter inside HasExceededAnnualBudget. Run with expensive-first: counter = total customer count. Reorder to cheap-first: counter drops by ~35%."),
|
|
1246
|
+
exercises=[44],
|
|
1247
|
+
)
|
|
1248
|
+
class PatternConditionOrderTrueIn:
|
|
1249
|
+
_RE = re.compile(r'if true in \[', re.MULTILINE | re.IGNORECASE)
|
|
1250
|
+
|
|
1251
|
+
@staticmethod
|
|
1252
|
+
def detect(text: str) -> list[dict]:
|
|
1253
|
+
return _findings(text, PatternConditionOrderTrueIn._RE,
|
|
1254
|
+
lambda m: "Review condition order: put cheapest check first for best short-circuit performance")
|
|
1255
|
+
|
|
1256
|
+
@staticmethod
|
|
1257
|
+
def fix(text: str) -> str:
|
|
1258
|
+
return text
|
|
1259
|
+
|
|
1260
|
+
|
|
1261
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
1262
|
+
# PATTERN 36 — N+1 nested FindSet inside repeat..until loop (Ex 27 / 43)
|
|
1263
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
1264
|
+
@_register(
|
|
1265
|
+
id="NESTED_FINDSET_N_PLUS_ONE",
|
|
1266
|
+
group="Data Transfer",
|
|
1267
|
+
severity="HIGH",
|
|
1268
|
+
title="Nested FindSet/FindFirst inside repeat..until loop (N+1 queries)",
|
|
1269
|
+
description=(
|
|
1270
|
+
"For each row in the outer FindSet, an inner FindSet on a different variable fires a new SQL query. "
|
|
1271
|
+
"With 1 000 outer items: 1 outer SELECT + 1 000 inner SELECTs = 1 001 SQL round-trips. "
|
|
1272
|
+
"A Query object with GROUP BY does the same work in a single SQL query.\n\n"
|
|
1273
|
+
"SQL IMPACT: 1 000 items × 1 inner query = 1 001 total SQL round-trips.\n"
|
|
1274
|
+
"Query object: 1 SQL query with GROUP BY — result set = N groups only.\n\n"
|
|
1275
|
+
"BAD: if Items.FindSet() then repeat\n"
|
|
1276
|
+
" SalesLines.SetRange(\"No.\", Items.\"No.\");\n"
|
|
1277
|
+
" if SalesLines.FindSet() then repeat ... until SalesLines.Next() = 0;\n"
|
|
1278
|
+
" until Items.Next() = 0;\n"
|
|
1279
|
+
"GOOD: var AnalysisQuery: Query \"Workshop Item Analysis\";\n"
|
|
1280
|
+
" if AnalysisQuery.Open() then\n"
|
|
1281
|
+
" while AnalysisQuery.Read() do ...\n\n"
|
|
1282
|
+
"HINT: Count SQL queries in Profiler for the nested loop. Each outer row fires 1 inner SQL query. "
|
|
1283
|
+
"Query object version: 1 SQL query with GROUP BY regardless of data volume."
|
|
1284
|
+
),
|
|
1285
|
+
exercises=[27, 43],
|
|
1286
|
+
)
|
|
1287
|
+
class PatternNestedFindSetNPlusOne:
|
|
1288
|
+
@staticmethod
|
|
1289
|
+
def detect(text: str) -> list[dict]:
|
|
1290
|
+
findings = []
|
|
1291
|
+
lines = text.splitlines()
|
|
1292
|
+
outer_var: str | None = None
|
|
1293
|
+
in_loop = False
|
|
1294
|
+
|
|
1295
|
+
for i, line in enumerate(lines):
|
|
1296
|
+
stripped = line.strip()
|
|
1297
|
+
if stripped.startswith('//'):
|
|
1298
|
+
continue
|
|
1299
|
+
|
|
1300
|
+
# Track outer FindSet before a repeat
|
|
1301
|
+
m_outer = re.search(r'(\w+)\.(FindSet|FindFirst)\(', line)
|
|
1302
|
+
if m_outer and not in_loop:
|
|
1303
|
+
outer_var = m_outer.group(1)
|
|
1304
|
+
|
|
1305
|
+
if re.match(r'^repeat\b', stripped, re.IGNORECASE) and outer_var:
|
|
1306
|
+
in_loop = True
|
|
1307
|
+
|
|
1308
|
+
if in_loop:
|
|
1309
|
+
m_inner = re.search(r'(\w+)\.(FindSet|FindFirst)\(', line)
|
|
1310
|
+
if m_inner and m_inner.group(1) != outer_var:
|
|
1311
|
+
findings.append({
|
|
1312
|
+
"line": i + 1,
|
|
1313
|
+
"snippet": line.strip()[:120],
|
|
1314
|
+
"message": (
|
|
1315
|
+
f"Nested {m_inner.group(2)}() on '{m_inner.group(1)}' inside outer "
|
|
1316
|
+
f"'{outer_var}' FindSet loop — N+1 SQL queries. "
|
|
1317
|
+
"Consider a Query object with GROUP BY."
|
|
1318
|
+
),
|
|
1319
|
+
})
|
|
1320
|
+
|
|
1321
|
+
if re.match(r'^until\s+\w+\.Next\(\)', stripped, re.IGNORECASE):
|
|
1322
|
+
in_loop = False
|
|
1323
|
+
outer_var = None
|
|
1324
|
+
|
|
1325
|
+
return findings
|
|
1326
|
+
|
|
1327
|
+
@staticmethod
|
|
1328
|
+
def fix(text: str) -> str:
|
|
1329
|
+
return text # Structural change required — no auto-fix
|
|
1330
|
+
|
|
1331
|
+
|
|
1332
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
1333
|
+
# PATTERN 37 — AutoIncrement = true disables SQL batch INSERT (Ex 36)
|
|
1334
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
1335
|
+
@_register(
|
|
1336
|
+
id="AUTOINCREMENT_DISABLES_BULK_INSERT",
|
|
1337
|
+
group="Bulk Operations",
|
|
1338
|
+
severity="HIGH",
|
|
1339
|
+
title="AutoIncrement = true on PK disables SQL batch INSERT",
|
|
1340
|
+
description=(
|
|
1341
|
+
"SQL Server IDENTITY (AutoIncrement = true) requires SCOPE_IDENTITY() to be returned "
|
|
1342
|
+
"after each INSERT — this prevents batch INSERT entirely. "
|
|
1343
|
+
"For 10 000 count sheet lines: 10 000 individual SQL INSERTs, one per row. "
|
|
1344
|
+
"Manual PK assignment via NumberSequence allows SQL Server to batch rows into one trip.\n\n"
|
|
1345
|
+
"SQL IMPACT: AutoIncrement: 10 000 × INSERT (+ SCOPE_IDENTITY per row).\n"
|
|
1346
|
+
"Manual PK with NumberSequence: ~10 batch INSERT statements — orders of magnitude faster.\n\n"
|
|
1347
|
+
"BAD: field(1; Id; Integer) { AutoIncrement = true; }\n"
|
|
1348
|
+
"GOOD: field(1; Id; Integer) { }\n"
|
|
1349
|
+
" // In code:\n"
|
|
1350
|
+
" Entry.Id := NumberSequence.Next('WS_COUNT_SEQ');\n"
|
|
1351
|
+
" Entry.Insert(false); // Batchable — no identity return needed\n\n"
|
|
1352
|
+
"HINT: Insert 10 000 rows into both tables in SQL Profiler. "
|
|
1353
|
+
"AutoIncrement table: 10 000 individual INSERT statements. "
|
|
1354
|
+
"Manual key table: ~10 batch INSERT statements total."
|
|
1355
|
+
),
|
|
1356
|
+
exercises=[36],
|
|
1357
|
+
)
|
|
1358
|
+
class PatternAutoIncrementBulkInsert:
|
|
1359
|
+
_RE = re.compile(r'AutoIncrement\s*=\s*true', re.IGNORECASE)
|
|
1360
|
+
|
|
1361
|
+
@staticmethod
|
|
1362
|
+
def detect(text: str) -> list[dict]:
|
|
1363
|
+
return _findings(
|
|
1364
|
+
text,
|
|
1365
|
+
PatternAutoIncrementBulkInsert._RE,
|
|
1366
|
+
lambda m: (
|
|
1367
|
+
"AutoIncrement = true requires SCOPE_IDENTITY() per INSERT — prevents SQL batch inserts. "
|
|
1368
|
+
"Remove AutoIncrement and use NumberSequence.Next() for PK assignment."
|
|
1369
|
+
),
|
|
1370
|
+
)
|
|
1371
|
+
|
|
1372
|
+
@staticmethod
|
|
1373
|
+
def fix(text: str) -> str:
|
|
1374
|
+
return text # Requires table redesign + code change — no auto-fix
|
|
1375
|
+
|
|
1376
|
+
|
|
1377
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
1378
|
+
# PATTERN 38 — AL-side GROUP BY aggregation (Ex 43)
|
|
1379
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
1380
|
+
@_register(
|
|
1381
|
+
id="AL_SIDE_AGGREGATION",
|
|
1382
|
+
group="Aggregation",
|
|
1383
|
+
severity="HIGH",
|
|
1384
|
+
title="AL-side GROUP BY aggregation — push to SQL with a Query object",
|
|
1385
|
+
description=(
|
|
1386
|
+
"Manual AL aggregation: FindSet fetches ALL rows into NST, accumulates sums per group in AL. "
|
|
1387
|
+
"For 100 000 entries — 100 000 rows transferred to NST memory. "
|
|
1388
|
+
"A Query object with Method = Sum pushes GROUP BY and SUM() to SQL Server. "
|
|
1389
|
+
"SQL returns only N rows (one per group) — 99 990 rows never leave the database.\n\n"
|
|
1390
|
+
"SQL IMPACT: AL loop: SELECT all 100 000 rows, SUM in NST memory.\n"
|
|
1391
|
+
"Query: SELECT Location, SUM(Amount) GROUP BY Location → 10 rows only.\n\n"
|
|
1392
|
+
"BAD: if WorkshopData.FindSet() then repeat\n"
|
|
1393
|
+
" TotalByLocation.Add(WorkshopData.\"Location Code\", WorkshopData.Amount);\n"
|
|
1394
|
+
" until WorkshopData.Next() = 0;\n"
|
|
1395
|
+
"GOOD: var RevenueQuery: Query \"WS Location Revenue\";\n"
|
|
1396
|
+
" if RevenueQuery.Open() then\n"
|
|
1397
|
+
" while RevenueQuery.Read() do\n"
|
|
1398
|
+
" ProcessSummary(RevenueQuery.Location_Code, RevenueQuery.Sum_Line_Amount);\n\n"
|
|
1399
|
+
"HINT: Run the manual loop on 50 000 rows. Check SQL Profiler: count SELECT rows returned. "
|
|
1400
|
+
"Then switch to the Query version. Profiler shows 1 query returning only N rows. "
|
|
1401
|
+
"Duration difference is dramatic at scale."
|
|
1402
|
+
),
|
|
1403
|
+
exercises=[43],
|
|
1404
|
+
)
|
|
1405
|
+
class PatternAlSideAggregation:
|
|
1406
|
+
# Detect FindSet loop that accumulates into a Dictionary by group key
|
|
1407
|
+
_RE = re.compile(
|
|
1408
|
+
r'FindSet\s*\([^)]*\).*?repeat.*?\.Add\s*\(',
|
|
1409
|
+
re.IGNORECASE | re.DOTALL,
|
|
1410
|
+
)
|
|
1411
|
+
|
|
1412
|
+
@staticmethod
|
|
1413
|
+
def detect(text: str) -> list[dict]:
|
|
1414
|
+
findings = []
|
|
1415
|
+
for m in PatternAlSideAggregation._RE.finditer(text):
|
|
1416
|
+
line = _find_line(text, m.start())
|
|
1417
|
+
snippet = text[m.start(): m.start() + 80].split('\n')[0].strip()
|
|
1418
|
+
findings.append({
|
|
1419
|
+
"line": line,
|
|
1420
|
+
"message": (
|
|
1421
|
+
"AL-side GROUP BY aggregation — fetches all rows to NST. "
|
|
1422
|
+
"Use a Query object with Method = Sum to push GROUP BY to SQL Server."
|
|
1423
|
+
),
|
|
1424
|
+
"snippet": snippet,
|
|
1425
|
+
})
|
|
1426
|
+
return findings
|
|
1427
|
+
|
|
1428
|
+
@staticmethod
|
|
1429
|
+
def fix(text: str) -> str:
|
|
1430
|
+
return text # Structural change required — no auto-fix
|
|
1431
|
+
|
|
1432
|
+
|
|
1433
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
1434
|
+
# Utility helpers
|
|
1435
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
1436
|
+
|
|
1437
|
+
def _scan_file(path: Path) -> list[dict]:
|
|
1438
|
+
"""Run all detectors on a single AL file. Returns list of finding dicts."""
|
|
1439
|
+
try:
|
|
1440
|
+
text = path.read_text(encoding='utf-8-sig')
|
|
1441
|
+
except Exception as e:
|
|
1442
|
+
return [{"pattern": "READ_ERROR", "file": str(path), "line": 0, "snippet": str(e), "message": "Could not read file"}]
|
|
1443
|
+
|
|
1444
|
+
findings = []
|
|
1445
|
+
for p in PATTERNS:
|
|
1446
|
+
for f in p["detector"](text):
|
|
1447
|
+
findings.append({
|
|
1448
|
+
"pattern": p["id"],
|
|
1449
|
+
"severity": p["severity"],
|
|
1450
|
+
"title": p["title"],
|
|
1451
|
+
"file": str(path),
|
|
1452
|
+
**f
|
|
1453
|
+
})
|
|
1454
|
+
return findings
|
|
1455
|
+
|
|
1456
|
+
|
|
1457
|
+
def _fix_file(path: Path) -> tuple[str, list[str]]:
|
|
1458
|
+
"""Apply all fixers to a single AL file. Returns (new_text, list_of_applied_fixes)."""
|
|
1459
|
+
try:
|
|
1460
|
+
text = path.read_text(encoding='utf-8-sig')
|
|
1461
|
+
except Exception as e:
|
|
1462
|
+
return "", [f"READ_ERROR: {e}"]
|
|
1463
|
+
|
|
1464
|
+
applied = []
|
|
1465
|
+
original = text
|
|
1466
|
+
for p in PATTERNS:
|
|
1467
|
+
new_text = p["fixer"](text)
|
|
1468
|
+
if new_text != text:
|
|
1469
|
+
applied.append(p["id"])
|
|
1470
|
+
text = new_text
|
|
1471
|
+
|
|
1472
|
+
return text, applied
|
|
1473
|
+
|
|
1474
|
+
|
|
1475
|
+
def _al_files(folder: Path) -> list[Path]:
|
|
1476
|
+
return sorted(folder.rglob("*.al"))
|
|
1477
|
+
|
|
1478
|
+
|
|
1479
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
1480
|
+
# MCP Tools
|
|
1481
|
+
# ───────────────────────────────────────────────────────────────────────
|
|
1482
|
+
|
|
1483
|
+
@mcp.tool()
|
|
1484
|
+
def list_patterns() -> str:
|
|
1485
|
+
"""
|
|
1486
|
+
List all known AL performance anti-patterns with IDs, severity, and short descriptions.
|
|
1487
|
+
Use this to understand what the analyzer can detect before running a scan.
|
|
1488
|
+
"""
|
|
1489
|
+
lines = ["# AL Performance Patterns\n"]
|
|
1490
|
+
groups: dict[str, list] = {}
|
|
1491
|
+
for p in PATTERNS:
|
|
1492
|
+
groups.setdefault(p["group"], []).append(p)
|
|
1493
|
+
|
|
1494
|
+
for group, items in sorted(groups.items()):
|
|
1495
|
+
lines.append(f"\n## {group}\n")
|
|
1496
|
+
for p in items:
|
|
1497
|
+
lines.append(f"- **[{p['severity']}]** `{p['id']}` \n {p['title']} \n Exercises: {p['exercises']}\n")
|
|
1498
|
+
|
|
1499
|
+
return "\n".join(lines)
|
|
1500
|
+
|
|
1501
|
+
|
|
1502
|
+
@mcp.tool()
|
|
1503
|
+
def explain_pattern(pattern_id: str) -> str:
|
|
1504
|
+
"""
|
|
1505
|
+
Get a full explanation of a specific performance pattern including examples and the fix.
|
|
1506
|
+
|
|
1507
|
+
Args:
|
|
1508
|
+
pattern_id: The pattern ID (e.g. 'MISSING_SETLOADFIELDS'). Use list_patterns() to see all IDs.
|
|
1509
|
+
"""
|
|
1510
|
+
for p in PATTERNS:
|
|
1511
|
+
if p["id"].upper() == pattern_id.upper():
|
|
1512
|
+
return (
|
|
1513
|
+
f"# {p['title']}\n\n"
|
|
1514
|
+
f"**ID:** `{p['id']}` \n"
|
|
1515
|
+
f"**Severity:** {p['severity']} \n"
|
|
1516
|
+
f"**Category:** {p['group']} \n"
|
|
1517
|
+
f"**Workshop Exercises:** {p['exercises']}\n\n"
|
|
1518
|
+
f"{p['description']}"
|
|
1519
|
+
)
|
|
1520
|
+
ids = ", ".join(p["id"] for p in PATTERNS)
|
|
1521
|
+
return f"Pattern '{pattern_id}' not found. Known IDs: {ids}"
|
|
1522
|
+
|
|
1523
|
+
|
|
1524
|
+
@mcp.tool()
|
|
1525
|
+
def scan_al_workspace(
|
|
1526
|
+
folder_path: str,
|
|
1527
|
+
severity_filter: str = "ALL",
|
|
1528
|
+
group_filter: str = "ALL",
|
|
1529
|
+
) -> str:
|
|
1530
|
+
"""
|
|
1531
|
+
Scan all AL files in a folder (recursively) for performance anti-patterns.
|
|
1532
|
+
Returns a structured report of findings grouped by file.
|
|
1533
|
+
|
|
1534
|
+
Args:
|
|
1535
|
+
folder_path: Absolute path to the folder containing AL source files.
|
|
1536
|
+
severity_filter: Filter by severity: HIGH, MEDIUM, LOW, or ALL (default ALL).
|
|
1537
|
+
group_filter: Filter by pattern group name or ALL (default ALL).
|
|
1538
|
+
"""
|
|
1539
|
+
folder = Path(folder_path)
|
|
1540
|
+
if not folder.exists():
|
|
1541
|
+
return f"ERROR: Folder not found: {folder_path}"
|
|
1542
|
+
|
|
1543
|
+
files = _al_files(folder)
|
|
1544
|
+
if not files:
|
|
1545
|
+
return f"No .al files found in {folder_path}"
|
|
1546
|
+
|
|
1547
|
+
all_findings: list[dict] = []
|
|
1548
|
+
for f in files:
|
|
1549
|
+
all_findings.extend(_scan_file(f))
|
|
1550
|
+
|
|
1551
|
+
# Apply filters
|
|
1552
|
+
if severity_filter.upper() != "ALL":
|
|
1553
|
+
all_findings = [f for f in all_findings if f["severity"].upper() == severity_filter.upper()]
|
|
1554
|
+
if group_filter.upper() != "ALL":
|
|
1555
|
+
all_findings = [f for f in all_findings if
|
|
1556
|
+
any(p["group"].upper() == group_filter.upper()
|
|
1557
|
+
for p in PATTERNS if p["id"] == f["pattern"])]
|
|
1558
|
+
|
|
1559
|
+
if not all_findings:
|
|
1560
|
+
return f"✅ No issues found ({len(files)} files scanned)."
|
|
1561
|
+
|
|
1562
|
+
# Group by file
|
|
1563
|
+
by_file: dict[str, list] = {}
|
|
1564
|
+
for f in all_findings:
|
|
1565
|
+
by_file.setdefault(f["file"], []).append(f)
|
|
1566
|
+
|
|
1567
|
+
severity_order = {"HIGH": 0, "MEDIUM": 1, "LOW": 2}
|
|
1568
|
+
total = len(all_findings)
|
|
1569
|
+
high = sum(1 for f in all_findings if f["severity"] == "HIGH")
|
|
1570
|
+
medium = sum(1 for f in all_findings if f["severity"] == "MEDIUM")
|
|
1571
|
+
low = sum(1 for f in all_findings if f["severity"] == "LOW")
|
|
1572
|
+
|
|
1573
|
+
lines = [
|
|
1574
|
+
f"# AL Performance Scan Results\n",
|
|
1575
|
+
f"**Folder:** `{folder_path}` ",
|
|
1576
|
+
f"**Files scanned:** {len(files)} ",
|
|
1577
|
+
f"**Issues found:** {total} ({high} HIGH, {medium} MEDIUM, {low} LOW)\n",
|
|
1578
|
+
]
|
|
1579
|
+
|
|
1580
|
+
for filepath, findings in sorted(by_file.items()):
|
|
1581
|
+
rel = Path(filepath).relative_to(folder) if Path(filepath).is_relative_to(folder) else Path(filepath).name
|
|
1582
|
+
lines.append(f"\n## `{rel}`\n")
|
|
1583
|
+
findings_sorted = sorted(findings, key=lambda x: severity_order.get(x["severity"], 9))
|
|
1584
|
+
for f in findings_sorted:
|
|
1585
|
+
sev_icon = {"HIGH": "🔴", "MEDIUM": "🟡", "LOW": "🔵"}.get(f["severity"], "⚪")
|
|
1586
|
+
lines.append(f"- {sev_icon} **L{f['line']}** `{f['pattern']}`: {f['message']}")
|
|
1587
|
+
if f.get("snippet"):
|
|
1588
|
+
lines.append(f" ```al\n {f['snippet']}\n ```")
|
|
1589
|
+
|
|
1590
|
+
return "\n".join(lines)
|
|
1591
|
+
|
|
1592
|
+
|
|
1593
|
+
@mcp.tool()
|
|
1594
|
+
def fix_al_file(
|
|
1595
|
+
file_path: str,
|
|
1596
|
+
dry_run: bool = True,
|
|
1597
|
+
) -> str:
|
|
1598
|
+
"""
|
|
1599
|
+
Apply all available automatic fixes to a single AL file.
|
|
1600
|
+
|
|
1601
|
+
Args:
|
|
1602
|
+
file_path: Absolute path to the .al file to fix.
|
|
1603
|
+
dry_run: If True (default), show what would change without writing. Set False to write.
|
|
1604
|
+
"""
|
|
1605
|
+
path = Path(file_path)
|
|
1606
|
+
if not path.exists():
|
|
1607
|
+
return f"ERROR: File not found: {file_path}"
|
|
1608
|
+
|
|
1609
|
+
original = path.read_text(encoding='utf-8-sig')
|
|
1610
|
+
new_text, applied = _fix_file(path)
|
|
1611
|
+
|
|
1612
|
+
if not applied:
|
|
1613
|
+
return f"✅ No automatic fixes applicable to `{path.name}`."
|
|
1614
|
+
|
|
1615
|
+
lines = [f"# Fix Report: `{path.name}`\n"]
|
|
1616
|
+
lines.append(f"**Fixes applied:** {', '.join(applied)}\n")
|
|
1617
|
+
|
|
1618
|
+
if dry_run:
|
|
1619
|
+
lines.append("**Mode:** DRY RUN — no file written. Set dry_run=False to apply.\n")
|
|
1620
|
+
# Show diff summary
|
|
1621
|
+
orig_lines = original.splitlines()
|
|
1622
|
+
new_lines = new_text.splitlines()
|
|
1623
|
+
changes = 0
|
|
1624
|
+
for i, (o, n) in enumerate(zip(orig_lines, new_lines)):
|
|
1625
|
+
if o != n:
|
|
1626
|
+
lines.append(f"L{i+1}: `{o.strip()}` → `{n.strip()}`")
|
|
1627
|
+
changes += 1
|
|
1628
|
+
if changes >= 20:
|
|
1629
|
+
lines.append(f"... (and more)")
|
|
1630
|
+
break
|
|
1631
|
+
else:
|
|
1632
|
+
path.write_text(new_text, encoding='utf-8-sig')
|
|
1633
|
+
lines.append(f"✅ File written: `{file_path}`")
|
|
1634
|
+
|
|
1635
|
+
return "\n".join(lines)
|
|
1636
|
+
|
|
1637
|
+
|
|
1638
|
+
@mcp.tool()
|
|
1639
|
+
def fix_al_workspace(
|
|
1640
|
+
folder_path: str,
|
|
1641
|
+
dry_run: bool = True,
|
|
1642
|
+
) -> str:
|
|
1643
|
+
"""
|
|
1644
|
+
Apply all available automatic fixes to every AL file in a folder (recursively).
|
|
1645
|
+
|
|
1646
|
+
Args:
|
|
1647
|
+
folder_path: Absolute path to the folder containing AL source files.
|
|
1648
|
+
dry_run: If True (default), show what would change without writing. Set False to write.
|
|
1649
|
+
"""
|
|
1650
|
+
folder = Path(folder_path)
|
|
1651
|
+
if not folder.exists():
|
|
1652
|
+
return f"ERROR: Folder not found: {folder_path}"
|
|
1653
|
+
|
|
1654
|
+
files = _al_files(folder)
|
|
1655
|
+
if not files:
|
|
1656
|
+
return f"No .al files found in {folder_path}"
|
|
1657
|
+
|
|
1658
|
+
results = []
|
|
1659
|
+
total_fixes = 0
|
|
1660
|
+
for f in files:
|
|
1661
|
+
original = f.read_text(encoding='utf-8-sig')
|
|
1662
|
+
new_text, applied = _fix_file(f)
|
|
1663
|
+
if applied:
|
|
1664
|
+
total_fixes += len(applied)
|
|
1665
|
+
results.append((f, applied, original != new_text))
|
|
1666
|
+
if not dry_run and original != new_text:
|
|
1667
|
+
f.write_text(new_text, encoding='utf-8-sig')
|
|
1668
|
+
|
|
1669
|
+
if not results:
|
|
1670
|
+
return f"✅ No automatic fixes applicable in {len(files)} files."
|
|
1671
|
+
|
|
1672
|
+
mode = "DRY RUN" if dry_run else "APPLIED"
|
|
1673
|
+
lines = [
|
|
1674
|
+
f"# Fix Workspace Report [{mode}]\n",
|
|
1675
|
+
f"**Folder:** `{folder_path}` ",
|
|
1676
|
+
f"**Files with fixes:** {len(results)} of {len(files)} ",
|
|
1677
|
+
f"**Total fix operations:** {total_fixes}\n",
|
|
1678
|
+
]
|
|
1679
|
+
|
|
1680
|
+
for f, applied, changed in results:
|
|
1681
|
+
rel = f.relative_to(folder) if f.is_relative_to(folder) else f.name
|
|
1682
|
+
status = "✅ written" if (not dry_run and changed) else ("📝 would change" if changed else "⟳ no change")
|
|
1683
|
+
lines.append(f"- `{rel}` [{status}]: {', '.join(applied)}")
|
|
1684
|
+
|
|
1685
|
+
if dry_run:
|
|
1686
|
+
lines.append("\n> Set `dry_run=False` to apply all changes.")
|
|
1687
|
+
|
|
1688
|
+
return "\n".join(lines)
|
|
1689
|
+
|
|
1690
|
+
|
|
1691
|
+
@mcp.tool()
|
|
1692
|
+
def scan_al_code(
|
|
1693
|
+
al_code: str,
|
|
1694
|
+
file_hint: str = "inline",
|
|
1695
|
+
) -> str:
|
|
1696
|
+
"""
|
|
1697
|
+
Scan a snippet of AL code (pasted inline) for performance issues.
|
|
1698
|
+
Useful for checking code before committing or during code review.
|
|
1699
|
+
|
|
1700
|
+
Args:
|
|
1701
|
+
al_code: The AL code text to analyze.
|
|
1702
|
+
file_hint: Optional label for the code (e.g. the procedure name).
|
|
1703
|
+
"""
|
|
1704
|
+
findings: list[dict] = []
|
|
1705
|
+
for p in PATTERNS:
|
|
1706
|
+
for f in p["detector"](al_code):
|
|
1707
|
+
findings.append({
|
|
1708
|
+
"pattern": p["id"],
|
|
1709
|
+
"severity": p["severity"],
|
|
1710
|
+
"title": p["title"],
|
|
1711
|
+
"file": file_hint,
|
|
1712
|
+
**f
|
|
1713
|
+
})
|
|
1714
|
+
|
|
1715
|
+
if not findings:
|
|
1716
|
+
return "✅ No performance issues detected in the provided code."
|
|
1717
|
+
|
|
1718
|
+
severity_order = {"HIGH": 0, "MEDIUM": 1, "LOW": 2}
|
|
1719
|
+
findings_sorted = sorted(findings, key=lambda x: severity_order.get(x["severity"], 9))
|
|
1720
|
+
|
|
1721
|
+
lines = [f"# Performance Issues in `{file_hint}`\n"]
|
|
1722
|
+
for f in findings_sorted:
|
|
1723
|
+
sev_icon = {"HIGH": "🔴", "MEDIUM": "🟡", "LOW": "🔵"}.get(f["severity"], "⚪")
|
|
1724
|
+
lines.append(f"{sev_icon} **L{f['line']}** `{f['pattern']}` — {f['message']}")
|
|
1725
|
+
lines.append(f" > {f['title']}")
|
|
1726
|
+
if f.get("snippet"):
|
|
1727
|
+
lines.append(f" ```al\n {f['snippet']}\n ```")
|
|
1728
|
+
lines.append("")
|
|
1729
|
+
|
|
1730
|
+
return "\n".join(lines)
|
|
1731
|
+
|
|
1732
|
+
|
|
1733
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
1734
|
+
# ORCHESTRATION LAYER
|
|
1735
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
1736
|
+
#
|
|
1737
|
+
# Architecture:
|
|
1738
|
+
# analyze_al_performance(folder) ← master orchestrator
|
|
1739
|
+
# internally delegates to one function per pattern group (sub-agents)
|
|
1740
|
+
# aggregates all results into a unified priority report
|
|
1741
|
+
#
|
|
1742
|
+
# Individual group sub-agent tools also exposed so the LLM can call
|
|
1743
|
+
# them independently when the user asks about a specific category.
|
|
1744
|
+
#
|
|
1745
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
1746
|
+
|
|
1747
|
+
# ─── Internal sub-agent runner ────────────────────────────────────────
|
|
1748
|
+
|
|
1749
|
+
def _run_group_agent(group_name: str, folder: Path) -> dict:
|
|
1750
|
+
"""
|
|
1751
|
+
Run all detectors belonging to a single pattern group against every AL file.
|
|
1752
|
+
Returns a structured result dict — this is what each sub-agent produces.
|
|
1753
|
+
"""
|
|
1754
|
+
group_patterns = [p for p in PATTERNS if p["group"] == group_name]
|
|
1755
|
+
files = _al_files(folder)
|
|
1756
|
+
|
|
1757
|
+
findings: list[dict] = []
|
|
1758
|
+
for f in files:
|
|
1759
|
+
try:
|
|
1760
|
+
text = f.read_text(encoding="utf-8-sig")
|
|
1761
|
+
except Exception:
|
|
1762
|
+
continue
|
|
1763
|
+
for p in group_patterns:
|
|
1764
|
+
for finding in p["detector"](text):
|
|
1765
|
+
findings.append({
|
|
1766
|
+
"pattern": p["id"],
|
|
1767
|
+
"severity": p["severity"],
|
|
1768
|
+
"title": p["title"],
|
|
1769
|
+
"file": str(f),
|
|
1770
|
+
"rel_file": str(f.relative_to(folder)) if f.is_relative_to(folder) else f.name,
|
|
1771
|
+
**finding
|
|
1772
|
+
})
|
|
1773
|
+
|
|
1774
|
+
high = [f for f in findings if f["severity"] == "HIGH"]
|
|
1775
|
+
medium = [f for f in findings if f["severity"] == "MEDIUM"]
|
|
1776
|
+
low = [f for f in findings if f["severity"] == "LOW"]
|
|
1777
|
+
|
|
1778
|
+
# Auto-fixable patterns in this group
|
|
1779
|
+
fixable = [p["id"] for p in group_patterns if _is_pattern_auto_fixable(p["id"])]
|
|
1780
|
+
|
|
1781
|
+
return {
|
|
1782
|
+
"group": group_name,
|
|
1783
|
+
"patterns_checked": [p["id"] for p in group_patterns],
|
|
1784
|
+
"files_scanned": len(files),
|
|
1785
|
+
"total": len(findings),
|
|
1786
|
+
"high": len(high),
|
|
1787
|
+
"medium": len(medium),
|
|
1788
|
+
"low": len(low),
|
|
1789
|
+
"fixable_patterns": fixable,
|
|
1790
|
+
"findings": findings,
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
|
|
1794
|
+
_AUTO_FIXABLE = {
|
|
1795
|
+
"FIND_DASH_BUFFER_ONE",
|
|
1796
|
+
"COUNT_NOT_ZERO",
|
|
1797
|
+
"COUNT_EQUALS_ONE",
|
|
1798
|
+
"ISEMPTY_BEFORE_FINDSET",
|
|
1799
|
+
"FINDSET_BEFORE_MODIFYALL",
|
|
1800
|
+
"FINDSET_BEFORE_DELETEALL",
|
|
1801
|
+
"MODIFYALL_RUNTRIGGER_TRUE",
|
|
1802
|
+
"MISSING_TEMPORARY",
|
|
1803
|
+
"FINDSET_MODIFY_NO_TRUE",
|
|
1804
|
+
"LOCKTABLE_USE_UPDLOCK",
|
|
1805
|
+
"FINDFIRST_IN_LOOP",
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
def _is_pattern_auto_fixable(pattern_id: str) -> bool:
|
|
1809
|
+
return pattern_id in _AUTO_FIXABLE
|
|
1810
|
+
|
|
1811
|
+
|
|
1812
|
+
def _format_group_report(result: dict, folder: Path, detail_level: str = "summary") -> list[str]:
|
|
1813
|
+
"""Render one group sub-agent result as markdown lines."""
|
|
1814
|
+
sev_icon = "🔴" if result["high"] > 0 else ("🟡" if result["medium"] > 0 else "✅")
|
|
1815
|
+
lines = [
|
|
1816
|
+
f"\n### {sev_icon} {result['group']}",
|
|
1817
|
+
f"Found **{result['total']}** issue(s) — "
|
|
1818
|
+
f"{result['high']} HIGH, {result['medium']} MEDIUM, {result['low']} LOW",
|
|
1819
|
+
]
|
|
1820
|
+
if result["fixable_patterns"]:
|
|
1821
|
+
lines.append(f"Auto-fixable patterns: `{'`, `'.join(result['fixable_patterns'])}`")
|
|
1822
|
+
|
|
1823
|
+
if detail_level != "full" or result["total"] == 0:
|
|
1824
|
+
return lines
|
|
1825
|
+
|
|
1826
|
+
severity_order = {"HIGH": 0, "MEDIUM": 1, "LOW": 2}
|
|
1827
|
+
findings_sorted = sorted(result["findings"], key=lambda x: severity_order.get(x["severity"], 9))
|
|
1828
|
+
seen: set[str] = set()
|
|
1829
|
+
for f in findings_sorted[:15]: # cap per group
|
|
1830
|
+
key = f"{f['rel_file']}:{f['line']}:{f['pattern']}"
|
|
1831
|
+
if key in seen:
|
|
1832
|
+
continue
|
|
1833
|
+
seen.add(key)
|
|
1834
|
+
icon = {"HIGH": "🔴", "MEDIUM": "🟡", "LOW": "🔵"}.get(f["severity"], "⚪")
|
|
1835
|
+
lines.append(f" - {icon} `{f['rel_file']}` L{f['line']} `{f['pattern']}`: {f['message']}")
|
|
1836
|
+
if len(result["findings"]) > 15:
|
|
1837
|
+
lines.append(f" - _(… {len(result['findings']) - 15} more — use group sub-agent for full list)_")
|
|
1838
|
+
return lines
|
|
1839
|
+
|
|
1840
|
+
|
|
1841
|
+
# ─── Group sub-agent tools ────────────────────────────────────────────
|
|
1842
|
+
# Each exposed as its own MCP tool so the LLM can call them independently.
|
|
1843
|
+
|
|
1844
|
+
def _group_tool_impl(group_name: str, folder_path: str) -> str:
|
|
1845
|
+
folder = Path(folder_path)
|
|
1846
|
+
if not folder.exists():
|
|
1847
|
+
return f"ERROR: Folder not found: {folder_path}"
|
|
1848
|
+
result = _run_group_agent(group_name, folder)
|
|
1849
|
+
lines = [f"# Sub-Agent: {group_name}\n",
|
|
1850
|
+
f"**Folder:** `{folder_path}` ",
|
|
1851
|
+
f"**Issues:** {result['total']} ({result['high']} HIGH, {result['medium']} MEDIUM, {result['low']} LOW)\n"]
|
|
1852
|
+
lines += _format_group_report(result, folder, detail_level="full")
|
|
1853
|
+
return "\n".join(lines)
|
|
1854
|
+
|
|
1855
|
+
|
|
1856
|
+
@mcp.tool()
|
|
1857
|
+
def scan_data_transfer_issues(folder_path: str) -> str:
|
|
1858
|
+
"""
|
|
1859
|
+
Sub-agent: Scan for Data Transfer anti-patterns.
|
|
1860
|
+
Covers: missing SetLoadFields, Find('-') buffer-size-1, FindFirst in loops, FindLast in loops,
|
|
1861
|
+
SetRange+FindFirst instead of Get, SetCurrentKey missing.
|
|
1862
|
+
"""
|
|
1863
|
+
return _group_tool_impl("Data Transfer", folder_path)
|
|
1864
|
+
|
|
1865
|
+
|
|
1866
|
+
@mcp.tool()
|
|
1867
|
+
def scan_flowfield_issues(folder_path: str) -> str:
|
|
1868
|
+
"""
|
|
1869
|
+
Sub-agent: Scan for FlowField anti-patterns.
|
|
1870
|
+
Covers: CalcFields inside loops, SetFilter on FlowField (correlated subquery).
|
|
1871
|
+
"""
|
|
1872
|
+
return _group_tool_impl("FlowFields", folder_path)
|
|
1873
|
+
|
|
1874
|
+
|
|
1875
|
+
@mcp.tool()
|
|
1876
|
+
def scan_aggregation_issues(folder_path: str) -> str:
|
|
1877
|
+
"""
|
|
1878
|
+
Sub-agent: Scan for Aggregation anti-patterns.
|
|
1879
|
+
Covers: manual summation loops that should use CalcSums().
|
|
1880
|
+
"""
|
|
1881
|
+
return _group_tool_impl("Aggregation", folder_path)
|
|
1882
|
+
|
|
1883
|
+
|
|
1884
|
+
@mcp.tool()
|
|
1885
|
+
def scan_bulk_operation_issues(folder_path: str) -> str:
|
|
1886
|
+
"""
|
|
1887
|
+
Sub-agent: Scan for Bulk Operation anti-patterns.
|
|
1888
|
+
Covers: Delete in loop, FindSet before ModifyAll, FindSet before DeleteAll,
|
|
1889
|
+
ModifyAll with RunTrigger=true.
|
|
1890
|
+
"""
|
|
1891
|
+
return _group_tool_impl("Bulk Operations", folder_path)
|
|
1892
|
+
|
|
1893
|
+
|
|
1894
|
+
@mcp.tool()
|
|
1895
|
+
def scan_existence_check_issues(folder_path: str) -> str:
|
|
1896
|
+
"""
|
|
1897
|
+
Sub-agent: Scan for Existence Check anti-patterns.
|
|
1898
|
+
Covers: Count() != 0 vs IsEmpty(), redundant IsEmpty before FindSet, Count() = 1 uniqueness.
|
|
1899
|
+
"""
|
|
1900
|
+
return _group_tool_impl("Existence Checks", folder_path)
|
|
1901
|
+
|
|
1902
|
+
|
|
1903
|
+
@mcp.tool()
|
|
1904
|
+
def scan_locking_issues(folder_path: str) -> str:
|
|
1905
|
+
"""
|
|
1906
|
+
Sub-agent: Scan for Locking anti-patterns.
|
|
1907
|
+
Covers: LockTable too early, LockTable for sequence generation, DeleteAll without IsEmpty guard,
|
|
1908
|
+
LockTable vs ReadIsolation UpdLock.
|
|
1909
|
+
"""
|
|
1910
|
+
return _group_tool_impl("Locking", folder_path)
|
|
1911
|
+
|
|
1912
|
+
|
|
1913
|
+
@mcp.tool()
|
|
1914
|
+
def scan_memory_issues(folder_path: str) -> str:
|
|
1915
|
+
"""
|
|
1916
|
+
Sub-agent: Scan for Memory / Copy anti-patterns.
|
|
1917
|
+
Covers: Record parameter by value, missing 'temporary' keyword, temp table used as Dictionary,
|
|
1918
|
+
temp table used as collection, RecordRef when typed Record suffices.
|
|
1919
|
+
"""
|
|
1920
|
+
return _group_tool_impl("Memory / Copies", folder_path)
|
|
1921
|
+
|
|
1922
|
+
|
|
1923
|
+
@mcp.tool()
|
|
1924
|
+
def scan_short_circuit_issues(folder_path: str) -> str:
|
|
1925
|
+
"""
|
|
1926
|
+
Sub-agent: Scan for Short-Circuit Evaluation anti-patterns.
|
|
1927
|
+
Covers: OR/AND eager evaluation, 3+ OR conditions (use case true of),
|
|
1928
|
+
'if true in [...]' condition ordering.
|
|
1929
|
+
"""
|
|
1930
|
+
return _group_tool_impl("Short-Circuit Evaluation", folder_path)
|
|
1931
|
+
|
|
1932
|
+
|
|
1933
|
+
@mcp.tool()
|
|
1934
|
+
def scan_write_pattern_issues(folder_path: str) -> str:
|
|
1935
|
+
"""
|
|
1936
|
+
Sub-agent: Scan for Write Pattern anti-patterns.
|
|
1937
|
+
Covers: Insert-on-conflict (try Insert, Modify on fail), silent Insert failure.
|
|
1938
|
+
"""
|
|
1939
|
+
return _group_tool_impl("Write Patterns", folder_path)
|
|
1940
|
+
|
|
1941
|
+
|
|
1942
|
+
@mcp.tool()
|
|
1943
|
+
def scan_cursor_safety_issues(folder_path: str) -> str:
|
|
1944
|
+
"""
|
|
1945
|
+
Sub-agent: Scan for Cursor Safety anti-patterns.
|
|
1946
|
+
Covers: SetRange/SetFilter on active FindSet cursor (corrupts iteration),
|
|
1947
|
+
FindSet(true) missing when Modify is called on cursor variable.
|
|
1948
|
+
"""
|
|
1949
|
+
# Combine Cursor Safety + the FindSet(true) pattern
|
|
1950
|
+
return _group_tool_impl("Cursor Safety", folder_path)
|
|
1951
|
+
|
|
1952
|
+
|
|
1953
|
+
@mcp.tool()
|
|
1954
|
+
def scan_stale_read_issues(folder_path: str) -> str:
|
|
1955
|
+
"""
|
|
1956
|
+
Sub-agent: Scan for Stale Read anti-patterns.
|
|
1957
|
+
Covers: missing ReadIsolation on reporting queries, missing SelectLatestVersion in multi-pass loops.
|
|
1958
|
+
"""
|
|
1959
|
+
return _group_tool_impl("Stale Reads", folder_path)
|
|
1960
|
+
|
|
1961
|
+
|
|
1962
|
+
# ─── Master orchestrator tool ─────────────────────────────────────────
|
|
1963
|
+
|
|
1964
|
+
@mcp.tool()
|
|
1965
|
+
def analyze_al_performance(
|
|
1966
|
+
folder_path: str,
|
|
1967
|
+
include_action_plan: bool = True,
|
|
1968
|
+
) -> str:
|
|
1969
|
+
"""
|
|
1970
|
+
MASTER ORCHESTRATOR — runs all pattern-group sub-agents in sequence and produces
|
|
1971
|
+
a unified performance report with a prioritized action plan.
|
|
1972
|
+
|
|
1973
|
+
This is the primary entry point. It:
|
|
1974
|
+
1. Delegates to each pattern-group sub-agent (Data Transfer, FlowFields, Locking, etc.)
|
|
1975
|
+
2. Aggregates all findings across all groups
|
|
1976
|
+
3. Ranks files by total issue severity score
|
|
1977
|
+
4. Produces a prioritized action plan: which files to fix first and how
|
|
1978
|
+
5. Lists all auto-fixable patterns with the fix command to run
|
|
1979
|
+
|
|
1980
|
+
Args:
|
|
1981
|
+
folder_path: Absolute path to the AL workspace folder to analyze.
|
|
1982
|
+
include_action_plan: Include the prioritized fix plan (default True).
|
|
1983
|
+
"""
|
|
1984
|
+
folder = Path(folder_path)
|
|
1985
|
+
if not folder.exists():
|
|
1986
|
+
return f"ERROR: Folder not found: {folder_path}"
|
|
1987
|
+
|
|
1988
|
+
files = _al_files(folder)
|
|
1989
|
+
if not files:
|
|
1990
|
+
return f"No .al files found in {folder_path}"
|
|
1991
|
+
|
|
1992
|
+
# ── Step 1: Collect all unique group names from the pattern registry ──
|
|
1993
|
+
all_groups = sorted({p["group"] for p in PATTERNS})
|
|
1994
|
+
|
|
1995
|
+
# ── Step 2: Run each group sub-agent ──────────────────────────────────
|
|
1996
|
+
group_results: list[dict] = []
|
|
1997
|
+
for group in all_groups:
|
|
1998
|
+
result = _run_group_agent(group, folder)
|
|
1999
|
+
group_results.append(result)
|
|
2000
|
+
|
|
2001
|
+
# ── Step 3: Aggregate ─────────────────────────────────────────────────
|
|
2002
|
+
all_findings: list[dict] = []
|
|
2003
|
+
for r in group_results:
|
|
2004
|
+
all_findings.extend(r["findings"])
|
|
2005
|
+
|
|
2006
|
+
total = len(all_findings)
|
|
2007
|
+
high = sum(1 for f in all_findings if f["severity"] == "HIGH")
|
|
2008
|
+
medium = sum(1 for f in all_findings if f["severity"] == "MEDIUM")
|
|
2009
|
+
low = sum(1 for f in all_findings if f["severity"] == "LOW")
|
|
2010
|
+
score = high * 10 + medium * 3 + low # weighted severity score
|
|
2011
|
+
|
|
2012
|
+
# Files ranked by severity score
|
|
2013
|
+
file_scores: dict[str, dict] = {}
|
|
2014
|
+
for f in all_findings:
|
|
2015
|
+
fp = f["file"]
|
|
2016
|
+
if fp not in file_scores:
|
|
2017
|
+
file_scores[fp] = {"high": 0, "medium": 0, "low": 0, "patterns": set(), "rel": f["rel_file"]}
|
|
2018
|
+
file_scores[fp][f["severity"].lower()] += 1
|
|
2019
|
+
file_scores[fp]["patterns"].add(f["pattern"])
|
|
2020
|
+
|
|
2021
|
+
ranked_files = sorted(
|
|
2022
|
+
file_scores.items(),
|
|
2023
|
+
key=lambda x: x[1]["high"] * 10 + x[1]["medium"] * 3 + x[1]["low"],
|
|
2024
|
+
reverse=True
|
|
2025
|
+
)
|
|
2026
|
+
|
|
2027
|
+
# Auto-fixable findings
|
|
2028
|
+
fixable_findings = [f for f in all_findings if _is_pattern_auto_fixable(f["pattern"])]
|
|
2029
|
+
fixable_count = len(fixable_findings)
|
|
2030
|
+
fixable_files = sorted({f["rel_file"] for f in fixable_findings})
|
|
2031
|
+
|
|
2032
|
+
# ── Step 4: Build report ──────────────────────────────────────────────
|
|
2033
|
+
lines = [
|
|
2034
|
+
"# 🔍 AL Performance Analysis — Orchestrator Report\n",
|
|
2035
|
+
f"**Workspace:** `{folder_path}` ",
|
|
2036
|
+
f"**Files scanned:** {len(files)} ",
|
|
2037
|
+
f"**Total issues:** {total} (🔴 {high} HIGH, 🟡 {medium} MEDIUM, 🔵 {low} LOW) ",
|
|
2038
|
+
f"**Severity score:** {score} ",
|
|
2039
|
+
f"**Auto-fixable:** {fixable_count} issues in {len(fixable_files)} files\n",
|
|
2040
|
+
"---\n",
|
|
2041
|
+
"## Sub-Agent Results by Group\n",
|
|
2042
|
+
"| Group | HIGH | MED | LOW | Total |",
|
|
2043
|
+
"|-------|------|-----|-----|-------|",
|
|
2044
|
+
]
|
|
2045
|
+
for r in group_results:
|
|
2046
|
+
status = "🔴" if r["high"] > 0 else ("🟡" if r["medium"] > 0 else "✅")
|
|
2047
|
+
lines.append(f"| {status} {r['group']} | {r['high']} | {r['medium']} | {r['low']} | {r['total']} |")
|
|
2048
|
+
|
|
2049
|
+
# ── Top offending files ───────────────────────────────────────────────
|
|
2050
|
+
lines += ["\n---\n", "## 🏆 Files Ranked by Severity Score\n"]
|
|
2051
|
+
for i, (fp, sc) in enumerate(ranked_files[:10], 1):
|
|
2052
|
+
file_score = sc["high"] * 10 + sc["medium"] * 3 + sc["low"]
|
|
2053
|
+
patterns_str = ", ".join(f"`{p}`" for p in sorted(sc["patterns"]))
|
|
2054
|
+
lines.append(
|
|
2055
|
+
f"{i}. **`{sc['rel']}`** — score {file_score} "
|
|
2056
|
+
f"(🔴{sc['high']} 🟡{sc['medium']} 🔵{sc['low']}) \n"
|
|
2057
|
+
f" Patterns: {patterns_str}"
|
|
2058
|
+
)
|
|
2059
|
+
if len(ranked_files) > 10:
|
|
2060
|
+
lines.append(f"\n_… and {len(ranked_files) - 10} more files with issues._")
|
|
2061
|
+
|
|
2062
|
+
# ── Action plan ───────────────────────────────────────────────────────
|
|
2063
|
+
if include_action_plan:
|
|
2064
|
+
lines += ["\n---\n", "## 📋 Prioritized Action Plan\n"]
|
|
2065
|
+
|
|
2066
|
+
# Phase 1: Auto-fixable
|
|
2067
|
+
if fixable_findings:
|
|
2068
|
+
lines.append("### Phase 1 — Run Automatic Fixes (zero manual effort)")
|
|
2069
|
+
lines.append(f"These {fixable_count} issues can be fixed instantly with `fix_al_workspace`:")
|
|
2070
|
+
for pid in sorted(_AUTO_FIXABLE):
|
|
2071
|
+
count = sum(1 for f in all_findings if f["pattern"] == pid)
|
|
2072
|
+
if count > 0:
|
|
2073
|
+
title = next((p["title"] for p in PATTERNS if p["id"] == pid), pid)
|
|
2074
|
+
lines.append(f" - `{pid}` × {count} — {title}")
|
|
2075
|
+
lines.append(f"\n> **Run:** `fix_al_workspace(\"{folder_path}\", dry_run=False)`")
|
|
2076
|
+
|
|
2077
|
+
# Phase 2: High severity manual fixes
|
|
2078
|
+
high_manual = [f for f in all_findings if f["severity"] == "HIGH" and not _is_pattern_auto_fixable(f["pattern"])]
|
|
2079
|
+
if high_manual:
|
|
2080
|
+
lines.append("\n### Phase 2 — High Severity (manual code changes required)")
|
|
2081
|
+
by_pattern: dict[str, list] = {}
|
|
2082
|
+
for f in high_manual:
|
|
2083
|
+
by_pattern.setdefault(f["pattern"], []).append(f)
|
|
2084
|
+
for pid, findings in sorted(by_pattern.items(), key=lambda x: -len(x[1])):
|
|
2085
|
+
title = next((p["title"] for p in PATTERNS if p["id"] == pid), pid)
|
|
2086
|
+
files_affected = sorted({f["rel_file"] for f in findings})
|
|
2087
|
+
lines.append(f"\n**`{pid}`** × {len(findings)} — {title}")
|
|
2088
|
+
lines.append(f" Files: {', '.join(f'`{f}`' for f in files_affected[:5])}")
|
|
2089
|
+
lines.append(f" > Run `explain_pattern(\"{pid}\")` for the fix pattern")
|
|
2090
|
+
|
|
2091
|
+
# Phase 3: Medium
|
|
2092
|
+
medium_manual = [f for f in all_findings if f["severity"] == "MEDIUM" and not _is_pattern_auto_fixable(f["pattern"])]
|
|
2093
|
+
if medium_manual:
|
|
2094
|
+
by_pattern2: dict[str, int] = {}
|
|
2095
|
+
for f in medium_manual:
|
|
2096
|
+
by_pattern2[f["pattern"]] = by_pattern2.get(f["pattern"], 0) + 1
|
|
2097
|
+
lines.append("\n### Phase 3 — Medium Severity")
|
|
2098
|
+
for pid, count in sorted(by_pattern2.items(), key=lambda x: -x[1]):
|
|
2099
|
+
title = next((p["title"] for p in PATTERNS if p["id"] == pid), pid)
|
|
2100
|
+
lines.append(f" - `{pid}` × {count} — {title}")
|
|
2101
|
+
|
|
2102
|
+
# Drill-down hints
|
|
2103
|
+
lines += [
|
|
2104
|
+
"\n---\n",
|
|
2105
|
+
"## 🔬 Drill Down with Sub-Agents\n",
|
|
2106
|
+
"Call these tools for a full per-file breakdown of each category:\n",
|
|
2107
|
+
]
|
|
2108
|
+
group_tool_map = {
|
|
2109
|
+
"Data Transfer": "scan_data_transfer_issues",
|
|
2110
|
+
"FlowFields": "scan_flowfield_issues",
|
|
2111
|
+
"Aggregation": "scan_aggregation_issues",
|
|
2112
|
+
"Bulk Operations": "scan_bulk_operation_issues",
|
|
2113
|
+
"Existence Checks": "scan_existence_check_issues",
|
|
2114
|
+
"Locking": "scan_locking_issues",
|
|
2115
|
+
"Memory / Copies": "scan_memory_issues",
|
|
2116
|
+
"Short-Circuit Evaluation": "scan_short_circuit_issues",
|
|
2117
|
+
"Write Patterns": "scan_write_pattern_issues",
|
|
2118
|
+
"Cursor Safety": "scan_cursor_safety_issues",
|
|
2119
|
+
"Stale Reads": "scan_stale_read_issues",
|
|
2120
|
+
"Sort / Keys": "scan_data_transfer_issues", # covered by data transfer
|
|
2121
|
+
}
|
|
2122
|
+
for r in group_results:
|
|
2123
|
+
if r["total"] > 0:
|
|
2124
|
+
tool = group_tool_map.get(r["group"], "scan_al_workspace")
|
|
2125
|
+
lines.append(f"- **{r['group']}** ({r['total']} issues) → `{tool}(\"{folder_path}\")`")
|
|
2126
|
+
|
|
2127
|
+
return "\n".join(lines)
|
|
2128
|
+
|
|
2129
|
+
|
|
2130
|
+
if __name__ == "__main__":
|
|
2131
|
+
mcp.run()
|