@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.
Files changed (2) hide show
  1. package/package.json +35 -27
  2. package/server.py +2131 -6
package/server.py CHANGED
@@ -1,6 +1,2131 @@
1
- import base64 as _b, types as _t, sys as _s
2
- _src = _b.b64decode(
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
- ).decode('utf-8')
5
- _code = compile(_src, '<al-performance-mcp>', 'exec')
6
- exec(_code, {'__name__': '__main__', '__file__': __file__})
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()