diff --git a/src/All.slnx b/src/All.slnx index 56b9455c5ed..2ccf0322499 100644 --- a/src/All.slnx +++ b/src/All.slnx @@ -330,4 +330,4 @@ - + \ No newline at end of file diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/OperationRequestBuilderExtensions.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/Extensions/OperationRequestBuilderExtensions.cs similarity index 100% rename from src/HotChocolate/Core/src/Execution.Abstractions/Execution/OperationRequestBuilderExtensions.cs rename to src/HotChocolate/Core/src/Execution.Abstractions/Execution/Extensions/OperationRequestBuilderExtensions.cs diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs index 7e8b2d76b2d..5c1da4b5368 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs @@ -238,6 +238,11 @@ public ImmutableArray CreateVariableValueSets( (next, current) = (current, next); next.Clear(); + + if (current.Count == 0) + { + return []; + } } PooledArrayWriter? buffer = null; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/ArchiveSession.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/ArchiveSession.cs index df4b3e4fecb..894f4b86282 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/ArchiveSession.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/ArchiveSession.cs @@ -1,3 +1,4 @@ +using System.Buffers; using System.IO.Compression; namespace HotChocolate.Fusion.Packaging; @@ -6,15 +7,17 @@ internal sealed class ArchiveSession : IDisposable { private readonly Dictionary _files = []; private readonly ZipArchive _archive; + private readonly FusionArchiveReadOptions _readOptions; private FusionArchiveMode _mode; private bool _disposed; - public ArchiveSession(ZipArchive archive, FusionArchiveMode mode) + public ArchiveSession(ZipArchive archive, FusionArchiveMode mode, FusionArchiveReadOptions readOptions) { ArgumentNullException.ThrowIfNull(archive); _archive = archive; _mode = mode; + _readOptions = readOptions; } public bool HasUncommittedChanges @@ -39,7 +42,7 @@ public IEnumerable GetFiles() return files; } - public async Task ExistsAsync(string path, CancellationToken cancellationToken) + public async Task ExistsAsync(string path, FileKind kind, CancellationToken cancellationToken) { if (_files.TryGetValue(path, out var file)) { @@ -49,12 +52,7 @@ public async Task ExistsAsync(string path, CancellationToken cancellationT if (_mode is not FusionArchiveMode.Create && _archive.GetEntry(path) is { } entry) { file = FileEntry.Read(path); -#if NET10_0_OR_GREATER - await entry.ExtractToFileAsync(file.TempPath, cancellationToken); -#else - entry.ExtractToFile(file.TempPath); - await Task.CompletedTask; -#endif + await ExtractFileAsync(entry, file, GetAllowedSize(kind), cancellationToken); _files.Add(path, file); return true; } @@ -72,7 +70,7 @@ public bool Exists(string path) return _mode is not FusionArchiveMode.Create && _archive.GetEntry(path) is not null; } - public async Task OpenReadAsync(string path, CancellationToken cancellationToken) + public async Task OpenReadAsync(string path, FileKind kind, CancellationToken cancellationToken) { if (_files.TryGetValue(path, out var file)) { @@ -87,12 +85,7 @@ public async Task OpenReadAsync(string path, CancellationToken cancellat if (_mode is not FusionArchiveMode.Create && _archive.GetEntry(path) is { } entry) { file = FileEntry.Read(path); -#if NET10_0_OR_GREATER - await entry.ExtractToFileAsync(file.TempPath, cancellationToken); -#else - entry.ExtractToFile(file.TempPath); - await Task.CompletedTask; -#endif + await ExtractFileAsync(entry, file, GetAllowedSize(kind), cancellationToken); var stream = File.OpenRead(file.TempPath); _files.Add(path, file); return stream; @@ -181,6 +174,43 @@ await _archive.CreateEntryFromFileAsync( } } + private static async Task ExtractFileAsync( + ZipArchiveEntry zipEntry, + FileEntry fileEntry, + int maxAllowedSize, + CancellationToken cancellationToken) + { + var buffer = ArrayPool.Shared.Rent(4096); + var consumed = 0; + + await using var readStream = zipEntry.Open(); + await using var writeStream = File.Open(fileEntry.TempPath, FileMode.Create, FileAccess.Write); + + int read; + while ((read = await readStream.ReadAsync(buffer, cancellationToken)) > 0) + { + consumed += read; + + if (consumed > maxAllowedSize) + { + throw new InvalidOperationException( + $"File is too large and exceeds the allowed size of {maxAllowedSize}."); + } + + await writeStream.WriteAsync(buffer.AsMemory(0, read), cancellationToken); + } + } + + private int GetAllowedSize(FileKind kind) + => kind switch + { + FileKind.Schema + => _readOptions.MaxAllowedSchemaSize, + FileKind.Manifest or FileKind.Settings or FileKind.Metadata or FileKind.Signature + => _readOptions.MaxAllowedSettingsSize, + _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null) + }; + public void Dispose() { if (_disposed) @@ -188,14 +218,22 @@ public void Dispose() return; } - _disposed = true; foreach (var file in _files.Values) { - if (file.State is not FileState.Deleted) + if (file.State is not FileState.Deleted && File.Exists(file.TempPath)) { - File.Delete(file.TempPath); + try + { + File.Delete(file.TempPath); + } + catch + { + // ignore + } } } + + _disposed = true; } private class FileEntry diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FileKind.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FileKind.cs new file mode 100644 index 00000000000..916809e8fe8 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FileKind.cs @@ -0,0 +1,10 @@ +namespace HotChocolate.Fusion.Packaging; + +internal enum FileKind +{ + Schema, + Settings, + Manifest, + Metadata, + Signature +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FileNames.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FileNames.cs index fba1f758cd0..c73a3e2e22a 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FileNames.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FileNames.cs @@ -5,6 +5,7 @@ internal static class FileNames private const string GatewaySchemaFormat = "gateway/{0}/gateway.graphqls"; private const string GatewaySettingsFormat = "gateway/{0}/gateway-settings.json"; private const string SourceSchemaFormat = "source-schemas/{0}/schema.graphqls"; + private const string SourceSchemaSettingsFormat = "source-schemas/{0}/schema-settings.json"; public const string ArchiveMetadata = "archive-metadata.json"; public const string CompositionSettings = "composition-settings.json"; @@ -19,4 +20,34 @@ public static string GetGatewaySettingsPath(Version version) public static string GetSourceSchemaPath(string schemaName) => string.Format(SourceSchemaFormat, schemaName); + + public static string GetSourceSchemaSettingsPath(string schemaName) + => string.Format(SourceSchemaSettingsFormat, schemaName); + + public static FileKind GetFileKind(string fileName) + { + switch (Path.GetFileName(fileName)) + { + case "gateway.graphqls": + case "schema.graphqls": + return FileKind.Schema; + + case "schema-settings.json": + case "gateway-settings.json": + case "composition-settings.json": + return FileKind.Settings; + + case "archive-metadata.json": + return FileKind.Metadata; + + case "manifest.json": + return FileKind.Manifest; + + case "signature.json": + return FileKind.Signature; + + default: + return FileKind.Settings; + } + } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FusionArchive.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FusionArchive.cs index 7f2d38bc389..dcb7f970c8f 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FusionArchive.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FusionArchive.cs @@ -26,13 +26,17 @@ public sealed class FusionArchive : IDisposable private ArchiveMetadata? _metadata; private bool _disposed; - private FusionArchive(Stream stream, FusionArchiveMode mode, bool leaveOpen = false) + private FusionArchive( + Stream stream, + FusionArchiveMode mode, + bool leaveOpen, + FusionArchiveReadOptions options) { _stream = stream; _mode = mode; _leaveOpen = leaveOpen; _archive = new ZipArchive(stream, (ZipArchiveMode)mode, leaveOpen); - _session = new ArchiveSession(_archive, mode); + _session = new ArchiveSession(_archive, mode, options); } /// @@ -57,7 +61,7 @@ public static FusionArchive Create(string filename) public static FusionArchive Create(Stream stream, bool leaveOpen = false) { ArgumentNullException.ThrowIfNull(stream); - return new FusionArchive(stream, FusionArchiveMode.Create, leaveOpen); + return new FusionArchive(stream, FusionArchiveMode.Create, leaveOpen, FusionArchiveReadOptions.Default); } /// @@ -89,15 +93,20 @@ public static FusionArchive Open( /// The stream containing the archive data. /// The mode to open the archive in. /// True to leave the stream open after disposal; otherwise, false. + /// The options to use when reading from the archive. /// A FusionArchive instance opened in the specified mode. /// Thrown when stream is null. public static FusionArchive Open( Stream stream, FusionArchiveMode mode = FusionArchiveMode.Read, - bool leaveOpen = false) + bool leaveOpen = false, + FusionArchiveOptions options = default) { ArgumentNullException.ThrowIfNull(stream); - return new FusionArchive(stream, mode, leaveOpen); + var readOptions = new FusionArchiveReadOptions( + options.MaxAllowedSchemaSize ?? FusionArchiveReadOptions.Default.MaxAllowedSchemaSize, + options.MaxAllowedSettingsSize ?? FusionArchiveReadOptions.Default.MaxAllowedSettingsSize); + return new FusionArchive(stream, mode, leaveOpen, readOptions); } /// @@ -156,7 +165,7 @@ public async Task SetArchiveMetadataAsync( return _metadata; } - if (!await _session.ExistsAsync(FileNames.ArchiveMetadata, cancellationToken)) + if (!await _session.ExistsAsync(FileNames.ArchiveMetadata, FileKind.Metadata, cancellationToken)) { return null; } @@ -165,7 +174,10 @@ public async Task SetArchiveMetadataAsync( try { - await using var stream = await _session.OpenReadAsync(FileNames.ArchiveMetadata, cancellationToken); + await using var stream = await _session.OpenReadAsync( + FileNames.ArchiveMetadata, + FileKind.Metadata, + cancellationToken); await stream.CopyToAsync(buffer, cancellationToken); var metadata = ArchiveMetadataSerializer.Parse(buffer.WrittenMemory); _metadata = metadata; @@ -292,51 +304,61 @@ public async Task SetCompositionSettingsAsync( { ObjectDisposedException.ThrowIf(_disposed, this); - if (!await _session.ExistsAsync(FileNames.CompositionSettings, cancellationToken)) + if (!await _session.ExistsAsync(FileNames.CompositionSettings, FileKind.Settings, cancellationToken)) { return null; } - await using var stream = await _session.OpenReadAsync(FileNames.CompositionSettings, cancellationToken); + await using var stream = await _session.OpenReadAsync( + FileNames.CompositionSettings, + FileKind.Settings, + cancellationToken); return await JsonDocument.ParseAsync(stream, default, cancellationToken); } /// - /// Sets the gateway schema for a specific format version. + /// Sets the gateway configuration for a specific format version using raw bytes. /// The version must be declared in the archive metadata before calling this method. /// /// The gateway schema as a GraphQL schema string. + /// The gateway settings as a JSON document. /// The gateway format version. /// Token to cancel the operation. /// Thrown when schema is null or empty. /// Thrown when version is null. /// Thrown when the archive has been disposed. /// Thrown when the archive is read-only, metadata is missing, or version is not declared. - public async Task SetGatewaySchemaAsync( + public async Task SetGatewayConfigurationAsync( string schema, + JsonDocument settings, Version version, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(schema); + ArgumentNullException.ThrowIfNull(settings); ArgumentNullException.ThrowIfNull(version); ObjectDisposedException.ThrowIf(_disposed, this); EnsureMutable(); - await SetGatewaySchemaAsync(Encoding.UTF8.GetBytes(schema), version, cancellationToken); + await SetGatewayConfigurationAsync(Encoding.UTF8.GetBytes(schema), settings, version, cancellationToken); } /// - /// Sets the gateway schema for a specific format version using raw bytes. + /// Sets the gateway configuration for a specific format version using raw bytes. /// The version must be declared in the archive metadata before calling this method. /// /// The gateway schema as UTF-8 encoded bytes. + /// The gateway settings as a JSON document. /// The gateway format version. /// Token to cancel the operation. /// Thrown when version is null. /// Thrown when the archive has been disposed. - /// Thrown when the archive is read-only, metadata is missing, or version is not declared. - public async Task SetGatewaySchemaAsync( + /// + /// Thrown when the archive is read-only, metadata is missing, or version is not declared. + /// + public async Task SetGatewayConfigurationAsync( ReadOnlyMemory schema, + JsonDocument settings, Version version, CancellationToken cancellationToken = default) { @@ -358,116 +380,30 @@ public async Task SetGatewaySchemaAsync( "You need to first declare the gateway schema version in the archive metadata."); } - await using var stream = _session.OpenWrite(FileNames.GetGatewaySchemaPath(version)); - await stream.WriteAsync(schema, cancellationToken); - } - - /// - /// Attempts to get a gateway schema with the highest version that is less than or equal to the specified maximum version. - /// The schema data is written to the provided buffer. - /// - /// The maximum version to consider. - /// The buffer to write the schema data to. - /// Token to cancel the operation. - /// A result indicating whether resolution was successful and the actual version used. - /// Thrown when maxVersion or buffer is null. - /// Thrown when the archive has been disposed. - /// Thrown when no supported gateway formats are found. - public async Task TryGetGatewaySchemaAsync( - Version maxVersion, - IBufferWriter buffer, - CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(maxVersion); - ArgumentNullException.ThrowIfNull(buffer); - ObjectDisposedException.ThrowIf(_disposed, this); - - var metadata = await GetArchiveMetadataAsync(cancellationToken); - if (metadata?.SupportedGatewayFormats == null || !metadata.SupportedGatewayFormats.Any()) - { - throw new InvalidOperationException("No supported gateway formats found in archive metadata."); - } - - // we need to find the version that is less than or equal to the maxVersion - var version = metadata.SupportedGatewayFormats.OrderByDescending(v => v).FirstOrDefault(v => v <= maxVersion); - if (version == null) - { - return new ResolvedGatewaySchemaResult { IsResolved = false, ActualVersion = null }; - } - - await using var stream = await _session.OpenReadAsync( - FileNames.GetGatewaySchemaPath(version), - cancellationToken); - await stream.CopyToAsync(buffer, cancellationToken); - return new ResolvedGatewaySchemaResult { IsResolved = true, ActualVersion = version }; - } - - /// - /// Sets the gateway settings for a specific format version. - /// The version must be declared in the archive metadata before calling this method. - /// - /// The gateway settings as a JSON document. - /// The gateway format version. - /// Token to cancel the operation. - /// Thrown when settings or version is null. - /// Thrown when the archive has been disposed. - /// Thrown when the archive is read-only, metadata is missing, or version is not declared. - public async Task SetGatewaySettingsAsync( - JsonDocument settings, - Version version, - CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(settings); - ArgumentNullException.ThrowIfNull(version); - ObjectDisposedException.ThrowIf(_disposed, this); - EnsureMutable(); - - var metadata = await GetArchiveMetadataAsync(cancellationToken); - - if (metadata is null) - { - throw new InvalidOperationException( - "You need to first define the archive metadata."); - } - - if (!metadata.SupportedGatewayFormats.Contains(version)) + await using (var stream = _session.OpenWrite(FileNames.GetGatewaySchemaPath(version))) { - throw new InvalidOperationException( - "You need to first declare the gateway schema version in the archive metadata."); + await stream.WriteAsync(schema, cancellationToken); } - Exception? exception = null; - await using var stream = _session.OpenWrite(FileNames.GetGatewaySettingsPath(version)); - var writer = PipeWriter.Create(stream); - - try + await using (var stream = _session.OpenWrite(FileNames.GetGatewaySettingsPath(version))) { - await using var jsonWriter = new Utf8JsonWriter(writer, new JsonWriterOptions { Indented = true }); + await using var jsonWriter = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true }); settings.WriteTo(jsonWriter); await jsonWriter.FlushAsync(cancellationToken); - await writer.FlushAsync(cancellationToken); - } - catch (Exception ex) - { - exception = ex; - throw; - } - finally - { - await writer.CompleteAsync(exception); } } /// - /// Attempts to get gateway settings with the highest version that is less than or equal to the specified maximum version. + /// Attempts to get a gateway schema with the highest version that is less + /// than or equal to the specified maximum version. /// /// The maximum version to consider. /// Token to cancel the operation. - /// A result indicating whether resolution was successful, the actual version used, and the settings. - /// Thrown when maxVersion is null. + /// A gateway configuration. + /// Thrown when maxVersion or buffer is null. /// Thrown when the archive has been disposed. /// Thrown when no supported gateway formats are found. - public async Task TryGetGatewaySettingsAsync( + public async Task TryGetGatewayConfigurationAsync( Version maxVersion, CancellationToken cancellationToken = default) { @@ -484,19 +420,22 @@ public async Task TryGetGatewaySettingsAsync( var version = metadata.SupportedGatewayFormats.OrderByDescending(v => v).FirstOrDefault(v => v <= maxVersion); if (version == null) { - return new ResolvedGatewaySettingsResult { IsResolved = false, ActualVersion = null, Settings = null }; + return null; } - if (!await _session.ExistsAsync(FileNames.GetGatewaySettingsPath(version), cancellationToken)) + JsonDocument settings; + await using (var stream = await _session.OpenReadAsync( + FileNames.GetGatewaySettingsPath(version), + FileKind.Settings, + cancellationToken)) { - return new ResolvedGatewaySettingsResult { IsResolved = false, ActualVersion = null, Settings = null }; + settings = await JsonDocument.ParseAsync(stream, default, cancellationToken); } - await using var stream = await _session.OpenReadAsync( - FileNames.GetGatewaySettingsPath(version), - cancellationToken); - var settings = await JsonDocument.ParseAsync(stream, default, cancellationToken); - return new ResolvedGatewaySettingsResult { IsResolved = true, ActualVersion = version, Settings = settings }; + return new GatewayConfiguration(OpenReadSchemaAsync, settings, version); + + Task OpenReadSchemaAsync(CancellationToken ct) + => _session.OpenReadAsync(FileNames.GetGatewaySchemaPath(version), FileKind.Schema, ct); } /// @@ -505,17 +444,20 @@ public async Task TryGetGatewaySettingsAsync( /// /// The name of the source schema. /// The source schema as UTF-8 encoded bytes. + /// The source schema configuration. /// Token to cancel the operation. /// Thrown when schemaName is null, empty, or invalid. /// Thrown when schema is empty. /// Thrown when the archive has been disposed. /// Thrown when the archive is read-only, metadata is missing, or schema name is not declared. - public async Task SetSourceSchemaAsync( + public async Task SetSourceSchemaConfigurationAsync( string schemaName, ReadOnlyMemory schema, + JsonDocument settings, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(schemaName); + ArgumentNullException.ThrowIfNull(settings); ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(schema.Length, 0); ObjectDisposedException.ThrowIf(_disposed, this); @@ -540,39 +482,55 @@ public async Task SetSourceSchemaAsync( "You need to first declare the source schema in the archive metadata."); } - await using var stream = _session.OpenWrite(FileNames.GetSourceSchemaPath(schemaName)); - await stream.WriteAsync(schema, cancellationToken); + await using (var stream = _session.OpenWrite(FileNames.GetSourceSchemaPath(schemaName))) + { + await stream.WriteAsync(schema, cancellationToken); + } + + await using (var stream = _session.OpenWrite(FileNames.GetSourceSchemaSettingsPath(schemaName))) + { + await using var jsonWriter = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true }); + settings.WriteTo(jsonWriter); + await jsonWriter.FlushAsync(cancellationToken); + } } /// - /// Attempts to get a source schema from the archive. - /// The schema data is written to the provided buffer if found. + /// Attempts to get a source schema configuration from the archive. /// /// The name of the source schema to retrieve. - /// The buffer to write the schema data to. /// Token to cancel the operation. - /// True if the schema was found and retrieved; otherwise, false. + /// A source schema configuration. /// Thrown when schemaName or buffer is null. /// Thrown when the archive has been disposed. - public async Task TryGetSourceSchemaAsync( + public async Task TryGetSourceSchemaConfigurationAsync( string schemaName, - IBufferWriter buffer, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(schemaName); - ArgumentNullException.ThrowIfNull(buffer); ObjectDisposedException.ThrowIf(_disposed, this); - if (!await _session.ExistsAsync(FileNames.GetSourceSchemaPath(schemaName), cancellationToken)) + if (!await _session.ExistsAsync( + FileNames.GetSourceSchemaPath(schemaName), + FileKind.Schema, + cancellationToken)) { - return false; + return null; } - await using var stream = await _session.OpenReadAsync( - FileNames.GetSourceSchemaPath(schemaName), - cancellationToken); - await stream.CopyToAsync(buffer, cancellationToken); - return true; + JsonDocument settings; + await using (var stream = await _session.OpenReadAsync( + FileNames.GetSourceSchemaSettingsPath(schemaName), + FileKind.Settings, + cancellationToken)) + { + settings = await JsonDocument.ParseAsync(stream, default, cancellationToken); + } + + return new SourceSchemaConfiguration(OpenReadSchemaAsync, settings); + + Task OpenReadSchemaAsync(CancellationToken ct) + => _session.OpenReadAsync(FileNames.GetSourceSchemaPath(schemaName), FileKind.Schema, ct); } /// @@ -642,8 +600,14 @@ public async Task VerifySignatureAsync( X509Certificate2 publicKey, CancellationToken cancellationToken = default) { - var manifestExists = await _session.ExistsAsync(FileNames.SignatureManifest, cancellationToken); - var signatureExists = await _session.ExistsAsync(FileNames.Signature, cancellationToken); + var manifestExists = await _session.ExistsAsync( + FileNames.SignatureManifest, + FileKind.Manifest, + cancellationToken); + var signatureExists = await _session.ExistsAsync( + FileNames.Signature, + FileKind.Signature, + cancellationToken); if (!manifestExists || !signatureExists) { @@ -657,9 +621,11 @@ public async Task VerifySignatureAsync( // 1. Load manifest and signature await using var manifestStream = await _session.OpenReadAsync( FileNames.SignatureManifest, + FileKind.Manifest, cancellationToken); await using var signatureStream = await _session.OpenReadAsync( FileNames.Signature, + FileKind.Signature, cancellationToken); await manifestStream.CopyToAsync(buffer, cancellationToken); var manifest = SignatureManifestSerializer.Parse(buffer.WrittenMemory); @@ -672,12 +638,14 @@ public async Task VerifySignatureAsync( // 2. Verify file integrity foreach (var file in manifest.Files.OrderBy(t => t.Key)) { - if (!await _session.ExistsAsync(file.Key, cancellationToken)) + var kind = FileNames.GetFileKind(file.Key); + + if (!await _session.ExistsAsync(file.Key, kind, cancellationToken)) { return SignatureVerificationResult.FilesMissing; } - var actualHash = await ComputeFileHashAsync(file.Key, cancellationToken); + var actualHash = await ComputeFileHashAsync(file.Key, kind, cancellationToken); if (!actualHash.Equals(file.Value, StringComparison.OrdinalIgnoreCase)) { return SignatureVerificationResult.FilesModified; @@ -726,8 +694,14 @@ public async Task VerifySignatureAsync( public async Task GetSignatureInfoAsync( CancellationToken cancellationToken = default) { - var manifestExists = await _session.ExistsAsync(FileNames.SignatureManifest, cancellationToken); - var signatureExists = await _session.ExistsAsync(FileNames.Signature, cancellationToken); + var manifestExists = await _session.ExistsAsync( + FileNames.SignatureManifest, + FileKind.Manifest, + cancellationToken); + var signatureExists = await _session.ExistsAsync( + FileNames.Signature, + FileKind.Signature, + cancellationToken); if (!manifestExists || !signatureExists) { @@ -740,9 +714,11 @@ public async Task VerifySignatureAsync( { await using var manifestStream = await _session.OpenReadAsync( FileNames.SignatureManifest, + FileKind.Manifest, cancellationToken); await using var signatureStream = await _session.OpenReadAsync( FileNames.Signature, + FileKind.Signature, cancellationToken); await manifestStream.CopyToAsync(buffer, 1024, cancellationToken); @@ -823,7 +799,8 @@ private async Task GenerateManifestAsync(CancellationToken ca continue; } - files[path] = await ComputeFileHashAsync(path, cancellationToken); + var kind = FileNames.GetFileKind(path); + files[path] = await ComputeFileHashAsync(path, kind, cancellationToken); } var manifest = new SignatureManifest @@ -846,9 +823,9 @@ private async Task GenerateManifestAsync(CancellationToken ca } } - private async Task ComputeFileHashAsync(string path, CancellationToken cancellationToken) + private async Task ComputeFileHashAsync(string path, FileKind kind, CancellationToken cancellationToken) { - await using var stream = await _session.OpenReadAsync(path, cancellationToken); + await using var stream = await _session.OpenReadAsync(path, kind, cancellationToken); using var sha256 = SHA256.Create(); var hashBytes = await sha256.ComputeHashAsync(stream, cancellationToken); #if NET9_0_OR_GREATER diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FusionArchiveOptions.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FusionArchiveOptions.cs new file mode 100644 index 00000000000..eced391031d --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FusionArchiveOptions.cs @@ -0,0 +1,17 @@ +namespace HotChocolate.Fusion.Packaging; + +/// +/// Specifies the options for a Fusion Archive. +/// +public struct FusionArchiveOptions +{ + /// + /// Gets or sets the maximum allowed size of a schema in the archive. + /// + public int? MaxAllowedSchemaSize { get; set; } + + /// + /// Gets or sets the maximum allowed size of the settings in the archive. + /// + public int? MaxAllowedSettingsSize { get; set; } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FusionArchiveReadOptions.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FusionArchiveReadOptions.cs new file mode 100644 index 00000000000..d26d53e688e --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FusionArchiveReadOptions.cs @@ -0,0 +1,14 @@ +namespace HotChocolate.Fusion.Packaging; + +/// +/// Specifies the read options for a Fusion Archive. +/// +internal readonly record struct FusionArchiveReadOptions( + int MaxAllowedSchemaSize, + int MaxAllowedSettingsSize) +{ + /// + /// Gets the default read options. + /// + public static FusionArchiveReadOptions Default { get; } = new(50_000_000, 512_000); +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/GatewayConfiguration.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/GatewayConfiguration.cs new file mode 100644 index 00000000000..cdfed0c3a3e --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/GatewayConfiguration.cs @@ -0,0 +1,53 @@ +using System.Text.Json; + +namespace HotChocolate.Fusion.Packaging; + +/// +/// Represents a Hot Chocolate Fusion gateway configuration. +/// +public sealed class GatewayConfiguration : IDisposable +{ + private readonly Func> _openReadSchema; + private bool _disposed; + + internal GatewayConfiguration( + Func> openReadSchema, + JsonDocument settings, + Version version) + { + ArgumentNullException.ThrowIfNull(openReadSchema); + ArgumentNullException.ThrowIfNull(settings); + + _openReadSchema = openReadSchema; + Settings = settings; + Version = version; + } + + /// + /// Gets the version of the gateway configuration. + /// + public Version Version { get; } + + /// + /// Opens the Hot Chocolate Fusion execution schema for reading. + /// + public Task OpenReadSchemaAsync(CancellationToken cancellationToken = default) + => _openReadSchema(cancellationToken); + + /// + /// Gets the settings of the gateway configuration. + /// + public JsonDocument Settings { get; } + + /// + /// Disposes the gateway configuration. + /// + public void Dispose() + { + if (!_disposed) + { + Settings.Dispose(); + _disposed = true; + } + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/ResolvedGatewaySchemaResult.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/ResolvedGatewaySchemaResult.cs deleted file mode 100644 index 01ec9469479..00000000000 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/ResolvedGatewaySchemaResult.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace HotChocolate.Fusion.Packaging; - -/// -/// Represents the result of attempting to resolve a gateway schema version from a Fusion Archive. -/// -public readonly struct ResolvedGatewaySchemaResult -{ - /// - /// Gets the actual gateway format version that was resolved. - /// This may be lower than the requested maximum version if a higher version is not available. - /// Null if no compatible version was found. - /// - public required Version? ActualVersion { get; init; } - - /// - /// Gets a value indicating whether a gateway schema version was successfully resolved. - /// When true, ActualVersion is guaranteed to be non-null. - /// - [MemberNotNullWhen(true, nameof(ActualVersion))] - public required bool IsResolved { get; init; } - - /// - /// Implicitly converts the result to the actual version that was resolved. - /// Returns null if no version was resolved. - /// - /// The result to convert. - /// The actual version or null. - public static implicit operator Version?(ResolvedGatewaySchemaResult result) - => result.ActualVersion; - - /// - /// Implicitly converts the result to a boolean indicating resolution success. - /// - /// The result to convert. - /// True if a schema version was resolved, false otherwise. - public static implicit operator bool(ResolvedGatewaySchemaResult result) - => result.IsResolved; -} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/ResolvedGatewaySettingsResult.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/ResolvedGatewaySettingsResult.cs deleted file mode 100644 index 53c67ebadcc..00000000000 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/ResolvedGatewaySettingsResult.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Text.Json; - -namespace HotChocolate.Fusion.Packaging; - -/// -/// Represents the result of attempting to resolve gateway settings from a Fusion Archive, -/// including the actual version used and the settings document. -/// -public readonly struct ResolvedGatewaySettingsResult -{ - /// - /// Gets the actual gateway format version that was resolved. - /// This may be lower than the requested maximum version if a higher version is not available. - /// Null if no compatible version was found. - /// - public required Version? ActualVersion { get; init; } - - /// - /// Gets a value indicating whether gateway settings were successfully resolved. - /// When true, both ActualVersion and Settings are guaranteed to be non-null. - /// - [MemberNotNullWhen(true, nameof(Settings), nameof(ActualVersion))] - public required bool IsResolved { get; init; } - - /// - /// Gets the resolved gateway settings as a JSON document. - /// Contains the configuration for transport profiles, source schema endpoints, - /// and other gateway runtime settings. Null if resolution failed. - /// - public required JsonDocument? Settings { get; init; } - - /// - /// Implicitly converts the result to the actual version that was resolved. - /// Returns null if no version was resolved. - /// - /// The result to convert. - /// The actual version or null. - public static implicit operator Version?(ResolvedGatewaySettingsResult result) - => result.ActualVersion; - - /// - /// Implicitly converts the result to a boolean indicating resolution success. - /// - /// The result to convert. - /// True if settings were resolved, false otherwise. - public static implicit operator bool(ResolvedGatewaySettingsResult result) - => result.IsResolved; - - /// - /// Implicitly converts the result to the resolved settings JSON document. - /// Returns null if resolution failed. - /// - /// The result to convert. - /// The settings document or null. - public static implicit operator JsonDocument?(ResolvedGatewaySettingsResult result) - => result.Settings; -} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/SourceSchemaConfiguration.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/SourceSchemaConfiguration.cs new file mode 100644 index 00000000000..2a9dc43c8e8 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/SourceSchemaConfiguration.cs @@ -0,0 +1,46 @@ +using System.Text.Json; + +namespace HotChocolate.Fusion.Packaging; + +/// +/// Represents a Hot Chocolate Fusion source schema configuration. +/// +public sealed class SourceSchemaConfiguration : IDisposable +{ + private readonly Func> _openReadSchema; + private bool _disposed; + + internal SourceSchemaConfiguration( + Func> openReadSchema, + JsonDocument settings) + { + ArgumentNullException.ThrowIfNull(openReadSchema); + ArgumentNullException.ThrowIfNull(settings); + + _openReadSchema = openReadSchema; + Settings = settings; + } + + /// + /// Opens the Hot Chocolate Fusion source schema for reading. + /// + public Task OpenReadSchemaAsync(CancellationToken cancellationToken = default) + => _openReadSchema(cancellationToken); + + /// + /// Gets the settings of the source schema configuration. + /// + public JsonDocument Settings { get; } + + /// + /// Disposes the source schema configuration. + /// + public void Dispose() + { + if (!_disposed) + { + Settings.Dispose(); + _disposed = true; + } + } +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Packaging.Tests/FusionArchiveTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Packaging.Tests/FusionArchiveTests.cs index 397dc2e3e98..388efa1ac45 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Packaging.Tests/FusionArchiveTests.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Packaging.Tests/FusionArchiveTests.cs @@ -1,4 +1,3 @@ -using System.Buffers; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; @@ -147,23 +146,28 @@ public async Task SetGatewaySchema_WithStringContent_StoresCorrectly() // Arrange await using var stream = CreateStream(); const string schema = "type Query { hello: String }"; + var settings = CreateSettingsJson(); var version = new Version("2.0.0"); // Act & Assert using var archive = FusionArchive.Create(stream, leaveOpen: true); var metadata = CreateTestMetadata(); await archive.SetArchiveMetadataAsync(metadata); - await archive.SetGatewaySchemaAsync(schema, version); + await archive.SetGatewayConfigurationAsync(schema, settings, version); // Can read immediately within the same session - var buffer = new ArrayBufferWriter(); - var result = await archive.TryGetGatewaySchemaAsync(version, buffer); + var result = await archive.TryGetGatewayConfigurationAsync(version); - Assert.True(result.IsResolved); - Assert.Equal(version, result.ActualVersion); + Assert.NotNull(result); + Assert.Equal(version, result.Version); - var retrievedSchema = Encoding.UTF8.GetString(buffer.WrittenSpan); - Assert.Equal(schema, retrievedSchema); + using (var streamReader = new StreamReader(await result.OpenReadSchemaAsync())) + { + var retrievedSchema = await streamReader.ReadToEndAsync(); + Assert.Equal(schema, retrievedSchema); + } + + result.Dispose(); } [Fact] @@ -172,21 +176,28 @@ public async Task SetGatewaySchema_WithByteContent_StoresCorrectly() // Arrange await using var stream = CreateStream(); var schema = "type Query { hello: String }"u8.ToArray(); + var settings = CreateSettingsJson(); var version = new Version("2.0.0"); // Act & Assert using var archive = FusionArchive.Create(stream, leaveOpen: true); var metadata = CreateTestMetadata(); await archive.SetArchiveMetadataAsync(metadata); - await archive.SetGatewaySchemaAsync(schema, version); + await archive.SetGatewayConfigurationAsync(schema, settings, version); // Can read immediately within the same session - var buffer = new ArrayBufferWriter(); - var result = await archive.TryGetGatewaySchemaAsync(version, buffer); + var result = await archive.TryGetGatewayConfigurationAsync(version); + + Assert.NotNull(result); + Assert.Equal(version, result.Version); - Assert.True(result.IsResolved); - Assert.Equal(version, result.ActualVersion); - Assert.True(schema.AsSpan().SequenceEqual(buffer.WrittenSpan)); + using (var streamReader = new StreamReader(await result.OpenReadSchemaAsync())) + { + var retrievedSchema = await streamReader.ReadToEndAsync(); + Assert.Equal(Encoding.UTF8.GetString(schema), retrievedSchema); + } + + result.Dispose(); } [Fact] @@ -198,7 +209,7 @@ public async Task SetGatewaySchema_WithoutMetadata_ThrowsInvalidOperationExcepti // Act & Assert using var archive = FusionArchive.Create(stream); await Assert.ThrowsAsync( - () => archive.SetGatewaySchemaAsync("schema", new Version("1.0.0"))); + () => archive.SetGatewayConfigurationAsync("schema", CreateSettingsJson(), new Version("1.0.0"))); } [Fact] @@ -213,7 +224,7 @@ public async Task SetGatewaySchema_WithUnsupportedVersion_ThrowsInvalidOperation await archive.SetArchiveMetadataAsync(metadata); await Assert.ThrowsAsync(() => - archive.SetGatewaySchemaAsync("schema", new Version("3.0.0"))); + archive.SetGatewayConfigurationAsync("schema", CreateSettingsJson(),new Version("3.0.0"))); } [Fact] @@ -230,19 +241,23 @@ public async Task TryGetGatewaySchema_WithCompatibleVersion_ReturnsCorrectVersio // Act & Assert using var archive = FusionArchive.Create(stream, leaveOpen: true); await archive.SetArchiveMetadataAsync(metadata); - await archive.SetGatewaySchemaAsync("schema v1.0", new Version("1.0.0")); - await archive.SetGatewaySchemaAsync("schema v2.0", new Version("2.0.0")); - await archive.SetGatewaySchemaAsync("schema v2.1", new Version("2.1.0")); + await archive.SetGatewayConfigurationAsync("schema v1.0", CreateSettingsJson(), new Version("1.0.0")); + await archive.SetGatewayConfigurationAsync("schema v2.0", CreateSettingsJson(), new Version("2.0.0")); + await archive.SetGatewayConfigurationAsync("schema v2.1", CreateSettingsJson(), new Version("2.1.0")); // Request max version 2.0.0, should get 2.0.0 - var buffer = new ArrayBufferWriter(); - var result = await archive.TryGetGatewaySchemaAsync(new Version("2.0.0"), buffer); + var result = await archive.TryGetGatewayConfigurationAsync(new Version("2.0.0")); + + Assert.NotNull(result); + Assert.Equal(new Version("2.0.0"), result.Version); - Assert.True(result.IsResolved); - Assert.Equal(new Version("2.0.0"), result.ActualVersion); + using (var streamReader = new StreamReader(await result.OpenReadSchemaAsync())) + { + var retrievedSchema = await streamReader.ReadToEndAsync(); + Assert.Equal("schema v2.0", retrievedSchema); + } - var schema = Encoding.UTF8.GetString(buffer.WrittenSpan); - Assert.Equal("schema v2.0", schema); + result.Dispose(); } [Fact] @@ -260,46 +275,9 @@ public async Task TryGetGatewaySchema_WithIncompatibleVersion_ReturnsFalse() using var archive = FusionArchive.Create(stream, leaveOpen: true); await archive.SetArchiveMetadataAsync(metadata); - var buffer = new ArrayBufferWriter(); - var result = await archive.TryGetGatewaySchemaAsync(new Version("1.0.0"), buffer); - - Assert.False(result.IsResolved); - Assert.Null(result.ActualVersion); - } - - [Fact] - public async Task SetGatewaySettings_WithValidSettings_StoresCorrectly() - { - // Arrange - await using var stream = CreateStream(); - const string settingsJson = - """ - { - "transportProfiles": { - "http-profile": { - "type": "graphql-over-http" - } - } - } - """; - using var settings = JsonDocument.Parse(settingsJson); - var version = new Version("2.0.0"); - - // Act & Assert - using var archive = FusionArchive.Create(stream, leaveOpen: true); - var metadata = CreateTestMetadata(); - await archive.SetArchiveMetadataAsync(metadata); - await archive.SetGatewaySettingsAsync(settings, version); - - // Can read immediately within the same session - var result = await archive.TryGetGatewaySettingsAsync(version); - Assert.True(result.IsResolved); - Assert.Equal(version, result.ActualVersion); - Assert.NotNull(result.Settings); + var result = await archive.TryGetGatewayConfigurationAsync(new Version("1.0.0")); - var transportProfiles = result.Settings.RootElement.GetProperty("transportProfiles"); - Assert.True(transportProfiles.TryGetProperty("http-profile", out var profile)); - Assert.Equal("graphql-over-http", profile.GetProperty("type").GetString()); + Assert.Null(result); } [Fact] @@ -308,20 +286,23 @@ public async Task SetSourceSchema_WithValidSchema_StoresCorrectly() // Arrange await using var stream = CreateStream(); var schemaContent = "type User { id: ID! name: String! }"u8.ToArray(); + var settings = CreateSettingsJson(); const string schemaName = "user-service"; // Act & Assert using var archive = FusionArchive.Create(stream, leaveOpen: true); var metadata = CreateTestMetadata(); await archive.SetArchiveMetadataAsync(metadata); - await archive.SetSourceSchemaAsync(schemaName, schemaContent); + await archive.SetSourceSchemaConfigurationAsync(schemaName, schemaContent, settings); // Can read immediately within the same session - var buffer = new ArrayBufferWriter(); - var found = await archive.TryGetSourceSchemaAsync(schemaName, buffer); + var found = await archive.TryGetSourceSchemaConfigurationAsync(schemaName); + + Assert.NotNull(found); - Assert.True(found); - Assert.True(schemaContent.AsSpan().SequenceEqual(buffer.WrittenSpan)); + using var streamReader = new StreamReader(await found.OpenReadSchemaAsync()); + var retrievedSchema = await streamReader.ReadToEndAsync(); + Assert.Equal(Encoding.UTF8.GetString(schemaContent), retrievedSchema); } [Fact] @@ -336,7 +317,10 @@ public async Task SetSourceSchema_WithInvalidSchemaName_ThrowsArgumentException( await archive.SetArchiveMetadataAsync(metadata); await Assert.ThrowsAsync( - () => archive.SetSourceSchemaAsync("invalid name!", "schema"u8.ToArray())); + () => archive.SetSourceSchemaConfigurationAsync( + "invalid name!", + "schema"u8.ToArray(), + CreateSettingsJson())); } [Fact] @@ -355,7 +339,10 @@ public async Task SetSourceSchema_WithUndeclaredSchemaName_ThrowsInvalidOperatio await archive.SetArchiveMetadataAsync(metadata); await Assert.ThrowsAsync( - () => archive.SetSourceSchemaAsync("undeclared-schema", "schema"u8.ToArray())); + () => archive.SetSourceSchemaConfigurationAsync( + "undeclared-schema", + "schema"u8.ToArray(), + CreateSettingsJson())); } [Fact] @@ -366,9 +353,8 @@ public async Task TryGetSourceSchema_WithNonExistentSchema_ReturnsFalse() // Act & Assert using var archive = FusionArchive.Create(stream, leaveOpen: true); - var buffer = new ArrayBufferWriter(); - var found = await archive.TryGetSourceSchemaAsync("non-existent", buffer); - Assert.False(found); + var found = await archive.TryGetSourceSchemaConfigurationAsync("non-existent"); + Assert.Null(found); } [Fact] @@ -382,7 +368,7 @@ public async Task SignArchive_WithValidCertificate_CreatesSignature() using var archive = FusionArchive.Create(stream, leaveOpen: true); var metadata = CreateTestMetadata(); await archive.SetArchiveMetadataAsync(metadata); - await archive.SetGatewaySchemaAsync("schema", new Version("2.0.0")); + await archive.SetGatewayConfigurationAsync("schema", CreateSettingsJson(), new Version("2.0.0")); await archive.SignArchiveAsync(cert); // Can verify immediately within the same session @@ -431,7 +417,7 @@ public async Task VerifySignature_WithValidSignature_ReturnsValid() { var metadata = CreateTestMetadata(); await archive.SetArchiveMetadataAsync(metadata); - await archive.SetGatewaySchemaAsync("schema", new Version("2.0.0")); + await archive.SetGatewayConfigurationAsync("schema", CreateSettingsJson(), new Version("2.0.0")); // Sign with private key await archive.SignArchiveAsync(cert); @@ -471,7 +457,7 @@ public async Task CommitAndReopen_PersistsChanges() using (var archive = FusionArchive.Create(stream, leaveOpen: true)) { await archive.SetArchiveMetadataAsync(metadata); - await archive.SetGatewaySchemaAsync(schema, new Version("2.0.0")); + await archive.SetGatewayConfigurationAsync(schema, CreateSettingsJson(), new Version("2.0.0")); await archive.CommitAsync(); } @@ -485,12 +471,14 @@ public async Task CommitAndReopen_PersistsChanges() metadata.SupportedGatewayFormats.ToArray(), retrievedMetadata.SupportedGatewayFormats.ToArray()); - var buffer = new ArrayBufferWriter(); - var result = await readArchive.TryGetGatewaySchemaAsync(new Version("2.0.0"), buffer); - Assert.True(result.IsResolved); + var result = await readArchive.TryGetGatewayConfigurationAsync(new Version("2.0.0")); + Assert.NotNull(result); - var retrievedSchema = Encoding.UTF8.GetString(buffer.WrittenSpan); + using var streamReader = new StreamReader(await result.OpenReadSchemaAsync()); + var retrievedSchema = await streamReader.ReadToEndAsync(); Assert.Equal(schema, retrievedSchema); + + result.Dispose(); } } @@ -509,7 +497,7 @@ public async Task UpdateMode_CanModifyExistingArchive() using (var archive = FusionArchive.Create(stream, leaveOpen: true)) { await archive.SetArchiveMetadataAsync(metadata); - await archive.SetGatewaySchemaAsync("original schema", new Version("2.0.0")); + await archive.SetGatewayConfigurationAsync("original schema", CreateSettingsJson(), new Version("2.0.0")); await archive.CommitAsync(); } @@ -517,7 +505,10 @@ public async Task UpdateMode_CanModifyExistingArchive() stream.Position = 0; using (var updateArchive = FusionArchive.Open(stream, FusionArchiveMode.Update, leaveOpen: true)) { - await updateArchive.SetGatewaySchemaAsync("modified schema", new Version("2.0.0")); + await updateArchive.SetGatewayConfigurationAsync( + "modified schema", + CreateSettingsJson(), + new Version("2.0.0")); await updateArchive.CommitAsync(); } @@ -525,12 +516,14 @@ public async Task UpdateMode_CanModifyExistingArchive() stream.Position = 0; using (var readArchive = FusionArchive.Open(stream, leaveOpen: true)) { - var buffer = new ArrayBufferWriter(); - var result = await readArchive.TryGetGatewaySchemaAsync(new Version("2.0.0"), buffer); - Assert.True(result.IsResolved); + var result = await readArchive.TryGetGatewayConfigurationAsync(new Version("2.0.0")); + Assert.NotNull(result); - var schema = Encoding.UTF8.GetString(buffer.WrittenSpan); - Assert.Equal("modified schema", schema); + using var streamReader = new StreamReader(await result.OpenReadSchemaAsync()); + var retrievedSchema = await streamReader.ReadToEndAsync(); + Assert.Equal("modified schema", retrievedSchema); + + result.Dispose(); } } @@ -546,16 +539,19 @@ public async Task OverwriteFile_WithinSession_ReplacesContent() await archive.SetArchiveMetadataAsync(metadata); // Set schema twice within the same session - await archive.SetGatewaySchemaAsync("first schema", new Version("2.0.0")); - await archive.SetGatewaySchemaAsync("second schema", new Version("2.0.0")); + await archive.SetGatewayConfigurationAsync("first schema", CreateSettingsJson(), new Version("2.0.0")); + await archive.SetGatewayConfigurationAsync("second schema", CreateSettingsJson(), new Version("2.0.0")); // Should get the last value - var buffer = new ArrayBufferWriter(); - var result = await archive.TryGetGatewaySchemaAsync(new Version("2.0.0"), buffer); + var result = await archive.TryGetGatewayConfigurationAsync(new Version("2.0.0")); + + Assert.NotNull(result); - Assert.True(result.IsResolved); - var schema = Encoding.UTF8.GetString(buffer.WrittenSpan); - Assert.Equal("second schema", schema); + using var streamReader = new StreamReader(await result.OpenReadSchemaAsync()); + var retrievedSchema = await streamReader.ReadToEndAsync(); + Assert.Equal("second schema", retrievedSchema); + + result.Dispose(); } [Fact] @@ -612,7 +608,7 @@ public async Task SetSourceSchema_WithValidSchemaNames_Succeeds(string schemaNam // Act & Assert - Should not throw using var archive = FusionArchive.Create(stream, leaveOpen: true); await archive.SetArchiveMetadataAsync(metadata); - await archive.SetSourceSchemaAsync(schemaName, "schema"u8.ToArray()); + await archive.SetSourceSchemaConfigurationAsync(schemaName, "schema"u8.ToArray(), CreateSettingsJson()); } [Theory] @@ -635,7 +631,10 @@ public async Task SetSourceSchema_WithInvalidSchemaNames_ThrowsException(string await archive.SetArchiveMetadataAsync(metadata); await Assert.ThrowsAsync( - () => archive.SetSourceSchemaAsync(schemaName, "schema"u8.ToArray())); + () => archive.SetSourceSchemaConfigurationAsync( + schemaName, + "schema"u8.ToArray(), + CreateSettingsJson())); } [Fact] @@ -678,6 +677,11 @@ private ArchiveMetadata CreateTestMetadata() }; } + private JsonDocument CreateSettingsJson() + { + return JsonDocument.Parse("{ }"); + } + private X509Certificate2 CreateTestCertificate() { using var rsa = RSA.Create(2048);