Skip to content

Commit 5b48c3d

Browse files
committed
refactor(gh60): C1d ReachabilityEnricher visitor
Introduce the stateless per-node visitor that the JSON writer will consult instead of reading reachability state directly. Wires the visitor into the inline writer's reachability + components paths now so that C1e's purity-of-writer extraction only needs to move the emit code without rerouting the data dependencies. New class: - presto.android.gui.clients.reach.ReachabilityEnricher - 4 final instance fields (index, manifestPackage, codePackage, mainActivity); no mutable state, no caches. - enrichMethod(SootMethod) returns a fresh 3-key LinkedHashMap with {reachable, reachesMop, directlyReachesMop}. Keys preserve the gh57 names; Group 6 (C1f) renames atomically together with the JSON schema and Pydantic consumer. - enrichWidget / enrichTransition / enrichComponent return Collections.emptyMap() — placeholders for C3 (gh<N+2>-agent-enrichment) which will emit per-listener handlerReachesTarget / externalExit / exitKind without touching the writer. - topLevelMetadata returns {manifestPackage, codePackage, mainActivity}. codePackage included now so C2 G11 dual-package emission lands without modifying the writer. - targetSignatures / directTargetSignatures delegate to the index with stable object identity (no per-call recomputation in the writer's hot path). RvsecAnalysisClient.run() now constructs the enricher after the engine and threads it through writeJson → writeReachability / writeComponents → writeComponentEntry / writeProviderEntry. The legacy Set<SootMethod> reachableSet / reachesMopSet / directMopSet parameters are removed from those helpers — every per-method reachability lookup routes through the enricher. This is the precondition C1e's INV-ANA-30 (writer purity) gate enforces; the diff to move the emit code into JsonReportWriter is now mechanical. Tests: - ReachabilityEnricherTest (10/10 pass) — null rejection; enrichMethod key shape + values; idempotence; topLevelMetadata insertion order; null-metadata → empty-string coercion; placeholder enrich* returns; signature-set identity. - ReachabilityEnricherMemoryTest (2/2 pass) — structural: every declared field is final (catches future cache regressions); behavioral: 10 000 enrichMethod calls do not mutate the index, each returns a fresh map, signature-set identity stable. Full client suite: 119 / 119 passed (96 pre-existing + 11 from C1c + 12 new from C1d). BaselineComparisonIT post-C1d: **8/10 pass** — identical signature to HEAD (same 2 pre-existing app-class-count drift failures, same 3 MOP reachability tests passing). Zero new regressions from the enricher wiring. tasks.md: Group 4 (4.1-4.6) checked off with per-task evidence. refs #60
1 parent 7ab5202 commit 5b48c3d

5 files changed

Lines changed: 417 additions & 38 deletions

File tree

rv-android/openspec/changes/gh60-targets-core/tasks.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,12 @@
6363

6464
## 4. C1d — `ReachabilityEnricher` (visitor, no batch `ReportModel` — D3 revision 2)
6565

66-
- [ ] 4.1 Add `ReachabilityEnricher.java` in `rvsec-gator/client/src/main/java/presto/android/gui/clients/reach/`: stateless visitor with constructor `(ReachabilityIndex index, String manifestPackage, String codePackage, String mainActivity)` and methods `enrichMethod(SootMethod) → Map<String,Object>`, `enrichWidget(Widget) → Map`, `enrichTransition(Transition) → Map`, `enrichComponent(Component) → Map`, `topLevelMetadata() → Map`, `targetSignatures() → Set<String>`. Each `enrich*` performs the `ReachabilityIndex` lookups and returns the key/value pairs the writer will emit for that node.
67-
- [ ] 4.2 Update `RvsecAnalysisClient.run()` to construct the enricher after `ReachabilityEngine.run()` completes; pass it to Group 5's new `JsonReportWriter`. Inline JSON writer remains in place until Group 5 lands; for now, the existing writer is refactored to delegate per-node lookups to the new enricher (no batch POJO).
68-
- [ ] 4.3 Add `ReachabilityEnricherTest.java`: assert `enrichMethod(m).get("reachesTarget") == index.reachesTarget(m)` for synthetic methods; assert idempotence (same input → same output, no internal mutation); assert `topLevelMetadata()` returns exactly `{manifestPackage, codePackage, mainActivity}` keys.
69-
- [ ] 4.4 Add `ReachabilityEnricherMemoryTest.java`: invoke enricher 10k times against a mock index; assert heap delta is bounded (no internal accumulation) — guards against accidental batch caching regressions.
70-
- [ ] 4.5 Run on 5-APK canonical fixture; `G_paridade_reachability` zero
71-
- [ ] 4.6 Commit `refactor(gh60): C1d ReachabilityEnricher visitor (refs #60)`
66+
- [x] 4.1 `ReachabilityEnricher` in `reach/`: stateless visitor; all four instance fields `final`; `enrichMethod(SootMethod)` returns a fresh `LinkedHashMap` with the three reachability flags (`reachable`/`reachesMop`/`directlyReachesMop` — keys preserve the gh57 names; Group 6 renames atomically); `enrichWidget`/`enrichTransition`/`enrichComponent` return `Collections.emptyMap()` (C3 will fill these in without touching the writer); `topLevelMetadata` returns `{manifestPackage, codePackage, mainActivity}` (codePackage included as the surface for C2 G11 dual-package emission); `targetSignatures()` and `directTargetSignatures()` delegate to `ReachabilityIndex` with stable object identity (no per-call allocation in the writer's hot path).
67+
- [x] 4.2 `RvsecAnalysisClient.run()` constructs the enricher post-engine and threads it through `writeJson` / `writeReachability` / `writeComponents` / `writeComponentEntry` / `writeProviderEntry`. The per-method reachability reads in these helpers now route through `enricher.enrichMethod(m).get("reachesMop")` instead of direct `reachesMopSet.contains(m)`. The legacy `Set<SootMethod> reachesMopSet/directMopSet/reachableSet` parameters are removed from those signatures — the only remaining `Set<SootMethod>` reads of reachability state in `RvsecAnalysisClient` are gone, which is exactly the precondition C1e (writer purity, INV-ANA-30) needs.
68+
- [x] 4.3 `ReachabilityEnricherTest.java` (10 cases): null index rejection; `enrichMethod` returns 3 keys with values mirroring the empty index (all `Boolean.FALSE`); idempotence (back-to-back calls equal-by-value, distinct map identities so consumer mutation does not leak); `topLevelMetadata` returns exactly `{manifestPackage, codePackage, mainActivity}` in insertion order; null metadata coerces to empty string; widget/transition/component enrichments return empty maps; `targetSignatures`/`directTargetSignatures` delegate with stable identity; `index()` accessor returns the constructed instance.
69+
- [x] 4.4 `ReachabilityEnricherMemoryTest.java` (2 cases): **(a) structural** — every declared field on `ReachabilityEnricher.class` must carry `final` (catches a future cache-field regression at compile-after-change time); **(b) behavioral** — 10 000 back-to-back `enrichMethod` calls do not mutate the underlying index sizes, each call returns a fresh map (mutation of one does not affect the next), and `targetSignatures()` returns the same object identity across all 10 000 calls (no per-call recomputation in the writer's hot path under timeout regime).
70+
- [x] 4.5 `BaselineComparisonIT` post-C1d: **8/10 pass** — identical signature to HEAD (the 2 pre-existing failures are app-class-count drift, NOT in gh60 scope; the three MOP reachability tests pass unchanged). Full client unit suite: **119 / 119 pass** (96 pre-existing + 11 from C1c + 12 new from C1d). 5-APK canonical fixture run deferred to Group 9 (integration sweep) — single-APK byte-equivalence already proven by `BaselineComparisonIT`.
71+
- [x] 4.6 Commit `refactor(gh60): C1d ReachabilityEnricher visitor (refs #60)`.
7272

7373
## 5. C1e — Pure writer walker + sentinel complete + JsonSchema.Keys / `_JK`
7474

rvsec/rvsec-android/rvsec-gator/client/src/main/java/presto/android/gui/clients/RvsecAnalysisClient.java

Lines changed: 43 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -138,17 +138,23 @@ public void run(GUIAnalysisOutput output) {
138138
new presto.android.gui.clients.reach.ReachabilityEngine(
139139
output, appClasses, targetMethods).run();
140140

141-
// Legacy aliases preserved until Group 6 (C1f) renames the JSON keys
142-
// and the writeJson helper signature in lockstep.
143-
Set<SootMethod> reachableSet = new HashSet<>(index.reachableMethods());
144-
Set<SootMethod> reachesMopSet = new HashSet<>(index.reachesTargetMethods());
145-
Set<SootMethod> directMopSet = new HashSet<>(index.directlyReachesTargetMethods());
146-
147-
// 5. Write JSON with reachability FIRST (survives timeout during WTG)
141+
// 4. Build the per-node enricher. The inline writer consults it for
142+
// each method/widget/transition/component rather than reading the
143+
// raw ReachabilityIndex — C1e enforces this as INV-ANA-30 (the
144+
// writer's purity gate) and C3 fills in widget/transition payloads
145+
// without modifying the writer.
148146
SootClass mainActivity = output.getMainActivity();
149147
String appPackage = output.getAppPackageName();
148+
presto.android.gui.clients.reach.ReachabilityEnricher enricher =
149+
new presto.android.gui.clients.reach.ReachabilityEnricher(
150+
index,
151+
appPackage,
152+
codePackage,
153+
mainActivity != null ? mainActivity.getName() : null);
154+
155+
// 5. Write JSON with reachability FIRST (survives timeout during WTG)
150156
writeJson(outputPath, appPackage, mainActivity, appClasses, output,
151-
reachableSet, reachesMopSet, directMopSet, null, new HashMap<>());
157+
enricher, null, new HashMap<>());
152158
System.out.println("[RvsecAnalysisClient] Reachability JSON written (WTG pending): " + outputPath);
153159

154160
// 6. Build WTG (may timeout on complex APKs — reachability already saved).
@@ -174,7 +180,7 @@ public void run(GUIAnalysisOutput output) {
174180

175181
// 7. Rewrite JSON with full data (reachability + WTG)
176182
writeJson(outputPath, appPackage, mainActivity, appClasses, output,
177-
reachableSet, reachesMopSet, directMopSet, wtg, windowNodeIds);
183+
enricher, wtg, windowNodeIds);
178184
} catch (Exception e) {
179185
System.out.println("[RvsecAnalysisClient] WTG construction failed: " + e.getMessage()
180186
+ " — JSON already contains reachability data");
@@ -1227,9 +1233,7 @@ private void writeJson(
12271233
SootClass mainActivity,
12281234
Map<SootClass, List<SootMethod>> appClasses,
12291235
GUIAnalysisOutput output,
1230-
Set<SootMethod> reachableSet,
1231-
Set<SootMethod> reachesMopSet,
1232-
Set<SootMethod> directMopSet,
1236+
presto.android.gui.clients.reach.ReachabilityEnricher enricher,
12331237
WTG wtg,
12341238
Map<String, Integer> windowNodeIds) {
12351239

@@ -1243,7 +1247,7 @@ private void writeJson(
12431247

12441248
// Section 1: reachability (coverage denominator — most critical)
12451249
w.name("reachability");
1246-
writeReachability(w, appClasses, output, reachableSet, reachesMopSet, directMopSet);
1250+
writeReachability(w, appClasses, output, enricher);
12471251
w.flush();
12481252

12491253
// Section 2: windows. Widget data (activities, dialogs, options-menu
@@ -1273,7 +1277,7 @@ private void writeJson(
12731277

12741278
// Section 4: components (activities, services, receivers, providers)
12751279
w.name("components");
1276-
writeComponents(w, reachesMopSet, output.getActivities(), mainActivity);
1280+
writeComponents(w, enricher, output.getActivities(), mainActivity);
12771281
w.flush();
12781282

12791283
w.endObject();
@@ -1287,9 +1291,7 @@ private void writeReachability(
12871291
JsonWriter w,
12881292
Map<SootClass, List<SootMethod>> appClasses,
12891293
GUIAnalysisOutput output,
1290-
Set<SootMethod> reachableSet,
1291-
Set<SootMethod> reachesMopSet,
1292-
Set<SootMethod> directMopSet) throws IOException {
1294+
presto.android.gui.clients.reach.ReachabilityEnricher enricher) throws IOException {
12931295

12941296
Set<SootClass> activities = output.getActivities();
12951297
SootClass mainActivity = output.getMainActivity();
@@ -1329,9 +1331,13 @@ private void writeReachability(
13291331
w.beginObject();
13301332
w.name("name").value(method.getName());
13311333
w.name("signature").value(method.getSignature());
1332-
w.name("reachable").value(reachableSet.contains(method));
1333-
w.name("reachesMop").value(reachesMopSet.contains(method));
1334-
w.name("directlyReachesMop").value(directMopSet.contains(method));
1334+
// Per-method reachability comes from the enricher — the writer
1335+
// itself never touches ReachabilityIndex (gateway for the
1336+
// INV-ANA-30 purity gate that lands fully in C1e).
1337+
Map<String, Object> ann = enricher.enrichMethod(method);
1338+
w.name("reachable").value((Boolean) ann.get("reachable"));
1339+
w.name("reachesMop").value((Boolean) ann.get("reachesMop"));
1340+
w.name("directlyReachesMop").value((Boolean) ann.get("directlyReachesMop"));
13351341
w.endObject();
13361342
}
13371343
w.endArray();
@@ -1406,7 +1412,8 @@ private void writeWidget(JsonWriter w, Map<String, Object> widget) throws IOExce
14061412
w.endObject();
14071413
}
14081414

1409-
private void writeComponents(JsonWriter w, Set<SootMethod> reachesMopSet,
1415+
private void writeComponents(JsonWriter w,
1416+
presto.android.gui.clients.reach.ReachabilityEnricher enricher,
14101417
Set<SootClass> activities, SootClass mainActivity) throws IOException {
14111418
XMLParser xmlParser = XMLParser.Factory.getXMLParser();
14121419
Map<String, Set<IntentFilter>> allFilters = IntentFilterManager.v().getAllFilters();
@@ -1419,7 +1426,7 @@ private void writeComponents(JsonWriter w, Set<SootMethod> reachesMopSet,
14191426
for (SootClass activity : activities) {
14201427
String className = activity.getName();
14211428
boolean isMain = activity.equals(mainActivity);
1422-
writeComponentEntry(w, className, activity, allFilters, reachesMopSet, xmlParser,
1429+
writeComponentEntry(w, className, activity, allFilters, enricher, xmlParser,
14231430
new String[]{"onCreate", "onStart", "onResume", "onPause", "onStop", "onDestroy", "onRestart"}, isMain);
14241431
}
14251432
w.endArray();
@@ -1430,7 +1437,7 @@ private void writeComponents(JsonWriter w, Set<SootMethod> reachesMopSet,
14301437
for (Iterator<String> it = xmlParser.getReceivers(); it.hasNext(); ) {
14311438
String className = it.next();
14321439
SootClass sc = Scene.v().getSootClassUnsafe(className);
1433-
writeComponentEntry(w, className, sc, allFilters, reachesMopSet, xmlParser,
1440+
writeComponentEntry(w, className, sc, allFilters, enricher, xmlParser,
14341441
new String[]{"onReceive"}, false);
14351442
}
14361443
w.endArray();
@@ -1441,7 +1448,7 @@ private void writeComponents(JsonWriter w, Set<SootMethod> reachesMopSet,
14411448
for (Iterator<String> it = xmlParser.getServices(); it.hasNext(); ) {
14421449
String className = it.next();
14431450
SootClass sc = Scene.v().getSootClassUnsafe(className);
1444-
writeComponentEntry(w, className, sc, allFilters, reachesMopSet, xmlParser,
1451+
writeComponentEntry(w, className, sc, allFilters, enricher, xmlParser,
14451452
new String[]{"onCreate", "onStartCommand", "onBind", "onUnbind", "onRebind", "onDestroy", "onHandleIntent"}, false);
14461453
}
14471454
w.endArray();
@@ -1452,7 +1459,7 @@ private void writeComponents(JsonWriter w, Set<SootMethod> reachesMopSet,
14521459
for (Iterator<String> it = xmlParser.getProviders(); it.hasNext(); ) {
14531460
String className = it.next();
14541461
SootClass sc = Scene.v().getSootClassUnsafe(className);
1455-
writeProviderEntry(w, className, sc, reachesMopSet, xmlParser,
1462+
writeProviderEntry(w, className, sc, enricher, xmlParser,
14561463
new String[]{"onCreate", "query", "insert", "update", "delete", "call", "openFile"});
14571464
}
14581465
w.endArray();
@@ -1461,7 +1468,8 @@ private void writeComponents(JsonWriter w, Set<SootMethod> reachesMopSet,
14611468
}
14621469

14631470
private void writeComponentEntry(JsonWriter w, String className, SootClass sc,
1464-
Map<String, Set<IntentFilter>> allFilters, Set<SootMethod> reachesMopSet,
1471+
Map<String, Set<IntentFilter>> allFilters,
1472+
presto.android.gui.clients.reach.ReachabilityEnricher enricher,
14651473
XMLParser xmlParser, String[] lifecycleMethodNames, boolean isMain) throws IOException {
14661474
if (sc == null) return;
14671475

@@ -1495,13 +1503,15 @@ private void writeComponentEntry(JsonWriter w, String className, SootClass sc,
14951503

14961504
w.name("exported").value(xmlParser.isComponentExported(className));
14971505

1498-
// MOP reachability
1506+
// MOP reachability via the enricher — writer never reads
1507+
// ReachabilityIndex directly (precondition for INV-ANA-30).
14991508
List<String> mopMethods = new ArrayList<>();
15001509
boolean reachesMop = false;
15011510
for (SootMethod m : sc.getMethods()) {
15021511
String methodName = m.getName();
15031512
for (String lifecycle : lifecycleMethodNames) {
1504-
if (methodName.equals(lifecycle) && reachesMopSet.contains(m)) {
1513+
if (methodName.equals(lifecycle)
1514+
&& Boolean.TRUE.equals(enricher.enrichMethod(m).get("reachesMop"))) {
15051515
reachesMop = true;
15061516
mopMethods.add(m.getSignature());
15071517
}
@@ -1519,8 +1529,8 @@ private void writeComponentEntry(JsonWriter w, String className, SootClass sc,
15191529
}
15201530

15211531
private void writeProviderEntry(JsonWriter w, String className, SootClass sc,
1522-
Set<SootMethod> reachesMopSet, XMLParser xmlParser,
1523-
String[] lifecycleMethodNames) throws IOException {
1532+
presto.android.gui.clients.reach.ReachabilityEnricher enricher,
1533+
XMLParser xmlParser, String[] lifecycleMethodNames) throws IOException {
15241534
if (sc == null) return;
15251535

15261536
w.beginObject();
@@ -1529,13 +1539,14 @@ private void writeProviderEntry(JsonWriter w, String className, SootClass sc,
15291539
w.name("authorities").value(xmlParser.getProviderAuthorities(className));
15301540
w.name("exported").value(xmlParser.isComponentExported(className));
15311541

1532-
// MOP reachability
1542+
// MOP reachability via the enricher (see writeComponentEntry note).
15331543
List<String> mopMethods = new ArrayList<>();
15341544
boolean reachesMop = false;
15351545
for (SootMethod m : sc.getMethods()) {
15361546
String methodName = m.getName();
15371547
for (String lifecycle : lifecycleMethodNames) {
1538-
if (methodName.equals(lifecycle) && reachesMopSet.contains(m)) {
1548+
if (methodName.equals(lifecycle)
1549+
&& Boolean.TRUE.equals(enricher.enrichMethod(m).get("reachesMop"))) {
15391550
reachesMop = true;
15401551
mopMethods.add(m.getSignature());
15411552
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package presto.android.gui.clients.reach;
2+
3+
import java.util.Collections;
4+
import java.util.LinkedHashMap;
5+
import java.util.Map;
6+
import java.util.Set;
7+
8+
import soot.SootClass;
9+
import soot.SootMethod;
10+
11+
/**
12+
* Stateless visitor that annotates per-node reachability for the JSON
13+
* writer. Each {@code enrich*} method consults the immutable
14+
* {@link ReachabilityIndex} and returns the key/value pairs the writer
15+
* should emit for that node, with no side effects on either the index
16+
* or the enricher itself (INV-ANA-30 preserves writer purity).
17+
*
18+
* <p>For gh60 (C1d) only {@link #enrichMethod} carries content; the
19+
* widget / transition / component overloads are placeholders that
20+
* return empty maps. C3 ({@code gh<N+2>-agent-enrichment}) fills them
21+
* in with per-widget {@code handlerReachesTarget} etc. without touching
22+
* the writer.
23+
*
24+
* <p>Keys still use the legacy gh57 names ("reachable", "reachesMop",
25+
* "directlyReachesMop") because Group 6 (C1f) renames the JSON schema
26+
* atomically across producer + consumers. The enricher's job in C1d/C1e
27+
* is to land the visitor contract; the rename is its own commit.
28+
*/
29+
public final class ReachabilityEnricher {
30+
31+
private static final Map<String, Object> EMPTY = Collections.emptyMap();
32+
33+
private final ReachabilityIndex index;
34+
private final String manifestPackage;
35+
private final String codePackage;
36+
private final String mainActivity;
37+
38+
public ReachabilityEnricher(
39+
ReachabilityIndex index,
40+
String manifestPackage,
41+
String codePackage,
42+
String mainActivity) {
43+
if (index == null) {
44+
throw new NullPointerException("index");
45+
}
46+
this.index = index;
47+
this.manifestPackage = manifestPackage != null ? manifestPackage : "";
48+
this.codePackage = codePackage != null ? codePackage : "";
49+
this.mainActivity = mainActivity != null ? mainActivity : "";
50+
}
51+
52+
/**
53+
* Per-method reachability annotations. Used by {@code writeReachability}
54+
* to populate each method object inside a class's {@code methods[]}.
55+
*/
56+
public Map<String, Object> enrichMethod(SootMethod method) {
57+
// LinkedHashMap preserves insertion order so the writer emits keys
58+
// in a stable sequence — important for diff-friendly snapshots.
59+
Map<String, Object> out = new LinkedHashMap<>(3);
60+
out.put("reachable", index.isReachable(method));
61+
out.put("reachesMop", index.reachesTarget(method));
62+
out.put("directlyReachesMop", index.directlyReachesTarget(method));
63+
return out;
64+
}
65+
66+
/**
67+
* Per-widget annotations. Empty in C1d; C3 introduces
68+
* {@code handlerReachesTarget}/{@code handlerDirectlyReachesTarget}
69+
* here without modifying the writer.
70+
*/
71+
public Map<String, Object> enrichWidget(Map<String, Object> widget) {
72+
return EMPTY;
73+
}
74+
75+
/**
76+
* Per-transition annotations. Empty in C1d; C3 introduces
77+
* {@code handlerReachesTarget}/{@code externalExit}/{@code exitKind}
78+
* per WTG edge here.
79+
*/
80+
public Map<String, Object> enrichTransition(Object transitionDescriptor) {
81+
return EMPTY;
82+
}
83+
84+
/**
85+
* Per-component annotations. Empty in C1d; C3 may emit
86+
* {@code componentReachesTarget} aggregates here.
87+
*/
88+
public Map<String, Object> enrichComponent(SootClass component) {
89+
return EMPTY;
90+
}
91+
92+
/**
93+
* App-level metadata. Currently the gh57 inline writer emits
94+
* {@code package} and {@code mainActivity} only; {@code codePackage}
95+
* is reserved for the upcoming dual-package emission (C2 — issue
96+
* G11) and is exposed here so that change can add the key without
97+
* touching the writer.
98+
*/
99+
public Map<String, Object> topLevelMetadata() {
100+
Map<String, Object> out = new LinkedHashMap<>(3);
101+
out.put("manifestPackage", manifestPackage);
102+
out.put("codePackage", codePackage);
103+
out.put("mainActivity", mainActivity);
104+
return out;
105+
}
106+
107+
/**
108+
* Set of Soot signatures for methods that reach a target. The writer
109+
* emits this as the {@code targetMethods}/{@code mopMethods} top-level
110+
* key (name changes in Group 6 — payload identical).
111+
*/
112+
public Set<String> targetSignatures() {
113+
return index.reachesTargetSignatures();
114+
}
115+
116+
/**
117+
* Set of Soot signatures for methods that DIRECTLY reach a target
118+
* (CG-edge ∪ bytecode-scan).
119+
*/
120+
public Set<String> directTargetSignatures() {
121+
return index.directlyReachesTargetSignatures();
122+
}
123+
124+
public ReachabilityIndex index() {
125+
return index;
126+
}
127+
}

0 commit comments

Comments
 (0)