雲端 Mac 上的 iOS 整數溢位測試:容量計算與無條件進位
下載模組會依「區塊數 × 區塊大小」預留容量,影像處理則依「列數 × 每列位元組數」配置緩衝區。這些運算平時看似正常,遇到損毀的檔案或異常的介面資料時,卻可能先發生整數溢位,進而導致應用程式崩潰。在雲端 Mac 上,可以將純算術邏輯抽成一支可執行的 Swift 檢查程式,不必啟動模擬器,也能為 iOS 專案建立穩定的邊界回歸測試。
先為容量計算訂定契約
本文只處理非負整數容量,不涉及浮點近似值。先約定三條規則:負數視為無效輸入;超出 Int 範圍時回報溢位錯誤;無條件進位除法的除數必須大於零。零容量合法,但零除數不合法。
不要先轉成無號型別再驗證。將負數直接轉成無號整數可能觸發陷阱而終止程式;採用截斷轉換,又可能將它變成極大的正數。若輸入來自字串,也應先確認能成功解析為 Int,不能在解析失敗時預設為零。
算術結果能以整數型別表示,不代表就能配置相應容量。整數範圍檢查與業務配額檢查是兩道獨立關卡,必須全部通過,才能配置資源。
請分別命名「數量」、「單項位元組數」與「總位元組數」,避免混用不同單位。錯誤也應包含發生問題的運算階段,但不要將原始敏感輸入整段寫入日誌。
使用可回報溢位的運算,避免回繞
一般的 +、* 在溢位時會觸發執行階段錯誤;&+、&* 則明確採用回繞運算,可能將超大容量變成小數值。容量驗證需要的是能回報溢位的運算:同時取得結果與溢位旗標,先檢查旗標,再使用結果。
將以下程式碼儲存為 arithmetic.swift。它不依賴應用程式專案,也不會實際配置大量記憶體。
enum CapacityError: Error, Equatable {
case invalidInput
case overflow
}
func checkedAdd(_ a: Int, _ b: Int) throws -> Int {
guard a >= 0, b >= 0 else {
throw CapacityError.invalidInput
}
let result = a.addingReportingOverflow(b)
guard !result.overflow else {
throw CapacityError.overflow
}
return result.partialValue
}
func checkedMultiply(_ a: Int, _ b: Int) throws -> Int {
guard a >= 0, b >= 0 else {
throw CapacityError.invalidInput
}
let result = a.multipliedReportingOverflow(by: b)
guard !result.overflow else {
throw CapacityError.overflow
}
return result.partialValue
}
func ceilingDivide(_ n: Int, by d: Int) throws -> Int {
guard n >= 0, d > 0 else {
throw CapacityError.invalidInput
}
let q = n / d
return n % d == 0 ? q : try checkedAdd(q, 1)
}
func require(_ condition: Bool) {
guard condition else {
fatalError("Boundary check failed")
}
}
func expectError(
_ expected: CapacityError,
_ operation: () throws -> Int
) {
do {
_ = try operation()
fatalError("Expected capacity error")
} catch let error as CapacityError {
require(error == expected)
} catch {
fatalError("Unexpected error type")
}
}
require(try checkedAdd(Int.max, 0) == Int.max)
require(try checkedMultiply(Int.max, 1) == Int.max)
require(try checkedMultiply(0, Int.max) == 0)
require(try ceilingDivide(0, by: 4) == 0)
require(try ceilingDivide(8, by: 4) == 2)
require(try ceilingDivide(9, by: 4) == 3)
require(try ceilingDivide(Int.max, by: 1) == Int.max)
require(try ceilingDivide(Int.max, by: 2) == Int.max / 2 + 1)
expectError(.overflow) {
try checkedAdd(Int.max, 1)
}
expectError(.overflow) {
try checkedMultiply(Int.max, 2)
}
expectError(.invalidInput) {
try checkedMultiply(-1, 0)
}
expectError(.invalidInput) {
try ceilingDivide(1, by: 0)
}
expectError(.invalidInput) {
try ceilingDivide(Int.min, by: -1)
}
這裡刻意以負數乘以零來測試驗證順序。若實作先走「乘數為零就回傳零」的捷徑,無效輸入就會繞過契約。測試必須檢查錯誤型別,不能只確認「曾經發生某個錯誤」。
無條件進位除法不要先加除數
計算區塊數時,常見的寫法是 (n + d - 1) / d。數學上成立,不代表在固定位元寬度的整數運算中也安全:即使最後的商很小,前面的加法仍可能超出範圍。
範例先計算商與餘數,只有餘數不為零時才加一。將輸入限制為非負數、除數限制為正數後,也能避免有號整數最小值除以負一所造成的特殊溢位情況。
測試至少應涵蓋零、整除、餘一、最大整數與零除數。不要將 Int.max 寫死成某個十進位常數;直接使用型別提供的邊界值,才能檢查實際編譯目標的整數寬度,而不是依賴開發者印象中的平台假設。
在兩種編譯模式下執行
雲端 Mac 必須已選定開發工具鏈。先確認編譯器可用,再分別編譯並執行:
set -eu
xcrun swiftc --version
mkdir -p .arithmetic-check
xcrun swiftc -Onone arithmetic.swift \
-o .arithmetic-check/debug
.arithmetic-check/debug
xcrun swiftc -O arithmetic.swift \
-o .arithmetic-check/optimized
.arithmetic-check/optimized
成功時,程式不會輸出任何訊息便結束;任何斷言失敗或未處理的錯誤,都會使這個步驟失敗。不要透過管線,讓日誌工具的成功狀態掩蓋程式的失敗結束碼。
範例沒有使用 assert,因為最佳化建置可能會將它移除。自訂的 require 使用明確的條件判斷與 fatalError,在這裡採用的兩種模式下都會保留檢查。也不要額外啟用會削弱執行階段安全檢查的非標準最佳化設定。
整合至應用程式時,再檢查資源上限
這支獨立程式是算術契約的最小驗證。正式整合時,應讓應用程式與測試引用同一份正式環境實作,而不是長期維護兩份相似的函式。
請依照以下順序檢查呼叫鏈:
- 原始輸入解析失敗時,立即回傳錯誤,不要使用預設容量繼續執行。
- 確認各項單位一致後,再進行有溢位檢查的乘加運算,避免中間運算式先溢位。
- 將結果與產品定義的單次配額比較,再決定是否配置資源。
- 轉換為位元寬度更小的整數型別時,使用可失敗的精確轉換,拒絕截斷。
- 驗證失敗路徑沒有建立檔案、配置緩衝區或提交後續工作。
最後,再加入一個「算術合法但超過業務配額」的案例,確認它與溢位錯誤分開處理。如此一來,追查失敗原因時,就能明確區分輸入無效、超出型別可表示範圍,以及資源策略拒絕,而不是將所有邊界問題都歸因於記憶體不足。
常見問題
容量計算溢位後可以直接回傳 Int.max 嗎?
不建議預設如此處理。飽和值會混淆合法的大容量與失敗結果,也可能導致後續配置異常。應回傳明確錯誤,只有業務契約要求飽和運算時才採用。
這套檢查需要啟動 iOS 模擬器嗎?
範例僅使用 Swift 標準函式庫,可直接在雲端 Mac 上編譯執行。它驗證的是算術契約;介面、平台專屬型別與實際記憶體配置仍須另外測試。
將建置工作交給專屬雲端 Mac
選擇 Apple Silicon 物理節點,獨享整台機器,使用原生 macOS 圖形介面與命令列。機型、租用週期與節點,請以訂單確認內容為準。