Skip to content

Commit 6970ebf

Browse files
committed
fix: parse install hook lines with proper quote handling
Signed-off-by: reddevillg <reddevillg@gmail.com>
1 parent d0b8671 commit 6970ebf

7 files changed

Lines changed: 206 additions & 21 deletions

File tree

libs/common/src/linglong/common/strings.cpp

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,29 @@ bool stringEqual(std::string_view str1, std::string_view str2, bool caseSensitiv
4242
});
4343
}
4444

45-
std::string_view trim(std::string_view str, std::string_view chars) noexcept
45+
std::string_view trim_left(std::string_view str, std::string_view chars) noexcept
4646
{
4747
auto first = str.find_first_not_of(chars);
4848
if (first == std::string_view::npos) {
4949
return {};
5050
}
5151

52+
return str.substr(first);
53+
}
54+
55+
std::string_view trim_right(std::string_view str, std::string_view chars) noexcept
56+
{
5257
auto last = str.find_last_not_of(chars);
53-
return str.substr(first, last - first + 1);
58+
if (last == std::string_view::npos) {
59+
return {};
60+
}
61+
62+
return str.substr(0, last + 1);
63+
}
64+
65+
std::string_view trim(std::string_view str, std::string_view chars) noexcept
66+
{
67+
return trim_right(trim_left(str, chars), chars);
5468
}
5569

5670
std::vector<std::string_view> split(std::string_view str,

libs/common/src/linglong/common/strings.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ inline splitOption operator&(splitOption a, splitOption b)
3232

3333
bool stringEqual(std::string_view str1, std::string_view str2, bool caseSensitive = false) noexcept;
3434

35+
std::string_view trim_left(std::string_view str, std::string_view chars = " ") noexcept;
36+
37+
std::string_view trim_right(std::string_view str, std::string_view chars = " ") noexcept;
38+
3539
std::string_view trim(std::string_view str, std::string_view chars = " ") noexcept;
3640

3741
std::vector<std::string_view> split(std::string_view str,

libs/linglong/tests/ll-tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ pfl_add_executable(
5858
src/linglong/utils/error/error_test.cpp
5959
src/linglong/utils/file_test.cpp
6060
src/linglong/utils/filelock_test.cpp
61+
src/linglong/utils/hooks_test.cpp
6162
src/linglong/utils/log.cpp
6263
src/linglong/utils/namespce.cpp
6364
src/linglong/utils/overlayfs_test.cpp

libs/linglong/tests/ll-tests/src/linglong/common/strings_test.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,26 @@ TEST(StringsTest, Trim)
3737
EXPECT_EQ(trim(" hello ", "."), " hello ");
3838
}
3939

40+
TEST(StringsTest, TrimLeft)
41+
{
42+
EXPECT_EQ(trim_left(" hello "), "hello ");
43+
EXPECT_EQ(trim_left("hello "), "hello ");
44+
EXPECT_EQ(trim_left(" "), "");
45+
EXPECT_EQ(trim_left("", " "), "");
46+
EXPECT_EQ(trim_left("...hello...", "."), "hello...");
47+
EXPECT_EQ(trim_left("\t hello", " \t"), "hello");
48+
}
49+
50+
TEST(StringsTest, TrimRight)
51+
{
52+
EXPECT_EQ(trim_right(" hello "), " hello");
53+
EXPECT_EQ(trim_right(" hello"), " hello");
54+
EXPECT_EQ(trim_right(" "), "");
55+
EXPECT_EQ(trim_right("", " "), "");
56+
EXPECT_EQ(trim_right("...hello...", "."), "...hello");
57+
EXPECT_EQ(trim_right("hello \t", " \t"), "hello");
58+
}
59+
4060
TEST(StringsTest, Split)
4161
{
4262
EXPECT_EQ(split("a,b,c", ',', splitOption::None),
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd.
2+
//
3+
// SPDX-License-Identifier: LGPL-3.0-or-later
4+
5+
#include <gtest/gtest.h>
6+
7+
#include "linglong/utils/hooks.h"
8+
9+
namespace linglong::utils::details {
10+
11+
TEST(InstallHooks, ParseUnquotedCommand)
12+
{
13+
auto command = parseInstallHookCommandLine("ll-pre-install=echo ok ", "ll-pre-install=");
14+
15+
ASSERT_TRUE(command.has_value()) << command.error().message();
16+
ASSERT_TRUE(command->has_value());
17+
EXPECT_EQ(**command, "echo ok ");
18+
}
19+
20+
TEST(InstallHooks, ParseDoubleQuotedCommand)
21+
{
22+
auto command = parseInstallHookCommandLine(R"(ll-post-install="echo ok")", "ll-post-install=");
23+
24+
ASSERT_TRUE(command.has_value()) << command.error().message();
25+
ASSERT_TRUE(command->has_value());
26+
EXPECT_EQ(**command, "echo ok");
27+
}
28+
29+
TEST(InstallHooks, ParseSingleQuotedCommand)
30+
{
31+
auto command = parseInstallHookCommandLine("ll-post-uninstall='echo ok'", "ll-post-uninstall=");
32+
33+
ASSERT_TRUE(command.has_value()) << command.error().message();
34+
ASSERT_TRUE(command->has_value());
35+
EXPECT_EQ(**command, "echo ok");
36+
}
37+
38+
TEST(InstallHooks, KeepInnerQuotesAndBackslashes)
39+
{
40+
auto command =
41+
parseInstallHookCommandLine(R"(ll-post-install="echo hello\")", "ll-post-install=");
42+
43+
ASSERT_TRUE(command.has_value()) << command.error().message();
44+
ASSERT_TRUE(command->has_value());
45+
EXPECT_EQ(**command, R"(echo hello\)");
46+
}
47+
48+
TEST(InstallHooks, KeepMixedInnerQuotes)
49+
{
50+
auto command =
51+
parseInstallHookCommandLine(R"(ll-post-install="printf '%s\n' \"ok\"")", "ll-post-install=");
52+
53+
ASSERT_TRUE(command.has_value()) << command.error().message();
54+
ASSERT_TRUE(command->has_value());
55+
EXPECT_EQ(**command, R"(printf '%s\n' \"ok\")");
56+
}
57+
58+
TEST(InstallHooks, KeepSemicolonInsideQuotedCommand)
59+
{
60+
auto command =
61+
parseInstallHookCommandLine(R"( ll-post-install="echo ok;" )", "ll-post-install=");
62+
63+
ASSERT_TRUE(command.has_value()) << command.error().message();
64+
ASSERT_TRUE(command->has_value());
65+
EXPECT_EQ(**command, "echo ok;");
66+
}
67+
68+
TEST(InstallHooks, RejectTrailingSemicolonAfterOuterQuote)
69+
{
70+
auto command = parseInstallHookCommandLine(R"(ll-post-install="echo ok";)", "ll-post-install=");
71+
72+
EXPECT_FALSE(command.has_value());
73+
}
74+
75+
TEST(InstallHooks, IgnoreLineWithoutPrefixAtStart)
76+
{
77+
auto command =
78+
parseInstallHookCommandLine(R"(# ll-post-install="echo ok")", "ll-post-install=");
79+
80+
ASSERT_TRUE(command.has_value()) << command.error().message();
81+
EXPECT_FALSE(command->has_value());
82+
}
83+
84+
TEST(InstallHooks, RejectUnterminatedOuterQuote)
85+
{
86+
auto command = parseInstallHookCommandLine(R"(ll-post-install="echo ok)", "ll-post-install=");
87+
88+
EXPECT_FALSE(command.has_value());
89+
}
90+
91+
} // namespace linglong::utils::details

libs/utils/src/linglong/utils/hooks.cpp

Lines changed: 64 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include "cmd.h"
1010
#include "configure.h"
1111
#include "linglong/common/error.h"
12+
#include "linglong/common/strings.h"
1213
#include "linglong/utils/error/error.h"
1314
#include "linglong/utils/log/log.h"
1415

@@ -19,7 +20,10 @@
1920
#include <cstring>
2021
#include <filesystem>
2122
#include <fstream>
23+
#include <optional>
2224
#include <sstream>
25+
#include <string_view>
26+
#include <utility>
2327
#include <vector>
2428

2529
#include <sys/wait.h>
@@ -34,6 +38,43 @@ constexpr std::string_view POST_UNINSTALL_ACTION_PREFIX = "ll-post-uninstall=";
3438

3539
using CommandList = const std::vector<std::string> &;
3640

41+
namespace {
42+
43+
constexpr std::string_view WHITESPACE_CHARS = " \t\n\r\f\v";
44+
45+
} // namespace
46+
47+
utils::error::Result<std::optional<std::string>>
48+
details::parseInstallHookCommandLine(std::string_view line, std::string_view prefix)
49+
{
50+
LINGLONG_TRACE("Parsing install hook command line");
51+
52+
line = common::strings::trim_left(line, WHITESPACE_CHARS);
53+
if (!common::strings::starts_with(line, prefix)) {
54+
return std::optional<std::string>{};
55+
}
56+
57+
auto command = common::strings::trim_left(line.substr(prefix.size()), WHITESPACE_CHARS);
58+
if (command.empty()) {
59+
return std::optional<std::string>{ std::string{} };
60+
}
61+
62+
const auto quote = command.front();
63+
if (quote != '"' && quote != '\'') {
64+
return std::optional<std::string>{ std::string(command) };
65+
}
66+
67+
command.remove_prefix(1);
68+
69+
auto suffix = common::strings::trim_right(command, WHITESPACE_CHARS);
70+
if (suffix.empty() || suffix.back() != quote) {
71+
return LINGLONG_ERR("Invalid install hook command: unterminated quoted command");
72+
}
73+
74+
suffix.remove_suffix(1);
75+
return std::optional<std::string>{ std::string(suffix) };
76+
}
77+
3778
utils::error::Result<void> executeHookCommands(
3879
CommandList commands, const std::vector<std::pair<std::string, std::string>> &envVars) noexcept
3980
{
@@ -79,26 +120,31 @@ utils::error::Result<void> InstallHookManager::parseInstallHooks()
79120
return LINGLONG_ERR(fmt::format("Couldn't open file: {}", entry.path()));
80121
}
81122

123+
const std::array<std::pair<std::string_view, std::vector<std::string> *>, 3> hookRules = {
124+
{ { PRE_INSTALL_ACTION_PREFIX, &preInstallCommands },
125+
{ POST_INSTALL_ACTION_PREFIX, &postInstallCommands },
126+
{ POST_UNINSTALL_ACTION_PREFIX, &postUninstallCommands } }
127+
};
128+
82129
std::string line;
130+
std::size_t lineNumber = 0;
83131
while (std::getline(file, line)) {
84-
std::size_t pos = line.find(PRE_INSTALL_ACTION_PREFIX);
85-
if (pos != std::string::npos) {
86-
preInstallCommands.emplace_back(
87-
line.substr(pos + PRE_INSTALL_ACTION_PREFIX.length()));
88-
break;
89-
}
90-
91-
pos = line.find(POST_INSTALL_ACTION_PREFIX);
92-
if (pos != std::string::npos) {
93-
postInstallCommands.emplace_back(
94-
line.substr(pos + POST_INSTALL_ACTION_PREFIX.length()));
95-
break;
96-
}
97-
98-
pos = line.find(POST_UNINSTALL_ACTION_PREFIX);
99-
if (pos != std::string::npos) {
100-
postUninstallCommands.emplace_back(
101-
line.substr(pos + POST_UNINSTALL_ACTION_PREFIX.length()));
132+
++lineNumber;
133+
134+
for (auto [prefix, commands] : hookRules) {
135+
auto command = details::parseInstallHookCommandLine(line, prefix);
136+
if (!command.has_value()) {
137+
return LINGLONG_ERR(fmt::format("Invalid install hook command in {}:{}",
138+
entry.path().string(),
139+
lineNumber),
140+
command);
141+
}
142+
143+
if (!command->has_value()) {
144+
continue;
145+
}
146+
147+
commands->emplace_back(std::move(**command));
102148
break;
103149
}
104150
}

libs/utils/src/linglong/utils/hooks.h

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd.
2+
* SPDX-FileCopyrightText: 2025 - 2026 UnionTech Software Technology Co., Ltd.
33
*
44
* SPDX-License-Identifier: LGPL-3.0-or-later
55
*/
@@ -8,11 +8,20 @@
88

99
#include "linglong/utils/error/error.h"
1010

11+
#include <optional>
1112
#include <string>
13+
#include <string_view>
1214
#include <vector>
1315

1416
namespace linglong::utils {
1517

18+
namespace details {
19+
20+
utils::error::Result<std::optional<std::string>>
21+
parseInstallHookCommandLine(std::string_view line, std::string_view prefix);
22+
23+
} // namespace details
24+
1625
class InstallHookManager final
1726
{
1827
public:

0 commit comments

Comments
 (0)