diff --git a/tools/inject_event/include/input_replay_command.h b/tools/inject_event/include/input_replay_command.h new file mode 100644 index 0000000000000000000000000000000000000000..029e970bdda24176dc16b51d72ce9526425eeee5 --- /dev/null +++ b/tools/inject_event/include/input_replay_command.h @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef CMD_PARSER_H +#define CMD_PARSER_H + +#include +#include +#include + +namespace OHOS { +namespace MMI { +class InputReplayCommand { +public: + InputReplayCommand(int32_t argc, char** argv); + bool Parse(); + bool Execute(); + + static int32_t HandleRecordReplayCommand(int32_t argc, char** argv); +private: + bool ParseOptions(bool& useAllDevices); + bool ParseDeviceMapping(const std::string& mappingStr); + void SetupSignalHandlers(); + bool ParseRecordCommand(bool useAllDevices); + bool ParseReplayCommand(); + bool ExecuteRecordCommand(); + bool ExecuteReplayCommand(); + void PrintUsage() const; + + int32_t argc_; + char *const *argv_; + std::string programName_; + std::string command_; + std::string filePath_; + std::vector devicePaths_; + bool useAllDevices_ { false }; + std::map deviceMapping_; +}; +} // namespace MMI +} // namespace OHOS +#endif // CMD_PARSER_H \ No newline at end of file diff --git a/tools/inject_event/src/input_replay_command.cpp b/tools/inject_event/src/input_replay_command.cpp new file mode 100644 index 0000000000000000000000000000000000000000..75f7cb79dd674cef41ebe735875a71a9cc53e02d --- /dev/null +++ b/tools/inject_event/src/input_replay_command.cpp @@ -0,0 +1,284 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "input_replay_command.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.h" +#include "device_manager.h" +#include "event_recorder.h" +#include "event_replayer.h" + +namespace OHOS { +namespace MMI { +namespace { +constexpr int32_t MIN_ARGC = 2; +} + +std::atomic g_shutdown { false }; + +InputReplayCommand::InputReplayCommand(int32_t argc, char** argv) + : argc_(argc), argv_(argv) +{ + programName_ = (argc > 0) ? argv[0] : "uinput"; +} + +bool InputReplayCommand::ParseOptions(bool& useAllDevices) +{ + static struct option longOptions[] = { + {"help", no_argument, 0, 'h'}, + {"list", no_argument, 0, 'l'}, + {"all", no_argument, 0, 'a'}, + {"map", required_argument, 0, 'm'}, + {0, 0, 0, 0} + }; + int32_t opt; + int32_t optionIndex = 0; + optind = 1; + while ((opt = getopt_long(argc_, argv_, "hlam:", longOptions, &optionIndex)) != -1) { + switch (opt) { + case 'h': + PrintUsage(); + exit(0); + case 'l': + DeviceManager().PrintDeviceList(); + exit(0); + case 'a': + useAllDevices = true; + break; + case 'm': + if (!ParseDeviceMapping(optarg)) { + return false; + } + break; + default: + return false; + } + } + return true; +} + +bool InputReplayCommand::Parse() +{ + if (argc_ < MIN_ARGC) { + PrintUsage(); + return false; + } + bool useAllDevices = false; + if (!ParseOptions(useAllDevices)) { + return false; + } + if (optind >= argc_) { + PrintError("Missing command (record/replay)"); + return false; + } + command_ = argv_[optind++]; + if (optind >= argc_) { + PrintError("Missing file path"); + return false; + } + filePath_ = argv_[optind++]; + if (command_ == "record") { + return ParseRecordCommand(useAllDevices); + } else if (command_ == "replay") { + return ParseReplayCommand(); + } else { + PrintError("Invalid command, supported: record/replay"); + return false; + } +} + +inline void SignalHandler(int32_t sig) +{ + if (sig == SIGINT || sig == SIGTERM) { + g_shutdown.store(true); + std::cout << "\nShutdown signal received, cleaning up..." << std::endl; + } +} + +int32_t InputReplayCommand::HandleRecordReplayCommand(int32_t argc, char** argv) +{ + OHOS::MMI::InputReplayCommand parser(argc, argv); + if (geteuid() != 0) { + std::cerr << "Error: This program must be run as root" << std::endl; + return RET_ERR; + } + if (!parser.Parse()) { + std::cerr << "Failed to parse record/replay command" << std::endl; + return RET_ERR; + } + + if (!parser.Execute()) { + return RET_ERR; + } + return RET_OK; +} + +bool InputReplayCommand::Execute() +{ + if (command_ == "record") { + return ExecuteRecordCommand(); + } else if (command_ == "replay") { + return ExecuteReplayCommand(); + } + return false; +} + +bool InputReplayCommand::ParseDeviceMapping(const std::string& mappingStr) +{ + deviceMapping_.clear(); + const char* ptr = mappingStr.c_str(); + const char* endPtr = ptr + mappingStr.length(); + while (ptr < endPtr) { + uint16_t key; + auto keyResult = std::from_chars(ptr, endPtr, key); + if (keyResult.ec != std::errc() || keyResult.ptr >= endPtr || *keyResult.ptr != ':') { + return false; + } + ptr = keyResult.ptr + 1; + uint16_t value; + auto valueResult = std::from_chars(ptr, endPtr, value); + if (valueResult.ec != std::errc()) { + return false; + } + deviceMapping_[key] = value; + ptr = valueResult.ptr; + if (ptr < endPtr) { + if (*ptr != ',') { + return false; + } + ptr++; + } + } + return !deviceMapping_.empty(); +} + +void InputReplayCommand::SetupSignalHandlers() +{ + struct sigaction sa; + sa.sa_handler = SignalHandler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sigaction(SIGINT, &sa, nullptr); + sigaction(SIGTERM, &sa, nullptr); +} + +bool InputReplayCommand::ParseRecordCommand(bool useAllDevices) +{ + useAllDevices_ = useAllDevices; + if (!useAllDevices_) { + for (int32_t i = optind; i < argc_; i++) { + devicePaths_.push_back(argv_[i]); + } + if (devicePaths_.empty()) { + PrintError("No devices specified for recording"); + PrintError("Use --all to record from all devices or specify device paths"); + return false; + } + } + return true; +} + +bool InputReplayCommand::ParseReplayCommand() +{ + if (optind < argc_) { + PrintError("Unexpected arguments for replay command"); + return false; + } + return true; +} + +bool InputReplayCommand::ExecuteRecordCommand() +{ + std::vector devices; + if (useAllDevices_) { + DeviceManager deviceManager; + devices = deviceManager.DiscoverDevices(); + } else { + const std::string PREFIX = "/dev/input/event"; + for (size_t i = 0; i < devicePaths_.size(); i++) { + const std::string& path = devicePaths_[i]; + if (path.substr(0, PREFIX.length()) != PREFIX) { + PrintError("Invalid input device path format: %s", path.c_str()); + return false; + } + uint16_t deviceId = 0; + const char* start = path.c_str() + PREFIX.length(); + const char* end = path.c_str() + path.length(); + std::from_chars_result result = std::from_chars(start, end, deviceId); + if (result.ec != std::errc() || result.ptr != end) { + PrintError("Invalid device number in path: %s", path.c_str()); + return false; + } + InputDevice device(path, deviceId); + if (device.IsOpen()) { + devices.push_back(std::move(device)); + } else { + PrintWarning("Failed to open device: %s", path.c_str()); + } + } + } + if (devices.empty()) { + PrintError("No valid input devices specified"); + return false; + } + SetupSignalHandlers(); + EventRecorder recorder(filePath_); + if (!recorder.Start(devices)) { + return false; + } + recorder.Stop(); + return true; +} + +bool InputReplayCommand::ExecuteReplayCommand() +{ + SetupSignalHandlers(); + PrintInfo("Press Enter to start replay..."); + std::cin.get(); + EventReplayer replayer(filePath_, deviceMapping_); + return replayer.Replay(); +} + +void InputReplayCommand::PrintUsage() const +{ + std::cout << "Usage:" << std::endl + << " " << programName_ << " [options] record [device paths...]" << std::endl + << " " << programName_ << " [options] replay " << std::endl + << std::endl + << "Options:" << std::endl + << " -h, --help Show this help message" << std::endl + << " -l, --list List available input devices" << std::endl + << " -a, --all Record from all available input devices" << std::endl + << " -m, --map Specify device mapping for replay (e.g., \"0:0,1:2,4:2\")" << std::endl + << " Format: sourceDeviceId:targetDeviceId,..." << std::endl + << std::endl + << "Examples:" << std::endl + << " " << programName_ << " record -a events.bin # Record from all devices" << std::endl + << " " << programName_ << " record events.bin /dev/input/event0 /dev/input/event1" << std::endl + << " " << programName_ << " replay events.bin # Replay to original devices" << std::endl + << " " << programName_ + << " replay -m \"0:1,1:0\" events.bin # Replay with custom device mapping" << std::endl; +} +} // namespace MMI +} // namespace OHOS \ No newline at end of file diff --git a/tools/inject_event/test/input_replay_command_test.cpp b/tools/inject_event/test/input_replay_command_test.cpp new file mode 100644 index 0000000000000000000000000000000000000000..b26a1b778e61bede9aa38392a56be76cd3f11ae3 --- /dev/null +++ b/tools/inject_event/test/input_replay_command_test.cpp @@ -0,0 +1,302 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "mmi_log.h" +#include "input_replay_command.h" + +namespace OHOS { +namespace MMI { +namespace { +using namespace testing::ext; +} // namespace + +class InputReplayCommandTest : public testing::Test { +public: + static void SetUpTestCase(void) {} + static void TearDownTestCase(void) {} + void SetUp() {} + void TearDown() {} +}; + +/** + * @tc.name: InputReplayCommandTest_Constructor + * @tc.desc: Test constructor of InputReplayCommand + * @tc.type: FUNC + * @tc.require: + */ +HWTEST_F(InputReplayCommandTest, InputReplayCommandTest_Constructor, TestSize.Level1) +{ + char programName[] = {"program_name"}; + char* argv[] = {programName, nullptr}; + InputReplayCommand command(1, argv); + SUCCEED(); +} + +/** + * @tc.name: InputReplayCommandTest_ParseDeviceMapping_Valid + * @tc.desc: Test parsing valid device mapping + * @tc.type: FUNC + * @tc.require: + */ +HWTEST_F(InputReplayCommandTest, InputReplayCommandTest_ParseDeviceMapping_Valid, TestSize.Level1) +{ + char programName[] = {"program_name"}; + char mapArg[] = {"--map"}; + char mapValue[] = {"0:1,2:3,4:5"}; + char recordArg[] = {"record"}; + char outputPath[] = {"output.bin"}; + char devicePath[] = {"/dev/input/event0"}; + + char* mappingArgv[] = { + programName, + mapArg, mapValue, + recordArg, outputPath, + devicePath + }; + InputReplayCommand mappingCommand(6, mappingArgv); + EXPECT_TRUE(mappingCommand.Parse()); +} + +/** + * @tc.name: InputReplayCommandTest_ParseDeviceMapping_Invalid + * @tc.desc: Test parsing invalid device mapping + * @tc.type: FUNC + * @tc.require: + */ +HWTEST_F(InputReplayCommandTest, InputReplayCommandTest_ParseDeviceMapping_Invalid, TestSize.Level1) +{ + char programName[] = {"program_name"}; + char mapArg[] = {"--map"}; + char invalidMapValue[] = {"0:1,invalid,4:5"}; + char invalidFormatValue[] = {"0:1:2"}; + char emptyMapValue[] = {""}; + char recordArg[] = {"record"}; + char outputPath[] = {"output.bin"}; + char devicePath[] = {"/dev/input/event0"}; + + // Invalid mapping value + char* invalidMappingArgv[] = { + programName, + mapArg, invalidMapValue, + recordArg, outputPath, + devicePath + }; + InputReplayCommand invalidCommand(6, invalidMappingArgv); + EXPECT_FALSE(invalidCommand.Parse()); + + // Invalid format + char* invalidFormatArgv[] = { + programName, + mapArg, invalidFormatValue, + recordArg, outputPath, + devicePath + }; + InputReplayCommand invalidFormatCommand(6, invalidFormatArgv); + EXPECT_FALSE(invalidFormatCommand.Parse()); + + // Empty mapping + char* emptyMappingArgv[] = { + programName, + mapArg, emptyMapValue, + recordArg, outputPath, + devicePath + }; + InputReplayCommand emptyMappingCommand(6, emptyMappingArgv); + EXPECT_FALSE(emptyMappingCommand.Parse()); +} + +/** + * @tc.name: InputReplayCommandTest_ParseRecord_WithDevices + * @tc.desc: Test parsing record command with specific devices + * @tc.type: FUNC + * @tc.require: + */ +HWTEST_F(InputReplayCommandTest, InputReplayCommandTest_ParseRecord_WithDevices, TestSize.Level1) +{ + char programName[] = {"program_name"}; + char recordArg[] = {"record"}; + char outputPath[] = {"output.bin"}; + char device1[] = {"/dev/input/event0"}; + char device2[] = {"/dev/input/event1"}; + + char* argv[] = { + programName, + recordArg, outputPath, + device1, device2 + }; + InputReplayCommand command(5, argv); + EXPECT_TRUE(command.Parse()); +} + +/** + * @tc.name: InputReplayCommandTest_ParseRecord_AllDevices + * @tc.desc: Test parsing record command with all devices flag + * @tc.type: FUNC + * @tc.require: + */ +HWTEST_F(InputReplayCommandTest, InputReplayCommandTest_ParseRecord_AllDevices, TestSize.Level1) +{ + char programName[] = {"program_name"}; + char allArg[] = {"--all"}; + char recordArg[] = {"record"}; + char outputPath[] = {"output.bin"}; + + char* argv[] = { + programName, + allArg, + recordArg, outputPath + }; + InputReplayCommand command(4, argv); + EXPECT_TRUE(command.Parse()); +} + +/** + * @tc.name: InputReplayCommandTest_ParseRecord_NoDevices + * @tc.desc: Test parsing record command with no devices specified + * @tc.type: FUNC + * @tc.require: + */ +HWTEST_F(InputReplayCommandTest, InputReplayCommandTest_ParseRecord_NoDevices, TestSize.Level1) +{ + char programName[] = {"program_name"}; + char recordArg[] = {"record"}; + char outputPath[] = {"output.bin"}; + + char* argv[] = { + programName, + recordArg, outputPath + }; + InputReplayCommand command(3, argv); + EXPECT_FALSE(command.Parse()); +} + +/** + * @tc.name: InputReplayCommandTest_ParseReplay_Valid + * @tc.desc: Test parsing replay command with valid arguments + * @tc.type: FUNC + * @tc.require: + */ +HWTEST_F(InputReplayCommandTest, InputReplayCommandTest_ParseReplay_Valid, TestSize.Level1) +{ + char programName[] = {"program_name"}; + char replayArg[] = {"replay"}; + char inputPath[] = {"input.bin"}; + + char* argv[] = { + programName, + replayArg, inputPath + }; + InputReplayCommand command(3, argv); + EXPECT_TRUE(command.Parse()); +} + +/** + * @tc.name: InputReplayCommandTest_ParseReplay_Invalid + * @tc.desc: Test parsing replay command with extra arguments + * @tc.type: FUNC + * @tc.require: + */ +HWTEST_F(InputReplayCommandTest, InputReplayCommandTest_ParseReplay_Invalid, TestSize.Level1) +{ + char programName[] = {"program_name"}; + char replayArg[] = {"replay"}; + char inputPath[] = {"input.bin"}; + char extraArg[] = {"extra"}; + + char* argv[] = { + programName, + replayArg, inputPath, extraArg + }; + InputReplayCommand command(4, argv); + EXPECT_FALSE(command.Parse()); +} + +/** + * @tc.name: InputReplayCommandTest_Parse_MissingCommand + * @tc.desc: Test parsing with missing command + * @tc.type: FUNC + * @tc.require: + */ +HWTEST_F(InputReplayCommandTest, InputReplayCommandTest_Parse_MissingCommand, TestSize.Level1) +{ + char programName[] = {"program_name"}; + char* argv[] = {programName, nullptr}; + InputReplayCommand command(1, argv); + EXPECT_FALSE(command.Parse()); +} + +/** + * @tc.name: InputReplayCommandTest_Parse_MissingFilePath + * @tc.desc: Test parsing with missing file path + * @tc.type: FUNC + * @tc.require: + */ +HWTEST_F(InputReplayCommandTest, InputReplayCommandTest_Parse_MissingFilePath, TestSize.Level1) +{ + char programName[] = {"program_name"}; + char recordArg[] = {"record"}; + char* argv[] = {programName, recordArg, nullptr}; + InputReplayCommand command(2, argv); + EXPECT_FALSE(command.Parse()); +} + +/** + * @tc.name: InputReplayCommandTest_Parse_InvalidCommand + * @tc.desc: Test parsing with invalid command + * @tc.type: FUNC + * @tc.require: + */ +HWTEST_F(InputReplayCommandTest, InputReplayCommandTest_Parse_InvalidCommand, TestSize.Level1) +{ + char programName[] = {"program_name"}; + char invalidArg[] = {"invalid"}; + char filePath[] = {"file.bin"}; + + char* argv[] = { + programName, + invalidArg, filePath + }; + InputReplayCommand command(3, argv); + EXPECT_FALSE(command.Parse()); +} + +/** + * @tc.name: InputReplayCommandTest_HandleCommand + * @tc.desc: Test the static HandleRecordReplayCommand function with error cases + * @tc.type: FUNC + * @tc.require: + */ +HWTEST_F(InputReplayCommandTest, InputReplayCommandTest_HandleCommand, TestSize.Level1) +{ + // Invalid command + char programName1[] = {"program_name"}; + char invalidArg[] = {"invalid"}; + char* invalidArgv[] = { + programName1, + invalidArg + }; + EXPECT_EQ(RET_ERR, InputReplayCommand::HandleRecordReplayCommand(2, invalidArgv)); + + // Incomplete arguments + char programName2[] = {"program_name"}; + char* incompleteArgv[] = { + programName2 + }; + EXPECT_EQ(RET_ERR, InputReplayCommand::HandleRecordReplayCommand(1, incompleteArgv)); +} +} // namespace MMI +} // namespace OHOS \ No newline at end of file