Eine aufbereitete Darstellung der Quelle

 
     
 
 
Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 

Benutzer

Quelle  UIWindowSecureTests.swift   Sprache: JAVA

 

import ObjectiveC
import Testing
import UIKit
@testable import ThreemaFramework

@MainActor @Suite(.serialized)
struct UIWindowSecureTests {

    @Test(
        "A secured window outlives every view of its secure field",
        arguments: [true, false]
    )
    func windowOutlivesSecureFieldViews(showsPreventionView: Bool) async throws {
        let windowScene = try #require(Self.windowScene)
        let tracker = LifetimeTracker()
        weak var lifetime: UIWindow.WindowLifetime?

        autoreleasepool {
            let window = Self.makeSecuredWindow(
                windowScene: windowScene,
                showsPreventionView: showsPreventionView,
                tracker: tracker
            )
            let field = window.secureField
            #expect(Self.layer(window.layer, isInside: field?.layer))

            // Same teardown as `PasscodeCoordinator.reportSuccess()` and `CallCoordinator.finish()`
            window.isHidden = true
            window.removeSecure()

            #expect(window.secureField == nil)
            #expect(!Self.layer(window.layer, isInside: field?.layer))
            #expect(field?.superview == nil)
            #expect(field?.layer.superlayer == nil)

            lifetime = Self.expectWindowLifetime(of: window, onEveryViewOf: field, tracker: tracker)
        }

        // The lifetime must go away with the views (possibly already with the pool), otherwise the fix leaks the window
        let isAllReleased = try await Self.waitUntil { tracker.liveViewCount == 0 && lifetime == nil }
        #expect(isAllReleased, "Live views: \(tracker.liveViewNames), lifetime released: \(lifetime == nil)")
        #expect(tracker.viewsDeallocatedAfterWindow.isEmpty)
    }

    enum LateReleasedView: String, CaseIterable, CustomTestStringConvertible {
        /// The view that crashed in `-[UITextField dealloc]`
        case secureField
        /// The `ScreenshotPreventionView` host, deallocated through `_UIHostingView.deinit`
        case preventionView

        var testDescription: String {
            rawValue
        }
    }

    @Test(
        "A view of the secure field released after the window keeps the window alive",
        arguments: LateReleasedView.allCases
    )
    func lateReleasedViewKeepsWindowAlive(_ lateReleased: LateReleasedView) async throws {
        let windowScene = try #require(Self.windowScene)
        let tracker = LifetimeTracker()

        // Stands in for the Core Animation transaction that holds the detached field until it commits
        var lateReleasedView: UIView?
        weak var weakField: UITextField?
        weak var lifetime: UIWindow.WindowLifetime?

        autoreleasepool {
            let window = Self.makeSecuredWindow(
                windowScene: windowScene,
                showsPreventionView: true,
                tracker: tracker
            )
            let field = window.secureField
            weakField = field
            switch lateReleased {
            case .secureField:
                lateReleasedView = field
            case .preventionView:
                lateReleasedView = field?.leftView
            }

            window.isHidden = true
            window.removeSecure()

            lifetime = Self.expectWindowLifetime(of: window, onEveryViewOf: field, tracker: tracker)
        }

        try #require(lateReleasedView != nil)
        try #require(lifetime != nil)

        // Precondition: everything except the late view (and what it retains) is gone
        if lateReleased == .preventionView {
            let isFieldReleased = try await Self.waitUntil { weakField == nil }
            try #require(isFieldReleased, "Field still alive, live views: \(tracker.liveViewNames)")
        }
        try await Self.drainMainRunLoop()

        // The late view alone keeps the lifetime, and with it the window
        #expect(lifetime != nil)
        #expect(!tracker.isWindowDeallocated)

        lateReleasedView = nil

        let isAllReleased = try await Self.waitUntil { tracker.liveViewCount == 0 && lifetime == nil }
        #expect(isAllReleased, "Live views: \(tracker.liveViewNames), lifetime released: \(lifetime == nil)")
        #expect(tracker.viewsDeallocatedAfterWindow.isEmpty)
    }

    @Test("Removing twice or without securing first does nothing")
    func removeSecureIsIdempotent() throws {
        let windowScene = try #require(Self.windowScene)
        let window = UIWindow(windowScene: windowScene)

        window.removeSecure()
        #expect(window.secureField == nil)

        window.installSecureField(showsPreventionView: false)
        #expect(window.secureField != nil)

        window.removeSecure()
        window.removeSecure()
        #expect(window.secureField == nil)
        #expect(window.subviews.isEmpty)
    }

    // MARK: - Helpers

    private static var windowScene: UIWindowScene? {
        UIApplication.shared.connectedScenes.lazy.compactMap { $0 as? UIWindowScene }.first
    }

    /// Mirrors `PasscodeWindow`: secured in init, then shown.
    private static func makeSecuredWindow(
        windowScene: UIWindowScene,
        showsPreventionView: Bool,
        tracker: LifetimeTracker
    ) -> UIWindow {
        let window = UIWindow(windowScene: windowScene)
        window.installSecureField(showsPreventionView: showsPreventionView)
        window.rootViewController = UIViewController()
        window.isHidden = false
        window.layoutIfNeeded()
        window.secureField?.layoutIfNeeded()

        tracker.track(window)
        if let field = window.secureField {
            tracker.track(subtreeOf: field)
            if let leftView = field.leftView, leftView.superview !== field {
                tracker.track(subtreeOf: leftView)
            }
        }

        return window
    }

    /// Expects every tracked view still attached to `field` to hold the same `WindowLifetime` of `window`.
    private static func expectWindowLifetime(
        of window: UIWindow,
        onEveryViewOf field: UITextField?,
        tracker: LifetimeTracker,
        sourceLocation: SourceLocation = #_sourceLocation
    ) -> UIWindow.WindowLifetime? {
        guard let field else {
            Issue.record("No secure field", sourceLocation: sourceLocation)
            return nil
        }

        let lifetime = UIWindow.windowLifetime(of: field)
        #expect(lifetime?.window === window, sourceLocation: sourceLocation)

        // A lifetime on the window or its own views would be a retain cycle that leaks the window
        let windowSideViews = Self.subtree(of: window)
        for view in windowSideViews where UIWindow.windowLifetime(of: view) != nil {
            Issue.record("\(type(of: view)) of the window keeps the window alive", sourceLocation: sourceLocation)
        }

        let attachedViews = tracker.liveViews.filter { view in
            view.isDescendant(of: field) || field.leftView.map { view.isDescendant(of: $0) } == true
        }
        for view in attachedViews {
            #expect(
                UIWindow.windowLifetime(of: view) === lifetime,
                "\(type(of: view)) does not keep the window alive",
                sourceLocation: sourceLocation
            )
        }

        return lifetime
    }

    /// `view` and all its real descendants.
    private static func subtree(of view: UIView) -> [UIView] {
        [view] + view.subviews.filter { $0.superview === view }.flatMap(subtree(of:))
    }

    private static func layer(_ layer: CALayer, isInside ancestor: CALayer?) -> Bool {
        var current = layer.superlayer
        // `makeSecure()` creates a layer cycle if the window's layer has no superlayer yet
        while let superlayer = current, superlayer !== layer {
            if superlayer === ancestor {
                return true
            }
            current = superlayer.superlayer
        }
        return false
    }

    /// Lets transactions commit, autorelease pools drain and main queue blocks run.
    private static func drainMainRunLoop() async throws {
        for _ in 0..<5 {
            try await Task.sleep(for: .milliseconds(50))
        }
    }

    /// Polls `condition` on the main run loop. Returns whether it became true within `timeout`.
    private static func waitUntil(
        timeout: Duration = .seconds(5),
        _ condition: () -> Bool
    ) async throws -> Bool {
        let clock = ContinuousClock()
        let deadline = clock.now.advanced(by: timeout)
        while !condition() {
            guard clock.now < deadline else {
                return false
            }
            try await Task.sleep(for: .milliseconds(20))
        }
        return true
    }
}

// MARK: - LifetimeTracker

/// Records whether a window was still alive when each tracked view finished deallocating.
@MainActor
private final class LifetimeTracker {
    private weak var window: UIWindow?
    private var trackedViews = Set<ObjectIdentifier>()
    private var liveViewNamesByID: [ObjectIdentifier: String] = [:]
    private let weakViews = NSHashTable<UIView>.weakObjects()

    private(set) var viewsDeallocatedAfterWindow: [String] = []

    var liveViewCount: Int {
        liveViewNamesByID.count
    }

    var liveViewNames: [String] {
        liveViewNamesByID.values.sorted()
    }

    var liveViews: [UIView] {
        weakViews.allObjects
    }

    var isWindowDeallocated: Bool {
        window == nil
    }

    func track(_ window: UIWindow) {
        self.window = window
    }

    func track(subtreeOf view: UIView) {
        let id = ObjectIdentifier(view)
        guard trackedViews.insert(id).inserted else {
            return
        }

        let name = String(describing: type(of: view))
        liveViewNamesByID[id] = name
        weakViews.add(view)
        let probe = DeallocProbe { [weak self] in
            guard let self else {
                return
            }
            liveViewNamesByID[id] = nil
            if window == nil {
                viewsDeallocatedAfterWindow.append(name)
            }
        }
        objc_setAssociatedObject(view, &DeallocProbe.key, probe, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)

        // While secured the window's layer sits inside the field, so `subviews` (derived from the layer tree) also
        // lists the window. Only follow real children.
        view.subviews.filter { $0.superview === view }.forEach(track(subtreeOf:))
    }
}

/// Runs a closure when its owner releases its associated objects, which is after the owner's `dealloc` ran.
private final class DeallocProbe {
    nonisolated(unsafe) static var key: UInt8 = 0

    private let onDeinit: @MainActor () -> Void

    init(onDeinit: @escaping @MainActor () -> Void) {
        self.onDeinit = onDeinit
    }

    deinit {
        MainActor.assumeIsolated {
            onDeinit()
        }
    }
}

Messung
CHZG
849610093

¤ Dauer der Verarbeitung: 0.1 Sekunden  (vorverarbeitet am  2026-11-04) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover


Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Open Source Software

     Quellcodebibliothek
     Eigene Quellcodes
     Fremde Quellcodes
     Suchen

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Statistik
#Sources=1104842
#Domains=499168