Skip to content

Commit 9f7b84f

Browse files
Port forcedotcom#4098: fix nil-sceneId crash on advanced-auth browser callback (unit 43/49)
Semantic re-implementation FROM forcedotcom/dev e4e8388 INTO the ObjC->Swift migration branch. OAuth advanced-auth (ASWebAuthenticationSession) scene fix. When advanced-auth login starts before any UIScene has connected (cold launch), or the weak authSession deallocates before the browser callback fires, sceneId was nil and got inserted into the URL-handler options dictionary, crashing on the nil insert and dropping the session from the authSessions store. Ported into the compiled Swift twins: - SFSDKAuthSession.swift: synthesize a unique per-session scene id (com.salesforce.mobilesdk.unscopedAuthSession-<UUID>) when no scene is connected. This also fixes a latent collision in the migration, where the non-optional sceneId defaulted to "" and all scene-less sessions shared one authSessions[] key. - SFOAuthCoordinator.swift: browserCallbackOptions(forSceneId:) omits the key when the id is nil (empty dict) instead of inserting nil; the URL handler then falls back to the default scene. Helper is internal (visible to @testable), no new public API. De-referenced .m files (SFSDKAuthSession.m, SFOAuthCoordinator.m, SFOAuthCoordinatorTests.m) ref-synced byte-faithful to upstream for clean future merges; SFOAuthCoordinator+Internal.h is a migration tombstone (skip). The 4 new tests were also ported to the compiled Swift twin SFOAuthCoordinatorTests.swift. SDKCore/SmartStore/MobileSync TEST BUILD SUCCEEDED (0 errors, 0 new warnings); 6 SFOAuthCoordinatorTests pass. Marker advanced 42->43 (e4e8388). Escalation: OAuth advanced-auth/scene callback behavior -- flag in PR.
1 parent fce599a commit 9f7b84f

9 files changed

Lines changed: 194 additions & 9 deletions

File tree

.claude/pr-escalation-digest.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,8 +256,26 @@ merges. (3) `SKILL.md` update is byte-identical to upstream (`SQLLite`→`SQLite
256256
build-cache artifact, not a code issue. SmartStore/MobileSync TEST BUILD green; 3 version tests pass (runtime-confirmed
257257
`4.17.0 community` / SQLite `3.53.3`). **Escalation = SQLCipher dependency bump — flag in PR (pre-approved).**
258258

259+
### Unit 43 · #4098 — nil-sceneId crash on advanced-auth browser callback (OAuth/scene)
260+
**Escalation class: OAuth advanced-authentication flow / scene routing.** No public API change; behavior fix in the
261+
native-browser (`ASWebAuthenticationSession`) login path.
262+
**The bug:** when advanced-auth login starts before any `UIScene` has connected (cold launch, extension-driven login) —
263+
or the weak `authSession` deallocates before the browser callback fires — the callback built its URL-handler options
264+
dictionary as `@{kSFIDPSceneIdKey : sceneId}` with a nil `sceneId`, crashing on the nil insert and dropping the session.
265+
**The fix (2 parts):** (1) `SFSDKAuthSession` synthesizes a unique per-session scene id (`com.salesforce.mobilesdk.unscopedAuthSession-<UUID>`)
266+
when no scene is connected, so every session gets its own `authSessions[]` key; (2) `SFOAuthCoordinator.browserCallbackOptions(forSceneId:)`
267+
omits the key entirely when the id is nil, letting the URL handler fall back to the default scene.
268+
**Reviewer notes:** (1) All production changes land in the compiled Swift twins (`SFSDKAuthSession.swift`,
269+
`SFOAuthCoordinator.swift`); the de-referenced `.m` files were ref-synced byte-faithful for clean future merges;
270+
`SFOAuthCoordinator+Internal.h` is a migration tombstone (no class-ext body) so the helper decl lives on the Swift class
271+
as `internal` (visible to `@testable`, no new public API). (2) **Migration-specific extra:** the migrated `sceneId` was a
272+
non-optional `String` defaulting to `""`, so it never *crashed* — but it had a latent **collision** bug (all scene-less
273+
sessions shared the empty-string key). This port fixes that collision too, matching upstream's per-session-unique intent.
274+
(3) The 4 new tests were both ref-synced into the de-ref `SFOAuthCoordinatorTests.m` (verbatim) and ported to the compiled
275+
Swift twin. SDKCore/SmartStore/MobileSync build green (0 new warnings — 2 pre-existing unrelated warnings confirmed
276+
unchanged); 6 SFOAuthCoordinatorTests pass. **Escalation = OAuth advanced-auth/scene callback behavior — flag in PR.**
277+
259278
## Pending escalation units (upcoming — port in order)
260-
- **43 · #4098 — OAuth/scene:** nil-sceneId crash fix on advanced-auth browser callback.
261279
- **44 · #4087 — OAuth/token (LIVE-AUTH UNBLOCKER):** token-refresh coordinator; 18 files. Unblocks Phase 2.
262280
- **45 · #4102 — OAuth/token:** improve token-refresh error handling.
263281
- **46 · #4105 — login-host:** iOS26 login-host classifier fix.

.claude/upstream-sync-backlog.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Upstream Sync Backlog Ledger
22

3-
Marker (done floor): `303013dd7` (units 1-42 done) · forcedotcom/dev HEAD (target): `b5d37d807` · **7 units remaining** · Re-seeded: 2026-07-19 (Phase 0 of the resume-porting plan).
3+
Marker (done floor): `e4e838863` (units 1-43 done) · forcedotcom/dev HEAD (target): `b5d37d807` · **6 units remaining** · Re-seeded: 2026-07-19 (Phase 0 of the resume-porting plan).
44

55
> **Direction:** we port changes **FROM** `forcedotcom/dev` **INTO** our ObjC→Swift migration branch
66
> (`feature/objc-to-swift-test-migration`). Each unit is a *semantic re-implementation* against the current
@@ -17,7 +17,7 @@ Marker (done floor): `303013dd7` (units 1-42 done) · forcedotcom/dev HEAD (ta
1717
> non-libs (CI, docs, skills, sample apps). Live progress bar = subject of lead task #9.
1818
1919
## Migration status
20-
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░ 42/49 units done (86%) · libs-production-impacting: 15/21 · Phase 1 porting (units 1-42 ✅)
20+
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░ 43/49 units done (88%) · libs-production-impacting: 16/21 · Phase 1 porting (units 1-43 ✅)
2121

2222
| Bucket | Count | Notes |
2323
|--------|-------|-------|
@@ -73,7 +73,7 @@ Marker (done floor): `303013dd7` (units 1-42 done) · forcedotcom/dev HEAD (ta
7373
| 40 | #4088 invalid login-host recovery | a2a271cca | B | ⚠ login-host + L10n(pre-appr) | ✅ ported 6263a436e4127e450592d5be35410ecbce8d9f53 | Validate login-host input; recover to previous host when the current one fails. Ported into compiled Swift twins: **NewLoginHostView.swift** (per-field inline error UI: `NewLoginHostField.errorMessage`/`errorAccessibilityID`; `save(...)` validates non-empty + contains `.` + no whitespace + parseable `https://` URL, sets `hostError`=`LOGIN_INVALID_HOST` and bails on invalid; `.onChange(of: host)` clears error) — twin diverged from pre-image only in 2 `init` API-parity lines (outside change regions) so applied hunks surgically. **SFUserAccountManager.swift** (new `internal var previousLoginHost`; set in `hostListViewController(_:didChange:)`; recovery rewrite in `hostConnectionErrorHandlerBlock` completion: only auto-remove a *deletable* failing host on a **strong-bad-host** signal [`kSFOAuthErrorInvalidURL` / `NSURLErrorBadURL`/`UnsupportedURL`/`AppTransportSecurityRequiresSecureConnection`], NOT on ambiguous DNS/timeout [captive-portal safety]; recovery host = `previousLoginHost` if still in storage else `loginHost(at:0)` guarded by `numberOfLoginHosts>0` to avoid range trap on empty storage). L10n `LOGIN_INVALID_HOST` (pre-approved). Test-visibility: relaxed `SFSDKLoginHostStorage.loginHostList` `private`→`internal` (mirrors ObjC test's KVC reach into the private ivar; no new public API). De-ref ref-sync: `SFUserAccountManager+Internal.h` — added `previousLoginHost` @property to the `#if !defined(SWIFT_CLASS)` ObjC class-extension region verbatim (anchor `nativeLoginEnabled`→`setCurrentUserInternal` matched pre-image; header has NO compiled consumer — all 7 importer `.m` de-referenced — but kept in sync for clean future merges). Stub SKIP (no matching region): `SFUserAccountManager.m` (79-line stub). Tests: NEW ObjC `SFUserAccountManagerLoginHostRecoveryTests.m` (357 lines, `method_exchangeImplementations` swizzle + KVC) ported to a **Swift twin** `SFUserAccountManagerLoginHostRecoveryTests.swift` (9 tests: previousLoginHost capture, restore-to-previous, index-0 fallback ×2, strong-signal removal, ambiguous/DNS keep, empty-storage guard, non-deletable keep) — swizzles `restartAuthentication:` via `method_exchangeImplementations` (mirrors `SFSDKLogoutBlocker.swift`), snapshots the now-`internal` `loginHostList` for the empty-storage case, and attaches the running app's `UIScene` to the request (the migrated `showErrorAlert` skips presentation on nil scene, unlike upstream ObjC — this is why the first test run timed out; fix = set `request.scene`); wired into pbxproj as **Swift** (2 fresh IDs `E40A1C01/02…`), NOT ObjC. `NewLoginHostTests.swift` +6 validation tests (verbatim, matched pre-image). SDKCore/SmartStore/MobileSync TEST BUILD ✓ (0 errors, 0 new warnings in diff); 19 SDKCore tests PASS (10 NewLoginHost + 9 recovery, fixture-based/not live-gated). ⚠ escalation: login-UI + login-host recovery behavior + L10n — flag in PR. |
7474
| 41 | #4095 token-exchange error tests | e4bdf6397 | A | — | ✅ ported fd0fde64acb2d2a1ba828deffa7262a5449facfd | OAuth2 token-endpoint error classification tests. NEW **SFSDKOAuth2TokenExchangeErrorTests.swift** (7 tests: invalid_grant family ×3 descriptions, invalid_client_id, invalid_client, unsupported_grant_type, invalid_request, enum-mapping lock-in for the 5 wire values, success-response control) ported byte-faithful except two migration adaptations forced by the unit-38 Swift port: (1) `SFSDKOAuthTokenEndpointErrorResponse.errorCode` is the **typed `SFOAuthErrorCode` enum** in the Swift twin (was `NSInteger` in ObjC), so the assertion compares enum-to-enum (`expectedEnum`) instead of `expectedEnum.rawValue`; (2) the Swift `SFSDKOAuthTokenEndpointResponse(dictionary:parseAdditionalFields:)` init is **non-failable** and takes **`NSDictionary`** (was an implicitly-optional ObjC init), so call sites bind directly (no `guard let`) with `let params: NSDictionary`. Wired into SDKCore-test pbxproj as Swift (2 fresh IDs `E40A1C03/04…`). Test-only, SDKCore-scoped — no production change. SDKCore TEST BUILD ✓ (0 errors, 0 new warnings); 7 tests PASS. No escalation. |
7575
| 42 | #4096 SQLCipher 4.17.0 | 303013dd7 | C | ⚠⚠ dependency bump (PRE-APPROVED gate) | ✅ ported 2e54cc1c8d37293aa0514f9d9fda7fd101e06df8 | SQLCipher **4.16.0 → 4.17.0** (SQLite 3.53.1 → **3.53.3** runtime). Mechanical version bump across 7 files: **SmartStore.podspec** (`smartstore.dependency 'SQLCipher', '~> 4.17.0'`), **mobilesdk_pods.rb** (`pod 'SQLCipher', '4.17.0'`), **SmartStore pbxproj** + **MobileSyncExplorer pbxproj** (SPM `XCRemoteSwiftPackageReference` exactVersion `4.17.0`, tab-indented, single occurrence each, `plutil` clean), **SFSmartStoreTests.swift** (the COMPILED twin — `testSqliteVersion` `3.53.3`, `testSqlCipherVersion` `4.17.0 community`) + de-ref **SFSmartStoreTests.m** ref-synced verbatim (`.m` is 0-in-Sources; `.swift` twin at pbxproj line 856 is compiled), **`.claude/skills/update-sqlcipher/SKILL.md`** (byte-identical to upstream post-image `79b7335f`: `SQLLite`→`SQLite` typo + example-placeholder assert → `NEW_SQLITE_VERSION`). SPM resolved+checked-out 4.17.0; a stale precompiled-module cache (`sqlite3.h` changed under the version swap) forced a **full Build-dir wipe** (SourcePackages retained) — clean rebuild GREEN. SmartStore + MobileSync (top-of-chain) TEST BUILD ✓ 0 errors; 3 version tests PASS (runtime confirmed `4.17.0 community` / SQLite `3.53.3`). ⚠⚠ dependency bump — PRE-APPROVED (Feedback #4), still flag in PR. |
76-
| 43 | #4098 fix nil-sceneId crash on advanced-auth browser callback | e4e838863 | B | ⚠ OAuth/scene | ⬜ pending | `SFOAuthCoordinator+Internal.h/.m`, `SFSDKAuthSession.m` + test. |
76+
| 43 | #4098 fix nil-sceneId crash on advanced-auth browser callback | e4e838863 | B | ⚠ OAuth/scene | ✅ ported acf8ccf568316871a03b2a026ffa7f6816b709f6 | Advanced-auth (ASWebAuthenticationSession) browser callback crashed when the login started before any UIScene connected: `sceneId` was nil (`persistentIdentifier` nil pre-scene / weak authSession dealloc'd) and got inserted into the callback options dict. Ported into compiled Swift twins: **SFSDKAuthSession.swift** (new `static let unscopedSceneIdPrefix`; init synthesizes a unique per-session id `"<prefix><UUID>"` when `request.scene?.session.persistentIdentifier` is nil — **also fixes a latent collision the migration had**: migrated `sceneId` was a non-optional `String` defaulting to `""`, so all scene-less sessions previously shared one `authSessions[]` key), **SFOAuthCoordinator.swift** (new `browserCallbackOptions(forSceneId:) -> [AnyHashable:Any]` returns `[:]` on nil else `[UserAccountManager.IDPSceneKey: sceneId]`; callback now calls it — no more empty-string key). Helper is `internal` → visible to `@testable` (no new public API; upstream declared it in `SFOAuthCoordinator+Internal.h`). De-ref ref-sync (byte-faithful to upstream post-image, matched pre-image): `SFSDKAuthSession.m` (prefix const + synthesized `_sceneId`), `SFOAuthCoordinator.m` (`browserCallbackOptionsForSceneId:` + call site). Tombstone SKIP: `SFOAuthCoordinator+Internal.h` (migration tombstone, no `@interface` region — the Swift class holds the real decl). Tests: de-ref `SFOAuthCoordinatorTests.m` +74 ref-synced verbatim (region byte-identical to upstream), AND the 4 tests ported to the compiled Swift twin `SFOAuthCoordinatorTests.swift` (adapted to Swift surface: `SFSDKAuthSession(with:credentials:)`, `SFOAuthCoordinator(authSession:)`, `UserAccountManager.IDPSceneKey`, `sceneId.isEmpty`/`hasPrefix` since non-optional String). SDKCore/SmartStore/MobileSync TEST BUILD ✓ (0 errors, 0 NEW warnings — 2 pre-existing unrelated warnings confirmed unchanged); 6 SFOAuthCoordinatorTests PASS (4 new + 2 existing, not live-gated). ⚠ escalation: OAuth/scene callback behavior — flag in PR. |
7777
| 44 | #4087 token refresh coordinator | 6e0967833 | B+D | ⚠⚠⚠ OAuth/token (LIVE-AUTH UNBLOCKER) | ⬜ pending | 18 files: NEW `SFSDKTokenRefreshCoordinator.h/.m`, `SFOAuthErrorCode.swift`(via #4094), `SFOAuthSessionRefresher.*`, `SFIdentityCoordinator.m`, `SFRestAPI.m`, `UserAccountManager.swift`, `SFSDKOAuth2.m` + tests. **THIS unblocks the 51 SKIP-gated live-org tests → enables Phase 2.** |
7878
| 45 | #4102 improve token-refresh error handling | 19d4436ab | B | ⚠ OAuth/token | ⬜ pending | `SFRestAPI.m`, `SFSDKOAuth2.h/.m` + tests + pbxproj. |
7979
| 46 | #4105 fix iOS26 login-host classifier | b155f785d | B | ⚠ login-host | ⬜ pending | `SFOAuthCoordinator.m`, `SFSDKAuthErrorManager.m/+Internal.h` + test + pbxproj. |

.claude/upstream-sync-marker

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
303013dd7d3a05234559947ab188c6647c26c296
1+
e4e8388636da5c6f993c8bfeb68214d7f74bb331

libs/SalesforceSDKCore/SalesforceSDKCore/Classes/OAuth/SFOAuthCoordinator.m

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@ - (void)continueNativeBrowserFlowWithSharedBrowserSessionEnabled:(BOOL)shareBrow
435435
_asWebAuthenticationSession = [[ASWebAuthenticationSession alloc] initWithURL:nativeBrowserUrl callbackURLScheme:[NSURL URLWithString:self.credentials.redirectUri].scheme completionHandler:^(NSURL *callbackURL, NSError *error) {
436436
__strong typeof(weakSelf) strongSelf = weakSelf;
437437
if (!error && [[SFSDKURLHandlerManager sharedInstance] canHandleRequest:callbackURL options:nil]) {
438-
NSDictionary *options = @{kSFIDPSceneIdKey : self.authSession.sceneId};
438+
NSDictionary *options = [self browserCallbackOptionsForSceneId:self.authSession.sceneId];
439439
[[SFSDKURLHandlerManager sharedInstance] processRequest:callbackURL options:options completion:nil failure:nil];
440440
} else {
441441
[strongSelf.delegate oauthCoordinatorDidCancelBrowserAuthentication:strongSelf];
@@ -445,6 +445,13 @@ - (void)continueNativeBrowserFlowWithSharedBrowserSessionEnabled:(BOOL)shareBrow
445445
[self.delegate oauthCoordinator:self didBeginAuthenticationWithSession:_asWebAuthenticationSession];
446446
}
447447

448+
- (NSDictionary *)browserCallbackOptionsForSceneId:(nullable NSString *)sceneId {
449+
// Guard against a nil sceneId so we never insert nil into the options dictionary; omit the
450+
// key and let the URL handler fall back to the default scene. sceneId can be nil if the weak
451+
// authSession has deallocated by the time the browser callback fires.
452+
return sceneId ? @{kSFIDPSceneIdKey : sceneId} : @{};
453+
}
454+
448455
- (void)beginWebViewFlow {
449456
if (![NSThread isMainThread]) {
450457
dispatch_async(dispatch_get_main_queue(), ^{

libs/SalesforceSDKCore/SalesforceSDKCore/Classes/OAuth/SFOAuthCoordinator.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ public class SFOAuthCoordinator: NSObject, WKNavigationDelegate, WKUIDelegate {
423423
_asWebAuthenticationSession = ASWebAuthenticationSession(url: nativeBrowserUrl, callbackURLScheme: redirectScheme) { [weak self] callbackURL, error in
424424
guard let self = self else { return }
425425
if error == nil, let url = callbackURL, SFSDKURLHandlerManager.sharedInstance.canHandleRequest(url, options: nil) {
426-
let options: [AnyHashable: Any] = [UserAccountManager.IDPSceneKey: self.authSession?.sceneId ?? ""]
426+
let options = self.browserCallbackOptions(forSceneId: self.authSession?.sceneId)
427427
SFSDKURLHandlerManager.sharedInstance.processRequest(url, options: options, completion: nil, failure: nil)
428428
} else {
429429
self.delegate?.oauthCoordinatorDidCancelBrowserAuthentication(self)
@@ -435,6 +435,18 @@ public class SFOAuthCoordinator: NSObject, WKNavigationDelegate, WKUIDelegate {
435435
}
436436
}
437437

438+
/// Builds the options dictionary handed to the URL handler on the advanced-auth browser callback.
439+
/// Guards against a nil sceneId so nil is never inserted into the dictionary.
440+
/// - Parameter sceneId: The auth session's scene id, or nil if no scene was connected / the session deallocated.
441+
/// - Returns: A dictionary keyed by `UserAccountManager.IDPSceneKey` when sceneId is non-nil, or an empty dictionary otherwise.
442+
func browserCallbackOptions(forSceneId sceneId: String?) -> [AnyHashable: Any] {
443+
// Guard against a nil sceneId so we never insert nil into the options dictionary; omit the
444+
// key and let the URL handler fall back to the default scene. sceneId can be nil if the weak
445+
// authSession has deallocated by the time the browser callback fires.
446+
guard let sceneId else { return [:] }
447+
return [UserAccountManager.IDPSceneKey: sceneId]
448+
}
449+
438450
func beginWebViewFlow() {
439451
guard Thread.isMainThread else {
440452
DispatchQueue.main.async { [weak self] in

libs/SalesforceSDKCore/SalesforceSDKCore/Classes/OAuth/SFSDKAuthSession.m

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
#import "SFOAuthCoordinator+Internal.h"
3030
#import "SFIdentityCoordinator.h"
3131

32+
// Prefix for the synthesized scene id used when a login starts before any UIScene has connected.
33+
static NSString * const kSFSDKAuthSessionUnscopedSceneIdPrefix = @"com.salesforce.mobilesdk.unscopedAuthSession-";
34+
3235
@interface SFSDKAuthSession()
3336
@end
3437

@@ -47,7 +50,9 @@ -(instancetype)initWith:(SFSDKAuthRequest *)request credentials:(SFOAuthCredenti
4750
_credentials = (creds == nil) ? [self newClientCredentials] : creds;
4851
_credentials.jwt = request.jwtToken;
4952
_spAppCredentials = spAppCredentials;
50-
_sceneId = request.scene.session.persistentIdentifier; // Pass through for convenience
53+
// When no scene is connected yet, persistentIdentifier is nil; synthesize a unique per-session id
54+
// so this session gets its own authSessions[] key and the browser callback can key back to it.
55+
_sceneId = request.scene.session.persistentIdentifier ?: [kSFSDKAuthSessionUnscopedSceneIdPrefix stringByAppendingString:[[NSUUID UUID] UUIDString]];
5156
[self initCoordinator];
5257
}
5358
return self;

libs/SalesforceSDKCore/SalesforceSDKCore/Classes/OAuth/SFSDKAuthSession.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ import Foundation
2525
@objcMembers
2626
public class SFSDKAuthSession: NSObject {
2727

28+
// Prefix for the synthesized scene id used when a login starts before any UIScene has connected.
29+
static let unscopedSceneIdPrefix = "com.salesforce.mobilesdk.unscopedAuthSession-"
30+
2831
public var isAuthenticating: Bool = false
2932
public var credentials: OAuthCredentials
3033
public var oauthCoordinator: SFOAuthCoordinator
@@ -52,7 +55,9 @@ public class SFSDKAuthSession: NSObject {
5255
resolvedCredentials.setValue(request.jwtToken, forKey: "jwt")
5356
self.credentials = resolvedCredentials
5457
self.spAppCredentials = spAppCredentials
55-
self.sceneId = request.scene?.session.persistentIdentifier ?? ""
58+
// When no scene is connected yet, persistentIdentifier is nil; synthesize a unique per-session id
59+
// so this session gets its own authSessions[] key and the browser callback can key back to it.
60+
self.sceneId = request.scene?.session.persistentIdentifier ?? "\(SFSDKAuthSession.unscopedSceneIdPrefix)\(UUID().uuidString)"
5661

5762
// Temp init — coordinator requires self
5863
self.oauthCoordinator = SFOAuthCoordinator(credentials: resolvedCredentials)

0 commit comments

Comments
 (0)