fix: prevent illegal characters in PSR-16 cache keys

@see https://www.php-fig.org/psr/psr-16/#12-definitions
This commit is contained in:
Romain Canon 2022-08-31 00:56:30 +02:00
parent 546fa5c6cf
commit 3c4d29901a
3 changed files with 32 additions and 25 deletions

View File

@ -4,22 +4,28 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Cache;
use Closure;
use CuyZ\Valinor\Utility\Package;
use Psr\SimpleCache\CacheInterface;
use Traversable;
use function sha1;
/**
* @internal
*
* @template EntryType
* @implements CacheInterface<EntryType>
*/
final class VersionedCache implements CacheInterface
final class KeySanitizerCache implements CacheInterface
{
/** @var CacheInterface<EntryType> */
private static string $version;
/** @var CacheInterface<EntryType> */
private CacheInterface $delegate;
private string $version;
/** @var Closure(string): string */
private Closure $sanitize;
/**
* @param CacheInterface<EntryType> $delegate
@ -27,23 +33,29 @@ final class VersionedCache implements CacheInterface
public function __construct(CacheInterface $delegate)
{
$this->delegate = $delegate;
/** @infection-ignore-all */
$this->version = PHP_VERSION . '/' . Package::version();
// Two things:
// 1. We append the current version of the package to the cache key in
// order to avoid collisions between entries from different versions
// of the library.
// 2. The key is sha1'd so that it does not contain illegal characters.
// @see https://www.php-fig.org/psr/psr-16/#12-definitions
$this->sanitize = static fn (string $key) => sha1("$key." . self::$version ??= PHP_VERSION . '/' . Package::version());
}
public function get($key, $default = null)
{
return $this->delegate->get($this->key($key), $default);
return $this->delegate->get(($this->sanitize)($key), $default);
}
public function set($key, $value, $ttl = null): bool
{
return $this->delegate->set($this->key($key), $value, $ttl);
return $this->delegate->set(($this->sanitize)($key), $value, $ttl);
}
public function delete($key): bool
{
return $this->delegate->delete($this->key($key));
return $this->delegate->delete(($this->sanitize)($key));
}
public function clear(): bool
@ -53,7 +65,7 @@ final class VersionedCache implements CacheInterface
public function has($key): bool
{
return $this->delegate->has($this->key($key));
return $this->delegate->has(($this->sanitize)($key));
}
/**
@ -62,7 +74,7 @@ final class VersionedCache implements CacheInterface
public function getMultiple($keys, $default = null): Traversable
{
foreach ($keys as $key) {
yield $key => $this->delegate->get($this->key($key), $default);
yield $key => $this->delegate->get(($this->sanitize)($key), $default);
}
}
@ -71,7 +83,7 @@ final class VersionedCache implements CacheInterface
$versionedValues = [];
foreach ($values as $key => $value) {
$versionedValues[$this->key($key)] = $value;
$versionedValues[($this->sanitize)($key)] = $value;
}
return $this->delegate->setMultiple($versionedValues, $ttl);
@ -82,14 +94,9 @@ final class VersionedCache implements CacheInterface
$transformedKeys = [];
foreach ($keys as $key) {
$transformedKeys[] = $this->key($key);
$transformedKeys[] = ($this->sanitize)($key);
}
return $this->delegate->deleteMultiple($transformedKeys);
}
private function key(string $key): string
{
return "$key.$this->version";
}
}

View File

@ -6,7 +6,7 @@ namespace CuyZ\Valinor\Library;
use CuyZ\Valinor\Cache\ChainCache;
use CuyZ\Valinor\Cache\RuntimeCache;
use CuyZ\Valinor\Cache\VersionedCache;
use CuyZ\Valinor\Cache\KeySanitizerCache;
use CuyZ\Valinor\Cache\Warmup\RecursiveCacheWarmupService;
use CuyZ\Valinor\Definition\FunctionsContainer;
use CuyZ\Valinor\Definition\Repository\AttributesRepository;
@ -222,10 +222,10 @@ final class Container
$cache = new RuntimeCache();
if (isset($settings->cache)) {
$cache = new ChainCache($cache, $settings->cache);
$cache = new ChainCache($cache, new KeySanitizerCache($settings->cache));
}
return new VersionedCache($cache);
return $cache;
},
];
}

View File

@ -4,25 +4,25 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Unit\Cache;
use CuyZ\Valinor\Cache\VersionedCache;
use CuyZ\Valinor\Cache\KeySanitizerCache;
use CuyZ\Valinor\Tests\Fake\Cache\FakeCache;
use PHPUnit\Framework\TestCase;
use function iterator_to_array;
final class VersionedCacheTest extends TestCase
final class KeySanitizerCacheTest extends TestCase
{
private FakeCache $delegate;
/** @var VersionedCache<mixed> */
private VersionedCache $cache;
/** @var KeySanitizerCache<mixed> */
private KeySanitizerCache $cache;
protected function setUp(): void
{
parent::setUp();
$this->delegate = new FakeCache();
$this->cache = new VersionedCache($this->delegate);
$this->cache = new KeySanitizerCache($this->delegate);
}
public function test_set_value_sets_value_in_delegate_with_changed_key(): void