chromium/ios/chrome/browser/shared/public/commands/command_dispatcher.mm

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"

#import <objc/runtime.h>

#import <ostream>
#import <unordered_map>
#import <vector>

#import "base/check.h"
#import "base/containers/contains.h"
#import "base/strings/sys_string_conversions.h"

#pragma mark - SilentlyFailingObject

// Object that responds to any selector and does nothing when its called.
// Used as a "nice OCMock" equivalent for CommandDispatcher that's preparing for
// shutdown.
@interface SilentlyFailingObject : NSProxy
@end
@implementation SilentlyFailingObject

- (instancetype)init {
  return self;
}

- (void)forwardInvocation:(NSInvocation*)invocation {
}

- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector {
  // Return some method signature to silence errors.
  // Here it's (void)(self, _cmd).
  return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

@end

#pragma mark - CommandDispatcher

@implementation CommandDispatcher {
  // Stores which target to forward to for a given selector.
  std::unordered_map<SEL, __weak id> _forwardingTargets;

  // Stores remembered targets while preparing for shutdown.
  std::unordered_map<SEL, __weak id> _silentlyFailingTargets;

  // Tracks if preparing for shutdown has been requested.
  // This is an ivar, not a property, to avoid having synthesized getter/setter
  // methods.
  BOOL _preparingForShutdown;
}

- (void)startDispatchingToTarget:(id)target forSelector:(SEL)selector {
  DCHECK(![self targetForSelector:selector]);
  DCHECK(![self shouldFailSilentlyForSelector:selector]);

  _forwardingTargets[selector] = target;
}

- (void)startDispatchingToTarget:(id)target forProtocol:(Protocol*)protocol {
  unsigned int methodCount;
  objc_method_description* requiredInstanceMethods =
      protocol_copyMethodDescriptionList(protocol, YES /* isRequiredMethod */,
                                         YES /* isInstanceMethod */,
                                         &methodCount);
  for (unsigned int i = 0; i < methodCount; i++) {
    [self startDispatchingToTarget:target
                       forSelector:requiredInstanceMethods[i].name];
  }
  free(requiredInstanceMethods);
}

- (void)stopDispatchingForSelector:(SEL)selector {
  if (_preparingForShutdown) {
    id target = _forwardingTargets[selector];
    _silentlyFailingTargets[selector] = target;
  }
  _forwardingTargets.erase(selector);
}

- (void)stopDispatchingForProtocol:(Protocol*)protocol {
  unsigned int methodCount;
  objc_method_description* requiredInstanceMethods =
      protocol_copyMethodDescriptionList(protocol, YES /* isRequiredMethod */,
                                         YES /* isInstanceMethod */,
                                         &methodCount);
  for (unsigned int i = 0; i < methodCount; i++) {
    [self stopDispatchingForSelector:requiredInstanceMethods[i].name];
  }
  free(requiredInstanceMethods);
}

// `-stopDispatchingToTarget` should be called much less often than
// `-forwardingTargetForSelector`, so removal is intentionally O(n) in order
// to prioritize the speed of lookups.
- (void)stopDispatchingToTarget:(id)target {
  std::vector<SEL> selectorsToErase;
  for (auto& kv : _forwardingTargets) {
    if (kv.second == target) {
      selectorsToErase.push_back(kv.first);
    }
  }

  for (auto* selector : selectorsToErase) {
    [self stopDispatchingForSelector:selector];
  }
}

- (BOOL)dispatchingForProtocol:(Protocol*)protocol {
  // Special-case the NSObject protocol.
  if ([@"NSObject" isEqualToString:NSStringFromProtocol(protocol)]) {
    return YES;
  }

  unsigned int methodCount;
  objc_method_description* requiredInstanceMethods =
      protocol_copyMethodDescriptionList(protocol, YES /* isRequiredMethod */,
                                         YES /* isInstanceMethod */,
                                         &methodCount);
  BOOL conforming = YES;
  for (unsigned int i = 0; i < methodCount; i++) {
    SEL selector = requiredInstanceMethods[i].name;
    BOOL targetFound = base::Contains(_forwardingTargets, selector);
    if (!targetFound && ![self shouldFailSilentlyForSelector:selector]) {
      conforming = NO;
      break;
    }
  }
  free(requiredInstanceMethods);
  if (!conforming) {
    return NO;
  }

  unsigned int protocolCount;
  Protocol* __unsafe_unretained _Nonnull* _Nullable conformedProtocols =
      protocol_copyProtocolList(protocol, &protocolCount);
  for (unsigned int i = 0; i < protocolCount; i++) {
    if (![self dispatchingForProtocol:conformedProtocols[i]]) {
      conforming = NO;
      break;
    }
  }

  free(conformedProtocols);
  return conforming;
}

- (CommandDispatcher*)strictCallableForProtocol:(Protocol*)protocol {
  CHECK([self dispatchingForProtocol:protocol])
      << "Dispatcher failed protocol conformance";
  return self;
}

- (void)prepareForShutdown {
  _preparingForShutdown = YES;
}

#pragma mark - NSObject

// Overridden to forward messages to registered handlers.
- (id)forwardingTargetForSelector:(SEL)selector {
  id target = [self targetForSelector:selector];
  if (target) {
    return target;
  }

  return [super forwardingTargetForSelector:selector];
}

// Overriden to return YES for any registered method.
- (BOOL)respondsToSelector:(SEL)selector {
  if ([self targetForSelector:selector]) {
    return YES;
  }
  return [super respondsToSelector:selector];
}

// Overriden because overrides of `forwardInvocation` also require an override
// of `methodSignatureForSelector`, as the method signature is needed to
// construct NSInvocations.
- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector {
  NSMethodSignature* signature = [super methodSignatureForSelector:aSelector];
  if (signature) {
    return signature;
  }

  id target = [self targetForSelector:aSelector];
  return [target methodSignatureForSelector:aSelector];
}

#pragma mark - Private

// Returns the target registered to receive messeages for `selector`.
- (id)targetForSelector:(SEL)selector {
  auto target = _forwardingTargets.find(selector);
  if (target == _forwardingTargets.end()) {
    if ([self shouldFailSilentlyForSelector:selector]) {
      return [[SilentlyFailingObject alloc] init];
    }
    return nil;
  }
  return target->second;
}

- (BOOL)shouldFailSilentlyForSelector:(SEL)selector {
  return _preparingForShutdown &&
         base::Contains(_silentlyFailingTargets, selector);
}

@end