Skip to content

Commit e1dbc79

Browse files
committed
fix(xlings): the env list separator is the platform's, not a literal ':'
Windows CI caught this; no amount of reading would have. resolve_env joined and split its lists on ':', which on Windows is both the wrong separator (it is ';') and a character that appears INSIDE every absolute path. So de-duplication split "C:\x" into "C" and "\x", matched nothing, and the joined value came back as "C:\...\x:C:\...\x" -- a list that grows on every nested invocation and that no consumer can parse. This repository has made the same mistake before, in the other direction: find_first_of(";:") over a Windows PATH cuts at the drive-letter colon. The three test expectations that failed were also wrong, but differently, and the difference matters: they compared against a path JOIN while the code does a literal substitution. The literal one is correct -- the separator inside a declaration belongs to the subos manifest, and rewriting it to the host's spelling would be editing a value we do not own. The assertions now say so, and the dedup test additionally asserts the result contains NO separator at all, which is the property that actually failed.
1 parent c84ee05 commit e1dbc79

2 files changed

Lines changed: 37 additions & 17 deletions

File tree

src/xlings/subos_info.cppm

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export module mcpp.xlings.subos_info;
3737

3838
import std;
3939
import mcpp.libs.json;
40+
import mcpp.platform;
4041

4142
export namespace mcpp::xlings::subos {
4243

@@ -177,10 +178,17 @@ Info read(const std::filesystem::path& subosDir) {
177178
// Resolve the declarations into concrete (var, value) pairs with
178179
// `${subosdir}` expanded.
179180
//
180-
// `prepend` joins with ':' in provider order and de-duplicates; `set`
181-
// replaces. That is xlings's own precedence, and the de-duplication matters
182-
// because these variables are inherited: without it a nested invocation
183-
// grows the list every time.
181+
// `prepend` joins in provider order and de-duplicates; `set` replaces. That
182+
// is xlings's own precedence, and the de-duplication matters because these
183+
// variables are inherited: without it a nested invocation grows the list
184+
// every time.
185+
//
186+
// The separator is the PLATFORM's, never a literal ':'. Hardcoding one is a
187+
// mistake this repository has made before and it is not cosmetic: on Windows
188+
// the list separator is ';' and ':' appears INSIDE every absolute path, so a
189+
// ':'-keyed split cuts "C:\\x" into "C" and "\\x" -- the de-duplication then
190+
// never matches and the joined value is a corrupt list. Caught by CI on
191+
// Windows, not by any amount of reading.
184192
std::vector<std::pair<std::string, std::string>>
185193
resolve_env(const Info& info, const std::filesystem::path& subosDir) {
186194
std::vector<std::pair<std::string, std::string>> out;
@@ -194,17 +202,19 @@ resolve_env(const Info& info, const std::filesystem::path& subosDir) {
194202
return v;
195203
};
196204

197-
// A colon-separated list already contains `value` as a whole element?
198-
// Compared element-wise rather than by substring: a plain `find` would
199-
// consider "/a/bc" already present in "/a/bcd".
200-
auto contains_element = [](std::string_view list, std::string_view value) {
205+
const std::string sep = mcpp::platform::env::path_list_separator();
206+
207+
// Does the list already contain `value` as a WHOLE element? Compared
208+
// element-wise rather than by substring: a plain `find` would consider
209+
// "/a/bc" already present in "/a/bcd".
210+
auto contains_element = [&](std::string_view list, std::string_view value) {
201211
for (std::size_t i = 0; i <= list.size();) {
202-
auto end = list.find(':', i);
212+
auto end = list.find(sep, i);
203213
auto piece = list.substr(i, end == std::string_view::npos
204214
? std::string_view::npos : end - i);
205215
if (piece == value) return true;
206216
if (end == std::string_view::npos) break;
207-
i = end + 1;
217+
i = end + sep.size();
208218
}
209219
return false;
210220
};
@@ -217,7 +227,7 @@ resolve_env(const Info& info, const std::filesystem::path& subosDir) {
217227
if (hit == out.end()) { out.emplace_back(d.var, value); continue; }
218228
if (d.op == "set") { hit->second = value; continue; }
219229
if (!contains_element(hit->second, value))
220-
hit->second = value + ":" + hit->second;
230+
hit->second = value + sep + hit->second;
221231
}
222232
}
223233
return out;

tests/unit/test_subos_info.cpp

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <gtest/gtest.h>
1111

1212
import std;
13+
import mcpp.platform;
1314
import mcpp.xlings.subos_info;
1415

1516
namespace su = mcpp::xlings::subos;
@@ -68,7 +69,11 @@ TEST(SubosInfo, ResolvesSubosdirPlaceholder) {
6869
auto env = su::resolve_env(su::read(t.dir), t.dir);
6970
ASSERT_EQ(env.size(), 1u);
7071
EXPECT_EQ(env[0].first, "LIBGL_DRIVERS_PATH");
71-
EXPECT_EQ(env[0].second, (t.dir / "usr" / "lib" / "dri").string());
72+
// Literal concatenation, NOT a path join. The separator in the declaration
73+
// belongs to the subos manifest and is substituted verbatim; turning it
74+
// into the host's would rewrite a value we do not own. On Windows the two
75+
// spellings differ and the path-join form is the wrong expectation.
76+
EXPECT_EQ(env[0].second, t.dir.string() + "/usr/lib/dri");
7277
}
7378

7479
// Several providers may contribute to one variable — that is the normal
@@ -83,10 +88,10 @@ TEST(SubosInfo, PrependJoinsProvidersInOrder) {
8388
{"var":"V","op":"prepend","value":"${subosdir}/two"}]}]}})");
8489
auto env = su::resolve_env(su::read(t.dir), t.dir);
8590
ASSERT_EQ(env.size(), 1u);
86-
auto one = (t.dir / "one").string();
87-
auto two = (t.dir / "two").string();
88-
EXPECT_NE(env[0].second.find(one), std::string::npos);
89-
EXPECT_NE(env[0].second.find(two), std::string::npos);
91+
const auto sep = mcpp::platform::env::path_list_separator();
92+
EXPECT_EQ(env[0].second,
93+
t.dir.string() + "/two" + sep + t.dir.string() + "/one")
94+
<< "both providers must survive, later binding front-most";
9095
}
9196

9297
// The same value arriving twice must not accumulate: nested invocations
@@ -98,7 +103,12 @@ TEST(SubosInfo, PrependDeduplicates) {
98103
{"binding":"b@1","decls":[{"var":"V","op":"prepend","value":"${subosdir}/x"}]}]}})");
99104
auto env = su::resolve_env(su::read(t.dir), t.dir);
100105
ASSERT_EQ(env.size(), 1u);
101-
EXPECT_EQ(env[0].second, (t.dir / "x").string());
106+
// One entry, not two. The de-duplication has to split on the PLATFORM's
107+
// list separator: keyed on ':' it would cut "C:\\x" apart on Windows,
108+
// match nothing, and grow the list on every nested invocation.
109+
EXPECT_EQ(env[0].second, t.dir.string() + "/x");
110+
EXPECT_EQ(env[0].second.find(mcpp::platform::env::path_list_separator()),
111+
std::string::npos);
102112
}
103113

104114
// `set` replaces rather than joins — xlings's own precedence.

0 commit comments

Comments
 (0)