From 44a7b2f315a953b3a309af0b506500295f50455d Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 18 Aug 2026 17:00:39 +0200 Subject: [PATCH 01/13] g-orchestrated: Add SDK wrapper identifier to GIDSignInPreferences --- GoogleSignIn/Sources/GIDSignInPreferences.h | 17 ++++ GoogleSignIn/Sources/GIDSignInPreferences.m | 92 ++++++++++++++++++++- 2 files changed, 106 insertions(+), 3 deletions(-) diff --git a/GoogleSignIn/Sources/GIDSignInPreferences.h b/GoogleSignIn/Sources/GIDSignInPreferences.h index 5bf45ec1..8d07dcc8 100644 --- a/GoogleSignIn/Sources/GIDSignInPreferences.h +++ b/GoogleSignIn/Sources/GIDSignInPreferences.h @@ -20,6 +20,7 @@ NS_ASSUME_NONNULL_BEGIN extern NSString *const kSDKVersionLoggingParameter; extern NSString *const kEnvironmentLoggingParameter; +extern NSString *const kSDKWrapperLoggingParameter; @interface GIDSignInPreferences : NSObject @@ -30,7 +31,23 @@ extern NSString *const kEnvironmentLoggingParameter; /// Returns the current Apple execution environment, such as `ios` or `macos`. + (NSString *)environment; +/// Returns the current SDK wrapper identifier, or `nil` if none is set. ++ (nullable NSString *)wrapperIdentifier; + +/// Sets the SDK wrapper identifier. +/// +/// A value may be up to 100 printable ASCII characters; a longer value is truncated to its first +/// 100 characters. A value that is empty, or that contains any non-ASCII or ASCII control +/// character, is dropped entirely and asserts in debug builds. +/// +/// The first accepted write wins; later differing writes are ignored. This method is thread-safe. +/// +/// @param wrapperIdentifier The identifier to report, or `nil` to reset it. ++ (void)setWrapperIdentifier:(nullable NSString *)wrapperIdentifier; + /// Returns the standard logging parameters to send with requests to Google's servers. +/// +/// The parameters are `gpsdk`, `gidenv`, and, when a wrapper identifier is set, `gidwrapper`. + (NSDictionary *)loggingParameters; + (NSString *)googleAuthorizationServer; diff --git a/GoogleSignIn/Sources/GIDSignInPreferences.m b/GoogleSignIn/Sources/GIDSignInPreferences.m index 022a76e7..1bc92616 100644 --- a/GoogleSignIn/Sources/GIDSignInPreferences.m +++ b/GoogleSignIn/Sources/GIDSignInPreferences.m @@ -14,6 +14,8 @@ #import "GoogleSignIn/Sources/GIDSignInPreferences.h" +#import + NS_ASSUME_NONNULL_BEGIN static NSString *const kLSOServer = @"accounts.google.com"; @@ -26,6 +28,14 @@ // The name of the query parameter used for logging the Apple execution environment. NSString *const kEnvironmentLoggingParameter = @"gidenv"; +// The name of the query parameter used for logging the SDK wrapper. +NSString *const kSDKWrapperLoggingParameter = @"gidwrapper"; + +static NSString *gWrapperIdentifier = nil; +static os_unfair_lock gWrapperIdentifierLock = OS_UNFAIR_LOCK_INIT; + + + // Supported Apple execution environments static NSString *const kAppleEnvironmentUnknown = @"unknown"; static NSString *const kAppleEnvironmentIOS = @"ios"; @@ -44,6 +54,38 @@ #define STR(x) STR_EXPAND(x) #define STR_EXPAND(x) #x +// Returns the sanitized form of `candidate`, or nil if it must be dropped. +// Callers must not pass nil. +static NSString * _Nullable GIDSanitizedWrapperIdentifier(NSString *candidate) { + static NSCharacterSet *allowedSet; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // The range of printable ASCII characters is U+0020 through U+007E inclusive. + allowedSet = [NSCharacterSet characterSetWithRange:NSMakeRange(0x20, 0x5F)]; + }); + + // The drop check happens before truncation: if the original string contains any character + // outside the printable ASCII range, we drop the entire value. + if ([candidate rangeOfCharacterFromSet:[allowedSet invertedSet]].location != NSNotFound) { + return nil; + } + + // An empty string is also discarded. + if (candidate.length == 0) { + return nil; + } + + // A surviving string longer than 100 characters is truncated to 100. + if (candidate.length > 100) { + // Truncating with -substringToIndex:100 is safe here precisely because the drop check has + // already guaranteed every character is single-unit ASCII, so there is no risk of splitting + // a surrogate pair. + return [candidate substringToIndex:100]; + } + + return candidate; +} + @implementation GIDSignInPreferences + (NSString *)sdkVersion { @@ -79,11 +121,55 @@ + (NSString *)environment { return appleEnvironment; } ++ (nullable NSString *)wrapperIdentifier { + os_unfair_lock_lock(&gWrapperIdentifierLock); + NSString *wrapper = [gWrapperIdentifier copy]; + os_unfair_lock_unlock(&gWrapperIdentifierLock); + return wrapper; +} + ++ (void)setWrapperIdentifier:(nullable NSString *)wrapperIdentifier { + if (wrapperIdentifier == nil) { + os_unfair_lock_lock(&gWrapperIdentifierLock); + gWrapperIdentifier = nil; + os_unfair_lock_unlock(&gWrapperIdentifierLock); + return; + } + + NSString *sanitized = GIDSanitizedWrapperIdentifier(wrapperIdentifier); + if (sanitized == nil) { +#if DEBUG + NSAssert(NO, @"SDK wrapper '%@' rejected: must not be empty and must only contain printable " + @"ASCII characters (U+0020 to U+007E). Value ignored.", wrapperIdentifier); +#endif + return; + } + + os_unfair_lock_lock(&gWrapperIdentifierLock); + NSString *current = gWrapperIdentifier; + if (current != nil && ![current isEqualToString:sanitized]) { + os_unfair_lock_unlock(&gWrapperIdentifierLock); +#if DEBUG + NSAssert(NO, @"SDK wrapper already set to '%@'; ignoring '%@'. More than one " + @"wrapper appears to be present.", current, sanitized); +#endif + return; + } + gWrapperIdentifier = [sanitized copy]; + os_unfair_lock_unlock(&gWrapperIdentifierLock); +} + + (NSDictionary *)loggingParameters { - return @{ + NSMutableDictionary *parameters = [@{ kSDKVersionLoggingParameter : [self sdkVersion], - kEnvironmentLoggingParameter : [self environment], - }; + kEnvironmentLoggingParameter : [self environment] + } mutableCopy]; + + NSString *wrapperIdentifier = [self wrapperIdentifier]; + if (wrapperIdentifier) { + parameters[kSDKWrapperLoggingParameter] = wrapperIdentifier; + } + return [parameters copy]; } + (NSString *)googleAuthorizationServer { From 0c0974946ee1f2c00258374058dd785d22df3527 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 18 Aug 2026 17:09:05 +0200 Subject: [PATCH 02/13] g-orchestrated: Expose wrapperIdentifier on GIDSignIn --- GoogleSignIn/Sources/GIDSignIn.m | 8 ++++++++ .../Sources/Public/GoogleSignIn/GIDSignIn.h | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/GoogleSignIn/Sources/GIDSignIn.m b/GoogleSignIn/Sources/GIDSignIn.m index f910e72d..d6d5c442 100644 --- a/GoogleSignIn/Sources/GIDSignIn.m +++ b/GoogleSignIn/Sources/GIDSignIn.m @@ -649,6 +649,14 @@ + (GIDSignIn *)sharedInstance { return sharedInstance; } +- (nullable NSString *)wrapperIdentifier { + return [GIDSignInPreferences wrapperIdentifier]; +} + +- (void)setWrapperIdentifier:(nullable NSString *)wrapperIdentifier { + [GIDSignInPreferences setWrapperIdentifier:wrapperIdentifier]; +} + #pragma mark - Configuring and pre-warming #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h index a6b95ead..c7995c79 100644 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h @@ -73,6 +73,26 @@ typedef NS_ERROR_ENUM(kGIDSignInErrorDomain, GIDSignInErrorCode) { /// The active configuration for this instance of `GIDSignIn`. @property(nonatomic, nullable) GIDConfiguration *configuration; +/// An optional identifier naming the SDK or wrapper that embeds Google Sign-In, +/// reported to Google as a diagnostic parameter for aggregate metrics only; it +/// is never used for authentication or authorization. +/// +/// Format: up to 100 printable ASCII characters (U+0020 to U+007E). A longer +/// value is truncated to its first 100 characters. A value containing any +/// non-ASCII character or any ASCII control character is dropped in its +/// entirety, and asserts in debug builds. +/// +/// Policy: +/// * Choose one stable name and keep it stable across your releases. +/// * Do NOT encode your version in it; per-release identifiers make aggregate +/// metrics useless. +/// * Never include anything user-specific, app-specific, or identifying. +/// * Register your identifier with Google before shipping it. +/// +/// Set this once, before your first sign-in call. The first valid value wins; +/// later differing values are ignored. +@property(nonatomic, nullable) NSString *wrapperIdentifier; + #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST /// Configures `GIDSignIn` for use. From 18ad3f2383fc14ded27208463fc68316c3c5aeb7 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 18 Aug 2026 17:14:12 +0200 Subject: [PATCH 03/13] g-orchestrated: Test gidwrapper emission and wrapper identifier validation --- GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m | 98 ++++++++++- .../Tests/Unit/GIDSignInPreferencesTest.m | 158 ++++++++++++++++++ GoogleSignIn/Tests/Unit/GIDSignInTest.m | 127 ++++++++++++++ 3 files changed, 382 insertions(+), 1 deletion(-) diff --git a/GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m b/GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m index dec1caaf..1afa6de7 100644 --- a/GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m +++ b/GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m @@ -23,6 +23,7 @@ #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDToken.h" #import "GoogleSignIn/Sources/GIDGoogleUser_Private.h" +#import "GoogleSignIn/Sources/GIDSignInPreferences.h" #import "GoogleSignIn/Tests/Unit/GIDGoogleUser+Testing.h" #import "GoogleSignIn/Tests/Unit/GIDProfileData+Testing.h" #import "GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h" @@ -67,10 +68,13 @@ @interface GIDGoogleUserTest : XCTestCase @implementation GIDGoogleUserTest { // The saved token fetch handler. OIDTokenCallback _tokenFetchHandler; + // The saved token request. + OIDTokenRequest *_savedTokenRequest; } - (void)setUp { _tokenFetchHandler = nil; + _savedTokenRequest = nil; // We need to use swizzle here because OCMock can not stub class method with arguments. [GULSwizzler swizzleClass:[OIDAuthorizationService class] @@ -80,7 +84,8 @@ - (void)setUp { OIDTokenRequest *request, OIDAuthorizationResponse *authorizationResponse, OIDTokenCallback callback) { - // Save the OIDTokenCallback. + // Save the OIDTokenRequest and OIDTokenCallback. + self->_savedTokenRequest = request; self->_tokenFetchHandler = [callback copy]; }]; } @@ -89,6 +94,7 @@ - (void)tearDown { [GULSwizzler unswizzleClass:[OIDAuthorizationService class] selector:@selector(performTokenRequest:originalAuthorizationResponse:callback:) isClassSelector:YES]; + GIDSignIn.sharedInstance.wrapperIdentifier = nil; } #pragma mark - Tests @@ -478,6 +484,96 @@ - (void)testRefreshTokensIfNeededWithCompletion_noRefresh_givenRefreshTokenExpir [self waitForExpectationsWithTimeout:1 handler:nil]; } +- (void)testWrapperIdentifier_PresentOnRefreshRequestWhenSet { + GIDSignIn.sharedInstance.wrapperIdentifier = @"firebase"; + + // Both tokens expired 10 seconds ago. + GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:-10 idTokenExpiresIn:-10]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback is called"]; + + // Save the intermediate states. + [user refreshTokensIfNeededWithCompletion:^(GIDGoogleUser * _Nullable user, + NSError * _Nullable error) { + [expectation fulfill]; + }]; + + XCTAssertEqualObjects(_savedTokenRequest.additionalParameters[@"gidwrapper"], @"firebase"); + + // Clean up the handler by providing a fake response to fulfill any internal state. + OIDTokenResponse *fakeResponse = [OIDTokenResponse testInstanceWithIDToken:nil + accessToken:kNewAccessToken + expiresIn:@(kAccessTokenExpiresIn) + refreshToken:kRefreshToken + tokenRequest:_savedTokenRequest]; + _tokenFetchHandler(fakeResponse, nil); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testWrapperIdentifier_AbsentOnRefreshRequestWhenUnset { + GIDSignIn.sharedInstance.wrapperIdentifier = nil; + + // Both tokens expired 10 seconds ago. + GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:-10 idTokenExpiresIn:-10]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback is called"]; + + // Save the intermediate states. + [user refreshTokensIfNeededWithCompletion:^(GIDGoogleUser * _Nullable user, + NSError * _Nullable error) { + [expectation fulfill]; + }]; + + XCTAssertNil(_savedTokenRequest.additionalParameters[@"gidwrapper"]); + + // Clean up the handler by providing a fake response. + OIDTokenResponse *fakeResponse = [OIDTokenResponse testInstanceWithIDToken:nil + accessToken:kNewAccessToken + expiresIn:@(kAccessTokenExpiresIn) + refreshToken:kRefreshToken + tokenRequest:_savedTokenRequest]; + _tokenFetchHandler(fakeResponse, nil); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testWrapperIdentifier_AbsentOnRefreshRequestWhenDropped { + // Assert that attempting to set a dropped identifier throws NSInternalInconsistencyException. + XCTAssertThrowsSpecificNamed(GIDSignIn.sharedInstance.wrapperIdentifier = @"firebasé", + NSException, NSInternalInconsistencyException); + + // The rejection leaves the store nil. + XCTAssertNil(GIDSignIn.sharedInstance.wrapperIdentifier); + + // Both tokens expired 10 seconds ago. + GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:-10 idTokenExpiresIn:-10]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback is called"]; + + // Save the intermediate states. + [user refreshTokensIfNeededWithCompletion:^(GIDGoogleUser * _Nullable user, + NSError * _Nullable error) { + [expectation fulfill]; + }]; + + // Assert the captured token request additionalParameters does NOT contain key @"gidwrapper". + XCTAssertNil(_savedTokenRequest.additionalParameters[@"gidwrapper"]); + + // Assert it DOES contain kSDKVersionLoggingParameter and kEnvironmentLoggingParameter. + XCTAssertEqualObjects(_savedTokenRequest.additionalParameters[kSDKVersionLoggingParameter], + [GIDSignInPreferences sdkVersion]); + XCTAssertEqualObjects(_savedTokenRequest.additionalParameters[kEnvironmentLoggingParameter], + [GIDSignInPreferences environment]); + + // Clean up the handler by providing a fake response. + OIDTokenResponse *fakeResponse = [OIDTokenResponse testInstanceWithIDToken:nil + accessToken:kNewAccessToken + expiresIn:@(kAccessTokenExpiresIn) + refreshToken:kRefreshToken + tokenRequest:_savedTokenRequest]; + _tokenFetchHandler(fakeResponse, nil); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + # pragma mark - Test `addScopes:` - (void)testAddScopes_success { diff --git a/GoogleSignIn/Tests/Unit/GIDSignInPreferencesTest.m b/GoogleSignIn/Tests/Unit/GIDSignInPreferencesTest.m index 3dff02e4..80361deb 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInPreferencesTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInPreferencesTest.m @@ -21,6 +21,11 @@ @interface GIDSignInPreferencesTest : XCTestCase @implementation GIDSignInPreferencesTest +- (void)tearDown { + [GIDSignInPreferences setWrapperIdentifier:nil]; + [super tearDown]; +} + - (void)testSDKVersion { NSString *version = [GIDSignInPreferences sdkVersion]; XCTAssertTrue([version hasPrefix:@"gid-"]); @@ -54,4 +59,157 @@ - (void)testLoggingParameters { [GIDSignInPreferences environment]); } +// Test that logging parameters include the wrapper identifier when set. +- (void)testLoggingParameters_includesWrapperWhenSet { + [GIDSignInPreferences setWrapperIdentifier:@"firebase"]; + NSDictionary *params = [GIDSignInPreferences loggingParameters]; + + XCTAssertEqual(params.count, (NSUInteger)3); + XCTAssertEqualObjects(params[kSDKWrapperLoggingParameter], @"firebase"); + XCTAssertEqualObjects(params[kSDKVersionLoggingParameter], + [GIDSignInPreferences sdkVersion]); + XCTAssertEqualObjects(params[kEnvironmentLoggingParameter], + [GIDSignInPreferences environment]); +} + +- (void)testWrapperIdentifier_UnsetIsNil { + // Test that when no identifier is set, nil is returned. + [GIDSignInPreferences setWrapperIdentifier:nil]; + XCTAssertNil([GIDSignInPreferences wrapperIdentifier]); +} + +- (void)testWrapperIdentifier_AcceptsSimpleValue { + // Test that a simple lowercase alphanumeric identifier is accepted. + [GIDSignInPreferences setWrapperIdentifier:@"firebase"]; + XCTAssertEqualObjects([GIDSignInPreferences wrapperIdentifier], @"firebase"); +} + +- (void)testWrapperIdentifier_AcceptsHyphenatedValue { + // Test that a value with internal hyphens is accepted. + [GIDSignInPreferences setWrapperIdentifier:@"react-native"]; + XCTAssertEqualObjects([GIDSignInPreferences wrapperIdentifier], @"react-native"); +} + +- (void)testWrapperIdentifier_AcceptsDigits { + // Test that a value with digits is accepted. + [GIDSignInPreferences setWrapperIdentifier:@"wrapper2"]; + XCTAssertEqualObjects([GIDSignInPreferences wrapperIdentifier], @"wrapper2"); +} + +- (void)testWrapperIdentifier_AcceptsMaximumLength { + // Test that a 100-character valid identifier is accepted and not truncated. + NSString *maxLength = [@"a" stringByPaddingToLength:100 withString:@"a" startingAtIndex:0]; + [GIDSignInPreferences setWrapperIdentifier:maxLength]; + XCTAssertEqual([GIDSignInPreferences wrapperIdentifier].length, (NSUInteger)100); + XCTAssertEqualObjects([GIDSignInPreferences wrapperIdentifier], maxLength); +} + +- (void)testWrapperIdentifier_AcceptsMixedCaseSpacesAndPunctuation { + // Test that mixed case, spaces and punctuation are accepted. + NSString *value = @"React Native SDK (v2.0)"; + [GIDSignInPreferences setWrapperIdentifier:value]; + XCTAssertEqualObjects([GIDSignInPreferences wrapperIdentifier], value); +} + +- (void)testWrapperIdentifier_DropsEmptyString { + // Test that an empty string is dropped. + XCTAssertThrowsSpecificNamed([GIDSignInPreferences setWrapperIdentifier:@""], + NSException, + NSInternalInconsistencyException); + XCTAssertNil([GIDSignInPreferences wrapperIdentifier]); +} + +- (void)testWrapperIdentifier_FirstValidWriteWins { + // Test that the first valid write is persistent and subsequent differing writes are rejected. + [GIDSignInPreferences setWrapperIdentifier:@"first"]; + XCTAssertThrowsSpecificNamed([GIDSignInPreferences setWrapperIdentifier:@"second"], + NSException, + NSInternalInconsistencyException); + XCTAssertEqualObjects([GIDSignInPreferences wrapperIdentifier], @"first"); +} + +- (void)testWrapperIdentifier_RepeatedIdenticalWriteIsAccepted { + // Test that writing the same valid value again does not throw or change the state. + [GIDSignInPreferences setWrapperIdentifier:@"firebase"]; + XCTAssertNoThrow([GIDSignInPreferences setWrapperIdentifier:@"firebase"]); + XCTAssertEqualObjects([GIDSignInPreferences wrapperIdentifier], @"firebase"); +} + +- (void)testWrapperIdentifier_NilResets { + // Test that passing nil resets the store, allowing a new first-write. + [GIDSignInPreferences setWrapperIdentifier:@"firebase"]; + [GIDSignInPreferences setWrapperIdentifier:nil]; + XCTAssertNil([GIDSignInPreferences wrapperIdentifier]); + + [GIDSignInPreferences setWrapperIdentifier:@"second"]; + XCTAssertEqualObjects([GIDSignInPreferences wrapperIdentifier], @"second"); +} + +- (void)testWrapperIdentifier_DroppedWriteLeavesPreviousValue { + // Test that a dropped write does not clear or change a previously set valid value. + [GIDSignInPreferences setWrapperIdentifier:@"firebase"]; + XCTAssertThrowsSpecificNamed([GIDSignInPreferences setWrapperIdentifier:@"firebasé"], + NSException, + NSInternalInconsistencyException); + XCTAssertEqualObjects([GIDSignInPreferences wrapperIdentifier], @"firebase"); +} + +- (void)testWrapperIdentifier_TruncatesOverLongValue { + // Test that a legal string longer than 100 characters is truncated to its first 100 characters. + NSString *overLong = [@"a" stringByPaddingToLength:150 withString:@"a" startingAtIndex:0]; + XCTAssertNoThrow([GIDSignInPreferences setWrapperIdentifier:overLong]); + XCTAssertEqual([GIDSignInPreferences wrapperIdentifier].length, (NSUInteger)100); + XCTAssertEqualObjects([GIDSignInPreferences wrapperIdentifier], [overLong substringToIndex:100]); +} + +- (void)testWrapperIdentifier_DropsNonASCII { + // Test that a value containing a non-ASCII character throws and leaves the store nil. + XCTAssertThrowsSpecificNamed([GIDSignInPreferences setWrapperIdentifier:@"firebasé"], + NSException, + NSInternalInconsistencyException); + XCTAssertNil([GIDSignInPreferences wrapperIdentifier]); +} + +- (void)testWrapperIdentifier_DropsControlCharacters { + // Test that values containing ASCII control characters throw and leave the store nil. + XCTAssertThrowsSpecificNamed([GIDSignInPreferences setWrapperIdentifier:@"fire\nbase"], + NSException, + NSInternalInconsistencyException); + XCTAssertNil([GIDSignInPreferences wrapperIdentifier]); + + [GIDSignInPreferences setWrapperIdentifier:nil]; + XCTAssertThrowsSpecificNamed([GIDSignInPreferences setWrapperIdentifier:@"fire\tbase"], + NSException, + NSInternalInconsistencyException); + XCTAssertNil([GIDSignInPreferences wrapperIdentifier]); + + [GIDSignInPreferences setWrapperIdentifier:nil]; + NSString *del = [NSString stringWithFormat:@"fire%Cbase", (unichar)0x7F]; + XCTAssertThrowsSpecificNamed([GIDSignInPreferences setWrapperIdentifier:del], + NSException, + NSInternalInconsistencyException); + XCTAssertNil([GIDSignInPreferences wrapperIdentifier]); +} + +- (void)testWrapperIdentifier_DropsWhenDisallowedCharacterIsPastTruncationPoint { + // Test that the drop check deliberately runs on the untruncated string so a payload hidden + // past the truncation point cannot survive. + NSString *prefix = [@"a" stringByPaddingToLength:120 withString:@"a" startingAtIndex:0]; + NSString *overLong = [prefix stringByReplacingCharactersInRange:NSMakeRange(110, 1) + withString:@"é"]; + XCTAssertThrowsSpecificNamed([GIDSignInPreferences setWrapperIdentifier:overLong], + NSException, + NSInternalInconsistencyException); + XCTAssertNil([GIDSignInPreferences wrapperIdentifier]); +} + +- (void)testWrapperIdentifier_WriteOnceComparesSanitizedValue { + // Test that the write-once check compares the sanitized value, allowing a repeated + // write of a value that truncates to the same result. + NSString *overLong = [@"a" stringByPaddingToLength:150 withString:@"a" startingAtIndex:0]; + [GIDSignInPreferences setWrapperIdentifier:overLong]; + XCTAssertNoThrow([GIDSignInPreferences setWrapperIdentifier:overLong]); + XCTAssertEqualObjects([GIDSignInPreferences wrapperIdentifier], [overLong substringToIndex:100]); +} + @end diff --git a/GoogleSignIn/Tests/Unit/GIDSignInTest.m b/GoogleSignIn/Tests/Unit/GIDSignInTest.m index a98cdf25..3d7d0cd9 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInTest.m @@ -389,6 +389,7 @@ - (void)tearDown { [_testUserDefaults removePersistentDomainForName:kUserDefaultsSuiteName]; [_fakeMainBundle stopFaking]; + GIDSignIn.sharedInstance.wrapperIdentifier = nil; [super tearDown]; } @@ -1168,6 +1169,121 @@ - (void)testOAuthLogin_HostedDomain { XCTAssertEqualObjects(params[@"hd"], kHostedDomain, @"hosted domain should match"); } +- (void)testWrapperIdentifier_PresentInAuthorizationRequestWhenSet { + GIDSignIn.sharedInstance.wrapperIdentifier = @"firebase"; + OCMStub( + [_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef] + ).andDo(^(NSInvocation *invocation) { + self->_keychainSaved = self->_saveAuthorizationReturnValue; + }); + + [self OAuthLoginWithAddScopesFlow:NO + authError:nil + tokenError:nil + emmPasscodeInfoRequired:NO + claimsAsJSONRequired:NO + keychainError:NO + restoredSignIn:NO + oldAccessToken:NO + modalCancel:NO]; + + NSDictionary *params = _savedAuthorizationRequest.additionalParameters; + XCTAssertEqualObjects(params[@"gidwrapper"], @"firebase", + @"The authorization request should contain the 'gidwrapper' parameter " + "when set."); +} + +- (void)testWrapperIdentifier_AbsentFromAuthorizationRequestWhenUnset { + GIDSignIn.sharedInstance.wrapperIdentifier = nil; + OCMStub( + [_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef] + ).andDo(^(NSInvocation *invocation) { + self->_keychainSaved = self->_saveAuthorizationReturnValue; + }); + + [self OAuthLoginWithAddScopesFlow:NO + authError:nil + tokenError:nil + emmPasscodeInfoRequired:NO + claimsAsJSONRequired:NO + keychainError:NO + restoredSignIn:NO + oldAccessToken:NO + modalCancel:NO]; + + NSDictionary *params = _savedAuthorizationRequest.additionalParameters; + XCTAssertNil(params[@"gidwrapper"], + @"The authorization request should not contain the 'gidwrapper' parameter " + "when unset."); +} + +- (void)testWrapperIdentifier_DroppedValueIsIgnored { + XCTAssertThrowsSpecificNamed(GIDSignIn.sharedInstance.wrapperIdentifier = @"firebasé", + NSException, NSInternalInconsistencyException, + @"Setting a dropped wrapper identifier should throw."); + XCTAssertNil(GIDSignIn.sharedInstance.wrapperIdentifier, + @"The wrapper identifier should be nil after a dropped assignment."); +} + +- (void)testWrapperIdentifier_PresentOnRevokeURL { + GIDSignIn.sharedInstance.wrapperIdentifier = @"my-sdk"; + + [[[_authorization expect] andReturn:_authState] authState]; + [[[_authState expect] andReturn:_tokenResponse] lastTokenResponse]; + [[[_tokenResponse expect] andReturn:kAccessToken] accessToken]; + [[[_authorization expect] andReturn:_fetcherService] fetcherService]; + + [_signIn disconnectWithCompletion:nil]; + + XCTAssertTrue([self isFetcherStarted], @"should start fetching"); + NSURL *url = [self fetchedURL]; + NSURLComponents *components = [NSURLComponents componentsWithURL:url + resolvingAgainstBaseURL:NO]; + NSArray *queryItems = components.queryItems; + + XCTAssertEqualObjects([self valueForQueryItemName:@"gidwrapper" inArray:queryItems], + @"my-sdk", @"The revoke URL should contain the 'gidwrapper' parameter."); + XCTAssertEqualObjects([self valueForQueryItemName:kSDKVersionLoggingParameter inArray:queryItems], + [GIDSignInPreferences sdkVersion], + @"The revoke URL should contain the SDK version parameter."); + XCTAssertEqualObjects([self valueForQueryItemName:kEnvironmentLoggingParameter + inArray:queryItems], + [GIDSignInPreferences environment], + @"The revoke URL should contain the environment parameter."); + XCTAssertEqualObjects([self valueForQueryItemName:@"token" inArray:queryItems], + kAccessToken, @"The revoke URL should contain the 'token' parameter."); +} + +- (void)testWrapperIdentifier_AbsentFromRevokeURLWhenUnset { + GIDSignIn.sharedInstance.wrapperIdentifier = nil; + + [[[_authorization expect] andReturn:_authState] authState]; + [[[_authState expect] andReturn:_tokenResponse] lastTokenResponse]; + [[[_tokenResponse expect] andReturn:kAccessToken] accessToken]; + [[[_authorization expect] andReturn:_fetcherService] fetcherService]; + + [_signIn disconnectWithCompletion:nil]; + + XCTAssertTrue([self isFetcherStarted], @"should start fetching"); + NSURL *url = [self fetchedURL]; + NSURLComponents *components = [NSURLComponents componentsWithURL:url + resolvingAgainstBaseURL:NO]; + NSArray *queryItems = components.queryItems; + + XCTAssertNil([self valueForQueryItemName:@"gidwrapper" inArray:queryItems], + @"The revoke URL should not contain the 'gidwrapper' parameter when unset."); + XCTAssertEqualObjects([self valueForQueryItemName:kSDKVersionLoggingParameter inArray:queryItems], + [GIDSignInPreferences sdkVersion], + @"The revoke URL should still contain the SDK version parameter."); + XCTAssertEqualObjects([self valueForQueryItemName:kEnvironmentLoggingParameter + inArray:queryItems], + [GIDSignInPreferences environment], + @"The revoke URL should still contain the environment parameter."); + XCTAssertEqualObjects([self valueForQueryItemName:@"token" inArray:queryItems], + kAccessToken, + @"The revoke URL should still contain the 'token' parameter."); +} + - (void)testOAuthLogin_ConsentCanceled { [self OAuthLoginWithAddScopesFlow:NO authError:@"access_denied" @@ -1779,6 +1895,17 @@ - (void)testTokenEndpointEMMError { #pragma mark - Helpers +// Returns the value for the query item with the given name in the array of query items. +- (nullable NSString *)valueForQueryItemName:(NSString *)name + inArray:(NSArray *)queryItems { + for (NSURLQueryItem *item in queryItems) { + if ([item.name isEqualToString:name]) { + return item.value; + } + } + return nil; +} + // Whether or not a fetcher has been started. - (BOOL)isFetcherStarted { NSUInteger count = _fetcherService.fetchers.count; From d3e09a218be5e91a06aadbde59a6300d52c3e402 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 18 Aug 2026 17:17:54 +0200 Subject: [PATCH 04/13] g-orchestrated: Changelog: add wrapperIdentifier / gidwrapper parameter --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 180da5dd..13b13ffe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# Unreleased +- Add `GIDSignIn.wrapperIdentifier`, an optional property for SDKs embedding Google Sign-In to self-identify in Google's diagnostic logs via a new `gidwrapper` parameter. It accepts up to 100 printable ASCII characters; longer values are truncated, and values containing non-ASCII or control characters are dropped. It is opt-in and default behavior is unchanged. + # 9.2.0 - Expose the refresh token expiration date ([#577](https://github.com/google/GoogleSignIn-iOS/pull/577)) - Support requesting the `amr` (Authentication Methods References) claim ([#600](https://github.com/google/GoogleSignIn-iOS/pull/600)) From 046a58e88362c731a605ca8aec655c961b306979 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:46:45 +0200 Subject: [PATCH 05/13] g-orchestrated: Ignore nil wrapper writes and log instead of asserting --- GoogleSignIn/Sources/GIDSignInPreferences.h | 13 +++++++++-- GoogleSignIn/Sources/GIDSignInPreferences.m | 24 ++++++++++----------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/GoogleSignIn/Sources/GIDSignInPreferences.h b/GoogleSignIn/Sources/GIDSignInPreferences.h index 8d07dcc8..e1d77841 100644 --- a/GoogleSignIn/Sources/GIDSignInPreferences.h +++ b/GoogleSignIn/Sources/GIDSignInPreferences.h @@ -38,13 +38,22 @@ extern NSString *const kSDKWrapperLoggingParameter; /// /// A value may be up to 100 printable ASCII characters; a longer value is truncated to its first /// 100 characters. A value that is empty, or that contains any non-ASCII or ASCII control -/// character, is dropped entirely and asserts in debug builds. +/// character, is dropped entirely. +/// +/// A nil argument is ignored. A value that is rejected, or that conflicts with an already-stored +/// value, is logged rather than asserted. /// /// The first accepted write wins; later differing writes are ignored. This method is thread-safe. /// -/// @param wrapperIdentifier The identifier to report, or `nil` to reset it. +/// @param wrapperIdentifier The identifier to report. + (void)setWrapperIdentifier:(nullable NSString *)wrapperIdentifier; +/// Resets the SDK wrapper identifier. +/// +/// This exists solely so unit tests can restore process state between cases. Production code +/// must not call it. ++ (void)resetWrapperIdentifierForTesting; + /// Returns the standard logging parameters to send with requests to Google's servers. /// /// The parameters are `gpsdk`, `gidenv`, and, when a wrapper identifier is set, `gidwrapper`. diff --git a/GoogleSignIn/Sources/GIDSignInPreferences.m b/GoogleSignIn/Sources/GIDSignInPreferences.m index 1bc92616..d7783108 100644 --- a/GoogleSignIn/Sources/GIDSignInPreferences.m +++ b/GoogleSignIn/Sources/GIDSignInPreferences.m @@ -34,8 +34,6 @@ static NSString *gWrapperIdentifier = nil; static os_unfair_lock gWrapperIdentifierLock = OS_UNFAIR_LOCK_INIT; - - // Supported Apple execution environments static NSString *const kAppleEnvironmentUnknown = @"unknown"; static NSString *const kAppleEnvironmentIOS = @"ios"; @@ -130,18 +128,14 @@ + (nullable NSString *)wrapperIdentifier { + (void)setWrapperIdentifier:(nullable NSString *)wrapperIdentifier { if (wrapperIdentifier == nil) { - os_unfair_lock_lock(&gWrapperIdentifierLock); - gWrapperIdentifier = nil; - os_unfair_lock_unlock(&gWrapperIdentifierLock); return; } NSString *sanitized = GIDSanitizedWrapperIdentifier(wrapperIdentifier); if (sanitized == nil) { -#if DEBUG - NSAssert(NO, @"SDK wrapper '%@' rejected: must not be empty and must only contain printable " - @"ASCII characters (U+0020 to U+007E). Value ignored.", wrapperIdentifier); -#endif + NSLog(@"[Google Sign-In iOS]: the SDK wrapper identifier '%@' was ignored, because it must be " + "non-empty and contain only printable ASCII characters (U+0020 to U+007E).", + wrapperIdentifier); return; } @@ -149,16 +143,20 @@ + (void)setWrapperIdentifier:(nullable NSString *)wrapperIdentifier { NSString *current = gWrapperIdentifier; if (current != nil && ![current isEqualToString:sanitized]) { os_unfair_lock_unlock(&gWrapperIdentifierLock); -#if DEBUG - NSAssert(NO, @"SDK wrapper already set to '%@'; ignoring '%@'. More than one " - @"wrapper appears to be present.", current, sanitized); -#endif + NSLog(@"[Google Sign-In iOS]: the SDK wrapper identifier is already set to '%@', so '%@' was " + "ignored; more than one wrapper appears to be present.", current, sanitized); return; } gWrapperIdentifier = [sanitized copy]; os_unfair_lock_unlock(&gWrapperIdentifierLock); } ++ (void)resetWrapperIdentifierForTesting { + os_unfair_lock_lock(&gWrapperIdentifierLock); + gWrapperIdentifier = nil; + os_unfair_lock_unlock(&gWrapperIdentifierLock); +} + + (NSDictionary *)loggingParameters { NSMutableDictionary *parameters = [@{ kSDKVersionLoggingParameter : [self sdkVersion], From 5d587a2380112bd1622cddcb6ceefa6e0cac00a8 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:47:00 +0200 Subject: [PATCH 06/13] g-orchestrated: Make wrapperIdentifier a copy class property --- GoogleSignIn/Sources/GIDSignIn.m | 4 +-- .../Sources/Public/GoogleSignIn/GIDSignIn.h | 25 +++++++++---------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/GoogleSignIn/Sources/GIDSignIn.m b/GoogleSignIn/Sources/GIDSignIn.m index d6d5c442..abbb803e 100644 --- a/GoogleSignIn/Sources/GIDSignIn.m +++ b/GoogleSignIn/Sources/GIDSignIn.m @@ -649,11 +649,11 @@ + (GIDSignIn *)sharedInstance { return sharedInstance; } -- (nullable NSString *)wrapperIdentifier { ++ (nullable NSString *)wrapperIdentifier { return [GIDSignInPreferences wrapperIdentifier]; } -- (void)setWrapperIdentifier:(nullable NSString *)wrapperIdentifier { ++ (void)setWrapperIdentifier:(nullable NSString *)wrapperIdentifier { [GIDSignInPreferences setWrapperIdentifier:wrapperIdentifier]; } diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h index c7995c79..823285bb 100644 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h @@ -73,25 +73,24 @@ typedef NS_ERROR_ENUM(kGIDSignInErrorDomain, GIDSignInErrorCode) { /// The active configuration for this instance of `GIDSignIn`. @property(nonatomic, nullable) GIDConfiguration *configuration; -/// An optional identifier naming the SDK or wrapper that embeds Google Sign-In, -/// reported to Google as a diagnostic parameter for aggregate metrics only; it -/// is never used for authentication or authorization. +/// An optional identifier naming the SDK or wrapper that embeds Google Sign-In, reported to +/// Google as a diagnostic parameter for aggregate metrics only; it is never used for +/// authentication or authorization. /// -/// Format: up to 100 printable ASCII characters (U+0020 to U+007E). A longer -/// value is truncated to its first 100 characters. A value containing any -/// non-ASCII character or any ASCII control character is dropped in its -/// entirety, and asserts in debug builds. +/// Format: up to 100 printable ASCII characters (U+0020 to U+007E). A longer value is +/// truncated to its first 100 characters. A value that is empty, or that contains any +/// non-ASCII character or any ASCII control character, is ignored in its entirety and logged. /// /// Policy: /// * Choose one stable name and keep it stable across your releases. -/// * Do NOT encode your version in it; per-release identifiers make aggregate -/// metrics useless. +/// * Do NOT encode your version in it; per-release identifiers make aggregate metrics +/// useless, and a value that has shipped cannot be retracted from Google's logs. /// * Never include anything user-specific, app-specific, or identifying. -/// * Register your identifier with Google before shipping it. /// -/// Set this once, before your first sign-in call. The first valid value wins; -/// later differing values are ignored. -@property(nonatomic, nullable) NSString *wrapperIdentifier; +/// Set this once, before your first sign-in call. The first accepted value wins: later +/// differing values are ignored and logged, and setting `nil` does not clear it. Reading this +/// property returns the value in effect, which is `nil` until a value has been accepted. +@property(class, nonatomic, nullable, copy) NSString *wrapperIdentifier; #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST From 9d4641ca92659902c5b3f0ef01766b62d384a5b0 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:47:18 +0200 Subject: [PATCH 07/13] g-orchestrated: Update wrapperIdentifier tests for logging and class property --- GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m | 13 +++-- .../Tests/Unit/GIDSignInPreferencesTest.m | 52 +++++++------------ GoogleSignIn/Tests/Unit/GIDSignInTest.m | 17 +++--- 3 files changed, 32 insertions(+), 50 deletions(-) diff --git a/GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m b/GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m index 1afa6de7..223e050d 100644 --- a/GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m +++ b/GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m @@ -94,7 +94,7 @@ - (void)tearDown { [GULSwizzler unswizzleClass:[OIDAuthorizationService class] selector:@selector(performTokenRequest:originalAuthorizationResponse:callback:) isClassSelector:YES]; - GIDSignIn.sharedInstance.wrapperIdentifier = nil; + [GIDSignInPreferences resetWrapperIdentifierForTesting]; } #pragma mark - Tests @@ -485,7 +485,7 @@ - (void)testRefreshTokensIfNeededWithCompletion_noRefresh_givenRefreshTokenExpir } - (void)testWrapperIdentifier_PresentOnRefreshRequestWhenSet { - GIDSignIn.sharedInstance.wrapperIdentifier = @"firebase"; + GIDSignIn.wrapperIdentifier = @"firebase"; // Both tokens expired 10 seconds ago. GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:-10 idTokenExpiresIn:-10]; @@ -511,7 +511,7 @@ - (void)testWrapperIdentifier_PresentOnRefreshRequestWhenSet { } - (void)testWrapperIdentifier_AbsentOnRefreshRequestWhenUnset { - GIDSignIn.sharedInstance.wrapperIdentifier = nil; + [GIDSignInPreferences resetWrapperIdentifierForTesting]; // Both tokens expired 10 seconds ago. GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:-10 idTokenExpiresIn:-10]; @@ -537,12 +537,11 @@ - (void)testWrapperIdentifier_AbsentOnRefreshRequestWhenUnset { } - (void)testWrapperIdentifier_AbsentOnRefreshRequestWhenDropped { - // Assert that attempting to set a dropped identifier throws NSInternalInconsistencyException. - XCTAssertThrowsSpecificNamed(GIDSignIn.sharedInstance.wrapperIdentifier = @"firebasé", - NSException, NSInternalInconsistencyException); + // Assert that attempting to set a dropped identifier is ignored. + XCTAssertNoThrow(GIDSignIn.wrapperIdentifier = @"firebasé"); // The rejection leaves the store nil. - XCTAssertNil(GIDSignIn.sharedInstance.wrapperIdentifier); + XCTAssertNil(GIDSignIn.wrapperIdentifier); // Both tokens expired 10 seconds ago. GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:-10 idTokenExpiresIn:-10]; diff --git a/GoogleSignIn/Tests/Unit/GIDSignInPreferencesTest.m b/GoogleSignIn/Tests/Unit/GIDSignInPreferencesTest.m index 80361deb..924ae0ec 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInPreferencesTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInPreferencesTest.m @@ -22,7 +22,7 @@ @interface GIDSignInPreferencesTest : XCTestCase @implementation GIDSignInPreferencesTest - (void)tearDown { - [GIDSignInPreferences setWrapperIdentifier:nil]; + [GIDSignInPreferences resetWrapperIdentifierForTesting]; [super tearDown]; } @@ -74,7 +74,7 @@ - (void)testLoggingParameters_includesWrapperWhenSet { - (void)testWrapperIdentifier_UnsetIsNil { // Test that when no identifier is set, nil is returned. - [GIDSignInPreferences setWrapperIdentifier:nil]; + [GIDSignInPreferences resetWrapperIdentifierForTesting]; XCTAssertNil([GIDSignInPreferences wrapperIdentifier]); } @@ -113,18 +113,14 @@ - (void)testWrapperIdentifier_AcceptsMixedCaseSpacesAndPunctuation { - (void)testWrapperIdentifier_DropsEmptyString { // Test that an empty string is dropped. - XCTAssertThrowsSpecificNamed([GIDSignInPreferences setWrapperIdentifier:@""], - NSException, - NSInternalInconsistencyException); + XCTAssertNoThrow([GIDSignInPreferences setWrapperIdentifier:@""]); XCTAssertNil([GIDSignInPreferences wrapperIdentifier]); } - (void)testWrapperIdentifier_FirstValidWriteWins { // Test that the first valid write is persistent and subsequent differing writes are rejected. [GIDSignInPreferences setWrapperIdentifier:@"first"]; - XCTAssertThrowsSpecificNamed([GIDSignInPreferences setWrapperIdentifier:@"second"], - NSException, - NSInternalInconsistencyException); + XCTAssertNoThrow([GIDSignInPreferences setWrapperIdentifier:@"second"]); XCTAssertEqualObjects([GIDSignInPreferences wrapperIdentifier], @"first"); } @@ -135,22 +131,20 @@ - (void)testWrapperIdentifier_RepeatedIdenticalWriteIsAccepted { XCTAssertEqualObjects([GIDSignInPreferences wrapperIdentifier], @"firebase"); } -- (void)testWrapperIdentifier_NilResets { - // Test that passing nil resets the store, allowing a new first-write. +- (void)testWrapperIdentifier_NilIsIgnored { + // Test that passing nil is ignored and does not reset the store. [GIDSignInPreferences setWrapperIdentifier:@"firebase"]; [GIDSignInPreferences setWrapperIdentifier:nil]; - XCTAssertNil([GIDSignInPreferences wrapperIdentifier]); + XCTAssertEqualObjects([GIDSignInPreferences wrapperIdentifier], @"firebase"); [GIDSignInPreferences setWrapperIdentifier:@"second"]; - XCTAssertEqualObjects([GIDSignInPreferences wrapperIdentifier], @"second"); + XCTAssertEqualObjects([GIDSignInPreferences wrapperIdentifier], @"firebase"); } - (void)testWrapperIdentifier_DroppedWriteLeavesPreviousValue { // Test that a dropped write does not clear or change a previously set valid value. [GIDSignInPreferences setWrapperIdentifier:@"firebase"]; - XCTAssertThrowsSpecificNamed([GIDSignInPreferences setWrapperIdentifier:@"firebasé"], - NSException, - NSInternalInconsistencyException); + XCTAssertNoThrow([GIDSignInPreferences setWrapperIdentifier:@"firebasé"]); XCTAssertEqualObjects([GIDSignInPreferences wrapperIdentifier], @"firebase"); } @@ -163,31 +157,23 @@ - (void)testWrapperIdentifier_TruncatesOverLongValue { } - (void)testWrapperIdentifier_DropsNonASCII { - // Test that a value containing a non-ASCII character throws and leaves the store nil. - XCTAssertThrowsSpecificNamed([GIDSignInPreferences setWrapperIdentifier:@"firebasé"], - NSException, - NSInternalInconsistencyException); + // Test that a value containing a non-ASCII character is ignored and leaves the store nil. + XCTAssertNoThrow([GIDSignInPreferences setWrapperIdentifier:@"firebasé"]); XCTAssertNil([GIDSignInPreferences wrapperIdentifier]); } - (void)testWrapperIdentifier_DropsControlCharacters { - // Test that values containing ASCII control characters throw and leave the store nil. - XCTAssertThrowsSpecificNamed([GIDSignInPreferences setWrapperIdentifier:@"fire\nbase"], - NSException, - NSInternalInconsistencyException); + // Test that values containing ASCII control characters are ignored and leave the store nil. + XCTAssertNoThrow([GIDSignInPreferences setWrapperIdentifier:@"fire\nbase"]); XCTAssertNil([GIDSignInPreferences wrapperIdentifier]); - [GIDSignInPreferences setWrapperIdentifier:nil]; - XCTAssertThrowsSpecificNamed([GIDSignInPreferences setWrapperIdentifier:@"fire\tbase"], - NSException, - NSInternalInconsistencyException); + [GIDSignInPreferences resetWrapperIdentifierForTesting]; + XCTAssertNoThrow([GIDSignInPreferences setWrapperIdentifier:@"fire\tbase"]); XCTAssertNil([GIDSignInPreferences wrapperIdentifier]); - [GIDSignInPreferences setWrapperIdentifier:nil]; + [GIDSignInPreferences resetWrapperIdentifierForTesting]; NSString *del = [NSString stringWithFormat:@"fire%Cbase", (unichar)0x7F]; - XCTAssertThrowsSpecificNamed([GIDSignInPreferences setWrapperIdentifier:del], - NSException, - NSInternalInconsistencyException); + XCTAssertNoThrow([GIDSignInPreferences setWrapperIdentifier:del]); XCTAssertNil([GIDSignInPreferences wrapperIdentifier]); } @@ -197,9 +183,7 @@ - (void)testWrapperIdentifier_DropsWhenDisallowedCharacterIsPastTruncationPoint NSString *prefix = [@"a" stringByPaddingToLength:120 withString:@"a" startingAtIndex:0]; NSString *overLong = [prefix stringByReplacingCharactersInRange:NSMakeRange(110, 1) withString:@"é"]; - XCTAssertThrowsSpecificNamed([GIDSignInPreferences setWrapperIdentifier:overLong], - NSException, - NSInternalInconsistencyException); + XCTAssertNoThrow([GIDSignInPreferences setWrapperIdentifier:overLong]); XCTAssertNil([GIDSignInPreferences wrapperIdentifier]); } diff --git a/GoogleSignIn/Tests/Unit/GIDSignInTest.m b/GoogleSignIn/Tests/Unit/GIDSignInTest.m index 3d7d0cd9..3bfb2805 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInTest.m @@ -389,7 +389,7 @@ - (void)tearDown { [_testUserDefaults removePersistentDomainForName:kUserDefaultsSuiteName]; [_fakeMainBundle stopFaking]; - GIDSignIn.sharedInstance.wrapperIdentifier = nil; + [GIDSignInPreferences resetWrapperIdentifierForTesting]; [super tearDown]; } @@ -1170,7 +1170,7 @@ - (void)testOAuthLogin_HostedDomain { } - (void)testWrapperIdentifier_PresentInAuthorizationRequestWhenSet { - GIDSignIn.sharedInstance.wrapperIdentifier = @"firebase"; + GIDSignIn.wrapperIdentifier = @"firebase"; OCMStub( [_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef] ).andDo(^(NSInvocation *invocation) { @@ -1194,7 +1194,7 @@ - (void)testWrapperIdentifier_PresentInAuthorizationRequestWhenSet { } - (void)testWrapperIdentifier_AbsentFromAuthorizationRequestWhenUnset { - GIDSignIn.sharedInstance.wrapperIdentifier = nil; + [GIDSignInPreferences resetWrapperIdentifierForTesting]; OCMStub( [_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef] ).andDo(^(NSInvocation *invocation) { @@ -1218,15 +1218,14 @@ - (void)testWrapperIdentifier_AbsentFromAuthorizationRequestWhenUnset { } - (void)testWrapperIdentifier_DroppedValueIsIgnored { - XCTAssertThrowsSpecificNamed(GIDSignIn.sharedInstance.wrapperIdentifier = @"firebasé", - NSException, NSInternalInconsistencyException, - @"Setting a dropped wrapper identifier should throw."); - XCTAssertNil(GIDSignIn.sharedInstance.wrapperIdentifier, + XCTAssertNoThrow(GIDSignIn.wrapperIdentifier = @"firebasé", + @"Setting a dropped wrapper identifier should be ignored."); + XCTAssertNil(GIDSignIn.wrapperIdentifier, @"The wrapper identifier should be nil after a dropped assignment."); } - (void)testWrapperIdentifier_PresentOnRevokeURL { - GIDSignIn.sharedInstance.wrapperIdentifier = @"my-sdk"; + GIDSignIn.wrapperIdentifier = @"my-sdk"; [[[_authorization expect] andReturn:_authState] authState]; [[[_authState expect] andReturn:_tokenResponse] lastTokenResponse]; @@ -1255,7 +1254,7 @@ - (void)testWrapperIdentifier_PresentOnRevokeURL { } - (void)testWrapperIdentifier_AbsentFromRevokeURLWhenUnset { - GIDSignIn.sharedInstance.wrapperIdentifier = nil; + [GIDSignInPreferences resetWrapperIdentifierForTesting]; [[[_authorization expect] andReturn:_authState] authState]; [[[_authState expect] andReturn:_tokenResponse] lastTokenResponse]; From f75d076705b4a3393cf332a1c53b31af05c9b15f Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:47:34 +0200 Subject: [PATCH 08/13] g-orchestrated: Trim the wrapperIdentifier changelog entry to one line --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13b13ffe..37c4d352 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ # Unreleased -- Add `GIDSignIn.wrapperIdentifier`, an optional property for SDKs embedding Google Sign-In to self-identify in Google's diagnostic logs via a new `gidwrapper` parameter. It accepts up to 100 printable ASCII characters; longer values are truncated, and values containing non-ASCII or control characters are dropped. It is opt-in and default behavior is unchanged. +- Add `GIDSignIn.wrapperIdentifier` so SDKs that embed Google Sign-In can self-identify in Google's diagnostic logs via a new `gidwrapper` parameter. It is opt-in and default behavior is unchanged. # 9.2.0 - Expose the refresh token expiration date ([#577](https://github.com/google/GoogleSignIn-iOS/pull/577)) From 51e8d8b3f2535b8f7f46f0daa63ed4549eaa4ca2 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 18 Aug 2026 22:45:19 +0200 Subject: [PATCH 09/13] g-orchestrated: Rename resetWrapperIdentifierForTesting to resetWrapperIdentifier --- GoogleSignIn/Sources/GIDSignInPreferences.h | 15 +++++++++------ GoogleSignIn/Sources/GIDSignInPreferences.m | 2 +- GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m | 4 ++-- .../Tests/Unit/GIDSignInPreferencesTest.m | 8 ++++---- GoogleSignIn/Tests/Unit/GIDSignInTest.m | 6 +++--- 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/GoogleSignIn/Sources/GIDSignInPreferences.h b/GoogleSignIn/Sources/GIDSignInPreferences.h index e1d77841..e2966276 100644 --- a/GoogleSignIn/Sources/GIDSignInPreferences.h +++ b/GoogleSignIn/Sources/GIDSignInPreferences.h @@ -40,19 +40,22 @@ extern NSString *const kSDKWrapperLoggingParameter; /// 100 characters. A value that is empty, or that contains any non-ASCII or ASCII control /// character, is dropped entirely. /// -/// A nil argument is ignored. A value that is rejected, or that conflicts with an already-stored -/// value, is logged rather than asserted. +/// A nil argument is ignored; use `+resetWrapperIdentifier` to clear a stored value. A value +/// that is rejected, or that conflicts with an already-stored value, is logged rather than +/// asserted. /// /// The first accepted write wins; later differing writes are ignored. This method is thread-safe. /// /// @param wrapperIdentifier The identifier to report. + (void)setWrapperIdentifier:(nullable NSString *)wrapperIdentifier; -/// Resets the SDK wrapper identifier. +/// Clears any stored SDK wrapper identifier. /// -/// This exists solely so unit tests can restore process state between cases. Production code -/// must not call it. -+ (void)resetWrapperIdentifierForTesting; +/// Callers should not normally need this method. The identifier is meant to be set once, early, +/// and left alone; clearing it defeats the first-write-wins rule that protects a wrapper's +/// registration from being overwritten. Its main use is letting unit tests restore process +/// state between cases. ++ (void)resetWrapperIdentifier; /// Returns the standard logging parameters to send with requests to Google's servers. /// diff --git a/GoogleSignIn/Sources/GIDSignInPreferences.m b/GoogleSignIn/Sources/GIDSignInPreferences.m index d7783108..bc34684c 100644 --- a/GoogleSignIn/Sources/GIDSignInPreferences.m +++ b/GoogleSignIn/Sources/GIDSignInPreferences.m @@ -151,7 +151,7 @@ + (void)setWrapperIdentifier:(nullable NSString *)wrapperIdentifier { os_unfair_lock_unlock(&gWrapperIdentifierLock); } -+ (void)resetWrapperIdentifierForTesting { ++ (void)resetWrapperIdentifier { os_unfair_lock_lock(&gWrapperIdentifierLock); gWrapperIdentifier = nil; os_unfair_lock_unlock(&gWrapperIdentifierLock); diff --git a/GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m b/GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m index 223e050d..0948a59d 100644 --- a/GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m +++ b/GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m @@ -94,7 +94,7 @@ - (void)tearDown { [GULSwizzler unswizzleClass:[OIDAuthorizationService class] selector:@selector(performTokenRequest:originalAuthorizationResponse:callback:) isClassSelector:YES]; - [GIDSignInPreferences resetWrapperIdentifierForTesting]; + [GIDSignInPreferences resetWrapperIdentifier]; } #pragma mark - Tests @@ -511,7 +511,7 @@ - (void)testWrapperIdentifier_PresentOnRefreshRequestWhenSet { } - (void)testWrapperIdentifier_AbsentOnRefreshRequestWhenUnset { - [GIDSignInPreferences resetWrapperIdentifierForTesting]; + [GIDSignInPreferences resetWrapperIdentifier]; // Both tokens expired 10 seconds ago. GIDGoogleUser *user = [self googleUserWithAccessTokenExpiresIn:-10 idTokenExpiresIn:-10]; diff --git a/GoogleSignIn/Tests/Unit/GIDSignInPreferencesTest.m b/GoogleSignIn/Tests/Unit/GIDSignInPreferencesTest.m index 924ae0ec..4d96d0fd 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInPreferencesTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInPreferencesTest.m @@ -22,7 +22,7 @@ @interface GIDSignInPreferencesTest : XCTestCase @implementation GIDSignInPreferencesTest - (void)tearDown { - [GIDSignInPreferences resetWrapperIdentifierForTesting]; + [GIDSignInPreferences resetWrapperIdentifier]; [super tearDown]; } @@ -74,7 +74,7 @@ - (void)testLoggingParameters_includesWrapperWhenSet { - (void)testWrapperIdentifier_UnsetIsNil { // Test that when no identifier is set, nil is returned. - [GIDSignInPreferences resetWrapperIdentifierForTesting]; + [GIDSignInPreferences resetWrapperIdentifier]; XCTAssertNil([GIDSignInPreferences wrapperIdentifier]); } @@ -167,11 +167,11 @@ - (void)testWrapperIdentifier_DropsControlCharacters { XCTAssertNoThrow([GIDSignInPreferences setWrapperIdentifier:@"fire\nbase"]); XCTAssertNil([GIDSignInPreferences wrapperIdentifier]); - [GIDSignInPreferences resetWrapperIdentifierForTesting]; + [GIDSignInPreferences resetWrapperIdentifier]; XCTAssertNoThrow([GIDSignInPreferences setWrapperIdentifier:@"fire\tbase"]); XCTAssertNil([GIDSignInPreferences wrapperIdentifier]); - [GIDSignInPreferences resetWrapperIdentifierForTesting]; + [GIDSignInPreferences resetWrapperIdentifier]; NSString *del = [NSString stringWithFormat:@"fire%Cbase", (unichar)0x7F]; XCTAssertNoThrow([GIDSignInPreferences setWrapperIdentifier:del]); XCTAssertNil([GIDSignInPreferences wrapperIdentifier]); diff --git a/GoogleSignIn/Tests/Unit/GIDSignInTest.m b/GoogleSignIn/Tests/Unit/GIDSignInTest.m index 3bfb2805..ed959f12 100644 --- a/GoogleSignIn/Tests/Unit/GIDSignInTest.m +++ b/GoogleSignIn/Tests/Unit/GIDSignInTest.m @@ -389,7 +389,7 @@ - (void)tearDown { [_testUserDefaults removePersistentDomainForName:kUserDefaultsSuiteName]; [_fakeMainBundle stopFaking]; - [GIDSignInPreferences resetWrapperIdentifierForTesting]; + [GIDSignInPreferences resetWrapperIdentifier]; [super tearDown]; } @@ -1194,7 +1194,7 @@ - (void)testWrapperIdentifier_PresentInAuthorizationRequestWhenSet { } - (void)testWrapperIdentifier_AbsentFromAuthorizationRequestWhenUnset { - [GIDSignInPreferences resetWrapperIdentifierForTesting]; + [GIDSignInPreferences resetWrapperIdentifier]; OCMStub( [_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef] ).andDo(^(NSInvocation *invocation) { @@ -1254,7 +1254,7 @@ - (void)testWrapperIdentifier_PresentOnRevokeURL { } - (void)testWrapperIdentifier_AbsentFromRevokeURLWhenUnset { - [GIDSignInPreferences resetWrapperIdentifierForTesting]; + [GIDSignInPreferences resetWrapperIdentifier]; [[[_authorization expect] andReturn:_authState] authState]; [[[_authState expect] andReturn:_tokenResponse] lastTokenResponse]; From 505676bb83d7c5f4394aefb5de6f62cf2e650bb5 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Fri, 21 Aug 2026 13:38:38 +0200 Subject: [PATCH 10/13] g-orchestrated: Tighten wrapperIdentifier docs: single source, consistent terms --- GoogleSignIn/Sources/GIDSignInPreferences.h | 32 ++++++------------- GoogleSignIn/Sources/GIDSignInPreferences.m | 20 ++++++------ .../Sources/Public/GoogleSignIn/GIDSignIn.h | 18 +++++++---- 3 files changed, 30 insertions(+), 40 deletions(-) diff --git a/GoogleSignIn/Sources/GIDSignInPreferences.h b/GoogleSignIn/Sources/GIDSignInPreferences.h index e2966276..9f181857 100644 --- a/GoogleSignIn/Sources/GIDSignInPreferences.h +++ b/GoogleSignIn/Sources/GIDSignInPreferences.h @@ -31,35 +31,21 @@ extern NSString *const kSDKWrapperLoggingParameter; /// Returns the current Apple execution environment, such as `ios` or `macos`. + (NSString *)environment; -/// Returns the current SDK wrapper identifier, or `nil` if none is set. +/// Returns the current SDK wrapper identifier, or `nil` if none has been accepted. + (nullable NSString *)wrapperIdentifier; -/// Sets the SDK wrapper identifier. -/// -/// A value may be up to 100 printable ASCII characters; a longer value is truncated to its first -/// 100 characters. A value that is empty, or that contains any non-ASCII or ASCII control -/// character, is dropped entirely. -/// -/// A nil argument is ignored; use `+resetWrapperIdentifier` to clear a stored value. A value -/// that is rejected, or that conflicts with an already-stored value, is logged rather than -/// asserted. -/// -/// The first accepted write wins; later differing writes are ignored. This method is thread-safe. -/// -/// @param wrapperIdentifier The identifier to report. +/// Sets the SDK wrapper identifier; a `nil` argument is ignored. See +/// `GIDSignIn.wrapperIdentifier` for the accepted format, validation, and first-write-wins +/// semantics. Thread-safe. + (void)setWrapperIdentifier:(nullable NSString *)wrapperIdentifier; -/// Clears any stored SDK wrapper identifier. -/// -/// Callers should not normally need this method. The identifier is meant to be set once, early, -/// and left alone; clearing it defeats the first-write-wins rule that protects a wrapper's -/// registration from being overwritten. Its main use is letting unit tests restore process -/// state between cases. +/// Clears any stored SDK wrapper identifier. Intended for unit tests, which must restore +/// process state between cases; production callers should not need it, and clearing defeats +/// the first-write-wins rule. Thread-safe. + (void)resetWrapperIdentifier; -/// Returns the standard logging parameters to send with requests to Google's servers. -/// -/// The parameters are `gpsdk`, `gidenv`, and, when a wrapper identifier is set, `gidwrapper`. +/// Returns the standard logging parameters sent with requests to Google's servers: the SDK +/// version and execution environment, plus the wrapper identifier when one is set. + (NSDictionary *)loggingParameters; + (NSString *)googleAuthorizationServer; diff --git a/GoogleSignIn/Sources/GIDSignInPreferences.m b/GoogleSignIn/Sources/GIDSignInPreferences.m index bc34684c..5b2a8c8d 100644 --- a/GoogleSignIn/Sources/GIDSignInPreferences.m +++ b/GoogleSignIn/Sources/GIDSignInPreferences.m @@ -52,32 +52,30 @@ #define STR(x) STR_EXPAND(x) #define STR_EXPAND(x) #x -// Returns the sanitized form of `candidate`, or nil if it must be dropped. -// Callers must not pass nil. +// Enforces the format documented on `GIDSignIn.wrapperIdentifier`: returns the accepted +// (possibly truncated) value, or nil if `candidate` must be rejected. `candidate` is non-nil. static NSString * _Nullable GIDSanitizedWrapperIdentifier(NSString *candidate) { static NSCharacterSet *allowedSet; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - // The range of printable ASCII characters is U+0020 through U+007E inclusive. + // Printable ASCII is U+0020–U+007E: length 0x5F starting at 0x20 (this is a length, not an + // end index). allowedSet = [NSCharacterSet characterSetWithRange:NSMakeRange(0x20, 0x5F)]; }); - // The drop check happens before truncation: if the original string contains any character - // outside the printable ASCII range, we drop the entire value. + // Validate the whole value before truncating: one out-of-range character anywhere rejects + // the entire value, so a valid 100-character prefix cannot rescue it. if ([candidate rangeOfCharacterFromSet:[allowedSet invertedSet]].location != NSNotFound) { return nil; } - // An empty string is also discarded. if (candidate.length == 0) { return nil; } - // A surviving string longer than 100 characters is truncated to 100. if (candidate.length > 100) { - // Truncating with -substringToIndex:100 is safe here precisely because the drop check has - // already guaranteed every character is single-unit ASCII, so there is no risk of splitting - // a surrogate pair. + // substringToIndex:100 is safe here only because the check above guaranteed every character + // is single-unit ASCII, so this cannot split a surrogate pair. return [candidate substringToIndex:100]; } @@ -133,7 +131,7 @@ + (void)setWrapperIdentifier:(nullable NSString *)wrapperIdentifier { NSString *sanitized = GIDSanitizedWrapperIdentifier(wrapperIdentifier); if (sanitized == nil) { - NSLog(@"[Google Sign-In iOS]: the SDK wrapper identifier '%@' was ignored, because it must be " + NSLog(@"[Google Sign-In iOS]: the SDK wrapper identifier '%@' was rejected, because it must be " "non-empty and contain only printable ASCII characters (U+0020 to U+007E).", wrapperIdentifier); return; diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h index 823285bb..5b824667 100644 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h @@ -77,9 +77,15 @@ typedef NS_ERROR_ENUM(kGIDSignInErrorDomain, GIDSignInErrorCode) { /// Google as a diagnostic parameter for aggregate metrics only; it is never used for /// authentication or authorization. /// -/// Format: up to 100 printable ASCII characters (U+0020 to U+007E). A longer value is -/// truncated to its first 100 characters. A value that is empty, or that contains any -/// non-ASCII character or any ASCII control character, is ignored in its entirety and logged. +/// Format: 1–100 printable ASCII characters (U+0020–U+007E). The whole value is validated +/// before any truncation: if it is empty or contains any character outside that range, the +/// entire value is rejected — a valid 100-character prefix does not save it. A value that +/// passes but is longer than 100 characters is then truncated to its first 100. A rejected +/// value is logged and leaves any previously stored value unchanged. +/// +/// The value is stored and sent verbatim — case- and whitespace-sensitive, never normalized — +/// so "Firebase", "firebase", and "firebase " are distinct in Google's logs. Pick one +/// canonical spelling. /// /// Policy: /// * Choose one stable name and keep it stable across your releases. @@ -87,9 +93,9 @@ typedef NS_ERROR_ENUM(kGIDSignInErrorDomain, GIDSignInErrorCode) { /// useless, and a value that has shipped cannot be retracted from Google's logs. /// * Never include anything user-specific, app-specific, or identifying. /// -/// Set this once, before your first sign-in call. The first accepted value wins: later -/// differing values are ignored and logged, and setting `nil` does not clear it. Reading this -/// property returns the value in effect, which is `nil` until a value has been accepted. +/// Set this once, before your first sign-in call. The first accepted value wins: a later +/// differing value is ignored and logged, and setting `nil` is ignored (it does not clear a +/// stored value). Reading returns the value in effect, or `nil` if none has been accepted. @property(class, nonatomic, nullable, copy) NSString *wrapperIdentifier; #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST From fa51e74f6db8761bf605cd460fa6564fa55f48d6 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Fri, 21 Aug 2026 13:46:46 +0200 Subject: [PATCH 11/13] g-orchestrated: Reword wrapperIdentifier docs to keep sources pure ASCII --- GoogleSignIn/Sources/GIDSignInPreferences.m | 4 ++-- .../Sources/Public/GoogleSignIn/GIDSignIn.h | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/GoogleSignIn/Sources/GIDSignInPreferences.m b/GoogleSignIn/Sources/GIDSignInPreferences.m index 5b2a8c8d..7ffdbb9b 100644 --- a/GoogleSignIn/Sources/GIDSignInPreferences.m +++ b/GoogleSignIn/Sources/GIDSignInPreferences.m @@ -58,8 +58,8 @@ static NSCharacterSet *allowedSet; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - // Printable ASCII is U+0020–U+007E: length 0x5F starting at 0x20 (this is a length, not an - // end index). + // Printable ASCII is U+0020 through U+007E: length 0x5F starting at 0x20 (this is a + // length, not an end index). allowedSet = [NSCharacterSet characterSetWithRange:NSMakeRange(0x20, 0x5F)]; }); diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h index 5b824667..f546107b 100644 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h @@ -77,15 +77,15 @@ typedef NS_ERROR_ENUM(kGIDSignInErrorDomain, GIDSignInErrorCode) { /// Google as a diagnostic parameter for aggregate metrics only; it is never used for /// authentication or authorization. /// -/// Format: 1–100 printable ASCII characters (U+0020–U+007E). The whole value is validated -/// before any truncation: if it is empty or contains any character outside that range, the -/// entire value is rejected — a valid 100-character prefix does not save it. A value that -/// passes but is longer than 100 characters is then truncated to its first 100. A rejected -/// value is logged and leaves any previously stored value unchanged. -/// -/// The value is stored and sent verbatim — case- and whitespace-sensitive, never normalized — -/// so "Firebase", "firebase", and "firebase " are distinct in Google's logs. Pick one -/// canonical spelling. +/// Format: 1 to 100 printable ASCII characters (U+0020 through U+007E). The whole value is +/// validated before any truncation. If it is empty or contains any character outside that +/// range, the entire value is rejected; a valid 100-character prefix does not save it. A +/// value that passes but is longer than 100 characters is then truncated to its first 100. A +/// rejected value is logged and leaves any previously stored value unchanged. +/// +/// The value is stored and sent verbatim. It is case-sensitive, whitespace-sensitive, and +/// never normalized, so "Firebase", "firebase", and "firebase " are distinct in Google's +/// logs. Pick one canonical spelling. /// /// Policy: /// * Choose one stable name and keep it stable across your releases. From f5741c219571f574f8a79c952b83e77fa14a256e Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Fri, 21 Aug 2026 16:51:24 +0200 Subject: [PATCH 12/13] Update doc comment --- .../Sources/Public/GoogleSignIn/GIDSignIn.h | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h index f546107b..7e5ec0be 100644 --- a/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h +++ b/GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h @@ -77,25 +77,16 @@ typedef NS_ERROR_ENUM(kGIDSignInErrorDomain, GIDSignInErrorCode) { /// Google as a diagnostic parameter for aggregate metrics only; it is never used for /// authentication or authorization. /// -/// Format: 1 to 100 printable ASCII characters (U+0020 through U+007E). The whole value is -/// validated before any truncation. If it is empty or contains any character outside that -/// range, the entire value is rejected; a valid 100-character prefix does not save it. A -/// value that passes but is longer than 100 characters is then truncated to its first 100. A -/// rejected value is logged and leaves any previously stored value unchanged. -/// -/// The value is stored and sent verbatim. It is case-sensitive, whitespace-sensitive, and -/// never normalized, so "Firebase", "firebase", and "firebase " are distinct in Google's -/// logs. Pick one canonical spelling. +/// Format: +/// * 1 to 100 printable ASCII characters (U+0020 to U+007E). +/// * Invalid values, including `nil`, will be dropped or truncated. /// /// Policy: -/// * Choose one stable name and keep it stable across your releases. -/// * Do NOT encode your version in it; per-release identifiers make aggregate metrics -/// useless, and a value that has shipped cannot be retracted from Google's logs. -/// * Never include anything user-specific, app-specific, or identifying. -/// -/// Set this once, before your first sign-in call. The first accepted value wins: a later -/// differing value is ignored and logged, and setting `nil` is ignored (it does not clear a -/// stored value). Reading returns the value in effect, or `nil` if none has been accepted. +/// * Choose one stable name and keep it identical across your releases. +/// * As the value is case- and whitespace-sensitive, we suggest a lowercase value with no +/// spaces. +/// +/// Set this once, before the first sign-in call. The property is write-once. @property(class, nonatomic, nullable, copy) NSString *wrapperIdentifier; #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST From 158f5499163a0c087872962ffdc8956fd069a67c Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Fri, 21 Aug 2026 19:32:34 +0200 Subject: [PATCH 13/13] Update doc comments --- GoogleSignIn/Sources/GIDSignInPreferences.h | 9 +++------ GoogleSignIn/Sources/GIDSignInPreferences.m | 12 ++++++------ 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/GoogleSignIn/Sources/GIDSignInPreferences.h b/GoogleSignIn/Sources/GIDSignInPreferences.h index 9f181857..6e2b4d82 100644 --- a/GoogleSignIn/Sources/GIDSignInPreferences.h +++ b/GoogleSignIn/Sources/GIDSignInPreferences.h @@ -34,14 +34,11 @@ extern NSString *const kSDKWrapperLoggingParameter; /// Returns the current SDK wrapper identifier, or `nil` if none has been accepted. + (nullable NSString *)wrapperIdentifier; -/// Sets the SDK wrapper identifier; a `nil` argument is ignored. See -/// `GIDSignIn.wrapperIdentifier` for the accepted format, validation, and first-write-wins -/// semantics. Thread-safe. +/// Sets the SDK wrapper identifier. +/// See `GIDSignIn.wrapperIdentifier` for additional information, including formatting rules. + (void)setWrapperIdentifier:(nullable NSString *)wrapperIdentifier; -/// Clears any stored SDK wrapper identifier. Intended for unit tests, which must restore -/// process state between cases; production callers should not need it, and clearing defeats -/// the first-write-wins rule. Thread-safe. +/// Clears any stored SDK wrapper identifier. Thread-safe. + (void)resetWrapperIdentifier; /// Returns the standard logging parameters sent with requests to Google's servers: the SDK diff --git a/GoogleSignIn/Sources/GIDSignInPreferences.m b/GoogleSignIn/Sources/GIDSignInPreferences.m index 7ffdbb9b..2b1de6ca 100644 --- a/GoogleSignIn/Sources/GIDSignInPreferences.m +++ b/GoogleSignIn/Sources/GIDSignInPreferences.m @@ -52,19 +52,20 @@ #define STR(x) STR_EXPAND(x) #define STR_EXPAND(x) #x -// Enforces the format documented on `GIDSignIn.wrapperIdentifier`: returns the accepted -// (possibly truncated) value, or nil if `candidate` must be rejected. `candidate` is non-nil. +// Enforces the format documented on `GIDSignIn.wrapperIdentifier`: returns the accepted value, +// or nil if `candidate` must be rejected. `candidate` is non-nil. static NSString * _Nullable GIDSanitizedWrapperIdentifier(NSString *candidate) { static NSCharacterSet *allowedSet; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // Printable ASCII is U+0020 through U+007E: length 0x5F starting at 0x20 (this is a // length, not an end index). + // Note that, if this set changes, the substringToIndex: in the truncation + // may also need to change. allowedSet = [NSCharacterSet characterSetWithRange:NSMakeRange(0x20, 0x5F)]; }); - // Validate the whole value before truncating: one out-of-range character anywhere rejects - // the entire value, so a valid 100-character prefix cannot rescue it. + // The whole value is validated before truncating. if ([candidate rangeOfCharacterFromSet:[allowedSet invertedSet]].location != NSNotFound) { return nil; } @@ -74,8 +75,7 @@ } if (candidate.length > 100) { - // substringToIndex:100 is safe here only because the check above guaranteed every character - // is single-unit ASCII, so this cannot split a surrogate pair. + // This cannot split a surrogate pair because each value is single-unit ASCII. return [candidate substringToIndex:100]; }