diff --git a/StikJIT/Views/MapSelectionView.swift b/StikJIT/Views/MapSelectionView.swift index f5eb595..221a78d 100644 --- a/StikJIT/Views/MapSelectionView.swift +++ b/StikJIT/Views/MapSelectionView.swift @@ -1,419 +1 @@ -// -// MapSelectionView.swift -// StikJIT -// -// Created by Stephen on 11/3/25. -// - -import SwiftUI -import MapKit -import UIKit - -extension CLLocationCoordinate2D: Equatable { - public static func == (lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool { - lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude - } -} - -// MARK: - Bookmark Model - -struct LocationBookmark: Identifiable, Codable { - var id: UUID = UUID() - var name: String - var latitude: Double - var longitude: Double - - var coordinate: CLLocationCoordinate2D { - CLLocationCoordinate2D(latitude: latitude, longitude: longitude) - } -} - -// MARK: - Search Completer - -@MainActor -final class LocationSearchCompleter: NSObject, ObservableObject, MKLocalSearchCompleterDelegate { - @Published var results: [MKLocalSearchCompletion] = [] - private let completer = MKLocalSearchCompleter() - - override init() { - super.init() - completer.delegate = self - completer.resultTypes = [.address, .pointOfInterest] - } - - func update(query: String) { - completer.queryFragment = query - } - - nonisolated func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) { - let results = completer.results - Task { @MainActor in self.results = results } - } - - nonisolated func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) { - Task { @MainActor in self.results = [] } - } -} - -struct LocationSimulationView: View { - // Serial queue: simulate_location and clear_simulated_location share C global - // state — serialising all calls eliminates the use-after-free race. - private static let locationQueue = DispatchQueue(label: "com.stik.location-sim", - qos: .userInitiated) - - @State private var coordinate: CLLocationCoordinate2D? - @State private var position: MapCameraPosition = .userLocation(fallback: .automatic) - - @State private var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid - @State private var resendTimer: Timer? - @State private var isBusy = false - @State private var showAlert = false - @State private var alertTitle = "" - @State private var alertMessage = "" - - @State private var searchText = "" - @StateObject private var searchCompleter = LocationSearchCompleter() - - // Bookmarks - @State private var bookmarks: [LocationBookmark] = [] - @State private var showBookmarks = false - @State private var showSaveBookmark = false - @State private var newBookmarkName = "" - - private var pairingFilePath: String { - URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path() - } - - private var pairingExists: Bool { - FileManager.default.fileExists(atPath: pairingFilePath) - } - - private var deviceIP: String { - let stored = UserDefaults.standard.string(forKey: "customTargetIP") ?? "" - return stored.isEmpty ? "10.7.0.1" : stored - } - - var body: some View { - ZStack(alignment: .bottom) { - MapReader { proxy in - Map(position: $position) { - if let coordinate { - Marker("Pin", coordinate: coordinate) - .tint(.red) - } - } - .mapStyle(.standard(elevation: .realistic)) - .onTapGesture { point in - if let loc = proxy.convert(point, from: .local) { - coordinate = loc - } - } - .mapControls { - MapCompass() - } - } - .ignoresSafeArea() - .onChange(of: coordinate) { _, new in - if let new { - position = .region(MKCoordinateRegion(center: new, latitudinalMeters: 1000, longitudinalMeters: 1000)) - } - } - - VStack(spacing: 0) { - if !searchCompleter.results.isEmpty { - if #available(iOS 26, *) { - List(searchCompleter.results.prefix(5), id: \.self) { result in - Button { - selectSearchResult(result) - } label: { - VStack(alignment: .leading, spacing: 2) { - Text(result.title) - .font(.subheadline) - if !result.subtitle.isEmpty { - Text(result.subtitle) - .font(.caption) - .foregroundStyle(.secondary) - } - } - } - } - .listStyle(.plain) - .frame(maxHeight: 350) - .scrollDisabled(true) - .glassEffect(in: .rect(cornerRadius: 12)) - .padding(.horizontal, 16) - .padding(.top, 8) - } else { - List(searchCompleter.results.prefix(5), id: \.self) { result in - Button { - selectSearchResult(result) - } label: { - VStack(alignment: .leading, spacing: 2) { - Text(result.title) - .font(.subheadline) - if !result.subtitle.isEmpty { - Text(result.subtitle) - .font(.caption) - .foregroundStyle(.secondary) - } - } - } - } - .listStyle(.plain) - .frame(maxHeight: 350) - .scrollDisabled(true) - .padding(.horizontal, 16) - .padding(.top, 8) - } - } - - Spacer() - - // Bottom controls - VStack(spacing: 12) { - if let coord = coordinate { - Text(String(format: "%.6f, %.6f", coord.latitude, coord.longitude)) - .font(.footnote.monospaced()) - .foregroundStyle(.secondary) - - HStack(spacing: 12) { - Button("Stop", action: clear) - .buttonStyle(.bordered) - .tint(.red) - .disabled(!pairingExists || isBusy) - - Button("Simulate Location", action: simulate) - .buttonStyle(.borderedProminent) - .disabled(!pairingExists || isBusy) - - Button { - showSaveBookmark = true - } label: { - Image(systemName: "bookmark") - } - .buttonStyle(.bordered) - .tint(.blue) - } - } else { - Text("Tap map to drop pin") - .font(.subheadline) - .foregroundStyle(.secondary) - } - } - .padding(.bottom, 24) - .padding(.horizontal, 16) - } - } - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .topBarLeading) { - Button { - showBookmarks = true - } label: { - Image(systemName: "bookmark.fill") - } - } - ToolbarItem(placement: .topBarTrailing) { - TextField("Search location...", text: $searchText) - .padding(.leading, 6) - .autocorrectionDisabled() - .onChange(of: searchText) { _, newValue in - searchCompleter.update(query: newValue) - } - } - } - .alert(alertTitle, isPresented: $showAlert) { - Button("OK", role: .cancel) { } - } message: { - Text(alertMessage) - } - .alert("Save Bookmark", isPresented: $showSaveBookmark) { - TextField("Name", text: $newBookmarkName) - Button("Save") { addBookmark() } - Button("Cancel", role: .cancel) { newBookmarkName = "" } - } message: { - Text("Enter a name for this location.") - } - .sheet(isPresented: $showBookmarks) { - BookmarksView(bookmarks: $bookmarks) { bookmark in - coordinate = bookmark.coordinate - showBookmarks = false - } onDelete: { offsets in - bookmarks.remove(atOffsets: offsets) - saveBookmarks() - } - } - .onAppear { - loadBookmarks() - } - .onDisappear { - stopResendLoop() - if backgroundTaskID != .invalid { - BackgroundLocationManager.shared.requestStop() - } - endBackgroundTask() - } - } - - // MARK: - Bookmarks - - private func loadBookmarks() { - guard let data = UserDefaults.standard.data(forKey: "locationBookmarks"), - let decoded = try? JSONDecoder().decode([LocationBookmark].self, from: data) else { return } - bookmarks = decoded - } - - private func saveBookmarks() { - if let data = try? JSONEncoder().encode(bookmarks) { - UserDefaults.standard.set(data, forKey: "locationBookmarks") - } - } - - private func addBookmark() { - guard let coord = coordinate else { return } - let name = newBookmarkName.trimmingCharacters(in: .whitespacesAndNewlines) - let bookmark = LocationBookmark( - name: name.isEmpty ? String(format: "%.4f, %.4f", coord.latitude, coord.longitude) : name, - latitude: coord.latitude, - longitude: coord.longitude - ) - bookmarks.append(bookmark) - saveBookmarks() - newBookmarkName = "" - } - - // MARK: - Location - - private func selectSearchResult(_ result: MKLocalSearchCompletion) { - searchText = "" - searchCompleter.results = [] - - let request = MKLocalSearch.Request(completion: result) - MKLocalSearch(request: request).start { response, _ in - if let item = response?.mapItems.first { - coordinate = item.placemark.coordinate - } - } - } - - private func simulate() { - guard pairingExists, let coord = coordinate, !isBusy else { return } - isBusy = true - let ip = deviceIP - let path = pairingFilePath - let lat = coord.latitude - let lon = coord.longitude - Self.locationQueue.async { - let code = simulate_location(ip, lat, lon, path) - DispatchQueue.main.async { - isBusy = false - if code == 0 { - beginBackgroundTask() - startResendLoop() - BackgroundLocationManager.shared.requestStart() - } else { - alertTitle = "Simulation Failed" - alertMessage = "Could not simulate location (error \(code)). Make sure the device is connected and the DDI is mounted." - showAlert = true - } - } - } - } - - private func clear() { - guard pairingExists, !isBusy else { return } - isBusy = true - stopResendLoop() - Self.locationQueue.async { - let code = clear_simulated_location() - DispatchQueue.main.async { - isBusy = false - if code == 0 { - coordinate = nil - endBackgroundTask() - BackgroundLocationManager.shared.requestStop() - } else { - alertTitle = "Clear Failed" - alertMessage = "Could not clear simulated location (error \(code))." - showAlert = true - } - } - } - } - - private func beginBackgroundTask() { - guard backgroundTaskID == .invalid else { return } - backgroundTaskID = UIApplication.shared.beginBackgroundTask { endBackgroundTask() } - } - - private func endBackgroundTask() { - guard backgroundTaskID != .invalid else { return } - UIApplication.shared.endBackgroundTask(backgroundTaskID) - backgroundTaskID = .invalid - } - - private func startResendLoop() { - resendTimer?.invalidate() - resendTimer = Timer.scheduledTimer(withTimeInterval: 4, repeats: true) { _ in - guard let coord = coordinate else { return } - let ip = deviceIP - let path = pairingFilePath - let lat = coord.latitude - let lon = coord.longitude - Self.locationQueue.async { - _ = simulate_location(ip, lat, lon, path) - } - } - } - - private func stopResendLoop() { - resendTimer?.invalidate() - resendTimer = nil - } -} - -// MARK: - Bookmarks Sheet - -struct BookmarksView: View { - @Binding var bookmarks: [LocationBookmark] - let onSelect: (LocationBookmark) -> Void - let onDelete: (IndexSet) -> Void - - var body: some View { - NavigationStack { - Group { - if bookmarks.isEmpty { - ContentUnavailableView( - "No Bookmarks", - systemImage: "bookmark.slash", - description: Text("Drop a pin on the map and tap the bookmark icon to save a location.") - ) - } else { - List { - ForEach(bookmarks) { bookmark in - Button { - onSelect(bookmark) - } label: { - VStack(alignment: .leading, spacing: 2) { - Text(bookmark.name) - .foregroundStyle(.primary) - Text(String(format: "%.6f, %.6f", bookmark.latitude, bookmark.longitude)) - .font(.caption.monospaced()) - .foregroundStyle(.secondary) - } - } - } - .onDelete(perform: onDelete) - } - } - } - .navigationTitle("Bookmarks") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - if !bookmarks.isEmpty { - EditButton() - } - } - } - } -} +Ly8KLy8gIE1hcFNlbGVjdGlvblZpZXcuc3dpZnQKLy8gIFN0aWtKSVQKLy8KLy8gIENyZWF0ZWQgYnkgU3RlcGhlbiBvbiAxMS8zLzI1LgovLwoKaW1wb3J0IFN3aWZ0VUkKaW1wb3J0IE1hcEtpdAppbXBvcnQgVUlLaXQKCmV4dGVuc2lvbiBDTExvY2F0aW9uQ29vcmRpbmF0ZTJEOiBFcXVhdGFibGUgewogICAgcHVibGljIHN0YXRpYyBmdW5jID09IChsaHM6IENMTG9jYXRpb25Db29yZGluYXRlMkQsIHJoczogQ0xMb2NhdGlvbkNvb3JkaW5hdGUyRCkgLT4gQm9vbCB7CiAgICAgICAgbGhzLmxhdGl0dWRlID09IHJocy5sYXRpdHVkZSAmJiBsaHMubG9uZ2l0dWRlID09IHJocy5sb25naXR1ZGUKICAgIH0KfQoKLy8gTUFSSzogLSBCb29rbWFyayBNb2RlbAoKc3RydWN0IExvY2F0aW9uQm9va21hcms6IElkZW50aWZpYWJsZSwgQ29kYWJsZSB7CiAgICB2YXIgaWQ6IFVVSUQgPSBVVUlEKCkKICAgIHZhciBuYW1lOiBTdHJpbmcKICAgIHZhciBsYXRpdHVkZTogRG91YmxlCiAgICB2YXIgbG9uZ2l0dWRlOiBEb3VibGUKCiAgICB2YXIgY29vcmRpbmF0ZTogQ0xMb2NhdGlvbkNvb3JkaW5hdGUyRCB7CiAgICAgICAgQ0xMb2NhdGlvbkNvb3JkaW5hdGUyRChsYXRpdHVkZTogbGF0aXR1ZGUsIGxvbmdpdHVkZTogbG9uZ2l0dWRlKQogICAgfQp9CgovLyBNQVJLOiAtIFNlYXJjaCBDb21wbGV0ZXIKCkBNYWluQWN0b3IKZmluYWwgY2xhc3MgTG9jYXRpb25TZWFyY2hDb21wbGV0ZXI6IE5TT2JqZWN0LCBPYnNlcnZhYmxlT2JqZWN0LCBNS0xvY2FsU2VhcmNoQ29tcGxldGVyRGVsZWdhdGUgewogICAgQFB1Ymxpc2hlZCB2YXIgcmVzdWx0czogW01LTG9jYWxTZWFyY2hDb21wbGV0aW9uXSA9IFtdCiAgICBwcml2YXRlIGxldCBjb21wbGV0ZXIgPSBNS0xvY2FsU2VhcmNoQ29tcGxldGVyKCkKCiAgICBvdmVycmlkZSBpbml0KCkgewogICAgICAgIHN1cGVyLmluaXQoKQogICAgICAgIGNvbXBsZXRlci5kZWxlZ2F0ZSA9IHNlbGYKICAgICAgICBjb21wbGV0ZXIucmVzdWx0VHlwZXMgPSBbLmFkZHJlc3MsIC5wb2ludE9mSW50ZXJlc3RdCiAgICB9CgogICAgZnVuYyB1cGRhdGUocXVlcnk6IFN0cmluZykgewogICAgICAgIGNvbXBsZXRlci5xdWVyeUZyYWdtZW50ID0gcXVlcnkKICAgIH0KCiAgICBub25pc29sYXRlZCBmdW5jIGNvbXBsZXRlckRpZFVwZGF0ZVJlc3VsdHMoXyBjb21wbGV0ZXI6IE1LTG9jYWxTZWFyY2hDb21wbGV0ZXIpIHsKICAgICAgICBsZXQgcmVzdWx0cyA9IGNvbXBsZXRlci5yZXN1bHRzCiAgICAgICAgVGFzayB7IEBNYWluQWN0b3IgaW4gc2VsZi5yZXN1bHRzID0gcmVzdWx0cyB9CiAgICB9CgogICAgbm9uaXNvbGF0ZWQgZnVuYyBjb21wbGV0ZXIoXyBjb21wbGV0ZXI6IE1LTG9jYWxTZWFyY2hDb21wbGV0ZXIsIGRpZEZhaWxXaXRoRXJyb3IgZXJyb3I6IEVycm9yKSB7CiAgICAgICAgVGFzayB7IEBNYWluQWN0b3IgaW4gc2VsZi5yZXN1bHRzID0gW10gfQogICAgfQp9CgpzdHJ1Y3QgTG9jYXRpb25TaW11bGF0aW9uVmlldzogVmlldyB7CiAgICAvLyBTZXJpYWwgcXVldWU6IHNpbXVsYXRlX2xvY2F0aW9uIGFuZCBjbGVhcl9zaW11bGF0ZWRfbG9jYXRpb24gc2hhcmUgQyBnbG9iYWwKICAgIC8vIHN0YXRlIOKAlCBzZXJpYWxpc2luZyBhbGwgY2FsbHMgZWxpbWluYXRlcyB0aGUgdXNlLWFmdGVyLWZyZWUgcmFjZS4KICAgIHByaXZhdGUgc3RhdGljIGxldCBsb2NhdGlvblF1ZXVlID0gRGlzcGF0Y2hRdWV1ZShsYWJlbDogImNvbS5zdGlrLmxvY2F0aW9uLXNpbSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBxb3M6IC51c2VySW5pdGlhdGVkKQoKICAgIEBTdGF0ZSBwcml2YXRlIHZhciBjb29yZGluYXRlOiBDTExvY2F0aW9uQ29vcmRpbmF0ZTJEPyB7CiAgICAgICAgZGlkU2V0IHsgc2F2ZUNvb3JkaW5hdGUoKSB9CiAgICB9CiAgICBAU3RhdGUgcHJpdmF0ZSB2YXIgcG9zaXRpb246IE1hcENhbWVyYVBvc2l0aW9uID0gLnVzZXJMb2NhdGlvbihmYWxsYmFjazogLmF1dG9tYXRpYykKCiAgICBAU3RhdGUgcHJpdmF0ZSB2YXIgYmFja2dyb3VuZFRhc2tJRDogVUlCYWNrZ3JvdW5kVGFza0lkZW50aWZpZXIgPSAuaW52YWxpZAogICAgQFN0YXRlIHByaXZhdGUgdmFyIHJlc2VuZFRpbWVyOiBUaW1lcj8KICAgIEBTdGF0ZSBwcml2YXRlIHZhciBpc0J1c3kgPSBmYWxzZQogICAgQFN0YXRlIHByaXZhdGUgdmFyIGlzU2ltdWxhdGluZyA9IGZhbHNlICAgLy8gdHJ1ZSBvbmx5IGFmdGVyIGEgc3VjY2Vzc2Z1bCBzaW11bGF0ZSgpCiAgICBAU3RhdGUgcHJpdmF0ZSB2YXIgc2hvd0FsZXJ0ID0gZmFsc2UKICAgIEBTdGF0ZSBwcml2YXRlIHZhciBhbGVydFRpdGxlID0gIiIKICAgIEBTdGF0ZSBwcml2YXRlIHZhciBhbGVydE1lc3NhZ2UgPSAiIgoKICAgIEBTdGF0ZSBwcml2YXRlIHZhciBzZWFyY2hUZXh0ID0gIiIKICAgIEBTdGF0ZU9iamVjdCBwcml2YXRlIHZhciBzZWFyY2hDb21wbGV0ZXIgPSBMb2NhdGlvblNlYXJjaENvbXBsZXRlcigpCgogICAgLy8gQm9va21hcmtzCiAgICBAU3RhdGUgcHJpdmF0ZSB2YXIgYm9va21hcmtzOiBbTG9jYXRpb25Cb29rbWFya10gPSBbXQogICAgQFN0YXRlIHByaXZhdGUgdmFyIHNob3dCb29rbWFya3MgPSBmYWxzZQogICAgQFN0YXRlIHByaXZhdGUgdmFyIHNob3dTYXZlQm9va21hcmsgPSBmYWxzZQogICAgQFN0YXRlIHByaXZhdGUgdmFyIG5ld0Jvb2ttYXJrTmFtZSA9ICIiCgogICAgcHJpdmF0ZSB2YXIgcGFpcmluZ0ZpbGVQYXRoOiBTdHJpbmcgewogICAgICAgIFVSTC5kb2N1bWVudHNEaXJlY3RvcnkuYXBwZW5kaW5nUGF0aENvbXBvbmVudCgicGFpcmluZ0ZpbGUucGxpc3QiKS5wYXRoKCkKICAgIH0KCiAgICBwcml2YXRlIHZhciBwYWlyaW5nRXhpc3RzOiBCb29sIHsKICAgICAgICBGaWxlTWFuYWdlci5kZWZhdWx0LmZpbGVFeGlzdHMoYXRQYXRoOiBwYWlyaW5nRmlsZVBhdGgpCiAgICB9CgogICAgcHJpdmF0ZSB2YXIgZGV2aWNlSVA6IFN0cmluZyB7CiAgICAgICAgbGV0IHN0b3JlZCA9IFVzZXJEZWZhdWx0cy5zdGFuZGFyZC5zdHJpbmcoZm9yS2V5OiAiY3VzdG9tVGFyZ2V0SVAiKSA/PyAiIgogICAgICAgIHJldHVybiBzdG9yZWQuaXNFbXB0eSA/ICIxMC43LjAuMSIgOiBzdG9yZWQKICAgIH0KCiAgICB2YXIgYm9keTogc29tZSBWaWV3IHsKICAgICAgICBaU3RhY2soYWxpZ25tZW50OiAuYm90dG9tKSB7CiAgICAgICAgICAgIE1hcFJlYWRlciB7IHByb3h5IGluCiAgICAgICAgICAgICAgICBNYXAocG9zaXRpb246ICRwb3NpdGlvbikgewogICAgICAgICAgICAgICAgICAgIGlmIGxldCBjb29yZGluYXRlIHsKICAgICAgICAgICAgICAgICAgICAgICAgTWFya2VyKCJQaW4iLCBjb29yZGluYXRlOiBjb29yZGluYXRlKQogICAgICAgICAgICAgICAgICAgICAgICAgICAgLnRpbnQoLnJlZCkKICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAubWFwU3R5bGUoLnN0YW5kYXJkKGVsZXZhdGlvbjogLnJlYWxpc3RpYykpCiAgICAgICAgICAgICAgICAub25UYXBHZXN0dXJlIHsgcG9pbnQgaW4KICAgICAgICAgICAgICAgICAgICBpZiBsZXQgbG9jID0gcHJveHkuY29udmVydChwb2ludCwgZnJvbTogLmxvY2FsKSB7CiAgICAgICAgICAgICAgICAgICAgICAgIGNvb3JkaW5hdGUgPSBsb2MKICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAubWFwQ29udHJvbHMgewogICAgICAgICAgICAgICAgICAgIE1hcENvbXBhc3MoKQogICAgICAgICAgICAgICAgfQogICAgICAgICAgICB9CiAgICAgICAgICAgIC5pZ25vcmVzU2FmZUFyZWEoKQogICAgICAgICAgICAub25DaGFuZ2Uob2Y6IGNvb3JkaW5hdGUpIHsgXywgbmV3IGluCiAgICAgICAgICAgICAgICBpZiBsZXQgbmV3IHsKICAgICAgICAgICAgICAgICAgICBwb3NpdGlvbiA9IC5yZWdpb24oTUtDb29yZGluYXRlUmVnaW9uKGNlbnRlcjogbmV3LCBsYXRpdHVkaW5hbE1ldGVyczogMTAwMCwgbG9uZ2l0dWRpbmFsTWV0ZXJzOiAxMDAwKSkKICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgfQoKICAgICAgICAgICAgVlN0YWNrKHNwYWNpbmc6IDApIHsKICAgICAgICAgICAgICAgIGlmICFzZWFyY2hDb21wbGV0ZXIucmVzdWx0cy5pc0VtcHR5IHsKICAgICAgICAgICAgICAgICAgICBpZiAjYXZhaWxhYmxlKGlPUyAyNiwgKikgewogICAgICAgICAgICAgICAgICAgICAgICBMaXN0KHNlYXJjaENvbXBsZXRlci5yZXN1bHRzLnByZWZpeCg1KSwgaWQ6IFwuc2VsZikgeyByZXN1bHQgaW4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIEJ1dHRvbiB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2VsZWN0U2VhcmNoUmVzdWx0KHJlc3VsdCkKICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0gbGFiZWw6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBWU3RhY2soYWxpZ25tZW50OiAubGVhZGluZywgc3BhY2luZzogMikgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBUZXh0KHJlc3VsdC50aXRsZSkKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC5mb250KC5zdWJoZWFkbGluZSkKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaWYgIXJlc3VsdC5zdWJ0aXRsZS5pc0VtcHR5IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIFRleHQocmVzdWx0LnN1YnRpdGxlKQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC5mb250KC5jYXB0aW9uKQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC5mb3JlZ3JvdW5kU3R5bGUoLnNlY29uZGFyeSkKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICAubGlzdFN0eWxlKC5wbGFpbikKICAgICAgICAgICAgICAgICAgICAgICAgLmZyYW1lKG1heEhlaWdodDogMzUwKQogICAgICAgICAgICAgICAgICAgICAgICAuc2Nyb2xsRGlzYWJsZWQodHJ1ZSkKICAgICAgICAgICAgICAgICAgICAgICAgLmdsYXNzRWZmZWN0KGluOiAucmVjdChjb3JuZXJSYWRpdXM6IDEyKSkKICAgICAgICAgICAgICAgICAgICAgICAgLnBhZGRpbmcoLmhvcml6b250YWwsIDE2KQogICAgICAgICAgICAgICAgICAgICAgICAucGFkZGluZygudG9wLCA4KQogICAgICAgICAgICAgICAgICAgIH0gZWxzZSB7CiAgICAgICAgICAgICAgICAgICAgICAgIExpc3Qoc2VhcmNoQ29tcGxldGVyLnJlc3VsdHMucHJlZml4KDUpLCBpZDogXC5zZWxmKSB7IHJlc3VsdCBpbgogICAgICAgICAgICAgICAgICAgICAgICAgICAgQnV0dG9uIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzZWxlY3RTZWFyY2hSZXN1bHQocmVzdWx0KQogICAgICAgICAgICAgICAgICAgICAgICAgICAgfSBsYWJlbDogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIFZTdGFjayhhbGlnbm1lbnQ6IC5sZWFkaW5nLCBzcGFjaW5nOiAyKSB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIFRleHQocmVzdWx0LnRpdGxlKQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgLmZvbnQoLnN1YmhlYWRsaW5lKQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBpZiAhcmVzdWx0LnN1YnRpdGxlLmlzRW1wdHkgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgVGV4dChyZXN1bHQuc3VidGl0bGUpCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgLmZvbnQoLmNhcHRpb24pCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgLmZvcmVncm91bmRTdHlsZSguc2Vjb25kYXJ5KQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgICAgIC5saXN0U3R5bGUoLnBsYWluKQogICAgICAgICAgICAgICAgICAgICAgICAuZnJhbWUobWF4SGVpZ2h0OiAzNTApCiAgICAgICAgICAgICAgICAgICAgICAgIC5zY3JvbGxEaXNhYmxlZCh0cnVlKQogICAgICAgICAgICAgICAgICAgICAgICAucGFkZGluZyguaG9yaXpvbnRhbCwgMTYpCiAgICAgICAgICAgICAgICAgICAgICAgIC5wYWRkaW5nKC50b3AsIDgpCiAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgfQoKICAgICAgICAgICAgICAgIFNwYWNlcigpCgogICAgICAgICAgICAgICAgLy8gQm90dG9tIGNvbnRyb2xzCiAgICAgICAgICAgICAgICBWU3RhY2soc3BhY2luZzogMTIpIHsKICAgICAgICAgICAgICAgICAgICBpZiBsZXQgY29vcmQgPSBjb29yZGluYXRlIHsKICAgICAgICAgICAgICAgICAgICAgICAgVGV4dChTdHJpbmcoZm9ybWF0OiAiJS42ZiwgJS42ZiIsIGNvb3JkLmxhdGl0dWRlLCBjb29yZC5sb25naXR1ZGUpKQogICAgICAgICAgICAgICAgICAgICAgICAgICAgLmZvbnQoLmZvb3Rub3RlLm1vbm9zcGFjZWQoKSkKICAgICAgICAgICAgICAgICAgICAgICAgICAgIC5mb3JlZ3JvdW5kU3R5bGUoLnNlY29uZGFyeSkKCiAgICAgICAgICAgICAgICAgICAgICAgIEhTdGFjayhzcGFjaW5nOiAxMikgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgLy8gIlN0b3AiIGlzIG9ubHkgc2hvd24gKGFuZCBjYWxsYWJsZSkgb25jZSBzaW11bGF0aW9uIGlzIGFjdGl2ZQogICAgICAgICAgICAgICAgICAgICAgICAgICAgaWYgaXNTaW11bGF0aW5nIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBCdXR0b24oIlN0b3AiLCBhY3Rpb246IGNsZWFyKQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAuYnV0dG9uU3R5bGUoLmJvcmRlcmVkKQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAudGludCgucmVkKQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAuZGlzYWJsZWQoIXBhaXJpbmdFeGlzdHMgfHwgaXNCdXN5KQogICAgICAgICAgICAgICAgICAgICAgICAgICAgfQoKICAgICAgICAgICAgICAgICAgICAgICAgICAgIEJ1dHRvbigiU2ltdWxhdGUgTG9jYXRpb24iLCBhY3Rpb246IHNpbXVsYXRlKQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC5idXR0b25TdHlsZSguYm9yZGVyZWRQcm9taW5lbnQpCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgLmRpc2FibGVkKCFwYWlyaW5nRXhpc3RzIHx8IGlzQnVzeSB8fCBpc1NpbXVsYXRpbmcpCgogICAgICAgICAgICAgICAgICAgICAgICAgICAgQnV0dG9uIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzaG93U2F2ZUJvb2ttYXJrID0gdHJ1ZQogICAgICAgICAgICAgICAgICAgICAgICAgICAgfSBsYWJlbDogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEltYWdlKHN5c3RlbU5hbWU6ICJib29rbWFyayIpCiAgICAgICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAuYnV0dG9uU3R5bGUoLmJvcmRlcmVkKQogICAgICAgICAgICAgICAgICAgICAgICAgICAgLnRpbnQoLmJsdWUpCiAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICB9IGVsc2UgewogICAgICAgICAgICAgICAgICAgICAgICBUZXh0KCJUYXAgbWFwIHRvIGRyb3AgcGluIikKICAgICAgICAgICAgICAgICAgICAgICAgICAgIC5mb250KC5zdWJoZWFkbGluZSkKICAgICAgICAgICAgICAgICAgICAgICAgICAgIC5mb3JlZ3JvdW5kU3R5bGUoLnNlY29uZGFyeSkKICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAucGFkZGluZyguYm90dG9tLCAyNCkKICAgICAgICAgICAgICAgIC5wYWRkaW5nKC5ob3Jpem9udGFsLCAxNikKICAgICAgICAgICAgfQogICAgICAgIH0KICAgICAgICAubmF2aWdhdGlvbkJhclRpdGxlRGlzcGxheU1vZGUoLmlubGluZSkKICAgICAgICAudG9vbGJhciB7CiAgICAgICAgICAgIFRvb2xiYXJJdGVtKHBsYWNlbWVudDogLnRvcEJhckxlYWRpbmcpIHsKICAgICAgICAgICAgICAgIEJ1dHRvbiB7CiAgICAgICAgICAgICAgICAgICAgc2hvd0Jvb2ttYXJrcyA9IHRydWUKICAgICAgICAgICAgICAgIH0gbGFiZWw6IHsKICAgICAgICAgICAgICAgICAgICBJbWFnZShzeXN0ZW1OYW1lOiAiYm9va21hcmsuZmlsbCIpCiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0KICAgICAgICAgICAgVG9vbGJhckl0ZW0ocGxhY2VtZW50OiAudG9wQmFyVHJhaWxpbmcpIHsKICAgICAgICAgICAgICAgIFRleHRGaWVsZCgiU2VhcmNoIGxvY2F0aW9uLi4uIiwgdGV4dDogJHNlYXJjaFRleHQpCiAgICAgICAgICAgICAgICAgICAgLnBhZGRpbmcoLmxlYWRpbmcsIDYpCiAgICAgICAgICAgICAgICAgICAgLmF1dG9jb3JyZWN0aW9uRGlzYWJsZWQoKQogICAgICAgICAgICAgICAgICAgIC5vbkNoYW5nZShvZjogc2VhcmNoVGV4dCkgeyBfLCBuZXdWYWx1ZSBpbgogICAgICAgICAgICAgICAgICAgICAgICBzZWFyY2hDb21wbGV0ZXIudXBkYXRlKHF1ZXJ5OiBuZXdWYWx1ZSkKICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0KICAgICAgICB9CiAgICAgICAgLmFsZXJ0KGFsZXJ0VGl0bGUsIGlzUHJlc2VudGVkOiAkc2hvd0FsZXJ0KSB7CiAgICAgICAgICAgIEJ1dHRvbigiT0siLCByb2xlOiAuY2FuY2VsKSB7IH0KICAgICAgICB9IG1lc3NhZ2U6IHsKICAgICAgICAgICAgVGV4dChhbGVydE1lc3NhZ2UpCiAgICAgICAgfQogICAgICAgIC5hbGVydCgiU2F2ZSBCb29rbWFyayIsIGlzUHJlc2VudGVkOiAkc2hvd1NhdmVCb29rbWFyaykgewogICAgICAgICAgICBUZXh0RmllbGQoIk5hbWUiLCB0ZXh0OiAkbmV3Qm9va21hcmtOYW1lKQogICAgICAgICAgICBCdXR0b24oIlNhdmUiKSB7IGFkZEJvb2ttYXJrKCkgfQogICAgICAgICAgICBCdXR0b24oIkNhbmNlbCIsIHJvbGU6IC5jYW5jZWwpIHsgbmV3Qm9va21hcmtOYW1lID0gIiIgfQogICAgICAgIH0gbWVzc2FnZTogewogICAgICAgICAgICBUZXh0KCJFbnRlciBhIG5hbWUgZm9yIHRoaXMgbG9jYXRpb24uIikKICAgICAgICB9CiAgICAgICAgLnNoZWV0KGlzUHJlc2VudGVkOiAkc2hvd0Jvb2ttYXJrcykgewogICAgICAgICAgICBCb29rbWFya3NWaWV3KGJvb2ttYXJrczogJGJvb2ttYXJrcykgeyBib29rbWFyayBpbgogICAgICAgICAgICAgICAgY29vcmRpbmF0ZSA9IGJvb2ttYXJrLmNvb3JkaW5hdGUKICAgICAgICAgICAgICAgIHNob3dCb29rbWFya3MgPSBmYWxzZQogICAgICAgICAgICB9IG9uRGVsZXRlOiB7IG9mZnNldHMgaW4KICAgICAgICAgICAgICAgIGJvb2ttYXJrcy5yZW1vdmUoYXRPZmZzZXRzOiBvZmZzZXRzKQogICAgICAgICAgICAgICAgc2F2ZUJvb2ttYXJrcygpCiAgICAgICAgICAgIH0KICAgICAgICB9CiAgICAgICAgLm9uQXBwZWFyIHsKICAgICAgICAgICAgbG9hZEJvb2ttYXJrcygpCiAgICAgICAgICAgIGxvYWRDb29yZGluYXRlKCkKICAgICAgICB9CiAgICAgICAgLm9uRGlzYXBwZWFyIHsKICAgICAgICAgICAgc3RvcFJlc2VuZExvb3AoKQogICAgICAgICAgICBpZiBiYWNrZ3JvdW5kVGFza0lEICE9IC5pbnZhbGlkIHsKICAgICAgICAgICAgICAgIEJhY2tncm91bmRMb2NhdGlvbk1hbmFnZXIuc2hhcmVkLnJlcXVlc3RTdG9wKCkKICAgICAgICAgICAgfQogICAgICAgICAgICBlbmRCYWNrZ3JvdW5kVGFzaygpCiAgICAgICAgfQogICAgfQoKICAgIC8vIE1BUks6IC0gQ29vcmRpbmF0ZSBQZXJzaXN0ZW5jZQoKICAgIHByaXZhdGUgZnVuYyBzYXZlQ29vcmRpbmF0ZSgpIHsKICAgICAgICBpZiBsZXQgY29vcmQgPSBjb29yZGluYXRlIHsKICAgICAgICAgICAgVXNlckRlZmF1bHRzLnN0YW5kYXJkLnNldChjb29yZC5sYXRpdHVkZSwgZm9yS2V5OiAicGlubmVkTG9jYXRpb25MYXQiKQogICAgICAgICAgICBVc2VyRGVmYXVsdHMuc3RhbmRhcmQuc2V0KGNvb3JkLmxvbmdpdHVkZSwgZm9yS2V5OiAicGlubmVkTG9jYXRpb25Mb24iKQogICAgICAgICAgICBVc2VyRGVmYXVsdHMuc3RhbmRhcmQuc2V0KHRydWUsIGZvcktleTogImhhc1Bpbm5lZExvY2F0aW9uIikKICAgICAgICB9IGVsc2UgewogICAgICAgICAgICBVc2VyRGVmYXVsdHMuc3RhbmRhcmQuc2V0KGZhbHNlLCBmb3JLZXk6ICJoYXNQaW5uZWRMb2NhdGlvbiIpCiAgICAgICAgfQogICAgfQoKICAgIHByaXZhdGUgZnVuYyBsb2FkQ29vcmRpbmF0ZSgpIHsKICAgICAgICBndWFyZCBVc2VyRGVmYXVsdHMuc3RhbmRhcmQuYm9vbChmb3JLZXk6ICJoYXNQaW5uZWRMb2NhdGlvbiIpIGVsc2UgeyByZXR1cm4gfQogICAgICAgIGxldCBsYXQgPSBVc2VyRGVmYXVsdHMuc3RhbmRhcmQuZG91YmxlKGZvcktleTogInBpbm5lZExvY2F0aW9uTGF0IikKICAgICAgICBsZXQgbG9uID0gVXNlckRlZmF1bHRzLnN0YW5kYXJkLmRvdWJsZShmb3JLZXk6ICJwaW5uZWRMb2NhdGlvbkxvbiIpCiAgICAgICAgLy8gQXZvaWQgdHJpZ2dlcmluZyBkaWRTZXQgLyBzYXZlQ29vcmRpbmF0ZSBkdXJpbmcgbG9hZAogICAgICAgIF9jb29yZGluYXRlLndyYXBwZWRWYWx1ZSA9IENMTG9jYXRpb25Db29yZGluYXRlMkQobGF0aXR1ZGU6IGxhdCwgbG9uZ2l0dWRlOiBsb24pCiAgICAgICAgcG9zaXRpb24gPSAucmVnaW9uKE1LQ29vcmRpbmF0ZVJlZ2lvbigKICAgICAgICAgICAgY2VudGVyOiBDTExvY2F0aW9uQ29vcmRpbmF0ZTJEKGxhdGl0dWRlOiBsYXQsIGxvbmdpdHVkZTogbG9uKSwKICAgICAgICAgICAgbGF0aXR1ZGluYWxNZXRlcnM6IDEwMDAsIGxvbmdpdHVkaW5hbE1ldGVyczogMTAwMAogICAgICAgICkpCiAgICB9CgogICAgLy8gTUFSSzogLSBCb29rbWFya3MKCiAgICBwcml2YXRlIGZ1bmMgbG9hZEJvb2ttYXJrcygpIHsKICAgICAgICBndWFyZCBsZXQgZGF0YSA9IFVzZXJEZWZhdWx0cy5zdGFuZGFyZC5kYXRhKGZvcktleTogImxvY2F0aW9uQm9va21hcmtzIiksCiAgICAgICAgICAgICAgbGV0IGRlY29kZWQgPSB0cnk/IEpTT05EZWNvZGVyKCkuZGVjb2RlKFtMb2NhdGlvbkJvb2ttYXJrXS5zZWxmLCBmcm9tOiBkYXRhKSBlbHNlIHsgcmV0dXJuIH0KICAgICAgICBib29rbWFya3MgPSBkZWNvZGVkCiAgICB9CgogICAgcHJpdmF0ZSBmdW5jIHNhdmVCb29rbWFya3MoKSB7CiAgICAgICAgaWYgbGV0IGRhdGEgPSB0cnk/IEpTT05FbmNvZGVyKCkuZW5jb2RlKGJvb2ttYXJrcykgewogICAgICAgICAgICBVc2VyRGVmYXVsdHMuc3RhbmRhcmQuc2V0KGRhdGEsIGZvcktleTogImxvY2F0aW9uQm9va21hcmtzIikKICAgICAgICB9CiAgICB9CgogICAgcHJpdmF0ZSBmdW5jIGFkZEJvb2ttYXJrKCkgewogICAgICAgIGd1YXJkIGxldCBjb29yZCA9IGNvb3JkaW5hdGUgZWxzZSB7IHJldHVybiB9CiAgICAgICAgbGV0IG5hbWUgPSBuZXdCb29rbWFya05hbWUudHJpbW1pbmdDaGFyYWN0ZXJzKGluOiAud2hpdGVzcGFjZXNBbmROZXdsaW5lcykKICAgICAgICBsZXQgYm9va21hcmsgPSBMb2NhdGlvbkJvb2ttYXJrKAogICAgICAgICAgICBuYW1lOiBuYW1lLmlzRW1wdHkgPyBTdHJpbmcoZm9ybWF0OiAiJS40ZiwgJS40ZiIsIGNvb3JkLmxhdGl0dWRlLCBjb29yZC5sb25naXR1ZGUpIDogbmFtZSwKICAgICAgICAgICAgbGF0aXR1ZGU6IGNvb3JkLmxhdGl0dWRlLAogICAgICAgICAgICBsb25naXR1ZGU6IGNvb3JkLmxvbmdpdHVkZQogICAgICAgICkKICAgICAgICBib29rbWFya3MuYXBwZW5kKGJvb2ttYXJrKQogICAgICAgIHNhdmVCb29rbWFya3MoKQogICAgICAgIG5ld0Jvb2ttYXJrTmFtZSA9ICIiCiAgICB9CgogICAgLy8gTUFSSzogLSBMb2NhdGlvbgoKICAgIHByaXZhdGUgZnVuYyBzZWxlY3RTZWFyY2hSZXN1bHQoXyByZXN1bHQ6IE1LTG9jYWxTZWFyY2hDb21wbGV0aW9uKSB7CiAgICAgICAgc2VhcmNoVGV4dCA9ICIiCiAgICAgICAgc2VhcmNoQ29tcGxldGVyLnJlc3VsdHMgPSBbXQoKICAgICAgICBsZXQgcmVxdWVzdCA9IE1LTG9jYWxTZWFyY2guUmVxdWVzdChjb21wbGV0aW9uOiByZXN1bHQpCiAgICAgICAgTUtMb2NhbFNlYXJjaChyZXF1ZXN0OiByZXF1ZXN0KS5zdGFydCB7IHJlc3BvbnNlLCBfIGluCiAgICAgICAgICAgIGlmIGxldCBpdGVtID0gcmVzcG9uc2U/Lm1hcEl0ZW1zLmZpcnN0IHsKICAgICAgICAgICAgICAgIGNvb3JkaW5hdGUgPSBpdGVtLnBsYWNlbWFyay5jb29yZGluYXRlCiAgICAgICAgICAgIH0KICAgICAgICB9CiAgICB9CgogICAgcHJpdmF0ZSBmdW5jIHNpbXVsYXRlKCkgewogICAgICAgIGd1YXJkIHBhaXJpbmdFeGlzdHMsIGxldCBjb29yZCA9IGNvb3JkaW5hdGUsICFpc0J1c3kgZWxzZSB7IHJldHVybiB9CiAgICAgICAgaXNCdXN5ID0gdHJ1ZQogICAgICAgIGxldCBpcCA9IGRldmljZUlQCiAgICAgICAgbGV0IHBhdGggPSBwYWlyaW5nRmlsZVBhdGgKICAgICAgICBsZXQgbGF0ID0gY29vcmQubGF0aXR1ZGUKICAgICAgICBsZXQgbG9uID0gY29vcmQubG9uZ2l0dWRlCiAgICAgICAgU2VsZi5sb2NhdGlvblF1ZXVlLmFzeW5jIHsKICAgICAgICAgICAgbGV0IGNvZGUgPSBzaW11bGF0ZV9sb2NhdGlvbihpcCwgbGF0LCBsb24sIHBhdGgpCiAgICAgICAgICAgIERpc3BhdGNoUXVldWUubWFpbi5hc3luYyB7CiAgICAgICAgICAgICAgICBpc0J1c3kgPSBmYWxzZQogICAgICAgICAgICAgICAgaWYgY29kZSA9PSAwIHsKICAgICAgICAgICAgICAgICAgICBpc1NpbXVsYXRpbmcgPSB0cnVlCiAgICAgICAgICAgICAgICAgICAgYmVnaW5CYWNrZ3JvdW5kVGFzaygpCiAgICAgICAgICAgICAgICAgICAgc3RhcnRSZXNlbmRMb29wKCkKICAgICAgICAgICAgICAgICAgICBCYWNrZ3JvdW5kTG9jYXRpb25NYW5hZ2VyLnNoYXJlZC5yZXF1ZXN0U3RhcnQoKQogICAgICAgICAgICAgICAgfSBlbHNlIHsKICAgICAgICAgICAgICAgICAgICBhbGVydFRpdGxlID0gIlNpbXVsYXRpb24gRmFpbGVkIgogICAgICAgICAgICAgICAgICAgIGFsZXJ0TWVzc2FnZSA9ICJDb3VsZCBub3Qgc2ltdWxhdGUgbG9jYXRpb24gKGVycm9yIFwoY29kZSkpLiBNYWtlIHN1cmUgdGhlIGRldmljZSBpcyBjb25uZWN0ZWQgYW5kIHRoZSBEREkgaXMgbW91bnRlZC4iCiAgICAgICAgICAgICAgICAgICAgc2hvd0FsZXJ0ID0gdHJ1ZQogICAgICAgICAgICAgICAgfQogICAgICAgICAgICB9CiAgICAgICAgfQogICAgfQoKICAgIHByaXZhdGUgZnVuYyBjbGVhcigpIHsKICAgICAgICBndWFyZCBwYWlyaW5nRXhpc3RzLCAhaXNCdXN5IGVsc2UgeyByZXR1cm4gfQogICAgICAgIGlzQnVzeSA9IHRydWUKICAgICAgICBzdG9wUmVzZW5kTG9vcCgpCiAgICAgICAgU2VsZi5sb2NhdGlvblF1ZXVlLmFzeW5jIHsKICAgICAgICAgICAgbGV0IGNvZGUgPSBjbGVhcl9zaW11bGF0ZWRfbG9jYXRpb24oKQogICAgICAgICAgICBEaXNwYXRjaFF1ZXVlLm1haW4uYXN5bmMgewogICAgICAgICAgICAgICAgaXNCdXN5ID0gZmFsc2UKICAgICAgICAgICAgICAgIGlmIGNvZGUgPT0gMCB7CiAgICAgICAgICAgICAgICAgICAgaXNTaW11bGF0aW5nID0gZmFsc2UKICAgICAgICAgICAgICAgICAgICBjb29yZGluYXRlID0gbmlsCiAgICAgICAgICAgICAgICAgICAgZW5kQmFja2dyb3VuZFRhc2soKQogICAgICAgICAgICAgICAgICAgIEJhY2tncm91bmRMb2NhdGlvbk1hbmFnZXIuc2hhcmVkLnJlcXVlc3RTdG9wKCkKICAgICAgICAgICAgICAgIH0gZWxzZSB7CiAgICAgICAgICAgICAgICAgICAgYWxlcnRUaXRsZSA9ICJDbGVhciBGYWlsZWQiCiAgICAgICAgICAgICAgICAgICAgYWxlcnRNZXNzYWdlID0gIkNvdWxkIG5vdCBjbGVhciBzaW11bGF0ZWQgbG9jYXRpb24gKGVycm9yIFwoY29kZSkpLiIKICAgICAgICAgICAgICAgICAgICBzaG93QWxlcnQgPSB0cnVlCiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0KICAgICAgICB9CiAgICB9CgogICAgcHJpdmF0ZSBmdW5jIGJlZ2luQmFja2dyb3VuZFRhc2soKSB7CiAgICAgICAgZ3VhcmQgYmFja2dyb3VuZFRhc2tJRCA9PSAuaW52YWxpZCBlbHNlIHsgcmV0dXJuIH0KICAgICAgICBiYWNrZ3JvdW5kVGFza0lEID0gVUlBcHBsaWNhdGlvbi5zaGFyZWQuYmVnaW5CYWNrZ3JvdW5kVGFzayB7IGVuZEJhY2tncm91bmRUYXNrKCkgfQogICAgfQoKICAgIHByaXZhdGUgZnVuYyBlbmRCYWNrZ3JvdW5kVGFzaygpIHsKICAgICAgICBndWFyZCBiYWNrZ3JvdW5kVGFza0lEICE9IC5pbnZhbGlkIGVsc2UgeyByZXR1cm4gfQogICAgICAgIFVJQXBwbGljYXRpb24uc2hhcmVkLmVuZEJhY2tncm91bmRUYXNrKGJhY2tncm91bmRUYXNrSUQpCiAgICAgICAgYmFja2dyb3VuZFRhc2tJRCA9IC5pbnZhbGlkCiAgICB9CgogICAgcHJpdmF0ZSBmdW5jIHN0YXJ0UmVzZW5kTG9vcCgpIHsKICAgICAgICByZXNlbmRUaW1lcj8uaW52YWxpZGF0ZSgpCiAgICAgICAgcmVzZW5kVGltZXIgPSBUaW1lci5zY2hlZHVsZWRUaW1lcih3aXRoVGltZUludGVydmFsOiA0LCByZXBlYXRzOiB0cnVlKSB7IF8gaW4KICAgICAgICAgICAgZ3VhcmQgbGV0IGNvb3JkID0gY29vcmRpbmF0ZSBlbHNlIHsgcmV0dXJuIH0KICAgICAgICAgICAgbGV0IGlwID0gZGV2aWNlSVAKICAgICAgICAgICAgbGV0IHBhdGggPSBwYWlyaW5nRmlsZVBhdGgKICAgICAgICAgICAgbGV0IGxhdCA9IGNvb3JkLmxhdGl0dWRlCiAgICAgICAgICAgIGxldCBsb24gPSBjb29yZC5sb25naXR1ZGUKICAgICAgICAgICAgU2VsZi5sb2NhdGlvblF1ZXVlLmFzeW5jIHsKICAgICAgICAgICAgICAgIF8gPSBzaW11bGF0ZV9sb2NhdGlvbihpcCwgbGF0LCBsb24sIHBhdGgpCiAgICAgICAgICAgIH0KICAgICAgICB9CiAgICB9CgogICAgcHJpdmF0ZSBmdW5jIHN0b3BSZXNlbmRMb29wKCkgewogICAgICAgIHJlc2VuZFRpbWVyPy5pbnZhbGlkYXRlKCkKICAgICAgICByZXNlbmRUaW1lciA9IG5pbAogICAgfQp9CgovLyBNQVJLOiAtIEJvb2ttYXJrcyBTaGVldAoKc3RydWN0IEJvb2ttYXJrc1ZpZXc6IFZpZXcgewogICAgQEJpbmRpbmcgdmFyIGJvb2ttYXJrczogW0xvY2F0aW9uQm9va21hcmtdCiAgICBsZXQgb25TZWxlY3Q6IChMb2NhdGlvbkJvb2ttYXJrKSAtPiBWb2lkCiAgICBsZXQgb25EZWxldGU6IChJbmRleFNldCkgLT4gVm9pZAoKICAgIHZhciBib2R5OiBzb21lIFZpZXcgewogICAgICAgIE5hdmlnYXRpb25TdGFjayB7CiAgICAgICAgICAgIEdyb3VwIHsKICAgICAgICAgICAgICAgIGlmIGJvb2ttYXJrcy5pc0VtcHR5IHsKICAgICAgICAgICAgICAgICAgICBDb250ZW50VW5hdmFpbGFibGVWaWV3KAogICAgICAgICAgICAgICAgICAgICAgICAiTm8gQm9va21hcmtzIiwKICAgICAgICAgICAgICAgICAgICAgICAgc3lzdGVtSW1hZ2U6ICJib29rbWFyay5zbGFzaCIsCiAgICAgICAgICAgICAgICAgICAgICAgIGRlc2NyaXB0aW9uOiBUZXh0KCJEcm9wIGEgcGluIG9uIHRoZSBtYXAgYW5kIHRhcCB0aGUgYm9va21hcmsgaWNvbiB0byBzYXZlIGEgbG9jYXRpb24uIikKICAgICAgICAgICAgICAgICAgICApCiAgICAgICAgICAgICAgICB9IGVsc2UgewogICAgICAgICAgICAgICAgICAgIExpc3QgewogICAgICAgICAgICAgICAgICAgICAgICBGb3JFYWNoKGJvb2ttYXJrcykgeyBib29rbWFyayBpbgogICAgICAgICAgICAgICAgICAgICAgICAgICAgQnV0dG9uIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBvblNlbGVjdChib29rbWFyaykKICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0gbGFiZWw6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBWU3RhY2soYWxpZ25tZW50OiAubGVhZGluZywgc3BhY2luZzogMikgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBUZXh0KGJvb2ttYXJrLm5hbWUpCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAuZm9yZWdyb3VuZFN0eWxlKC5wcmltYXJ5KQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBUZXh0KFN0cmluZyhmb3JtYXQ6ICIlLjZmLCAlLjZmIiwgYm9va21hcmsubGF0aXR1ZGUsIGJvb2ttYXJrLmxvbmdpdHVkZSkpCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAuZm9udCguY2FwdGlvbi5tb25vc3BhY2VkKCkpCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAuZm9yZWdyb3VuZFN0eWxlKC5zZWNvbmRhcnkpCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgICAgIC5vbkRlbGV0ZShwZXJmb3JtOiBvbkRlbGV0ZSkKICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0KICAgICAgICAgICAgLm5hdmlnYXRpb25UaXRsZSgiQm9va21hcmtzIikKICAgICAgICAgICAgLm5hdmlnYXRpb25CYXJUaXRsZURpc3BsYXlNb2RlKC5pbmxpbmUpCiAgICAgICAgICAgIC50b29sYmFyIHsKICAgICAgICAgICAgICAgIGlmICFib29rbWFya3MuaXNFbXB0eSB7CiAgICAgICAgICAgICAgICAgICAgRWRpdEJ1dHRvbigpCiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0KICAgICAgICB9CiAgICB9Cn0K \ No newline at end of file