@tq1086/urpf-cli 1.0.0
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 +15 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2267 -0
- package/package.json +63 -0
- package/readme.md +631 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2267 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/pack.ts
|
|
7
|
+
import path2 from "path";
|
|
8
|
+
import fs3 from "fs/promises";
|
|
9
|
+
|
|
10
|
+
// src/core/scanner/file-scanner.ts
|
|
11
|
+
import * as fs from "fs/promises";
|
|
12
|
+
import * as path from "path";
|
|
13
|
+
|
|
14
|
+
// src/core/scanner/ignore/ignore-engine.ts
|
|
15
|
+
var IgnoreEngine = class {
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.rules = [];
|
|
18
|
+
this.options = {
|
|
19
|
+
caseSensitive: false,
|
|
20
|
+
pathSeparator: "/",
|
|
21
|
+
gitignoreExtended: true
|
|
22
|
+
};
|
|
23
|
+
if (options) {
|
|
24
|
+
this.options = { ...this.options, ...options };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 添加忽略规则
|
|
29
|
+
* @param rule 忽略规则
|
|
30
|
+
*/
|
|
31
|
+
addRule(rule) {
|
|
32
|
+
this.rules.push(rule);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 添加多个忽略规则
|
|
36
|
+
* @param rules 忽略规则列表
|
|
37
|
+
*/
|
|
38
|
+
addRules(rules) {
|
|
39
|
+
this.rules.push(...rules);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 移除所有规则
|
|
43
|
+
*/
|
|
44
|
+
clear() {
|
|
45
|
+
this.rules = [];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 获取所有规则
|
|
49
|
+
* @returns 规则列表
|
|
50
|
+
*/
|
|
51
|
+
getRules() {
|
|
52
|
+
return [...this.rules];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 匹配文件路径
|
|
56
|
+
* @param filePath 文件路径
|
|
57
|
+
* @param isDirectory 是否为目录
|
|
58
|
+
* @returns 匹配结果
|
|
59
|
+
*/
|
|
60
|
+
match(filePath, isDirectory = false) {
|
|
61
|
+
const normalizedPath = this.normalizePath(filePath);
|
|
62
|
+
let lastMatchedRuleIndex;
|
|
63
|
+
let lastMatchedRule;
|
|
64
|
+
for (let i = 0; i < this.rules.length; i++) {
|
|
65
|
+
const rule = this.rules[i];
|
|
66
|
+
if (rule.isDirectoryOnly && !isDirectory) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (this.matchRule(normalizedPath, rule, isDirectory)) {
|
|
70
|
+
lastMatchedRuleIndex = i;
|
|
71
|
+
lastMatchedRule = rule;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (lastMatchedRule) {
|
|
75
|
+
return {
|
|
76
|
+
matched: !lastMatchedRule.isNegation,
|
|
77
|
+
ruleIndex: lastMatchedRuleIndex,
|
|
78
|
+
rule: lastMatchedRule
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return { matched: false };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 批量匹配文件路径
|
|
85
|
+
* @param filePaths 文件路径列表
|
|
86
|
+
* @param isDirectory 是否为目录
|
|
87
|
+
* @returns 匹配结果列表
|
|
88
|
+
*/
|
|
89
|
+
matchBatch(filePaths, isDirectory = false) {
|
|
90
|
+
return filePaths.map((filePath) => this.match(filePath, isDirectory));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 匹配单个规则
|
|
94
|
+
* @param filePath 文件路径
|
|
95
|
+
* @param rule 忽略规则
|
|
96
|
+
* @param isDirectory 是否为目录
|
|
97
|
+
* @returns 是否匹配
|
|
98
|
+
*/
|
|
99
|
+
matchRule(filePath, rule, isDirectory) {
|
|
100
|
+
const pattern = rule.pattern;
|
|
101
|
+
if (rule.isAbsolute) {
|
|
102
|
+
return this.matchPattern(filePath, pattern, isDirectory, true);
|
|
103
|
+
}
|
|
104
|
+
const parts = filePath.split(this.options.pathSeparator);
|
|
105
|
+
for (let i = 0; i < parts.length; i++) {
|
|
106
|
+
const subPath = parts.slice(i).join(this.options.pathSeparator);
|
|
107
|
+
if (this.matchPattern(subPath, pattern, isDirectory, false)) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* 匹配通配符模式
|
|
115
|
+
* @param filePath 文件路径
|
|
116
|
+
* @param pattern 模式
|
|
117
|
+
* @param isDirectory 是否为目录
|
|
118
|
+
* @param fromStart 是否从开头匹配
|
|
119
|
+
* @returns 是否匹配
|
|
120
|
+
*/
|
|
121
|
+
matchPattern(filePath, pattern, isDirectory, fromStart) {
|
|
122
|
+
const regex = this.patternToRegex(pattern, fromStart);
|
|
123
|
+
if (regex.test(filePath)) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
if (isDirectory && !filePath.endsWith(this.options.pathSeparator)) {
|
|
127
|
+
const dirPath = filePath + this.options.pathSeparator;
|
|
128
|
+
return regex.test(dirPath);
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* 将通配符模式转换为正则表达式
|
|
134
|
+
* @param pattern 通配符模式
|
|
135
|
+
* @param fromStart 是否从开头匹配
|
|
136
|
+
* @returns 正则表达式
|
|
137
|
+
*/
|
|
138
|
+
patternToRegex(pattern, fromStart) {
|
|
139
|
+
let regexPattern = "";
|
|
140
|
+
const specialChars = /[.+?^${}()|[\]\\]/g;
|
|
141
|
+
regexPattern = pattern.replace(specialChars, "\\$&");
|
|
142
|
+
regexPattern = regexPattern.replace(/\*\*/g, ".*");
|
|
143
|
+
regexPattern = regexPattern.replace(/(?<!\.)\*(?!\*)/g, "[^/]*");
|
|
144
|
+
regexPattern = regexPattern.replace(/\?/g, "[^/]");
|
|
145
|
+
regexPattern = regexPattern.replace(/\[!\]/g, "\\[!\\]");
|
|
146
|
+
regexPattern = regexPattern.replace(/\[([^\]]+)\]/g, (match, content) => {
|
|
147
|
+
if (content.startsWith("!")) {
|
|
148
|
+
return `[^${content.substring(1)}]`;
|
|
149
|
+
}
|
|
150
|
+
return `[${content}]`;
|
|
151
|
+
});
|
|
152
|
+
if (fromStart) {
|
|
153
|
+
regexPattern = `^${regexPattern}$`;
|
|
154
|
+
} else {
|
|
155
|
+
regexPattern = `(^|/)${regexPattern}$`;
|
|
156
|
+
}
|
|
157
|
+
const flags = this.options.caseSensitive ? "" : "i";
|
|
158
|
+
return new RegExp(regexPattern, flags);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* 规范化路径
|
|
162
|
+
* @param filePath 文件路径
|
|
163
|
+
* @returns 规范化后的路径
|
|
164
|
+
*/
|
|
165
|
+
normalizePath(filePath) {
|
|
166
|
+
let normalized = filePath;
|
|
167
|
+
normalized = normalized.replace(/\\/g, this.options.pathSeparator);
|
|
168
|
+
if (normalized.startsWith(this.options.pathSeparator)) {
|
|
169
|
+
normalized = normalized.substring(1);
|
|
170
|
+
}
|
|
171
|
+
if (normalized.endsWith(this.options.pathSeparator)) {
|
|
172
|
+
normalized = normalized.slice(0, -1);
|
|
173
|
+
}
|
|
174
|
+
return normalized;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// src/core/scanner/file-scanner.ts
|
|
179
|
+
var FileScanner = class {
|
|
180
|
+
/**
|
|
181
|
+
* 扫描目录
|
|
182
|
+
* @param rootPath 根目录路径
|
|
183
|
+
* @param options 扫描选项
|
|
184
|
+
* @returns 扫描结果
|
|
185
|
+
*/
|
|
186
|
+
static async scanDirectory(rootPath, options = {}) {
|
|
187
|
+
const startTime = Date.now();
|
|
188
|
+
const files = [];
|
|
189
|
+
const directories = [];
|
|
190
|
+
const skippedFiles = [];
|
|
191
|
+
try {
|
|
192
|
+
const absoluteRootPath = path.resolve(rootPath);
|
|
193
|
+
const ignoreEngine = options.ignoreRules ? new IgnoreEngine() : void 0;
|
|
194
|
+
if (ignoreEngine && options.ignoreRules) {
|
|
195
|
+
ignoreEngine.addRules(options.ignoreRules);
|
|
196
|
+
}
|
|
197
|
+
await this.scanRecursive(
|
|
198
|
+
absoluteRootPath,
|
|
199
|
+
"",
|
|
200
|
+
0,
|
|
201
|
+
options,
|
|
202
|
+
files,
|
|
203
|
+
directories,
|
|
204
|
+
skippedFiles,
|
|
205
|
+
ignoreEngine
|
|
206
|
+
);
|
|
207
|
+
const endTime = Date.now();
|
|
208
|
+
const duration = endTime - startTime;
|
|
209
|
+
return {
|
|
210
|
+
rootPath: absoluteRootPath,
|
|
211
|
+
files,
|
|
212
|
+
directories,
|
|
213
|
+
skippedFiles,
|
|
214
|
+
startTime,
|
|
215
|
+
endTime,
|
|
216
|
+
duration
|
|
217
|
+
};
|
|
218
|
+
} catch (error) {
|
|
219
|
+
throw new Error(`\u626B\u63CF\u76EE\u5F55\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* 递归扫描目录
|
|
224
|
+
* @param currentPath 当前路径
|
|
225
|
+
* @param relativePath 相对路径
|
|
226
|
+
* @param currentDepth 当前深度
|
|
227
|
+
* @param options 扫描选项
|
|
228
|
+
* @param files 文件列表
|
|
229
|
+
* @param directories 目录列表
|
|
230
|
+
* @param skippedFiles 被忽略的文件列表
|
|
231
|
+
*/
|
|
232
|
+
static async scanRecursive(currentPath, relativePath, currentDepth, options, files, directories, skippedFiles, ignoreEngine) {
|
|
233
|
+
if (options.maxDepth !== void 0 && options.maxDepth > 0 && currentDepth >= options.maxDepth) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
const entries = await fs.readdir(currentPath, { withFileTypes: true });
|
|
238
|
+
for (const entry of entries) {
|
|
239
|
+
const entryName = entry.name;
|
|
240
|
+
const entryPath = path.join(currentPath, entryName);
|
|
241
|
+
const entryRelativePath = relativePath ? path.join(relativePath, entryName) : entryName;
|
|
242
|
+
if (!options.includeHidden && entryName.startsWith(".")) {
|
|
243
|
+
skippedFiles.push(entryRelativePath);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
const stats = await fs.stat(entryPath);
|
|
248
|
+
const metadata = {
|
|
249
|
+
path: entryRelativePath,
|
|
250
|
+
name: entryName,
|
|
251
|
+
isDirectory: entry.isDirectory(),
|
|
252
|
+
isSymlink: entry.isSymbolicLink(),
|
|
253
|
+
size: stats.size,
|
|
254
|
+
permissions: this.modeToPermissions(stats.mode),
|
|
255
|
+
mtime: stats.mtimeMs,
|
|
256
|
+
ctime: stats.ctimeMs,
|
|
257
|
+
encoding: "utf-8",
|
|
258
|
+
newlineType: "lf",
|
|
259
|
+
isBinary: false
|
|
260
|
+
};
|
|
261
|
+
if (entry.isSymbolicLink()) {
|
|
262
|
+
if (!options.followSymlinks) {
|
|
263
|
+
skippedFiles.push(entryRelativePath);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const realStats = await fs.stat(entryPath);
|
|
267
|
+
metadata.isDirectory = realStats.isDirectory();
|
|
268
|
+
metadata.size = realStats.size;
|
|
269
|
+
}
|
|
270
|
+
if (ignoreEngine) {
|
|
271
|
+
const matchResult = ignoreEngine.match(entryRelativePath, metadata.isDirectory);
|
|
272
|
+
if (matchResult.matched) {
|
|
273
|
+
skippedFiles.push(entryRelativePath);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (options.fileFilter && !options.fileFilter(metadata)) {
|
|
278
|
+
skippedFiles.push(entryRelativePath);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (metadata.isDirectory) {
|
|
282
|
+
directories.push(metadata);
|
|
283
|
+
await this.scanRecursive(
|
|
284
|
+
entryPath,
|
|
285
|
+
entryRelativePath,
|
|
286
|
+
currentDepth + 1,
|
|
287
|
+
options,
|
|
288
|
+
files,
|
|
289
|
+
directories,
|
|
290
|
+
skippedFiles,
|
|
291
|
+
ignoreEngine
|
|
292
|
+
);
|
|
293
|
+
} else {
|
|
294
|
+
files.push(metadata);
|
|
295
|
+
}
|
|
296
|
+
} catch (error) {
|
|
297
|
+
skippedFiles.push(entryRelativePath);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
} catch (error) {
|
|
301
|
+
throw new Error(`\u8BFB\u53D6\u76EE\u5F55\u5931\u8D25: ${currentPath} - ${error instanceof Error ? error.message : String(error)}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* 将文件模式转换为权限字符串
|
|
306
|
+
* @param mode 文件模式
|
|
307
|
+
* @returns 权限字符串(如 "0644")
|
|
308
|
+
*/
|
|
309
|
+
static modeToPermissions(mode) {
|
|
310
|
+
return (mode & 511).toString(8).padStart(3, "0");
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* 计算扫描统计信息
|
|
314
|
+
* @param result 扫描结果
|
|
315
|
+
* @returns 统计信息
|
|
316
|
+
*/
|
|
317
|
+
static calculateStatistics(result) {
|
|
318
|
+
const totalFiles = result.files.length;
|
|
319
|
+
const totalDirectories = result.directories.length;
|
|
320
|
+
const ignoredFiles = result.skippedFiles.length;
|
|
321
|
+
const symlinks = result.files.filter((f) => f.isSymlink).length + result.directories.filter((d) => d.isSymlink).length;
|
|
322
|
+
const totalSize = result.files.reduce((sum, f) => sum + f.size, 0);
|
|
323
|
+
return {
|
|
324
|
+
totalFiles,
|
|
325
|
+
totalDirectories,
|
|
326
|
+
ignoredFiles,
|
|
327
|
+
symlinks,
|
|
328
|
+
totalSize
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* 过滤文件列表
|
|
333
|
+
* @param files 文件列表
|
|
334
|
+
* @param rules 忽略规则
|
|
335
|
+
* @returns 过滤后的文件列表
|
|
336
|
+
*/
|
|
337
|
+
static filterFiles(files, rules) {
|
|
338
|
+
if (rules.length === 0) {
|
|
339
|
+
return files;
|
|
340
|
+
}
|
|
341
|
+
return files;
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// src/core/scanner/ignore/ignore-parser.ts
|
|
346
|
+
var IgnoreRuleParser = class {
|
|
347
|
+
/**
|
|
348
|
+
* 解析忽略规则字符串
|
|
349
|
+
* @param pattern 规则模式字符串
|
|
350
|
+
* @param sourceFile 规则来源文件(可选)
|
|
351
|
+
* @returns 忽略规则
|
|
352
|
+
*/
|
|
353
|
+
parse(pattern, sourceFile) {
|
|
354
|
+
const trimmedPattern = pattern.trim();
|
|
355
|
+
if (!trimmedPattern || trimmedPattern.startsWith("#")) {
|
|
356
|
+
throw new Error("\u65E0\u6548\u7684\u5FFD\u7565\u89C4\u5219\uFF1A\u7A7A\u884C\u6216\u6CE8\u91CA");
|
|
357
|
+
}
|
|
358
|
+
let rulePattern = trimmedPattern;
|
|
359
|
+
let isNegation = false;
|
|
360
|
+
let isDirectoryOnly = false;
|
|
361
|
+
let isAbsolute = false;
|
|
362
|
+
if (rulePattern.startsWith("!")) {
|
|
363
|
+
isNegation = true;
|
|
364
|
+
rulePattern = rulePattern.substring(1).trim();
|
|
365
|
+
}
|
|
366
|
+
if (rulePattern.endsWith("/")) {
|
|
367
|
+
isDirectoryOnly = true;
|
|
368
|
+
rulePattern = rulePattern.slice(0, -1);
|
|
369
|
+
}
|
|
370
|
+
if (rulePattern.startsWith("/")) {
|
|
371
|
+
isAbsolute = true;
|
|
372
|
+
rulePattern = rulePattern.substring(1);
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
pattern: rulePattern,
|
|
376
|
+
isNegation,
|
|
377
|
+
isDirectoryOnly,
|
|
378
|
+
isAbsolute,
|
|
379
|
+
sourceFile
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* 解析忽略文件内容
|
|
384
|
+
* @param content 文件内容
|
|
385
|
+
* @param sourceFile 规则来源文件(可选)
|
|
386
|
+
* @returns 忽略规则列表
|
|
387
|
+
*/
|
|
388
|
+
parseFile(content, sourceFile) {
|
|
389
|
+
const rules = [];
|
|
390
|
+
const lines = content.split("\n");
|
|
391
|
+
for (let i = 0; i < lines.length; i++) {
|
|
392
|
+
const line = lines[i].trim();
|
|
393
|
+
if (!line || line.startsWith("#")) {
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
const rule = this.parse(line, sourceFile);
|
|
398
|
+
rules.push(rule);
|
|
399
|
+
} catch (error) {
|
|
400
|
+
console.warn(`\u7B2C ${i + 1} \u884C: \u65E0\u6548\u7684\u5FFD\u7565\u89C4\u5219 "${line}"`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return rules;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* 解析忽略文件(从文件系统)
|
|
407
|
+
* @param filePath 文件路径
|
|
408
|
+
* @returns 忽略规则列表
|
|
409
|
+
*/
|
|
410
|
+
async parseFileFromPath(filePath) {
|
|
411
|
+
const fs7 = await import("fs/promises");
|
|
412
|
+
const content = await fs7.readFile(filePath, "utf-8");
|
|
413
|
+
return this.parseFile(content, filePath);
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* 从多个文件加载忽略规则
|
|
417
|
+
* @param filePaths 文件路径列表
|
|
418
|
+
* @returns 忽略规则列表
|
|
419
|
+
*/
|
|
420
|
+
async parseMultipleFiles(filePaths) {
|
|
421
|
+
const allRules = [];
|
|
422
|
+
for (const filePath of filePaths) {
|
|
423
|
+
try {
|
|
424
|
+
const rules = await this.parseFileFromPath(filePath);
|
|
425
|
+
allRules.push(...rules);
|
|
426
|
+
} catch (error) {
|
|
427
|
+
console.warn(`\u65E0\u6CD5\u8BFB\u53D6\u5FFD\u7565\u6587\u4EF6: ${filePath}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return allRules;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* 查找并解析项目根目录的忽略文件
|
|
434
|
+
* @param rootPath 项目根目录
|
|
435
|
+
* @param ignoreFileNames 忽略文件名列表
|
|
436
|
+
* @returns 忽略规则列表
|
|
437
|
+
*/
|
|
438
|
+
async findAndParseIgnoreFiles(rootPath, ignoreFileNames = [".gitignore", ".iflowignore", ".npmignore"]) {
|
|
439
|
+
const fs7 = await import("fs/promises");
|
|
440
|
+
const path6 = await import("path");
|
|
441
|
+
const rules = [];
|
|
442
|
+
for (const fileName of ignoreFileNames) {
|
|
443
|
+
const filePath = path6.join(rootPath, fileName);
|
|
444
|
+
try {
|
|
445
|
+
await fs7.access(filePath);
|
|
446
|
+
const fileRules = await this.parseFileFromPath(filePath);
|
|
447
|
+
rules.push(...fileRules);
|
|
448
|
+
} catch {
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return rules;
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
// src/core/generator/urpf-generator.ts
|
|
456
|
+
import crypto from "crypto";
|
|
457
|
+
var URPFGenerator = class {
|
|
458
|
+
constructor(options = {}) {
|
|
459
|
+
this.state = {
|
|
460
|
+
boundaryToken: options.boundaryToken || this.generateBoundaryToken(),
|
|
461
|
+
includeBoundaries: options.includeBoundaries !== false,
|
|
462
|
+
newlineType: options.newlineType || "lf"
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* 生成随机边界令牌(8位十六进制)
|
|
467
|
+
*/
|
|
468
|
+
generateBoundaryToken() {
|
|
469
|
+
return crypto.randomBytes(4).toString("hex");
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* 获取换行符
|
|
473
|
+
*/
|
|
474
|
+
getNewline() {
|
|
475
|
+
return this.state.newlineType === "lf" ? "\n" : "\r\n";
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* 生成开始边界
|
|
479
|
+
*/
|
|
480
|
+
generateStartBoundary() {
|
|
481
|
+
return `--URPF-BOUNDARY-${this.state.boundaryToken}--`;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* 生成结束边界
|
|
485
|
+
*/
|
|
486
|
+
generateEndBoundary() {
|
|
487
|
+
return `--URPF-BOUNDARY-${this.state.boundaryToken}--`;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* 生成分隔符
|
|
491
|
+
*/
|
|
492
|
+
generateDelimiter() {
|
|
493
|
+
return `--URPF-BOUNDARY-${this.state.boundaryToken}--`;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* 生成属性部分
|
|
497
|
+
*/
|
|
498
|
+
generatePropertySection(properties) {
|
|
499
|
+
const newline = this.getNewline();
|
|
500
|
+
const lines = [];
|
|
501
|
+
for (const prop of properties) {
|
|
502
|
+
if (prop.name.includes(" ")) {
|
|
503
|
+
lines.push(`\${${prop.name}}=${prop.value}`);
|
|
504
|
+
} else {
|
|
505
|
+
lines.push(`$${prop.name}=${prop.value}`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return lines.join(newline);
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* 生成资源头部
|
|
512
|
+
*/
|
|
513
|
+
generateResourceHeader(resource) {
|
|
514
|
+
const header = `@${resource.header.udrsReference} ${resource.header.encoding} ${resource.header.permissions} ${resource.header.newlineType}`;
|
|
515
|
+
return header;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* 生成资源部分
|
|
519
|
+
*/
|
|
520
|
+
generateResourceSection(resource) {
|
|
521
|
+
const newline = this.getNewline();
|
|
522
|
+
const header = this.generateResourceHeader(resource);
|
|
523
|
+
const content = resource.content;
|
|
524
|
+
return `${header}${newline}${content}`;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* 生成 URPF 文档
|
|
528
|
+
*/
|
|
529
|
+
async generate(document) {
|
|
530
|
+
const startTime = Date.now();
|
|
531
|
+
const newline = this.getNewline();
|
|
532
|
+
const parts = [];
|
|
533
|
+
if (this.state.includeBoundaries) {
|
|
534
|
+
parts.push(this.generateStartBoundary());
|
|
535
|
+
}
|
|
536
|
+
if (document.properties && document.properties.length > 0) {
|
|
537
|
+
parts.push(this.generatePropertySection(document.properties));
|
|
538
|
+
parts.push(this.generateDelimiter());
|
|
539
|
+
}
|
|
540
|
+
for (const resource of document.resources) {
|
|
541
|
+
parts.push(this.generateResourceSection(resource));
|
|
542
|
+
parts.push(this.generateDelimiter());
|
|
543
|
+
}
|
|
544
|
+
if (this.state.includeBoundaries) {
|
|
545
|
+
parts.pop();
|
|
546
|
+
parts.push(this.generateEndBoundary());
|
|
547
|
+
} else {
|
|
548
|
+
parts.pop();
|
|
549
|
+
}
|
|
550
|
+
const content = parts.join(newline);
|
|
551
|
+
const duration = Date.now() - startTime;
|
|
552
|
+
return {
|
|
553
|
+
content,
|
|
554
|
+
size: Buffer.byteLength(content, "utf-8"),
|
|
555
|
+
resourceCount: document.resources.length,
|
|
556
|
+
duration
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* 获取当前边界令牌
|
|
561
|
+
*/
|
|
562
|
+
getBoundaryToken() {
|
|
563
|
+
return this.state.boundaryToken;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* 设置边界令牌
|
|
567
|
+
*/
|
|
568
|
+
setBoundaryToken(token) {
|
|
569
|
+
if (!/^[0-9a-fA-F]{8}$/.test(token)) {
|
|
570
|
+
throw new Error("\u8FB9\u754C\u4EE4\u724C\u5FC5\u987B\u662F8\u4F4D\u5341\u516D\u8FDB\u5236\u6570\u5B57");
|
|
571
|
+
}
|
|
572
|
+
this.state.boundaryToken = token;
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
// src/core/generator/resource-serializer.ts
|
|
577
|
+
import fs2 from "fs/promises";
|
|
578
|
+
var ResourceSerializer = class {
|
|
579
|
+
constructor(options = {}) {
|
|
580
|
+
this.options = {
|
|
581
|
+
encoding: options.encoding || "utf-8",
|
|
582
|
+
detectEncoding: options.detectEncoding !== false,
|
|
583
|
+
detectNewline: options.detectNewline !== false
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* 检测文件编码
|
|
588
|
+
*/
|
|
589
|
+
async detectFileEncoding(buffer) {
|
|
590
|
+
if (buffer.length >= 3 && buffer[0] === 239 && buffer[1] === 187 && buffer[2] === 191) {
|
|
591
|
+
return "utf-8";
|
|
592
|
+
}
|
|
593
|
+
if (buffer.length >= 2 && buffer[0] === 255 && buffer[1] === 254) {
|
|
594
|
+
return "utf-16le";
|
|
595
|
+
}
|
|
596
|
+
if (buffer.length >= 2 && buffer[0] === 254 && buffer[1] === 255) {
|
|
597
|
+
return "utf-16be";
|
|
598
|
+
}
|
|
599
|
+
try {
|
|
600
|
+
Buffer.from(buffer.toString("utf-8"), "utf-8");
|
|
601
|
+
return "utf-8";
|
|
602
|
+
} catch {
|
|
603
|
+
try {
|
|
604
|
+
Buffer.from(buffer.toString("gbk"), "gbk");
|
|
605
|
+
return "gbk";
|
|
606
|
+
} catch {
|
|
607
|
+
return "binary";
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* 检测换行符类型
|
|
613
|
+
*/
|
|
614
|
+
detectNewlineType(buffer) {
|
|
615
|
+
const content = buffer.toString("utf-8");
|
|
616
|
+
let crlfCount = 0;
|
|
617
|
+
let lfCount = 0;
|
|
618
|
+
let crCount = 0;
|
|
619
|
+
for (let i = 0; i < content.length; i++) {
|
|
620
|
+
if (content[i] === "\r" && content[i + 1] === "\n") {
|
|
621
|
+
crlfCount++;
|
|
622
|
+
i++;
|
|
623
|
+
} else if (content[i] === "\n") {
|
|
624
|
+
lfCount++;
|
|
625
|
+
} else if (content[i] === "\r") {
|
|
626
|
+
crCount++;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
if (crlfCount >= lfCount && crlfCount >= crCount) {
|
|
630
|
+
return "crlf";
|
|
631
|
+
} else if (lfCount >= crCount) {
|
|
632
|
+
return "lf";
|
|
633
|
+
} else {
|
|
634
|
+
return "cr";
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* 转换文件权限为八进制字符串
|
|
639
|
+
*/
|
|
640
|
+
convertPermissions(mode) {
|
|
641
|
+
const permissions = mode & 511;
|
|
642
|
+
return permissions.toString(8).padStart(3, "0");
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* 读取文件内容
|
|
646
|
+
*/
|
|
647
|
+
async readFile(filePath, encoding) {
|
|
648
|
+
if (encoding === "binary") {
|
|
649
|
+
const buffer = await fs2.readFile(filePath);
|
|
650
|
+
return buffer.toString("base64");
|
|
651
|
+
} else {
|
|
652
|
+
return await fs2.readFile(filePath, encoding);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* 序列化文件为资源部分
|
|
657
|
+
*/
|
|
658
|
+
async serializeFile(filePath, metadata, udrsReference) {
|
|
659
|
+
let encoding = this.options.encoding || "utf-8";
|
|
660
|
+
let newlineType = "lf";
|
|
661
|
+
if (this.options.detectEncoding) {
|
|
662
|
+
const buffer = await fs2.readFile(filePath);
|
|
663
|
+
encoding = await this.detectFileEncoding(buffer);
|
|
664
|
+
}
|
|
665
|
+
if (this.options.detectNewline && encoding !== "binary") {
|
|
666
|
+
const buffer = await fs2.readFile(filePath);
|
|
667
|
+
newlineType = this.detectNewlineType(buffer);
|
|
668
|
+
}
|
|
669
|
+
const content = await this.readFile(filePath, encoding);
|
|
670
|
+
const permissions = this.convertPermissions(metadata.permissions);
|
|
671
|
+
const header = {
|
|
672
|
+
udrsReference,
|
|
673
|
+
encoding,
|
|
674
|
+
permissions,
|
|
675
|
+
newlineType
|
|
676
|
+
};
|
|
677
|
+
return {
|
|
678
|
+
header,
|
|
679
|
+
content
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* 批量序列化文件
|
|
684
|
+
*/
|
|
685
|
+
async serializeFiles(files) {
|
|
686
|
+
const resources = [];
|
|
687
|
+
for (const file of files) {
|
|
688
|
+
const resource = await this.serializeFile(file.filePath, file.metadata, file.udrsReference);
|
|
689
|
+
resources.push(resource);
|
|
690
|
+
}
|
|
691
|
+
return resources;
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
// src/commands/pack.ts
|
|
696
|
+
var PackCommand = class {
|
|
697
|
+
/**
|
|
698
|
+
* 执行 pack 命令
|
|
699
|
+
*/
|
|
700
|
+
async execute(inputPath, options = {}) {
|
|
701
|
+
const startTime = Date.now();
|
|
702
|
+
try {
|
|
703
|
+
const resolvedPath = path2.resolve(inputPath);
|
|
704
|
+
await this.validatePath(resolvedPath);
|
|
705
|
+
const outputPath = options.output || this.generateOutputPath(resolvedPath);
|
|
706
|
+
const scanResult = await this.scanFiles(resolvedPath, options);
|
|
707
|
+
const urpfDocument = await this.generateURPFDocument(scanResult, resolvedPath);
|
|
708
|
+
const urpfResult = await this.saveURPFDocument(urpfDocument, outputPath, options);
|
|
709
|
+
const duration = Date.now() - startTime;
|
|
710
|
+
if (options.verbose) {
|
|
711
|
+
this.logVerbose(scanResult, urpfResult, outputPath, duration);
|
|
712
|
+
}
|
|
713
|
+
return {
|
|
714
|
+
success: true,
|
|
715
|
+
outputPath,
|
|
716
|
+
fileCount: scanResult.files.length,
|
|
717
|
+
directoryCount: scanResult.directories.length,
|
|
718
|
+
skippedCount: scanResult.skippedFiles.length,
|
|
719
|
+
urpfSize: urpfResult.size,
|
|
720
|
+
duration
|
|
721
|
+
};
|
|
722
|
+
} catch (err) {
|
|
723
|
+
const duration = Date.now() - startTime;
|
|
724
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
725
|
+
if (options.verbose) {
|
|
726
|
+
console.error(`\u9519\u8BEF: ${errorMessage}`);
|
|
727
|
+
}
|
|
728
|
+
return {
|
|
729
|
+
success: false,
|
|
730
|
+
outputPath: "",
|
|
731
|
+
fileCount: 0,
|
|
732
|
+
directoryCount: 0,
|
|
733
|
+
skippedCount: 0,
|
|
734
|
+
urpfSize: 0,
|
|
735
|
+
duration,
|
|
736
|
+
error: errorMessage
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* 验证输入路径
|
|
742
|
+
*/
|
|
743
|
+
async validatePath(filePath) {
|
|
744
|
+
try {
|
|
745
|
+
const stats = await fs3.stat(filePath);
|
|
746
|
+
if (!stats.isFile() && !stats.isDirectory()) {
|
|
747
|
+
throw new Error(`\u8DEF\u5F84\u4E0D\u662F\u6587\u4EF6\u6216\u76EE\u5F55: ${filePath}`);
|
|
748
|
+
}
|
|
749
|
+
} catch {
|
|
750
|
+
throw new Error(`\u65E0\u6CD5\u8BBF\u95EE\u8DEF\u5F84: ${filePath}`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* 生成输出路径
|
|
755
|
+
*/
|
|
756
|
+
generateOutputPath(inputPath) {
|
|
757
|
+
return `${inputPath}.urpf`;
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* 扫描文件
|
|
761
|
+
*/
|
|
762
|
+
async scanFiles(rootPath, options) {
|
|
763
|
+
const stats = await fs3.stat(rootPath);
|
|
764
|
+
if (stats.isFile()) {
|
|
765
|
+
const metadata = await this.collectFileMetadata(rootPath);
|
|
766
|
+
return {
|
|
767
|
+
files: [metadata],
|
|
768
|
+
directories: [],
|
|
769
|
+
skippedFiles: [],
|
|
770
|
+
statistics: {
|
|
771
|
+
totalFiles: 1,
|
|
772
|
+
totalDirectories: 0,
|
|
773
|
+
skippedFiles: 0
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
let ignoreRules = [];
|
|
778
|
+
if (options.ignoreFile) {
|
|
779
|
+
const parser = new IgnoreRuleParser();
|
|
780
|
+
const content = await fs3.readFile(options.ignoreFile, "utf-8");
|
|
781
|
+
ignoreRules = parser.parseFile(content);
|
|
782
|
+
} else {
|
|
783
|
+
const defaultIgnoreFiles = [".gitignore", ".iflowignore"];
|
|
784
|
+
for (const ignoreFile of defaultIgnoreFiles) {
|
|
785
|
+
const ignorePath = path2.join(rootPath, ignoreFile);
|
|
786
|
+
try {
|
|
787
|
+
await fs3.access(ignorePath);
|
|
788
|
+
const parser = new IgnoreRuleParser();
|
|
789
|
+
const content = await fs3.readFile(ignorePath, "utf-8");
|
|
790
|
+
ignoreRules = [...ignoreRules, ...parser.parseFile(content)];
|
|
791
|
+
break;
|
|
792
|
+
} catch {
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
const effectiveMaxDepth = options.noRecurse ? 1 : options.maxDepth;
|
|
797
|
+
return await FileScanner.scanDirectory(rootPath, {
|
|
798
|
+
ignoreRules,
|
|
799
|
+
followSymlinks: options.followSymlinks || false,
|
|
800
|
+
maxDepth: effectiveMaxDepth
|
|
801
|
+
// undefined 或 1
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* 收集文件元数据
|
|
806
|
+
*/
|
|
807
|
+
async collectFileMetadata(filePath) {
|
|
808
|
+
const stats = await fs3.stat(filePath);
|
|
809
|
+
return {
|
|
810
|
+
path: filePath,
|
|
811
|
+
name: path2.basename(filePath),
|
|
812
|
+
size: stats.size,
|
|
813
|
+
permissions: stats.mode,
|
|
814
|
+
mtime: stats.mtime,
|
|
815
|
+
encoding: "utf-8",
|
|
816
|
+
newlineType: "lf"
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* 生成 URPF 文档
|
|
821
|
+
*/
|
|
822
|
+
async generateURPFDocument(scanResult, rootPath) {
|
|
823
|
+
const serializer = new ResourceSerializer();
|
|
824
|
+
const files = scanResult.files.map((file) => {
|
|
825
|
+
const isAbsolutePath = path2.isAbsolute(file.path);
|
|
826
|
+
const filePath = isAbsolutePath ? file.path : path2.join(rootPath, file.path);
|
|
827
|
+
const relativePath = isAbsolutePath ? path2.relative(rootPath, file.path) : file.path;
|
|
828
|
+
return {
|
|
829
|
+
filePath,
|
|
830
|
+
metadata: file,
|
|
831
|
+
udrsReference: `file://${relativePath}`
|
|
832
|
+
};
|
|
833
|
+
});
|
|
834
|
+
const resources = await serializer.serializeFiles(files);
|
|
835
|
+
return {
|
|
836
|
+
boundaryToken: "",
|
|
837
|
+
// 将由生成器自动生成
|
|
838
|
+
properties: [],
|
|
839
|
+
resources
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* 保存 URPF 文件
|
|
844
|
+
*/
|
|
845
|
+
async saveURPFDocument(document, outputPath, options) {
|
|
846
|
+
const generator = new URPFGenerator();
|
|
847
|
+
const result = await generator.generate(document);
|
|
848
|
+
const outputDir = path2.dirname(outputPath);
|
|
849
|
+
await fs3.mkdir(outputDir, { recursive: true });
|
|
850
|
+
try {
|
|
851
|
+
await fs3.access(outputPath);
|
|
852
|
+
if (!options.force) {
|
|
853
|
+
throw new Error(`\u8F93\u51FA\u6587\u4EF6\u5DF2\u5B58\u5728: ${outputPath}\u3002\u8BF7\u4F7F\u7528 --force \u9009\u9879\u5F3A\u5236\u8986\u76D6\uFF0C\u6216\u6307\u5B9A\u4E0D\u540C\u7684\u8F93\u51FA\u8DEF\u5F84\u3002`);
|
|
854
|
+
}
|
|
855
|
+
} catch (error) {
|
|
856
|
+
if (error.code !== "ENOENT") {
|
|
857
|
+
throw error;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
await fs3.writeFile(outputPath, result.content, "utf-8");
|
|
861
|
+
return result;
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* 输出详细日志
|
|
865
|
+
*/
|
|
866
|
+
logVerbose(scanResult, urpfResult, outputPath, duration) {
|
|
867
|
+
console.log("URPF \u6253\u5305\u5B8C\u6210:");
|
|
868
|
+
console.log(` \u8F93\u51FA\u6587\u4EF6: ${outputPath}`);
|
|
869
|
+
console.log(` \u6587\u4EF6\u6570\u91CF: ${scanResult.files.length}`);
|
|
870
|
+
console.log(` \u76EE\u5F55\u6570\u91CF: ${scanResult.directories.length}`);
|
|
871
|
+
console.log(` \u8DF3\u8FC7\u6587\u4EF6: ${scanResult.skippedFiles.length}`);
|
|
872
|
+
console.log(` URPF \u5927\u5C0F: ${urpfResult.size} \u5B57\u8282`);
|
|
873
|
+
console.log(` \u8D44\u6E90\u6570\u91CF: ${urpfResult.resourceCount}`);
|
|
874
|
+
console.log(` \u6267\u884C\u8017\u65F6: ${duration}ms`);
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
// src/commands/unpack.ts
|
|
879
|
+
import path3 from "path";
|
|
880
|
+
import fs4 from "fs/promises";
|
|
881
|
+
|
|
882
|
+
// src/core/parser/types.ts
|
|
883
|
+
var URPFParseError = class extends Error {
|
|
884
|
+
constructor(message, line, column) {
|
|
885
|
+
super(message);
|
|
886
|
+
this.line = line;
|
|
887
|
+
this.column = column;
|
|
888
|
+
this.name = "URPFParseError";
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
|
|
892
|
+
// src/core/parser/boundary-parser.ts
|
|
893
|
+
var BoundaryParser = class _BoundaryParser {
|
|
894
|
+
static {
|
|
895
|
+
/** 边界前缀 */
|
|
896
|
+
this.BOUNDARY_PREFIX = "--URPF-BOUNDARY-";
|
|
897
|
+
}
|
|
898
|
+
static {
|
|
899
|
+
/** 边界后缀 */
|
|
900
|
+
this.BOUNDARY_SUFFIX = "--";
|
|
901
|
+
}
|
|
902
|
+
static {
|
|
903
|
+
/** 边界令牌正则表达式(8位十六进制) */
|
|
904
|
+
this.TOKEN_REGEX = /^[0-9a-fA-F]{8}$/;
|
|
905
|
+
}
|
|
906
|
+
static {
|
|
907
|
+
/** 完整边界正则表达式 */
|
|
908
|
+
this.BOUNDARY_REGEX = /^--URPF-BOUNDARY-([0-9a-fA-F]{8})--$/;
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* 验证边界令牌格式
|
|
912
|
+
* @param token 边界令牌
|
|
913
|
+
* @returns 是否有效
|
|
914
|
+
*/
|
|
915
|
+
static isValidToken(token) {
|
|
916
|
+
return _BoundaryParser.TOKEN_REGEX.test(token);
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* 提取边界令牌
|
|
920
|
+
* @param line 边界行
|
|
921
|
+
* @returns 边界令牌,如果无效则返回 null
|
|
922
|
+
*/
|
|
923
|
+
static extractToken(line) {
|
|
924
|
+
const match = line.match(_BoundaryParser.BOUNDARY_REGEX);
|
|
925
|
+
return match ? match[1].toLowerCase() : null;
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* 判断是否为边界行
|
|
929
|
+
* @param line 文本行
|
|
930
|
+
* @returns 是否为边界行
|
|
931
|
+
*/
|
|
932
|
+
static isBoundaryLine(line) {
|
|
933
|
+
return _BoundaryParser.BOUNDARY_REGEX.test(line);
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* 生成边界行
|
|
937
|
+
* @param token 边界令牌
|
|
938
|
+
* @param type 边界类型
|
|
939
|
+
* @returns 边界行字符串
|
|
940
|
+
*/
|
|
941
|
+
static generateBoundaryLine(token, type) {
|
|
942
|
+
return `${_BoundaryParser.BOUNDARY_PREFIX}${token}${_BoundaryParser.BOUNDARY_SUFFIX}`;
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* 生成随机边界令牌
|
|
946
|
+
* @returns 随机边界令牌
|
|
947
|
+
*/
|
|
948
|
+
static generateRandomToken() {
|
|
949
|
+
let token = "";
|
|
950
|
+
const hexChars = "0123456789abcdef";
|
|
951
|
+
for (let i = 0; i < 8; i++) {
|
|
952
|
+
token += hexChars[Math.floor(Math.random() * 16)];
|
|
953
|
+
}
|
|
954
|
+
return token;
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* 查找所有边界行
|
|
958
|
+
* @param content 内容字符串
|
|
959
|
+
* @returns 边界匹配结果列表
|
|
960
|
+
*/
|
|
961
|
+
static findBoundaries(content) {
|
|
962
|
+
const lines = content.split("\n");
|
|
963
|
+
const boundaries = [];
|
|
964
|
+
lines.forEach((line, index) => {
|
|
965
|
+
if (_BoundaryParser.isBoundaryLine(line)) {
|
|
966
|
+
const token = _BoundaryParser.extractToken(line);
|
|
967
|
+
if (token) {
|
|
968
|
+
const lineNumber = index + 1;
|
|
969
|
+
let type;
|
|
970
|
+
if (index === 0) {
|
|
971
|
+
type = "start" /* START */;
|
|
972
|
+
} else if (index === lines.length - 1) {
|
|
973
|
+
type = "end" /* END */;
|
|
974
|
+
} else {
|
|
975
|
+
type = "delimiter" /* DELIMITER */;
|
|
976
|
+
}
|
|
977
|
+
boundaries.push({ type, token, line: lineNumber });
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
});
|
|
981
|
+
return boundaries;
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* 验证边界一致性
|
|
985
|
+
* @param boundaries 边界列表
|
|
986
|
+
* @throws {URPFParseError} 如果边界不一致
|
|
987
|
+
*/
|
|
988
|
+
static validateBoundaries(boundaries) {
|
|
989
|
+
if (boundaries.length < 2) {
|
|
990
|
+
throw new URPFParseError(
|
|
991
|
+
"URPF \u6587\u6863\u5FC5\u987B\u5305\u542B\u81F3\u5C11\u5F00\u59CB\u8FB9\u754C\u548C\u7ED3\u675F\u8FB9\u754C"
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
if (boundaries[0].type !== "start" /* START */) {
|
|
995
|
+
throw new URPFParseError(
|
|
996
|
+
`\u7B2C ${boundaries[0].line} \u884C: \u6587\u6863\u5FC5\u987B\u4EE5\u5F00\u59CB\u8FB9\u754C\u5F00\u5934`,
|
|
997
|
+
boundaries[0].line
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
if (boundaries[boundaries.length - 1].type !== "end" /* END */) {
|
|
1001
|
+
throw new URPFParseError(
|
|
1002
|
+
`\u7B2C ${boundaries[boundaries.length - 1].line} \u884C: \u6587\u6863\u5FC5\u987B\u4EE5\u7ED3\u675F\u8FB9\u754C\u7ED3\u5C3E`,
|
|
1003
|
+
boundaries[boundaries.length - 1].line
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
const token = boundaries[0].token;
|
|
1007
|
+
for (let i = 1; i < boundaries.length; i++) {
|
|
1008
|
+
if (boundaries[i].token !== token) {
|
|
1009
|
+
throw new URPFParseError(
|
|
1010
|
+
`\u7B2C ${boundaries[i].line} \u884C: \u8FB9\u754C\u4EE4\u724C\u4E0D\u4E00\u81F4\uFF0C\u671F\u671B "${token}"\uFF0C\u5B9E\u9645 "${boundaries[i].token}"`,
|
|
1011
|
+
boundaries[i].line
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* 根据边界分割内容
|
|
1018
|
+
* @param content 内容字符串
|
|
1019
|
+
* @param boundaries 边界列表
|
|
1020
|
+
* @returns 分割后的内容段列表
|
|
1021
|
+
*/
|
|
1022
|
+
static splitByBoundaries(content, boundaries) {
|
|
1023
|
+
const lines = content.split("\n");
|
|
1024
|
+
const sections = [];
|
|
1025
|
+
for (let i = 0; i < boundaries.length - 1; i++) {
|
|
1026
|
+
const startLine = boundaries[i].line - 1;
|
|
1027
|
+
const endLine = boundaries[i + 1].line - 1;
|
|
1028
|
+
if (endLine > startLine + 1) {
|
|
1029
|
+
const sectionLines = lines.slice(startLine + 1, endLine);
|
|
1030
|
+
sections.push(sectionLines.join("\n"));
|
|
1031
|
+
} else {
|
|
1032
|
+
sections.push("");
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
return sections;
|
|
1036
|
+
}
|
|
1037
|
+
};
|
|
1038
|
+
|
|
1039
|
+
// src/core/parser/property-parser.ts
|
|
1040
|
+
var PropertyParser = class _PropertyParser {
|
|
1041
|
+
static {
|
|
1042
|
+
/** 简单变量名正则表达式($name) */
|
|
1043
|
+
this.SIMPLE_NAME_REGEX = /^\$([a-zA-Z0-9_]+)$/;
|
|
1044
|
+
}
|
|
1045
|
+
static {
|
|
1046
|
+
/** 带空格变量名正则表达式(${full name}) */
|
|
1047
|
+
this.EXTENDED_NAME_REGEX = /^\$\{([a-zA-Z0-9_ ]+)\}$/;
|
|
1048
|
+
}
|
|
1049
|
+
static {
|
|
1050
|
+
/** 属性行正则表达式 */
|
|
1051
|
+
this.PROPERTY_LINE_REGEX = /^(.+?)=(.*)$/;
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* 解析属性行
|
|
1055
|
+
* @param line 属性行
|
|
1056
|
+
* @param lineNo 行号
|
|
1057
|
+
* @returns 属性项
|
|
1058
|
+
* @throws {URPFParseError} 如果格式无效
|
|
1059
|
+
*/
|
|
1060
|
+
static parsePropertyLine(line, lineNo) {
|
|
1061
|
+
const trimmedLine = line.trim();
|
|
1062
|
+
if (!trimmedLine || trimmedLine.startsWith("#")) {
|
|
1063
|
+
throw new URPFParseError(`\u7B2C ${lineNo} \u884C: \u7A7A\u884C\u6216\u6CE8\u91CA`, lineNo);
|
|
1064
|
+
}
|
|
1065
|
+
const match = trimmedLine.match(_PropertyParser.PROPERTY_LINE_REGEX);
|
|
1066
|
+
if (!match) {
|
|
1067
|
+
throw new URPFParseError(
|
|
1068
|
+
`\u7B2C ${lineNo} \u884C: \u65E0\u6548\u7684\u5C5E\u6027\u884C\u683C\u5F0F\uFF0C\u5E94\u4E3A "name=value"`,
|
|
1069
|
+
lineNo
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
const namePart = match[1].trim();
|
|
1073
|
+
const valuePart = match[2];
|
|
1074
|
+
const simpleMatch = namePart.match(_PropertyParser.SIMPLE_NAME_REGEX);
|
|
1075
|
+
const extendedMatch = namePart.match(_PropertyParser.EXTENDED_NAME_REGEX);
|
|
1076
|
+
let name;
|
|
1077
|
+
let isExtended = false;
|
|
1078
|
+
if (simpleMatch) {
|
|
1079
|
+
name = simpleMatch[1];
|
|
1080
|
+
isExtended = false;
|
|
1081
|
+
} else if (extendedMatch) {
|
|
1082
|
+
name = extendedMatch[1];
|
|
1083
|
+
isExtended = true;
|
|
1084
|
+
} else {
|
|
1085
|
+
throw new URPFParseError(
|
|
1086
|
+
`\u7B2C ${lineNo} \u884C: \u65E0\u6548\u7684\u53D8\u91CF\u540D\u683C\u5F0F\uFF0C\u5E94\u4E3A "$name" \u6216 "\${name with space}"`,
|
|
1087
|
+
lineNo
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
return {
|
|
1091
|
+
name,
|
|
1092
|
+
value: valuePart,
|
|
1093
|
+
isExtended
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* 解析属性部分
|
|
1098
|
+
* @param content 属性部分内容
|
|
1099
|
+
* @param startLineNo 起始行号
|
|
1100
|
+
* @returns 属性部分
|
|
1101
|
+
* @throws {URPFParseError} 如果解析失败
|
|
1102
|
+
*/
|
|
1103
|
+
static parsePropertySection(content, startLineNo = 1) {
|
|
1104
|
+
const lines = content.split("\n");
|
|
1105
|
+
const properties = [];
|
|
1106
|
+
const nameSet = /* @__PURE__ */ new Set();
|
|
1107
|
+
lines.forEach((line, index) => {
|
|
1108
|
+
const lineNo = startLineNo + index;
|
|
1109
|
+
const trimmedLine = line.trim();
|
|
1110
|
+
if (!trimmedLine || trimmedLine.startsWith("#")) {
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
const property = _PropertyParser.parsePropertyLine(line, lineNo);
|
|
1114
|
+
if (nameSet.has(property.name)) {
|
|
1115
|
+
throw new URPFParseError(
|
|
1116
|
+
`\u7B2C ${lineNo} \u884C: \u53D8\u91CF\u540D "${property.name}" \u91CD\u590D\u5B9A\u4E49`,
|
|
1117
|
+
lineNo
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
nameSet.add(property.name);
|
|
1121
|
+
properties.push(property);
|
|
1122
|
+
});
|
|
1123
|
+
return { properties };
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* 检查内容是否为属性部分
|
|
1127
|
+
* @param content 内容
|
|
1128
|
+
* @returns 是否为属性部分
|
|
1129
|
+
*/
|
|
1130
|
+
static isPropertySection(content) {
|
|
1131
|
+
const lines = content.split("\n");
|
|
1132
|
+
for (const line of lines) {
|
|
1133
|
+
const trimmedLine = line.trim();
|
|
1134
|
+
if (!trimmedLine || trimmedLine.startsWith("#")) {
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
return trimmedLine.startsWith("$") || trimmedLine.startsWith("${");
|
|
1138
|
+
}
|
|
1139
|
+
return false;
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* 生成属性行
|
|
1143
|
+
* @param name 变量名
|
|
1144
|
+
* @param value 变量值
|
|
1145
|
+
* @param isExtended 是否为带空格格式
|
|
1146
|
+
* @returns 属性行字符串
|
|
1147
|
+
*/
|
|
1148
|
+
static generatePropertyLine(name, value, isExtended = false) {
|
|
1149
|
+
const namePart = isExtended ? `\${${name}}` : `$${name}`;
|
|
1150
|
+
return `${namePart}=${value}`;
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* 生成属性部分内容
|
|
1154
|
+
* @param propertySection 属性部分
|
|
1155
|
+
* @returns 属性部分内容字符串
|
|
1156
|
+
*/
|
|
1157
|
+
static generatePropertySectionContent(propertySection) {
|
|
1158
|
+
return propertySection.properties.map((p) => _PropertyParser.generatePropertyLine(p.name, p.value, p.isExtended)).join("\n");
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
|
|
1162
|
+
// src/core/parser/resource-parser.ts
|
|
1163
|
+
var VALID_ENCODINGS = [
|
|
1164
|
+
"utf-8",
|
|
1165
|
+
"ascii",
|
|
1166
|
+
"gbk",
|
|
1167
|
+
"gb2312",
|
|
1168
|
+
"iso-8859-1",
|
|
1169
|
+
"binary"
|
|
1170
|
+
];
|
|
1171
|
+
var VALID_NEWLINES = ["lf", "cr", "crlf"];
|
|
1172
|
+
var ResourceParser = class _ResourceParser {
|
|
1173
|
+
static {
|
|
1174
|
+
/** 资源头部正则表达式 */
|
|
1175
|
+
this.RESOURCE_HEADER_REGEX = /^@(\S+)\s+(\S+)\s+(\d{3})\s+(lf|cr|crlf)$/i;
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* 解析资源头部
|
|
1179
|
+
* @param line 资源头部行
|
|
1180
|
+
* @param lineNo 行号
|
|
1181
|
+
* @returns 资源头部
|
|
1182
|
+
* @throws {URPFParseError} 如果格式无效
|
|
1183
|
+
*/
|
|
1184
|
+
static parseResourceHeader(line, lineNo) {
|
|
1185
|
+
const trimmedLine = line.trim();
|
|
1186
|
+
if (!trimmedLine.startsWith("@")) {
|
|
1187
|
+
throw new URPFParseError(
|
|
1188
|
+
`\u7B2C ${lineNo} \u884C: \u8D44\u6E90\u5934\u90E8\u5FC5\u987B\u4EE5 '@' \u5F00\u5934`,
|
|
1189
|
+
lineNo
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
const match = trimmedLine.match(_ResourceParser.RESOURCE_HEADER_REGEX);
|
|
1193
|
+
if (!match) {
|
|
1194
|
+
throw new URPFParseError(
|
|
1195
|
+
`\u7B2C ${lineNo} \u884C: \u65E0\u6548\u7684\u8D44\u6E90\u5934\u90E8\u683C\u5F0F\uFF0C\u5E94\u4E3A "@<udrs-reference> <encoding> <permissions> <newline-type>"`,
|
|
1196
|
+
lineNo
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
const udrsRefString = match[1];
|
|
1200
|
+
const encoding = match[2].toLowerCase();
|
|
1201
|
+
const permissions = match[3];
|
|
1202
|
+
const newlineType = match[4].toLowerCase();
|
|
1203
|
+
if (!VALID_ENCODINGS.includes(encoding)) {
|
|
1204
|
+
throw new URPFParseError(
|
|
1205
|
+
`\u7B2C ${lineNo} \u884C: \u65E0\u6548\u7684\u7F16\u7801\u7C7B\u578B "${encoding}"\uFF0C\u6709\u6548\u503C\u4E3A: ${VALID_ENCODINGS.join(", ")}`,
|
|
1206
|
+
lineNo
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
if (!VALID_NEWLINES.includes(newlineType)) {
|
|
1210
|
+
throw new URPFParseError(
|
|
1211
|
+
`\u7B2C ${lineNo} \u884C: \u65E0\u6548\u7684\u6362\u884C\u7B26\u7C7B\u578B "${newlineType}"\uFF0C\u6709\u6548\u503C\u4E3A: ${VALID_NEWLINES.join(", ")}`,
|
|
1212
|
+
lineNo
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
const udrsReference = _ResourceParser.parseUDRSReference(udrsRefString, lineNo);
|
|
1216
|
+
return {
|
|
1217
|
+
udrsReference,
|
|
1218
|
+
encoding,
|
|
1219
|
+
permissions,
|
|
1220
|
+
newlineType
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* 解析 UDRS 引用
|
|
1225
|
+
* @param udrsRefString UDRS 引用字符串
|
|
1226
|
+
* @param lineNo 行号
|
|
1227
|
+
* @returns UDRS 引用
|
|
1228
|
+
* @throws {URPFParseError} 如果格式无效
|
|
1229
|
+
*/
|
|
1230
|
+
static parseUDRSReference(udrsRefString, lineNo) {
|
|
1231
|
+
const protocolMatch = udrsRefString.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\/(.*)$/);
|
|
1232
|
+
if (!protocolMatch) {
|
|
1233
|
+
throw new URPFParseError(
|
|
1234
|
+
`\u7B2C ${lineNo} \u884C: \u65E0\u6548\u7684 UDRS \u5F15\u7528\u683C\u5F0F "${udrsRefString}"\uFF0C\u5E94\u4E3A "<protocol>://<uri>[#<fragment>]"`,
|
|
1235
|
+
lineNo
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
const protocol = protocolMatch[1].toLowerCase();
|
|
1239
|
+
const uriAndFragment = protocolMatch[2];
|
|
1240
|
+
const fragmentIndex = uriAndFragment.indexOf("#");
|
|
1241
|
+
const uri = fragmentIndex !== -1 ? uriAndFragment.substring(0, fragmentIndex) : uriAndFragment;
|
|
1242
|
+
const fragment = fragmentIndex !== -1 ? uriAndFragment.substring(fragmentIndex + 1) : void 0;
|
|
1243
|
+
let fragmentType;
|
|
1244
|
+
if (fragment) {
|
|
1245
|
+
if (fragment.startsWith("::line=")) {
|
|
1246
|
+
fragmentType = "raw-line";
|
|
1247
|
+
} else if (fragment.startsWith("::byte=")) {
|
|
1248
|
+
fragmentType = "raw-byte";
|
|
1249
|
+
} else {
|
|
1250
|
+
fragmentType = "structured";
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
return {
|
|
1254
|
+
protocol,
|
|
1255
|
+
uri,
|
|
1256
|
+
fragment,
|
|
1257
|
+
fragmentType
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* 解析资源内容
|
|
1262
|
+
* @param content 资源内容字符串
|
|
1263
|
+
* @param encoding 编码类型
|
|
1264
|
+
* @param newlineType 换行符类型
|
|
1265
|
+
* @returns 资源内容(Buffer 或 string)
|
|
1266
|
+
*/
|
|
1267
|
+
static parseResourceContent(content, encoding, newlineType) {
|
|
1268
|
+
if (encoding === "binary") {
|
|
1269
|
+
return Buffer.from(content, "binary");
|
|
1270
|
+
}
|
|
1271
|
+
let normalizedContent = content;
|
|
1272
|
+
switch (newlineType) {
|
|
1273
|
+
case "lf":
|
|
1274
|
+
break;
|
|
1275
|
+
case "cr":
|
|
1276
|
+
normalizedContent = content.replace(/\r\n/g, "\r").replace(/\n/g, "\r");
|
|
1277
|
+
break;
|
|
1278
|
+
case "crlf":
|
|
1279
|
+
normalizedContent = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\r\n");
|
|
1280
|
+
break;
|
|
1281
|
+
}
|
|
1282
|
+
return normalizedContent;
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* 解析资源部分
|
|
1286
|
+
* @param content 资源部分内容
|
|
1287
|
+
* @param startLineNo 起始行号
|
|
1288
|
+
* @returns 资源部分
|
|
1289
|
+
* @throws {URPFParseError} 如果解析失败
|
|
1290
|
+
*/
|
|
1291
|
+
static parseResourceSection(content, startLineNo = 1) {
|
|
1292
|
+
const lines = content.split("\n");
|
|
1293
|
+
if (lines.length === 0) {
|
|
1294
|
+
throw new URPFParseError(
|
|
1295
|
+
`\u7B2C ${startLineNo} \u884C: \u8D44\u6E90\u90E8\u5206\u4E0D\u80FD\u4E3A\u7A7A`,
|
|
1296
|
+
startLineNo
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
const header = _ResourceParser.parseResourceHeader(lines[0], startLineNo);
|
|
1300
|
+
const contentLines = lines.slice(1);
|
|
1301
|
+
const contentString = contentLines.join("\n");
|
|
1302
|
+
const parsedContent = _ResourceParser.parseResourceContent(
|
|
1303
|
+
contentString,
|
|
1304
|
+
header.encoding,
|
|
1305
|
+
header.newlineType
|
|
1306
|
+
);
|
|
1307
|
+
return {
|
|
1308
|
+
header,
|
|
1309
|
+
content: parsedContent
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* 检查内容是否为资源部分
|
|
1314
|
+
* @param content 内容
|
|
1315
|
+
* @returns 是否为资源部分
|
|
1316
|
+
*/
|
|
1317
|
+
static isResourceSection(content) {
|
|
1318
|
+
const lines = content.split("\n");
|
|
1319
|
+
if (lines.length === 0) {
|
|
1320
|
+
return false;
|
|
1321
|
+
}
|
|
1322
|
+
return lines[0].trim().startsWith("@");
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* 生成资源头部行
|
|
1326
|
+
* @param header 资源头部
|
|
1327
|
+
* @returns 资源头部行字符串
|
|
1328
|
+
*/
|
|
1329
|
+
static generateResourceHeaderLine(header) {
|
|
1330
|
+
const udrsRefString = _ResourceParser.generateUDRSReferenceString(header.udrsReference);
|
|
1331
|
+
return `@${udrsRefString} ${header.encoding} ${header.permissions} ${header.newlineType}`;
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* 生成 UDRS 引用字符串
|
|
1335
|
+
* @param udrsReference UDRS 引用
|
|
1336
|
+
* @returns UDRS 引用字符串
|
|
1337
|
+
*/
|
|
1338
|
+
static generateUDRSReferenceString(udrsReference) {
|
|
1339
|
+
let result = `${udrsReference.protocol}://${udrsReference.uri}`;
|
|
1340
|
+
if (udrsReference.fragment) {
|
|
1341
|
+
result += `#${udrsReference.fragment}`;
|
|
1342
|
+
}
|
|
1343
|
+
return result;
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* 生成资源部分内容
|
|
1347
|
+
* @param resourceSection 资源部分
|
|
1348
|
+
* @returns 资源部分内容字符串
|
|
1349
|
+
*/
|
|
1350
|
+
static generateResourceSectionContent(resourceSection) {
|
|
1351
|
+
const headerLine = _ResourceParser.generateResourceHeaderLine(resourceSection.header);
|
|
1352
|
+
let content = "";
|
|
1353
|
+
if (typeof resourceSection.content === "string") {
|
|
1354
|
+
content = resourceSection.content;
|
|
1355
|
+
} else {
|
|
1356
|
+
content = resourceSection.content.toString("binary");
|
|
1357
|
+
}
|
|
1358
|
+
return `${headerLine}
|
|
1359
|
+
${content}`;
|
|
1360
|
+
}
|
|
1361
|
+
};
|
|
1362
|
+
|
|
1363
|
+
// src/core/parser/variable-replacer.ts
|
|
1364
|
+
var VariableReplacer = class _VariableReplacer {
|
|
1365
|
+
static {
|
|
1366
|
+
/** 变量引用正则表达式(${name} 或 ${full name}) */
|
|
1367
|
+
this.VARIABLE_REF_REGEX = /\$\{([a-zA-Z0-9_ ]+)\}/g;
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* 创建变量上下文
|
|
1371
|
+
* @param properties 属性列表
|
|
1372
|
+
* @returns 变量上下文
|
|
1373
|
+
*/
|
|
1374
|
+
static createContext(properties) {
|
|
1375
|
+
const map = /* @__PURE__ */ new Map();
|
|
1376
|
+
properties.forEach((p) => map.set(p.name, p.value));
|
|
1377
|
+
return {
|
|
1378
|
+
get(name) {
|
|
1379
|
+
return map.get(name);
|
|
1380
|
+
},
|
|
1381
|
+
has(name) {
|
|
1382
|
+
return map.has(name);
|
|
1383
|
+
},
|
|
1384
|
+
keys() {
|
|
1385
|
+
return Array.from(map.keys());
|
|
1386
|
+
}
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* 替换字符串中的变量引用
|
|
1391
|
+
* @param input 输入字符串
|
|
1392
|
+
* @param context 变量上下文
|
|
1393
|
+
* @param lineNo 行号(用于错误报告)
|
|
1394
|
+
* @returns 替换后的字符串
|
|
1395
|
+
* @throws {URPFParseError} 如果变量未定义
|
|
1396
|
+
*/
|
|
1397
|
+
static replace(input, context, lineNo) {
|
|
1398
|
+
return input.replace(_VariableReplacer.VARIABLE_REF_REGEX, (match, name) => {
|
|
1399
|
+
const trimmedName = name.trim();
|
|
1400
|
+
if (!context.has(trimmedName)) {
|
|
1401
|
+
const lineInfo = lineNo !== void 0 ? `\u7B2C ${lineNo} \u884C: ` : "";
|
|
1402
|
+
throw new URPFParseError(
|
|
1403
|
+
`${lineInfo}\u672A\u5B9A\u4E49\u7684\u53D8\u91CF: "${trimmedName}"`,
|
|
1404
|
+
lineNo
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1407
|
+
const value = context.get(trimmedName);
|
|
1408
|
+
return value || "";
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* 检查字符串中是否包含变量引用
|
|
1413
|
+
* @param input 输入字符串
|
|
1414
|
+
* @returns 是否包含变量引用
|
|
1415
|
+
*/
|
|
1416
|
+
static hasVariableReferences(input) {
|
|
1417
|
+
return _VariableReplacer.VARIABLE_REF_REGEX.test(input);
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* 提取字符串中的所有变量名
|
|
1421
|
+
* @param input 输入字符串
|
|
1422
|
+
* @returns 变量名列表
|
|
1423
|
+
*/
|
|
1424
|
+
static extractVariableNames(input) {
|
|
1425
|
+
const names = [];
|
|
1426
|
+
let match;
|
|
1427
|
+
while ((match = _VariableReplacer.VARIABLE_REF_REGEX.exec(input)) !== null) {
|
|
1428
|
+
names.push(match[1].trim());
|
|
1429
|
+
}
|
|
1430
|
+
return [...new Set(names)];
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* 验证所有变量引用是否已定义
|
|
1434
|
+
* @param input 输入字符串
|
|
1435
|
+
* @param context 变量上下文
|
|
1436
|
+
* @param lineNo 行号(用于错误报告)
|
|
1437
|
+
* @returns 是否所有变量都已定义
|
|
1438
|
+
*/
|
|
1439
|
+
static validateVariables(input, context, lineNo) {
|
|
1440
|
+
const names = _VariableReplacer.extractVariableNames(input);
|
|
1441
|
+
for (const name of names) {
|
|
1442
|
+
if (!context.has(name)) {
|
|
1443
|
+
const lineInfo = lineNo !== void 0 ? `\u7B2C ${lineNo} \u884C: ` : "";
|
|
1444
|
+
throw new URPFParseError(
|
|
1445
|
+
`${lineInfo}\u672A\u5B9A\u4E49\u7684\u53D8\u91CF: "${name}"`,
|
|
1446
|
+
lineNo
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
return true;
|
|
1451
|
+
}
|
|
1452
|
+
};
|
|
1453
|
+
|
|
1454
|
+
// src/core/parser/urpf-parser.ts
|
|
1455
|
+
var URPFParser = class _URPFParser {
|
|
1456
|
+
/**
|
|
1457
|
+
* 解析 URPF 文档
|
|
1458
|
+
* @param content URPF 文档内容
|
|
1459
|
+
* @param options 解析选项
|
|
1460
|
+
* @returns 解析结果
|
|
1461
|
+
* @throws {URPFParseError} 如果解析失败
|
|
1462
|
+
*/
|
|
1463
|
+
static parse(content, options = {}) {
|
|
1464
|
+
const warnings = [];
|
|
1465
|
+
const strict = options.strict !== false;
|
|
1466
|
+
try {
|
|
1467
|
+
const boundaries = BoundaryParser.findBoundaries(content);
|
|
1468
|
+
if (strict) {
|
|
1469
|
+
BoundaryParser.validateBoundaries(boundaries);
|
|
1470
|
+
} else {
|
|
1471
|
+
try {
|
|
1472
|
+
BoundaryParser.validateBoundaries(boundaries);
|
|
1473
|
+
} catch (error) {
|
|
1474
|
+
if (error instanceof URPFParseError) {
|
|
1475
|
+
warnings.push(error.message);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
if (boundaries.length < 2) {
|
|
1480
|
+
throw new URPFParseError("URPF \u6587\u6863\u5FC5\u987B\u5305\u542B\u81F3\u5C11\u5F00\u59CB\u8FB9\u754C\u548C\u7ED3\u675F\u8FB9\u754C");
|
|
1481
|
+
}
|
|
1482
|
+
const boundaryToken = boundaries[0].token;
|
|
1483
|
+
const sections = BoundaryParser.splitByBoundaries(content, boundaries);
|
|
1484
|
+
if (sections.length === 0) {
|
|
1485
|
+
throw new URPFParseError("URPF \u6587\u6863\u4E0D\u5305\u542B\u4EFB\u4F55\u90E8\u5206");
|
|
1486
|
+
}
|
|
1487
|
+
const document = {
|
|
1488
|
+
boundaryToken,
|
|
1489
|
+
resourceSections: []
|
|
1490
|
+
};
|
|
1491
|
+
let startSectionIndex = 0;
|
|
1492
|
+
if (sections.length > 0 && PropertyParser.isPropertySection(sections[0])) {
|
|
1493
|
+
try {
|
|
1494
|
+
document.propertySection = PropertyParser.parsePropertySection(sections[0], boundaries[0].line + 1);
|
|
1495
|
+
const variableContext = VariableReplacer.createContext(document.propertySection.properties);
|
|
1496
|
+
for (let i = 1; i < sections.length; i++) {
|
|
1497
|
+
try {
|
|
1498
|
+
const resourceSection = ResourceParser.parseResourceSection(sections[i], boundaries[i].line + 1);
|
|
1499
|
+
const udrsRefString = ResourceParser.generateUDRSReferenceString(resourceSection.header.udrsReference);
|
|
1500
|
+
const replacedRefString = VariableReplacer.replace(udrsRefString, variableContext);
|
|
1501
|
+
resourceSection.header.udrsReference = ResourceParser.parseUDRSReference(replacedRefString, boundaries[i].line + 1);
|
|
1502
|
+
document.resourceSections.push(resourceSection);
|
|
1503
|
+
} catch (error) {
|
|
1504
|
+
if (strict) {
|
|
1505
|
+
throw error;
|
|
1506
|
+
} else if (error instanceof Error) {
|
|
1507
|
+
warnings.push(`\u7B2C ${boundaries[i].line + 1} \u884C: ${error.message}`);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
startSectionIndex = 1;
|
|
1512
|
+
} catch (error) {
|
|
1513
|
+
if (strict) {
|
|
1514
|
+
throw error;
|
|
1515
|
+
} else if (error instanceof Error) {
|
|
1516
|
+
warnings.push(`\u7B2C ${boundaries[0].line + 1} \u884C: ${error.message}`);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
if (startSectionIndex === 0) {
|
|
1521
|
+
for (let i = startSectionIndex; i < sections.length; i++) {
|
|
1522
|
+
try {
|
|
1523
|
+
const resourceSection = ResourceParser.parseResourceSection(sections[i], boundaries[i].line + 1);
|
|
1524
|
+
document.resourceSections.push(resourceSection);
|
|
1525
|
+
} catch (error) {
|
|
1526
|
+
if (strict) {
|
|
1527
|
+
throw error;
|
|
1528
|
+
} else if (error instanceof Error) {
|
|
1529
|
+
warnings.push(`\u7B2C ${boundaries[i].line + 1} \u884C: ${error.message}`);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
if (document.resourceSections.length === 0) {
|
|
1535
|
+
warnings.push("URPF \u6587\u6863\u4E0D\u5305\u542B\u4EFB\u4F55\u8D44\u6E90\u90E8\u5206");
|
|
1536
|
+
}
|
|
1537
|
+
return {
|
|
1538
|
+
document,
|
|
1539
|
+
warnings
|
|
1540
|
+
};
|
|
1541
|
+
} catch (error) {
|
|
1542
|
+
if (error instanceof URPFParseError) {
|
|
1543
|
+
throw error;
|
|
1544
|
+
}
|
|
1545
|
+
throw new URPFParseError(`\u89E3\u6790\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
/**
|
|
1549
|
+
* 解析 URPF 文档(从文件)
|
|
1550
|
+
* @param filePath 文件路径
|
|
1551
|
+
* @param options 解析选项
|
|
1552
|
+
* @returns 解析结果
|
|
1553
|
+
* @throws {URPFParseError} 如果解析失败
|
|
1554
|
+
*/
|
|
1555
|
+
static async parseFile(filePath, options = {}) {
|
|
1556
|
+
const fs7 = await import("fs/promises");
|
|
1557
|
+
const content = await fs7.readFile(filePath, "utf-8");
|
|
1558
|
+
return _URPFParser.parse(content, options);
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* 生成 URPF 文档
|
|
1562
|
+
* @param document URPF 文档
|
|
1563
|
+
* @returns URPF 文档字符串
|
|
1564
|
+
*/
|
|
1565
|
+
static generate(document) {
|
|
1566
|
+
const lines = [];
|
|
1567
|
+
lines.push(BoundaryParser.generateBoundaryLine(document.boundaryToken, "start" /* START */));
|
|
1568
|
+
if (document.propertySection) {
|
|
1569
|
+
lines.push(PropertyParser.generatePropertySectionContent(document.propertySection));
|
|
1570
|
+
lines.push(BoundaryParser.generateBoundaryLine(document.boundaryToken, "delimiter" /* DELIMITER */));
|
|
1571
|
+
}
|
|
1572
|
+
document.resourceSections.forEach((resourceSection, index) => {
|
|
1573
|
+
lines.push(ResourceParser.generateResourceSectionContent(resourceSection));
|
|
1574
|
+
if (index < document.resourceSections.length - 1) {
|
|
1575
|
+
lines.push(BoundaryParser.generateBoundaryLine(document.boundaryToken, "delimiter" /* DELIMITER */));
|
|
1576
|
+
}
|
|
1577
|
+
});
|
|
1578
|
+
lines.push(BoundaryParser.generateBoundaryLine(document.boundaryToken, "end" /* END */));
|
|
1579
|
+
return lines.join("\n");
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1582
|
+
* 验证 URPF 文档
|
|
1583
|
+
* @param document URPF 文档
|
|
1584
|
+
* @returns 验证结果
|
|
1585
|
+
*/
|
|
1586
|
+
static validate(document) {
|
|
1587
|
+
const errors = [];
|
|
1588
|
+
if (!BoundaryParser.isValidToken(document.boundaryToken)) {
|
|
1589
|
+
errors.push(`\u65E0\u6548\u7684\u8FB9\u754C\u4EE4\u724C: ${document.boundaryToken}`);
|
|
1590
|
+
}
|
|
1591
|
+
if (document.propertySection) {
|
|
1592
|
+
const propertyNames = /* @__PURE__ */ new Set();
|
|
1593
|
+
for (const property of document.propertySection.properties) {
|
|
1594
|
+
if (propertyNames.has(property.name)) {
|
|
1595
|
+
errors.push(`\u91CD\u590D\u7684\u53D8\u91CF\u540D: ${property.name}`);
|
|
1596
|
+
}
|
|
1597
|
+
propertyNames.add(property.name);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
if (document.resourceSections.length === 0) {
|
|
1601
|
+
errors.push("URPF \u6587\u6863\u5FC5\u987B\u5305\u542B\u81F3\u5C11\u4E00\u4E2A\u8D44\u6E90\u90E8\u5206");
|
|
1602
|
+
}
|
|
1603
|
+
return {
|
|
1604
|
+
valid: errors.length === 0,
|
|
1605
|
+
errors
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
};
|
|
1609
|
+
|
|
1610
|
+
// src/commands/unpack.ts
|
|
1611
|
+
var UnpackCommand = class {
|
|
1612
|
+
/**
|
|
1613
|
+
* 执行 unpack 命令
|
|
1614
|
+
*/
|
|
1615
|
+
async execute(urpfFilePath, options = {}) {
|
|
1616
|
+
const startTime = Date.now();
|
|
1617
|
+
try {
|
|
1618
|
+
const resolvedPath = path3.resolve(urpfFilePath);
|
|
1619
|
+
await this.validateURPFFile(resolvedPath);
|
|
1620
|
+
const outputDir = options.output ? path3.resolve(options.output) : process.cwd();
|
|
1621
|
+
await fs4.mkdir(outputDir, { recursive: true });
|
|
1622
|
+
const parseResult = await URPFParser.parseFile(resolvedPath);
|
|
1623
|
+
const results = [];
|
|
1624
|
+
let totalBytes = 0;
|
|
1625
|
+
for (const resource of parseResult.document.resourceSections) {
|
|
1626
|
+
const result = await this.extractResource(
|
|
1627
|
+
resource,
|
|
1628
|
+
outputDir,
|
|
1629
|
+
options
|
|
1630
|
+
);
|
|
1631
|
+
if (result.success && result.action !== "skipped") {
|
|
1632
|
+
totalBytes += this.getFileSize(resource);
|
|
1633
|
+
}
|
|
1634
|
+
results.push(result);
|
|
1635
|
+
}
|
|
1636
|
+
const duration = Date.now() - startTime;
|
|
1637
|
+
const createdCount = results.filter((r) => r.action === "created").length;
|
|
1638
|
+
const overwrittenCount = results.filter((r) => r.action === "overwritten").length;
|
|
1639
|
+
const skippedCount = results.filter((r) => r.action === "skipped").length;
|
|
1640
|
+
const failedCount = results.filter((r) => r.action === "error").length;
|
|
1641
|
+
if (options.verbose) {
|
|
1642
|
+
this.logVerbose(results, outputDir, duration);
|
|
1643
|
+
}
|
|
1644
|
+
return {
|
|
1645
|
+
success: failedCount === 0,
|
|
1646
|
+
outputDir,
|
|
1647
|
+
files: results,
|
|
1648
|
+
createdCount,
|
|
1649
|
+
overwrittenCount,
|
|
1650
|
+
skippedCount,
|
|
1651
|
+
failedCount,
|
|
1652
|
+
totalBytes,
|
|
1653
|
+
duration
|
|
1654
|
+
};
|
|
1655
|
+
} catch (err) {
|
|
1656
|
+
const duration = Date.now() - startTime;
|
|
1657
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1658
|
+
if (options.verbose) {
|
|
1659
|
+
console.error(`\u9519\u8BEF: ${errorMessage}`);
|
|
1660
|
+
}
|
|
1661
|
+
return {
|
|
1662
|
+
success: false,
|
|
1663
|
+
outputDir: options.output ? path3.resolve(options.output) : process.cwd(),
|
|
1664
|
+
files: [],
|
|
1665
|
+
createdCount: 0,
|
|
1666
|
+
overwrittenCount: 0,
|
|
1667
|
+
skippedCount: 0,
|
|
1668
|
+
failedCount: 0,
|
|
1669
|
+
totalBytes: 0,
|
|
1670
|
+
duration,
|
|
1671
|
+
error: errorMessage
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
/**
|
|
1676
|
+
* 验证 URPF 文件
|
|
1677
|
+
*/
|
|
1678
|
+
async validateURPFFile(filePath) {
|
|
1679
|
+
try {
|
|
1680
|
+
const stats = await fs4.stat(filePath);
|
|
1681
|
+
if (!stats.isFile()) {
|
|
1682
|
+
throw new Error(`\u8DEF\u5F84\u4E0D\u662F\u6587\u4EF6: ${filePath}`);
|
|
1683
|
+
}
|
|
1684
|
+
} catch (error) {
|
|
1685
|
+
if (error.code === "ENOENT") {
|
|
1686
|
+
throw new Error(`\u6587\u4EF6\u4E0D\u5B58\u5728: ${filePath}`);
|
|
1687
|
+
}
|
|
1688
|
+
throw error;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
/**
|
|
1692
|
+
* 提取资源到文件系统
|
|
1693
|
+
*/
|
|
1694
|
+
async extractResource(resource, outputDir, options) {
|
|
1695
|
+
try {
|
|
1696
|
+
const filePath = this.extractFilePath(resource.header.udrsReference.uri);
|
|
1697
|
+
const fullPath = path3.join(outputDir, filePath);
|
|
1698
|
+
const parentDir = path3.dirname(fullPath);
|
|
1699
|
+
await fs4.mkdir(parentDir, { recursive: true });
|
|
1700
|
+
const fileExists = await this.fileExists(fullPath);
|
|
1701
|
+
if (fileExists) {
|
|
1702
|
+
if (options.force) {
|
|
1703
|
+
await this.writeFile(resource, fullPath, options);
|
|
1704
|
+
return {
|
|
1705
|
+
path: filePath,
|
|
1706
|
+
success: true,
|
|
1707
|
+
action: "overwritten"
|
|
1708
|
+
};
|
|
1709
|
+
} else if (options.forceWhenNewer) {
|
|
1710
|
+
const sourceMtime = this.extractMtime(resource);
|
|
1711
|
+
const targetMtime = await this.getFileMtime(fullPath);
|
|
1712
|
+
if (sourceMtime > targetMtime) {
|
|
1713
|
+
await this.writeFile(resource, fullPath, options);
|
|
1714
|
+
return {
|
|
1715
|
+
path: filePath,
|
|
1716
|
+
success: true,
|
|
1717
|
+
action: "overwritten"
|
|
1718
|
+
};
|
|
1719
|
+
} else {
|
|
1720
|
+
if (options.verbose) {
|
|
1721
|
+
console.log(`\u8DF3\u8FC7\uFF08\u76EE\u6807\u6587\u4EF6\u66F4\u65B0\uFF09: ${filePath}`);
|
|
1722
|
+
}
|
|
1723
|
+
return {
|
|
1724
|
+
path: filePath,
|
|
1725
|
+
success: true,
|
|
1726
|
+
action: "skipped"
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
} else {
|
|
1730
|
+
throw new Error(`\u6587\u4EF6\u5DF2\u5B58\u5728: ${filePath}\u3002\u8BF7\u4F7F\u7528 --force \u9009\u9879\u5F3A\u5236\u8986\u76D6\uFF0C\u6216\u4F7F\u7528 --force-when-newer \u9009\u9879\u4EC5\u8986\u76D6\u66F4\u65B0\u7684\u6587\u4EF6\u3002`);
|
|
1731
|
+
}
|
|
1732
|
+
} else {
|
|
1733
|
+
await this.writeFile(resource, fullPath, options);
|
|
1734
|
+
return {
|
|
1735
|
+
path: filePath,
|
|
1736
|
+
success: true,
|
|
1737
|
+
action: "created"
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
} catch (error) {
|
|
1741
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1742
|
+
return {
|
|
1743
|
+
path: this.extractFilePath(resource.header.udrsReference.uri),
|
|
1744
|
+
success: false,
|
|
1745
|
+
action: "error",
|
|
1746
|
+
error: errorMessage
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
/**
|
|
1751
|
+
* 从 UDRS 引用中提取文件路径
|
|
1752
|
+
*/
|
|
1753
|
+
extractFilePath(uri) {
|
|
1754
|
+
let filePath = uri.replace(/^file:\/\//, "");
|
|
1755
|
+
if (filePath.startsWith("/")) {
|
|
1756
|
+
filePath = filePath.substring(1);
|
|
1757
|
+
}
|
|
1758
|
+
if (/^[A-Za-z]:/.test(filePath)) {
|
|
1759
|
+
filePath = filePath;
|
|
1760
|
+
}
|
|
1761
|
+
return filePath;
|
|
1762
|
+
}
|
|
1763
|
+
/**
|
|
1764
|
+
* 检查文件是否存在
|
|
1765
|
+
*/
|
|
1766
|
+
async fileExists(filePath) {
|
|
1767
|
+
try {
|
|
1768
|
+
await fs4.access(filePath);
|
|
1769
|
+
return true;
|
|
1770
|
+
} catch {
|
|
1771
|
+
return false;
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
/**
|
|
1775
|
+
* 获取文件修改时间
|
|
1776
|
+
*/
|
|
1777
|
+
async getFileMtime(filePath) {
|
|
1778
|
+
const stats = await fs4.stat(filePath);
|
|
1779
|
+
return stats.mtime;
|
|
1780
|
+
}
|
|
1781
|
+
/**
|
|
1782
|
+
* 从资源中提取修改时间
|
|
1783
|
+
* 注意:URPF 规范中不直接存储 mtime,这里我们使用一个合理的时间戳
|
|
1784
|
+
* 在实际应用中,可能需要从 URPF 文件的属性部分读取
|
|
1785
|
+
*/
|
|
1786
|
+
extractMtime(resource) {
|
|
1787
|
+
return new Date(Date.now() - 36e5);
|
|
1788
|
+
}
|
|
1789
|
+
/**
|
|
1790
|
+
* 获取文件大小
|
|
1791
|
+
*/
|
|
1792
|
+
getFileSize(resource) {
|
|
1793
|
+
if (Buffer.isBuffer(resource.content)) {
|
|
1794
|
+
return resource.content.length;
|
|
1795
|
+
}
|
|
1796
|
+
return Buffer.byteLength(resource.content, "utf-8");
|
|
1797
|
+
}
|
|
1798
|
+
/**
|
|
1799
|
+
* 写入文件
|
|
1800
|
+
*/
|
|
1801
|
+
async writeFile(resource, filePath, options) {
|
|
1802
|
+
let content;
|
|
1803
|
+
if (Buffer.isBuffer(resource.content)) {
|
|
1804
|
+
content = resource.content;
|
|
1805
|
+
} else {
|
|
1806
|
+
content = resource.content;
|
|
1807
|
+
}
|
|
1808
|
+
if (Buffer.isBuffer(content)) {
|
|
1809
|
+
await fs4.writeFile(filePath, content);
|
|
1810
|
+
} else {
|
|
1811
|
+
await fs4.writeFile(filePath, content, {
|
|
1812
|
+
encoding: resource.header.encoding
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
if (resource.header.permissions) {
|
|
1816
|
+
const permissions = parseInt(resource.header.permissions, 8);
|
|
1817
|
+
await fs4.chmod(filePath, permissions);
|
|
1818
|
+
}
|
|
1819
|
+
if (options.preserveMtime) {
|
|
1820
|
+
const mtime = this.extractMtime(resource);
|
|
1821
|
+
await fs4.utimes(filePath, mtime, mtime);
|
|
1822
|
+
}
|
|
1823
|
+
if (options.verbose) {
|
|
1824
|
+
console.log(`\u5DF2${await this.fileExists(filePath) ? "\u8986\u76D6" : "\u521B\u5EFA"}: ${filePath}`);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
/**
|
|
1828
|
+
* 输出详细日志
|
|
1829
|
+
*/
|
|
1830
|
+
logVerbose(results, outputDir, duration) {
|
|
1831
|
+
console.log("URPF \u89E3\u5305\u5B8C\u6210:");
|
|
1832
|
+
console.log(` \u8F93\u51FA\u76EE\u5F55: ${outputDir}`);
|
|
1833
|
+
console.log(` \u6587\u4EF6\u603B\u6570: ${results.length}`);
|
|
1834
|
+
const createdCount = results.filter((r) => r.action === "created").length;
|
|
1835
|
+
const overwrittenCount = results.filter((r) => r.action === "overwritten").length;
|
|
1836
|
+
const skippedCount = results.filter((r) => r.action === "skipped").length;
|
|
1837
|
+
const failedCount = results.filter((r) => r.action === "error").length;
|
|
1838
|
+
console.log(` \u521B\u5EFA: ${createdCount}`);
|
|
1839
|
+
console.log(` \u8986\u76D6: ${overwrittenCount}`);
|
|
1840
|
+
console.log(` \u8DF3\u8FC7: ${skippedCount}`);
|
|
1841
|
+
console.log(` \u5931\u8D25: ${failedCount}`);
|
|
1842
|
+
console.log(` \u6267\u884C\u8017\u65F6: ${duration}ms`);
|
|
1843
|
+
if (failedCount > 0) {
|
|
1844
|
+
console.log("\n\u5931\u8D25\u7684\u6587\u4EF6:");
|
|
1845
|
+
for (const result of results) {
|
|
1846
|
+
if (result.action === "error") {
|
|
1847
|
+
console.log(` - ${result.path}: ${result.error}`);
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
};
|
|
1853
|
+
|
|
1854
|
+
// src/commands/pack-add.ts
|
|
1855
|
+
import path4 from "path";
|
|
1856
|
+
import fs5 from "fs/promises";
|
|
1857
|
+
var PackAddCommand = class {
|
|
1858
|
+
/**
|
|
1859
|
+
* 执行 pack-add 命令
|
|
1860
|
+
*/
|
|
1861
|
+
async execute(urpfFilePath, targetPath, options = {}) {
|
|
1862
|
+
const startTime = Date.now();
|
|
1863
|
+
try {
|
|
1864
|
+
const resolvedUrpfPath = path4.resolve(urpfFilePath);
|
|
1865
|
+
await this.validatePath(resolvedUrpfPath);
|
|
1866
|
+
const resolvedTargetPath = path4.resolve(targetPath);
|
|
1867
|
+
await this.validatePath(resolvedTargetPath);
|
|
1868
|
+
const urpfContent = await fs5.readFile(resolvedUrpfPath, "utf-8");
|
|
1869
|
+
const parseResult = URPFParser.parse(urpfContent, { strict: false });
|
|
1870
|
+
const document = parseResult.document;
|
|
1871
|
+
const variableContext = document.propertySection ? VariableReplacer.createContext(document.propertySection.properties) : void 0;
|
|
1872
|
+
const scanResult = await this.scanFiles(resolvedTargetPath, options, variableContext);
|
|
1873
|
+
if (scanResult.files.length === 0) {
|
|
1874
|
+
return {
|
|
1875
|
+
success: true,
|
|
1876
|
+
urpfFilePath: resolvedUrpfPath,
|
|
1877
|
+
addedCount: 0,
|
|
1878
|
+
skippedCount: scanResult.skippedFiles.length,
|
|
1879
|
+
totalBytes: 0,
|
|
1880
|
+
duration: Date.now() - startTime
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
const serializer = new ResourceSerializer();
|
|
1884
|
+
const filesToAdd = scanResult.files.map((file) => {
|
|
1885
|
+
const isAbsolutePath = path4.isAbsolute(file.path);
|
|
1886
|
+
const filePath = isAbsolutePath ? file.path : path4.join(resolvedTargetPath, file.path);
|
|
1887
|
+
const relativePath = isAbsolutePath ? path4.relative(resolvedTargetPath, file.path) : file.path;
|
|
1888
|
+
return {
|
|
1889
|
+
filePath,
|
|
1890
|
+
metadata: file,
|
|
1891
|
+
udrsReference: `file://${relativePath}`
|
|
1892
|
+
};
|
|
1893
|
+
});
|
|
1894
|
+
const newResources = await serializer.serializeFiles(filesToAdd);
|
|
1895
|
+
const generatorDoc = {
|
|
1896
|
+
boundaryToken: document.boundaryToken,
|
|
1897
|
+
properties: document.propertySection?.properties.map((p) => ({
|
|
1898
|
+
name: p.name,
|
|
1899
|
+
value: p.value,
|
|
1900
|
+
isExtended: p.isExtended
|
|
1901
|
+
})),
|
|
1902
|
+
resources: document.resourceSections.map((rs) => ({
|
|
1903
|
+
header: {
|
|
1904
|
+
udrsReference: rs.header.udrsReference.protocol + "://" + rs.header.udrsReference.uri + (rs.header.udrsReference.fragment ? "#" + rs.header.udrsReference.fragment : ""),
|
|
1905
|
+
encoding: rs.header.encoding,
|
|
1906
|
+
permissions: rs.header.permissions,
|
|
1907
|
+
newlineType: rs.header.newlineType
|
|
1908
|
+
},
|
|
1909
|
+
content: rs.content
|
|
1910
|
+
}))
|
|
1911
|
+
};
|
|
1912
|
+
generatorDoc.resources = [...generatorDoc.resources, ...newResources];
|
|
1913
|
+
const generator = new URPFGenerator({
|
|
1914
|
+
boundaryToken: generatorDoc.boundaryToken,
|
|
1915
|
+
includeBoundaries: true
|
|
1916
|
+
});
|
|
1917
|
+
const result = await generator.generate(generatorDoc);
|
|
1918
|
+
await fs5.writeFile(resolvedUrpfPath, result.content, "utf-8");
|
|
1919
|
+
const duration = Date.now() - startTime;
|
|
1920
|
+
if (options.verbose) {
|
|
1921
|
+
console.log(`Successfully added ${newResources.length} files to URPF package: ${resolvedUrpfPath}`);
|
|
1922
|
+
console.log(` Added files: ${newResources.length}`);
|
|
1923
|
+
console.log(` Skipped files: ${scanResult.skippedFiles.length}`);
|
|
1924
|
+
console.log(` Total bytes: ${scanResult.files.reduce((sum, f) => sum + f.size, 0)}`);
|
|
1925
|
+
console.log(` Duration: ${duration}ms`);
|
|
1926
|
+
}
|
|
1927
|
+
return {
|
|
1928
|
+
success: true,
|
|
1929
|
+
urpfFilePath: resolvedUrpfPath,
|
|
1930
|
+
addedCount: newResources.length,
|
|
1931
|
+
skippedCount: scanResult.skippedFiles.length,
|
|
1932
|
+
totalBytes: scanResult.files.reduce((sum, f) => sum + f.size, 0),
|
|
1933
|
+
duration
|
|
1934
|
+
};
|
|
1935
|
+
} catch (err) {
|
|
1936
|
+
const duration = Date.now() - startTime;
|
|
1937
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1938
|
+
if (options.verbose) {
|
|
1939
|
+
console.error(`Error: ${errorMessage}`);
|
|
1940
|
+
}
|
|
1941
|
+
return {
|
|
1942
|
+
success: false,
|
|
1943
|
+
urpfFilePath: path4.resolve(urpfFilePath),
|
|
1944
|
+
addedCount: 0,
|
|
1945
|
+
skippedCount: 0,
|
|
1946
|
+
totalBytes: 0,
|
|
1947
|
+
duration,
|
|
1948
|
+
error: errorMessage
|
|
1949
|
+
};
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
/**
|
|
1953
|
+
* 验证路径
|
|
1954
|
+
*/
|
|
1955
|
+
async validatePath(filePath) {
|
|
1956
|
+
try {
|
|
1957
|
+
const stats = await fs5.stat(filePath);
|
|
1958
|
+
if (!stats.isFile() && !stats.isDirectory()) {
|
|
1959
|
+
throw new Error(`Path is not a file or directory: ${filePath}`);
|
|
1960
|
+
}
|
|
1961
|
+
} catch {
|
|
1962
|
+
throw new Error(`Cannot access path: ${filePath}`);
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
/**
|
|
1966
|
+
* 扫描文件
|
|
1967
|
+
*/
|
|
1968
|
+
async scanFiles(rootPath, options, variableContext) {
|
|
1969
|
+
const stats = await fs5.stat(rootPath);
|
|
1970
|
+
if (stats.isFile()) {
|
|
1971
|
+
const metadata = await this.collectFileMetadata(rootPath);
|
|
1972
|
+
return {
|
|
1973
|
+
files: [metadata],
|
|
1974
|
+
directories: [],
|
|
1975
|
+
skippedFiles: [],
|
|
1976
|
+
statistics: {
|
|
1977
|
+
totalFiles: 1,
|
|
1978
|
+
totalDirectories: 0,
|
|
1979
|
+
skippedFiles: 0
|
|
1980
|
+
}
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
let ignoreRules = [];
|
|
1984
|
+
if (options.ignoreFile) {
|
|
1985
|
+
const parser = new IgnoreRuleParser();
|
|
1986
|
+
const content = await fs5.readFile(options.ignoreFile, "utf-8");
|
|
1987
|
+
ignoreRules = parser.parseFile(content);
|
|
1988
|
+
} else {
|
|
1989
|
+
const defaultIgnoreFiles = [".gitignore", ".iflowignore"];
|
|
1990
|
+
for (const ignoreFile of defaultIgnoreFiles) {
|
|
1991
|
+
const ignorePath = path4.join(rootPath, ignoreFile);
|
|
1992
|
+
try {
|
|
1993
|
+
await fs5.access(ignorePath);
|
|
1994
|
+
const parser = new IgnoreRuleParser();
|
|
1995
|
+
const content = await fs5.readFile(ignorePath, "utf-8");
|
|
1996
|
+
ignoreRules = [...ignoreRules, ...parser.parseFile(content)];
|
|
1997
|
+
break;
|
|
1998
|
+
} catch {
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
const effectiveMaxDepth = options.noRecurse ? 1 : options.maxDepth;
|
|
2003
|
+
return await FileScanner.scanDirectory(rootPath, {
|
|
2004
|
+
ignoreRules,
|
|
2005
|
+
followSymlinks: options.followSymlinks || false,
|
|
2006
|
+
maxDepth: effectiveMaxDepth
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
/**
|
|
2010
|
+
* 收集文件元数据
|
|
2011
|
+
*/
|
|
2012
|
+
async collectFileMetadata(filePath) {
|
|
2013
|
+
const stats = await fs5.stat(filePath);
|
|
2014
|
+
return {
|
|
2015
|
+
path: filePath,
|
|
2016
|
+
name: path4.basename(filePath),
|
|
2017
|
+
size: stats.size,
|
|
2018
|
+
permissions: stats.mode,
|
|
2019
|
+
mtime: stats.mtime,
|
|
2020
|
+
encoding: "utf-8",
|
|
2021
|
+
newlineType: "lf"
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
};
|
|
2025
|
+
|
|
2026
|
+
// src/commands/pack-remove.ts
|
|
2027
|
+
import path5 from "path";
|
|
2028
|
+
import fs6 from "fs/promises";
|
|
2029
|
+
var PackRemoveCommand = class {
|
|
2030
|
+
/**
|
|
2031
|
+
* 执行 pack-remove 命令
|
|
2032
|
+
*/
|
|
2033
|
+
async execute(urpfFilePath, filenamePattern, options = {}) {
|
|
2034
|
+
const startTime = Date.now();
|
|
2035
|
+
try {
|
|
2036
|
+
const resolvedUrpfPath = path5.resolve(urpfFilePath);
|
|
2037
|
+
await this.validateUrpfFile(resolvedUrpfPath);
|
|
2038
|
+
const urpfContent = await fs6.readFile(resolvedUrpfPath, "utf-8");
|
|
2039
|
+
const parseResult = URPFParser.parse(urpfContent, { strict: false });
|
|
2040
|
+
const document = parseResult.document;
|
|
2041
|
+
const variableContext = document.propertySection ? VariableReplacer.createContext(document.propertySection.properties) : void 0;
|
|
2042
|
+
let processedPattern = filenamePattern;
|
|
2043
|
+
if (options.applyVariables !== false && variableContext) {
|
|
2044
|
+
try {
|
|
2045
|
+
processedPattern = VariableReplacer.replace(filenamePattern, variableContext);
|
|
2046
|
+
if (options.verbose) {
|
|
2047
|
+
console.log(`Variable replacement: ${filenamePattern} -> ${processedPattern}`);
|
|
2048
|
+
}
|
|
2049
|
+
} catch (error) {
|
|
2050
|
+
if (error instanceof Error) {
|
|
2051
|
+
throw new Error(`Variable replacement failed: ${error.message}`);
|
|
2052
|
+
}
|
|
2053
|
+
throw error;
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
const generatorDoc = {
|
|
2057
|
+
boundaryToken: document.boundaryToken,
|
|
2058
|
+
properties: document.propertySection?.properties.map((p) => ({
|
|
2059
|
+
name: p.name,
|
|
2060
|
+
value: p.value,
|
|
2061
|
+
isExtended: p.isExtended
|
|
2062
|
+
})),
|
|
2063
|
+
resources: document.resourceSections.map((rs) => ({
|
|
2064
|
+
header: {
|
|
2065
|
+
udrsReference: rs.header.udrsReference.protocol + "://" + rs.header.udrsReference.uri + (rs.header.udrsReference.fragment ? "#" + rs.header.udrsReference.fragment : ""),
|
|
2066
|
+
encoding: rs.header.encoding,
|
|
2067
|
+
permissions: rs.header.permissions,
|
|
2068
|
+
newlineType: rs.header.newlineType
|
|
2069
|
+
},
|
|
2070
|
+
content: rs.content
|
|
2071
|
+
}))
|
|
2072
|
+
};
|
|
2073
|
+
const { removedResources, notFoundCount } = this.removeResources(generatorDoc, processedPattern);
|
|
2074
|
+
if (removedResources.length === 0) {
|
|
2075
|
+
return {
|
|
2076
|
+
success: true,
|
|
2077
|
+
urpfFilePath: resolvedUrpfPath,
|
|
2078
|
+
removedCount: 0,
|
|
2079
|
+
notFoundCount,
|
|
2080
|
+
duration: Date.now() - startTime
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
2083
|
+
const generator = new URPFGenerator({
|
|
2084
|
+
boundaryToken: generatorDoc.boundaryToken,
|
|
2085
|
+
includeBoundaries: true
|
|
2086
|
+
});
|
|
2087
|
+
const result = await generator.generate(generatorDoc);
|
|
2088
|
+
await fs6.writeFile(resolvedUrpfPath, result.content, "utf-8");
|
|
2089
|
+
const duration = Date.now() - startTime;
|
|
2090
|
+
if (options.verbose) {
|
|
2091
|
+
console.log(`Successfully removed ${removedResources.length} files: ${resolvedUrpfPath}`);
|
|
2092
|
+
console.log(` Removed files:`);
|
|
2093
|
+
removedResources.forEach((resource) => {
|
|
2094
|
+
console.log(` - ${resource.header.udrsReference}`);
|
|
2095
|
+
});
|
|
2096
|
+
console.log(` Not found count: ${notFoundCount}`);
|
|
2097
|
+
console.log(` Duration: ${duration}ms`);
|
|
2098
|
+
}
|
|
2099
|
+
return {
|
|
2100
|
+
success: true,
|
|
2101
|
+
urpfFilePath: resolvedUrpfPath,
|
|
2102
|
+
removedCount: removedResources.length,
|
|
2103
|
+
notFoundCount,
|
|
2104
|
+
duration
|
|
2105
|
+
};
|
|
2106
|
+
} catch (err) {
|
|
2107
|
+
const duration = Date.now() - startTime;
|
|
2108
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
2109
|
+
if (options.verbose) {
|
|
2110
|
+
console.error(`Error: ${errorMessage}`);
|
|
2111
|
+
}
|
|
2112
|
+
return {
|
|
2113
|
+
success: false,
|
|
2114
|
+
urpfFilePath: path5.resolve(urpfFilePath),
|
|
2115
|
+
removedCount: 0,
|
|
2116
|
+
notFoundCount: 0,
|
|
2117
|
+
duration,
|
|
2118
|
+
error: errorMessage
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
/**
|
|
2123
|
+
* 验证 URPF 文件
|
|
2124
|
+
*/
|
|
2125
|
+
async validateUrpfFile(filePath) {
|
|
2126
|
+
try {
|
|
2127
|
+
const stats = await fs6.stat(filePath);
|
|
2128
|
+
if (!stats.isFile()) {
|
|
2129
|
+
throw new Error(`Path is not a file: ${filePath}`);
|
|
2130
|
+
}
|
|
2131
|
+
} catch {
|
|
2132
|
+
throw new Error(`Cannot access URPF file: ${filePath}`);
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
/**
|
|
2136
|
+
* 从文档中移除匹配的资源
|
|
2137
|
+
*/
|
|
2138
|
+
removeResources(document, pattern) {
|
|
2139
|
+
const removedResources = [];
|
|
2140
|
+
let notFoundCount = 0;
|
|
2141
|
+
const normalizedPattern = pattern.startsWith("/") || pattern.startsWith("\\") ? pattern.slice(1) : pattern;
|
|
2142
|
+
const normalizedPatternWithSlash = normalizedPattern.replace(/\\/g, "/");
|
|
2143
|
+
for (let i = document.resources.length - 1; i >= 0; i--) {
|
|
2144
|
+
const resource = document.resources[i];
|
|
2145
|
+
const udrsRefString = resource.header.udrsReference;
|
|
2146
|
+
if (this.matchResource(udrsRefString, normalizedPatternWithSlash)) {
|
|
2147
|
+
removedResources.push(resource);
|
|
2148
|
+
document.resources.splice(i, 1);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
if (removedResources.length === 0) {
|
|
2152
|
+
notFoundCount = 1;
|
|
2153
|
+
}
|
|
2154
|
+
return { removedResources, notFoundCount };
|
|
2155
|
+
}
|
|
2156
|
+
/**
|
|
2157
|
+
* 检查资源是否匹配模式
|
|
2158
|
+
*/
|
|
2159
|
+
matchResource(udrsRefString, pattern) {
|
|
2160
|
+
const match = udrsRefString.match(/^file:\/\/(.+)$/);
|
|
2161
|
+
if (!match) {
|
|
2162
|
+
return false;
|
|
2163
|
+
}
|
|
2164
|
+
const resourcePath = match[1];
|
|
2165
|
+
const normalizedResourcePath = resourcePath.replace(/\\/g, "/");
|
|
2166
|
+
if (normalizedResourcePath === pattern) {
|
|
2167
|
+
return true;
|
|
2168
|
+
}
|
|
2169
|
+
const regexPattern = pattern.replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
2170
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
2171
|
+
return regex.test(normalizedResourcePath);
|
|
2172
|
+
}
|
|
2173
|
+
};
|
|
2174
|
+
|
|
2175
|
+
// src/index.ts
|
|
2176
|
+
var program = new Command();
|
|
2177
|
+
program.name("urpfcli").description("URPF CLI \u5DE5\u5177 - \u57FA\u4E8E URPF \u89C4\u8303\u7684\u547D\u4EE4\u884C\u6253\u5305\u5DE5\u5177").version("0.1.0");
|
|
2178
|
+
program.command("pack").description("\u5C06\u6587\u4EF6\u6216\u76EE\u5F55\u6253\u5305\u6210 URPF \u683C\u5F0F").argument("<file|dir>", "\u8981\u6253\u5305\u7684\u6587\u4EF6\u6216\u76EE\u5F55\u8DEF\u5F84").option("-o, --output <path>", "\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84\uFF08\u9ED8\u8BA4\uFF1A\u8F93\u5165\u8DEF\u5F84.urpf\uFF09").option("-v, --verbose", "\u663E\u793A\u8BE6\u7EC6\u65E5\u5FD7").option("-i, --ignore <path>", "\u6307\u5B9A\u5FFD\u7565\u89C4\u5219\u6587\u4EF6\u8DEF\u5F84").option("-f, --follow-symlinks", "\u8DDF\u968F\u7B26\u53F7\u94FE\u63A5").option("-d, --max-depth <number>", "\u6700\u5927\u626B\u63CF\u6DF1\u5EA6\uFF080 \u8868\u793A\u65E0\u9650\u5236\uFF09").option("--no-recurse", "\u4E0D\u9012\u5F52\u626B\u63CF\u5B50\u76EE\u5F55").option("--force", "\u5F3A\u5236\u8986\u76D6\u5DF2\u5B58\u5728\u7684\u8F93\u51FA\u6587\u4EF6").action(async (file, options) => {
|
|
2179
|
+
try {
|
|
2180
|
+
const packCommand = new PackCommand();
|
|
2181
|
+
const result = await packCommand.execute(file, options);
|
|
2182
|
+
if (result.success) {
|
|
2183
|
+
console.log(`\u2705 URPF \u6587\u4EF6\u5DF2\u751F\u6210: ${result.outputPath}`);
|
|
2184
|
+
if (!options.verbose) {
|
|
2185
|
+
console.log(` \u6587\u4EF6\u6570\u91CF: ${result.fileCount}`);
|
|
2186
|
+
console.log(` \u76EE\u5F55\u6570\u91CF: ${result.directoryCount}`);
|
|
2187
|
+
console.log(` \u8DF3\u8FC7\u6587\u4EF6: ${result.skippedCount}`);
|
|
2188
|
+
console.log(` URPF \u5927\u5C0F: ${result.urpfSize} \u5B57\u8282`);
|
|
2189
|
+
console.log(` \u6267\u884C\u8017\u65F6: ${result.duration}ms`);
|
|
2190
|
+
}
|
|
2191
|
+
} else {
|
|
2192
|
+
console.error(`\u274C \u6253\u5305\u5931\u8D25: ${result.error}`);
|
|
2193
|
+
process.exit(1);
|
|
2194
|
+
}
|
|
2195
|
+
} catch (error) {
|
|
2196
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2197
|
+
console.error(`\u274C \u9519\u8BEF: ${errorMessage}`);
|
|
2198
|
+
process.exit(1);
|
|
2199
|
+
}
|
|
2200
|
+
});
|
|
2201
|
+
program.command("unpack").description("\u89E3\u5305 URPF \u6587\u4EF6\u5230\u6307\u5B9A\u76EE\u5F55").argument("<file.urpf>", "\u8981\u89E3\u5305\u7684 URPF \u6587\u4EF6\u8DEF\u5F84").option("-o, --output <path>", "\u8F93\u51FA\u76EE\u5F55\u8DEF\u5F84\uFF08\u9ED8\u8BA4\uFF1A\u5F53\u524D\u76EE\u5F55\uFF09").option("-v, --verbose", "\u663E\u793A\u8BE6\u7EC6\u65E5\u5FD7").option("--force", "\u5F3A\u5236\u8986\u76D6\u5DF2\u5B58\u5728\u7684\u6587\u4EF6").option("--force-when-newer", "\u4EC5\u5F53\u6E90\u6587\u4EF6\u6BD4\u76EE\u6807\u6587\u4EF6\u66F4\u65B0\u65F6\u624D\u8986\u76D6").option("--preserve-mtime", "\u4FDD\u7559\u539F\u59CB\u4FEE\u6539\u65F6\u95F4").action(async (file, options) => {
|
|
2202
|
+
try {
|
|
2203
|
+
const unpackCommand = new UnpackCommand();
|
|
2204
|
+
const result = await unpackCommand.execute(file, options);
|
|
2205
|
+
if (result.success) {
|
|
2206
|
+
console.log(`\u2705 URPF \u6587\u4EF6\u5DF2\u89E3\u5305\u5230: ${result.outputDir}`);
|
|
2207
|
+
if (!options.verbose) {
|
|
2208
|
+
console.log(` \u521B\u5EFA: ${result.createdCount}`);
|
|
2209
|
+
console.log(` \u8986\u76D6: ${result.overwrittenCount}`);
|
|
2210
|
+
console.log(` \u8DF3\u8FC7: ${result.skippedCount}`);
|
|
2211
|
+
console.log(` \u603B\u5B57\u8282: ${result.totalBytes}`);
|
|
2212
|
+
console.log(` \u6267\u884C\u8017\u65F6: ${result.duration}ms`);
|
|
2213
|
+
}
|
|
2214
|
+
} else {
|
|
2215
|
+
console.error(`\u274C \u89E3\u5305\u5931\u8D25: ${result.error}`);
|
|
2216
|
+
process.exit(1);
|
|
2217
|
+
}
|
|
2218
|
+
} catch (error) {
|
|
2219
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2220
|
+
console.error(`\u274C \u9519\u8BEF: ${errorMessage}`);
|
|
2221
|
+
process.exit(1);
|
|
2222
|
+
}
|
|
2223
|
+
});
|
|
2224
|
+
program.command("pack-add").description("\u5411\u5DF2\u6709 URPF \u5305\u6DFB\u52A0\u6587\u4EF6\u6216\u76EE\u5F55").argument("<file.urpf>", "URPF \u6587\u4EF6\u8DEF\u5F84").argument("<file|dir>", "\u8981\u6DFB\u52A0\u7684\u6587\u4EF6\u6216\u76EE\u5F55\u8DEF\u5F84").option("-v, --verbose", "\u663E\u793A\u8BE6\u7EC6\u65E5\u5FD7").option("-i, --ignore <path>", "\u6307\u5B9A\u5FFD\u7565\u89C4\u5219\u6587\u4EF6\u8DEF\u5F84").option("-f, --follow-symlinks", "\u8DDF\u968F\u7B26\u53F7\u94FE\u63A5").option("-d, --max-depth <number>", "\u6700\u5927\u626B\u63CF\u6DF1\u5EA6\uFF080 \u8868\u793A\u65E0\u9650\u5236\uFF09").option("--no-recurse", "\u4E0D\u9012\u5F52\u626B\u63CF\u5B50\u76EE\u5F55").action(async (urpfFile, target, options) => {
|
|
2225
|
+
try {
|
|
2226
|
+
const packAddCommand = new PackAddCommand();
|
|
2227
|
+
const result = await packAddCommand.execute(urpfFile, target, options);
|
|
2228
|
+
if (result.success) {
|
|
2229
|
+
console.log(`\u2705 \u5DF2\u6210\u529F\u6DFB\u52A0\u6587\u4EF6\u5230 URPF \u5305: ${result.urpfFilePath}`);
|
|
2230
|
+
if (!options.verbose) {
|
|
2231
|
+
console.log(` \u6DFB\u52A0\u6587\u4EF6: ${result.addedCount}`);
|
|
2232
|
+
console.log(` \u8DF3\u8FC7\u6587\u4EF6: ${result.skippedCount}`);
|
|
2233
|
+
console.log(` \u603B\u5B57\u8282: ${result.totalBytes}`);
|
|
2234
|
+
console.log(` \u6267\u884C\u8017\u65F6: ${result.duration}ms`);
|
|
2235
|
+
}
|
|
2236
|
+
} else {
|
|
2237
|
+
console.error(`\u274C \u6DFB\u52A0\u5931\u8D25: ${result.error}`);
|
|
2238
|
+
process.exit(1);
|
|
2239
|
+
}
|
|
2240
|
+
} catch (error) {
|
|
2241
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2242
|
+
console.error(`\u274C \u9519\u8BEF: ${errorMessage}`);
|
|
2243
|
+
process.exit(1);
|
|
2244
|
+
}
|
|
2245
|
+
});
|
|
2246
|
+
program.command("pack-remove").description("\u4ECE URPF \u5305\u4E2D\u79FB\u9664\u6587\u4EF6\uFF08\u652F\u6301\u53D8\u91CF\u66FF\u6362\uFF09").argument("<file.urpf>", "URPF \u6587\u4EF6\u8DEF\u5F84").argument("<filename>", "\u8981\u79FB\u9664\u7684\u6587\u4EF6\u540D\u6216\u6A21\u5F0F\uFF08\u652F\u6301\u901A\u914D\u7B26 * \u548C ?\uFF09").option("-v, --verbose", "\u663E\u793A\u8BE6\u7EC6\u65E5\u5FD7").option("--no-apply-variables", "\u4E0D\u5E94\u7528\u53D8\u91CF\u66FF\u6362").action(async (urpfFile, filename, options) => {
|
|
2247
|
+
try {
|
|
2248
|
+
const packRemoveCommand = new PackRemoveCommand();
|
|
2249
|
+
const result = await packRemoveCommand.execute(urpfFile, filename, options);
|
|
2250
|
+
if (result.success) {
|
|
2251
|
+
console.log(`\u2705 \u5DF2\u6210\u529F\u4ECE URPF \u5305\u79FB\u9664\u6587\u4EF6: ${result.urpfFilePath}`);
|
|
2252
|
+
if (!options.verbose) {
|
|
2253
|
+
console.log(` \u79FB\u9664\u6587\u4EF6: ${result.removedCount}`);
|
|
2254
|
+
console.log(` \u672A\u627E\u5230: ${result.notFoundCount}`);
|
|
2255
|
+
console.log(` \u6267\u884C\u8017\u65F6: ${result.duration}ms`);
|
|
2256
|
+
}
|
|
2257
|
+
} else {
|
|
2258
|
+
console.error(`\u274C \u79FB\u9664\u5931\u8D25: ${result.error}`);
|
|
2259
|
+
process.exit(1);
|
|
2260
|
+
}
|
|
2261
|
+
} catch (error) {
|
|
2262
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2263
|
+
console.error(`\u274C \u9519\u8BEF: ${errorMessage}`);
|
|
2264
|
+
process.exit(1);
|
|
2265
|
+
}
|
|
2266
|
+
});
|
|
2267
|
+
program.parse();
|