Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
111 commits
Select commit Hold shift + click to select a range
ae40837
Add cross-platform App Hardening (DexGuard-class), Enterprise-gated
shai-almog Aug 6, 2026
010fe08
CI: complete copyright headers, /// docs, and force engine build order
shai-almog Aug 6, 2026
fc8822a
cn1-hardening: guard ProGuard against JDK 21+ class files
shai-almog Aug 6, 2026
efa163b
Address Codex P1 review: frame hierarchy, FQ main class, reactor dep
shai-almog Aug 6, 2026
605f722
Address Codex round-2 review + fix core/CI build breaks
shai-almog Aug 6, 2026
0fffb4c
Address Codex round-3 review (verifier hierarchy, service descriptors…
shai-almog Aug 6, 2026
8899127
docs: satisfy the developer-guide prose gate (Vale + xref + LanguageT…
shai-almog Aug 6, 2026
d2ebbef
Address Codex round-4 + Copilot review
shai-almog Aug 6, 2026
5009358
Address Codex round-5 review
shai-almog Aug 6, 2026
fdc77f2
Address Codex round-6 review (interface literals, empty-config)
shai-almog Aug 6, 2026
6efa3ef
Address Codex round-7 review
shai-almog Aug 6, 2026
d354db3
Address Codex round-8 review
shai-almog Aug 6, 2026
71b9c68
Address Codex round-9 review
shai-almog Aug 6, 2026
95a7fdb
CI: fix illegal '--' inside an XML comment in the plugin POM
shai-almog Aug 6, 2026
495a599
Address Codex round-10 review
shai-almog Aug 6, 2026
c94e6dd
Address Codex round-11 review
shai-almog Aug 6, 2026
b625567
Address Codex round-12 review
shai-almog Aug 6, 2026
df007c4
Address Codex round-13 review + fix developer-guide LanguageTool gate
shai-almog Aug 6, 2026
9fa7384
Fix JS launcher compile: import Display for the hardening stamp
shai-almog Aug 6, 2026
23e52cd
Address Codex review round 14 (unresolved #5527 threads)
shai-almog Aug 7, 2026
107abbb
Fix Vale gate in App-Hardening doc: drop adverb, use contraction
shai-almog Aug 7, 2026
f7c4e26
Fix SpotBugs DM_DEFAULT_ENCODING in CrashProtection.safeRawStack
shai-almog Aug 7, 2026
30e4d61
Address Codex review round 15 (#5527)
shai-almog Aug 7, 2026
ba739f5
Address Codex review round 16 (#5527)
shai-almog Aug 7, 2026
5435b58
Address Codex review round 18 (#5527)
shai-almog Aug 7, 2026
245c14c
Narrow keep rules and rewrite hardening docs per project-owner review
shai-almog Aug 7, 2026
c5404f2
Address Codex review round 19: fix the real ones, push back on one
shai-almog Aug 7, 2026
9c54748
Address Codex round 20: fix two real hoisting bugs
shai-almog Aug 7, 2026
9c12bc2
Address Codex round 21 (mapping-id + idempotence)
shai-almog Aug 7, 2026
f47cffb
Address Codex round 22 (docs): correct engine-secrecy claim, note ann…
shai-almog Aug 7, 2026
e195c21
Security: never trust client-supplied cn1.hardened / hardenLevel / ma…
shai-almog Aug 7, 2026
2867390
Address Codex round 26 (#5527): preserve metadata on idempotent path …
shai-almog Aug 7, 2026
78c0e90
Doc: qualify SourceFile stripping for the unminified Android case
shai-almog Aug 7, 2026
e6aebfc
Address Codex round 27 (#5527): gate SourceFile on verified hardening…
shai-almog Aug 8, 2026
7ba3a56
Keep the generated SVGRegistry under its fixed name
shai-almog Aug 8, 2026
35e2ae2
Address Codex round 29: gate R8 keep rules on verified hardening
shai-almog Aug 8, 2026
7532a4c
Derive crash trace format from platform so JVM traces are not mislabe…
shai-almog Aug 8, 2026
bcd9de1
Preserve JS stack coordinates when scrubbing; report unencryptable co…
shai-almog Aug 8, 2026
c31273c
Tighten JS frame detection; print replacement frames after setStackTr…
shai-almog Aug 8, 2026
2c09704
Serialize hardening warnings in the report; correct PARANOID profile …
shai-almog Aug 8, 2026
84fab7a
Route raw-stack messages through scrubMessage; base preflight on the …
shai-almog Aug 8, 2026
35b0969
Require a real location for 'at' frame lines; keep invalid harden.lev…
shai-almog Aug 8, 2026
fb92dc1
Split oversized hoist <clinit>; report legacy-interface constants; no…
shai-almog Aug 8, 2026
0982015
Validate parenthesized frame locations; reject every enableProguard v…
shai-almog Aug 8, 2026
76917eb
Keep the distinct watch entry class; require a release variant for An…
shai-almog Aug 8, 2026
40a71a5
Report string literals left plaintext because they are too large to e…
shai-almog Aug 8, 2026
00114d0
Report constant-dynamic string exclusions; account for the existing <…
shai-almog Aug 8, 2026
13ec40d
Measure encoded bytecode before growing a method; skip and report whe…
shai-almog Aug 8, 2026
2461ea0
Require a whitespace-free frame identity; size-check the guard-field …
shai-almog Aug 8, 2026
1353f18
Keep R8 source-file metadata; size-check interface method-body string…
shai-almog Aug 8, 2026
5446ef9
Bound raw-stack capture; count annotation-value strings excluded from…
shai-almog Aug 8, 2026
ab3d013
Require a dotted/URL frame identity; recurse into nested constant-dyn…
shai-almog Aug 8, 2026
63f050a
Scrub frame URL/query data; keep name-bound background callbacks; cap…
shai-almog Aug 8, 2026
ebb8ef8
Keep BackgroundWorker; fall back to per-access encryption instead of …
shai-almog Aug 8, 2026
9de216b
Scrub coordinate-free frame mimics; exclude un-encryptable literals j…
shai-almog Aug 8, 2026
773b977
Preserve only the terminal two coordinate groups in a frame line
shai-almog Aug 8, 2026
75b9446
Size-check hoist LDC->GETSTATIC replacements; count type-use and reco…
shai-almog Aug 8, 2026
4478857
Budget the constant pool for static-final fields and control-flow guards
shai-almog Aug 8, 2026
16cf907
Keep Login subclasses; run the hardening preflight against library-me…
shai-almog Aug 8, 2026
6549725
Document why carrying .java/.kt sources through the demuxer is rename…
shai-almog Aug 8, 2026
6b81363
Validate @-frame source shape before preserving coordinates; skip OFF…
shai-almog Aug 8, 2026
52ed531
Budget per-access ciphertext growth; use inlinee's own sourceFile in …
shai-almog Aug 8, 2026
a4ce422
Preserve literal identity across the library boundary; charge widened…
shai-almog Aug 8, 2026
9cf83ca
Apply the engine's platform-safety rules in the hardening preflight
shai-almog Aug 8, 2026
9edd05b
Disclose non-concat invokedynamic string args and short literals in t…
shai-almog Aug 8, 2026
16a5c8f
Preserve static-final ConstantValues a bundled Android source referen…
shai-almog Aug 8, 2026
4f1e274
Honor jar-wide and library exclusions in the static-final string path
shai-almog Aug 8, 2026
60132cf
Exclude the ParparVM runtime jar's literals and jar-wide-exclude budg…
shai-almog Aug 8, 2026
d606187
Skip Android R8-rename enforcement when hardening was forced off
shai-almog Aug 8, 2026
0d51b7c
Require indentation before treating an 'at ...' line as a stack frame
shai-almog Aug 8, 2026
0db5a6d
Record real source filenames in the engine's own mapping
shai-almog Aug 8, 2026
862d10b
Require a URL path separator in an @-frame source, not just a dot
shai-almog Aug 9, 2026
2020215
Exclude a pre-Java-8 interface's un-encryptable constant jar-wide
shai-almog Aug 9, 2026
fa59896
Accept multi-word V8 frame identities in the at-paren form
shai-almog Aug 9, 2026
7cd8411
Reject conflicting per-slice hardening opt-outs in a combined iOS+Mac…
shai-almog Aug 9, 2026
07e05bb
Combine per-slice hardening opt-outs instead of consulting one slice,…
shai-almog Aug 9, 2026
b3ae16c
Restrict at-paren multi-word identities to genuine V8 label shapes
shai-almog Aug 9, 2026
49034a3
Validate the V8 accessor alias content before accepting an at-paren f…
shai-almog Aug 9, 2026
e83f164
Apply scrubFrame to raw stack, combine Apple opt-outs in preflight, q…
shai-almog Aug 9, 2026
b4ed029
Size the obfuscation dictionary for the biggest class's member count too
shai-almog Aug 9, 2026
2844caf
Stop preserving coordinates from the ambiguous Firefox/Safari @-frame…
shai-almog Aug 9, 2026
875477d
Scrub the whole raw stack uniformly; stop preserving frame coordinates
shai-almog Aug 9, 2026
fcbaf7f
Honor a null scrubFrame redaction in the raw stack; size the dictiona…
shai-almog Aug 9, 2026
5c3ae44
Resolve a common supertype from class bytes when a shared base can't …
shai-almog Aug 9, 2026
f4f35e8
Include interfaces in byte-based common-supertype resolution
shai-almog Aug 9, 2026
34cb401
Fall back to structural verification when the output verifier can't r…
shai-almog Aug 9, 2026
20ef9be
Decode JSON escapes when reading sourceFile metadata
shai-almog Aug 9, 2026
be80678
Harden ParparVM-JS runtime literals; document array-frame contract
shai-almog Aug 9, 2026
5e4833c
Count short static-final constants in the string-coverage disclosure
shai-almog Aug 9, 2026
8cd67a8
Merge remote-tracking branch 'origin/master' into app-hardening
shai-almog Aug 9, 2026
8c9e103
Carry the hardening force-off/library-jars decision per build, not JV…
shai-almog Aug 9, 2026
63a4fe7
Ship a class unhardened when its frame hierarchy is incomplete
shai-almog Aug 9, 2026
474a723
Zero applied counts when a class is discarded as unresolvable
shai-almog Aug 9, 2026
799372c
Escape JSON control characters in sourceFile mapping metadata
shai-almog Aug 9, 2026
374c216
Preserve whitespace in retraced source filenames
shai-almog Aug 9, 2026
364d110
Keep HealthBackgroundListener implementors so their names stay stable
shai-almog Aug 9, 2026
a93fc64
Propagate the hardening force-off decision into every local request
shai-almog Aug 10, 2026
42e9d45
Preserve ConstantValue for constants read by a non-inlined GETSTATIC
shai-almog Aug 10, 2026
9757e15
Document interface-merge soundness and the package-relative-resource …
shai-almog Aug 10, 2026
5fb8161
docs: drop the adverb the Vale prose gate flagged
shai-almog Aug 10, 2026
54e0528
Close two gaps in the incomplete-hierarchy and GETSTATIC-owner handling
shai-almog Aug 10, 2026
1bf1604
Gate parparvm-text trace classification on a ParparVM-C platform
shai-almog Aug 10, 2026
d4cc154
Distinguish native ParparVM-C from JavaSE by runtime, not platform name
shai-almog Aug 10, 2026
fba9418
Accept a verifier report whose TEXT names only a missing type
shai-almog Aug 10, 2026
4124a5e
Read retrace mapping files as UTF-8, not the platform default
shai-almog Aug 10, 2026
ffe1519
docs: correct scrubRawStack javadoc to match uniform masking
shai-almog Aug 10, 2026
df5a3b6
Run hardening preflight before the Android up-to-date cache short-cir…
shai-almog Aug 10, 2026
8b6485f
Verify data flow per method so a real error survives a missing-type peer
shai-almog Aug 10, 2026
7608043
Bypass the Android up-to-date cache when hardening will actually run
shai-almog Aug 10, 2026
4684e09
Invalidate the Android APK cache on any hardening-outcome change
shai-almog Aug 10, 2026
bf5ea64
Fingerprint all effective harden.* settings in the APK cache key
shai-almog Aug 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 86 additions & 2 deletions CodenameOne/src/com/codename1/crash/CrashProtection.java
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,8 @@ public void exception(Throwable t) {
"Process terminated by native fault",
new ArrayList<Frame>(0),
null,
pendingNative);
pendingNative,
null);
persistJson(synthetic.toJson());
}
installed = true;
Expand Down Expand Up @@ -240,8 +241,91 @@ static CrashReportPayload build(Throwable t) {
String message = scrubber.scrubMessage(t.getMessage());
List<Frame> frames = extractFrames(t);
String nativeLog = safeNativeLog();
String rawStack = scrubber.scrubRawStack(safeRawStack(t));
return new CrashReportPayload(newEventId(), exClass, message,
frames, nativeLog, null);
frames, nativeLog, null, rawStack);
}

/// Renders the throwable (and its cause chain) as a pre-rendered stack
/// string via `printStackTrace`. This is the one trace API that behaves
/// identically on every port, and on the ParparVM C targets -- where
/// `getStackTrace()` may return the trace only as a formatted string --
/// it is what keeps a Java crash readable, especially once obfuscated.
/// Swallows any failure: capturing a crash report must never itself crash.
private static String safeRawStack(Throwable t) {
try {
// Capture the platform's own rendering via printStackTrace(PrintStream) -- PrintStream
// (unlike PrintWriter) is in the restricted CLDC core API. This is what preserves the
// real trace on the ParparVM ports: the pre-rendered C shadow-call-stack text, or the
// JavaScript engine's Error().stack on the JS port (where getStackTrace() has no
// structured frames to offer). On the JVM ports it is the standard full trace.
//
// Render into a BOUNDED buffer that discards bytes past the cap, rather than an unbounded
// ByteArrayOutputStream truncated afterwards: a throwable with a huge message or a deep
// cause chain could otherwise allocate many times the 16 KiB cap during crash handling and
// trigger a second OutOfMemoryError, losing the report. The cap sits a little above
// MAX_RAW_STACK_LEN so scrubbing (which only shrinks -- digit runs and emails collapse) and
// the final trim still yield a full-length raw stack.
BoundedOutputStream bout = new BoundedOutputStream(
CrashReportPayload.MAX_RAW_STACK_LEN + 8192);
try {
// Encode explicitly as UTF-8 on both ends rather than relying on the platform default
// (which SpotBugs flags and which would garble a non-ASCII exception message differently
// per device); the PrintStream and the readback share the charset.
java.io.PrintStream ps = new java.io.PrintStream(bout, true, "UTF-8");
try {
t.printStackTrace(ps);
ps.flush();
} finally {
ps.close();
}
String s = bout.toUtf8();
return s.length() == 0 ? null : s;
} finally {
bout.close();
}
} catch (Throwable ignored) {
return null;
}
}

/// A fixed-capacity {@link java.io.OutputStream} that keeps the first {@code cap} bytes and
/// silently discards the rest, so rendering a pathologically large stack cannot grow the buffer
/// without bound (and cannot itself OOM during crash handling). Not thread-safe; used by a single
/// capturing thread.
static final class BoundedOutputStream extends OutputStream {
private final byte[] buf;
private int count;

BoundedOutputStream(int cap) {
buf = new byte[cap];
}

@Override
public void write(int b) {
if (count < buf.length) {
buf[count++] = (byte) b;
}
}

@Override
public void write(byte[] b, int off, int len) {
int room = buf.length - count;
if (room <= 0) {
return;
}
int n = len < room ? len : room;
System.arraycopy(b, off, buf, count, n);
count += n;
}

String toUtf8() throws java.io.UnsupportedEncodingException {
// Explicit UTF-8 (never the platform default, which SpotBugs flags and which would garble a
// non-ASCII message per device). UTF-8 is always available, so this never actually throws;
// the checked exception is handled by the single caller's catch-all rather than swallowed
// here into a default-encoding String.
return new String(buf, 0, count, "UTF-8");
}
}

/// Pulls the platform log snapshot, swallowing any exception the
Expand Down
117 changes: 115 additions & 2 deletions CodenameOne/src/com/codename1/crash/CrashReportPayload.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,20 @@ final class CrashReportPayload {
/// signal handlers are usually compact (~64 frames * ~120 chars),
/// but a corrupt stack can produce arbitrarily long output.
static final int MAX_NATIVE_STACK_LEN = 16 * 1024;
/// Hard cap on the raw (pre-rendered) Java stack string captured via
/// `printStackTrace` -- the verbatim platform rendering plus the cause
/// chain. It complements the structured {@link #frames} (populated on
/// every port), and on the JS port, where there are no structured
/// frames, it carries the JavaScript engine stack. Mirrors
/// {@link #MAX_NATIVE_STACK_LEN}.
static final int MAX_RAW_STACK_LEN = 16 * 1024;

/// Trace-format discriminator values. Tells the server how to parse
/// {@link #rawStack} for this build.
static final String TRACE_STRUCTURED = "structured";
static final String TRACE_PARPARVM = "parparvm-text";
static final String TRACE_JS = "js-error";
static final String TRACE_NONE = "none";

final String eventId;
final String buildKey;
Expand All @@ -56,6 +70,23 @@ final class CrashReportPayload {
final String exceptionClass;
final String messageScrubbed;
final List<Frame> frames;
/// The pre-rendered Java stack captured via `printStackTrace`, which
/// works identically on every port. On the ParparVM C targets this is
/// the only readable Java trace once obfuscated; the server parses it
/// with the mapping. `null` when no stack was available.
final String rawStack;
/// One of {@link #TRACE_STRUCTURED}, {@link #TRACE_PARPARVM},
/// {@link #TRACE_JS} or {@link #TRACE_NONE}: how the server should read
/// {@link #rawStack}. Derived, never guessed.
final String traceFormat;
/// SHA-256 of the obfuscation mapping this build was hardened with,
/// stamped into the app so a report can be tied to the exact mapping.
/// Empty for unhardened builds.
final String mappingId;
/// The hardening level the build shipped with (`off` / `standard` /
/// `aggressive` / `paranoid`); lets the server answer "why can't I
/// retrace this?" with the honest reason.
final String hardenLevel;
/// Recent platform-log output captured at crash time. Provides
/// context the Java stack frame alone can't (NSLog/os_log on iOS,
/// logcat on Android). `null` if the platform has no readable log
Expand All @@ -71,25 +102,103 @@ final class CrashReportPayload {

CrashReportPayload(String eventId, String exceptionClass,
String messageScrubbed, List<Frame> frames,
String nativeLog, String nativeStack) {
String nativeLog, String nativeStack, String rawStack) {
this.eventId = eventId;
this.exceptionClass = exceptionClass;
this.messageScrubbed = trim(messageScrubbed, MAX_MESSAGE_LEN);
this.frames = capFrames(frames);
this.nativeLog = trim(nativeLog, MAX_NATIVE_LOG_LEN);
this.nativeStack = trim(nativeStack, MAX_NATIVE_STACK_LEN);
this.rawStack = trim(rawStack, MAX_RAW_STACK_LEN);
Display d = Display.getInstance();
this.platform = d.getPlatformName();
this.traceFormat = deriveTraceFormat(this.frames, this.rawStack, this.platform);
this.buildKey = d.getProperty("build_key", "");
this.packageName = d.getProperty("package_name", "");
this.appName = d.getProperty("AppName", "");
this.appVersion = d.getProperty("AppVersion", "");
this.platform = d.getPlatformName();
this.osVersion = d.getProperty("OSVer", "");
this.mappingId = d.getProperty("cn1.mappingId", "");
this.hardenLevel = d.getProperty("cn1.hardenLevel", "");
Locale loc = Locale.getDefault();
this.locale = loc == null ? "" : loc.toString();
this.clientTs = System.currentTimeMillis();
}

/// Derives the trace format from what we actually have, so the server picks the right parser --
/// never a guess. Structured frames win. Otherwise: the JavaScript port's raw stack is a JS engine
/// stack; a ParparVM C target's is the " at <fqcn>.<method>:<line>" text; a JVM target
/// (Android/desktop) reaching here has an ordinary JVM printStackTrace (e.g. a stackless throwable
/// whose only frames are in its cause) that the JS parser must NOT touch. The old heuristic looked
/// only for the 4-space ParparVM shape and labeled everything else -- including the tab-indented
/// JVM trace -- as JavaScript; derive from the platform so that never happens.
static String deriveTraceFormat(List<Frame> frames, String rawStack, String platform) {
// A real JVM sets java.vm.name; ParparVM's System.getProperty always returns null. This is the
// only reliable way to tell a native ParparVM-C target from a JavaSE desktop app or the simulator,
// because JavaSEPort.getPlatformName() returns the SAME mac/win names a native build reports.
return deriveTraceFormat(frames, rawStack, platform, System.getProperty("java.vm.name") != null);
}

/// The testable core of {@link #deriveTraceFormat(List, String, String)}: {@code runningOnJvm} is
/// supplied explicitly (production derives it from {@code java.vm.name}) so a JVM-hosted unit test can
/// exercise both the native ParparVM-C path and the JavaSE-on-mac/win path.
static String deriveTraceFormat(List<Frame> frames, String rawStack, String platform,
boolean runningOnJvm) {
if (frames != null && !frames.isEmpty()) {
return TRACE_STRUCTURED;
}
if (rawStack == null || rawStack.length() == 0) {
return TRACE_NONE;
}
if (isJavaScriptPlatform(platform)) {
return TRACE_JS;
}
// The " at <fqcn>.<method>:<line>" text is only produced by ParparVM's own printStackTrace on a
// native C target. Gate on the runtime: an Android/desktop/simulator (real-JVM) throwable whose
// MESSAGE happens to contain such a line -- the message is echoed into the raw stack by
// printStackTrace -- must NOT be labeled parparvm-text, or the server would fabricate a frame from
// message text or drop a real cause trace. The shape check still guards an unexpected stack on a
// ParparVM-C target.
if (isParparVmTextPlatform(platform, runningOnJvm)) {
// A ParparVM frame line is exactly " at <fqcn>.<method>:<line>" -- no '(', URL or '@'.
int at = rawStack.indexOf(" at ");
if (at >= 0) {
int lineEnd = rawStack.indexOf('\n', at);
String body = lineEnd < 0 ? rawStack.substring(at + 7) : rawStack.substring(at + 7, lineEnd);
if (body.indexOf('(') < 0 && body.indexOf('/') < 0 && body.indexOf('@') < 0) {
return TRACE_PARPARVM;
}
}
}
// Not JavaScript and not a native ParparVM-C target's text shape: an ordinary JVM printStackTrace
// body. There is no JVM raw parser, so report NONE and let the server keep the text verbatim rather
// than misparsing it.
return TRACE_NONE;
}

/// True only on a native ParparVM-to-C runtime, whose {@code printStackTrace} emits the
/// " at &lt;fqcn&gt;.&lt;method&gt;:&lt;line&gt;" text. The displayed platform name is NOT enough:
/// a skinless JavaSE desktop app returns {@code mac}/{@code win} (Linux falls through to {@code win})
/// and the simulator can return {@code ios}, all colliding with the native names. So anything running
/// on a real JVM ({@code runningOnJvm}: JavaSE desktop, the simulator, or Android) is excluded, and of
/// the remaining native runtimes only the C-target names qualify.
private static boolean isParparVmTextPlatform(String platform, boolean runningOnJvm) {
if (runningOnJvm || platform == null) {
return false;
}
String p = platform.toLowerCase();
return "ios".equals(p) || "mac".equals(p) || "linux".equals(p) || "win".equals(p);
Comment thread
shai-almog marked this conversation as resolved.
}

/// The JavaScript port's platform name; its raw stack is a JS engine {@code Error().stack}.
private static boolean isJavaScriptPlatform(String platform) {
if (platform == null) {
return false;
}
String p = platform.toLowerCase();
return p.indexOf("html") >= 0 || p.indexOf("javascript") >= 0 || "js".equals(p);
}

static final class Frame {
final String className;
final String methodName;
Expand Down Expand Up @@ -124,6 +233,10 @@ String toJson() {
appendString(b, "locale", locale, false);
appendString(b, "nativeLog", nativeLog, false);
appendString(b, "nativeStack", nativeStack, false);
appendString(b, "rawStack", rawStack, false);
appendString(b, "traceFormat", traceFormat, false);
appendString(b, "mappingId", mappingId, false);
appendString(b, "hardenLevel", hardenLevel, false);
b.append(",\"clientTs\":").append(clientTs);
b.append(",\"frames\":[");
for (int i = 0; i < frames.size(); i++) {
Expand Down
Loading
Loading