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;
}
///