Phase 1.12:LocalVersionReader 读 version.xml + ATS 全局放行

新增 LocalVersionReader 读两个本地版本号 + Info.plist 加
NSAllowsArbitraryLoads 全局放行 HTTP(为 Phase 1.13 zip 下载链路准备)。

- 新增 ylgamehall/Source/Resource/LocalVersionReader.swift
  - public enum LocalVersionReader,nonisolated static 命名空间
  - localAppVersion: Int 从 BundleConfig.shared.appVersion 转 Int
    (沿用 ChannelConfig.plist 注入),解析失败返回 0
  - localGameVersion: Int 用 Foundation XMLParser SAX 风格解析
    SandboxPaths.lobbyVersionXML 的 /game/version@value 节点,
    缺失 / 损坏返回 0(msext NewRootVC.m:664-682 `[nil intValue]=0`
    兜底语义,是首装后首次升级的关键机制)
  - subGameVersion(dir:start:) 子游戏版本号读取,Phase 6 用
  - 内部 VersionXMLHandler 标 nonisolated(Swift 6 严格并发下
    XMLParserDelegate 跨边界),SAX 风格只在 didStartElement 抓
    version 节点的 value 属性
- Info.plist 加 NSAppTransportSecurity / NSAllowsArbitraryLoads = true
  - 企业签 / 超签分发场景,后端域名易变(zip CDN / 配置服 /
    七牛 / 错误日志上报等),白名单维护成本高于安全收益
  - 全局放行的副作用主要影响第三方 SDK 流量;本项目无意外引入未审计
    SDK,且第三方 SDK 自身一般也走 HTTPS,不受影响
  - 详见 ADR-008 决策上下文与 CLAUDE.md「不上架 App Store,企业签 /
    超签 / TF 分发」契约
- RootViewController 烟雾测试串接 LocalVersionReader:打印本地 vs 远端
  对比 + IPA / zip 升级判定
- 真机实测(现网 .txt 本地 + 当前 IPA 内 gamehall.zip 解压物):
  - 本地 appVersion = 43, 远端 = 43 → 不升级
  - 本地 gameVersion = 260(IPA 内 2023-12 zip 的 version.xml),
    远端 = 261 → 需要 zip 升级。Phase 1.13 将实现下载 +
    解压 + 替换链路
This commit is contained in:
joywayer
2026-06-22 02:24:22 +08:00
parent dfbc26a024
commit 9e50c38323
3 changed files with 99 additions and 0 deletions
+9
View File
@@ -19,6 +19,15 @@
</array>
</dict>
</dict>
<key>NSAppTransportSecurity</key>
<dict>
<!-- 企业签 / 超签分发场景,后端域名可能调整(zip CDN / 配置服 / 七牛 / 错误日志
上报等),白名单维护成本高于安全收益。决策:全局放行,由代码层面通过 HTTPS
优先 + URLSession session 校验保证非白名单的第三方 SDK 流量自身仍走加密通道。
详见 ADR-008 / CLAUDE.md「不上架 App Store,通过企业签 / 超签 / TF 分发」。 -->
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UIRequiresFullScreen</key>
<true/>
<key>UIStatusBarHidden</key>
+15
View File
@@ -113,6 +113,21 @@ final class RootViewController: UIViewController {
gameZip = \(resolved.gameZip ?? "<nil>")
showmessage = "\(resolved.showmessage ?? "<nil>")"
""")
// Phase 1.12 +
let localApp = LocalVersionReader.localAppVersion
let localGame = LocalVersionReader.localGameVersion
let needIPAUpgrade = resolved.appVersion > localApp
let needZipUpgrade = resolved.gameVersion > localGame
print("""
[LocalVersionReader] 本地 vs 远端对比:
本地 appVersion = \(localApp)(来自 ChannelConfig.plist
远端 appVersion = \(resolved.appVersion)
→ IPA 升级 = \(needIPAUpgrade ? "需要" : "不需要")
本地 gameVersion = \(localGame)(来自 version.xml /game/version@value
远端 gameVersion = \(resolved.gameVersion)
→ H5 zip 升级 = \(needZipUpgrade ? "需要" : "不需要")
""")
case .shortText(let msg):
print("""
[RemoteConfigClient] 拉到短文本响应(耗时 \(String(format: "%.3f", elapsed))s
@@ -0,0 +1,75 @@
//
// LocalVersionReader.swift
// ylgamehall
//
//
// - IPA ChannelConfig.plist appversion
// - H5 Library/Caches/{gamedir}/{gamestart}/version.xml /game/version@value
//
// docs/Development-Plan.md ADR-008-F / msext NewRootVC.m:664-682
//
import Foundation
public enum LocalVersionReader {
/// IPA msext `[FuncPublic filename:@"appversion"] integerValue`
/// ChannelConfig.plist `appversion` 沿
/// IPA plist
/// 0msext `[nil intValue] = 0`
nonisolated public static var localAppVersion: Int {
Int(BundleConfig.shared.appVersion) ?? 0
}
/// H5 msext `NewRootVC.m:664-682` GDataXML
/// Foundation `XMLParser`
///
/// `SandboxPaths.lobbyVersionXML` =
/// `Library/Caches/{gamedir}/{gamestart}/version.xml`
/// `/game/version` `value` Int
///
/// ** / 0**
/// `[nil intValue] = 0` gameVersion > 0
nonisolated public static var localGameVersion: Int {
readGameVersion(from: SandboxPaths.lobbyVersionXML) ?? 0
}
/// H5 msext `gameController.m:1249-1254`
nonisolated public static func subGameVersion(dir: String, start: String) -> Int {
let url = SandboxPaths.caches
.appendingPathComponent(dir)
.appendingPathComponent(start)
.appendingPathComponent("version.xml")
return readGameVersion(from: url) ?? 0
}
// MARK: - XML
nonisolated private static func readGameVersion(from url: URL) -> Int? {
guard let data = try? Data(contentsOf: url) else { return nil }
let parser = XMLParser(data: data)
let handler = VersionXMLHandler()
parser.delegate = handler
guard parser.parse(), let v = handler.gameVersion else { return nil }
return v
}
}
// MARK: - XMLParser delegate
/// `<version value="N"/>` `/game/version` value Int
/// msext xpath `/game/version` `value` SAX XMLParser
/// `version`
nonisolated private final class VersionXMLHandler: NSObject, XMLParserDelegate {
var gameVersion: Int?
func parser(_ parser: XMLParser,
didStartElement elementName: String,
namespaceURI: String?,
qualifiedName qName: String?,
attributes attributeDict: [String: String] = [:]) {
if elementName == "version", let raw = attributeDict["value"], let v = Int(raw) {
gameVersion = v
}
}
}