Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/firebase-runtime-failure-backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ This backlog tracks binding drifts that are concrete candidates for the runtime-

| Case id | Target/member | Audit finding reference | Expected runtime failure mechanism | Proof status | Fix PR | Merged commit |
| --- | --- | --- | --- | --- | --- | --- |
| `cloudfirestore-getquerynamed` | `Firebase.CloudFirestore.Firestore.GetQueryNamed` | `output/firebase-binding-audit/report.json` -> `CloudFirestore` -> `GetQueryNamed` (`signature-drift`) | The binding currently exports `getQueryNamed:completion:` as `NSInputStream` instead of `NSString`, so native Firestore receives `__NSCFInputStream` and throws an Objective-C exception when it treats the argument like a string. | Proved locally on the unfixed binding. Evidence: `ObjCRuntime.ObjCException`, `NSInvalidArgumentException`, selector `-[__NSCFInputStream length]`, artifact `tests/E2E/Firebase.Foundation/artifacts/firebase-foundation-result-cloudfirestore-getquerynamed-failure.json`, log `tests/E2E/Firebase.Foundation/artifacts/firebase-foundation-sim-cloudfirestore-getquerynamed-failure.log`. | Pending | - |
| `cloudfunctions-usefunctionsemulatororigin` | `Firebase.CloudFunctions.CloudFunctions.UseFunctionsEmulatorOrigin` | Manual runtime candidate from the current binding surface and native `FirebaseFunctions` Swift header | The binding still exports `useFunctionsEmulatorOrigin:` even though the current framework only exposes `useEmulatorWithHost:port:`. Calling the stale selector raises `ObjCRuntime.ObjCException` with an unrecognized-selector native exception. | Proved locally on the unfixed binding. Evidence: `ObjCRuntime.ObjCException`, `NSInvalidArgumentException`, selector `-[FIRFunctions useFunctionsEmulatorOrigin:]`, artifact `tests/E2E/Firebase.Foundation/artifacts/firebase-foundation-result-cloudfunctions-usefunctionsemulatororigin-failure.json`, log `tests/E2E/Firebase.Foundation/artifacts/firebase-foundation-sim-cloudfunctions-usefunctionsemulatororigin-failure.log`. | [#113](https://github.com/AdamEssenmacher/GoogleApisForiOSComponents/pull/113) | - |

## Investigating

No additional runtime-failure candidates are currently promoted.

## Done

None yet.
| Case id | Target/member | Audit finding reference | Runtime failure mechanism | Proof status | Fix PR | Merged commit |
| --- | --- | --- | --- | --- | --- | --- |
| `cloudfirestore-getquerynamed` | `Firebase.CloudFirestore.Firestore.GetQueryNamed` | `output/firebase-binding-audit/report.json` -> `CloudFirestore` -> `GetQueryNamed` (`signature-drift`) | The old binding exported `getQueryNamed:completion:` as `NSInputStream` instead of `NSString`, so native Firestore received `__NSCFInputStream` and threw an Objective-C exception when it treated the argument like a string. | Proved and fixed. Evidence artifact: `tests/E2E/Firebase.Foundation/artifacts/firebase-foundation-result-cloudfirestore-getquerynamed-failure.json`. Regression artifact: `tests/E2E/Firebase.Foundation/artifacts/firebase-foundation-result-cloudfirestore-getquerynamed-success.json`. | [#112](https://github.com/AdamEssenmacher/GoogleApisForiOSComponents/pull/112) | `3077a538a892de0db3350f0a459bf8620729f4db` |
12 changes: 2 additions & 10 deletions source/Firebase/CloudFunctions/ApiDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,10 @@ interface CloudFunctions
[Export("HTTPSCallableWithName:")]
HttpsCallable HttpsCallable(string name);

// @property(nonatomic, readonly, nullable) NSString *emulatorOrigin;
[NullAllowed]
[Export("emulatorOrigin")]
string EmulatorOrigin { get; }

//- (void)useFunctionsEmulatorOrigin:(NSString *)origin
[Export("useFunctionsEmulatorOrigin:")]
void UseFunctionsEmulatorOrigin(string origin);

//- (void)useEmulatorWithHost:(NSString *)host port:(NSInteger) port;
[Internal]
[Export ("useEmulatorWithHost:port:")]
void UseEmulatorOriginWithHost (string host, uint port);
void _UseEmulatorOriginWithHost (string host, nint port);
}

// void (^)(FIRHTTPSCallableResult *_Nullable result, NSError *_Nullable error);
Expand Down
69 changes: 63 additions & 6 deletions source/Firebase/CloudFunctions/Extension.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,80 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using ObjCRuntime;
namespace Firebase.CloudFunctions {
public partial class CloudFunctions {
static string currentVersion;
static string? currentVersion;
// The current native SDK only exposes useEmulatorWithHost:port: to ObjC, so preserve the
// managed emulatorOrigin/useFunctionsEmulatorOrigin API by caching the last requested origin.
static readonly ConditionalWeakTable<CloudFunctions, EmulatorOriginState> emulatorOrigins = new ();
Comment thread
AdamEssenmacher marked this conversation as resolved.
Outdated

sealed class EmulatorOriginState {
public string? Origin { get; set; }
}

public static string CurrentVersion {
get {
if (currentVersion == null) {
IntPtr RTLD_MAIN_ONLY = Dlfcn.dlopen (null, 0);
IntPtr ptr = Dlfcn.dlsym (RTLD_MAIN_ONLY, "FirebaseCloudFunctionsVersionStr");
currentVersion = Marshal.PtrToStringAnsi (ptr);
Dlfcn.dlclose (RTLD_MAIN_ONLY);
}
currentVersion = Marshal.PtrToStringAnsi (ptr) ?? string.Empty;
Dlfcn.dlclose (RTLD_MAIN_ONLY);
}

return currentVersion;
return currentVersion ?? string.Empty;
}
}
}

public const string CloudFunctionsErrorDomain = "com.firebase.functions";
public const string CloudFunctionsErrorDetailsKey = "details";

public string? EmulatorOrigin => emulatorOrigins.GetOrCreateValue (this).Origin;

public void UseFunctionsEmulatorOrigin (string origin)
{
var (host, port) = ParseEmulatorOrigin (origin);
UseEmulatorOriginWithHost (host, port);
Comment thread
AdamEssenmacher marked this conversation as resolved.
Outdated
emulatorOrigins.GetOrCreateValue (this).Origin = origin;
}

public void UseEmulatorOriginWithHost (string host, uint port)
{
if (string.IsNullOrWhiteSpace (host))
throw new ArgumentException ("Host must not be null or empty.", nameof (host));

_UseEmulatorOriginWithHost (host, checked ((nint) port));
emulatorOrigins.GetOrCreateValue (this).Origin = $"{host}:{port}";
}

static (string Host, uint Port) ParseEmulatorOrigin (string origin)
{
if (string.IsNullOrWhiteSpace (origin))
throw new ArgumentException ("Origin must not be null or empty.", nameof (origin));

if (TryParseOrigin (origin, out var host, out var port))
return (host, port);

if (TryParseOrigin ($"http://{origin}", out host, out port))
return (host, port);

throw new ArgumentException ("Expected a Cloud Functions emulator origin in 'host:port' or absolute URL form.", nameof (origin));
}

static bool TryParseOrigin (string origin, out string host, out uint port)
{
host = string.Empty;
port = 0;

if (!Uri.TryCreate (origin, UriKind.Absolute, out var uri))
return false;

if (string.IsNullOrWhiteSpace (uri.Host) || uri.Port <= 0)
return false;

host = uri.Host;
port = checked ((uint) uri.Port);
return true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
using ObjCRuntime;
#endif

#if ENABLE_RUNTIME_DRIFT_CASE_CLOUDFUNCTIONS_USEFUNCTIONSEMULATORORIGIN
using Firebase.CloudFunctions;
using Foundation;
using ObjCRuntime;
#endif

namespace FirebaseFoundationE2E;

static class FirebaseRuntimeDriftCases
Expand Down Expand Up @@ -136,6 +142,79 @@ void OnMarshalObjectiveCException(object? sender, MarshalObjectiveCExceptionEven
}
#endif

#if ENABLE_RUNTIME_DRIFT_CASE_CLOUDFUNCTIONS_USEFUNCTIONSEMULATORORIGIN
static Task<string> VerifyCloudFunctionsUseFunctionsEmulatorOriginAsync()
{
const string selector = "useFunctionsEmulatorOrigin:";

var functions = CloudFunctions.DefaultInstance;
if (functions is null)
{
throw new InvalidOperationException("Firebase.CloudFunctions.CloudFunctions.DefaultInstance returned null after App.Configure().");
}

const string origin = "localhost:5001";
NSException? marshaledException = null;
MarshalObjectiveCExceptionMode? marshaledExceptionMode = null;

void OnMarshalObjectiveCException(object? sender, MarshalObjectiveCExceptionEventArgs args)
{
marshaledException ??= args.Exception;
marshaledExceptionMode ??= args.ExceptionMode;
}

Runtime.MarshalObjectiveCException += OnMarshalObjectiveCException;
try
{
try
{
functions.UseFunctionsEmulatorOrigin(origin);
var cachedOrigin = functions.EmulatorOrigin;
if (!string.Equals(cachedOrigin, origin, StringComparison.Ordinal))
{
throw new InvalidOperationException(
$"CloudFunctions.EmulatorOrigin returned '{FormatDetail(cachedOrigin)}' after UseFunctionsEmulatorOrigin, expected '{origin}'.");
}

functions.UseEmulatorOriginWithHost("127.0.0.1", 5002);
cachedOrigin = functions.EmulatorOrigin;
if (!string.Equals(cachedOrigin, "127.0.0.1:5002", StringComparison.Ordinal))
{
throw new InvalidOperationException(
$"CloudFunctions.EmulatorOrigin returned '{FormatDetail(cachedOrigin)}' after UseEmulatorOriginWithHost, expected '127.0.0.1:5002'.");
}
}
catch (ObjCException ex)
{
throw new InvalidOperationException(
$"Selector '{selector}' should not throw after the binding fix, but observed {ex.GetType().FullName}. " +
$"Runtime argument type: {origin.GetType().FullName}. " +
$"NSException.Name: {FormatDetail(marshaledException?.Name?.ToString())}. " +
$"NSException.Reason: {FormatDetail(marshaledException?.Reason)}. " +
$"Marshal mode: {FormatDetail(marshaledExceptionMode?.ToString())}.",
ex);
}

if (marshaledException is not null)
{
throw new InvalidOperationException(
$"Cloud Functions emulator API completed, but Runtime.MarshalObjectiveCException captured unexpected NSException.Name '{marshaledException.Name}'. " +
$"Reason: {FormatDetail(marshaledException.Reason)}. Marshal mode: {FormatDetail(marshaledExceptionMode?.ToString())}.");
}

return Task.FromResult(
$"Selector '{selector}' completed without ObjC exception after the binding fix. " +
$"Runtime argument type: {origin.GetType().FullName}. " +
$"Cached origin after legacy method: {origin}. " +
$"Cached origin after host/port method: 127.0.0.1:5002.");
}
finally
{
Runtime.MarshalObjectiveCException -= OnMarshalObjectiveCException;
}
}
#endif

static string FormatDetail(string? value)
{
return string.IsNullOrWhiteSpace(value) ? "<empty>" : value;
Expand Down
2 changes: 1 addition & 1 deletion tests/E2E/Firebase.Foundation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ The runner writes logs and JSON results under:

## Optional nullability validation mode

This project also supports an opt-in nullability validation lane that exercises the shipped consumer APIs for the current Firebase nullability fixes and verifies those APIs still work with the broadened contracts. The lane is binding-focused: configuration-dependent Firebase errors are acceptable as long as the managed API crosses into native correctly and does not fail with a binding-layer exception. It currently covers the fixes that are safely exercisable in the E2E app; Cloud Functions is intentionally out of scope here because `emulatorOrigin` and `useFunctionsEmulatorOrigin:` are a separate selector-binding issue.
This project also supports an opt-in nullability validation lane that exercises the shipped consumer APIs for the current Firebase nullability fixes and verifies those APIs still work with the broadened contracts. The lane is binding-focused: configuration-dependent Firebase errors are acceptable as long as the managed API crosses into native correctly and does not fail with a binding-layer exception. It currently covers the fixes that are safely exercisable in the E2E app; Cloud Functions remains out of scope for the nullability lane, and selector-oriented runtime cases are handled through the targeted runtime drift mode instead.

1. Pack the Firebase slices used by the validation lane and their dependencies:

Expand Down
11 changes: 11 additions & 0 deletions tests/E2E/Firebase.Foundation/runtime-drift-cases.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@
"version": "12.6.0"
}
]
},
{
"id": "cloudfunctions-usefunctionsemulatororigin",
"method": "VerifyCloudFunctionsUseFunctionsEmulatorOriginAsync",
"bindingPackage": "AdamE.Firebase.iOS.CloudFunctions",
"packages": [
{
"id": "AdamE.Firebase.iOS.CloudFunctions",
"version": "12.6.0"
}
]
}
]
}
Loading