Skip to content

Commit 85a3c68

Browse files
committed
Two contributors naming one file: the closer one decides it, and one destination has one claimant
Review of the 0.12.0 branch before merge found the graph-contribution merge inconsistent with the precedence its own documentation states. `contributions` is ordered highest priority first: the application's own libraries, then the graph's packages requesters first. The resources merge walks it backwards, so a higher-priority contributor's resource overrides a lower one's through `aapt2 -R`. The assets merge walked it forwards into `collect_tree`, which copies with `overwrite_existing`, so the LAST write won and the deepest dependency decided a file two packages name. Measured on the fixture below: `assets/graph-asset.txt` held `from-the-deeper-library` where the documented order gives `from-the-requester`. The walk is now backwards, for the same reason the resources walk is. `lib/<abi>/` is flat, so the application's own library, an archive's native library of the same name, and a second archive's copy of it all address one file. Each went through `place_library`, which emits a step whose id and output are derived from the ABI and the leaf name: two claimants produced two steps with one id writing one path. One destination now has one claimant, the first in the priority order, and a later claim is reported by name rather than written. `mcpp::plugins::json` decoded each `\u` escape on its own, so a code point above U+FFFF -- which reaches JSON as a surrogate PAIR -- became two three-byte sequences holding unpaired surrogates: not UTF-8, and silently wrong in whatever `AndroidManifest.xml` or `Info.plist` the value was written into. Pairs are now combined, and a surrogate that is not half of one is refused. mcpp's own writer escapes only characters below 0x20 and passes UTF-8 through, so no producer reaches this path today, which is why nothing reported it; the reader is shared by `dist-apk` and `dist-apple` and should not corrupt what it cannot represent. Criterion: `tests/apk-consumer-graph` gains `lib2/`, which `lib/` depends on, so the two contribute one asset name at two distances. Leg (6) reads the packed `assets/graph-asset.txt`. With the walk reverted it fails with `from-the-deeper-library`; with it in place the five legs pass on 2026.9.16.1.
1 parent b2d5fe0 commit 85a3c68

8 files changed

Lines changed: 118 additions & 18 deletions

File tree

dist/apk.cppm

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2090,7 +2090,25 @@ inline plan plan_for(options opt = {}) {
20902090
// an archive's native libraries -- and to every library under an older
20912091
// engine, which decides nothing.
20922092
const bool engineDecided = engineStrip && *engineStrip;
2093-
const auto place_library = [&](const std::string& so, const std::string& abi, bool staged) {
2093+
// ONE DESTINATION HAS ONE CLAIMANT, AND THE FIRST CLAIM WINS. `lib/<abi>/`
2094+
// is flat, so the application's own library and an archive's native library
2095+
// of the same name address one file; so do two archives that carry it. Both
2096+
// loops below walk highest priority first -- the application's staged
2097+
// libraries, then `contributions`, which is ordered application first and
2098+
// each package above the packages it depends on -- so the first claim is the
2099+
// one the priority order names, and a later one is reported rather than
2100+
// written. Writing it would place two steps with one id and one output.
2101+
std::map<std::string, std::string> claimed; // "<abi>/<leaf>" -> claimant
2102+
const auto place_library = [&](const std::string& so, const std::string& abi,
2103+
bool staged, std::string_view claimant) {
2104+
const std::string leafKey = abi + "/" + fs::path(so).filename().string();
2105+
if (auto [it, fresh] = claimed.try_emplace(leafKey, std::string(claimant)); !fresh) {
2106+
mcpp::warning(std::format(
2107+
"mcpp.dist.apk: {} and {} both carry lib/{}; {} is packed, because it comes "
2108+
"first in the priority order, and {} is left out.",
2109+
it->second, claimant, leafKey, it->second, claimant).c_str());
2110+
return;
2111+
}
20942112
const fs::path dst = work / "lib" / abi / fs::path(so).filename();
20952113
if (llvmStrip.empty() || (staged && engineDecided)) {
20962114
collect_tree(so, dst, libInputs);
@@ -2144,7 +2162,7 @@ inline plan plan_for(options opt = {}) {
21442162
};
21452163
for (auto const& leg : legs)
21462164
for (auto const& so : leg.libraries)
2147-
place_library(so, leg.abi, /*staged=*/true);
2165+
place_library(so, leg.abi, /*staged=*/true, "the application");
21482166

21492167
// An AAR's native libraries, for every ABI this package carries.
21502168
for (auto const& c : contributions) {
@@ -2158,16 +2176,20 @@ inline plan plan_for(options opt = {}) {
21582176
continue;
21592177
}
21602178
for (auto const& so : shared_objects_in(abiDir))
2161-
place_library(so, leg.abi, /*staged=*/false);
2179+
place_library(so, leg.abi, /*staged=*/false, c.label);
21622180
}
21632181
}
21642182

21652183
const fs::path assetsDir = work / "assets";
21662184
std::vector<std::string> assetInputs;
21672185
// Library and archive assets first, so a file the build program deploys
2168-
// under the same name replaces theirs.
2169-
for (auto const& c : contributions)
2170-
if (!c.assets.empty()) collect_tree(c.assets, assetsDir, assetInputs);
2186+
// under the same name replaces theirs. `collect_tree` overwrites, so the
2187+
// last write wins; `contributions` is highest priority first, and is walked
2188+
// backwards here for the same reason the resources below are -- a package
2189+
// above the packages it depends on decides the file they share.
2190+
for (std::size_t k = contributions.size(); k-- > 0;)
2191+
if (!contributions[k].assets.empty())
2192+
collect_tree(contributions[k].assets, assetsDir, assetInputs);
21712193
{ // every deploy'd file, `<stage>/bin/<rel>` -> `assets/<rel>`: the engine
21722194
// stages `mcpp::deploy`'s destinations under `bin/` on this row as on
21732195
// every other.

src/plugins.cppm

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,8 @@ inline void set_attr(node& n, const std::string& key, const std::string& value)
269269
// resolution report, and `mcpp::plugins::graph` below reads the engine's graph
270270
// document for `dist-apk` and `dist-apple`. A second parser for the second
271271
// document is how two readers come to disagree about one file, so there is one.
272-
// It reads objects, arrays, strings (with `\u` escapes below U+10000), numbers,
272+
// It reads objects, arrays, strings (with `\u` escapes, surrogate pairs
273+
// included, and a refusal for a surrogate that is not part of one), numbers,
273274
// `true`, `false` and `null`; a number keeps its spelling in `text`.
274275
export namespace mcpp::plugins::json {
275276

@@ -293,6 +294,20 @@ struct reader {
293294
void skip_space() {
294295
while (i < s.size() && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r')) ++i;
295296
}
297+
// The four hexadecimal digits of one `\u` escape, `i` already past the `u`.
298+
bool hex4(unsigned& cp) {
299+
if (i + 4 > s.size()) return false;
300+
cp = 0;
301+
for (int k = 0; k < 4; ++k) {
302+
const char h = s[i++];
303+
cp <<= 4;
304+
if (h >= '0' && h <= '9') cp |= static_cast<unsigned>(h - '0');
305+
else if (h >= 'a' && h <= 'f') cp |= static_cast<unsigned>(h - 'a' + 10);
306+
else if (h >= 'A' && h <= 'F') cp |= static_cast<unsigned>(h - 'A' + 10);
307+
else return false;
308+
}
309+
return true;
310+
}
296311
bool string(std::string& out) {
297312
if (i >= s.size() || s[i] != '"') return false;
298313
++i;
@@ -307,19 +322,35 @@ struct reader {
307322
case 'b': out += '\b'; break;
308323
case 'f': out += '\f'; break;
309324
case 'u': {
310-
if (i + 4 > s.size()) return false;
325+
// A code point above U+FFFF reaches JSON as a surrogate
326+
// PAIR, which is two `\u` escapes. Encoding each half on its
327+
// own produces two three-byte sequences holding unpaired
328+
// surrogates -- not UTF-8, and accepted by nothing that
329+
// reads the manifest this value is written into. The pair is
330+
// combined here, and a surrogate that is not part of one is
331+
// refused, so a malformed document is a refusal rather than
332+
// a silently corrupted string.
311333
unsigned cp = 0;
312-
for (int k = 0; k < 4; ++k) {
313-
const char h = s[i++];
314-
cp <<= 4;
315-
if (h >= '0' && h <= '9') cp |= static_cast<unsigned>(h - '0');
316-
else if (h >= 'a' && h <= 'f') cp |= static_cast<unsigned>(h - 'a' + 10);
317-
else if (h >= 'A' && h <= 'F') cp |= static_cast<unsigned>(h - 'A' + 10);
318-
else return false;
334+
if (!hex4(cp)) return false;
335+
if (cp >= 0xd800 && cp <= 0xdbff) {
336+
if (i + 6 > s.size() || s[i] != '\\' || s[i + 1] != 'u') return false;
337+
i += 2;
338+
unsigned lo = 0;
339+
if (!hex4(lo)) return false;
340+
if (lo < 0xdc00 || lo > 0xdfff) return false;
341+
cp = 0x10000 + ((cp - 0xd800) << 10) + (lo - 0xdc00);
342+
} else if (cp >= 0xdc00 && cp <= 0xdfff) {
343+
return false; // a low surrogate with no high half
319344
}
320345
if (cp < 0x80) out += static_cast<char>(cp);
321346
else if (cp < 0x800) { out += static_cast<char>(0xc0 | (cp >> 6)); out += static_cast<char>(0x80 | (cp & 0x3f)); }
322-
else { out += static_cast<char>(0xe0 | (cp >> 12)); out += static_cast<char>(0x80 | ((cp >> 6) & 0x3f)); out += static_cast<char>(0x80 | (cp & 0x3f)); }
347+
else if (cp < 0x10000) { out += static_cast<char>(0xe0 | (cp >> 12)); out += static_cast<char>(0x80 | ((cp >> 6) & 0x3f)); out += static_cast<char>(0x80 | (cp & 0x3f)); }
348+
else {
349+
out += static_cast<char>(0xf0 | (cp >> 18));
350+
out += static_cast<char>(0x80 | ((cp >> 12) & 0x3f));
351+
out += static_cast<char>(0x80 | ((cp >> 6) & 0x3f));
352+
out += static_cast<char>(0x80 | (cp & 0x3f));
353+
}
323354
break;
324355
}
325356
default: out += e;

tests/apk-consumer-graph/check-apk-graph.sh

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
# 1. the library's string resource is in the APK with the library's value;
99
# 2. `options::graph_libraries = false` packs no contribution;
1010
# 3. an application library defining the same resource wins it;
11-
# 4. a malformed contribution is refused, naming the package and the key.
11+
# 4. a malformed contribution is refused, naming the package and the key;
12+
# 6. of two contributors naming one asset, the one closer to the
13+
# application decides it -- `lib/` over the `lib2/` it depends on.
1214
# an older engine (no graph):
1315
# 5. the pack succeeds, and the APK carries no contribution.
1416
#
@@ -81,7 +83,7 @@ echo "== (4) a malformed contribution is refused, naming the package and the key
8183
COPY="../.apk-consumer-graph-malformed"
8284
rm -rf "$COPY"
8385
mkdir -p "$COPY"
84-
cp -r mcpp.toml build.mcpp src app-res lib "$COPY"/
86+
cp -r mcpp.toml build.mcpp src app-res lib lib2 "$COPY"/
8587
sed -i.bak 's/^resources = "res"$/resources = 3/' "$COPY/lib/mcpp.toml"
8688
grep -q '^resources = 3$' "$COPY/lib/mcpp.toml" || fail "the malformed copy was not written" "$COPY/lib/mcpp.toml"
8789
if ( cd "$COPY" && "$MCPP" pack --format apk --target "$TARGET" > refuse.log 2>&1 ); then
@@ -92,5 +94,19 @@ grep -q 'mcpp.apk-graph-lib@0.1.0 states `resources` as something other than a p
9294
rm -rf "$COPY"
9395
echo "ok: the refusal names mcpp.apk-graph-lib@0.1.0 and resources"
9496

97+
echo "== (6) of two contributors naming one asset, the closer one decides it =="
98+
# `contributions` is highest priority first, and `collect_tree` overwrites, so a
99+
# forward walk would let the DEEPEST contributor win. lib/ requests lib2/, so
100+
# lib/ is closer to the application and its asset is the one that must survive.
101+
apk=$(pack graph-6.log)
102+
rm -rf graph-assets && mkdir -p graph-assets
103+
unzip -q -o "$apk" "assets/graph-asset.txt" -d graph-assets \
104+
|| fail "assets/graph-asset.txt is not in the APK" graph-6.log
105+
value=$(tr -d '\r\n' < graph-assets/assets/graph-asset.txt)
106+
[ "$value" = "from-the-requester" ] \
107+
|| fail "the deeper library won an asset the closer one names (got: '${value}')" graph-6.log
108+
rm -rf graph-assets
109+
echo "ok: assets/graph-asset.txt = from-the-requester"
110+
95111
rm -rf target graph-*.log
96112
echo "PASS: dist-apk collects library contributions from the resolved graph ($engine)"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from-the-requester

tests/apk-consumer-graph/lib/mcpp.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,14 @@ authors = ["mcpp-community"]
1111
[package.metadata.dist-apk]
1212
package = "org.mcpp.apkgraph.lib"
1313
resources = "res"
14+
assets = "assets"
1415

1516
[language]
1617
standard = "c++23"
1718

19+
[dependencies]
20+
mcpp.apk-graph-lib2 = { path = "../lib2" }
21+
1822
[targets.apk-graph-lib]
1923
kind = "lib"
2024

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from-the-deeper-library
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# The deeper library of `tests/apk-consumer-graph`: `lib/` depends on it, so it
2+
# is further from the application than `lib/` is. Both contribute an asset of
3+
# the same name, which is how the precedence between two contributors is
4+
# measured: the one closer to the application decides the file they share.
5+
[package]
6+
name = "apk-graph-lib2"
7+
namespace = "mcpp"
8+
version = "0.1.0"
9+
description = "Fixture: the deeper of two libraries contributing an asset of one name"
10+
license = "Apache-2.0"
11+
authors = ["mcpp-community"]
12+
13+
[package.metadata.dist-apk]
14+
package = "org.mcpp.apkgraph.lib2"
15+
assets = "assets"
16+
17+
[language]
18+
standard = "c++23"
19+
20+
[targets.apk-graph-lib2]
21+
kind = "lib"
22+
23+
[build]
24+
sources = ["src/lib2.cpp"]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
int apk_graph_lib2_answer() { return 43; }

0 commit comments

Comments
 (0)