<?php
namespace Aws;

use Aws\Api\Service;
use Psr\Http\Message\RequestInterface;
use Psr\Log\InvalidArgumentException;

/**
 * Used to update the host based on a modeled endpoint trait
 *
 * IMPORTANT: this middleware must be added after the "build" step.
 *
 * @internal
 */
class EndpointParameterMiddleware
{
    /** @var callable */
    private $nextHandler;

    /** @var Service */
    private $service;

    /**
     * Create a middleware wrapper function
     *
     * @param Service $service
     * @param array $args
     * @return \Closure
     */
    public static function wrap(Service $service)
    {
        return function (callable $handler) use ($service) {
            return new self($handler, $service);
        };
    }

    public function __construct(callable $nextHandler, Service $service)
    {
        $this->nextHandler = $nextHandler;
        $this->service = $service;
    }

    public function __invoke(CommandInterface $command, RequestInterface $request)
    {
        $nextHandler = $this->nextHandler;

        $operation = $this->service->getOperation($command->getName());

        if (!empty($operation['endpoint']['hostPrefix'])) {
            $prefix = $operation['endpoint']['hostPrefix'];

            // Captures endpoint parameters stored in the modeled host.
            // These are denoted by enclosure in braces, i.e. '{param}'
            preg_match_all("/\{([a-zA-Z0-9]+)}/", $prefix, $parameters);

            if (!empty($parameters[1])) {

                // Captured parameters without braces stored in $parameters[1],
                // which should correspond to members in the Command object
                foreach ($parameters[1] as $index => $parameter) {
                    if (empty($command[$parameter])) {
                        throw new \InvalidArgumentException(
                            "The parameter '{$parameter}' must be set and not empty."
                        );
                    }

                    // Captured parameters with braces stored in $parameters[0],
                    // which are replaced by their corresponding Command value
                    $prefix = str_replace(
                        $parameters[0][$index],
                        $command[$parameter],
                        $prefix
                    );
                }
            }

            $uri = $request->getUri();
            $host = $prefix . $uri->getHost();
            if (!\Aws\is_valid_hostname($host)) {
                throw new \InvalidArgumentException(
                    "The supplied parameters result in an invalid hostname: '{$host}'."
                );
            }
            $request = $request->withUri($uri->withHost($host));
        }

        return $nextHandler($command, $request);
    }
}


<?php
namespace Aws;

/**
 * Builds a single handler function from zero or more middleware functions and
 * a handler. The handler function is then used to send command objects and
 * return a promise that is resolved with an AWS result object.
 *
 * The "front" of the list is invoked before the "end" of the list. You can add
 * middleware to the front of the list using one of the "prepend" method, and
 * the end of the list using one of the "append" method. The last function
 * invoked in a handler list is the handler (a function that does not accept a
 * next handler but rather is responsible for returning a promise that is
 * fulfilled with an Aws\ResultInterface object).
 *
 * Handlers are ordered using a "step" that describes the step at which the
 * SDK is when sending a command. The available steps are:
 *
 * - init: The command is being initialized, allowing you to do things like add
 *   default options.
 * - validate: The command is being validated before it is serialized
 * - build: The command is being serialized into an HTTP request. A middleware
 *   in this step MUST serialize an HTTP request and populate the "@request"
 *   parameter of a command with the request such that it is available to
 *   subsequent middleware.
 * - sign: The request is being signed and prepared to be sent over the wire.
 *
 * Middleware can be registered with a name to allow you to easily add a
 * middleware before or after another middleware by name. This also allows you
 * to remove a middleware by name (in addition to removing by instance).
 */
class HandlerList implements \Countable
{
    const INIT = 'init';
    const VALIDATE = 'validate';
    const BUILD = 'build';
    const SIGN = 'sign';
    const ATTEMPT = 'attempt';

    /** @var callable */
    private $handler;

    /** @var array */
    private $named = [];

    /** @var array */
    private $sorted;

    /** @var callable|null */
    private $interposeFn;

    /** @var array Steps (in reverse order) */
    private $steps = [
        self::ATTEMPT  => [],
        self::SIGN     => [],
        self::BUILD    => [],
        self::VALIDATE => [],
        self::INIT     => [],
    ];

    /**
     * @param callable $handler HTTP handler.
     */
    public function __construct(callable $handler = null)
    {
        $this->handler = $handler;
    }

    /**
     * Dumps a string representation of the list.
     *
     * @return string
     */
    public function __toString()
    {
        $str = '';
        $i = 0;

        foreach (array_reverse($this->steps) as $k => $step) {
            foreach (array_reverse($step) as $j => $tuple) {
                $str .= "{$i}) Step: {$k}, ";
                if ($tuple[1]) {
                    $str .= "Name: {$tuple[1]}, ";
                }
                $str .= "Function: " . $this->debugCallable($tuple[0]) . "\n";
                $i++;
            }
        }

        if ($this->handler) {
            $str .= "{$i}) Handler: " . $this->debugCallable($this->handler) . "\n";
        }

        return $str;
    }

    /**
     * Set the HTTP handler that actually returns a response.
     *
     * @param callable $handler Function that accepts a request and array of
     *                          options and returns a Promise.
     */
    public function setHandler(callable $handler)
    {
        $this->handler = $handler;
    }

    /**
     * Returns true if the builder has a handler.
     *
     * @return bool
     */
    public function hasHandler()
    {
        return (bool) $this->handler;
    }

    /**
     * Append a middleware to the init step.
     *
     * @param callable $middleware Middleware function to add.
     * @param string   $name       Name of the middleware.
     */
    public function appendInit(callable $middleware, $name = null)
    {
        $this->add(self::INIT, $name, $middleware);
    }

    /**
     * Prepend a middleware to the init step.
     *
     * @param callable $middleware Middleware function to add.
     * @param string   $name       Name of the middleware.
     */
    public function prependInit(callable $middleware, $name = null)
    {
        $this->add(self::INIT, $name, $middleware, true);
    }

    /**
     * Append a middleware to the validate step.
     *
     * @param callable $middleware Middleware function to add.
     * @param string   $name       Name of the middleware.
     */
    public function appendValidate(callable $middleware, $name = null)
    {
        $this->add(self::VALIDATE, $name, $middleware);
    }

    /**
     * Prepend a middleware to the validate step.
     *
     * @param callable $middleware Middleware function to add.
     * @param string   $name       Name of the middleware.
     */
    public function prependValidate(callable $middleware, $name = null)
    {
        $this->add(self::VALIDATE, $name, $middleware, true);
    }

    /**
     * Append a middleware to the build step.
     *
     * @param callable $middleware Middleware function to add.
     * @param string   $name       Name of the middleware.
     */
    public function appendBuild(callable $middleware, $name = null)
    {
        $this->add(self::BUILD, $name, $middleware);
    }

    /**
     * Prepend a middleware to the build step.
     *
     * @param callable $middleware Middleware function to add.
     * @param string   $name       Name of the middleware.
     */
    public function prependBuild(callable $middleware, $name = null)
    {
        $this->add(self::BUILD, $name, $middleware, true);
    }

    /**
     * Append a middleware to the sign step.
     *
     * @param callable $middleware Middleware function to add.
     * @param string   $name       Name of the middleware.
     */
    public function appendSign(callable $middleware, $name = null)
    {
        $this->add(self::SIGN, $name, $middleware);
    }

    /**
     * Prepend a middleware to the sign step.
     *
     * @param callable $middleware Middleware function to add.
     * @param string   $name       Name of the middleware.
     */
    public function prependSign(callable $middleware, $name = null)
    {
        $this->add(self::SIGN, $name, $middleware, true);
    }

    /**
     * Append a middleware to the attempt step.
     *
     * @param callable $middleware Middleware function to add.
     * @param string   $name       Name of the middleware.
     */
    public function appendAttempt(callable $middleware, $name = null)
    {
        $this->add(self::ATTEMPT, $name, $middleware);
    }

    /**
     * Prepend a middleware to the attempt step.
     *
     * @param callable $middleware Middleware function to add.
     * @param string   $name       Name of the middleware.
     */
    public function prependAttempt(callable $middleware, $name = null)
    {
        $this->add(self::ATTEMPT, $name, $middleware, true);
    }

    /**
     * Add a middleware before the given middleware by name.
     *
     * @param string|callable $findName   Add before this
     * @param string          $withName   Optional name to give the middleware
     * @param callable        $middleware Middleware to add.
     */
    public function before($findName, $withName, callable $middleware)
    {
        $this->splice($findName, $withName, $middleware, true);
    }

    /**
     * Add a middleware after the given middleware by name.
     *
     * @param string|callable $findName   Add after this
     * @param string          $withName   Optional name to give the middleware
     * @param callable        $middleware Middleware to add.
     */
    public function after($findName, $withName, callable $middleware)
    {
        $this->splice($findName, $withName, $middleware, false);
    }

    /**
     * Remove a middleware by name or by instance from the list.
     *
     * @param string|callable $nameOrInstance Middleware to remove.
     */
    public function remove($nameOrInstance)
    {
        if (is_callable($nameOrInstance)) {
            $this->removeByInstance($nameOrInstance);
        } elseif (is_string($nameOrInstance)) {
            $this->removeByName($nameOrInstance);
        }
    }

    /**
     * Interpose a function between each middleware (e.g., allowing for a trace
     * through the middleware layers).
     *
     * The interpose function is a function that accepts a "step" argument as a
     * string and a "name" argument string. This function must then return a
     * function that accepts the next handler in the list. This function must
     * then return a function that accepts a CommandInterface and optional
     * RequestInterface and returns a promise that is fulfilled with an
     * Aws\ResultInterface or rejected with an Aws\Exception\AwsException
     * object.
     *
     * @param callable|null $fn Pass null to remove any previously set function
     */
    public function interpose(callable $fn = null)
    {
        $this->sorted = null;
        $this->interposeFn = $fn;
    }

    /**
     * Compose the middleware and handler into a single callable function.
     *
     * @return callable
     */
    public function resolve()
    {
        if (!($prev = $this->handler)) {
            throw new \LogicException('No handler has been specified');
        }

        if ($this->sorted === null) {
            $this->sortMiddleware();
        }

        foreach ($this->sorted as $fn) {
            $prev = $fn($prev);
        }

        return $prev;
    }

    /**
     * @return int
     */
    #[\ReturnTypeWillChange]
    public function count()
    {
        return count($this->steps[self::INIT])
            + count($this->steps[self::VALIDATE])
            + count($this->steps[self::BUILD])
            + count($this->steps[self::SIGN])
            + count($this->steps[self::ATTEMPT]);
    }

    /**
     * Splices a function into the middleware list at a specific position.
     *
     * @param          $findName
     * @param          $withName
     * @param callable $middleware
     * @param          $before
     */
    private function splice($findName, $withName, callable $middleware, $before)
    {
        if (!isset($this->named[$findName])) {
            throw new \InvalidArgumentException("$findName not found");
        }

        $idx = $this->sorted = null;
        $step = $this->named[$findName];

        if ($withName) {
            $this->named[$withName] = $step;
        }

        foreach ($this->steps[$step] as $i => $tuple) {
            if ($tuple[1] === $findName) {
                $idx = $i;
                break;
            }
        }

        $replacement = $before
            ? [$this->steps[$step][$idx], [$middleware, $withName]]
            : [[$middleware, $withName], $this->steps[$step][$idx]];
        array_splice($this->steps[$step], $idx, 1, $replacement);
    }

    /**
     * Provides a debug string for a given callable.
     *
     * @param array|callable $fn Function to write as a string.
     *
     * @return string
     */
    private function debugCallable($fn)
    {
        if (is_string($fn)) {
            return "callable({$fn})";
        }

        if (is_array($fn)) {
            $ele = is_string($fn[0]) ? $fn[0] : get_class($fn[0]);
            return "callable(['{$ele}', '{$fn[1]}'])";
        }

        return 'callable(' . spl_object_hash($fn) . ')';
    }

    /**
     * Sort the middleware, and interpose if needed in the sorted list.
     */
    private function sortMiddleware()
    {
        $this->sorted = [];

        if (!$this->interposeFn) {
            foreach ($this->steps as $step) {
                foreach ($step as $fn) {
                    $this->sorted[] = $fn[0];
                }
            }
            return;
        }

        $ifn = $this->interposeFn;
        // Interpose the interposeFn into the handler stack.
        foreach ($this->steps as $stepName => $step) {
            foreach ($step as $fn) {
                $this->sorted[] = $ifn($stepName, $fn[1]);
                $this->sorted[] = $fn[0];
            }
        }
    }

    private function removeByName($name)
    {
        if (!isset($this->named[$name])) {
            return;
        }

        $this->sorted = null;
        $step = $this->named[$name];
        $this->steps[$step] = array_values(
            array_filter(
                $this->steps[$step],
                function ($tuple) use ($name) {
                    return $tuple[1] !== $name;
                }
            )
        );
    }

    private function removeByInstance(callable $fn)
    {
        foreach ($this->steps as $k => $step) {
            foreach ($step as $j => $tuple) {
                if ($tuple[0] === $fn) {
                    $this->sorted = null;
                    unset($this->named[$this->steps[$k][$j][1]]);
                    unset($this->steps[$k][$j]);
                }
            }
        }
    }

    /**
     * Add a middleware to a step.
     *
     * @param string   $step       Middleware step.
     * @param string   $name       Middleware name.
     * @param callable $middleware Middleware function to add.
     * @param bool     $prepend    Prepend instead of append.
     */
    private function add($step, $name, callable $middleware, $prepend = false)
    {
        $this->sorted = null;

        if ($prepend) {
            $this->steps[$step][] = [$middleware, $name];
        } else {
            array_unshift($this->steps[$step], [$middleware, $name]);
        }

        if ($name) {
            $this->named[$name] = $step;
        }
    }
}


<?php
namespace Aws;

/**
 * Trait implementing ToArrayInterface, \ArrayAccess, \Countable, and
 * \IteratorAggregate
 */
trait HasDataTrait
{
    /** @var array */
    private $data = [];

    /**
     * @return \Traversable
     */
    #[\ReturnTypeWillChange]
    public function getIterator()
    {
        return new \ArrayIterator($this->data);
    }

    /**
     * This method returns a reference to the variable to allow for indirect
     * array modification (e.g., $foo['bar']['baz'] = 'qux').
     *
     * @param $offset
     *
     * @return mixed|null
     */
    #[\ReturnTypeWillChange]
    public function & offsetGet($offset)
    {
        if (isset($this->data[$offset])) {
            return $this->data[$offset];
        }

        $value = null;
        return $value;
    }

    /**
     * @return void
     */
    #[\ReturnTypeWillChange]
    public function offsetSet($offset, $value)
    {
        $this->data[$offset] = $value;
    }

    /**
     * @return bool
     */
    #[\ReturnTypeWillChange]
    public function offsetExists($offset)
    {
        return isset($this->data[$offset]);
    }

    /**
     * @return void
     */
    #[\ReturnTypeWillChange]
    public function offsetUnset($offset)
    {
        unset($this->data[$offset]);
    }

    public function toArray()
    {
        return $this->data;
    }

    /**
     * @return int
     */
    #[\ReturnTypeWillChange]
    public function count()
    {
        return count($this->data);
    }
}


<?php
namespace Aws;

use GuzzleHttp\Psr7\StreamDecoratorTrait;
use Psr\Http\Message\StreamInterface;

/**
 * Stream decorator that calculates a rolling hash of the stream as it is read.
 */
class HashingStream implements StreamInterface
{
    use StreamDecoratorTrait;

    /** @var StreamInterface */
    private $stream;

    /** @var HashInterface */
    private $hash;

    /** @var callable|null */
    private $callback;

    /**
     * @param StreamInterface $stream     Stream that is being read.
     * @param HashInterface   $hash       Hash used to calculate checksum.
     * @param callable        $onComplete Optional function invoked when the
     *                                    hash calculation is completed.
     */
    public function __construct(
        StreamInterface $stream,
        HashInterface $hash,
        callable $onComplete = null
    ) {
        $this->stream = $stream;
        $this->hash = $hash;
        $this->callback = $onComplete;
    }

    public function read($length)
    {
        $data = $this->stream->read($length);
        $this->hash->update($data);
        if ($this->eof()) {
            $result = $this->hash->complete();
            if ($this->callback) {
                call_user_func($this->callback, $result);
            }
        }

        return $data;
    }

    public function seek($offset, $whence = SEEK_SET)
    {
        if ($offset === 0) {
            $this->hash->reset();
            return $this->stream->seek($offset);
        }

        // Seeking arbitrarily is not supported.
        return false;
    }
}


<?php
namespace Aws;

/**
 * Interface that allows implementing various incremental hashes.
 */
interface HashInterface
{
    /**
     * Adds data to the hash.
     *
     * @param string $data Data to add to the hash
     */
    public function update($data);

    /**
     * Finalizes the incremental hash and returns the resulting digest.
     *
     * @return string
     */
    public function complete();

    /**
     * Removes all data from the hash, effectively starting a new hash.
     */
    public function reset();
}


<?php
namespace Aws;

trait HasMonitoringEventsTrait
{
    private $monitoringEvents = [];

    /**
     * Get client-side monitoring events attached to this object. Each event is
     * represented as an associative array within the returned array.
     *
     * @return array
     */
    public function getMonitoringEvents()
    {
        return $this->monitoringEvents;
    }

    /**
     * Prepend a client-side monitoring event to this object's event list
     *
     * @param array $event
     */
    public function prependMonitoringEvent(array $event)
    {
        array_unshift($this->monitoringEvents, $event);
    }

    /**
     * Append a client-side monitoring event to this object's event list
     *
     * @param array $event
     */
    public function appendMonitoringEvent(array $event)
    {
        $this->monitoringEvents []= $event;
    }
}


<?php
namespace Aws;

use Psr\Http\Message\RequestInterface;
use Aws\Exception\AwsException;

/**
 * Represents a history container that is required when using the history
 * middleware.
 */
class History implements \Countable, \IteratorAggregate
{
    private $maxEntries;
    private $entries = array();

    /**
     * @param int $maxEntries Maximum number of entries to store.
     */
    public function __construct($maxEntries = 10)
    {
        $this->maxEntries = $maxEntries;
    }

    /**
     * @return int
     */
    #[\ReturnTypeWillChange]
    public function count()
    {
        return count($this->entries);
    }

    #[\ReturnTypeWillChange]
    public function getIterator()
    {
        return new \ArrayIterator(array_values($this->entries));
    }

    /**
     * Get the last finished command seen by the history container.
     *
     * @return CommandInterface
     * @throws \LogicException if no commands have been seen.
     */
    public function getLastCommand()
    {
        if (!$this->entries) {
            throw new \LogicException('No commands received');
        }

        return end($this->entries)['command'];
    }

    /**
     * Get the last finished request seen by the history container.
     *
     * @return RequestInterface
     * @throws \LogicException if no requests have been seen.
     */
    public function getLastRequest()
    {
        if (!$this->entries) {
            throw new \LogicException('No requests received');
        }

        return end($this->entries)['request'];
    }

    /**
     * Get the last received result or exception.
     *
     * @return ResultInterface|AwsException
     * @throws \LogicException if no return values have been received.
     */
    public function getLastReturn()
    {
        if (!$this->entries) {
            throw new \LogicException('No entries');
        }

        $last = end($this->entries);

        if (isset($last['result'])) {
            return $last['result'];
        }

        if (isset($last['exception'])) {
            return $last['exception'];
        }

        throw new \LogicException('No return value for last entry.');
    }

    /**
     * Initiate an entry being added to the history.
     *
     * @param CommandInterface $cmd Command be executed.
     * @param RequestInterface $req Request being sent.
     *
     * @return string Returns the ticket used to finish the entry.
     */
    public function start(CommandInterface $cmd, RequestInterface $req)
    {
        $ticket = uniqid();
        $this->entries[$ticket] = [
            'command'   => $cmd,
            'request'   => $req,
            'result'    => null,
            'exception' => null,
        ];

        return $ticket;
    }

    /**
     * Finish adding an entry to the history container.
     *
     * @param string $ticket Ticket returned from the start call.
     * @param mixed  $result The result (an exception or AwsResult).
     */
    public function finish($ticket, $result)
    {
        if (!isset($this->entries[$ticket])) {
            throw new \InvalidArgumentException('Invalid history ticket');
        }

        if (isset($this->entries[$ticket]['result'])
            || isset($this->entries[$ticket]['exception'])
        ) {
            throw new \LogicException('History entry is already finished');
        }

        if ($result instanceof \Exception) {
            $this->entries[$ticket]['exception'] = $result;
        } else {
            $this->entries[$ticket]['result'] = $result;
        }

        if (count($this->entries) >= $this->maxEntries) {
            $this->entries = array_slice($this->entries, -$this->maxEntries, null, true);
        }
    }

    /**
     * Flush the history
     */
    public function clear()
    {
        $this->entries = [];
    }

    /**
     * Converts the history to an array.
     *
     * @return array
     */
    public function toArray()
    {
        return array_values($this->entries);
    }
}


<?php
namespace Aws;

use Aws\Api\Service;
use Psr\Http\Message\RequestInterface;

/**
 * @internal Middleware that auto fills parameters with `idempotencyToken` trait
 */
class IdempotencyTokenMiddleware
{
    /** @var Service */
    private $service;
    /** @var string */
    private $bytesGenerator;
    /** @var callable */
    private $nextHandler;

    /**
     * Creates a middleware that populates operation parameter
     * with trait 'idempotencyToken' enabled with a random UUIDv4
     *
     * One of following functions needs to be available
     * in order to generate random bytes used for UUID
     * (SDK will attempt to utilize function in following order):
     *  - random_bytes (requires PHP 7.0 or above)
     *  - openssl_random_pseudo_bytes (requires 'openssl' module enabled)
     *  - mcrypt_create_iv (requires 'mcrypt' module enabled)
     *
     * You may also supply a custom bytes generator as an optional second
     * parameter.
     *
     * @param \Aws\Api\Service $service
     * @param callable|null $bytesGenerator
     *
     * @return callable
     */
    public static function wrap(
        Service $service,
        callable $bytesGenerator = null
    ) {
        return function (callable $handler) use ($service, $bytesGenerator) {
            return new self($handler, $service, $bytesGenerator);
        };
    }

    public function __construct(
        callable $nextHandler,
        Service $service,
        callable $bytesGenerator = null
    ) {
        $this->bytesGenerator = $bytesGenerator
            ?: $this->findCompatibleRandomSource();
        $this->service = $service;
        $this->nextHandler = $nextHandler;
    }

    public function __invoke(
        CommandInterface $command,
        RequestInterface $request = null
    ) {
        $handler = $this->nextHandler;
        if ($this->bytesGenerator) {
            $operation = $this->service->getOperation($command->getName());
            $members = $operation->getInput()->getMembers();
            foreach ($members as $member => $value) {
                if ($value['idempotencyToken']) {
                    $bytes = call_user_func($this->bytesGenerator, 16);
                    // populating UUIDv4 only when the parameter is not set
                    $command[$member] = $command[$member]
                        ?: $this->getUuidV4($bytes);
                    // only one member could have the trait enabled
                    break;
                }
            }
        }
        return $handler($command, $request);
    }

    /**
     * This function generates a random UUID v4 string,
     * which is used as auto filled token value.
     *
     * @param string $bytes 16 bytes of pseudo-random bytes
     * @return string
     * More information about UUID v4, see:
     * https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_.28random.29
     * https://tools.ietf.org/html/rfc4122#page-14
     */
    private static function getUuidV4($bytes)
    {
        // set version to 0100
        $bytes[6] = chr(ord($bytes[6]) & 0x0f | 0x40);
        // set bits 6-7 to 10
        $bytes[8] = chr(ord($bytes[8]) & 0x3f | 0x80);
        return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($bytes), 4));
    }

    /**
     * This function decides the PHP function used in generating random bytes.
     *
     * @return callable|null
     */
    private function findCompatibleRandomSource()
    {
        if (function_exists('random_bytes')) {
            return 'random_bytes';
        }

        if (function_exists('openssl_random_pseudo_bytes')) {
            return 'openssl_random_pseudo_bytes';
        }

        if (function_exists('mcrypt_create_iv')) {
            return 'mcrypt_create_iv';
        }
    }
}


<?php
namespace Aws;

use Aws\Api\Service;

/**
 * Validates the required input parameters of commands are non empty
 *
 * @internal
 */
class InputValidationMiddleware
{

    /** @var callable */
    private $nextHandler;

    /** @var array */
    private $mandatoryAttributeList;

    /** @var Service */
    private $service;

    /**
     * Create a middleware wrapper function.
     *
     * @param Service $service
     * @param array $mandatoryAttributeList
     * @return callable     */
    public static function wrap(Service $service, $mandatoryAttributeList) {
        if (!is_array($mandatoryAttributeList) ||
            array_filter($mandatoryAttributeList, 'is_string') !== $mandatoryAttributeList
        ) {
            throw new \InvalidArgumentException(
                "The mandatory attribute list must be an array of strings"
            );
        }
        return function (callable $handler) use ($service, $mandatoryAttributeList) {
            return new self($handler, $service, $mandatoryAttributeList);
        };
    }

    public function __construct(
        callable $nextHandler,
        Service $service,
        $mandatoryAttributeList
    ) {
        $this->service = $service;
        $this->nextHandler = $nextHandler;
        $this->mandatoryAttributeList = $mandatoryAttributeList;
    }

    public function __invoke(CommandInterface $cmd) {
        $nextHandler = $this->nextHandler;
        $op = $this->service->getOperation($cmd->getName())->toArray();
        if (!empty($op['input']['shape'])) {
            $service = $this->service->toArray();
            if (!empty($input = $service['shapes'][$op['input']['shape']])) {
                if (!empty($input['required'])) {
                    foreach ($input['required'] as $key => $member) {
                        if (in_array($member, $this->mandatoryAttributeList)) {
                            $argument = is_string($cmd[$member]) ?  trim($cmd[$member]) : $cmd[$member];
                            if ($argument === '' || $argument === null) {
                                $commandName = $cmd->getName();
                                throw new \InvalidArgumentException(
                                    "The {$commandName} operation requires non-empty parameter: {$member}"
                                );
                            }
                        }
                    }
                }
            }
        }
        return $nextHandler($cmd);
    }

}


<?php
namespace Aws;

/**
 * Loads JSON files and compiles them into PHP arrays.
 *
 * @internal Please use json_decode instead.
 * @deprecated
 */
class JsonCompiler
{
    const CACHE_ENV = 'AWS_PHP_CACHE_DIR';

    /**
     * Loads a JSON file from cache or from the JSON file directly.
     *
     * @param string $path Path to the JSON file to load.
     *
     * @return mixed
     */
    public function load($path)
    {
        return load_compiled_json($path);
    }
}


<?php
namespace Aws;

/**
 * Simple in-memory LRU cache that limits the number of cached entries.
 *
 * The LRU cache is implemented using PHP's ordered associative array. When
 * accessing an element, the element is removed from the hash and re-added to
 * ensure that recently used items are always at the end of the list while
 * least recently used are at the beginning. When a value is added to the
 * cache, if the number of cached items exceeds the allowed number, the first
 * N number of items are removed from the array.
 */
class LruArrayCache implements CacheInterface, \Countable
{
    /** @var int */
    private $maxItems;

    /** @var array */
    private $items = array();

    /**
     * @param int $maxItems Maximum number of allowed cache items.
     */
    public function __construct($maxItems = 1000)
    {
        $this->maxItems = $maxItems;
    }

    public function get($key)
    {
        if (!isset($this->items[$key])) {
            return null;
        }

        $entry = $this->items[$key];

        // Ensure the item is not expired.
        if (!$entry[1] || time() < $entry[1]) {
            // LRU: remove the item and push it to the end of the array.
            unset($this->items[$key]);
            $this->items[$key] = $entry;
            return $entry[0];
        }

        unset($this->items[$key]);
        return null;
    }

    public function set($key, $value, $ttl = 0)
    {
        // Only call time() if the TTL is not 0/false/null
        $ttl = $ttl ? time() + $ttl : 0;
        $this->items[$key] = [$value, $ttl];

        // Determine if there are more items in the cache than allowed.
        $diff = count($this->items) - $this->maxItems;

        // Clear out least recently used items.
        if ($diff > 0) {
            // Reset to the beginning of the array and begin unsetting.
            reset($this->items);
            for ($i = 0; $i < $diff; $i++) {
                unset($this->items[key($this->items)]);
                next($this->items);
            }
        }
    }

    public function remove($key)
    {
        unset($this->items[$key]);
    }

    /**
     * @return int
     */
    #[\ReturnTypeWillChange]
    public function count()
    {
        return count($this->items);
    }
}


<?php
namespace Aws;

use Aws\Api\Service;
use Aws\Api\Validator;
use Aws\Credentials\CredentialsInterface;
use Aws\EndpointV2\EndpointProviderV2;
use Aws\Exception\AwsException;
use Aws\Token\TokenAuthorization;
use Aws\Token\TokenInterface;
use GuzzleHttp\Promise;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\LazyOpenStream;
use Psr\Http\Message\RequestInterface;

final class Middleware
{
    /**
     * Middleware used to allow a command parameter (e.g., "SourceFile") to
     * be used to specify the source of data for an upload operation.
     *
     * @param Service $api
     * @param string  $bodyParameter
     * @param string  $sourceParameter
     *
     * @return callable
     */
    public static function sourceFile(
        Service $api,
        $bodyParameter = 'Body',
        $sourceParameter = 'SourceFile'
    ) {
        return function (callable $handler) use (
            $api,
            $bodyParameter,
            $sourceParameter
        ) {
            return function (
                CommandInterface $command,
                RequestInterface $request = null)
            use (
                $handler,
                $api,
                $bodyParameter,
                $sourceParameter
            ) {
                $operation = $api->getOperation($command->getName());
                $source = $command[$sourceParameter];

                if ($source !== null
                    && $operation->getInput()->hasMember($bodyParameter)
                ) {
                    $command[$bodyParameter] = new LazyOpenStream($source, 'r');
                    unset($command[$sourceParameter]);
                }

                return $handler($command, $request);
            };
        };
    }

    /**
     * Adds a middleware that uses client-side validation.
     *
     * @param Service $api API being accessed.
     *
     * @return callable
     */
    public static function validation(Service $api, Validator $validator = null)
    {
        $validator = $validator ?: new Validator();
        return function (callable $handler) use ($api, $validator) {
            return function (
                CommandInterface $command,
                RequestInterface $request = null
            ) use ($api, $validator, $handler) {
                if ($api->isModifiedModel()) {
                    $api = new Service(
                        $api->getDefinition(),
                        $api->getProvider()
                    );
                }
                $operation = $api->getOperation($command->getName());
                $validator->validate(
                    $command->getName(),
                    $operation->getInput(),
                    $command->toArray()
                );
                return $handler($command, $request);
            };
        };
    }

    /**
     * Builds an HTTP request for a command.
     *
     * @param callable $serializer Function used to serialize a request for a
     *                             command.
     * @param EndpointProviderV2 | null $endpointProvider
     * @param array $providerArgs
     * @return callable
     */
    public static function requestBuilder(
        $serializer,
        $endpointProvider = null,
        array $providerArgs = null
    )
    {
        return function (callable $handler) use ($serializer, $endpointProvider, $providerArgs) {
            return function (CommandInterface $command) use ($serializer, $handler, $endpointProvider, $providerArgs) {
                return $handler($command, $serializer($command, $endpointProvider, $providerArgs));
            };
        };
    }

    /**
     * Creates a middleware that signs requests for a command.
     *
     * @param callable $credProvider      Credentials provider function that
     *                                    returns a promise that is resolved
     *                                    with a CredentialsInterface object.
     * @param callable $signatureFunction Function that accepts a Command
     *                                    object and returns a
     *                                    SignatureInterface.
     *
     * @return callable
     */
    public static function signer(callable $credProvider, callable $signatureFunction, $tokenProvider = null)
    {
        return function (callable $handler) use ($signatureFunction, $credProvider, $tokenProvider) {
            return function (
                CommandInterface $command,
                RequestInterface $request
            ) use ($handler, $signatureFunction, $credProvider, $tokenProvider) {
                $signer = $signatureFunction($command);
                if ($signer instanceof TokenAuthorization) {
                    return $tokenProvider()->then(
                        function (TokenInterface $token)
                        use ($handler, $command, $signer, $request) {
                            return $handler(
                                $command,
                                $signer->authorizeRequest($request, $token)
                            );
                        }
                    );
                } else {
                    return $credProvider()->then(
                        function (CredentialsInterface $creds)
                        use ($handler, $command, $signer, $request) {
                            return $handler(
                                $command,
                                $signer->signRequest($request, $creds)
                            );
                        }
                    );
                }
            };
        };
    }

    /**
     * Creates a middleware that invokes a callback at a given step.
     *
     * The tap callback accepts a CommandInterface and RequestInterface as
     * arguments but is not expected to return a new value or proxy to
     * downstream middleware. It's simply a way to "tap" into the handler chain
     * to debug or get an intermediate value.
     *
     * @param callable $fn Tap function
     *
     * @return callable
     */
    public static function tap(callable $fn)
    {
        return function (callable $handler) use ($fn) {
            return function (
                CommandInterface $command,
                RequestInterface $request = null
            ) use ($handler, $fn) {
                $fn($command, $request);
                return $handler($command, $request);
            };
        };
    }

    /**
     * Middleware wrapper function that retries requests based on the boolean
     * result of invoking the provided "decider" function.
     *
     * If no delay function is provided, a simple implementation of exponential
     * backoff will be utilized.
     *
     * @param callable $decider Function that accepts the number of retries,
     *                          a request, [result], and [exception] and
     *                          returns true if the command is to be retried.
     * @param callable $delay   Function that accepts the number of retries and
     *                          returns the number of milliseconds to delay.
     * @param bool $stats       Whether to collect statistics on retries and the
     *                          associated delay.
     *
     * @return callable
     */
    public static function retry(
        callable $decider = null,
        callable $delay = null,
        $stats = false
    ) {
        $decider = $decider ?: RetryMiddleware::createDefaultDecider();
        $delay = $delay ?: [RetryMiddleware::class, 'exponentialDelay'];

        return function (callable $handler) use ($decider, $delay, $stats) {
            return new RetryMiddleware($decider, $delay, $handler, $stats);
        };
    }
    /**
     * Middleware wrapper function that adds an invocation id header to
     * requests, which is only applied after the build step.
     *
     * This is a uniquely generated UUID to identify initial and subsequent
     * retries as part of a complete request lifecycle.
     *
     * @return callable
     */
    public static function invocationId()
    {
        return function (callable $handler) {
            return function (
                CommandInterface $command,
                RequestInterface $request
            ) use ($handler){
                return $handler($command, $request->withHeader(
                    'aws-sdk-invocation-id',
                    md5(uniqid(gethostname(), true))
                ));
            };
        };
    }
    /**
     * Middleware wrapper function that adds a Content-Type header to requests.
     * This is only done when the Content-Type has not already been set, and the
     * request body's URI is available. It then checks the file extension of the
     * URI to determine the mime-type.
     *
     * @param array $operations Operations that Content-Type should be added to.
     *
     * @return callable
     */
    public static function contentType(array $operations)
    {
        return function (callable $handler) use ($operations) {
            return function (
                CommandInterface $command,
                RequestInterface $request = null
            ) use ($handler, $operations) {
                if (!$request->hasHeader('Content-Type')
                    && in_array($command->getName(), $operations, true)
                    && ($uri = $request->getBody()->getMetadata('uri'))
                ) {
                    $request = $request->withHeader(
                        'Content-Type',
                        Psr7\MimeType::fromFilename($uri) ?: 'application/octet-stream'
                    );
                }

                return $handler($command, $request);
            };
        };
    }
    /**
     * Middleware wrapper function that adds a trace id header to requests
     * from clients instantiated in supported Lambda runtime environments.
     *
     * The purpose for this header is to track and stop Lambda functions
     * from being recursively invoked due to misconfigured resources.
     *
     * @return callable
     */
    public static function recursionDetection()
    {
        return function (callable $handler) {
            return function (
                CommandInterface $command,
                RequestInterface $request
            ) use ($handler){
                $isLambda = getenv('AWS_LAMBDA_FUNCTION_NAME');
                $traceId = str_replace('\e', '\x1b', getenv('_X_AMZN_TRACE_ID'));

                if ($isLambda && $traceId) {
                    if (!$request->hasHeader('X-Amzn-Trace-Id')) {
                        $ignoreChars = ['=', ';', ':', '+', '&', '[', ']', '{', '}', '"', '\'', ','];
                        $traceIdEncoded = rawurlencode(stripcslashes($traceId));

                        foreach($ignoreChars as $char) {
                            $encodedChar = rawurlencode($char);
                            $traceIdEncoded = str_replace($encodedChar, $char,  $traceIdEncoded);
                        }

                        return $handler($command, $request->withHeader(
                            'X-Amzn-Trace-Id',
                            $traceIdEncoded
                        ));
                    }
                }
                return $handler($command, $request);
            };
        };
    }
    /**
     * Tracks command and request history using a history container.
     *
     * This is useful for testing.
     *
     * @param History $history History container to store entries.
     *
     * @return callable
     */
    public static function history(History $history)
    {
        return function (callable $handler) use ($history) {
            return function (
                CommandInterface $command,
                RequestInterface $request = null
            ) use ($handler, $history) {
                $ticket = $history->start($command, $request);
                return $handler($command, $request)
                    ->then(
                        function ($result) use ($history, $ticket) {
                            $history->finish($ticket, $result);
                            return $result;
                        },
                        function ($reason) use ($history, $ticket) {
                            $history->finish($ticket, $reason);
                            return Promise\Create::rejectionFor($reason);
                        }
                    );
            };
        };
    }

    /**
     * Creates a middleware that applies a map function to requests as they
     * pass through the middleware.
     *
     * @param callable $f Map function that accepts a RequestInterface and
     *                    returns a RequestInterface.
     *
     * @return callable
     */
    public static function mapRequest(callable $f)
    {
        return function (callable $handler) use ($f) {
            return function (
                CommandInterface $command,
                RequestInterface $request = null
            ) use ($handler, $f) {
                return $handler($command, $f($request));
            };
        };
    }

    /**
     * Creates a middleware that applies a map function to commands as they
     * pass through the middleware.
     *
     * @param callable $f Map function that accepts a command and returns a
     *                    command.
     *
     * @return callable
     */
    public static function mapCommand(callable $f)
    {
        return function (callable $handler) use ($f) {
            return function (
                CommandInterface $command,
                RequestInterface $request = null
            ) use ($handler, $f) {
                return $handler($f($command), $request);
            };
        };
    }

    /**
     * Creates a middleware that applies a map function to results.
     *
     * @param callable $f Map function that accepts an Aws\ResultInterface and
     *                    returns an Aws\ResultInterface.
     *
     * @return callable
     */
    public static function mapResult(callable $f)
    {
        return function (callable $handler) use ($f) {
            return function (
                CommandInterface $command,
                RequestInterface $request = null
            ) use ($handler, $f) {
                return $handler($command, $request)->then($f);
            };
        };
    }

    public static function timer()
    {
        return function (callable $handler) {
            return function (
                CommandInterface $command,
                RequestInterface $request = null
            ) use ($handler) {
                $start = microtime(true);
                return $handler($command, $request)
                    ->then(
                        function (ResultInterface $res) use ($start) {
                            if (!isset($res['@metadata'])) {
                                $res['@metadata'] = [];
                            }
                            if (!isset($res['@metadata']['transferStats'])) {
                                $res['@metadata']['transferStats'] = [];
                            }

                            $res['@metadata']['transferStats']['total_time']
                                = microtime(true) - $start;

                            return $res;
                        },
                        function ($err) use ($start) {
                            if ($err instanceof AwsException) {
                                $err->setTransferInfo([
                                    'total_time' => microtime(true) - $start,
                                ] + $err->getTransferInfo());
                            }
                            return Promise\Create::rejectionFor($err);
                        }
                    );
            };
        };
    }
}


<?php
namespace Aws;

use Aws\Exception\AwsException;
use GuzzleHttp\Promise;
use GuzzleHttp\Promise\RejectedPromise;
use Psr\Http\Message\RequestInterface;
use Exception;

/**
 * Returns promises that are rejected or fulfilled using a queue of
 * Aws\ResultInterface and Aws\Exception\AwsException objects.
 */
class MockHandler implements \Countable
{
    private $queue;
    private $lastCommand;
    private $lastRequest;
    private $onFulfilled;
    private $onRejected;

    /**
     * The passed in value must be an array of {@see Aws\ResultInterface} or
     * {@see AwsException} objects that acts as a queue of results or
     * exceptions to return each time the handler is invoked.
     *
     * @param array    $resultOrQueue
     * @param callable $onFulfilled Callback to invoke when the return value is fulfilled.
     * @param callable $onRejected  Callback to invoke when the return value is rejected.
     */
    public function __construct(
        array $resultOrQueue = [],
        callable $onFulfilled = null,
        callable $onRejected = null
    ) {
        $this->queue = [];
        $this->onFulfilled = $onFulfilled;
        $this->onRejected = $onRejected;

        if ($resultOrQueue) {
            call_user_func_array([$this, 'append'], array_values($resultOrQueue));
        }
    }

    /**
     * Adds one or more variadic ResultInterface or AwsException objects to the
     * queue.
     */
    public function append()
    {
        foreach (func_get_args() as $value) {
            if ($value instanceof ResultInterface
                || $value instanceof Exception
                || is_callable($value)
            ) {
                $this->queue[] = $value;
            } else {
                throw new \InvalidArgumentException('Expected an Aws\ResultInterface or Exception.');
            }
        }
    }

    /**
     * Adds one or more \Exception or \Throwable to the queue
     */
    public function appendException()
    {
        foreach (func_get_args() as $value) {
            if ($value instanceof \Exception || $value instanceof \Throwable) {
                $this->queue[] = $value;
            } else {
                throw new \InvalidArgumentException('Expected an \Exception or \Throwable.');
            }
        }
    }

    public function __invoke(
        CommandInterface $command,
        RequestInterface $request
    ) {
        if (!$this->queue) {
            $last = $this->lastCommand
                ? ' The last command sent was ' . $this->lastCommand->getName() . '.'
                : '';
            throw new \RuntimeException('Mock queue is empty. Trying to send a '
                . $command->getName() . ' command failed.' . $last);
        }

        $this->lastCommand = $command;
        $this->lastRequest = $request;

        $result = array_shift($this->queue);

        if (is_callable($result)) {
            $result = $result($command, $request);
        }

        if ($result instanceof \Exception) {
            $result = new RejectedPromise($result);
        } else {
            // Add an effective URI and statusCode if not present.
            $meta = $result['@metadata'];
            if (!isset($meta['effectiveUri'])) {
                $meta['effectiveUri'] = (string) $request->getUri();
            }
            if (!isset($meta['statusCode'])) {
                $meta['statusCode'] = 200;
            }
            $result['@metadata'] = $meta;
            $result = Promise\Create::promiseFor($result);
        }

        $result->then($this->onFulfilled, $this->onRejected);

        return $result;
    }

    /**
     * Get the last received request.
     *
     * @return RequestInterface
     */
    public function getLastRequest()
    {
        return $this->lastRequest;
    }

    /**
     * Get the last received command.
     *
     * @return CommandInterface
     */
    public function getLastCommand()
    {
        return $this->lastCommand;
    }

    /**
     * Returns the number of remaining items in the queue.
     *
     * @return int
     */
    #[\ReturnTypeWillChange]
    public function count()
    {
        return count($this->queue);
    }
}


<?php
namespace Aws;

/**
 * Interface for adding and retrieving client-side monitoring events
 */
interface MonitoringEventsInterface
{

    /**
     * Get client-side monitoring events attached to this object. Each event is
     * represented as an associative array within the returned array.
     *
     * @return array
     */
    public function getMonitoringEvents();

    /**
     * Prepend a client-side monitoring event to this object's event list
     *
     * @param array $event
     */
    public function prependMonitoringEvent(array $event);

    /**
     * Append a client-side monitoring event to this object's event list
     *
     * @param array $event
     */
    public function appendMonitoringEvent(array $event);

}


<?php
namespace Aws;

use Aws\Endpoint\PartitionEndpointProvider;
use Aws\Endpoint\PartitionInterface;
use Aws\EndpointV2\EndpointProviderV2;
use Aws\EndpointV2\EndpointDefinitionProvider;

class MultiRegionClient implements AwsClientInterface
{
    use AwsClientTrait;

    /** @var AwsClientInterface[] A pool of clients keyed by region. */
    private $clientPool = [];
    /** @var callable */
    private $factory;
    /** @var PartitionInterface */
    private $partition;
    /** @var array */
    private $args;
    /** @var array */
    private $config;
    /** @var HandlerList */
    private $handlerList;
    /** @var array */
    private $aliases;
    /** @var callable */
    private $customHandler;

    public static function getArguments()
    {
        $args = array_intersect_key(
            ClientResolver::getDefaultArguments(),
            ['service' => true, 'region' => true]
        );
        $args['region']['required'] = false;

        return $args + [
            'client_factory' => [
                'type' => 'config',
                'valid' => ['callable'],
                'doc' => 'A callable that takes an array of client'
                    . ' configuration arguments and returns a regionalized'
                    . ' client.',
                'required' => true,
                'internal' => true,
                'default' => function (array $args) {
                    $namespace = manifest($args['service'])['namespace'];
                    $klass = "Aws\\{$namespace}\\{$namespace}Client";
                    $region = isset($args['region']) ? $args['region'] : null;

                    return function (array $args) use ($klass, $region) {
                        if ($region && empty($args['region'])) {
                            $args['region'] = $region;
                        }

                        return new $klass($args);
                    };
                },
            ],
            'partition' => [
                'type'    => 'config',
                'valid'   => ['string', PartitionInterface::class],
                'doc'     => 'AWS partition to connect to. Valid partitions'
                    . ' include "aws," "aws-cn," and "aws-us-gov." Used to'
                    . ' restrict the scope of the mapRegions method.',
                'default' => function (array $args) {
                    $region = isset($args['region']) ? $args['region'] : '';
                    return PartitionEndpointProvider::defaultProvider()
                        ->getPartition($region, $args['service']);
                },
                'fn'      => function ($value, array &$args) {
                    if (is_string($value)) {
                        $value = PartitionEndpointProvider::defaultProvider()
                            ->getPartitionByName($value);
                    }

                    if (!$value instanceof PartitionInterface) {
                        throw new \InvalidArgumentException('No valid partition'
                            . ' was provided. Provide a concrete partition or'
                            . ' the name of a partition (e.g., "aws," "aws-cn,"'
                            . ' or "aws-us-gov").'
                        );
                    }
                    $ruleset = EndpointDefinitionProvider::getEndpointRuleset(
                        $args['service'],
                        isset($args['version']) ? $args['version'] : 'latest'
                    );
                    $partitions = EndpointDefinitionProvider::getPartitions();
                    $args['endpoint_provider'] = new EndpointProviderV2($ruleset, $partitions);
                }
            ],
        ];
    }

    /**
     * The multi-region client constructor accepts the following options:
     *
     * - client_factory: (callable) An optional callable that takes an array of
     *   client configuration arguments and returns a regionalized client.
     * - partition: (Aws\Endpoint\Partition|string) AWS partition to connect to.
     *   Valid partitions include "aws," "aws-cn," and "aws-us-gov." Used to
     *   restrict the scope of the mapRegions method.
     * - region: (string) Region to connect to when no override is provided.
     *   Used to create the default client factory and determine the appropriate
     *   AWS partition when present.
     *
     * @param array $args Client configuration arguments.
     */
    public function __construct(array $args = [])
    {
        if (!isset($args['service'])) {
            $args['service'] = $this->parseClass();
        }

        $this->handlerList = new HandlerList(function (
            CommandInterface $command
        ) {
            list($region, $args) = $this->getRegionFromArgs($command->toArray());
            $command = $this->getClientFromPool($region)
                ->getCommand($command->getName(), $args);

            if ($this->isUseCustomHandler()) {
                $command->getHandlerList()->setHandler($this->customHandler);
            }

            return $this->executeAsync($command);
        });

        $argDefinitions = static::getArguments();
        $resolver = new ClientResolver($argDefinitions);
        $args = $resolver->resolve($args, $this->handlerList);
        $this->config = $args['config'];
        $this->factory = $args['client_factory'];
        $this->partition = $args['partition'];
        $this->args = array_diff_key($args, $args['config']);
    }

    /**
     * Get the region to which the client is configured to send requests by
     * default.
     *
     * @return string
     */
    public function getRegion()
    {
        return $this->getClientFromPool()->getRegion();
    }

    /**
     * Create a command for an operation name.
     *
     * Special keys may be set on the command to control how it behaves,
     * including:
     *
     * - @http: Associative array of transfer specific options to apply to the
     *   request that is serialized for this command. Available keys include
     *   "proxy", "verify", "timeout", "connect_timeout", "debug", "delay", and
     *   "headers".
     * - @region: The region to which the command should be sent.
     *
     * @param string $name Name of the operation to use in the command
     * @param array  $args Arguments to pass to the command
     *
     * @return CommandInterface
     * @throws \InvalidArgumentException if no command can be found by name
     */
    public function getCommand($name, array $args = [])
    {
        return new Command($name, $args, clone $this->getHandlerList());
    }

    public function getConfig($option = null)
    {
        if (null === $option) {
            return $this->config;
        }

        if (isset($this->config[$option])) {
            return $this->config[$option];
        }

        return $this->getClientFromPool()->getConfig($option);
    }

    public function getCredentials()
    {
        return $this->getClientFromPool()->getCredentials();
    }

    public function getHandlerList()
    {
        return $this->handlerList;
    }

    public function getApi()
    {
        return $this->getClientFromPool()->getApi();
    }

    public function getEndpoint()
    {
        return $this->getClientFromPool()->getEndpoint();
    }

    public function useCustomHandler(callable $handler)
    {
        $this->customHandler = $handler;
    }

    private function isUseCustomHandler()
    {
        return isset($this->customHandler);
    }

    /**
     * @param string $region    Omit this argument or pass in an empty string to
     *                          allow the configured client factory to apply the
     *                          region.
     *
     * @return AwsClientInterface
     */
    protected function getClientFromPool($region = '')
    {
        if (empty($this->clientPool[$region])) {
            $factory = $this->factory;
            $this->clientPool[$region] = $factory(
                array_replace($this->args, array_filter(['region' => $region]))
            );
        }

        return $this->clientPool[$region];
    }

    /**
     * Parse the class name and return the "service" name of the client.
     *
     * @return string
     */
    private function parseClass()
    {
        $klass = get_class($this);

        if ($klass === __CLASS__) {
            return '';
        }

        return strtolower(substr($klass, strrpos($klass, '\\') + 1, -17));
    }

    private function getRegionFromArgs(array $args)
    {
        $region = isset($args['@region'])
            ? $args['@region']
            : $this->getRegion();
        unset($args['@region']);

        return [$region, $args];
    }
}


<?php
namespace Aws;

/**
 * Incremental hashing using PHP's hash functions.
 */
class PhpHash implements HashInterface
{
    /** @var resource|\HashContext */
    private $context;

    /** @var string */
    private $algo;

    /** @var array */
    private $options;

    /** @var string */
    private $hash;

    /**
     * @param string $algo Hashing algorithm. One of PHP's hash_algos()
     *     return values (e.g. md5, sha1, etc...).
     * @param array  $options Associative array of hashing options:
     *     - key: Secret key used with the hashing algorithm.
     *     - base64: Set to true to base64 encode the value when complete.
     */
    public function __construct($algo, array $options = [])
    {
        $this->algo = $algo;
        $this->options = $options;
    }

    public function update($data)
    {
        if ($this->hash !== null) {
            $this->reset();
        }

        hash_update($this->getContext(), $data);
    }

    public function complete()
    {
        if ($this->hash) {
            return $this->hash;
        }

        $this->hash = hash_final($this->getContext(), true);

        if (isset($this->options['base64']) && $this->options['base64']) {
            $this->hash = base64_encode($this->hash);
        }

        return $this->hash;
    }

    public function reset()
    {
        $this->context = $this->hash = null;
    }

    /**
     * Get a hash context or create one if needed
     *
     * @return resource|\HashContext
     */
    private function getContext()
    {
        if (!$this->context) {
            $key = isset($this->options['key']) ? $this->options['key'] : '';
            $this->context = hash_init(
                $this->algo,
                $key ? HASH_HMAC : 0,
                $key
            );
        }

        return $this->context;
    }
}


<?php
namespace Aws;

use Aws\Signature\SignatureV4;
use Aws\Endpoint\EndpointProvider;
use GuzzleHttp\Psr7\Uri;
use Psr\Http\Message\RequestInterface;

/**
 * @internal Adds computed values to service operations that need presigned url.
 */
class PresignUrlMiddleware
{
    private $client;
    private $endpointProvider;
    private $nextHandler;
    /** @var array names of operations that require presign url */
    private $commandPool;
    /** @var array query params that are not on the operation's model to add before signing */
    private $extraQueryParams;
    /** @var string */
    private $serviceName;
    /** @var string */
    private $presignParam;
    /** @var bool */
    private $requireDifferentRegion;

    public function __construct(
        array $options,
        $endpointProvider,
        AwsClientInterface $client,
        callable $nextHandler
    ) {
        $this->endpointProvider = $endpointProvider;
        $this->client = $client;
        $this->nextHandler = $nextHandler;
        $this->commandPool = $options['operations'];
        $this->serviceName = $options['service'];
        $this->presignParam = !empty($options['presign_param'])
            ? $options['presign_param']
            : 'PresignedUrl';
        $this->extraQueryParams = !empty($options['extra_query_params'])
            ? $options['extra_query_params']
            : [];
        $this->requireDifferentRegion = !empty($options['require_different_region']);
    }

    public static function wrap(
        AwsClientInterface $client,
        $endpointProvider,
        array $options = []
    ) {
        return function (callable $handler) use ($endpointProvider, $client, $options) {
            $f = new PresignUrlMiddleware($options, $endpointProvider, $client, $handler);
            return $f;
        };
    }

    public function __invoke(CommandInterface $cmd, RequestInterface $request = null)
    {
        if (in_array($cmd->getName(), $this->commandPool)
            && (!isset($cmd->{'__skip' . $cmd->getName()}))
        ) {
            $cmd['DestinationRegion'] = $this->client->getRegion();
            if (!empty($cmd['SourceRegion']) && !empty($cmd[$this->presignParam])) {
                goto nexthandler;
            }
            if (!$this->requireDifferentRegion
                || (!empty($cmd['SourceRegion'])
                    && $cmd['SourceRegion'] !== $cmd['DestinationRegion'])
            ) {
                $cmd[$this->presignParam] = $this->createPresignedUrl($this->client, $cmd);
            }
        }
        nexthandler:
        $nextHandler = $this->nextHandler;
        return $nextHandler($cmd, $request);
    }

    private function createPresignedUrl(
        AwsClientInterface $client,
        CommandInterface $cmd
    ) {
        $cmdName = $cmd->getName();
        $newCmd = $client->getCommand($cmdName, $cmd->toArray());
        // Avoid infinite recursion by flagging the new command.
        $newCmd->{'__skip' . $cmdName} = true;

        // Serialize a request for the operation.
        $request = \Aws\serialize($newCmd);
        // Create the new endpoint for the target endpoint.
        if ($this->endpointProvider instanceof \Aws\EndpointV2\EndpointProviderV2) {
            $providerArgs = array_merge(
                $this->client->getEndpointProviderArgs(),
                ['Region' => $cmd['SourceRegion']]
            );
            $endpoint = $this->endpointProvider->resolveEndpoint($providerArgs)->getUrl();
        } else {
            $endpoint = EndpointProvider::resolve($this->endpointProvider, [
                'region'  => $cmd['SourceRegion'],
                'service' => $this->serviceName,
            ])['endpoint'];
        }

        // Set the request to hit the target endpoint.
        $uri = $request->getUri()->withHost((new Uri($endpoint))->getHost());
        $request = $request->withUri($uri);

        // Create a presigned URL for our generated request.
        $signer = new SignatureV4($this->serviceName, $cmd['SourceRegion']);

        $currentQueryParams = (string) $request->getBody();
        $paramsToAdd = false;
        if (!empty($this->extraQueryParams[$cmdName])) {
            foreach ($this->extraQueryParams[$cmdName] as $param) {
                if (!strpos($currentQueryParams, $param)) {
                    $paramsToAdd =  "&{$param}={$cmd[$param]}";
                }
            }
        }

        return (string) $signer->presign(
            SignatureV4::convertPostToGet($request, $paramsToAdd ?: ""),
            $client->getCredentials()->wait(),
            '+1 hour'
        )->getUri();
    }
}


<?php
namespace Aws;

use Psr\SimpleCache\CacheInterface as SimpleCacheInterface;

class Psr16CacheAdapter implements CacheInterface
{
    /** @var SimpleCacheInterface */
    private $cache;

    public function __construct(SimpleCacheInterface $cache)
    {
        $this->cache = $cache;
    }

    public function get($key)
    {
        return $this->cache->get($key);
    }

    public function set($key, $value, $ttl = 0)
    {
        $this->cache->set($key, $value, $ttl);
    }

    public function remove($key)
    {
        $this->cache->delete($key);
    }
}


<?php
namespace Aws;

use Psr\Cache\CacheItemPoolInterface;

class PsrCacheAdapter implements CacheInterface
{
    /** @var CacheItemPoolInterface */
    private $pool;

    public function __construct(CacheItemPoolInterface $pool)
    {
        $this->pool = $pool;
    }

    public function get($key)
    {
        $item = $this->pool->getItem($key);

        return $item->isHit() ? $item->get() : null;
    }

    public function set($key, $value, $ttl = 0)
    {
        $item = $this->pool->getItem($key);
        $item->set($value);
        if ($ttl > 0) {
            $item->expiresAfter($ttl);
        }

        $this->pool->save($item);
    }

    public function remove($key)
    {
        $this->pool->deleteItem($key);
    }
}


<?php
namespace Aws;

use GuzzleHttp\Psr7;
use Psr\Http\Message\RequestInterface;

/**
 * Used to compress request payloads if the service/operation support it.
 *
 * IMPORTANT: this middleware must be added after the "build" step.
 *
 * @internal
 */
class RequestCompressionMiddleware
{
    private $api;
    private $minimumCompressionSize;
    private $nextHandler;
    private $encodings;
    private $encoding;
    private $encodingMap = [
        'gzip' => 'gzencode'
    ];

    /**
     * Create a middleware wrapper function.
     *
     * @return callable
     */
    public static function wrap(array $config)
    {
        return function (callable $handler) use ($config) {
            return new self($handler, $config);
        };
    }

    public function __construct(callable $nextHandler, $config)
    {
        $this->minimumCompressionSize = $this->determineMinimumCompressionSize($config);
        $this->api = $config['api'];
        $this->nextHandler = $nextHandler;
    }

    public function __invoke(CommandInterface $command, RequestInterface $request)
    {
        if (isset($command['@request_min_compression_size_bytes'])
            && is_int($command['@request_min_compression_size_bytes'])
            && $this->isValidCompressionSize($command['@request_min_compression_size_bytes'])
        ) {
            $this->minimumCompressionSize = $command['@request_min_compression_size_bytes'];
        }
        $nextHandler = $this->nextHandler;
        $operation = $this->api->getOperation($command->getName());
        $requestBodySize = $request->getBody()->getSize();
        $compressionInfo = isset($operation['requestcompression'])
            ? $operation['requestcompression']
            : null;

        if (!$this->shouldCompressRequestBody(
            $compressionInfo,
            $command,
            $operation,
            $requestBodySize
        )) {
            return $nextHandler($command, $request);
        }

        $this->encodings = $compressionInfo['encodings'];
        $request = $this->compressRequestBody($request);

        return $nextHandler($command, $request);
    }

    private function compressRequestBody(
        RequestInterface $request
    ) {
        $fn = $this->determineEncoding();
        if (is_null($fn)) {
            return $request;
        }

        $body = $request->getBody()->getContents();
        $compressedBody = $fn($body);

        return $request->withBody(Psr7\Utils::streamFor($compressedBody))
            ->withHeader('content-encoding', $this->encoding);
    }

    private function determineEncoding()
    {
        foreach ($this->encodings as $encoding) {
            if (isset($this->encodingMap[$encoding])) {
                $this->encoding = $encoding;
                return $this->encodingMap[$encoding];
            }
        }
        return null;
    }

    private function shouldCompressRequestBody(
        $compressionInfo,
        $command,
        $operation,
        $requestBodySize
    ){
        if ($compressionInfo) {
            if (isset($command['@disable_request_compression'])
                && $command['@disable_request_compression'] === true
            ) {
                return false;
            } elseif ($this->hasStreamingTraitWithoutRequiresLength($command, $operation)
                || $requestBodySize >= $this->minimumCompressionSize
            ) {
                return true;
            }
        }
        return false;
    }

    private function hasStreamingTraitWithoutRequiresLength($command, $operation)
    {
        foreach ($operation->getInput()->getMembers() as $name => $member) {
            if (isset($command[$name])
                && !empty($member['streaming'])
                && empty($member['requiresLength'])
            ){
                return true;
            }
        }
        return false;
    }

    private function determineMinimumCompressionSize($config) {
        if (is_callable($config['request_min_compression_size_bytes'])) {
            $minCompressionSz = $config['request_min_compression_size_bytes']();
        } else {
            $minCompressionSz = $config['request_min_compression_size_bytes'];
        }

        if ($this->isValidCompressionSize($minCompressionSz)) {
            return $minCompressionSz;
        }
    }

    private function isValidCompressionSize($compressionSize)
    {
        if (is_numeric($compressionSize)
            && ($compressionSize >= 0 && $compressionSize <= 10485760)
        ) {
            return true;
        }

        throw new \InvalidArgumentException(
            'The minimum request compression size must be a '
            . 'non-negative integer value between 0 and 10485760 bytes, inclusive.'
        );
    }
}


<?php

namespace Aws;

use Psr\Http\Message\ResponseInterface;

interface ResponseContainerInterface
{
    /**
     * Get the received HTTP response if any.
     *
     * @return ResponseInterface|null
     */
    public function getResponse();
}


<?php
namespace Aws;

use JmesPath\Env as JmesPath;

/**
 * AWS result.
 */
class Result implements ResultInterface, MonitoringEventsInterface
{
    use HasDataTrait;
    use HasMonitoringEventsTrait;

    public function __construct(array $data = [])
    {
        $this->data = $data;
    }

    public function hasKey($name)
    {
        return isset($this->data[$name]);
    }

    public function get($key)
    {
        return $this[$key];
    }

    public function search($expression)
    {
        return JmesPath::search($expression, $this->toArray());
    }

    public function __toString()
    {
        $jsonData = json_encode($this->toArray(), JSON_PRETTY_PRINT);
        return <<<EOT
Model Data
----------
Data can be retrieved from the model object using the get() method of the
model (e.g., `\$result->get(\$key)`) or "accessing the result like an
associative array (e.g. `\$result['key']`). You can also execute JMESPath
expressions on the result data using the search() method.

{$jsonData}

EOT;
    }

    /**
     * @deprecated
     */
    public function getPath($path)
    {
        return $this->search(str_replace('/', '.', $path));
    }
}


<?php
namespace Aws;

/**
 * Represents an AWS result object that is returned from executing an operation.
 */
interface ResultInterface extends \ArrayAccess, \IteratorAggregate, \Countable
{
    /**
     * Provides debug information about the result object
     *
     * @return string
     */
    public function __toString();

    /**
     * Convert the result to an array.
     *
     * @return array
     */
    public function toArray();

    /**
     * Check if the model contains a key by name
     *
     * @param string $name Name of the key to retrieve
     *
     * @return bool
     */
    public function hasKey($name);

    /**
     * Get a specific key value from the result model.
     *
     * @param string $key Key to retrieve.
     *
     * @return mixed|null Value of the key or NULL if not found.
     */
    public function get($key);

    /**
     * Returns the result of executing a JMESPath expression on the contents
     * of the Result model.
     *
     *     $result = $client->execute($command);
     *     $jpResult = $result->search('foo.*.bar[?baz > `10`]');
     *
     * @param string $expression JMESPath expression to execute
     *
     * @return mixed Returns the result of the JMESPath expression.
     * @link http://jmespath.readthedocs.org/en/latest/ JMESPath documentation
     */
    public function search($expression);
};


<?php
namespace Aws;

use GuzzleHttp\Promise;

/**
 * Iterator that yields each page of results of a pageable operation.
 */
class ResultPaginator implements \Iterator
{
    /** @var AwsClientInterface Client performing operations. */
    private $client;

    /** @var string Name of the operation being paginated. */
    private $operation;

    /** @var array Args for the operation. */
    private $args;

    /** @var array Configuration for the paginator. */
    private $config;

    /** @var Result Most recent result from the client. */
    private $result;

    /** @var string|array Next token to use for pagination. */
    private $nextToken;

    /** @var int Number of operations/requests performed. */
    private $requestCount = 0;

    /**
     * @param AwsClientInterface $client
     * @param string             $operation
     * @param array              $args
     * @param array              $config
     */
    public function __construct(
        AwsClientInterface $client,
        $operation,
        array $args,
        array $config
    ) {
        $this->client = $client;
        $this->operation = $operation;
        $this->args = $args;
        $this->config = $config;
    }

    /**
     * Runs a paginator asynchronously and uses a callback to handle results.
     *
     * The callback should have the signature: function (Aws\Result $result).
     * A non-null return value from the callback will be yielded by the
     * promise. This means that you can return promises from the callback that
     * will need to be resolved before continuing iteration over the remaining
     * items, essentially merging in other promises to the iteration. The last
     * non-null value returned by the callback will be the result that fulfills
     * the promise to any downstream promises.
     *
     * @param callable $handleResult Callback for handling each page of results.
     *                               The callback accepts the result that was
     *                               yielded as a single argument. If the
     *                               callback returns a promise, the promise
     *                               will be merged into the coroutine.
     *
     * @return Promise\Promise
     */
    public function each(callable $handleResult)
    {
        return Promise\Coroutine::of(function () use ($handleResult) {
            $nextToken = null;
            do {
                $command = $this->createNextCommand($this->args, $nextToken);
                $result = (yield $this->client->executeAsync($command));
                $nextToken = $this->determineNextToken($result);
                $retVal = $handleResult($result);
                if ($retVal !== null) {
                    yield Promise\Create::promiseFor($retVal);
                }
            } while ($nextToken);
        });
    }

    /**
     * Returns an iterator that iterates over the values of applying a JMESPath
     * search to each result yielded by the iterator as a flat sequence.
     *
     * @param string $expression JMESPath expression to apply to each result.
     *
     * @return \Iterator
     */
    public function search($expression)
    {
        // Apply JMESPath expression on each result, but as a flat sequence.
        return flatmap($this, function (Result $result) use ($expression) {
            return (array) $result->search($expression);
        });
    }

    /**
     * @return Result
     */
    #[\ReturnTypeWillChange]
    public function current()
    {
        return $this->valid() ? $this->result : false;
    }

    #[\ReturnTypeWillChange]
    public function key()
    {
        return $this->valid() ? $this->requestCount - 1 : null;
    }

    #[\ReturnTypeWillChange]
    public function next()
    {
        $this->result = null;
    }

    #[\ReturnTypeWillChange]
    public function valid()
    {
        if ($this->result) {
            return true;
        }

        if ($this->nextToken || !$this->requestCount) {
            //Forward/backward paging can result in a case where the last page's nextforwardtoken
            //is the same as the one that came before it.  This can cause an infinite loop.
            $hasBidirectionalPaging = $this->config['output_token'] === 'nextForwardToken';
            if ($hasBidirectionalPaging && $this->nextToken) {
                $tokenKey = $this->config['input_token'];
                $previousToken = $this->nextToken[$tokenKey];
            }

            $this->result = $this->client->execute(
                $this->createNextCommand($this->args, $this->nextToken)
            );

            $this->nextToken = $this->determineNextToken($this->result);

            if (isset($previousToken)
                && $previousToken === $this->nextToken[$tokenKey]
            ) {
                return false;
            }

            $this->requestCount++;
            return true;
        }

        return false;
    }

    #[\ReturnTypeWillChange]
    public function rewind()
    {
        $this->requestCount = 0;
        $this->nextToken = null;
        $this->result = null;
    }

    private function createNextCommand(array $args, array $nextToken = null)
    {
        return $this->client->getCommand($this->operation, array_merge($args, ($nextToken ?: [])));
    }

    private function determineNextToken(Result $result)
    {
        if (!$this->config['output_token']) {
            return null;
        }

        if ($this->config['more_results']
            && !$result->search($this->config['more_results'])
        ) {
            return null;
        }

        $nextToken = is_scalar($this->config['output_token'])
            ? [$this->config['input_token'] => $this->config['output_token']]
            : array_combine($this->config['input_token'], $this->config['output_token']);

        return array_filter(array_map(function ($outputToken) use ($result) {
            return $result->search($outputToken);
        }, $nextToken));
    }
}


<?php
namespace Aws;

use Aws\Exception\AwsException;
use Aws\Retry\RetryHelperTrait;
use GuzzleHttp\Exception\RequestException;
use Psr\Http\Message\RequestInterface;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Promise;

/**
 * Middleware that retries failures. V1 implemention that supports 'legacy' mode.
 *
 * @internal
 */
class RetryMiddleware
{
    use RetryHelperTrait;

    private static $retryStatusCodes = [
        500 => true,
        502 => true,
        503 => true,
        504 => true
    ];

    private static $retryCodes = [
        // Throttling error
        'RequestLimitExceeded'                   => true,
        'Throttling'                             => true,
        'ThrottlingException'                    => true,
        'ThrottledException'                     => true,
        'ProvisionedThroughputExceededException' => true,
        'RequestThrottled'                       => true,
        'BandwidthLimitExceeded'                 => true,
        'RequestThrottledException'              => true,
        'TooManyRequestsException'               => true,
        'IDPCommunicationError'                  => true,
        'EC2ThrottledException'                  => true,
    ];

    private $decider;
    private $delay;
    private $nextHandler;
    private $collectStats;

    public function __construct(
        callable $decider,
        callable $delay,
        callable $nextHandler,
        $collectStats = false
    ) {
        $this->decider = $decider;
        $this->delay = $delay;
        $this->nextHandler = $nextHandler;
        $this->collectStats = (bool) $collectStats;
    }

    /**
     * Creates a default AWS retry decider function.
     *
     * The optional $extraConfig parameter is an associative array
     * that specifies additional retry conditions on top of the ones specified
     * by default by the Aws\RetryMiddleware class, with the following keys:
     *
     * - errorCodes: (string[]) An indexed array of AWS exception codes to retry.
     *   Optional.
     * - statusCodes: (int[]) An indexed array of HTTP status codes to retry.
     *   Optional.
     * - curlErrors: (int[]) An indexed array of Curl error codes to retry. Note
     *   these should be valid Curl constants. Optional.
     *
     * @param int $maxRetries
     * @param array $extraConfig
     * @return callable
     */
    public static function createDefaultDecider(
        $maxRetries = 3,
        $extraConfig = []
    ) {
        $retryCurlErrors = [];
        if (extension_loaded('curl')) {
            $retryCurlErrors[CURLE_RECV_ERROR] = true;
        }

        return function (
            $retries,
            CommandInterface $command,
            RequestInterface $request,
            ResultInterface $result = null,
            $error = null
        ) use ($maxRetries, $retryCurlErrors, $extraConfig) {
            // Allow command-level options to override this value
            $maxRetries = null !== $command['@retries'] ?
                $command['@retries']
                : $maxRetries;

            $isRetryable = self::isRetryable(
                $result,
                $error,
                $retryCurlErrors,
                $extraConfig
            );

            if ($retries >= $maxRetries) {
                if (!empty($error)
                    && $error instanceof AwsException
                    && $isRetryable
                ) {
                    $error->setMaxRetriesExceeded();
                }
                return false;
            }

            return $isRetryable;
        };
    }

    private static function isRetryable(
        $result,
        $error,
        $retryCurlErrors,
        $extraConfig = []
    ) {
        $errorCodes = self::$retryCodes;
        if (!empty($extraConfig['error_codes'])
            && is_array($extraConfig['error_codes'])
        ) {
            foreach($extraConfig['error_codes'] as $code) {
                $errorCodes[$code] = true;
            }
        }

        $statusCodes = self::$retryStatusCodes;
        if (!empty($extraConfig['status_codes'])
            && is_array($extraConfig['status_codes'])
        ) {
            foreach($extraConfig['status_codes'] as $code) {
                $statusCodes[$code] = true;
            }
        }

        if (!empty($extraConfig['curl_errors'])
            && is_array($extraConfig['curl_errors'])
        ) {
            foreach($extraConfig['curl_errors'] as $code) {
                $retryCurlErrors[$code] = true;
            }
        }

        if (!$error) {
            if (!isset($result['@metadata']['statusCode'])) {
                return false;
            }
            return isset($statusCodes[$result['@metadata']['statusCode']]);
        }

        if (!($error instanceof AwsException)) {
            return false;
        }

        if ($error->isConnectionError()) {
            return true;
        }

        if (isset($errorCodes[$error->getAwsErrorCode()])) {
            return true;
        }

        if (isset($statusCodes[$error->getStatusCode()])) {
            return true;
        }

        if (count($retryCurlErrors)
            && ($previous = $error->getPrevious())
            && $previous instanceof RequestException
        ) {
            if (method_exists($previous, 'getHandlerContext')) {
                $context = $previous->getHandlerContext();
                return !empty($context['errno'])
                    && isset($retryCurlErrors[$context['errno']]);
            }

            $message = $previous->getMessage();
            foreach (array_keys($retryCurlErrors) as $curlError) {
                if (strpos($message, 'cURL error ' . $curlError . ':') === 0) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Delay function that calculates an exponential delay.
     *
     * Exponential backoff with jitter, 100ms base, 20 sec ceiling
     *
     * @param $retries - The number of retries that have already been attempted
     *
     * @return int
     *
     * @link https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
     */
    public static function exponentialDelay($retries)
    {
        return mt_rand(0, (int) min(20000, (int) pow(2, $retries) * 100));
    }

    /**
     * @param CommandInterface $command
     * @param RequestInterface $request
     *
     * @return PromiseInterface
     */
    public function __invoke(
        CommandInterface $command,
        RequestInterface $request = null
    ) {
        $retries = 0;
        $requestStats = [];
        $monitoringEvents = [];
        $handler = $this->nextHandler;
        $decider = $this->decider;
        $delay = $this->delay;

        $request = $this->addRetryHeader($request, 0, 0);

        $g = function ($value) use (
            $handler,
            $decider,
            $delay,
            $command,
            $request,
            &$retries,
            &$requestStats,
            &$monitoringEvents,
            &$g
        ) {
            $this->updateHttpStats($value, $requestStats);

            if ($value instanceof MonitoringEventsInterface) {
                $reversedEvents = array_reverse($monitoringEvents);
                $monitoringEvents = array_merge($monitoringEvents, $value->getMonitoringEvents());
                foreach ($reversedEvents as $event) {
                    $value->prependMonitoringEvent($event);
                }
            }
            if ($value instanceof \Exception || $value instanceof \Throwable) {
                if (!$decider($retries, $command, $request, null, $value)) {
                    return Promise\Create::rejectionFor(
                        $this->bindStatsToReturn($value, $requestStats)
                    );
                }
            } elseif ($value instanceof ResultInterface
                && !$decider($retries, $command, $request, $value, null)
            ) {
                return $this->bindStatsToReturn($value, $requestStats);
            }

            // Delay fn is called with 0, 1, ... so increment after the call.
            $delayBy = $delay($retries++);
            $command['@http']['delay'] = $delayBy;
            if ($this->collectStats) {
                $this->updateStats($retries, $delayBy, $requestStats);
            }

            // Update retry header with retry count and delayBy
            $request = $this->addRetryHeader($request, $retries, $delayBy);

            return $handler($command, $request)->then($g, $g);
        };

        return $handler($command, $request)->then($g, $g);
    }
}