Files
xianyan/tools/check_ohos_permissions.dart
Developer 5a083bdbab feat: 新增多模块后端管理、数据同步工具与鸿蒙路由适配
本次提交新增了以下核心内容:
1. 后端管理模块:包含字体同步、插件元数据、插件用户设置、稍后读消息/共享列表的控制器、模型、验证器与多语言配置
2. Flutter数据同步模块:统一的事件总线与兼容层,替代分散的StreamController
3. 鸿蒙端路由适配:完整的路由定义、构建器与占位组件
4. 后端API接口:字体同步与插件更新的服务端API,支持自动建表与跨域请求
5. 鸿蒙权限校验脚本:用于校验module.json5与string.json的权限声明一致性
2026-06-01 05:50:13 +08:00

369 lines
12 KiB
Dart

/// ============================================================
/// 闲言APP — 鸿蒙端权限一致性校验脚本
/// 创建时间: 2026-05-31
/// 更新时间: 2026-05-31
/// 作用: 校验 module.json5 与 string.json 之间的权限声明一致性
/// - 检查 module.json5 中权限的 reason 引用是否在 string.json 中存在
/// - 检查 string.json 中 permission_*_reason 键是否被 module.json5 引用
/// - 检查 user_grant 权限是否缺少 reason 说明
/// 上次更新: 初始创建
/// ============================================================
///
/// 用法:
/// dart run tools/check_ohos_permissions.dart
///
/// 退出码:
/// 0 — 校验通过,无错误
/// 1 — 存在错误(缺失引用或未使用的键)
import 'dart:convert';
import 'dart:io';
// ── 路径常量 ──────────────────────────────────────────────────
const String projectRoot = 'e:\\project\\flutter\\f\\xianyan';
const String moduleJson5Path =
'$projectRoot\\ohos\\entry\\src\\main\\module.json5';
const String stringJsonPath =
'$projectRoot\\ohos\\entry\\src\\main\\resources\\base\\element\\string.json';
// ── 终端颜色 ──────────────────────────────────────────────────
const String _reset = '\x1B[0m';
const String _red = '\x1B[31m';
const String _green = '\x1B[32m';
const String _yellow = '\x1B[33m';
const String _cyan = '\x1B[36m';
const String _bold = '\x1B[1m';
// ── 数据模型 ──────────────────────────────────────────────────
/// module.json5 中的权限条目
class PermissionEntry {
final String name;
final String? reasonRef;
final String? when_;
PermissionEntry({
required this.name,
this.reasonRef,
this.when_,
});
}
// ── JSON5 解析 ────────────────────────────────────────────────
/// 将 JSON5 文本转换为标准 JSON
/// 处理: 单行注释、多行注释、尾逗号
String _json5ToJson(String source) {
final buffer = StringBuffer();
var i = 0;
while (i < source.length) {
// 单行注释 //
if (i < source.length - 1 && source[i] == '/' && source[i + 1] == '/') {
while (i < source.length && source[i] != '\n') {
i++;
}
continue;
}
// 多行注释 /* */
if (i < source.length - 1 && source[i] == '/' && source[i + 1] == '*') {
i += 2;
while (i < source.length - 1) {
if (source[i] == '*' && source[i + 1] == '/') {
i += 2;
break;
}
i++;
}
continue;
}
// 字符串字面量 — 原样保留,避免误判注释
if (source[i] == '"' || source[i] == "'") {
final quote = source[i];
buffer.write('"');
i++;
while (i < source.length && source[i] != quote) {
if (source[i] == '\\' && i + 1 < source.length) {
buffer.write(source[i]);
buffer.write(source[i + 1]);
i += 2;
continue;
}
if (source[i] == '"') {
buffer.write('\\"');
i++;
continue;
}
buffer.write(source[i]);
i++;
}
buffer.write('"');
i++;
continue;
}
buffer.write(source[i]);
i++;
}
var result = buffer.toString();
// 移除尾逗号: , 后跟 } 或 ]
result = result.replaceAll(RegExp(r',\s*([}\]])'), r'$1');
return result;
}
/// 解析 module.json5 文件,提取 requestPermissions 列表
List<PermissionEntry> _parseModuleJson5(String filePath) {
final file = File(filePath);
if (!file.existsSync()) {
stderr.writeln('${_red}错误: 文件不存在 $filePath$_reset');
exit(1);
}
final raw = file.readAsStringSync();
final jsonStr = _json5ToJson(raw);
late final Map<String, dynamic> root;
try {
root = json.decode(jsonStr) as Map<String, dynamic>;
} catch (e) {
stderr.writeln('${_red}错误: JSON5 解析失败 — $e$_reset');
exit(1);
}
final module = root['module'] as Map<String, dynamic>?;
if (module == null) {
stderr.writeln('${_red}错误: module.json5 缺少 module 根节点$_reset');
exit(1);
}
final permissions = module['requestPermissions'] as List<dynamic>?;
if (permissions == null) {
stderr.writeln('${_yellow}警告: module.json5 无 requestPermissions 声明$_reset');
return [];
}
return permissions.map((p) {
final map = p as Map<String, dynamic>;
String? reasonRef;
final reason = map['reason'];
if (reason is String && reason.startsWith('\$string:')) {
reasonRef = reason.substring('\$string:'.length);
}
String? when_;
final usedScene = map['usedScene'] as Map<String, dynamic>?;
if (usedScene != null) {
when_ = usedScene['when'] as String?;
}
return PermissionEntry(
name: map['name'] as String,
reasonRef: reasonRef,
when_: when_,
);
}).toList();
}
/// 解析 string.json 文件,提取所有 permission_*_reason 键
Map<String, String> _parseStringJson(String filePath) {
final file = File(filePath);
if (!file.existsSync()) {
stderr.writeln('${_red}错误: 文件不存在 $filePath$_reset');
exit(1);
}
final raw = file.readAsStringSync();
late final Map<String, dynamic> root;
try {
root = json.decode(raw) as Map<String, dynamic>;
} catch (e) {
stderr.writeln('${_red}错误: string.json 解析失败 — $e$_reset');
exit(1);
}
final strings = root['string'] as List<dynamic>;
final result = <String, String>{};
for (final item in strings) {
final map = item as Map<String, dynamic>;
final name = map['name'] as String;
if (name.startsWith('permission_') && name.endsWith('_reason')) {
result[name] = map['value'] as String;
}
}
return result;
}
// ── 已知的 user_grant 权限列表 ────────────────────────────────
/// 鸿蒙系统需要用户授权的权限
/// 参考: https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/permissions
const Set<String> _userGrantPermissions = {
'ohos.permission.CAMERA',
'ohos.permission.MICROPHONE',
'ohos.permission.LOCATION',
'ohos.permission.APPROXIMATELY_LOCATION',
'ohos.permission.READ_MEDIA',
'ohos.permission.WRITE_MEDIA',
'ohos.permission.ACCESS_BLUETOOTH',
'ohos.permission.NFC_TAG',
};
// ── 校验逻辑 ──────────────────────────────────────────────────
/// 校验结果
class CheckResult {
final List<String> matched = [];
final List<String> missingReasons = [];
final List<String> unusedReasons = [];
final List<String> permissionsWithoutReason = [];
bool get hasErrors => missingReasons.isNotEmpty || unusedReasons.isNotEmpty;
bool get hasWarnings => permissionsWithoutReason.isNotEmpty;
}
CheckResult _validate(
List<PermissionEntry> permissions,
Map<String, String> reasonStrings,
) {
final result = CheckResult();
// 收集 module.json5 中引用的所有 reason key
final referencedReasons = <String>{};
for (final perm in permissions) {
if (perm.reasonRef != null) {
referencedReasons.add(perm.reasonRef!);
if (reasonStrings.containsKey(perm.reasonRef)) {
result.matched.add(perm.name);
} else {
result.missingReasons.add(
'${perm.name}\$string:${perm.reasonRef}',
);
}
} else {
// user_grant 权限应该有 reason
if (_userGrantPermissions.contains(perm.name)) {
result.permissionsWithoutReason.add(perm.name);
}
}
}
// 检查 string.json 中未被引用的 reason key
for (final key in reasonStrings.keys) {
if (!referencedReasons.contains(key)) {
result.unusedReasons.add(key);
}
}
return result;
}
// ── 报告输出 ──────────────────────────────────────────────────
void _printReport(CheckResult result) {
stdout.writeln();
stdout.writeln('${_bold}╔══════════════════════════════════════════════════╗$_reset');
stdout.writeln('${_bold}║ 鸿蒙权限一致性校验报告 ║$_reset');
stdout.writeln('${_bold}╚══════════════════════════════════════════════════╝$_reset');
stdout.writeln();
// ✅ 匹配项
stdout.writeln(
'${_green}✅ 匹配的权限 (${result.matched.length})$_reset',
);
if (result.matched.isEmpty) {
stdout.writeln(' (无)');
} else {
for (final name in result.matched) {
stdout.writeln(' $name');
}
}
stdout.writeln();
// ❌ 缺失的 reason 字符串
stdout.writeln(
'${_red}❌ 缺失的 reason 字符串 (${result.missingReasons.length})$_reset',
);
if (result.missingReasons.isEmpty) {
stdout.writeln(' (无)');
} else {
for (final item in result.missingReasons) {
stdout.writeln(' $item');
}
}
stdout.writeln();
// ⚠️ 未使用的 reason 字符串
stdout.writeln(
'${_yellow}⚠️ 未使用的 reason 字符串 (${result.unusedReasons.length})$_reset',
);
if (result.unusedReasons.isEmpty) {
stdout.writeln(' (无)');
} else {
for (final key in result.unusedReasons) {
stdout.writeln(' $key');
}
}
stdout.writeln();
// ⚠️ 缺少 reason 的 user_grant 权限
stdout.writeln(
'${_yellow}⚠️ 缺少 reason 的 user_grant 权限 (${result.permissionsWithoutReason.length})$_reset',
);
if (result.permissionsWithoutReason.isEmpty) {
stdout.writeln(' (无)');
} else {
for (final name in result.permissionsWithoutReason) {
stdout.writeln(' $name');
}
}
stdout.writeln();
// 总结
stdout.writeln('${_cyan}──────────────────────────────────────────────────$_reset');
if (result.hasErrors) {
stdout.writeln(
'${_red}${_bold}结果: 失败 — 发现 ${result.missingReasons.length} 个缺失引用, ${result.unusedReasons.length} 个未使用键$_reset',
);
} else if (result.hasWarnings) {
stdout.writeln(
'${_yellow}${_bold}结果: 通过(有警告) — ${result.permissionsWithoutReason.length} 个 user_grant 权限缺少 reason$_reset',
);
} else {
stdout.writeln(
'${_green}${_bold}结果: 通过 — 所有权限声明一致$_reset',
);
}
stdout.writeln('${_cyan}──────────────────────────────────────────────────$_reset');
stdout.writeln();
}
// ── 入口 ──────────────────────────────────────────────────────
void main() {
stdout.writeln('${_cyan}正在解析 module.json5...$_reset');
final permissions = _parseModuleJson5(moduleJson5Path);
stdout.writeln(' 找到 ${permissions.length} 个权限声明');
stdout.writeln('${_cyan}正在解析 string.json...$_reset');
final reasonStrings = _parseStringJson(stringJsonPath);
stdout.writeln(' 找到 ${reasonStrings.length} 个 permission_*_reason 键');
final result = _validate(permissions, reasonStrings);
_printReport(result);
if (result.hasErrors) {
exit(1);
}
}