publish-apk 0.0.1-alpha.10
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/LICENSE +21 -0
- package/README.md +795 -0
- package/package.json +40 -0
- package/src/apk.js +694 -0
- package/src/cert.js +1468 -0
- package/src/cli.js +185 -0
- package/src/google.js +1884 -0
- package/src/hap.js +939 -0
- package/src/harmony-material.js +174 -0
- package/src/help.js +50 -0
- package/src/huawei-signing.js +1279 -0
- package/src/huawei.js +3295 -0
- package/src/jwt.js +161 -0
- package/src/profile.js +753 -0
- package/src/publish-apk.js +161 -0
package/src/hap.js
ADDED
|
@@ -0,0 +1,939 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HAP 安装包查看命令(`publish-apk hap`)。
|
|
3
|
+
*
|
|
4
|
+
* 定位:与具体应用市场无关的本地工具——
|
|
5
|
+
* - HAP 本质是 ZIP,读取 pack.info / module.json 输出包名、版本、模块、设备、API 等;
|
|
6
|
+
* - 检测 HAP 签名块(签名块 / Code Sign Block),区分 signed 与 unsigned;
|
|
7
|
+
* - --verbose 追加 ZIP 条目、签名子块与完整 pack.info / module.json。
|
|
8
|
+
*
|
|
9
|
+
* 本命令只读文件,不安装、不验签。签名块存在只说明「带签名结构」,
|
|
10
|
+
* 证书链、摘要、Merkle 树的验签需要额外工具,输出里会明确标注「未验签」。
|
|
11
|
+
* 不引入 npm 第三方依赖:ZIP 解析用 node:zlib,签名状态只解析块头。
|
|
12
|
+
*
|
|
13
|
+
* 退出代码与其它命令一致:0 成功,1 参数错误或文件读取 / 解析失败。
|
|
14
|
+
*/
|
|
15
|
+
import { readFileSync } from 'node:fs'
|
|
16
|
+
import { inflateRawSync } from 'node:zlib'
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
EXIT_OK,
|
|
20
|
+
PROGRAM,
|
|
21
|
+
UsageError,
|
|
22
|
+
isHelpToken,
|
|
23
|
+
parseOptions,
|
|
24
|
+
readEnv,
|
|
25
|
+
stdout,
|
|
26
|
+
} from './cli.js'
|
|
27
|
+
import { certificateDetailLines } from './cert.js'
|
|
28
|
+
import { renderHelp } from './help.js'
|
|
29
|
+
import { parseP7bDer } from './profile.js'
|
|
30
|
+
|
|
31
|
+
/** 命令名 */
|
|
32
|
+
const COMMAND_NAME = 'hap'
|
|
33
|
+
/** HAP 文件路径;必填 */
|
|
34
|
+
const OPTION_FILE = '--file'
|
|
35
|
+
/** 开关:输出全部字段 */
|
|
36
|
+
const OPTION_VERBOSE = '--verbose'
|
|
37
|
+
/** hap 支持的全部选项 */
|
|
38
|
+
const HAP_OPTIONS = new Set([OPTION_FILE, OPTION_VERBOSE])
|
|
39
|
+
/** hap 中的开关型选项 */
|
|
40
|
+
const HAP_FLAGS = new Set([OPTION_VERBOSE])
|
|
41
|
+
const HAP_MULTI_OPTIONS = new Set([OPTION_FILE])
|
|
42
|
+
|
|
43
|
+
/** HAP 文件路径,等价于 --file(必填) */
|
|
44
|
+
const ENV_FILE = 'HAP_FILE'
|
|
45
|
+
/** 开关:输出全部字段,等价于 --verbose */
|
|
46
|
+
const ENV_VERBOSE = 'HAP_VERBOSE'
|
|
47
|
+
|
|
48
|
+
/** app 命令的文件与开关环境变量 */
|
|
49
|
+
const ENV_APP_FILE = 'APP_FILE'
|
|
50
|
+
const ENV_APP_VERBOSE = 'APP_VERBOSE'
|
|
51
|
+
|
|
52
|
+
const VERBOSE_ON_VALUES = new Set(['1', 'true', 'yes', 'on'])
|
|
53
|
+
const VERBOSE_OFF_VALUES = new Set(['0', 'false', 'no', 'off'])
|
|
54
|
+
|
|
55
|
+
/** ZIP 结构签名 */
|
|
56
|
+
const EOCD_SIGNATURE = 0x06054b50
|
|
57
|
+
const CENTRAL_SIGNATURE = 0x02014b50
|
|
58
|
+
const LOCAL_SIGNATURE = 0x04034b50
|
|
59
|
+
|
|
60
|
+
/** HAP 签名块尾部的两个 magic(v2 / v3) */
|
|
61
|
+
const HAP_SIGNATURE_MAGIC_V2 = Buffer.from('HAP Sig Block 42')
|
|
62
|
+
const HAP_SIGNATURE_MAGIC_V3 = Buffer.from('<hap sign block>')
|
|
63
|
+
/** HAP Code Sign Block 头部的 magic(小端 u64 的字节序) */
|
|
64
|
+
const CODE_SIGN_MAGIC = Buffer.from('cdfc8953c6c846e0', 'hex')
|
|
65
|
+
|
|
66
|
+
/** HAP 签名块类型 */
|
|
67
|
+
const SIGN_BLOCK_TYPES = new Map([
|
|
68
|
+
[0x20000000, 'HAP 签名方案 v1'],
|
|
69
|
+
[0x20000001, '未签名 Profile'],
|
|
70
|
+
[0x20000002, '已签名 Profile'],
|
|
71
|
+
[0x20000003, 'Code Sign Block'],
|
|
72
|
+
[0x20000004, '密钥轮换'],
|
|
73
|
+
])
|
|
74
|
+
|
|
75
|
+
// -------------------------------------------------------------- 开关型参数
|
|
76
|
+
|
|
77
|
+
/** 解析 --verbose / HAP_VERBOSE */
|
|
78
|
+
function resolveVerbose(options, env, help, envName = ENV_VERBOSE) {
|
|
79
|
+
if (options.get(OPTION_VERBOSE) === true) {
|
|
80
|
+
return true
|
|
81
|
+
}
|
|
82
|
+
const value = readEnv(env, envName)
|
|
83
|
+
if (value === undefined) {
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
const normalized = value.trim().toLowerCase()
|
|
87
|
+
if (VERBOSE_ON_VALUES.has(normalized)) {
|
|
88
|
+
return true
|
|
89
|
+
}
|
|
90
|
+
if (VERBOSE_OFF_VALUES.has(normalized)) {
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
throw new UsageError(
|
|
94
|
+
`环境变量 ${envName} 的值无效: "${value}"(开启用 1/true/yes/on,关闭用 0/false/no/off)`,
|
|
95
|
+
help(),
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ------------------------------------------------------------------ 帮助文档
|
|
100
|
+
|
|
101
|
+
function renderHapHelp() {
|
|
102
|
+
return renderHelp({
|
|
103
|
+
title: `${PROGRAM} ${COMMAND_NAME} - 查看 HAP 安装包信息`,
|
|
104
|
+
usage: [`${PROGRAM} ${COMMAND_NAME} [选项]`],
|
|
105
|
+
sections: [
|
|
106
|
+
{
|
|
107
|
+
title: '选项',
|
|
108
|
+
entries: [
|
|
109
|
+
[`${OPTION_FILE} <路径...>`, `一个或多个 HAP 文件路径(支持 shell glob),必填(可用 ${ENV_FILE} 补齐)`],
|
|
110
|
+
[
|
|
111
|
+
OPTION_VERBOSE,
|
|
112
|
+
`输出全部字段(ZIP 条目、签名子块、全部签名证书链、完整 pack.info / module.json);命令行未给出时读取 ${ENV_VERBOSE}`,
|
|
113
|
+
],
|
|
114
|
+
['-h, --help', '显示帮助信息'],
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
title: '环境变量',
|
|
119
|
+
entries: [
|
|
120
|
+
[ENV_FILE, `HAP 文件路径,等价于 ${OPTION_FILE}`],
|
|
121
|
+
[ENV_VERBOSE, '输出全部字段,1/true/yes/on 开启、0/false/no/off 关闭,等价于 --verbose'],
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
title: '输出',
|
|
126
|
+
entries: [
|
|
127
|
+
['stdout', 'HAP 包信息与签名证书,每行一条「标签: 值」;--verbose 时追加 ZIP 条目、完整证书链与原始 JSON'],
|
|
128
|
+
['stderr', '错误信息'],
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
title: '退出代码',
|
|
133
|
+
entries: [
|
|
134
|
+
['0', '成功'],
|
|
135
|
+
['1', '参数缺失、参数错误或文件读取 / 解析失败'],
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
notes: [
|
|
140
|
+
'示例:',
|
|
141
|
+
` ${PROGRAM} ${COMMAND_NAME} --file ./app-signed.hap`,
|
|
142
|
+
` ${PROGRAM} ${COMMAND_NAME} --file ./app-unsigned.hap --verbose`,
|
|
143
|
+
` ${ENV_FILE}=./app.hap ${PROGRAM} ${COMMAND_NAME}`,
|
|
144
|
+
` ${ENV_VERBOSE}=1 ${PROGRAM} ${COMMAND_NAME} --file ./app.hap`,
|
|
145
|
+
'',
|
|
146
|
+
'说明: --file 可以一次给多个文件(shell glob 也可),按顺序输出,文件之间空一行',
|
|
147
|
+
'说明: HAP 是 ZIP,读取 pack.info / module.json;签名只检测 HAP 签名块并从内嵌 PKCS#7 提取证书,未执行验签',
|
|
148
|
+
'说明: 默认每组证书只输出叶子证书;--verbose 输出完整证书链',
|
|
149
|
+
'说明: signed / unsigned 的包内容通常相同,区别在是否带 HAP 签名块',
|
|
150
|
+
'说明: 不输出证书 PEM 原文;HAP 里没有私钥',
|
|
151
|
+
],
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function renderAppHelp() {
|
|
156
|
+
return renderHelp({
|
|
157
|
+
title: `${PROGRAM} app - 查看 APP 安装包信息`,
|
|
158
|
+
usage: [`${PROGRAM} app [选项]`],
|
|
159
|
+
sections: [
|
|
160
|
+
{
|
|
161
|
+
title: '选项',
|
|
162
|
+
entries: [
|
|
163
|
+
[`${OPTION_FILE} <路径...>`, `一个或多个 APP 文件路径(支持 shell glob),必填(可用 ${ENV_APP_FILE} 补齐)`],
|
|
164
|
+
[
|
|
165
|
+
OPTION_VERBOSE,
|
|
166
|
+
`输出全部字段(ZIP 条目、签名子块、全部签名证书链、完整 pack.info / pac.json);命令行未给出时读取 ${ENV_APP_VERBOSE}`,
|
|
167
|
+
],
|
|
168
|
+
['-h, --help', '显示帮助信息'],
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
title: '环境变量',
|
|
173
|
+
entries: [
|
|
174
|
+
[ENV_APP_FILE, `APP 文件路径,等价于 ${OPTION_FILE}`],
|
|
175
|
+
[ENV_APP_VERBOSE, '输出全部字段,1/true/yes/on 开启、0/false/no/off 关闭,等价于 --verbose'],
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
title: '输出',
|
|
180
|
+
entries: [
|
|
181
|
+
['stdout', 'APP 包信息、内层模块与签名证书,每行一条「标签: 值」;--verbose 时追加 ZIP 条目、完整证书链与原始 JSON'],
|
|
182
|
+
['stderr', '错误信息'],
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
title: '退出代码',
|
|
187
|
+
entries: [
|
|
188
|
+
['0', '成功'],
|
|
189
|
+
['1', '参数缺失、参数错误或文件读取 / 解析失败'],
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
notes: [
|
|
194
|
+
'示例:',
|
|
195
|
+
` ${PROGRAM} app --file ./app-signed.app`,
|
|
196
|
+
` ${PROGRAM} app --file ./app-unsigned.app --verbose`,
|
|
197
|
+
` ${ENV_APP_FILE}=./app.app ${PROGRAM} app`,
|
|
198
|
+
` ${ENV_APP_VERBOSE}=1 ${PROGRAM} app --file ./app.app`,
|
|
199
|
+
'',
|
|
200
|
+
'说明: --file 可以一次给多个文件(shell glob 也可),按顺序输出,文件之间空一行',
|
|
201
|
+
'说明: APP 是 ZIP,含 pac.json、pack.info 与一个或多个内层 .hap / .hsp;本命令读取外层信息',
|
|
202
|
+
'说明: 签名块在外层,内层 HAP 可能没有单独签名;签名证书从外层 PKCS#7 提取,未执行验签',
|
|
203
|
+
'说明: 默认每组证书只输出叶子证书;--verbose 输出完整证书链',
|
|
204
|
+
'说明: 不输出证书 PEM 原文;APP 里没有私钥',
|
|
205
|
+
],
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ------------------------------------------------------------------ ZIP 解析
|
|
210
|
+
|
|
211
|
+
/** 从文件尾部找 EOCD,返回中央目录位置与条目数 */
|
|
212
|
+
export function readEndOfCentralDirectory(buffer, help) {
|
|
213
|
+
const min = Math.max(0, buffer.length - 22 - 0xffff)
|
|
214
|
+
for (let offset = buffer.length - 22; offset >= min; offset -= 1) {
|
|
215
|
+
if (buffer.readUInt32LE(offset) !== EOCD_SIGNATURE) {
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
const commentLength = buffer.readUInt16LE(offset + 20)
|
|
219
|
+
if (offset + 22 + commentLength !== buffer.length) {
|
|
220
|
+
continue
|
|
221
|
+
}
|
|
222
|
+
const count = buffer.readUInt16LE(offset + 10)
|
|
223
|
+
const centralSize = buffer.readUInt32LE(offset + 12)
|
|
224
|
+
const centralOffset = buffer.readUInt32LE(offset + 16)
|
|
225
|
+
if (count === 0xffff || centralSize === 0xffffffff || centralOffset === 0xffffffff) {
|
|
226
|
+
throw new UsageError('暂不支持 ZIP64 格式的 HAP', help())
|
|
227
|
+
}
|
|
228
|
+
if (centralOffset + centralSize > buffer.length) {
|
|
229
|
+
throw new UsageError('ZIP 中央目录越界,文件可能已损坏', help())
|
|
230
|
+
}
|
|
231
|
+
return { count, centralOffset, centralSize }
|
|
232
|
+
}
|
|
233
|
+
throw new UsageError('不是有效的 ZIP / HAP(找不到 EOCD)', help())
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** 读取中央目录里的条目元数据(不立即解压) */
|
|
237
|
+
export function readZipEntries(buffer, eocd, help) {
|
|
238
|
+
const entries = []
|
|
239
|
+
let offset = eocd.centralOffset
|
|
240
|
+
for (let index = 0; index < eocd.count; index += 1) {
|
|
241
|
+
if (offset + 46 > buffer.length || buffer.readUInt32LE(offset) !== CENTRAL_SIGNATURE) {
|
|
242
|
+
throw new UsageError(`ZIP 中央目录损坏(第 ${index + 1} 个条目)`, help())
|
|
243
|
+
}
|
|
244
|
+
const flags = buffer.readUInt16LE(offset + 8)
|
|
245
|
+
const method = buffer.readUInt16LE(offset + 10)
|
|
246
|
+
const compressedSize = buffer.readUInt32LE(offset + 20)
|
|
247
|
+
const size = buffer.readUInt32LE(offset + 24)
|
|
248
|
+
const nameLength = buffer.readUInt16LE(offset + 28)
|
|
249
|
+
const extraLength = buffer.readUInt16LE(offset + 30)
|
|
250
|
+
const commentLength = buffer.readUInt16LE(offset + 32)
|
|
251
|
+
const localOffset = buffer.readUInt32LE(offset + 42)
|
|
252
|
+
const nameStart = offset + 46
|
|
253
|
+
const nameEnd = nameStart + nameLength
|
|
254
|
+
if (nameEnd > buffer.length) {
|
|
255
|
+
throw new UsageError('ZIP 条目名越界,文件可能已损坏', help())
|
|
256
|
+
}
|
|
257
|
+
if (compressedSize === 0xffffffff || size === 0xffffffff || localOffset === 0xffffffff) {
|
|
258
|
+
throw new UsageError('暂不支持 ZIP64 格式的 HAP', help())
|
|
259
|
+
}
|
|
260
|
+
entries.push({
|
|
261
|
+
name: buffer.subarray(nameStart, nameEnd).toString('utf8'),
|
|
262
|
+
flags,
|
|
263
|
+
method,
|
|
264
|
+
compressedSize,
|
|
265
|
+
size,
|
|
266
|
+
localOffset,
|
|
267
|
+
})
|
|
268
|
+
offset = nameEnd + extraLength + commentLength
|
|
269
|
+
}
|
|
270
|
+
return entries
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** 按文件名找条目;兼容前面带目录的打包方式 */
|
|
274
|
+
export function findZipEntry(entries, name) {
|
|
275
|
+
return (
|
|
276
|
+
entries.find((entry) => entry.name === name) ??
|
|
277
|
+
entries.find((entry) => entry.name.endsWith(`/${name}`)) ??
|
|
278
|
+
null
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** 解压单个 ZIP 条目 */
|
|
283
|
+
export function readZipEntryData(buffer, entry, help) {
|
|
284
|
+
if ((entry.flags & 0x1) !== 0) {
|
|
285
|
+
throw new UsageError(`ZIP 条目已加密,无法读取: ${entry.name}`, help())
|
|
286
|
+
}
|
|
287
|
+
if (entry.localOffset + 30 > buffer.length) {
|
|
288
|
+
throw new UsageError(`ZIP 条目的本地头越界: ${entry.name}`, help())
|
|
289
|
+
}
|
|
290
|
+
if (buffer.readUInt32LE(entry.localOffset) !== LOCAL_SIGNATURE) {
|
|
291
|
+
throw new UsageError(`ZIP 条目的本地头损坏: ${entry.name}`, help())
|
|
292
|
+
}
|
|
293
|
+
const nameLength = buffer.readUInt16LE(entry.localOffset + 26)
|
|
294
|
+
const extraLength = buffer.readUInt16LE(entry.localOffset + 28)
|
|
295
|
+
const dataStart = entry.localOffset + 30 + nameLength + extraLength
|
|
296
|
+
const dataEnd = dataStart + entry.compressedSize
|
|
297
|
+
if (dataEnd > buffer.length) {
|
|
298
|
+
throw new UsageError(`ZIP 条目数据越界: ${entry.name}`, help())
|
|
299
|
+
}
|
|
300
|
+
const compressed = buffer.subarray(dataStart, dataEnd)
|
|
301
|
+
let data
|
|
302
|
+
if (entry.method === 0) {
|
|
303
|
+
data = compressed
|
|
304
|
+
} else if (entry.method === 8) {
|
|
305
|
+
try {
|
|
306
|
+
data = inflateRawSync(compressed)
|
|
307
|
+
} catch (error) {
|
|
308
|
+
throw new UsageError(`ZIP 条目解压失败: ${entry.name}: ${error.message}`, help())
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
throw new UsageError(`不支持的 ZIP 压缩方法 ${entry.method}: ${entry.name}`, help())
|
|
312
|
+
}
|
|
313
|
+
if (data.length !== entry.size) {
|
|
314
|
+
throw new UsageError(
|
|
315
|
+
`ZIP 条目解压后大小不一致: ${entry.name}(${data.length} != ${entry.size})`,
|
|
316
|
+
help(),
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
return data
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** 读取并解析 JSON 条目;不存在返回 null */
|
|
323
|
+
function readJsonEntry(buffer, entries, name, help) {
|
|
324
|
+
const entry = findZipEntry(entries, name)
|
|
325
|
+
if (entry === null) {
|
|
326
|
+
return null
|
|
327
|
+
}
|
|
328
|
+
let text = readZipEntryData(buffer, entry, help).toString('utf8')
|
|
329
|
+
if (text.charCodeAt(0) === 0xfeff) {
|
|
330
|
+
text = text.slice(1)
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
return JSON.parse(text)
|
|
334
|
+
} catch (error) {
|
|
335
|
+
throw new UsageError(`HAP 的 ${name} 不是合法 JSON: ${error.message}`, help())
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ------------------------------------------------------------------ 签名块
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* 读取 HAP 签名块。
|
|
343
|
+
*
|
|
344
|
+
* 签名块紧邻 ZIP 中央目录之前,尾部固定为:
|
|
345
|
+
* u64 size
|
|
346
|
+
* u128 magic
|
|
347
|
+
* u32 version
|
|
348
|
+
* 因此 magic 位于 centralOffset - 20,size 位于 centralOffset - 28。
|
|
349
|
+
*/
|
|
350
|
+
function readHapSignature(buffer, centralOffset) {
|
|
351
|
+
if (centralOffset < 32) {
|
|
352
|
+
return null
|
|
353
|
+
}
|
|
354
|
+
const magicOffset = centralOffset - 20
|
|
355
|
+
const magic = buffer.subarray(magicOffset, magicOffset + 16)
|
|
356
|
+
let magicVersion
|
|
357
|
+
if (magic.equals(HAP_SIGNATURE_MAGIC_V3)) {
|
|
358
|
+
magicVersion = 3
|
|
359
|
+
} else if (magic.equals(HAP_SIGNATURE_MAGIC_V2)) {
|
|
360
|
+
magicVersion = 2
|
|
361
|
+
} else {
|
|
362
|
+
// 少数只带 Code Sign Block 的包没有 HAP 签名块;校验头部,避免把普通资源里的
|
|
363
|
+
// 同名字节误判成签名。
|
|
364
|
+
const codeOffset = buffer.indexOf(CODE_SIGN_MAGIC)
|
|
365
|
+
if (codeOffset < 0 || codeOffset + 32 > centralOffset) {
|
|
366
|
+
return null
|
|
367
|
+
}
|
|
368
|
+
const codeVersion = buffer.readUInt32LE(codeOffset + 8)
|
|
369
|
+
const codeBlockSize = buffer.readUInt32LE(codeOffset + 12)
|
|
370
|
+
const codeSegmentNum = buffer.readUInt32LE(codeOffset + 16)
|
|
371
|
+
const codeFlags = buffer.readUInt32LE(codeOffset + 20)
|
|
372
|
+
if (
|
|
373
|
+
codeVersion !== 1 ||
|
|
374
|
+
codeSegmentNum !== 3 ||
|
|
375
|
+
codeBlockSize < 32 ||
|
|
376
|
+
codeOffset + codeBlockSize > centralOffset ||
|
|
377
|
+
codeFlags < 0 ||
|
|
378
|
+
codeFlags > 3
|
|
379
|
+
) {
|
|
380
|
+
return null
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
fallback: true,
|
|
384
|
+
version: codeVersion,
|
|
385
|
+
size: codeBlockSize,
|
|
386
|
+
count: 0,
|
|
387
|
+
start: codeOffset,
|
|
388
|
+
blocks: [],
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const version = buffer.readUInt32LE(centralOffset - 4)
|
|
393
|
+
const sizeBig = buffer.readBigUInt64LE(centralOffset - 28)
|
|
394
|
+
const count = buffer.readUInt32LE(centralOffset - 32)
|
|
395
|
+
if (sizeBig > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
396
|
+
return {
|
|
397
|
+
magicVersion,
|
|
398
|
+
version,
|
|
399
|
+
size: 0,
|
|
400
|
+
count,
|
|
401
|
+
start: centralOffset,
|
|
402
|
+
blocks: [],
|
|
403
|
+
invalid: true,
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const size = Number(sizeBig)
|
|
407
|
+
const start = centralOffset - size
|
|
408
|
+
if (!Number.isSafeInteger(size) || size < 32 || start < 0 || start >= centralOffset) {
|
|
409
|
+
return {
|
|
410
|
+
magicVersion,
|
|
411
|
+
version,
|
|
412
|
+
size,
|
|
413
|
+
count,
|
|
414
|
+
start,
|
|
415
|
+
blocks: [],
|
|
416
|
+
invalid: true,
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const blocks = []
|
|
421
|
+
for (let index = 0; index < count; index += 1) {
|
|
422
|
+
const header = start + index * 12
|
|
423
|
+
if (header + 12 > centralOffset) {
|
|
424
|
+
break
|
|
425
|
+
}
|
|
426
|
+
const type = buffer.readUInt32LE(header)
|
|
427
|
+
const length = buffer.readUInt32LE(header + 4)
|
|
428
|
+
const offset = buffer.readUInt32LE(header + 8)
|
|
429
|
+
const valueStart = start + offset
|
|
430
|
+
const valueEnd = valueStart + length
|
|
431
|
+
blocks.push({
|
|
432
|
+
type,
|
|
433
|
+
length,
|
|
434
|
+
offset,
|
|
435
|
+
valueStart,
|
|
436
|
+
valueEnd,
|
|
437
|
+
valueValid: valueStart >= start && valueEnd <= centralOffset,
|
|
438
|
+
})
|
|
439
|
+
}
|
|
440
|
+
return { magicVersion, version, size, count, start, blocks }
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** 签名块状态文案 */
|
|
444
|
+
function signatureLabel(signature, kind = 'HAP') {
|
|
445
|
+
const blockName = kind === 'APP' ? 'APP 签名块' : 'HAP 签名块'
|
|
446
|
+
if (signature === null) {
|
|
447
|
+
return `未签名(未检测到 ${blockName})`
|
|
448
|
+
}
|
|
449
|
+
if (signature.fallback) {
|
|
450
|
+
return `已签名(检测到 ${kind} Code Sign Block;未验签)`
|
|
451
|
+
}
|
|
452
|
+
if (signature.invalid) {
|
|
453
|
+
return `已签名(${blockName}头异常;未验签)`
|
|
454
|
+
}
|
|
455
|
+
const version =
|
|
456
|
+
signature.version === undefined || signature.version === null
|
|
457
|
+
? ''
|
|
458
|
+
: ` v${signature.version}`
|
|
459
|
+
return `已签名(${blockName}${version},${signature.count} 个子块;未验签)`
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** 从 HAP 签名块里的 PKCS#7 子块提取证书链 */
|
|
463
|
+
function collectHapSignatureCertificates(signature, buffer) {
|
|
464
|
+
if (signature === null || signature.fallback || signature.invalid) {
|
|
465
|
+
return []
|
|
466
|
+
}
|
|
467
|
+
const groups = []
|
|
468
|
+
for (const block of signature.blocks) {
|
|
469
|
+
if (block.type !== 0x20000000 && block.type !== 0x20000002) {
|
|
470
|
+
continue
|
|
471
|
+
}
|
|
472
|
+
const label = block.type === 0x20000000 ? 'HAP 签名方案 v1' : 'Profile 签名'
|
|
473
|
+
if (!block.valueValid) {
|
|
474
|
+
groups.push({ label, certificates: [], error: '签名子块数据越界' })
|
|
475
|
+
continue
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
const p7b = parseP7bDer(buffer.subarray(block.valueStart, block.valueEnd))
|
|
479
|
+
groups.push({ label, certificates: p7b.certificates })
|
|
480
|
+
} catch (error) {
|
|
481
|
+
groups.push({ label, certificates: [], error: error.message })
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return groups.sort((left, right) => {
|
|
485
|
+
const order = (label) => (label === 'HAP 签名方案 v1' ? 0 : 1)
|
|
486
|
+
return order(left.label) - order(right.label)
|
|
487
|
+
})
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** 证书链里找出叶子证书(不作为任何其它证书的颁发者);找不到时退回第一张 */
|
|
491
|
+
function leafCertificate(certificates) {
|
|
492
|
+
const issuerSubjects = new Set(certificates.map((certificate) => certificate.issuer))
|
|
493
|
+
return (
|
|
494
|
+
certificates.find((certificate) => !issuerSubjects.has(certificate.subject)) ??
|
|
495
|
+
certificates[0]
|
|
496
|
+
)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** 证书链排序:叶子 -> 中间 CA -> 根,找不到的证书按原顺序追加 */
|
|
500
|
+
function orderCertificateChain(certificates) {
|
|
501
|
+
const leaf = leafCertificate(certificates)
|
|
502
|
+
const bySubject = new Map(certificates.map((certificate) => [certificate.subject, certificate]))
|
|
503
|
+
const ordered = [leaf]
|
|
504
|
+
const seen = new Set([leaf])
|
|
505
|
+
let current = leaf
|
|
506
|
+
while (true) {
|
|
507
|
+
const issuer = bySubject.get(current.issuer)
|
|
508
|
+
if (issuer === undefined || seen.has(issuer)) {
|
|
509
|
+
break
|
|
510
|
+
}
|
|
511
|
+
ordered.push(issuer)
|
|
512
|
+
seen.add(issuer)
|
|
513
|
+
current = issuer
|
|
514
|
+
}
|
|
515
|
+
for (const certificate of certificates) {
|
|
516
|
+
if (!seen.has(certificate)) {
|
|
517
|
+
ordered.push(certificate)
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return ordered
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** 输出签名证书;默认只输出每组的第一张,--verbose 输出完整证书链 */
|
|
524
|
+
export function addSignatureCertificates(lines, groups, verbose) {
|
|
525
|
+
if (groups.length === 0) {
|
|
526
|
+
return
|
|
527
|
+
}
|
|
528
|
+
lines.push('', '签名证书:')
|
|
529
|
+
for (const group of groups) {
|
|
530
|
+
if (group.error !== undefined) {
|
|
531
|
+
lines.push(`${group.label}: 解析失败(${group.error})`)
|
|
532
|
+
continue
|
|
533
|
+
}
|
|
534
|
+
if (group.certificates.length === 0) {
|
|
535
|
+
lines.push(`${group.label}: 未找到证书`)
|
|
536
|
+
continue
|
|
537
|
+
}
|
|
538
|
+
lines.push(`${group.label}: ${group.certificates.length} 张`)
|
|
539
|
+
const chain = orderCertificateChain(group.certificates)
|
|
540
|
+
const selected = verbose ? chain : chain.slice(0, 1)
|
|
541
|
+
selected.forEach((certificate, index) => {
|
|
542
|
+
lines.push(`证书 #${index + 1}:`)
|
|
543
|
+
for (const detail of certificateDetailLines(certificate, verbose)) {
|
|
544
|
+
lines.push(` ${detail}`)
|
|
545
|
+
}
|
|
546
|
+
})
|
|
547
|
+
if (!verbose && group.certificates.length > 1) {
|
|
548
|
+
lines.push(` (证书链其余 ${group.certificates.length - 1} 张用 --verbose 查看)`)
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// ------------------------------------------------------------------ 输出
|
|
554
|
+
|
|
555
|
+
function pick(...values) {
|
|
556
|
+
return values.find((value) => value !== undefined && value !== null && value !== '')
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function values(value) {
|
|
560
|
+
return Array.isArray(value) ? value : value === undefined || value === null ? [] : [value]
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function names(value) {
|
|
564
|
+
return values(value)
|
|
565
|
+
.map((item) => (typeof item === 'string' ? item : item?.name))
|
|
566
|
+
.filter((item) => typeof item === 'string' && item !== '')
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export function formatBytes(size) {
|
|
570
|
+
const units = ['B', 'KiB', 'MiB', 'GiB']
|
|
571
|
+
let value = size
|
|
572
|
+
let index = 0
|
|
573
|
+
while (value >= 1024 && index < units.length - 1) {
|
|
574
|
+
value /= 1024
|
|
575
|
+
index += 1
|
|
576
|
+
}
|
|
577
|
+
return index === 0 ? `${size} B` : `${value.toFixed(1)} ${units[index]}`
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function renderHap(
|
|
581
|
+
path,
|
|
582
|
+
size,
|
|
583
|
+
entries,
|
|
584
|
+
pack,
|
|
585
|
+
moduleJson,
|
|
586
|
+
signature,
|
|
587
|
+
certificateGroups,
|
|
588
|
+
verbose,
|
|
589
|
+
options = {},
|
|
590
|
+
) {
|
|
591
|
+
const kind = options.kind ?? 'HAP'
|
|
592
|
+
const pac = options.pac ?? null
|
|
593
|
+
const innerModules = options.innerModules ?? []
|
|
594
|
+
const summary = pack?.summary ?? {}
|
|
595
|
+
const packApp = summary.app ?? {}
|
|
596
|
+
const packVersion = packApp.version ?? {}
|
|
597
|
+
const summaryModules = values(summary.modules)
|
|
598
|
+
const summaryModule = summaryModules[0] ?? {}
|
|
599
|
+
const distro = summaryModule.distro ?? {}
|
|
600
|
+
const api = summaryModule.apiVersion ?? {}
|
|
601
|
+
const packageInfo = values(pack?.packages)[0] ?? {}
|
|
602
|
+
const packageNames = values(pack?.packages)
|
|
603
|
+
.map((item) => item?.name)
|
|
604
|
+
.filter((item) => typeof item === 'string' && item !== '')
|
|
605
|
+
const app = moduleJson?.app ?? {}
|
|
606
|
+
const module = moduleJson?.module ?? {}
|
|
607
|
+
|
|
608
|
+
const bundleName = pick(app.bundleName, packApp.bundleName)
|
|
609
|
+
const versionName = pick(app.versionName, packVersion.name)
|
|
610
|
+
const versionCode = pick(app.versionCode, packVersion.code)
|
|
611
|
+
const build = pick(app.buildVersion, packVersion.build)
|
|
612
|
+
const bundleType = pick(app.bundleType, packApp.bundleType)
|
|
613
|
+
const moduleName = pick(module.name, distro.moduleName)
|
|
614
|
+
const moduleType = pick(module.type, distro.moduleType, packageInfo.moduleType)
|
|
615
|
+
const deviceTypes = values(pick(module.deviceTypes, summaryModule.deviceType, packageInfo.deviceType))
|
|
616
|
+
const abilities = names(pick(module.abilities, summaryModule.abilities))
|
|
617
|
+
const mainAbility = pick(summaryModule.mainAbility, module.mainElement)
|
|
618
|
+
const permissions = names(module.requestPermissions)
|
|
619
|
+
const label = pick(app.label)
|
|
620
|
+
const vendor = pick(app.vendor)
|
|
621
|
+
const compileSdk = pick(app.compileSdkVersion)
|
|
622
|
+
const compileSdkType = pick(app.compileSdkType)
|
|
623
|
+
const minApi = pick(api.compatible, app.minAPIVersion)
|
|
624
|
+
const targetApi = pick(api.target, app.targetAPIVersion)
|
|
625
|
+
const releaseType = pick(api.releaseType, app.apiReleaseType)
|
|
626
|
+
const deliveryWithInstall = pick(
|
|
627
|
+
module.deliveryWithInstall,
|
|
628
|
+
distro.deliveryWithInstall,
|
|
629
|
+
packageInfo.deliveryWithInstall,
|
|
630
|
+
)
|
|
631
|
+
const installationFree = pick(module.installationFree, distro.installationFree)
|
|
632
|
+
const buildMode = pick(app.buildMode)
|
|
633
|
+
const debug = typeof app.debug === 'boolean' ? app.debug : undefined
|
|
634
|
+
|
|
635
|
+
const lines = [
|
|
636
|
+
`文件: ${path}`,
|
|
637
|
+
`大小: ${formatBytes(size)}(${size} 字节)`,
|
|
638
|
+
`格式: ${kind} / ZIP(${entries.length} 个条目)`,
|
|
639
|
+
`签名: ${signatureLabel(signature, kind)}`,
|
|
640
|
+
]
|
|
641
|
+
|
|
642
|
+
if (bundleName !== undefined) {
|
|
643
|
+
lines.push(`包名: ${bundleName}`)
|
|
644
|
+
}
|
|
645
|
+
if (bundleType !== undefined) {
|
|
646
|
+
lines.push(`包类型: ${bundleType}`)
|
|
647
|
+
}
|
|
648
|
+
if (versionName !== undefined || versionCode !== undefined) {
|
|
649
|
+
const version = [versionName, versionCode === undefined ? '' : `(${versionCode})`]
|
|
650
|
+
.filter(Boolean)
|
|
651
|
+
.join(' ')
|
|
652
|
+
lines.push(`版本: ${version}`)
|
|
653
|
+
}
|
|
654
|
+
if (build !== undefined) {
|
|
655
|
+
lines.push(`构建号: ${build}`)
|
|
656
|
+
}
|
|
657
|
+
if (moduleName !== undefined || moduleType !== undefined) {
|
|
658
|
+
const moduleText = [moduleName, moduleType === undefined ? '' : `(${moduleType})`]
|
|
659
|
+
.filter(Boolean)
|
|
660
|
+
.join('')
|
|
661
|
+
lines.push(`模块: ${moduleText}`)
|
|
662
|
+
}
|
|
663
|
+
if (deviceTypes.length > 0) {
|
|
664
|
+
lines.push(`设备: ${deviceTypes.join('、')}`)
|
|
665
|
+
}
|
|
666
|
+
if (mainAbility !== undefined) {
|
|
667
|
+
lines.push(`主 Ability: ${mainAbility}`)
|
|
668
|
+
}
|
|
669
|
+
if (abilities.length > 1) {
|
|
670
|
+
lines.push(`Abilities: ${abilities.join('、')}`)
|
|
671
|
+
}
|
|
672
|
+
if (minApi !== undefined || targetApi !== undefined) {
|
|
673
|
+
const apiText =
|
|
674
|
+
[
|
|
675
|
+
minApi === undefined ? '' : `兼容 ${minApi}`,
|
|
676
|
+
targetApi === undefined ? '' : `目标 ${targetApi}`,
|
|
677
|
+
]
|
|
678
|
+
.filter(Boolean)
|
|
679
|
+
.join(' / ') + (releaseType === undefined ? '' : `(${releaseType})`)
|
|
680
|
+
lines.push(`API: ${apiText}`)
|
|
681
|
+
}
|
|
682
|
+
if (deliveryWithInstall !== undefined || installationFree !== undefined) {
|
|
683
|
+
const install = []
|
|
684
|
+
if (deliveryWithInstall === true) {
|
|
685
|
+
install.push('随应用安装')
|
|
686
|
+
} else if (deliveryWithInstall === false) {
|
|
687
|
+
install.push('按需安装')
|
|
688
|
+
}
|
|
689
|
+
if (installationFree === true) {
|
|
690
|
+
install.push('免安装')
|
|
691
|
+
}
|
|
692
|
+
if (install.length > 0) {
|
|
693
|
+
lines.push(`安装: ${install.join(',')}`)
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
if (buildMode !== undefined) {
|
|
697
|
+
lines.push(`构建模式: ${buildMode}`)
|
|
698
|
+
}
|
|
699
|
+
if (debug !== undefined) {
|
|
700
|
+
lines.push(`调试: ${debug ? '是' : '否'}`)
|
|
701
|
+
}
|
|
702
|
+
if (permissions.length > 0) {
|
|
703
|
+
lines.push(`权限: ${permissions.join('、')}`)
|
|
704
|
+
}
|
|
705
|
+
if (kind === 'APP') {
|
|
706
|
+
for (const item of innerModules) {
|
|
707
|
+
lines.push(`内层模块: ${item.name}(${formatBytes(item.size)})`)
|
|
708
|
+
}
|
|
709
|
+
if (pac !== null) {
|
|
710
|
+
const dataProcess = values(pac.dataProcess).length
|
|
711
|
+
const specialApis = values(pac.specialAPIs).length
|
|
712
|
+
lines.push(`pac.json: 数据处理 ${dataProcess} 项、特殊 API ${specialApis} 项`)
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
addSignatureCertificates(lines, certificateGroups, verbose)
|
|
716
|
+
if (signature !== null && certificateGroups.length === 0) {
|
|
717
|
+
lines.push('', '签名证书: 未解析到证书(仅检测到签名块)')
|
|
718
|
+
}
|
|
719
|
+
if (verbose) {
|
|
720
|
+
if (label !== undefined) {
|
|
721
|
+
lines.push(`应用标签: ${label}`)
|
|
722
|
+
}
|
|
723
|
+
if (vendor !== undefined) {
|
|
724
|
+
lines.push(`厂商: ${vendor}`)
|
|
725
|
+
}
|
|
726
|
+
if (compileSdk !== undefined || compileSdkType !== undefined) {
|
|
727
|
+
const sdk = [compileSdk, compileSdkType === undefined ? '' : `(${compileSdkType})`]
|
|
728
|
+
.filter(Boolean)
|
|
729
|
+
.join('')
|
|
730
|
+
lines.push(`编译 SDK: ${sdk}`)
|
|
731
|
+
}
|
|
732
|
+
if (packageNames.length > 0) {
|
|
733
|
+
lines.push(`模块包: ${packageNames.join('、')}`)
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
lines.push('', '签名块:')
|
|
737
|
+
if (signature === null) {
|
|
738
|
+
lines.push('无')
|
|
739
|
+
} else if (signature.fallback) {
|
|
740
|
+
lines.push(`Code Sign Block 偏移: ${signature.start}`)
|
|
741
|
+
} else {
|
|
742
|
+
lines.push(`版本: ${signature.version}`)
|
|
743
|
+
lines.push(`大小: ${signature.size} 字节`)
|
|
744
|
+
lines.push(`子块数量: ${signature.count}`)
|
|
745
|
+
lines.push(`起始偏移: ${signature.start}`)
|
|
746
|
+
for (const block of signature.blocks) {
|
|
747
|
+
const name = SIGN_BLOCK_TYPES.get(block.type) ?? '未知类型'
|
|
748
|
+
lines.push(`- 0x${block.type.toString(16)} ${name},${block.length} 字节`)
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
lines.push('', 'ZIP 条目:')
|
|
753
|
+
for (const entry of entries) {
|
|
754
|
+
const method = entry.method === 0 ? 'store' : entry.method === 8 ? 'deflate' : `method ${entry.method}`
|
|
755
|
+
lines.push(`- ${entry.name}(${entry.size} 字节,${method})`)
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (pack !== null) {
|
|
759
|
+
lines.push('', `pack.info: ${JSON.stringify(pack)}`)
|
|
760
|
+
}
|
|
761
|
+
if (moduleJson !== null) {
|
|
762
|
+
lines.push(`module.json: ${JSON.stringify(moduleJson)}`)
|
|
763
|
+
}
|
|
764
|
+
if (pac !== null) {
|
|
765
|
+
lines.push(`pac.json: ${JSON.stringify(pac)}`)
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
return lines.join('\n')
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// ------------------------------------------------------------------ 命令入口
|
|
773
|
+
|
|
774
|
+
/** 按 HAP 文件读取 ZIP、pack.info、module.json 与签名块 */
|
|
775
|
+
function parseHap(buffer, path, help) {
|
|
776
|
+
const eocd = readEndOfCentralDirectory(buffer, help)
|
|
777
|
+
const entries = readZipEntries(buffer, eocd, help)
|
|
778
|
+
const pack = readJsonEntry(buffer, entries, 'pack.info', help)
|
|
779
|
+
const moduleJson = readJsonEntry(buffer, entries, 'module.json', help)
|
|
780
|
+
if (pack === null && moduleJson === null) {
|
|
781
|
+
throw new UsageError(
|
|
782
|
+
`不是 HAP:${path} 里没有 pack.info 或 module.json`,
|
|
783
|
+
help(),
|
|
784
|
+
)
|
|
785
|
+
}
|
|
786
|
+
const signature = readHapSignature(buffer, eocd.centralOffset)
|
|
787
|
+
return { entries, pack, moduleJson, signature }
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/** 按 APP 文件读取外层 ZIP、pack.info、pac.json、内层模块与签名块 */
|
|
791
|
+
function parseApp(buffer, path, help) {
|
|
792
|
+
const eocd = readEndOfCentralDirectory(buffer, help)
|
|
793
|
+
const entries = readZipEntries(buffer, eocd, help)
|
|
794
|
+
const pack = readJsonEntry(buffer, entries, 'pack.info', help)
|
|
795
|
+
const pac = readJsonEntry(buffer, entries, 'pac.json', help)
|
|
796
|
+
if (pack === null && pac === null) {
|
|
797
|
+
throw new UsageError(
|
|
798
|
+
`不是 APP:${path} 里没有 pack.info 或 pac.json`,
|
|
799
|
+
help(),
|
|
800
|
+
)
|
|
801
|
+
}
|
|
802
|
+
const innerModules = entries.filter((entry) => /\.(hap|hsp)$/i.test(entry.name))
|
|
803
|
+
const signature = readHapSignature(buffer, eocd.centralOffset)
|
|
804
|
+
return { entries, pack, pac, innerModules, signature }
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function readHapFile(path, help) {
|
|
808
|
+
try {
|
|
809
|
+
return readFileSync(path)
|
|
810
|
+
} catch (error) {
|
|
811
|
+
if (error.code === 'ENOENT') {
|
|
812
|
+
throw new UsageError(`文件不存在: ${path}`, help())
|
|
813
|
+
}
|
|
814
|
+
throw new UsageError(`读取文件失败: ${path}: ${error.message}`, help())
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function runHap(argv, env) {
|
|
819
|
+
if (argv.some((token) => isHelpToken(token))) {
|
|
820
|
+
stdout(renderHapHelp())
|
|
821
|
+
return EXIT_OK
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const options = parseOptions(
|
|
825
|
+
argv,
|
|
826
|
+
HAP_OPTIONS,
|
|
827
|
+
renderHapHelp,
|
|
828
|
+
HAP_FLAGS,
|
|
829
|
+
HAP_MULTI_OPTIONS,
|
|
830
|
+
)
|
|
831
|
+
const verbose = resolveVerbose(options, env, renderHapHelp)
|
|
832
|
+
const envPath = readEnv(env, ENV_FILE)
|
|
833
|
+
const optionPaths = options.get(OPTION_FILE)
|
|
834
|
+
const paths =
|
|
835
|
+
optionPaths === undefined
|
|
836
|
+
? envPath === undefined
|
|
837
|
+
? undefined
|
|
838
|
+
: [envPath]
|
|
839
|
+
: Array.isArray(optionPaths)
|
|
840
|
+
? optionPaths
|
|
841
|
+
: [optionPaths]
|
|
842
|
+
if (paths === undefined) {
|
|
843
|
+
throw new UsageError(
|
|
844
|
+
`缺少必填参数: ${OPTION_FILE}(HAP 文件路径,可多个),可用环境变量 ${ENV_FILE}`,
|
|
845
|
+
renderHapHelp(),
|
|
846
|
+
)
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
for (const [index, path] of paths.entries()) {
|
|
850
|
+
const buffer = readHapFile(path, renderHapHelp)
|
|
851
|
+
const parsed = parseHap(buffer, path, renderHapHelp)
|
|
852
|
+
const certificateGroups = collectHapSignatureCertificates(parsed.signature, buffer)
|
|
853
|
+
stdout(
|
|
854
|
+
renderHap(
|
|
855
|
+
path,
|
|
856
|
+
buffer.length,
|
|
857
|
+
parsed.entries,
|
|
858
|
+
parsed.pack,
|
|
859
|
+
parsed.moduleJson,
|
|
860
|
+
parsed.signature,
|
|
861
|
+
certificateGroups,
|
|
862
|
+
verbose,
|
|
863
|
+
),
|
|
864
|
+
)
|
|
865
|
+
if (index < paths.length - 1) {
|
|
866
|
+
stdout('')
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return EXIT_OK
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function runApp(argv, env) {
|
|
873
|
+
if (argv.some((token) => isHelpToken(token))) {
|
|
874
|
+
stdout(renderAppHelp())
|
|
875
|
+
return EXIT_OK
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const options = parseOptions(
|
|
879
|
+
argv,
|
|
880
|
+
HAP_OPTIONS,
|
|
881
|
+
renderAppHelp,
|
|
882
|
+
HAP_FLAGS,
|
|
883
|
+
HAP_MULTI_OPTIONS,
|
|
884
|
+
)
|
|
885
|
+
const verbose = resolveVerbose(options, env, renderAppHelp, ENV_APP_VERBOSE)
|
|
886
|
+
const envPath = readEnv(env, ENV_APP_FILE)
|
|
887
|
+
const optionPaths = options.get(OPTION_FILE)
|
|
888
|
+
const paths =
|
|
889
|
+
optionPaths === undefined
|
|
890
|
+
? envPath === undefined
|
|
891
|
+
? undefined
|
|
892
|
+
: [envPath]
|
|
893
|
+
: Array.isArray(optionPaths)
|
|
894
|
+
? optionPaths
|
|
895
|
+
: [optionPaths]
|
|
896
|
+
if (paths === undefined) {
|
|
897
|
+
throw new UsageError(
|
|
898
|
+
`缺少必填参数: ${OPTION_FILE}(APP 文件路径,可多个),可用环境变量 ${ENV_APP_FILE}`,
|
|
899
|
+
renderAppHelp(),
|
|
900
|
+
)
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
for (const [index, path] of paths.entries()) {
|
|
904
|
+
const buffer = readHapFile(path, renderAppHelp)
|
|
905
|
+
const parsed = parseApp(buffer, path, renderAppHelp)
|
|
906
|
+
const certificateGroups = collectHapSignatureCertificates(parsed.signature, buffer)
|
|
907
|
+
stdout(
|
|
908
|
+
renderHap(
|
|
909
|
+
path,
|
|
910
|
+
buffer.length,
|
|
911
|
+
parsed.entries,
|
|
912
|
+
parsed.pack,
|
|
913
|
+
null,
|
|
914
|
+
parsed.signature,
|
|
915
|
+
certificateGroups,
|
|
916
|
+
verbose,
|
|
917
|
+
{ kind: 'APP', pac: parsed.pac, innerModules: parsed.innerModules },
|
|
918
|
+
),
|
|
919
|
+
)
|
|
920
|
+
if (index < paths.length - 1) {
|
|
921
|
+
stdout('')
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return EXIT_OK
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/** app 命令描述,由 publish-apk.js 注册到根命令表 */
|
|
928
|
+
export const appCommand = {
|
|
929
|
+
name: 'app',
|
|
930
|
+
summary: '查看 APP 安装包信息(包名 / 版本 / 内层模块 / 签名)',
|
|
931
|
+
run: runApp,
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/** hap 命令描述,由 publish-apk.js 注册到根命令表 */
|
|
935
|
+
export const hapCommand = {
|
|
936
|
+
name: COMMAND_NAME,
|
|
937
|
+
summary: '查看 HAP 安装包信息(包名 / 版本 / API / 签名)',
|
|
938
|
+
run: runHap,
|
|
939
|
+
}
|