diff --git a/Directory.Packages.props b/Directory.Packages.props index cc1bd445047..08c3a35cfb1 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -49,6 +49,7 @@ See https://github.com/OrchardCMS/OrchardCore/pull/16057 for more information. --> + diff --git a/src/OrchardCore.Modules/OrchardCore.Media/Recipes/MediaStep.cs b/src/OrchardCore.Modules/OrchardCore.Media/Recipes/MediaStep.cs index b73f9e069af..9550cf37ba0 100644 --- a/src/OrchardCore.Modules/OrchardCore.Media/Recipes/MediaStep.cs +++ b/src/OrchardCore.Modules/OrchardCore.Media/Recipes/MediaStep.cs @@ -1,3 +1,4 @@ +using System.Text; using System.Text.Json.Nodes; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Localization; @@ -46,35 +47,38 @@ protected override async Task HandleAsync(RecipeExecutionContext context) Stream stream = null; - if (!string.IsNullOrWhiteSpace(file.Base64)) + try { - stream = new MemoryStream(Convert.FromBase64String(file.Base64)); - } - else if (!string.IsNullOrWhiteSpace(file.SourcePath)) - { - var fileInfo = context.RecipeDescriptor.FileProvider.GetRelativeFileInfo(context.RecipeDescriptor.BasePath, file.SourcePath); + if (!string.IsNullOrWhiteSpace(file.Base64)) + { + stream = Base64.DecodedToStream(file.Base64); + } + else if (!string.IsNullOrWhiteSpace(file.SourcePath)) + { + var fileInfo = context.RecipeDescriptor.FileProvider.GetRelativeFileInfo(context.RecipeDescriptor.BasePath, file.SourcePath); - stream = fileInfo.CreateReadStream(); - } - else if (!string.IsNullOrWhiteSpace(file.SourceUrl)) - { - var httpClient = _httpClientFactory.CreateClient(); + stream = fileInfo.CreateReadStream(); + } + else if (!string.IsNullOrWhiteSpace(file.SourceUrl)) + { + var httpClient = _httpClientFactory.CreateClient(); - var response = await httpClient.GetAsync(file.SourceUrl); + var response = await httpClient.GetAsync(file.SourceUrl); - if (response.IsSuccessStatusCode) - { - stream = await response.Content.ReadAsStreamAsync(); + if (response.IsSuccessStatusCode) + { + stream = await response.Content.ReadAsStreamAsync(); + } } - } - if (stream != null) - { - try + if (stream != null) { await _mediaFileStore.CreateFileFromStreamAsync(file.TargetPath, stream, true); } - finally + } + finally + { + if (stream != null) { await stream.DisposeAsync(); } diff --git a/src/OrchardCore.Modules/OrchardCore.Queries/Sql/Controllers/AdminController.cs b/src/OrchardCore.Modules/OrchardCore.Queries/Sql/Controllers/AdminController.cs index eb62f0b6882..97bf13907f4 100644 --- a/src/OrchardCore.Modules/OrchardCore.Queries/Sql/Controllers/AdminController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Queries/Sql/Controllers/AdminController.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Text; using System.Text.Json; using Dapper; using Fluid; @@ -43,7 +44,10 @@ public AdminController( [Admin("Queries/Sql/Query", "QueriesRunSql")] public Task Query(string query) { - query = string.IsNullOrWhiteSpace(query) ? "" : System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(query)); + query = string.IsNullOrWhiteSpace(query) + ? "" + : Base64.FromUTF8Base64String(query); + return Query(new AdminQueryViewModel { DecodedQuery = query, diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Controllers/AdminController.cs b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Controllers/AdminController.cs index 87d1914c512..12419aeed3d 100644 --- a/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Controllers/AdminController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Elasticsearch/Controllers/AdminController.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.Globalization; +using System.Text; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Nodes; @@ -466,7 +467,9 @@ public async Task Query(string indexName, string query) return await Query(new AdminQueryViewModel { IndexName = indexName, - DecodedQuery = string.IsNullOrWhiteSpace(query) ? string.Empty : System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(query)) + DecodedQuery = string.IsNullOrWhiteSpace(query) + ? string.Empty + : Base64.FromUTF8Base64String(query) }); } diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Controllers/AdminController.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Controllers/AdminController.cs index c94dfe3a011..c8245412ab2 100644 --- a/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Controllers/AdminController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Controllers/AdminController.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.Globalization; +using System.Text; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Nodes; @@ -355,7 +356,10 @@ public async Task Delete(LuceneIndexSettingsViewModel model) public Task Query(string indexName, string query) { - query = string.IsNullOrWhiteSpace(query) ? "" : System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(query)); + query = string.IsNullOrWhiteSpace(query) + ? "" + : Base64.FromUTF8Base64String(query); + return Query(new AdminQueryViewModel { IndexName = indexName, DecodedQuery = query }); } diff --git a/src/OrchardCore.Modules/OrchardCore.Sitemaps/Controllers/SitemapController.cs b/src/OrchardCore.Modules/OrchardCore.Sitemaps/Controllers/SitemapController.cs index 859550d9e15..fb2b84d8bc2 100644 --- a/src/OrchardCore.Modules/OrchardCore.Sitemaps/Controllers/SitemapController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Sitemaps/Controllers/SitemapController.cs @@ -86,7 +86,7 @@ public async Task Index(string sitemapId, CancellationToken cance document.Declaration = new XDeclaration("1.0", "utf-8", null); - var stream = new MemoryStream(); + using var stream = MemoryStreamFactory.GetStream(); await document.SaveAsync(stream, SaveOptions.None, cancellationToken); if (stream.Length >= ErrorLength) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ResetPasswordController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ResetPasswordController.cs index 490b13f3dda..f58d9900b86 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ResetPasswordController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ResetPasswordController.cs @@ -160,7 +160,7 @@ public async Task ResetPasswordPOST() if (ModelState.IsValid) { - var token = Encoding.UTF8.GetString(Convert.FromBase64String(model.ResetToken)); + var token = Base64.FromUTF8Base64String(model.ResetToken); if (await _userService.ResetPasswordAsync(model.UsernameOrEmail, token, model.NewPassword, ModelState.AddModelError)) { diff --git a/src/OrchardCore.Modules/OrchardCore.XmlRpc/Controllers/HomeController.cs b/src/OrchardCore.Modules/OrchardCore.XmlRpc/Controllers/HomeController.cs index 7d4dc97022d..d69f4cf772c 100644 --- a/src/OrchardCore.Modules/OrchardCore.XmlRpc/Controllers/HomeController.cs +++ b/src/OrchardCore.Modules/OrchardCore.XmlRpc/Controllers/HomeController.cs @@ -47,14 +47,15 @@ public async Task ServiceEndpoint([ModelBinder(BinderType = typeo }; // Save to an intermediate MemoryStream to preserve the encoding declaration. - using var stream = new MemoryStream(); + using var stream = MemoryStreamFactory.GetStream(); using (var w = XmlWriter.Create(stream, settings)) { var result = _writer.MapMethodResponse(methodResponse); result.Save(w); } - var content = Encoding.UTF8.GetString(stream.ToArray()); + var content = Encoding.UTF8.GetString(stream.GetBuffer(), 0, (int)stream.Length); + return Content(content, "text/xml"); } diff --git a/src/OrchardCore/OrchardCore.Abstractions/Base64.cs b/src/OrchardCore/OrchardCore.Abstractions/Base64.cs new file mode 100644 index 00000000000..5781418277a --- /dev/null +++ b/src/OrchardCore/OrchardCore.Abstractions/Base64.cs @@ -0,0 +1,68 @@ +using System.Text; + +namespace OrchardCore; + +public static class Base64 +{ + /// + /// Converts a base64 encoded UTF8 string to the original value. + /// + /// The base64 encoded string. + /// The decoded string. + /// This method is equivalent to Encoding.UTF8.GetString(Convert.FromBase64String(base64)) but uses a buffer pool to decode the string. + public static string FromUTF8Base64String(string base64) + { + ArgumentNullException.ThrowIfNull(base64); + + // Due to padding the deserialized buffer could be smaller than this value. + var maxBufferLength = GetDeserializedBase64Length(base64.Length); + + using var memoryStream = MemoryStreamFactory.GetStream(maxBufferLength); + var span = memoryStream.GetSpan(maxBufferLength); + + if (!Convert.TryFromBase64String(base64, span, out var bytesWritten)) + { + throw new FormatException("Invalid Base64 string."); + } + + return Encoding.UTF8.GetString(span.Slice(0, bytesWritten)); + } + + /// + /// Converts a base64 encoded string to a stream. + /// + /// The base64 encoded string. + /// The resulting should be disposed once used. + /// The decoded stream. + /// + public static Stream DecodedToStream(string base64) + { + ArgumentNullException.ThrowIfNull(base64); + + // Due to padding the deserialized buffer could be smaller than this value. + var maxBufferLength = GetDeserializedBase64Length(base64.Length); + + var memoryStream = MemoryStreamFactory.GetStream(maxBufferLength); + var span = memoryStream.GetSpan(maxBufferLength); + + if (!Convert.TryFromBase64String(base64, span, out var bytesWritten)) + { + throw new FormatException("Invalid Base64 string."); + } + + memoryStream.Advance(bytesWritten); + + return memoryStream; + } + + /// + /// Gets the maximum buffer length required to decode a base64 string. + /// + /// The length value to decode. + /// The size of the decoded buffer. + public static int GetDeserializedBase64Length(int base64Length) + { + // Do the multiplication first to prevent precision loss. + return base64Length * 3 / 4; + } +} diff --git a/src/OrchardCore/OrchardCore.Abstractions/MemoryStreamFactory.cs b/src/OrchardCore/OrchardCore.Abstractions/MemoryStreamFactory.cs new file mode 100644 index 00000000000..4e2a4fb8b93 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Abstractions/MemoryStreamFactory.cs @@ -0,0 +1,25 @@ +using Microsoft.IO; + +namespace OrchardCore; + +public static class MemoryStreamFactory +{ + private static readonly RecyclableMemoryStreamManager _manager = new(); + + static MemoryStreamFactory() + { + var options = new RecyclableMemoryStreamManager.Options + { + BlockSize = 4 * 1024, // 4 KB + AggressiveBufferReturn = true + }; + + _manager = new RecyclableMemoryStreamManager(options); + } + + public static RecyclableMemoryStream GetStream(string tag = null) + => _manager.GetStream(tag); + + public static RecyclableMemoryStream GetStream(int requiredSize, string tag = null) + => _manager.GetStream(tag, requiredSize); +} diff --git a/src/OrchardCore/OrchardCore.Abstractions/OrchardCore.Abstractions.csproj b/src/OrchardCore/OrchardCore.Abstractions/OrchardCore.Abstractions.csproj index 3f580b270c1..025ed9c0599 100644 --- a/src/OrchardCore/OrchardCore.Abstractions/OrchardCore.Abstractions.csproj +++ b/src/OrchardCore/OrchardCore.Abstractions/OrchardCore.Abstractions.csproj @@ -17,6 +17,7 @@ + diff --git a/src/OrchardCore/OrchardCore.FileStorage.AzureBlob/BlobFileStore.cs b/src/OrchardCore/OrchardCore.FileStorage.AzureBlob/BlobFileStore.cs index 081f26a8e77..067102cc3b8 100644 --- a/src/OrchardCore/OrchardCore.FileStorage.AzureBlob/BlobFileStore.cs +++ b/src/OrchardCore/OrchardCore.FileStorage.AzureBlob/BlobFileStore.cs @@ -39,14 +39,17 @@ public class BlobFileStore : IFileStore private readonly IClock _clock; private readonly BlobContainerClient _blobContainer; private readonly IContentTypeProvider _contentTypeProvider; + private readonly string _basePrefix; - public BlobFileStore(BlobStorageOptions options, IClock clock, IContentTypeProvider contentTypeProvider) + public BlobFileStore( + BlobStorageOptions options, + IClock clock, + IContentTypeProvider contentTypeProvider) { _options = options; _clock = clock; _contentTypeProvider = contentTypeProvider; - _blobContainer = new BlobContainerClient(_options.ConnectionString, _options.ContainerName); if (!string.IsNullOrEmpty(_options.BasePath)) @@ -436,6 +439,7 @@ private async Task CreateDirectoryAsync(string path) // Create a directory marker file to make this directory appear when listing directories. using var stream = new MemoryStream(MarkerFileContent); + await placeholderBlob.UploadAsync(stream); } diff --git a/src/OrchardCore/OrchardCore.Infrastructure/Documents/DefaultDocumentSerializer.cs b/src/OrchardCore/OrchardCore.Infrastructure/Documents/DefaultDocumentSerializer.cs index aab1271b1c5..f7c6dec9cec 100644 --- a/src/OrchardCore/OrchardCore.Infrastructure/Documents/DefaultDocumentSerializer.cs +++ b/src/OrchardCore/OrchardCore.Infrastructure/Documents/DefaultDocumentSerializer.cs @@ -1,5 +1,6 @@ using System.IO.Compression; using System.Text.Json; +using Microsoft.IO; using OrchardCore.Data.Documents; namespace OrchardCore.Documents; @@ -9,6 +10,8 @@ namespace OrchardCore.Documents; /// public class DefaultDocumentSerializer : IDocumentSerializer { + private const string StreamTag = nameof(DefaultDocumentSerializer); + private static readonly byte[] _gZipHeaderBytes = [0x1f, 0x8b]; private readonly JsonSerializerOptions _serializerOptions; @@ -18,76 +21,73 @@ public DefaultDocumentSerializer(JsonSerializerOptions serializerOptions) _serializerOptions = serializerOptions; } - public Task SerializeAsync(TDocument document, int compressThreshold = int.MaxValue) + public async Task SerializeAsync(TDocument document, int compressThreshold = int.MaxValue) where TDocument : class, IDocument, new() { - var data = JsonSerializer.SerializeToUtf8Bytes(document, _serializerOptions); - if (data.Length >= compressThreshold) + using var utf8Stream = MemoryStreamFactory.GetStream(StreamTag); + byte[] result; + + await JsonSerializer.SerializeAsync(utf8Stream, document, _serializerOptions); + utf8Stream.Seek(0, SeekOrigin.Begin); + + if (utf8Stream.Length >= compressThreshold) { - data = Compress(data); + using var stream = MemoryStreamFactory.GetStream(StreamTag); + await CompressAsync(utf8Stream, stream); + + result = new byte[stream.Length]; + stream.Seek(0, SeekOrigin.Begin); + await stream.CopyToAsync(new MemoryStream(result)); + } + else + { + result = new byte[utf8Stream.Length]; + await utf8Stream.CopyToAsync(new MemoryStream(result)); } - return Task.FromResult(data); + return result; } - public Task DeserializeAsync(byte[] data) + public async Task DeserializeAsync(byte[] data) where TDocument : class, IDocument, new() { + TDocument document; + if (IsCompressed(data)) { - data = Decompress(data); - } + // Assume the decompressed data could fill a twice as big buffer. + var stream = MemoryStreamFactory.GetStream(data.Length * 2, StreamTag); - using var ms = new MemoryStream(data); + await DecompressAsync(data, stream); + stream.Seek(0, SeekOrigin.Begin); - var document = JsonSerializer.Deserialize(ms, _serializerOptions); + document = await JsonSerializer.DeserializeAsync(stream, _serializerOptions); + } + else + { + document = JsonSerializer.Deserialize(data, _serializerOptions); + } - return Task.FromResult(document); + return document; } internal static bool IsCompressed(byte[] data) { - // Ensure data is at least as long as the GZip header - if (data.Length >= _gZipHeaderBytes.Length) - { - // Compare the header bytes. - return data.Take(_gZipHeaderBytes.Length).SequenceEqual(_gZipHeaderBytes); - } + ArgumentNullException.ThrowIfNull(data); - return false; + return data.AsSpan().StartsWith(_gZipHeaderBytes); } - internal static byte[] Compress(byte[] data) + internal static async Task CompressAsync(Stream source, RecyclableMemoryStream output) { - using var input = new MemoryStream(data); - using var output = new MemoryStream(); - using (var gzip = new GZipStream(output, CompressionMode.Compress)) - { - input.CopyTo(gzip); - } - - if (output.TryGetBuffer(out var buffer)) - { - return buffer.Array; - } - - return output.ToArray(); + using var gZip = new GZipStream(output, CompressionMode.Compress, leaveOpen: true); + await source.CopyToAsync(gZip); } - internal static byte[] Decompress(byte[] data) + internal static async Task DecompressAsync(byte[] data, RecyclableMemoryStream output) { using var input = new MemoryStream(data); - using var output = new MemoryStream(); - using (var gzip = new GZipStream(input, CompressionMode.Decompress)) - { - gzip.CopyTo(output); - } - - if (output.TryGetBuffer(out var buffer)) - { - return buffer.Array; - } - - return output.ToArray(); + using var gZip = new GZipStream(input, CompressionMode.Decompress); + await gZip.CopyToAsync(output); } } diff --git a/src/OrchardCore/OrchardCore.Infrastructure/Properties/AssemblyInfo.cs b/src/OrchardCore/OrchardCore.Infrastructure/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..ad0bee5678a --- /dev/null +++ b/src/OrchardCore/OrchardCore.Infrastructure/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("OrchardCore.Tests")] diff --git a/src/OrchardCore/OrchardCore.Infrastructure/Scripting/CommonGeneratorMethods.cs b/src/OrchardCore/OrchardCore.Infrastructure/Scripting/CommonGeneratorMethods.cs index 1029b9aeef2..4c9874f9e81 100644 --- a/src/OrchardCore/OrchardCore.Infrastructure/Scripting/CommonGeneratorMethods.cs +++ b/src/OrchardCore/OrchardCore.Infrastructure/Scripting/CommonGeneratorMethods.cs @@ -1,21 +1,29 @@ using System.IO.Compression; using System.Net; -using System.Text; namespace OrchardCore.Scripting; public class CommonGeneratorMethods : IGlobalMethodProvider { - private static readonly GlobalMethod _base64 = new() + private static readonly GlobalMethod[] _allMethods; + + public IEnumerable GetMethods() => _allMethods; + + static CommonGeneratorMethods() + { + _allMethods = [_base64, _html, _gZip]; + } + + internal static readonly GlobalMethod _base64 = new() { Name = "base64", Method = serviceProvider => (Func)(encoded => { - return Encoding.UTF8.GetString(Convert.FromBase64String(encoded)); + return Base64.FromUTF8Base64String(encoded); }), }; - private static readonly GlobalMethod _html = new() + internal static readonly GlobalMethod _html = new() { Name = "html", Method = serviceProvider => (Func)(encoded => @@ -28,26 +36,21 @@ public class CommonGeneratorMethods : IGlobalMethodProvider /// Converts a Base64 encoded gzip stream to an uncompressed Base64 string. /// See http://www.txtwizard.net/compression. /// - private static readonly GlobalMethod _gZip = new() + internal static readonly GlobalMethod _gZip = new() { Name = "gzip", Method = serviceProvider => (Func)(encoded => { - var bytes = Convert.FromBase64String(encoded); - using var gzip = new GZipStream(new MemoryStream(bytes), CompressionMode.Decompress); + var compressedStream = Base64.DecodedToStream(encoded); + compressedStream.Seek(0, SeekOrigin.Begin); - var decompressed = new MemoryStream(); - var buffer = new byte[1024]; - int nRead; + using var gZip = new GZipStream(compressedStream, CompressionMode.Decompress, leaveOpen: true); - while ((nRead = gzip.Read(buffer, 0, buffer.Length)) > 0) - { - decompressed.Write(buffer, 0, nRead); - } + // The decompressed stream will be bigger that the source. + using var uncompressedStream = MemoryStreamFactory.GetStream((int)compressedStream.Length); + gZip.CopyTo(uncompressedStream); - return Convert.ToBase64String(decompressed.ToArray()); + return Convert.ToBase64String(uncompressedStream.GetBuffer(), 0, (int)uncompressedStream.Length); }), }; - - public IEnumerable GetMethods() => new[] { _base64, _html, _gZip }; } diff --git a/src/OrchardCore/OrchardCore.Infrastructure/Scripting/Files/FilesScriptEngine.cs b/src/OrchardCore/OrchardCore.Infrastructure/Scripting/Files/FilesScriptEngine.cs index 1854717278c..cf8277d1d17 100644 --- a/src/OrchardCore/OrchardCore.Infrastructure/Scripting/Files/FilesScriptEngine.cs +++ b/src/OrchardCore/OrchardCore.Infrastructure/Scripting/Files/FilesScriptEngine.cs @@ -46,9 +46,11 @@ public object Evaluate(IScriptingScope scope, string script) } using var fileStream = fileInfo.CreateReadStream(); - using var ms = new MemoryStream(); - fileStream.CopyTo(ms); - return Convert.ToBase64String(ms.ToArray()); + using var memoryStream = MemoryStreamFactory.GetStream(); + memoryStream.WriteTo(fileStream); + memoryStream.Seek(0, SeekOrigin.Begin); + + return Convert.ToBase64String(memoryStream.GetBuffer(), 0, (int)memoryStream.Length); } else { diff --git a/src/OrchardCore/OrchardCore.Infrastructure/Shells.Database/Configuration/DatabaseShellConfigurationSources.cs b/src/OrchardCore/OrchardCore.Infrastructure/Shells.Database/Configuration/DatabaseShellConfigurationSources.cs index 533c6dccad5..04965303430 100644 --- a/src/OrchardCore/OrchardCore.Infrastructure/Shells.Database/Configuration/DatabaseShellConfigurationSources.cs +++ b/src/OrchardCore/OrchardCore.Infrastructure/Shells.Database/Configuration/DatabaseShellConfigurationSources.cs @@ -74,7 +74,9 @@ public async Task AddSourcesAsync(string tenant, IConfigurationBuilder builder) if (configuration is not null) { var configurationString = configuration.ToJsonString(JOptions.Default); - builder.AddTenantJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(configurationString))); + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(configurationString)); + + builder.AddTenantJsonStream(stream); } } diff --git a/src/OrchardCore/OrchardCore.Infrastructure/Shells.Database/Configuration/DatabaseShellsSettingsSources.cs b/src/OrchardCore/OrchardCore.Infrastructure/Shells.Database/Configuration/DatabaseShellsSettingsSources.cs index f6cfc8072e8..5e73c9892da 100644 --- a/src/OrchardCore/OrchardCore.Infrastructure/Shells.Database/Configuration/DatabaseShellsSettingsSources.cs +++ b/src/OrchardCore/OrchardCore.Infrastructure/Shells.Database/Configuration/DatabaseShellsSettingsSources.cs @@ -42,7 +42,9 @@ public async Task AddSourcesAsync(IConfigurationBuilder builder) if (document.ShellsSettings is not null) { var shellsSettingsString = document.ShellsSettings.ToJsonString(JOptions.Default); - builder.AddTenantJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(shellsSettingsString))); + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(shellsSettingsString)); + + builder.AddTenantJsonStream(stream); } } @@ -53,7 +55,9 @@ public async Task AddSourcesAsync(string tenant, IConfigurationBuilder builder) { var shellSettings = new JsonObject { [tenant] = document.ShellsSettings[tenant] }; var shellSettingsString = shellSettings.ToJsonString(JOptions.Default); - builder.AddTenantJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(shellSettingsString))); + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(shellSettingsString)); + + builder.AddTenantJsonStream(stream); } } diff --git a/src/OrchardCore/OrchardCore.Shells.Azure/Configuration/BlobShellConfigurationSources.cs b/src/OrchardCore/OrchardCore.Shells.Azure/Configuration/BlobShellConfigurationSources.cs index 08b5e42a803..3497264f521 100644 --- a/src/OrchardCore/OrchardCore.Shells.Azure/Configuration/BlobShellConfigurationSources.cs +++ b/src/OrchardCore/OrchardCore.Shells.Azure/Configuration/BlobShellConfigurationSources.cs @@ -91,12 +91,12 @@ public async Task SaveAsync(string tenant, IDictionary data) public async Task RemoveAsync(string tenant) { - var appsettings = IFileStoreExtensions.Combine(null, _container, tenant, OrchardCoreConstants.Configuration.ApplicationSettingsFileName); + var appSettings = IFileStoreExtensions.Combine(null, _container, tenant, OrchardCoreConstants.Configuration.ApplicationSettingsFileName); - var fileInfo = await _shellsFileStore.GetFileInfoAsync(appsettings); + var fileInfo = await _shellsFileStore.GetFileInfoAsync(appSettings); if (fileInfo != null) { - await _shellsFileStore.RemoveFileAsync(appsettings); + await _shellsFileStore.RemoveFileAsync(appSettings); } } diff --git a/test/OrchardCore.Tests/Abstractions/Base64Tests.cs b/test/OrchardCore.Tests/Abstractions/Base64Tests.cs new file mode 100644 index 00000000000..aae947741f6 --- /dev/null +++ b/test/OrchardCore.Tests/Abstractions/Base64Tests.cs @@ -0,0 +1,14 @@ +namespace OrchardCore.Json.Nodes.Test; + +public class Base64Tests +{ + [Theory] + [InlineData("YTw+OmE/", "a<>:a?")] + [InlineData("SGVsbA==", "Hell")] + [InlineData("SGVsbG8=", "Hello")] + [InlineData("", "")] + public void MergeArrayShouldRespectJsonMergeSettings(string source, string expected) + { + Assert.Equal(expected, Base64.FromUTF8Base64String(source)); + } +} diff --git a/test/OrchardCore.Tests/Apis/Context/BlogPostDeploymentContext.cs b/test/OrchardCore.Tests/Apis/Context/BlogPostDeploymentContext.cs index 97bc5f6faa7..5a14ee17023 100644 --- a/test/OrchardCore.Tests/Apis/Context/BlogPostDeploymentContext.cs +++ b/test/OrchardCore.Tests/Apis/Context/BlogPostDeploymentContext.cs @@ -67,7 +67,7 @@ public static JsonObject GetContentStepRecipe(ContentItem contentItem, Action PostRecipeAsync(JsonObject recipe, bool ensureSuccess = true) { - using var zipStream = new MemoryStream(); + await using var zipStream = MemoryStreamFactory.GetStream(); using (var zip = new ZipArchive(zipStream, ZipArchiveMode.Create, true)) { var entry = zip.CreateEntry("Recipe.json"); @@ -75,7 +75,7 @@ public async Task PostRecipeAsync(JsonObject recipe, bool e recipe.WriteTo(streamWriter); } - zipStream.Position = 0; + zipStream.Seek(0, SeekOrigin.Begin); using var requestContent = new MultipartFormDataContent { diff --git a/test/OrchardCore.Tests/Localization/PortableObjectStringLocalizerTests.cs b/test/OrchardCore.Tests/Localization/PortableObjectStringLocalizerTests.cs index e6fa51feb97..e3d6712c832 100644 --- a/test/OrchardCore.Tests/Localization/PortableObjectStringLocalizerTests.cs +++ b/test/OrchardCore.Tests/Localization/PortableObjectStringLocalizerTests.cs @@ -163,7 +163,7 @@ public void HtmlLocalizerDoesNotFormatTwiceIfFormattedTranslationContainsCurlyBr { var htmlLocalizer = new PortableObjectHtmlLocalizer(localizer); var unformatted = htmlLocalizer["The page (ID:{0}) was deleted.", "{1}"]; - var memStream = new MemoryStream(); + var memStream = MemoryStreamFactory.GetStream(); var textWriter = new StreamWriter(memStream); var textReader = new StreamReader(memStream); diff --git a/test/OrchardCore.Tests/Modules/OrchardCore.Media/MediaEventTests.cs b/test/OrchardCore.Tests/Modules/OrchardCore.Media/MediaEventTests.cs index e4e7b84ef26..f00a0c27ff6 100644 --- a/test/OrchardCore.Tests/Modules/OrchardCore.Media/MediaEventTests.cs +++ b/test/OrchardCore.Tests/Modules/OrchardCore.Media/MediaEventTests.cs @@ -24,7 +24,7 @@ public async Task DisposesMediaCreatingStreams() #pragma warning restore CA1859 try { - inputStream = new MemoryStream(); + inputStream = MemoryStreamFactory.GetStream(); originalStream = inputStream; // Add original stream to streams to maintain reference to test disposal. @@ -78,8 +78,9 @@ public class TestMediaEventHandler : IMediaCreatingEventHandler { public async Task MediaCreatingAsync(MediaCreatingContext context, Stream inputStream) { - var outStream = new MemoryStream(); + var outStream = MemoryStreamFactory.GetStream(); await inputStream.CopyToAsync(outStream); + outStream.Seek(0, SeekOrigin.Begin); return outStream; } diff --git a/test/OrchardCore.Tests/Modules/OrchardCore.Scripting/GlobalMethodsTests.cs b/test/OrchardCore.Tests/Modules/OrchardCore.Scripting/GlobalMethodsTests.cs new file mode 100644 index 00000000000..db65a41a687 --- /dev/null +++ b/test/OrchardCore.Tests/Modules/OrchardCore.Scripting/GlobalMethodsTests.cs @@ -0,0 +1,36 @@ +using OrchardCore.Scripting; + +namespace OrchardCore.Json.Nodes.Test; + +public class GlobalMethodsTests +{ + [Fact] + public void ShouldBase64EncodeCompressedBase64() + { + var gzip = (Func)CommonGeneratorMethods._gZip.Method.Invoke(null); + var source = "H4sIAOCaLmcAA/NIzcnJVwjPL8pJUQQAoxwpHAwAAAA="; + var expected = Convert.ToBase64String(Encoding.UTF8.GetBytes("Hello World!")); + + Assert.Equal(expected, gzip(source)); + } + + [Fact] + public void ShouldBase64Decode() + { + var base64encode = (Func)CommonGeneratorMethods._base64.Method.Invoke(null); + var source = Convert.ToBase64String(Encoding.UTF8.GetBytes("Hello World!")); + var expected = "Hello World!"; + + Assert.Equal(expected, base64encode(source)); + } + + [Fact] + public void ShouldHtmlDecode() + { + var htmldecode = (Func)CommonGeneratorMethods._html.Method.Invoke(null); + var source = "<Hello>"; + var expected = ""; + + Assert.Equal(expected, htmldecode(source)); + } +} diff --git a/test/OrchardCore.Tests/Serializers/DefaultDocumentSerializerTests.cs b/test/OrchardCore.Tests/Serializers/DefaultDocumentSerializerTests.cs new file mode 100644 index 00000000000..a1bbe4d46b9 --- /dev/null +++ b/test/OrchardCore.Tests/Serializers/DefaultDocumentSerializerTests.cs @@ -0,0 +1,30 @@ +using System.Text.Json; +using OrchardCore.Documents; +using OrchardCore.Settings; + +namespace OrchardCore.Tests.Serializers; + +public class DefaultDocumentSerializerTests +{ + [Fact] + public async Task ShouldSerializeAndDeserialize() + { + var settings = new SiteSettings + { + AppendVersion = true, + BaseUrl = "http://localhost", + }; + + var serializer = new DefaultDocumentSerializer(JsonSerializerOptions.Default); + + var data = await serializer.SerializeAsync(settings, 0); + + // Data should be gzipped + Assert.Equal([0x1f, 0x8b], data.AsSpan().Slice(0, 2).ToArray()); + + var settings2 = await serializer.DeserializeAsync(data); + + Assert.Equal(settings.AppendVersion, settings2.AppendVersion); + Assert.Equal(settings.BaseUrl, settings2.BaseUrl); + } +} diff --git a/test/OrchardCore.Tests/Stubs/MemoryFileBuilder.cs b/test/OrchardCore.Tests/Stubs/MemoryFileBuilder.cs index 1896687c859..370e1c9c772 100644 --- a/test/OrchardCore.Tests/Stubs/MemoryFileBuilder.cs +++ b/test/OrchardCore.Tests/Stubs/MemoryFileBuilder.cs @@ -6,16 +6,16 @@ namespace OrchardCore.Tests.Stubs; /// In memory file builder that uses a dictionary as virtual file system. /// Intended for unit testing. /// -public class MemoryFileBuilder - : IFileBuilder +public class MemoryFileBuilder : IFileBuilder { public Dictionary VirtualFiles { get; private set; } = []; public async Task SetFileAsync(string subpath, Stream stream) { - using var ms = new MemoryStream(); + var buffer = new byte[stream.Length]; + using var ms = new MemoryStream(buffer); await stream.CopyToAsync(ms); - VirtualFiles[subpath] = ms.ToArray(); + VirtualFiles[subpath] = buffer; } ///