已开启
增加execCmd执行cli tool #20318
piki创建于 4 天前
增加execCmd执行cli tool #20318
已开启
共 8 个文件变更+498-4
| @@ -291,6 +291,25 @@ bool UnwrapExecCmdOptions(napi_env env, napi_value obj, ExecCmdParam ¶m, | |||
| 291 | } | 291 | } |
| 292 | } | 292 | } |
| 293 | 293 | ||
| 294 | + if (napi_has_named_property(env, obj, "isShellCommand", &hasProperty) != napi_ok) { | ||
| 295 | + msg = "has isShellCommand failed"; | ||
| 296 | + TAG_LOGE(AAFwkTag::CLI_TOOL, "%{public}s", msg.c_str()); | ||
| 297 | + return false; | ||
| 298 | + } | ||
| 299 | + if (hasProperty) { | ||
| 300 | + napi_value isShellProp = nullptr; | ||
| 301 | + if (napi_get_named_property(env, obj, "isShellCommand", &isShellProp) != napi_ok) { | ||
| 302 | + msg = "invalid isShellCommand property"; | ||
| 303 | + TAG_LOGE(AAFwkTag::CLI_TOOL, "%{public}s", msg.c_str()); | ||
| 304 | + return false; | ||
| 305 | + } | ||
| 306 | + if (!AppExecFwk::UnwrapBoolFromJS2(env, isShellProp, param.isShellCommand)) { | ||
| 307 | + msg = "unwrap isShellCommand failed"; | ||
| 308 | + TAG_LOGE(AAFwkTag::CLI_TOOL, "%{public}s", msg.c_str()); | ||
| 309 | + return false; | ||
| 310 | + } | ||
| 311 | + } | ||
| 312 | + | ||
| 294 | if (napi_has_named_property(env, obj, "callback", &hasProperty) != napi_ok) { | 313 | if (napi_has_named_property(env, obj, "callback", &hasProperty) != napi_ok) { |
| 295 | msg = "has callback failed"; | 314 | msg = "has callback failed"; |
| 296 | TAG_LOGE(AAFwkTag::CLI_TOOL, "%{public}s", msg.c_str()); | 315 | TAG_LOGE(AAFwkTag::CLI_TOOL, "%{public}s", msg.c_str()); |
| @@ -33,6 +33,7 @@ public: | |||
| 33 | std::string env; | 33 | std::string env; |
| 34 | std::string policy; | 34 | std::string policy; |
| 35 | ExecOptions options; | 35 | ExecOptions options; |
| 36 | + bool isShellCommand = true; | ||
| 36 | 37 | ||
| 37 | bool Marshalling(Parcel &parcel) const; | 38 | bool Marshalling(Parcel &parcel) const; |
| 38 | static ExecCmdParam *Unmarshalling(Parcel &parcel); | 39 | static ExecCmdParam *Unmarshalling(Parcel &parcel); |
| @@ -31,6 +31,16 @@ namespace CliTool { | |||
| 31 | 31 | ||
| 32 | namespace { | 32 | namespace { |
| 33 | constexpr int32_t ERR_OK = 0; | 33 | constexpr int32_t ERR_OK = 0; |
| 34 | + | ||
| 35 | +std::string ExtractToolName(const std::string &cmd) | ||
| 36 | +{ | ||
H | |||
| 37 | + auto first = cmd.find_first_not_of(" \t"); | ||
| 38 | + if (first == std::string::npos) { | ||
| 39 | + return ""; | ||
| 40 | + } | ||
| 41 | + auto pos = cmd.find_first_of(" \t", first); | ||
| 42 | + return (pos == std::string::npos) ? cmd.substr(first) : cmd.substr(first, pos - first); | ||
| 43 | +} | ||
| 34 | } // namespace | 44 | } // namespace |
| 35 | 45 | ||
| 36 | CliToolMGRClient& CliToolMGRClient::GetInstance() | 46 | CliToolMGRClient& CliToolMGRClient::GetInstance() |
| @@ -85,7 +95,8 @@ ErrCode CliToolMGRClient::ExecCmd(const ExecCmdParam ¶m, | |||
| 85 | return GET_CLI_TOOL_MGR_SERVICE_FAILED; | 95 | return GET_CLI_TOOL_MGR_SERVICE_FAILED; |
| 86 | } | 96 | } |
| 87 | 97 | ||
| 88 | - std::string eventId = CliEventReplyManager::GetInstance().AddEventReplyCallback("shell", | 98 | + std::string eventKey = param.isShellCommand ? "shell" : ExtractToolName(param.cmd); |
H [次要] 当 param.isShellCommand 为 false 且 param.cmd 为空或纯空白时,ExtractToolName 返回空字符串作为 eventKey,可能导致注册无法匹配的回调。建议在客户端侧也增加 cmd 非空校验,提前返回错误,避免无效回调注册。 ![]() ![]() | |||
| 99 | + std::string eventId = CliEventReplyManager::GetInstance().AddEventReplyCallback(eventKey, | ||
| 89 | [cb = std::move(callback)](const CliEventReplyResult &result) { | 100 | [cb = std::move(callback)](const CliEventReplyResult &result) { |
| 90 | if (cb) { | 101 | if (cb) { |
| 91 | CliSessionInfo sessionInfo; | 102 | CliSessionInfo sessionInfo; |
| @@ -96,7 +107,7 @@ ErrCode CliToolMGRClient::ExecCmd(const ExecCmdParam ¶m, | |||
| 96 | } | 107 | } |
| 97 | }); | 108 | }); |
| 98 | 109 | ||
| 99 | - std::string subscriptionId = CliSessionSubscriptionManager::GetInstance().AddProvisionalSubscription("shell", | 110 | + std::string subscriptionId = CliSessionSubscriptionManager::GetInstance().AddProvisionalSubscription(eventKey, |
| 100 | [sessionEventCallback](const std::string &sessionId, | 111 | [sessionEventCallback](const std::string &sessionId, |
| 101 | const std::string &subscriptionId, const CliToolEvent &event) { | 112 | const std::string &subscriptionId, const CliToolEvent &event) { |
| 102 | if (sessionEventCallback) { | 113 | if (sessionEventCallback) { |
| @@ -41,6 +41,10 @@ bool ExecCmdParam::Marshalling(Parcel &parcel) const | |||
| 41 | TAG_LOGE(AAFwkTag::CLI_TOOL, "Write options failed."); | 41 | TAG_LOGE(AAFwkTag::CLI_TOOL, "Write options failed."); |
| 42 | return false; | 42 | return false; |
| 43 | } | 43 | } |
| 44 | + if (!parcel.WriteBool(isShellCommand)) { | ||
| 45 | + TAG_LOGE(AAFwkTag::CLI_TOOL, "Write isShellCommand failed."); | ||
| 46 | + return false; | ||
| 47 | + } | ||
| 44 | return true; | 48 | return true; |
| 45 | } | 49 | } |
| 46 | 50 | ||
| @@ -74,6 +78,13 @@ ExecCmdParam *ExecCmdParam::Unmarshalling(Parcel &parcel) | |||
| 74 | return nullptr; | 78 | return nullptr; |
| 75 | } | 79 | } |
| 76 | result->options = *execOptions; | 80 | result->options = *execOptions; |
| 81 | + | ||
| 82 | + // Tail field for backward compat: ReadBool fails on old clients, defaults to true. | ||
| 83 | + result->isShellCommand = true; | ||
| 84 | + if (parcel.ReadBool(result->isShellCommand)) { | ||
H [提示] Unmarshalling 中 ReadBool 成功后直接 return result,若未来在 isShellCommand 之后新增字段,此早返回会导致后续字段无法读取。建议移除 if 内的 return,统一在函数末尾返回,保持与 Marshalling 字段顺序的一致性。 ![]() ![]() | |||
| 85 | + return result; | ||
| 86 | + } | ||
| 87 | + TAG_LOGD(AAFwkTag::CLI_TOOL, "isShellCommand not present in parcel, using default(true)."); | ||
| 77 | return result; | 88 | return result; |
| 78 | } | 89 | } |
| 79 | } // namespace CliTool | 90 | } // namespace CliTool |
| @@ -220,6 +220,14 @@ private: | |||
| 220 | 220 | ||
| 221 | int32_t ValidateAndPrepareCmd(const ExecCmdParam ¶m, uint32_t tokenId, | 221 | int32_t ValidateAndPrepareCmd(const ExecCmdParam ¶m, uint32_t tokenId, |
| 222 | std::string &sandboxConfig, std::string &bundleName); | 222 | std::string &sandboxConfig, std::string &bundleName); |
| 223 | + int32_t ExecCmdToolMode(const ExecCmdParam ¶m, const std::string &eventId, | ||
| 224 | + const sptr<ICliToolManagerScheduler> &scheduler, const std::string &subscriptionId, | ||
| 225 | + int32_t callerPid, int32_t callerUid, uint32_t tokenId, const std::string &bundleName); | ||
| 226 | + int32_t SetupCmdSession(const ExecToolParam &toolParam, const ToolInfo &toolInfo, | ||
| 227 | + const std::string &sandboxConfig, const std::string &eventId, | ||
| 228 | + const std::string &subscriptionId, const sptr<ICliToolManagerScheduler> &scheduler, | ||
| 229 | + int32_t callerPid, int32_t callerUid, const std::string &bundleName, | ||
| 230 | + const std::string &toolName); | ||
| 223 | int32_t SetupAndStartSession(const ExecToolParam ¶m, const std::string &eventId, | 231 | int32_t SetupAndStartSession(const ExecToolParam ¶m, const std::string &eventId, |
| 224 | const ToolInfo &toolInfo, const std::string &sandboxConfig, const std::string &bundleName); | 232 | const ToolInfo &toolInfo, const std::string &sandboxConfig, const std::string &bundleName); |
| 225 | 233 | ||
| @@ -62,6 +62,12 @@ public: | |||
| 62 | 62 | ||
| 63 | static void TransferToCmdParam(const AAFwk::WantParams &args, std::vector<std::string> &execArgs); | 63 | static void TransferToCmdParam(const AAFwk::WantParams &args, std::vector<std::string> &execArgs); |
| 64 | 64 | ||
| 65 | + // Parse a concatenated tool command string (e.g. "ohos-aa start --bundlename com.x") | ||
| 66 | + // into an ExecToolParam. The parameter mapping and type recovery are driven by the | ||
| 67 | + // tool's inputSchema (subcommand schema takes precedence when subcommand is present). | ||
| 68 | + static int32_t ParseToolCommand(const std::string &cmd, const ToolInfo &toolInfo, | ||
| 69 | + ExecToolParam ¶m, std::string &detail); | ||
| 70 | + | ||
| 65 | static bool IsSkillTool(const std::string &toolName); | 71 | static bool IsSkillTool(const std::string &toolName); |
| 66 | static void NormalizeSkillParamKeys(AAFwk::WantParams &args); | 72 | static void NormalizeSkillParamKeys(AAFwk::WantParams &args); |
| 67 | static void ExpandArgsJsonString(AAFwk::WantParams &args); | 73 | static void ExpandArgsJsonString(AAFwk::WantParams &args); |
| @@ -1058,8 +1058,20 @@ int32_t CliToolManagerService::ExecCmd(const ExecCmdParam ¶m, const std::str | |||
| 1058 | return ret; | 1058 | return ret; |
| 1059 | } | 1059 | } |
| 1060 | auto tokenId = IPCSkeleton::GetCallingTokenID(); | 1060 | auto tokenId = IPCSkeleton::GetCallingTokenID(); |
| 1061 | - std::string sandboxConfig; | ||
| 1062 | std::string bundleName; | 1061 | std::string bundleName; |
| 1062 | + AppExecFwk::BundleInfo bundleInfo; | ||
| 1063 | + if (ToolUtil::GetBundleInfoByTokenId(tokenId, bundleInfo)) { | ||
| 1064 | + bundleName = bundleInfo.name; | ||
| 1065 | + } | ||
| 1066 | + | ||
| 1067 | + // Tool command mode | ||
| 1068 | + if (!param.isShellCommand) { | ||
| 1069 | + return ExecCmdToolMode(param, eventId, scheduler, subscriptionId, | ||
| 1070 | + callerPid, callerUid, tokenId, bundleName); | ||
| 1071 | + } | ||
| 1072 | + | ||
| 1073 | + // Shell path (original logic) | ||
| 1074 | + std::string sandboxConfig; | ||
| 1063 | if (auto ret = ValidateAndPrepareCmd(param, tokenId, sandboxConfig, bundleName); ret != ERR_OK) { | 1075 | if (auto ret = ValidateAndPrepareCmd(param, tokenId, sandboxConfig, bundleName); ret != ERR_OK) { |
| 1064 | return ret; | 1076 | return ret; |
| 1065 | } | 1077 | } |
| @@ -1101,6 +1113,109 @@ int32_t CliToolManagerService::ExecCmd(const ExecCmdParam ¶m, const std::str | |||
| 1101 | return ERR_OK; | 1113 | return ERR_OK; |
| 1102 | } | 1114 | } |
| 1103 | 1115 | ||
| 1116 | +int32_t CliToolManagerService::ExecCmdToolMode(const ExecCmdParam ¶m, const std::string &eventId, | ||
| 1117 | + const sptr<ICliToolManagerScheduler> &scheduler, const std::string &subscriptionId, | ||
| 1118 | + int32_t callerPid, int32_t callerUid, uint32_t tokenId, const std::string &bundleName) | ||
| 1119 | +{ | ||
| 1120 | + TAG_LOGI(AAFwkTag::CLI_TOOL, | ||
| 1121 | + "ExecCmdToolMode: cmd=%{public}s, callerPid=%{public}d, callerUid=%{public}d", | ||
| 1122 | + param.cmd.c_str(), callerPid, callerUid); | ||
H [重要] ExecCmdToolMode 中使用 %{public}s 打印完整的 param.cmd,命令字符串可能包含文件路径等敏感信息,违反安全编码规范'禁止打印文件路径等敏感信息'。建议仅打印 toolName 或使用 %{private}s 脱敏。 ![]() ![]() | |||
| 1123 | + | ||
| 1124 | + // Step 1: Extract toolName, look up tool. | ||
| 1125 | + auto first = param.cmd.find_first_not_of(" \t"); | ||
| 1126 | + if (first == std::string::npos) { | ||
| 1127 | + ReportCliExecuteFailed(bundleName, "", "invalid_tool_name"); | ||
| 1128 | + return ERR_INVALID_PARAM; | ||
| 1129 | + } | ||
| 1130 | + auto pos = param.cmd.find_first_of(" \t", first); | ||
| 1131 | + std::string toolName = (pos == std::string::npos) ? param.cmd.substr(first) : param.cmd.substr(first, pos - first); | ||
| 1132 | + if (toolName.empty() || toolName[0] == '/') { | ||
| 1133 | + ReportCliExecuteFailed(bundleName, toolName, "invalid_tool_name"); | ||
H [重要] toolName 校验仅检查 empty 和首字符 '/',未对 '..'、'./'、路径分隔符等特殊字符进行校验。虽然 toolName 主要用于 GetToolByName 查找,但根据安全编码规范'外部传入的路径要做规范化校验,对路径中的.、..、../等特殊字符严格校验',建议增加对 toolName 的白名单字符校验或拒绝包含路径分隔符的输入。 ![]() ![]() | |||
| 1134 | + return ERR_INVALID_PARAM; | ||
| 1135 | + } | ||
| 1136 | + | ||
| 1137 | + ToolInfo toolInfo; | ||
| 1138 | + if (CliToolDataManager::GetInstance().GetToolByName(toolName, toolInfo) != ERR_OK) { | ||
| 1139 | + ReportCliExecuteFailed(bundleName, toolName, GetFailureReason(ERR_TOOL_NOT_EXIST)); | ||
| 1140 | + return ERR_TOOL_NOT_EXIST; | ||
| 1141 | + } | ||
| 1142 | + | ||
| 1143 | + // Step 2: Parse command string. | ||
| 1144 | + ExecToolParam toolParam; | ||
| 1145 | + std::string detail; | ||
| 1146 | + auto parseRet = ToolUtil::ParseToolCommand(param.cmd, toolInfo, toolParam, detail); | ||
| 1147 | + if (parseRet != ERR_OK) { | ||
| 1148 | + ReportCliExecuteFailed(bundleName, toolName, GetFailureReason(parseRet), detail); | ||
| 1149 | + return parseRet; | ||
| 1150 | + } | ||
| 1151 | + | ||
| 1152 | + toolParam.options = param.options; | ||
| 1153 | + toolParam.challenge = ""; | ||
| 1154 | + | ||
| 1155 | + // Step 3: Validate and prepare sandbox. GenerateSandboxConfig may overwrite bundleName. | ||
| 1156 | + std::string sandboxConfig; | ||
| 1157 | + std::string mutableBundleName = bundleName; | ||
| 1158 | + if (auto ret = ValidateAndPrepareTool(toolParam, tokenId, toolInfo, sandboxConfig, mutableBundleName, detail); | ||
| 1159 | + ret != ERR_OK) { | ||
| 1160 | + ReportCliExecuteFailed(bundleName, toolName, GetFailureReason(ret), detail); | ||
| 1161 | + return ret; | ||
| 1162 | + } | ||
| 1163 | + | ||
| 1164 | + // Step 4: Subscribe before process creation (aligned with shell path). | ||
| 1165 | + return SetupCmdSession(toolParam, toolInfo, sandboxConfig, eventId, subscriptionId, | ||
| 1166 | + scheduler, callerPid, callerUid, mutableBundleName, toolName); | ||
| 1167 | +} | ||
| 1168 | + | ||
| 1169 | +int32_t CliToolManagerService::SetupCmdSession(const ExecToolParam &toolParam, const ToolInfo &toolInfo, | ||
| 1170 | + const std::string &sandboxConfig, const std::string &eventId, | ||
| 1171 | + const std::string &subscriptionId, const sptr<ICliToolManagerScheduler> &scheduler, | ||
| 1172 | + int32_t callerPid, int32_t callerUid, const std::string &bundleName, | ||
| 1173 | + const std::string &toolName) | ||
| 1174 | +{ | ||
| 1175 | + auto record = CreateSessionRecord(toolParam, eventId); | ||
| 1176 | + if (record == nullptr) { | ||
| 1177 | + ReportCliExecuteFailed(bundleName, toolName, GetFailureReason(ERR_NO_INIT)); | ||
| 1178 | + return ERR_NO_INIT; | ||
| 1179 | + } | ||
| 1180 | + AddSessionRecord(record); | ||
| 1181 | + | ||
| 1182 | + auto subscribeRet = SubscribeSession(record->sessionId, subscriptionId, scheduler); | ||
| 1183 | + if (subscribeRet != ERR_OK) { | ||
| 1184 | + RemoveSessionRecord(record->sessionId); | ||
| 1185 | + ReportCliExecuteFailed(bundleName, toolName, GetFailureReason(subscribeRet)); | ||
| 1186 | + return subscribeRet; | ||
| 1187 | + } | ||
| 1188 | + | ||
| 1189 | + auto createRet = ProcessManager::GetInstance().CreateChildProcess( | ||
| 1190 | + toolParam, sandboxConfig, toolInfo, record); | ||
| 1191 | + if (createRet != ERR_OK) { | ||
| 1192 | + EventDispatcher::GetInstance().UnregisterSubscriber( | ||
| 1193 | + record->sessionId, subscriptionId, callerPid, callerUid); | ||
| 1194 | + RemoveSessionRecord(record->sessionId); | ||
| 1195 | + ReportCliExecuteFailed(bundleName, toolName, GetFailureReason(createRet)); | ||
| 1196 | + return createRet; | ||
| 1197 | + } | ||
| 1198 | + | ||
| 1199 | + if (!RegisterSessionWithMonitors(record, toolParam.options)) { | ||
| 1200 | + ProcessManager::GetInstance().Killpg(record->processId); | ||
| 1201 | + EventDispatcher::GetInstance().UnregisterSubscriber( | ||
| 1202 | + record->sessionId, subscriptionId, callerPid, callerUid); | ||
| 1203 | + RemoveSessionRecord(record->sessionId); | ||
| 1204 | + ReportCliExecuteFailed(bundleName, toolName, GetFailureReason(ERR_NO_INIT)); | ||
| 1205 | + return ERR_NO_INIT; | ||
| 1206 | + } | ||
| 1207 | + | ||
| 1208 | + if (!bundleName.empty()) { | ||
| 1209 | + RegisterAppStateObserver(bundleName, record->callerPid); | ||
| 1210 | + } | ||
| 1211 | + | ||
| 1212 | + if (toolParam.options.background) { | ||
| 1213 | + HandleBackgroundSessionReply(record, eventId); | ||
| 1214 | + } | ||
| 1215 | + | ||
| 1216 | + return ERR_OK; | ||
| 1217 | +} | ||
| 1218 | + | ||
| 1104 | void CliToolManagerService::PostExecToolTask(int64_t time, const std::string &sessionId, bool isTimeout) | 1219 | void CliToolManagerService::PostExecToolTask(int64_t time, const std::string &sessionId, bool isTimeout) |
| 1105 | { | 1220 | { |
| 1106 | auto timeoutTask = [sessionId, isTimeout]() { | 1221 | auto timeoutTask = [sessionId, isTimeout]() { |
| @@ -16,7 +16,10 @@ | |||
| 16 | 16 | ||
| 17 | 17 | ||
| 18 | 18 | ||
| 19 | + | ||
| 19 | 20 | ||
| 21 | + | ||
| 22 | + | ||
| 20 | 23 | ||
| 21 | 24 | ||
| 22 | 25 | ||
| @@ -25,10 +28,12 @@ | |||
| 25 | 28 | ||
| 26 | 29 | ||
| 27 | 30 | ||
| 31 | + | ||
| 28 | 32 | ||
| 29 | 33 | ||
| 30 | 34 | ||
| 31 | 35 | ||
| 36 | + | ||
| 32 | 37 | ||
| 33 | 38 | ||
| 34 | 39 | ||
| @@ -43,7 +48,6 @@ | |||
| 43 | 48 | ||
| 44 | 49 | ||
| 45 | 50 | ||
| 46 | - | ||
| 47 | 51 | ||
| 48 | namespace OHOS { | 52 | namespace OHOS { |
| 49 | namespace CliTool { | 53 | namespace CliTool { |
| @@ -311,6 +315,325 @@ void ToolUtil::TransferToCmdParam(const AAFwk::WantParams &args, std::vector<std | |||
| 311 | } | 315 | } |
| 312 | } | 316 | } |
| 313 | 317 | ||
| 318 | +// ============================================================================ | ||
| 319 | +// ParseToolCommand: parse a concatenated tool command string (tool command mode) | ||
| 320 | +// Supported syntax (long-flag only): | ||
| 321 | +// <toolName> [subcommand] [--key=value | --key value | --flag | --flag false]... | ||
| 322 | +// Single-quoted values are kept as one token and the quotes are stripped. | ||
| 323 | +// Repeated flags of the same key are collected into an array. | ||
| 324 | +// ============================================================================ | ||
| 325 | + | ||
| 326 | +namespace { | ||
| 327 | +constexpr char FLAG_PREFIX[] = "--"; | ||
| 328 | +constexpr size_t FLAG_PREFIX_LEN = 2; | ||
| 329 | +constexpr char HELP_KEY[] = "help"; | ||
| 330 | +// Argument start index: tokens[0] is always toolName. | ||
| 331 | +constexpr size_t ARG_START_TOOLNAME_ONLY = 1; // no subcommand | ||
| 332 | +constexpr size_t ARG_START_WITH_SUBCOMMAND = 2; // tokens[1] is subcommand | ||
| 333 | + | ||
| 334 | +struct ParsedCommandArgs { | ||
| 335 | + std::map<std::string, std::vector<std::string>> values; | ||
| 336 | +}; | ||
| 337 | + | ||
| 338 | +struct SubCommandResult { | ||
| 339 | + std::string inputSchema; | ||
| 340 | + size_t argStart = ARG_START_TOOLNAME_ONLY; | ||
| 341 | + std::string subcommand; | ||
| 342 | +}; | ||
| 343 | + | ||
| 344 | +bool TokenizeCommand(const std::string &cmd, std::vector<std::string> &tokens) | ||
| 345 | +{ | ||
H [次要] TokenizeCommand 仅处理单引号,不支持双引号和转义字符。对于包含双引号或嵌套引号的命令字符串可能解析错误。建议在注释或文档中明确说明支持的语法范围,或补充对双引号和反斜杠转义的支持。 ![]() ![]() | |||
| 346 | + std::string current; | ||
| 347 | + bool inQuote = false; | ||
| 348 | + for (char c : cmd) { | ||
| 349 | + if (c == '\'') { | ||
| 350 | + inQuote = !inQuote; | ||
| 351 | + continue; | ||
| 352 | + } | ||
| 353 | + if ((c == ' ' || c == '\t') && !inQuote) { | ||
| 354 | + if (!current.empty()) { | ||
| 355 | + tokens.push_back(current); | ||
| 356 | + current.clear(); | ||
| 357 | + } | ||
| 358 | + continue; | ||
| 359 | + } | ||
| 360 | + current.push_back(c); | ||
| 361 | + } | ||
| 362 | + if (inQuote) { | ||
| 363 | + return false; | ||
| 364 | + } | ||
| 365 | + if (!current.empty()) { | ||
| 366 | + tokens.push_back(current); | ||
| 367 | + } | ||
| 368 | + return true; | ||
| 369 | +} | ||
| 370 | + | ||
| 371 | +bool IsFlagToken(const std::string &token) | ||
| 372 | +{ | ||
| 373 | + return token.size() >= FLAG_PREFIX_LEN && token.compare(0, FLAG_PREFIX_LEN, FLAG_PREFIX) == 0; | ||
| 374 | +} | ||
| 375 | + | ||
| 376 | +bool IsBooleanSchema(const nlohmann::json &properties, const std::string &key) | ||
| 377 | +{ | ||
| 378 | + auto it = properties.find(key); | ||
| 379 | + if (it == properties.end() || !it->contains("type") || !it->at("type").is_string()) { | ||
| 380 | + return false; | ||
| 381 | + } | ||
| 382 | + return it->at("type").get<std::string>() == "boolean"; | ||
| 383 | +} | ||
| 384 | + | ||
| 385 | +int32_t ParseArgTokens(const std::vector<std::string> &tokens, size_t start, | ||
| 386 | + const nlohmann::json &properties, ParsedCommandArgs &parsed, std::string &detail) | ||
| 387 | +{ | ||
| 388 | + for (size_t i = start; i < tokens.size(); ++i) { | ||
| 389 | + const std::string &token = tokens[i]; | ||
| 390 | + if (!IsFlagToken(token)) { | ||
| 391 | + // Bare token without "--" prefix (short flags / custom prefix) is not supported. | ||
| 392 | + detail = DETAIL_PARAM_NOT_FOUND; | ||
| 393 | + return ERR_INVALID_PARAM; | ||
| 394 | + } | ||
| 395 | + std::string flagText = token.substr(FLAG_PREFIX_LEN); | ||
| 396 | + auto eqPos = flagText.find('='); | ||
| 397 | + std::string key = (eqPos == std::string::npos) ? flagText : flagText.substr(0, eqPos); | ||
| 398 | + if (key.empty()) { | ||
| 399 | + detail = DETAIL_PARAM_NOT_FOUND; | ||
| 400 | + return ERR_INVALID_PARAM; | ||
| 401 | + } | ||
| 402 | + // "help" is always allowed, matching ValidateInputSchemaProperties. | ||
| 403 | + if (key == HELP_KEY) { | ||
| 404 | + parsed.values[key].push_back("true"); | ||
| 405 | + continue; | ||
| 406 | + } | ||
| 407 | + if (!properties.contains(key)) { | ||
| 408 | + detail = DETAIL_PARAM_NOT_FOUND; | ||
| 409 | + return ERR_INVALID_PARAM; | ||
| 410 | + } | ||
| 411 | + // --key=value | ||
| 412 | + if (eqPos != std::string::npos) { | ||
| 413 | + parsed.values[key].push_back(flagText.substr(eqPos + 1)); | ||
| 414 | + continue; | ||
| 415 | + } | ||
| 416 | + // --key with no inline value: decide whether it takes a value or is a boolean flag. | ||
| 417 | + bool isBool = IsBooleanSchema(properties, key); | ||
| 418 | + if (i + 1 >= tokens.size() || IsFlagToken(tokens[i + 1])) { | ||
| 419 | + if (!isBool) { | ||
| 420 | + detail = DETAIL_PARAM_TYPE_MISMATCH; | ||
| 421 | + return ERR_INVALID_PARAM; | ||
| 422 | + } | ||
| 423 | + parsed.values[key].push_back("true"); | ||
| 424 | + continue; | ||
| 425 | + } | ||
| 426 | + // --key false (explicit boolean false) | ||
| 427 | + if (isBool && tokens[i + 1] == "false") { | ||
| 428 | + parsed.values[key].push_back("false"); | ||
| 429 | + ++i; | ||
| 430 | + continue; | ||
| 431 | + } | ||
| 432 | + // --key value | ||
| 433 | + parsed.values[key].push_back(tokens[i + 1]); | ||
| 434 | + ++i; | ||
| 435 | + } | ||
| 436 | + return ERR_OK; | ||
| 437 | +} | ||
| 438 | + | ||
| 439 | +bool ConvertRawValue(const std::string &raw, const std::string &type, sptr<AAFwk::IInterface> &value) | ||
| 440 | +{ | ||
| 441 | + if (type == "boolean") { | ||
| 442 | + if (raw == "true") { | ||
| 443 | + value = AAFwk::Boolean::Box(true); | ||
| 444 | + return true; | ||
| 445 | + } | ||
| 446 | + if (raw == "false") { | ||
| 447 | + value = AAFwk::Boolean::Box(false); | ||
| 448 | + return true; | ||
| 449 | + } | ||
| 450 | + return false; | ||
| 451 | + } | ||
| 452 | + if (type == "integer") { | ||
| 453 | + errno = 0; | ||
| 454 | + char *end = nullptr; | ||
| 455 | + long long intVal = std::strtoll(raw.c_str(), &end, 10); | ||
| 456 | + if (errno != 0 || end == raw.c_str() || *end != '\0' || | ||
| 457 | + intVal < INT32_MIN || intVal > INT32_MAX) { | ||
| 458 | + return false; | ||
| 459 | + } | ||
| 460 | + value = AAFwk::Integer::Box(static_cast<int32_t>(intVal)); | ||
| 461 | + return true; | ||
| 462 | + } | ||
| 463 | + if (type == "number") { | ||
| 464 | + errno = 0; | ||
| 465 | + char *end = nullptr; | ||
| 466 | + double dblVal = std::strtod(raw.c_str(), &end); | ||
| 467 | + if (errno != 0 || end == raw.c_str() || *end != '\0') { | ||
| 468 | + return false; | ||
| 469 | + } | ||
| 470 | + value = AAFwk::Double::Box(dblVal); | ||
| 471 | + return true; | ||
| 472 | + } | ||
| 473 | + // string and other unknown types are kept as string (compatible with | ||
| 474 | + // ValidateBasicType which allows unknown types). | ||
| 475 | + value = AAFwk::String::Box(raw); | ||
| 476 | + return true; | ||
| 477 | +} | ||
| 478 | + | ||
| 479 | +AAFwk::InterfaceID GetArrayTypeId(const std::string &type) | ||
| 480 | +{ | ||
| 481 | + if (type == "boolean") { | ||
| 482 | + return AAFwk::g_IID_IBoolean; | ||
| 483 | + } | ||
| 484 | + if (type == "integer") { | ||
| 485 | + return AAFwk::g_IID_IInteger; | ||
| 486 | + } | ||
| 487 | + if (type == "number") { | ||
| 488 | + return AAFwk::g_IID_IDouble; | ||
| 489 | + } | ||
| 490 | + return AAFwk::g_IID_IString; | ||
| 491 | +} | ||
| 492 | + | ||
| 493 | +int32_t ConvertArrayArg(const nlohmann::json &prop, const std::vector<std::string> &values, | ||
| 494 | + AAFwk::WantParams &args, const std::string &key, std::string &detail) | ||
| 495 | +{ | ||
| 496 | + std::string itemType = "string"; | ||
| 497 | + if (prop.contains("items") && prop["items"].is_object() && | ||
| 498 | + prop["items"].contains("type") && prop["items"]["type"].is_string()) { | ||
| 499 | + itemType = prop["items"]["type"].get<std::string>(); | ||
| 500 | + } | ||
| 501 | + sptr<AAFwk::IArray> array = new (std::nothrow) AAFwk::Array( | ||
| 502 | + static_cast<long>(values.size()), GetArrayTypeId(itemType)); | ||
| 503 | + if (array == nullptr) { | ||
| 504 | + detail = DETAIL_PARAM_TYPE_MISMATCH; | ||
| 505 | + return ERR_INVALID_PARAM; | ||
| 506 | + } | ||
| 507 | + for (size_t i = 0; i < values.size(); ++i) { | ||
| 508 | + sptr<AAFwk::IInterface> element; | ||
| 509 | + if (!ConvertRawValue(values[i], itemType, element)) { | ||
| 510 | + detail = DETAIL_PARAM_TYPE_MISMATCH; | ||
| 511 | + return ERR_INVALID_PARAM; | ||
| 512 | + } | ||
| 513 | + array->Set(static_cast<long>(i), element); | ||
| 514 | + } | ||
| 515 | + args.SetParam(key, array); | ||
| 516 | + return ERR_OK; | ||
| 517 | +} | ||
| 518 | + | ||
| 519 | +int32_t ConvertParsedArgs(const ParsedCommandArgs &parsed, const nlohmann::json &properties, | ||
| 520 | + AAFwk::WantParams &args, std::string &detail) | ||
| 521 | +{ | ||
| 522 | + for (const auto &[key, values] : parsed.values) { | ||
| 523 | + if (key == HELP_KEY) { | ||
| 524 | + args.SetParam(key, AAFwk::Boolean::Box(true)); | ||
| 525 | + continue; | ||
| 526 | + } | ||
| 527 | + auto propIt = properties.find(key); | ||
| 528 | + if (propIt == properties.end()) { | ||
| 529 | + continue; | ||
| 530 | + } | ||
| 531 | + const nlohmann::json &prop = propIt.value(); | ||
| 532 | + std::string type = "string"; | ||
| 533 | + if (prop.contains("type") && prop["type"].is_string()) { | ||
| 534 | + type = prop["type"].get<std::string>(); | ||
| 535 | + } | ||
| 536 | + if (type == "array") { | ||
| 537 | + if (ConvertArrayArg(prop, values, args, key, detail) != ERR_OK) { | ||
| 538 | + return ERR_INVALID_PARAM; | ||
| 539 | + } | ||
| 540 | + continue; | ||
| 541 | + } | ||
| 542 | + // Non-array parameter must not be repeated. | ||
| 543 | + if (values.size() > 1) { | ||
| 544 | + detail = DETAIL_PARAM_TYPE_MISMATCH; | ||
| 545 | + return ERR_INVALID_PARAM; | ||
| 546 | + } | ||
| 547 | + sptr<AAFwk::IInterface> value; | ||
| 548 | + if (!ConvertRawValue(values[0], type, value)) { | ||
| 549 | + detail = DETAIL_PARAM_TYPE_MISMATCH; | ||
| 550 | + return ERR_INVALID_PARAM; | ||
| 551 | + } | ||
| 552 | + args.SetParam(key, value); | ||
| 553 | + } | ||
| 554 | + return ERR_OK; | ||
| 555 | +} | ||
| 556 | + | ||
| 557 | +int32_t ResolveSubCommand(const std::vector<std::string> &tokens, const ToolInfo &toolInfo, | ||
| 558 | + SubCommandResult &result, std::string &detail) | ||
| 559 | +{ | ||
| 560 | + result.inputSchema = toolInfo.inputSchema; | ||
| 561 | + result.argStart = ARG_START_TOOLNAME_ONLY; | ||
| 562 | + if (tokens.size() <= 1 || IsFlagToken(tokens[1])) { | ||
| 563 | + return ERR_OK; | ||
| 564 | + } | ||
| 565 | + if (!toolInfo.hasSubCommand) { | ||
| 566 | + TAG_LOGE(AAFwkTag::CLI_TOOL, "tool has no subcommand"); | ||
| 567 | + detail = DETAIL_SUBCOMMAND_NOT_FOUND; | ||
| 568 | + return ERR_TOOL_NOT_EXIST; | ||
| 569 | + } | ||
| 570 | + auto it = toolInfo.subcommands.find(tokens[1]); | ||
| 571 | + if (it == toolInfo.subcommands.end()) { | ||
| 572 | + TAG_LOGE(AAFwkTag::CLI_TOOL, "subcommand not found: %{public}s", tokens[1].c_str()); | ||
| 573 | + detail = DETAIL_SUBCOMMAND_NOT_FOUND; | ||
| 574 | + return ERR_TOOL_NOT_EXIST; | ||
| 575 | + } | ||
| 576 | + result.subcommand = tokens[1]; | ||
| 577 | + result.inputSchema = it->second.inputSchema; | ||
| 578 | + result.argStart = ARG_START_WITH_SUBCOMMAND; | ||
| 579 | + return ERR_OK; | ||
| 580 | +} | ||
| 581 | +} // namespace | ||
| 582 | + | ||
| 583 | +int32_t ToolUtil::ParseToolCommand(const std::string &cmd, const ToolInfo &toolInfo, | ||
| 584 | + ExecToolParam ¶m, std::string &detail) | ||
| 585 | +{ | ||
| 586 | + // Stage 0: tokenize. | ||
| 587 | + std::vector<std::string> tokens; | ||
| 588 | + if (!TokenizeCommand(cmd, tokens) || tokens.empty()) { | ||
| 589 | + TAG_LOGE(AAFwkTag::CLI_TOOL, "invalid tool command string"); | ||
| 590 | + detail = DETAIL_PARAM_NOT_FOUND; | ||
| 591 | + return ERR_INVALID_PARAM; | ||
| 592 | + } | ||
| 593 | + | ||
| 594 | + // Stage 1: validate toolName. | ||
| 595 | + const std::string &toolName = tokens[0]; | ||
| 596 | + if (toolName.empty() || toolName[0] == '/') { | ||
| 597 | + TAG_LOGE(AAFwkTag::CLI_TOOL, "invalid toolName"); | ||
| 598 | + detail = DETAIL_PARAM_NOT_FOUND; | ||
| 599 | + return ERR_INVALID_PARAM; | ||
| 600 | + } | ||
| 601 | + if (!toolInfo.name.empty() && toolInfo.name != toolName) { | ||
| 602 | + TAG_LOGE(AAFwkTag::CLI_TOOL, "tool not found: %{public}s", toolName.c_str()); | ||
| 603 | + detail = DETAIL_TOOL_NOT_FOUND; | ||
| 604 | + return ERR_TOOL_NOT_EXIST; | ||
| 605 | + } | ||
| 606 | + param.toolName = toolName; | ||
| 607 | + | ||
| 608 | + // Stage 2: resolve subcommand. | ||
| 609 | + SubCommandResult subResult; | ||
| 610 | + if (auto ret = ResolveSubCommand(tokens, toolInfo, subResult, detail); ret != ERR_OK) { | ||
| 611 | + return ret; | ||
| 612 | + } | ||
| 613 | + param.subcommand = std::move(subResult.subcommand); | ||
| 614 | + const std::string &inputSchema = subResult.inputSchema; | ||
| 615 | + size_t argStart = subResult.argStart; | ||
| 616 | + | ||
| 617 | + // Load properties from schema. | ||
| 618 | + nlohmann::json properties = nlohmann::json::object(); | ||
| 619 | + if (!inputSchema.empty()) { | ||
| 620 | + nlohmann::json schema = nlohmann::json::parse(inputSchema, nullptr, false); | ||
| 621 | + if (!schema.is_discarded() && schema.contains("properties") && schema["properties"].is_object()) { | ||
| 622 | + properties = schema["properties"]; | ||
| 623 | + } | ||
| 624 | + } | ||
| 625 | + | ||
| 626 | + // Stage 3: parse arguments. | ||
| 627 | + ParsedCommandArgs parsed; | ||
| 628 | + auto res = ParseArgTokens(tokens, argStart, properties, parsed, detail); | ||
| 629 | + if (res != ERR_OK) { | ||
| 630 | + return res; | ||
| 631 | + } | ||
| 632 | + | ||
| 633 | + // Stage 4: type recovery. | ||
| 634 | + return ConvertParsedArgs(parsed, properties, param.args, detail); | ||
| 635 | +} | ||
| 636 | + | ||
| 314 | void ToolUtil::ProcessBooleanParam(const std::string &key, const sptr<AAFwk::IInterface> &value, | 637 | void ToolUtil::ProcessBooleanParam(const std::string &key, const sptr<AAFwk::IInterface> &value, |
| 315 | std::vector<std::string> &execArgs) | 638 | std::vector<std::string> &execArgs) |
| 316 | { | 639 | { |


[次要] ExtractToolName 的逻辑与 cli_tool_manager_service.cpp 中 ExecCmdToolMode 内的 toolName 提取逻辑完全重复,建议将其抽取到 ToolUtil 中作为公共静态方法复用,避免逻辑不一致风险。