@adep/cli 0.4.4 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -11
- package/dist/index.js +33409 -27038
- package/dist/vite.js +209 -15
- package/dist/worker-entry.js +163 -1
- package/package.json +5 -4
- package/templates/app-desktop/README.md +67 -2
- package/templates/app-desktop/identity-store.mjs +309 -0
- package/templates/app-desktop/identity.mjs +517 -0
- package/templates/app-desktop/main.cjs +17 -0
- package/templates/app-desktop/main.mjs +218 -2
- package/templates/app-desktop/offline-runtime.mjs +72 -3
- package/templates/app-desktop/package.json +10 -6
- package/templates/app-desktop/recovery-ui.mjs +300 -0
- package/templates/app-desktop/sync-loop.mjs +137 -0
- package/templates/app-mobile/README.md +47 -9
- package/templates/app-mobile/package.json +2 -1
- package/templates/app-mobile/plugins/adep-bridge/Package.swift +5 -1
- package/templates/app-mobile/plugins/adep-bridge/android/build.gradle +34 -3
- package/templates/app-mobile/plugins/adep-bridge/android/src/main/AndroidManifest.xml +12 -5
- package/templates/app-mobile/plugins/adep-bridge/android/src/main/java/com/adep/bridge/AdepBridge.kt +198 -12
- package/templates/app-mobile/plugins/adep-bridge/android/src/main/java/com/adep/bridge/AdepIdentity.kt +342 -0
- package/templates/app-mobile/plugins/adep-bridge/android/src/main/java/com/adep/bridge/AdepKeystore.kt +56 -2
- package/templates/app-mobile/plugins/adep-bridge/android/src/main/java/com/adep/bridge/AdepSqlite.kt +506 -0
- package/templates/app-mobile/plugins/adep-bridge/android/src/main/java/com/adep/bridge/AdepSqliteLogic.kt +235 -0
- package/templates/app-mobile/plugins/adep-bridge/dist/plugin.js +30 -0
- package/templates/app-mobile/plugins/adep-bridge/ios/AdepBridge.podspec +3 -0
- package/templates/app-mobile/plugins/adep-bridge/ios/Sources/AdepBridge.swift +221 -9
- package/templates/app-mobile/plugins/adep-bridge/ios/Sources/AdepIdentity.swift +500 -0
- package/templates/app-mobile/plugins/adep-bridge/ios/Sources/AdepKeychain.swift +69 -17
- package/templates/app-mobile/plugins/adep-bridge/ios/Sources/AdepPermissionGate.swift +25 -4
- package/templates/app-mobile/plugins/adep-bridge/ios/Sources/AdepShellConfig.swift +54 -0
- package/templates/app-mobile/plugins/adep-bridge/ios/Sources/AdepSqlite.swift +614 -0
- package/templates/app-mobile/plugins/adep-bridge/package.json +10 -0
- package/templates/app-mobile/template.json +12 -3
- package/templates/app-mobile/web/adep-bridge.js +103 -9
- package/templates/app-mobile/web/offline-runtime.js +230 -0
- package/templates/app-mobile-xtool/AdepShell-Info.plist +24 -0
- package/templates/app-mobile-xtool/Package.swift +59 -0
- package/templates/app-mobile-xtool/README.md +81 -0
- package/templates/app-mobile-xtool/Sources/AdepShell/App.swift +36 -0
- package/templates/app-mobile-xtool/Sources/AdepShell/ShellViewController.swift +223 -0
- package/templates/app-mobile-xtool/adep.permissions.json +4 -0
- package/templates/app-mobile-xtool/app/site/.gitkeep +0 -0
- package/templates/app-mobile-xtool/template.json +15 -0
- package/templates/app-mobile-xtool/xtool.yml +35 -0
- package/templates/app-mobile/plugins/adep-bridge/capacitor.plugin.json +0 -10
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import SQLCipher
|
|
3
|
+
|
|
4
|
+
/// 设备本地 SQLite 桥(APP-022 B 段;PRD §2.11.3「设备本地 SQLite」行)。
|
|
5
|
+
///
|
|
6
|
+
/// 供 `AdepBridge` 的 `sqlite.run` / `sqlite.all` / `sqlite.get` 三个能力消费,
|
|
7
|
+
/// 形状对齐 `@adep/app-runtime` 的 `DeviceSqliteBridge`(bridge.ts:run → { changes }、
|
|
8
|
+
/// all → 对象行数组、get → 行或 null)。**门控不在本文件**:能力声明由 `adep.permissions.json`
|
|
9
|
+
/// 决定,未声明即 `PERMISSION_DENIED`(ADR-0007 硬约束 3,门控做在原生侧)。
|
|
10
|
+
///
|
|
11
|
+
/// ## 为什么是 SQLCipher(APP-025,回补 ADR-0007 的显式偏离)
|
|
12
|
+
///
|
|
13
|
+
/// APP-022 首版用系统 SQLite 明文(SPM + xtool 接 SQLCipher 当时是独立课题)。
|
|
14
|
+
/// APP-025 换 **SQLCipher.swift 4.19 的 binary xcframework**(checksum 钉死在
|
|
15
|
+
/// 包根 Package.swift),keying 走 **PRAGMA key**——不走 `sqlite3_key` C API:
|
|
16
|
+
/// 该声明在 `#ifdef SQLITE_HAS_CODEC` 里,消费方必须定义宏才可见;纯 SPM 包里
|
|
17
|
+
/// `.define()` 走 `-D` 对 clang importer 无效,`unsafeFlags` 又被 SwiftPM 在
|
|
18
|
+
/// **被依赖的包**里禁止(adep-bridge 正是被 AdepShell 依赖的包)⇒ C API 路线
|
|
19
|
+
/// 在包形态下是死路。PRAGMA 是纯 SQL 面,不碰宏(macOS 行为级探针 2026-10-22 全 PASS)。
|
|
20
|
+
///
|
|
21
|
+
/// run/all/get 契约**零改动**——APP-022 偏离声明承诺的升级路径(只换打开方式)。
|
|
22
|
+
///
|
|
23
|
+
/// ## key 来源与明文迁移(APP-025)
|
|
24
|
+
///
|
|
25
|
+
/// - key 由 `AdepBridge` 装配时注入(`init(databasePath:keyProvider:)`);缺省 provider
|
|
26
|
+
/// 读 Keychain 的 ownerKey(`AdepKeychain.loadOwnerKey`),读不到用设备固定缺省
|
|
27
|
+
/// (首启未签发也要加密落盘,不能明文裸奔)。
|
|
28
|
+
/// - **ownerKey 迟到 ⇒ key 升级(APP-027)**:签发是首启异步完成的,库可能先用
|
|
29
|
+
/// `fallbackKey` 建好(离线首启就是这形态)。ownerKey 落 Keychain 后由 `adoptKey` 原地
|
|
30
|
+
/// `PRAGMA rekey`;重启时若换代发生在「上次退出前」,`open()` 会探测到「当前 key 打不开
|
|
31
|
+
/// 但缺省 key 打得开」并自动升级(`upgradeFallbackKeyedDatabase`)。
|
|
32
|
+
/// ⚠️ **不处理就是「换 key 后老库打不开」**——用户看到的是数据全丢。
|
|
33
|
+
/// - **明文迁移**:APP-022 时代建的库是明文(头 = `SQLite format 3\0`)。open 时检测到
|
|
34
|
+
/// 明文头即走 `sqlcipher_export + ATTACH` 原地转密(`PRAGMA key=''` + `rekey` 被
|
|
35
|
+
/// SQLCipher 4.19 拒绝——「requires a key of one or more characters」,export 是唯一
|
|
36
|
+
/// 路径,探针实测),同目录临时文件 + 原子替换,迁移后行数校验,旧明文库不留盘。
|
|
37
|
+
/// - **错 key fail loud 钉在 open() 阶段**:PRAGMA key 本身不报错(SQLCipher 惰性验证),
|
|
38
|
+
/// 错 key 要到首次读表才爆(`file is not a database`)⇒ keying 后立刻跑探活语句,
|
|
39
|
+
/// 把报错点钉在打开时而不是第一次业务查询(报错点漂移是排查噩梦)。
|
|
40
|
+
///
|
|
41
|
+
/// ## 为什么长持单连接 + 串行队列(2026-10-22 真机第六轮改版)
|
|
42
|
+
///
|
|
43
|
+
/// 首版「每次调用现开现关」被真机第六轮读数证伪:`@adep/runtime` builder 的
|
|
44
|
+
/// `runOwnedWrite` 事务协议是 **SAVEPOINT → 业务 SQL → RELEASE/ROLLBACK 三次独立桥调用**
|
|
45
|
+
/// (owned.ts),而 SAVEPOINT 是**连接级**状态——第三条语句到达时连接已换,
|
|
46
|
+
/// SQLite 报 `no such savepoint`。
|
|
47
|
+
///
|
|
48
|
+
/// - 库路径唯一(Application Support/AdepBridge/app.sqlite):运行时侧 `ProjectDriver`
|
|
49
|
+
/// 语义是「项目本地库」,不需要多库句柄;
|
|
50
|
+
/// - 长持连接:懒开(首次调用建目录 + open),跨调用复用——SAVEPOINT / 事务 / PRAGMA
|
|
51
|
+
/// 等连接级状态因此跨桥调用存活;
|
|
52
|
+
/// - 串行队列:桥调用可能来自不同线程(Capacitor 派发),SQLite 句柄非线程安全;
|
|
53
|
+
/// 同队列串行也让 `sqlite3_changes`「step 后立刻读」的时序天然成立;
|
|
54
|
+
/// - `close()` 幂等收口 + deinit:插件销毁时关连接,防 WebView reload 场景 fd 泄漏;
|
|
55
|
+
/// 进程被杀由 SQLite journal 恢复兜底。
|
|
56
|
+
///
|
|
57
|
+
/// ## 参数绑定与取值(对齐 node:sqlite / better-sqlite3 的 JS 面)
|
|
58
|
+
///
|
|
59
|
+
/// - 占位符 `?` / `?N` / `:name` 全支持(C API 原生语义);
|
|
60
|
+
/// - JS 侧绑定值支持:String / Double / Int(转 Double 语义等价)/ Bool(→ 0/1)/ null;
|
|
61
|
+
/// - 取值映射:INTEGER → JS number(⚠️ 超 2^53 精度丢失——设备端 owned 表 id 是 snowflake 的
|
|
62
|
+
/// 场景见 §2.11.5,运行时 id 一律字符串,见 SHOP-007 纪律);REAL → number;TEXT → string;
|
|
63
|
+
/// BLOB → ArrayBuffer;NULL → null。
|
|
64
|
+
public final class AdepSqlite {
|
|
65
|
+
/// 可复用的错误码族(JS 侧 catch 分支判断;与 DEVICE_CODES 风格同源但**不**复用其常量——
|
|
66
|
+
/// 错误码是跨实现锚点,原生侧自有清单)。
|
|
67
|
+
public enum Code {
|
|
68
|
+
public static let openFailed = "SQLITE_OPEN_FAILED"
|
|
69
|
+
public static let multipleStatements = "SQLITE_MULTIPLE_STATEMENTS"
|
|
70
|
+
public static let prepareFailed = "SQLITE_PREPARE_FAILED"
|
|
71
|
+
public static let execFailed = "SQLITE_EXEC_FAILED"
|
|
72
|
+
public static let bindFailed = "SQLITE_BIND_FAILED"
|
|
73
|
+
public static let columnFailed = "SQLITE_COLUMN_FAILED"
|
|
74
|
+
public static let invalidParam = "SQLITE_INVALID_PARAM"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// 库文件唯一落点(Application Support/AdepBridge/app.sqlite)。
|
|
78
|
+
/// 目录不存在时 `open` 幂等创建(iOS 沙盒保证 Application Support 一定可写)。
|
|
79
|
+
private let databasePath: String
|
|
80
|
+
|
|
81
|
+
/// `open` 抛出的结构化错误(domain 固定,code 走 `Code.openFailed` 语义——NSError 的
|
|
82
|
+
/// code 是 Int,JS 侧要的是字符串码,所以 message 里带 `Code.openFailed` 前缀)。
|
|
83
|
+
public static func defaultDatabasePath() -> String {
|
|
84
|
+
let support = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
|
|
85
|
+
return support.appendingPathComponent("AdepBridge", isDirectory: true)
|
|
86
|
+
.appendingPathComponent("app.sqlite", isDirectory: false).path
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// 注入式构造(测试与 C 段 WebView 装配可指定路径;生产走 `init()` 缺省路径)。
|
|
90
|
+
/// `keyProvider` 在每次 open 时被调用(同队列串行 ⇒ 无并发读)——返回 nil 时用
|
|
91
|
+
/// 设备固定缺省(首启未签发也要加密落盘)。
|
|
92
|
+
public init(databasePath: String, keyProvider: (() -> String?)? = nil) {
|
|
93
|
+
self.databasePath = databasePath
|
|
94
|
+
self.keyProvider = keyProvider
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// 生产缺省构造:Application Support/AdepBridge/app.sqlite + 静态缺省 key 源。
|
|
98
|
+
/// ⚠️ key 源走 `defaultKeyProvider` 静态钩子而非直连 `AdepKeychain`:本类型被行为级门
|
|
99
|
+
/// 单文件编译(无 Security framework / AdepKeychain 同模块),钩子由壳装配层
|
|
100
|
+
/// (AdepBridge 安装点)设置——依赖倒置,行为级门不必拖进 Keychain。
|
|
101
|
+
public convenience init() {
|
|
102
|
+
self.init(databasePath: AdepSqlite.defaultDatabasePath(), keyProvider: AdepSqlite.defaultKeyProvider)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// 缺省 key 源(壳装配层注入;缺省返回 nil ⇒ 用设备固定缺省 key)。
|
|
106
|
+
/// AdepBridge 装配时设为 `{ AdepKeychain.loadOwnerKey() }`(ownerKey 即库 key)。
|
|
107
|
+
nonisolated(unsafe) public static var defaultKeyProvider: (() -> String?)? = nil
|
|
108
|
+
|
|
109
|
+
// MARK: - 加密(APP-025)
|
|
110
|
+
|
|
111
|
+
/// key 来源(装配时注入;缺省读 Keychain ownerKey)。
|
|
112
|
+
private let keyProvider: (() -> String?)?
|
|
113
|
+
|
|
114
|
+
/// keyProvider 返回 nil(首启未签发)时的设备固定缺省。
|
|
115
|
+
/// ⚠️ 不是安全 key——它只是把「明文裸奔」变成「有锁的库」;ownerKey 签发后由
|
|
116
|
+
/// 产品级 key 轮换换成正式 key(另议,超出本单)。
|
|
117
|
+
private static let fallbackKey = "adep-device-default-key-v1"
|
|
118
|
+
|
|
119
|
+
/// 生效 key:provider 优先,nil 时走缺省。
|
|
120
|
+
private var effectiveKey: String {
|
|
121
|
+
keyProvider?() ?? Self.fallbackKey
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// 本次打开**实际使用**的 key(仅在连接成功打开后非 nil)。
|
|
125
|
+
///
|
|
126
|
+
/// 为什么要记住:ownerKey 是首启签发、**异步到达**的(APP-027)——连接可能先用
|
|
127
|
+
/// `fallbackKey` 建好,ownerKey 随后才落 Keychain。这时必须知道「手上这把连接用的是
|
|
128
|
+
/// 哪把 key」才能决定要不要 rekey(`adoptKey`)。
|
|
129
|
+
private var currentKey: String?
|
|
130
|
+
|
|
131
|
+
/// SQLite 明文库 magic 头(16 字节,含结尾 \0)。文件头与之相等 = 明文库。
|
|
132
|
+
private static let plainHeaderMagic = "SQLite format 3\u{0}"
|
|
133
|
+
|
|
134
|
+
// MARK: - 对外三方法(DeviceSqliteBridge 同形)
|
|
135
|
+
|
|
136
|
+
/// 执行写语句(DDL / DML),返回受影响行数。
|
|
137
|
+
/// ⚠️ 连接长持(懒开):SAVEPOINT / 事务状态可跨调用存活——`runOwnedWrite`
|
|
138
|
+
/// 协议(SAVEPOINT → 业务 → RELEASE,三次独立桥调用)依赖这一点。
|
|
139
|
+
public func run(_ sql: String, params: [Any]?) throws -> Int32 {
|
|
140
|
+
try enqueue {
|
|
141
|
+
let statement = try self.prepare(sql, params: params)
|
|
142
|
+
defer { sqlite3_finalize(statement) }
|
|
143
|
+
// ⚠️ 不能只认 SQLITE_DONE:run 也可能收到 SELECT(调用方想拿 changes=0 的语义),
|
|
144
|
+
// SQLite 对 SELECT 的 step 返回 SQLITE_ROW——只认 DONE 会报「another row available」
|
|
145
|
+
// (行为探针 case7 当场抓住:SELECT 走 run 报 execFailed,而不是返回 changes=0)。
|
|
146
|
+
// 语义与 node:sqlite 对齐:run 完成 = 语句执行到无更多行,行数据丢弃。
|
|
147
|
+
var rc = sqlite3_step(statement)
|
|
148
|
+
while rc == SQLITE_ROW {
|
|
149
|
+
rc = sqlite3_step(statement)
|
|
150
|
+
}
|
|
151
|
+
guard rc == SQLITE_DONE else {
|
|
152
|
+
throw self.sqliteError(Code.execFailed, sql: sql)
|
|
153
|
+
}
|
|
154
|
+
return sqlite3_changes(self.db!)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// 执行查询,返回对象行数组。
|
|
159
|
+
public func all(_ sql: String, params: [Any]?) throws -> [[String: Any]] {
|
|
160
|
+
try enqueue {
|
|
161
|
+
let statement = try self.prepare(sql, params: params)
|
|
162
|
+
defer { sqlite3_finalize(statement) }
|
|
163
|
+
var rows: [[String: Any]] = []
|
|
164
|
+
while true {
|
|
165
|
+
let rc = sqlite3_step(statement)
|
|
166
|
+
if rc == SQLITE_ROW {
|
|
167
|
+
rows.append(try self.readRow(statement, sql: sql))
|
|
168
|
+
} else if rc == SQLITE_DONE {
|
|
169
|
+
break
|
|
170
|
+
} else {
|
|
171
|
+
throw self.sqliteError(Code.columnFailed, sql: sql)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return rows
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/// 执行单行查询;无行返回 nil。
|
|
179
|
+
public func get(_ sql: String, params: [Any]?) throws -> [String: Any]? {
|
|
180
|
+
try enqueue {
|
|
181
|
+
let statement = try self.prepare(sql, params: params)
|
|
182
|
+
defer { sqlite3_finalize(statement) }
|
|
183
|
+
let rc = sqlite3_step(statement)
|
|
184
|
+
if rc == SQLITE_ROW {
|
|
185
|
+
return try self.readRow(statement, sql: sql)
|
|
186
|
+
}
|
|
187
|
+
if rc == SQLITE_DONE {
|
|
188
|
+
return nil
|
|
189
|
+
}
|
|
190
|
+
throw self.sqliteError(Code.columnFailed, sql: sql)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// MARK: - 内部:连接 / prepare / 绑定 / 取值
|
|
195
|
+
|
|
196
|
+
/// 长持连接句柄(懒开)。所有访问必须经 `queue` 串行化。
|
|
197
|
+
private var db: OpaquePointer?
|
|
198
|
+
|
|
199
|
+
/// 串行队列:SQLite 句柄非线程安全,桥调用可能来自不同线程;
|
|
200
|
+
/// 同队列串行也让 `sqlite3_changes`「step 后立刻读」的时序天然成立。
|
|
201
|
+
private let queue = DispatchQueue(label: "adep.sqlite.bridge")
|
|
202
|
+
|
|
203
|
+
/// 打开失败的结构化错误(与 `sqliteError` 同 message 形态,enqueue 包装复用)。
|
|
204
|
+
private func openError(_ message: String) -> NSError {
|
|
205
|
+
NSError(
|
|
206
|
+
domain: "AdepSqlite", code: 1,
|
|
207
|
+
userInfo: [NSLocalizedDescriptionKey: "\(Code.openFailed): \(message)(path: \(databasePath))"]
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/// 打开连接(目录幂等创建;`sqlite3_open` 会自动建库文件,不存在时)。
|
|
212
|
+
/// ⚠️ 不能用 `try?`:打开失败必须带 sqlite errmsg 抛出,静默 nil 会让后续
|
|
213
|
+
/// prepare 段错误(野指针)。已打开时直接返回(长持复用)。
|
|
214
|
+
///
|
|
215
|
+
/// APP-025 keying 顺序(探针实证的语义,顺序不能动):
|
|
216
|
+
/// 1. `sqlite3_open` 成功后**立刻** `PRAGMA key`(任何其他语句先于 key 都会被
|
|
217
|
+
/// 当作对明文/错密库的操作);
|
|
218
|
+
/// 2. **明文库检测与迁移**:库文件已存在且头是明文 magic ⇒ `sqlcipher_export`
|
|
219
|
+
/// 原地转密(在 keying 之前的裸连接上做——export 的源就是当前明文库);
|
|
220
|
+
/// 3. **探活语句**:PRAGMA key 惰性验证,错 key 要到首次读表才报
|
|
221
|
+
/// `file is not a database` ⇒ 这里跑一条 `SELECT count(*) FROM sqlite_master;`
|
|
222
|
+
/// 把报错点钉在 open(),错误码 `SQLITE_OPEN_FAILED`。
|
|
223
|
+
private func open() throws {
|
|
224
|
+
guard db == nil else { return }
|
|
225
|
+
let dir = (databasePath as NSString).deletingLastPathComponent
|
|
226
|
+
if !FileManager.default.fileExists(atPath: dir) {
|
|
227
|
+
try FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true)
|
|
228
|
+
}
|
|
229
|
+
var handle: OpaquePointer?
|
|
230
|
+
guard sqlite3_open(databasePath, &handle) == SQLITE_OK else {
|
|
231
|
+
let message = handle.map { String(cString: sqlite3_errmsg($0)) } ?? "sqlite3_open 失败"
|
|
232
|
+
// 打开失败时 handle 仍可能非 nil(SQLite 语义:失败也回填句柄),必须关掉防泄漏。
|
|
233
|
+
handle.map { sqlite3_close($0) }
|
|
234
|
+
throw openError(message)
|
|
235
|
+
}
|
|
236
|
+
db = handle
|
|
237
|
+
do {
|
|
238
|
+
// 明文库检测必须在 keying 之前:keying 后这个连接就对明文不可读了。
|
|
239
|
+
try migratePlainDatabaseIfNeeded()
|
|
240
|
+
let desired = effectiveKey
|
|
241
|
+
if try keyAndProbe(desired) {
|
|
242
|
+
currentKey = desired
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
// 探活**返回 false**(而非抛错)只可能是「库不是用这把 key 建的」:
|
|
246
|
+
// `sqlite3_open` 新建的空库对任何 key 都能探活通过,所以「文件是本次才建的」
|
|
247
|
+
// 与「走到了这里」互斥——不需要再判文件是否存在(初版写过这个条件,
|
|
248
|
+
// 变异自证时发现它恒为 true,属不可达死码,已删;保留会让读者误以为
|
|
249
|
+
// 存在「新库被误判成老库」的路径)。
|
|
250
|
+
//
|
|
251
|
+
// 剩下的两种真实成因:①此前用设备固定缺省 key 建好的存量库,而 ownerKey
|
|
252
|
+
// 刚签发(APP-027 之前的所有安装都是这个形态)⇒ 用缺省 key 打开并 rekey;
|
|
253
|
+
// ②库坏了 / 是别人的 key 建的 ⇒ `upgradeFallbackKeyedDatabase` 里旧 key
|
|
254
|
+
// 也打不开,抛带原因的错误(绝不按新库重建 = 静默丢数据)。
|
|
255
|
+
// `desired == fallbackKey` 短路掉:同一把 key 再试一次毫无意义,且此时
|
|
256
|
+
// 唯一成因就是库已损坏,直接报「库已损坏」比报「升级失败」好排障。
|
|
257
|
+
guard desired != Self.fallbackKey else {
|
|
258
|
+
throw openError("key 校验失败(\(lastErrorMessage()))——密钥不匹配或库已损坏")
|
|
259
|
+
}
|
|
260
|
+
try upgradeFallbackKeyedDatabase(to: desired)
|
|
261
|
+
} catch {
|
|
262
|
+
// keying / 迁移 / 探活失败:关掉半开连接(防泄漏),统一按 openFailed 语义。
|
|
263
|
+
closeLocked()
|
|
264
|
+
currentKey = nil
|
|
265
|
+
throw error
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/// 用指定 key keying 并探活。返回是否可用。
|
|
270
|
+
///
|
|
271
|
+
/// ⚠️ 探活是必须的:`PRAGMA key` **惰性验证**——错 key 要到首次读表才报
|
|
272
|
+
/// `file is not a database`。不在这里探,报错点会漂到第一次业务查询(排查噩梦)。
|
|
273
|
+
private func keyAndProbe(_ key: String) throws -> Bool {
|
|
274
|
+
try rawExec("PRAGMA key = '\(Self.escape(key))';")
|
|
275
|
+
return rawScalar("SELECT count(*) FROM sqlite_master;") != nil
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/// 老库 key 升级:`fallbackKey` → 正式 key(ownerKey)原地 rekey(APP-027)。
|
|
279
|
+
///
|
|
280
|
+
/// 只能在「旧 key 已知」的方向做(SQLCipher 4.19 拒绝 `key='' + rekey`,APP-025 探针实证),
|
|
281
|
+
/// 而设备固定缺省 key 是常量、永远可知 ⇒ 这条路对存量设备是通的。
|
|
282
|
+
///
|
|
283
|
+
/// 顺序不能省:关连接 → 重开 → 用**旧 key** keying 成功 → `PRAGMA rekey` → 用**新 key** 探活。
|
|
284
|
+
/// 任何一步失败都关连接抛出(保持原库不动,下次启动重试;绝不「打不开就当新库建」)。
|
|
285
|
+
private func upgradeFallbackKeyedDatabase(to desired: String) throws {
|
|
286
|
+
closeLocked()
|
|
287
|
+
var handle: OpaquePointer?
|
|
288
|
+
guard sqlite3_open(databasePath, &handle) == SQLITE_OK else {
|
|
289
|
+
let message = handle.map { String(cString: sqlite3_errmsg($0)) } ?? "sqlite3_open 失败"
|
|
290
|
+
handle.map { sqlite3_close($0) }
|
|
291
|
+
throw openError("key 升级重开失败:\(message)")
|
|
292
|
+
}
|
|
293
|
+
db = handle
|
|
294
|
+
guard try keyAndProbe(Self.fallbackKey) else {
|
|
295
|
+
throw openError(
|
|
296
|
+
"库既不能用当前 key 打开、也不能用设备缺省 key 打开(\(lastErrorMessage()))"
|
|
297
|
+
+ "——密钥不匹配或库已损坏;不会按新库重建(那等于静默丢数据)"
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
try rawExec("PRAGMA rekey = '\(Self.escape(desired))';")
|
|
301
|
+
guard rawScalar("SELECT count(*) FROM sqlite_master;") != nil else {
|
|
302
|
+
throw openError("key 升级后探活失败(\(lastErrorMessage()))——rekey 未生效,原库仍以缺省 key 可读")
|
|
303
|
+
}
|
|
304
|
+
currentKey = desired
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/// 库 key 换成正式 key(APP-027 自举拿到 ownerKey 后由壳装配层调用)。
|
|
308
|
+
///
|
|
309
|
+
/// 三种情形都要正确:
|
|
310
|
+
/// 1. 连接**还没开**(最常见)⇒ 什么都不做——下次 open 时 keyProvider 已能读到 ownerKey;
|
|
311
|
+
/// 2. 连接已开且就是这把 key ⇒ 什么都不做(幂等);
|
|
312
|
+
/// 3. 连接已开、用的是 fallbackKey ⇒ `PRAGMA rekey` 原地换 key(数据行原样保留)。
|
|
313
|
+
///
|
|
314
|
+
/// ⚠️ 失败**不改变可用性**:库仍以旧 key 可读(只是还没升级),下次启动 `open()` 会走
|
|
315
|
+
/// `upgradeFallbackKeyedDatabase` 重试。所以这里只记日志,不抛(抛也无处可抛——
|
|
316
|
+
/// 调用方是后台自举线程)。
|
|
317
|
+
public func adoptKey(_ key: String) {
|
|
318
|
+
guard !key.isEmpty else { return }
|
|
319
|
+
queue.sync {
|
|
320
|
+
guard db != nil else { return }
|
|
321
|
+
guard currentKey != key else { return }
|
|
322
|
+
do {
|
|
323
|
+
try rawExec("PRAGMA rekey = '\(Self.escape(key))';")
|
|
324
|
+
guard rawScalar("SELECT count(*) FROM sqlite_master;") != nil else {
|
|
325
|
+
throw openError("rekey 后探活失败(\(lastErrorMessage()))")
|
|
326
|
+
}
|
|
327
|
+
currentKey = key
|
|
328
|
+
} catch {
|
|
329
|
+
NSLog("[adep-sqlite] 库 key 升级(adoptKey)失败,库仍以旧 key 可读:\(error.localizedDescription)")
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/// SQL 字面量里的单引号转义(key 来自 Keychain / 配置,可能是任意字符串)。
|
|
335
|
+
private static func escape(_ value: String) -> String {
|
|
336
|
+
value.replacingOccurrences(of: "'", with: "''")
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/// 连接级错误文案(未打开时给固定串,不段错误)。
|
|
340
|
+
private func lastErrorMessage() -> String {
|
|
341
|
+
db.map { String(cString: sqlite3_errmsg($0)) } ?? "连接未打开"
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/// 裸 exec(不做 keying 假设;迁移路径专用——export 要跑在 keying 之前的明文连接上)。
|
|
345
|
+
private func rawExec(_ sql: String) throws {
|
|
346
|
+
var errorPointer: UnsafeMutablePointer<CChar>?
|
|
347
|
+
let rc = sqlite3_exec(db!, sql, nil, nil, &errorPointer)
|
|
348
|
+
if rc != SQLITE_OK {
|
|
349
|
+
let message = errorPointer.map { String(cString: $0) } ?? "sqlite3_exec 失败(rc=\(rc))"
|
|
350
|
+
if let errorPointer { sqlite3_free(errorPointer) }
|
|
351
|
+
throw openError("\(message)(sql: \(sql.prefix(120)))")
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/// 裸单值查询(返回首行首列整数值;失败/无行返回 nil——由调用方决定语义)。
|
|
356
|
+
private func rawScalar(_ sql: String) -> Int32? {
|
|
357
|
+
var statement: OpaquePointer?
|
|
358
|
+
guard sqlite3_prepare_v2(db!, sql, -1, &statement, nil) == SQLITE_OK, let statement else {
|
|
359
|
+
return nil
|
|
360
|
+
}
|
|
361
|
+
defer { sqlite3_finalize(statement) }
|
|
362
|
+
guard sqlite3_step(statement) == SQLITE_ROW else { return nil }
|
|
363
|
+
return sqlite3_column_int(statement, 0)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/// 明文库 → 密文库原地迁移(APP-025)。仅在库文件存在且头是明文 magic 时执行。
|
|
367
|
+
///
|
|
368
|
+
/// 路径:ATTACH 临时文件**带真实 key**,`sqlcipher_export(migrateTarget ← main)`
|
|
369
|
+
/// 产出密文临时库,校验表数一致后**原子替换**原文件。旧明文库不留盘(替换即消失)。
|
|
370
|
+
/// 任何一步失败都保持原明文库不动(可重试)。
|
|
371
|
+
private func migratePlainDatabaseIfNeeded() throws {
|
|
372
|
+
guard FileManager.default.fileExists(atPath: databasePath) else { return }
|
|
373
|
+
guard let header = fileHeader16(databasePath), header == Self.plainHeaderMagic else { return }
|
|
374
|
+
let tempPath = databasePath + ".migrating"
|
|
375
|
+
// 幂等清理:上次迁移中断留下的半成品直接删(export 从原明文库重跑)。
|
|
376
|
+
try? FileManager.default.removeItem(atPath: tempPath)
|
|
377
|
+
let before = rawScalar("SELECT count(*) FROM sqlite_master;") ?? -1
|
|
378
|
+
// sqlcipher_export(dst, src) 语义:把 src 写进 dst 所在库,密文形态由 dst 的
|
|
379
|
+
// key 决定(探针实证:ATTACH 带 KEY '' 产出明文假迁移;带真实 key 产出密文)。
|
|
380
|
+
// ⚠️ ATTACH 的 KEY 就是新库的加密 key——不是「声明源库 key」。
|
|
381
|
+
// 临时文件名里的单引号:databasePath 由本类管理(固定目录 + 固定文件名),
|
|
382
|
+
// 不含单引号;即便未来可配,这里同样做转义。
|
|
383
|
+
let escapedTemp = tempPath.replacingOccurrences(of: "'", with: "''")
|
|
384
|
+
let escapedKey = effectiveKey.replacingOccurrences(of: "'", with: "''")
|
|
385
|
+
try rawExec("ATTACH DATABASE '\(escapedTemp)' AS migrateTarget KEY '\(escapedKey)';")
|
|
386
|
+
defer {
|
|
387
|
+
_ = rawExecIgnoreError("DETACH DATABASE migrateTarget;")
|
|
388
|
+
}
|
|
389
|
+
try rawExec("SELECT sqlcipher_export('migrateTarget', 'main');")
|
|
390
|
+
// 校验:迁移后的目标库表数与源一致(行数校验针对业务表不可行——sqlite_master
|
|
391
|
+
// 的表数是结构完整性最直接的代理指标;数据级校验由行为级门覆盖典型场景)。
|
|
392
|
+
let after = rawScalar("SELECT count(*) FROM migrateTarget.sqlite_master;") ?? -2
|
|
393
|
+
guard after == before, before >= 0 else {
|
|
394
|
+
throw openError("明文库迁移校验失败:源 sqlite_master \(before) 条 vs 目标 \(after) 条")
|
|
395
|
+
}
|
|
396
|
+
// 关掉当前对明文库的连接,才能替换文件(Windows 语义不存在,但 macOS 上
|
|
397
|
+
// 替换打开中的文件会让句柄指向旧 inode——长持连接必须重开指向新文件)。
|
|
398
|
+
closeLocked()
|
|
399
|
+
do {
|
|
400
|
+
let backupPath = databasePath + ".plain-backup"
|
|
401
|
+
try? FileManager.default.removeItem(atPath: backupPath)
|
|
402
|
+
try FileManager.default.moveItem(atPath: databasePath, toPath: backupPath)
|
|
403
|
+
do {
|
|
404
|
+
try FileManager.default.moveItem(atPath: tempPath, toPath: databasePath)
|
|
405
|
+
} catch {
|
|
406
|
+
// 替换失败:回滚备份,保持明文原状(可重试)。
|
|
407
|
+
try? FileManager.default.moveItem(atPath: backupPath, toPath: databasePath)
|
|
408
|
+
throw openError("明文库迁移替换失败:\(error.localizedDescription)")
|
|
409
|
+
}
|
|
410
|
+
// 替换成功:删明文备份(不留盘是验收标准;失败不致命——下次迁移会重建)。
|
|
411
|
+
try? FileManager.default.removeItem(atPath: backupPath)
|
|
412
|
+
// 重开连接到新密文库(本次调用继续用;keying 由调用方 open() 继续执行)。
|
|
413
|
+
var handle: OpaquePointer?
|
|
414
|
+
guard sqlite3_open(databasePath, &handle) == SQLITE_OK else {
|
|
415
|
+
let message = handle.map { String(cString: sqlite3_errmsg($0)) } ?? "sqlite3_open 失败"
|
|
416
|
+
handle.map { sqlite3_close($0) }
|
|
417
|
+
throw openError("迁移后重开失败:\(message)")
|
|
418
|
+
}
|
|
419
|
+
db = handle
|
|
420
|
+
} catch let error as NSError where error.domain == "AdepSqlite" {
|
|
421
|
+
throw error
|
|
422
|
+
} catch {
|
|
423
|
+
throw openError("明文库迁移文件操作失败:\(error.localizedDescription)")
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/// 读文件头 16 字节(不足 16 字节 = 不是 SQLite 库,返回 nil 走「非明文」分支)。
|
|
428
|
+
private func fileHeader16(_ path: String) -> String? {
|
|
429
|
+
guard let handle = FileHandle(forReadingAtPath: path) else { return nil }
|
|
430
|
+
defer { handle.closeFile() }
|
|
431
|
+
let data = handle.readData(ofLength: 16)
|
|
432
|
+
guard data.count == 16 else { return nil }
|
|
433
|
+
return String(data: data, encoding: .ascii)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/// 仅用于 defer 清理的 exec(失败静默——DETACH 失败不影响主流程,连接即将关闭)。
|
|
437
|
+
private func rawExecIgnoreError(_ sql: String) {
|
|
438
|
+
var errorPointer: UnsafeMutablePointer<CChar>?
|
|
439
|
+
_ = sqlite3_exec(db!, sql, nil, nil, &errorPointer)
|
|
440
|
+
if let errorPointer { sqlite3_free(errorPointer) }
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/// 关闭连接(幂等:nil 时无操作)。只在 `close()` 内调用(同队列)。
|
|
444
|
+
private func closeLocked() {
|
|
445
|
+
if let handle = db {
|
|
446
|
+
sqlite3_close(handle)
|
|
447
|
+
db = nil
|
|
448
|
+
}
|
|
449
|
+
currentKey = nil
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/// 插件实例销毁时收口(幂等)。进程被杀的场景由 SQLite journal 恢复兜底。
|
|
453
|
+
public func close() {
|
|
454
|
+
queue.sync { closeLocked() }
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
deinit {
|
|
458
|
+
close()
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/// 串行化包装:queue.sync → ensure open → body。⚠️ 连接**不**在调用结束时关闭
|
|
462
|
+
/// ——长持是 SAVEPOINT 跨调用存活的前提。body 内 `self.db!` 强解包安全:
|
|
463
|
+
/// open() 已保证非 nil,且同队列串行,无并发 closeLocked 置 nil 的窗口。
|
|
464
|
+
private func enqueue<T>(_ body: () throws -> T) throws -> T {
|
|
465
|
+
try queue.sync {
|
|
466
|
+
try open()
|
|
467
|
+
return try body()
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/// prepare + 绑定。`defer finalize` 由调用方负责(本方法返回语句句柄)。
|
|
472
|
+
/// ⚠️ 无强解包:prepare 失败 / 绑定失败 / 参数个数超出语句占位符,都要 finalize + 抛错,
|
|
473
|
+
/// 不能段错误(行为探针 11/11 里的 fail-loud 三例就是走这里)。
|
|
474
|
+
private func prepare(_ sql: String, params: [Any]?) throws -> OpaquePointer {
|
|
475
|
+
guard let db = db else {
|
|
476
|
+
throw sqliteError(Code.openFailed, sql: sql)
|
|
477
|
+
}
|
|
478
|
+
var statement: OpaquePointer?
|
|
479
|
+
// ⚠️ sql 必须先落**自管** C 缓冲:Swift String 直传时是桥接临时副本,prepare 返回后
|
|
480
|
+
// 副本销毁 ⇒ pzTail 悬空(读到全 0,行为探针 case15 抓到「该红不红」)。cString 的
|
|
481
|
+
// 数组由本作用域持有,tail 相对它有效。
|
|
482
|
+
guard let sqlBytes = sql.cString(using: .utf8) else {
|
|
483
|
+
throw sqliteError(Code.prepareFailed, sql: sql)
|
|
484
|
+
}
|
|
485
|
+
var tail: UnsafePointer<CChar>?
|
|
486
|
+
guard sqlite3_prepare_v2(db, sqlBytes, -1, &statement, &tail) == SQLITE_OK, let statement else {
|
|
487
|
+
throw sqliteError(Code.prepareFailed, sql: sql)
|
|
488
|
+
}
|
|
489
|
+
// ⚠️ 多语句 fail loud:sqlite3_prepare_v2 只编译**第一条**语句,尾随语句被 SQLite
|
|
490
|
+
// **静默丢弃**(行为探针 case15 实测:`SELECT * FROM m; DROP TABLE m` ⇒ DROP 没执行,
|
|
491
|
+
// 调用方以为两条都跑了)。tail 指向剩余字节——剥掉空白与注释后非空即拒绝。
|
|
492
|
+
if let tail {
|
|
493
|
+
let remainder = String(cString: tail)
|
|
494
|
+
var stripped = ""
|
|
495
|
+
var inLineComment = false
|
|
496
|
+
var inBlockComment = false
|
|
497
|
+
var previous: Character = "\0"
|
|
498
|
+
for char in remainder {
|
|
499
|
+
if inLineComment {
|
|
500
|
+
if char == "\n" { inLineComment = false }
|
|
501
|
+
continue
|
|
502
|
+
}
|
|
503
|
+
if inBlockComment {
|
|
504
|
+
if previous == "*" && char == "/" { inBlockComment = false; previous = "\0"; continue }
|
|
505
|
+
previous = char
|
|
506
|
+
continue
|
|
507
|
+
}
|
|
508
|
+
if previous == "-" && char == "-" { inLineComment = true; stripped = String(stripped.dropLast()); previous = char; continue }
|
|
509
|
+
if previous == "/" && char == "*" { inBlockComment = true; stripped = String(stripped.dropLast()); previous = char; continue }
|
|
510
|
+
previous = char
|
|
511
|
+
if !char.isWhitespace { stripped.append(char) }
|
|
512
|
+
}
|
|
513
|
+
if !stripped.isEmpty {
|
|
514
|
+
sqlite3_finalize(statement)
|
|
515
|
+
throw NSError(
|
|
516
|
+
domain: "AdepSqlite", code: 4,
|
|
517
|
+
userInfo: [NSLocalizedDescriptionKey:
|
|
518
|
+
"\(Code.multipleStatements): SQL 含多条语句(尾随语句会被 SQLite 静默丢弃,已拒绝执行);sql: \(sql.prefix(120))"]
|
|
519
|
+
)
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if let params {
|
|
523
|
+
// 越界参数 fail loud:SQLite 对多余绑定值静默忽略(bind 返回 SQLITE_RANGE),
|
|
524
|
+
// 静默 = 参数清单与语句漂移时查不出来(与「列类型不映射」同族的静默陷阱)。
|
|
525
|
+
// ⚠️ 参数个数必须**恰好等于**占位符数:多了 SQLite 静默忽略(SQLITE_RANGE),
|
|
526
|
+
// 少了缺的占位符按 NULL 语义执行(行为探针 case12 实测:INSERT 两列只给一列
|
|
527
|
+
// 的值 ⇒ 行照插、另一列 NULL——「不报错只恒不命中」的插入版)。两侧都 fail loud。
|
|
528
|
+
let placeholderCount = sqlite3_bind_parameter_count(statement)
|
|
529
|
+
guard params.count == Int(placeholderCount) else {
|
|
530
|
+
sqlite3_finalize(statement)
|
|
531
|
+
throw NSError(
|
|
532
|
+
domain: "AdepSqlite", code: 3,
|
|
533
|
+
userInfo: [NSLocalizedDescriptionKey:
|
|
534
|
+
"\(Code.bindFailed): 参数个数 \(params.count) 与占位符数 \(placeholderCount) 不一致(多了会被静默忽略,少了按 NULL 执行——两侧都不允许);sql: \(sql.prefix(120))"]
|
|
535
|
+
)
|
|
536
|
+
}
|
|
537
|
+
for (index, param) in params.enumerated() {
|
|
538
|
+
let rc = bind(param, at: Int32(index + 1), to: statement)
|
|
539
|
+
if rc != SQLITE_OK {
|
|
540
|
+
sqlite3_finalize(statement)
|
|
541
|
+
throw sqliteError(Code.bindFailed, sql: sql)
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return statement
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/// 绑定单参数(1-based index;支持 String / 数值 / Bool / null;其他类型抛 invalidParam)。
|
|
549
|
+
private func bind(_ value: Any, at index: Int32, to statement: OpaquePointer) -> Int32 {
|
|
550
|
+
switch value {
|
|
551
|
+
case let text as String:
|
|
552
|
+
return sqlite3_bind_text(statement, index, text, -1, SQLITE_TRANSIENT)
|
|
553
|
+
case let number as Double:
|
|
554
|
+
return sqlite3_bind_double(statement, index, number)
|
|
555
|
+
case let number as Int:
|
|
556
|
+
// Int64 语义:SQLite 整数上限;超过 2^53 的 number 语义由调用方保证(id 一律字符串)。
|
|
557
|
+
return sqlite3_bind_int64(statement, index, Int64(number))
|
|
558
|
+
case let number as Bool:
|
|
559
|
+
return sqlite3_bind_int64(statement, index, number ? 1 : 0)
|
|
560
|
+
default:
|
|
561
|
+
// null 之外的未知类型 fail loud:静默绑 NULL 会让 WHERE 条件静默不命中
|
|
562
|
+
// (「不报错只恒不命中」正是本仓最忌讳的静默陷阱形态)。
|
|
563
|
+
if value is NSNull {
|
|
564
|
+
return sqlite3_bind_null(statement, index)
|
|
565
|
+
}
|
|
566
|
+
return SQLITE_MISUSE
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/// 读一行成对象(列名 → 值;类型映射见文件头注释)。
|
|
571
|
+
private func readRow(_ statement: OpaquePointer, sql: String) throws -> [String: Any] {
|
|
572
|
+
let columnCount = sqlite3_column_count(statement)
|
|
573
|
+
var row: [String: Any] = [:]
|
|
574
|
+
for index in 0..<columnCount {
|
|
575
|
+
let name = String(cString: sqlite3_column_name(statement, index))
|
|
576
|
+
let value: Any
|
|
577
|
+
switch sqlite3_column_type(statement, index) {
|
|
578
|
+
case SQLITE_INTEGER:
|
|
579
|
+
value = sqlite3_column_int64(statement, index) // Int64 → JS number(精度见文件头)
|
|
580
|
+
case SQLITE_FLOAT:
|
|
581
|
+
value = sqlite3_column_double(statement, index)
|
|
582
|
+
case SQLITE_TEXT:
|
|
583
|
+
value = String(cString: sqlite3_column_text(statement, index))
|
|
584
|
+
case SQLITE_BLOB:
|
|
585
|
+
if let bytes = sqlite3_column_blob(statement, index) {
|
|
586
|
+
let count = Int(sqlite3_column_bytes(statement, index))
|
|
587
|
+
value = Data(bytes: bytes, count: count)
|
|
588
|
+
} else {
|
|
589
|
+
value = Data()
|
|
590
|
+
}
|
|
591
|
+
case SQLITE_NULL:
|
|
592
|
+
value = NSNull()
|
|
593
|
+
default:
|
|
594
|
+
throw sqliteError(Code.columnFailed, sql: sql)
|
|
595
|
+
}
|
|
596
|
+
row[name] = value
|
|
597
|
+
}
|
|
598
|
+
return row
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/// 统一错误构造(带 SQL 文本截断与 sqlite errmsg;message 前缀是 JS 侧 catch 的锚)。
|
|
602
|
+
private func sqliteError(_ code: String, sql: String) -> NSError {
|
|
603
|
+
let snippet = sql.count > 120 ? String(sql.prefix(120)) + "…" : sql
|
|
604
|
+
let message = db.map { String(cString: sqlite3_errmsg($0)) } ?? "连接未打开"
|
|
605
|
+
return NSError(
|
|
606
|
+
domain: "AdepSqlite", code: 2,
|
|
607
|
+
userInfo: [NSLocalizedDescriptionKey: "\(code): \(message)(sql: \(snippet))"]
|
|
608
|
+
)
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/// SQLITE_TRANSIENT:让 SQLite 自己拷贝绑定字符串(C API 的经典坑——传静态常量时
|
|
613
|
+
/// SQLite 不拷贝,语句 finalize 前字符串必须存活;Swift 字符串作用域短,必须 TRANSIENT)。
|
|
614
|
+
private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
|
|
@@ -9,8 +9,17 @@
|
|
|
9
9
|
"description": "平台项目 slug(写入包名 / adep.app.json / bundle id 推导)"
|
|
10
10
|
},
|
|
11
11
|
{ "name": "appName", "description": "应用显示名(iOS / Android 显示名)" },
|
|
12
|
-
{
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
{
|
|
13
|
+
"name": "platformUrl",
|
|
14
|
+
"description": "平台地址(页面侧 @adep/client 以此为 baseUrl invoke 云函数)"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "iosBundleId",
|
|
18
|
+
"description": "iOS bundle identifier(top.adep.<slug>,连字符合法)"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "androidPackageId",
|
|
22
|
+
"description": "Android applicationId(top.adep.<slug 剔除连字符>,双端共用一个 appId:capacitor 双端校验禁连字符、下划线形态 iOS 签不了名)"
|
|
23
|
+
}
|
|
15
24
|
]
|
|
16
25
|
}
|