在雲端 Mac 驗收 iOS CSV 匯出:欄位跳脫與危險值攔截

在雲端 Mac 驗收 iOS CSV 匯出:欄位跳脫與危險值攔截

團隊把 iOS App 中的紀錄匯出成 CSV,放到遠端建置節點執行測試。單元測試顯示「檔案寫入成功」,同事開啟檔案後卻發現備註被拆成兩行,另一筆使用者輸入則變成試算表公式。這裡要驗收的不是分享按鈕,而是匯出的位元組、欄位邊界,以及危險輸入的處理結果。雲端 Mac 適合把這些不需人工點選的檢查固定在每次建置流程中。

先訂清楚匯出規格

本文採用明確的文字規格:輸出 UTF-8、不含位元組順序標記;每個欄位都以雙引號包住,欄位內的雙引號則寫成兩個;紀錄之間與檔案結尾都使用 CRLF。欄位內的換行保留原樣,不能先按換行符號拆開整份 CSV 再計算紀錄數。

另訂一條安全規則:自由輸入欄位去除前後空白及控制字元後,若以 =、+、- 或 @ 開頭,就讓匯出失敗。這也會拒絕某些原本可視為一般文字的負數;若產品確實需要數值欄,應先依欄位定義解析並驗證數值,而不是放寬所有文字欄位。表頭也使用同一個編碼函式,避免維護兩套跳脫邏輯。

CSV 的引號處理的是欄位分隔,不是試算表對公式的解讀。在文字前加上單引號,不同匯入工具未必會得到相同結果;拒絕危險輸入較容易定義與測試,但仍須核對目標匯入工具的行為。

用單一函式產生可檢查的位元組

將編碼邏輯放在 App 與測試目標都能引用的 Swift 檔案中。以下函式接收二維字串陣列;實際應用可先把業務模型轉成指定順序的欄位。trimmingCharacters 只用於偵測風險,輸出時仍保留原值,不會悄悄修改使用者資料。

import Foundation

enum CSVExportError: Error {
    case unsafeValue
}

func csvField(_ value: String) throws -> String {
    let ignored = CharacterSet.whitespacesAndNewlines
        .union(.controlCharacters)
    let probe = value.trimmingCharacters(in: ignored)

    if let first = probe.first, "=+-@".contains(first) {
        throw CSVExportError.unsafeValue
    }

    let escaped = value.replacingOccurrences(of: "\"", with: "\"\"")
    return "\"" + escaped + "\""
}

func csvDocument(_ rows: [[String]]) throws -> Data {
    let lines = try rows.map { row in
        try row.map(csvField).joined(separator: ",")
    }
    return Data((lines.joined(separator: "\r\n") + "\r\n").utf8)
}

呼叫端還應檢查每列的欄位數是否符合表頭、決定空資料集是只匯出表頭還是提示使用者,並將錯誤轉成可見的提示。不要捕捉 unsafeValue 後改為輸出未經檢查的原文;那會讓測試通過,實際分享的檔案卻繞過規則。若匯出內容包含敏感資料,也應在分享前確認目標檔案及其存取權限。

同時測試正常路徑與拒絕路徑

測試應比較實際輸出,而不只是斷言 Data 非空。第一個範例涵蓋逗號、引號、中文及欄位內換行;第二個範例確認前置定位字元無法繞過檢查。

import XCTest

final class CSVExportTests: XCTestCase {
    func testQuotingAndLineEndings() throws {
        let data = try csvDocument([
            ["name", "note"],
            ["张三", "甲,\"乙\"\n丙"]
        ])

        XCTAssertEqual(
            String(decoding: data, as: UTF8.self),
            "\"name\",\"note\"\r\n\"张三\",\"甲,\"\"乙\"\"\n丙\"\r\n"
        )
    }

    func testFormulaLikeTextIsRejected() {
        for value in ["=1+1", "\t@SUM(A1:A2)", "+1", " -2"] {
            XCTAssertThrowsError(try csvDocument([[value]]))
        }
    }
}

這組測試證明輸出符合約定格式,也驗證了拒絕策略;但它不能證明每種試算表都會如預期匯入。尤其是引號內的換行、外觀像日期的文字,以及非 ASCII 字元,仍須用實際交付對象採用的匯入方式驗收。

在建置節點留下可複查的結果

先在雲端 Mac 確認 Xcode 命令列工具可用,再使用專案中已共享的 scheme 執行測試。將範例名稱換成儲存庫中的實際名稱;執行前可用 xcodebuild -list -project App.xcodeproj 核對入口。測試目標應能直接存取編碼檔案,不要為測試另複製一份實作。

xcodebuild test \
  -project App.xcodeproj \
  -scheme App \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -only-testing:AppTests/CSVExportTests \
  -resultBundlePath "$PWD/CSVExport.xcresult"

模擬器名稱須與該節點已安裝的執行階段相符,可先執行 xcrun simctl list devices available 檢查。執行前,CSVExport.xcresult 指定的路徑不得已存在;重複執行時,應為每次作業指定獨立目錄,避免舊結果干擾。這項測試只驗證純 Swift 編碼,不依賴圖形介面。若還要驗收分享流程,應另行為 UI 測試準備工作階段與權限條件。

從測試資料檢查到實際檔案

單元測試通過後,透過 App 實際的匯出路徑產生一份固定樣本,至少包含空欄位、逗號、雙引號、欄位內換行及中文。確認檔案確實由同一個 csvDocument 產生,而不是由分享層再次拼接字串。儲存到 App 可寫入的位置時,可使用 Data.write(to:options: .atomic),避免讀到寫入一半的檔案;分享失敗後如何清理暫存檔,也應依 App 流程測試。

不要只靠終端機顯示的「行數」判斷是否成功:引號內原本就可能有換行。更穩妥的做法是依樣本逐欄匯入比對,並檢查欄位順序、最後一列及錯誤提示。若匯入工具自行把編號轉成數字,或把日期文字轉成日期,那是另一項格式規格,需要透過明確的欄位型別或專用交換格式解決,不能假設 CSV 引號能固定儲存格型別。

合併前的檢查清單

CSV 匯出最難察覺的問題,往往不在一般範例,而在某筆真實使用者的備註。先把邊界輸入變成固定測試,再檢查最終檔案,遠比檔案產生後才靠肉眼排錯可靠。

常見問題

欄位已用雙引號包住,內部引號還需要跳脫嗎?

需要。將欄位內每個雙引號寫成兩個雙引號,再以雙引號包住整個欄位。

在欄位前加單引號,能保證試算表不執行公式嗎?

不能。匯入器的處理方式不同;本文採用拒絕危險文字的策略,正式流程仍須以目標匯入器測試。

UDPMac 雲端 Mac

將下一次 Xcode 建置作業移至獨享實體主機。

選擇 Apple Silicon 機型、節點與租期,透過遠端連線執行自己的建置流程。實際可用情況以訂購時控制台顯示為準。

查看雲端 Mac 租用方案