зеркало из https://github.com/dotnet/aspnetcore.git
key/tag validation
This commit is contained in:
Родитель
db96936619
Коммит
ee1b427019
|
@ -111,8 +111,40 @@ internal sealed partial class DefaultHybridCache : HybridCache
|
|||
private HybridCacheEntryFlags GetEffectiveFlags(HybridCacheEntryOptions? options)
|
||||
=> (options?.Flags | _hardFlags) ?? _defaultFlags;
|
||||
|
||||
private static void Validate(string key, IReadOnlyCollection<string>? tags)
|
||||
{
|
||||
ValidateKeyOrTag(key, nameof(key));
|
||||
if (tags is not null)
|
||||
{
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
ValidateKeyOrTag(tag, nameof(tag));
|
||||
}
|
||||
}
|
||||
}
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2249:Consider using 'string.Contains' instead of 'string.IndexOf'", Justification = "Multi-targeting makes impractical")]
|
||||
private static void ValidateKeyOrTag(string value, string paramName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
ThrowNullOrWhiteSpace(paramName);
|
||||
}
|
||||
if (value.IndexOf('\0') >= 0)
|
||||
{
|
||||
ThrowInvalid(paramName);
|
||||
}
|
||||
|
||||
static void ThrowNullOrWhiteSpace(string paramName)
|
||||
=> throw new ArgumentException("Value must be non-empty", paramName);
|
||||
|
||||
static void ThrowInvalid(string paramName)
|
||||
=> throw new ArgumentException("Value contains invalid token", paramName);
|
||||
}
|
||||
|
||||
public override ValueTask<T> GetOrCreateAsync<TState, T>(string key, TState state, Func<TState, CancellationToken, ValueTask<T>> underlyingDataCallback, HybridCacheEntryOptions? options = null, IReadOnlyCollection<string>? tags = null, CancellationToken token = default)
|
||||
{
|
||||
Validate(key, tags);
|
||||
|
||||
var canBeCanceled = token.CanBeCanceled;
|
||||
if (canBeCanceled)
|
||||
{
|
||||
|
@ -149,12 +181,16 @@ internal sealed partial class DefaultHybridCache : HybridCache
|
|||
|
||||
public override ValueTask RemoveKeyAsync(string key, CancellationToken token = default)
|
||||
{
|
||||
ValidateKeyOrTag(key, nameof(key));
|
||||
|
||||
_localCache.Remove(key);
|
||||
return _backendCache is null ? default : new(_backendCache.RemoveAsync(key, token));
|
||||
}
|
||||
|
||||
public override ValueTask SetAsync<T>(string key, T value, HybridCacheEntryOptions? options = null, IReadOnlyCollection<string>? tags = null, CancellationToken token = default)
|
||||
{
|
||||
Validate(key, tags);
|
||||
|
||||
// since we're forcing a write: disable L1+L2 read; we'll use a direct pass-thru of the value as the callback, to reuse all the code;
|
||||
// note also that stampede token is not shared with anyone else
|
||||
var flags = GetEffectiveFlags(options) | (HybridCacheEntryFlags.DisableLocalCacheRead | HybridCacheEntryFlags.DisableDistributedCacheRead);
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using Microsoft.Extensions.Caching.Hybrid.Internal;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.Extensions.Caching.Hybrid.Tests;
|
||||
public class FrameTests(ITestOutputHelper Log)
|
||||
{
|
||||
[Fact]
|
||||
public void BasicFrameRoundTrip()
|
||||
{
|
||||
DefaultHybridCache.PayloadHeader header = new(42, 1000000, 99, 12345, "some key", []);
|
||||
|
||||
using var writer = RecyclableArrayBufferWriter<byte>.Create(100);
|
||||
header.Write(writer);
|
||||
|
||||
|
||||
var original = writer.GetCommittedMemory().Span;
|
||||
var remaining = original;
|
||||
Assert.True(DefaultHybridCache.PayloadHeader.TryParse(ref remaining, out var parsed));
|
||||
|
||||
Assert.Equal(42, parsed.Flags);
|
||||
Assert.Equal(1000000UL, parsed.EntropyAndCreationTime);
|
||||
Assert.Equal(99U, parsed.PayloadSize);
|
||||
Assert.Equal(12345UL, parsed.TTL);
|
||||
Assert.Equal("some key", parsed.Key);
|
||||
Assert.Empty(parsed.Tags);
|
||||
Assert.True(remaining.IsEmpty);
|
||||
|
||||
var hex = BitConverter.ToString(writer.GetBuffer(out int length), 0, length);
|
||||
Log.WriteLine(hex);
|
||||
Assert.Equal("03-01-2A-00-40-42-0F-00-00-00-00-00-63-00-00-00-39-30-00-00-00-00-10-73-6F-6D-65-20-6B-65-79", hex);
|
||||
// 03-01 sentinel+version
|
||||
// 2A-00 flags
|
||||
// 40-42-0F-00-00-00-00-00 entropy+creationtime
|
||||
// 63-00-00-00 payload size
|
||||
// 39-30-00-00-00 ttl
|
||||
// 00 tag count
|
||||
// 10 key length + marker
|
||||
// 73-6F-6D-65-20-6B-65-79 key
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FrameRoundTripWithTags()
|
||||
{
|
||||
DefaultHybridCache.PayloadHeader header = new(42, 1000000, 99, 12345, "some key", ["abc", "def"]);
|
||||
|
||||
using var writer = RecyclableArrayBufferWriter<byte>.Create(100);
|
||||
header.Write(writer);
|
||||
|
||||
var original = writer.GetCommittedMemory().Span;
|
||||
var remaining = original;
|
||||
Assert.True(DefaultHybridCache.PayloadHeader.TryParse(ref remaining, out var parsed));
|
||||
|
||||
Assert.Equal(42, parsed.Flags);
|
||||
Assert.Equal(1000000UL, parsed.EntropyAndCreationTime);
|
||||
Assert.Equal(99U, parsed.PayloadSize);
|
||||
Assert.Equal(12345UL, parsed.TTL);
|
||||
Assert.Equal("some key", parsed.Key);
|
||||
Assert.Equal(["abc", "def"], parsed.Tags);
|
||||
Assert.True(remaining.IsEmpty);
|
||||
|
||||
var hex = BitConverter.ToString(writer.GetBuffer(out int length), 0, length);
|
||||
Log.WriteLine(hex);
|
||||
Assert.Equal("03-01-2A-00-40-42-0F-00-00-00-00-00-63-00-00-00-39-30-00-00-00-02-10-73-6F-6D-65-20-6B-65-79-06-61-62-63-06-64-65-66", hex);
|
||||
// 03-01 sentinel+version
|
||||
// 2A-00 flags
|
||||
// 40-42-0F-00-00-00-00-00 entropy+creationtime
|
||||
// 63-00-00-00 payload size
|
||||
// 39-30-00-00-00 ttl
|
||||
// 02 tag count
|
||||
// 10 key length + marker
|
||||
// 73-6F-6D-65-20-6B-65-79 key
|
||||
// 06-61-62-63 "abc"
|
||||
// 06-64-65-66 "def"
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FrameRoundTripWithLongKey()
|
||||
{
|
||||
const string ALPHABET = "abcdefghijklmnopqrstuvwxyz";
|
||||
var key = ALPHABET + ALPHABET + ALPHABET + ALPHABET + ALPHABET + ALPHABET;
|
||||
Assert.True(key.Length > 130);
|
||||
|
||||
DefaultHybridCache.PayloadHeader header = new(42, 1000000, 99, 12345, key, []);
|
||||
|
||||
using var writer = RecyclableArrayBufferWriter<byte>.Create(1000);
|
||||
header.Write(writer);
|
||||
|
||||
|
||||
var original = writer.GetCommittedMemory().Span;
|
||||
var remaining = original;
|
||||
Assert.True(DefaultHybridCache.PayloadHeader.TryParse(ref remaining, out var parsed));
|
||||
|
||||
Assert.Equal(42, parsed.Flags);
|
||||
Assert.Equal(1000000UL, parsed.EntropyAndCreationTime);
|
||||
Assert.Equal(99U, parsed.PayloadSize);
|
||||
Assert.Equal(12345UL, parsed.TTL);
|
||||
Assert.Equal(key, parsed.Key);
|
||||
Assert.Empty(parsed.Tags);
|
||||
Assert.True(remaining.IsEmpty);
|
||||
|
||||
var hex = BitConverter.ToString(writer.GetBuffer(out int length), 0, length);
|
||||
Log.WriteLine(hex);
|
||||
Assert.Equal("03-01-2A-00-40-42-0F-00-00-00-00-00-63-00-00-00-39-30-00-00-00-00-39-01-61-62-63-64-65-66-67-68-69-6A-6B-6C-6D-6E-6F-70-71-72-73-74-75-76-77-78-79-7A-61-62-63-64-65-66-67-68-69-6A-6B-6C-6D-6E-6F-70-71-72-73-74-75-76-77-78-79-7A-61-62-63-64-65-66-67-68-69-6A-6B-6C-6D-6E-6F-70-71-72-73-74-75-76-77-78-79-7A-61-62-63-64-65-66-67-68-69-6A-6B-6C-6D-6E-6F-70-71-72-73-74-75-76-77-78-79-7A-61-62-63-64-65-66-67-68-69-6A-6B-6C-6D-6E-6F-70-71-72-73-74-75-76-77-78-79-7A-61-62-63-64-65-66-67-68-69-6A-6B-6C-6D-6E-6F-70-71-72-73-74-75-76-77-78-79-7A", hex);
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using Microsoft.Extensions.Caching.Hybrid.Internal;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.Extensions.Caching.Hybrid.Tests;
|
||||
public class ValidationTests
|
||||
{
|
||||
ServiceProvider GetDefaultCache(out DefaultHybridCache cache)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddHybridCache();
|
||||
var provider = services.BuildServiceProvider();
|
||||
cache = Assert.IsType<DefaultHybridCache>(provider.GetRequiredService<HybridCache>());
|
||||
return provider;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidKeyWorks()
|
||||
{
|
||||
using var provider = GetDefaultCache(out var cache);
|
||||
var id = await cache.GetOrCreateAsync<int>("some key", _ => new(42));
|
||||
Assert.Equal(42, id);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
[InlineData("\t\t")]
|
||||
public async Task EmptyKeyIsInvalid(string key)
|
||||
{
|
||||
using var provider = GetDefaultCache(out var cache);
|
||||
var ex = await Assert.ThrowsAsync<ArgumentException>(async () => await cache.GetOrCreateAsync<int>("", _ => new(42)));
|
||||
Assert.Equal("key", ex.ParamName);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("abc", "ABC")] // case sensitivity
|
||||
[InlineData("abc", "a%2Db")] // %-encoding
|
||||
[InlineData("abc", "aḃc")] // accented b
|
||||
public async Task KeyIsNotAliased(string primary, string alt)
|
||||
{
|
||||
using var provider = GetDefaultCache(out var cache);
|
||||
Assert.Equal(42, await cache.GetOrCreateAsync<int>(primary, _ => new(42)));
|
||||
Assert.Equal(96, await cache.GetOrCreateAsync<int>(alt, _ => new(96)));
|
||||
Assert.Equal(42, await cache.GetOrCreateAsync<int>(primary, _ => new(42)));
|
||||
Assert.Equal(96, await cache.GetOrCreateAsync<int>(alt, _ => new(96)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("a\0bc")]
|
||||
public async Task InvalidKeyDetected(string key)
|
||||
{
|
||||
using var provider = GetDefaultCache(out var cache);
|
||||
var ex = await Assert.ThrowsAsync<ArgumentException>(async () => await cache.GetOrCreateAsync<int>(key, _ => new(42)));
|
||||
Assert.Equal("key", ex.ParamName);
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче