From 2a5e530af2d8e1ceb64ac384bcef0ba5ca139930 Mon Sep 17 00:00:00 2001 From: Fuwn Date: Wed, 18 Feb 2026 15:55:25 -0800 Subject: fix: prevent thumbnail grid launch crash from column and data drift --- Sora/Views/Shared/ThumbnailGridView.swift | 21 ++++++++++++++++--- SoraTests/ViewDerivedDataTests.swift | 34 +++++++++++++++++-------------- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/Sora/Views/Shared/ThumbnailGridView.swift b/Sora/Views/Shared/ThumbnailGridView.swift index a7130c2..ec8cd10 100644 --- a/Sora/Views/Shared/ThumbnailGridView.swift +++ b/Sora/Views/Shared/ThumbnailGridView.swift @@ -8,12 +8,27 @@ struct ThumbnailGridView: View { let columnsData: [[Item]] let content: (Item) -> Content + private var resolvedColumnCount: Int { + max(columnCount, 1) + } + + private var resolvedColumnsData: [[Item]] { + let clippedColumnsData = Array(columnsData.prefix(resolvedColumnCount)) + let missingColumnCount = resolvedColumnCount - clippedColumnsData.count + + if missingColumnCount <= 0 { + return clippedColumnsData + } + + return clippedColumnsData + Array(repeating: [], count: missingColumnCount) + } + var body: some View { if useAlternativeGrid { HStack(alignment: .top) { - ForEach(0..: View { content(item) .id(item.id) } - .gridStyle(columns: columnCount) + .gridStyle(columns: resolvedColumnCount) .transaction { $0.animation = nil } #if os(macOS) .padding(8) diff --git a/SoraTests/ViewDerivedDataTests.swift b/SoraTests/ViewDerivedDataTests.swift index d8760e7..bc3a998 100644 --- a/SoraTests/ViewDerivedDataTests.swift +++ b/SoraTests/ViewDerivedDataTests.swift @@ -267,28 +267,32 @@ final class ViewDerivedDataTests: XCTestCase { ) } - private func referenceCount(for symbol: String, in source: String) -> Int { - let totalMatches = tokenCount( - matching: #"\b\#(symbol)\b"#, - in: source + func testThumbnailGridViewAvoidsDirectColumnArrayIndexing() throws { + let source = try loadSource(at: "Sora/Views/Shared/ThumbnailGridView.swift") + let normalizedSource = strippingCommentsAndStrings(from: source) + let directColumnIndexingCount = tokenCount( + matching: #"\bcolumnsData\s*\[\s*columnIndex\s*\]"#, + in: normalizedSource ) - let declarationMatches = tokenCount( - matching: #"\b(?:var|let)\s+\#(symbol)\b"#, - in: source + + // swiftlint:disable:next prefer_nimble + XCTAssertEqual( + directColumnIndexingCount, + 0, + "Thumbnail grid should avoid direct column indexing that can crash when column and data counts drift." ) + } + + private func referenceCount(for symbol: String, in source: String) -> Int { + let totalMatches = tokenCount(matching: #"\b\#(symbol)\b"#, in: source) + let declarationMatches = tokenCount(matching: #"\b(?:var|let)\s+\#(symbol)\b"#, in: source) return max(0, totalMatches - declarationMatches) } private func invocationCount(forFunction name: String, in source: String) -> Int { - let totalMatches = tokenCount( - matching: #"\b\#(name)\s*\("#, - in: source - ) - let declarationMatches = tokenCount( - matching: #"\bfunc\s+\#(name)\s*\("#, - in: source - ) + let totalMatches = tokenCount(matching: #"\b\#(name)\s*\("#, in: source) + let declarationMatches = tokenCount(matching: #"\bfunc\s+\#(name)\s*\("#, in: source) return max(0, totalMatches - declarationMatches) } -- cgit v1.2.3