blob: ed4d2b2d033a84f6b77c3a361c447038d6850dc5 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
using OpenDAL.Interop.Result;
namespace OpenDAL;
/// <summary>
/// Read-only stream over an OpenDAL path.
/// </summary>
public sealed class OperatorInputStream : Stream
{
private IntPtr handle;
private bool disposed;
private bool abandoned;
// Current native chunk. Keeping the native handle instead of a managed
// copy lets Read copy each byte exactly once, straight into the caller's
// destination. Released once fully consumed, on EOF, or on dispose.
private OpenDALReadResult? currentChunk;
private nuint chunkOffset;
internal OperatorInputStream(IntPtr handle)
{
if (handle == IntPtr.Zero)
{
throw new ArgumentException("Native input stream handle must not be zero.", nameof(handle));
}
this.handle = handle;
}
public override bool CanRead => !disposed && !abandoned;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => throw new NotSupportedException();
public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
public override int Read(byte[] buffer, int offset, int count)
{
ArgumentNullException.ThrowIfNull(buffer);
return Read(buffer.AsSpan(offset, count));
}
public override int Read(Span<byte> destination)
{
ThrowIfDisposed();
var totalRead = 0;
while (destination.Length > 0)
{
if (currentChunk is null || chunkOffset >= currentChunk.Value.Buffer.Len)
{
ReleaseCurrentChunk();
if (!AcceptChunk(NativeMethods.operator_input_stream_read_next(handle)))
{
break;
}
}
var copied = CopyFromCurrentChunk(destination);
if (copied == 0)
{
break;
}
destination = destination[copied..];
totalRead += copied;
}
return totalRead;
}
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(buffer);
return ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask();
}
public override async ValueTask<int> ReadAsync(Memory<byte> destination, CancellationToken cancellationToken = default)
{
ThrowIfDisposed();
cancellationToken.ThrowIfCancellationRequested();
var totalRead = 0;
while (!destination.IsEmpty)
{
if (currentChunk is null || chunkOffset >= currentChunk.Value.Buffer.Len)
{
ReleaseCurrentChunk();
if (!AcceptChunk(await FetchNextChunkAsync(cancellationToken).ConfigureAwait(false)))
{
break;
}
}
var copied = CopyFromCurrentChunk(destination.Span);
if (copied == 0)
{
break;
}
destination = destination[copied..];
totalRead += copied;
}
return totalRead;
}
/// <summary>
/// Awaits the next chunk from the native async read path.
/// </summary>
/// <remarks>
/// On success the returned result still owns its native buffer; the caller
/// takes that ownership through <see cref="AcceptChunk"/>. When the token
/// fires first, the late native callback finds no awaiter and releases the
/// chunk itself, and the stream is poisoned because its position no
/// longer matches what the caller consumed.
/// </remarks>
private async ValueTask<OpenDALReadResult> FetchNextChunkAsync(CancellationToken cancellationToken)
{
var context = AsyncStateRegistry.Register<OpenDALReadResult>(out var state);
OpenDALResult submit;
unsafe
{
submit = NativeMethods.operator_input_stream_read_next_async(
handle, &Operator.OnReadResultRetained, context);
}
try
{
Operator.ThrowIfErrorAndRelease(submit);
}
catch
{
AsyncStateRegistry.Unregister(context);
throw;
}
state.BindCancellation(cancellationToken);
try
{
return await state.Completion.Task.ConfigureAwait(false);
}
catch
{
// The native position may already sit past a chunk this caller
// never received. Poison the stream instead of silently skipping
// those bytes on the next read.
abandoned = true;
throw;
}
}
/// <summary>
/// Takes ownership of a fetched chunk, releasing empty results.
/// </summary>
/// <returns><see langword="false"/> on EOF.</returns>
private bool AcceptChunk(OpenDALReadResult next)
{
var error = next.GetError();
if (error.IsError)
{
var exception = new OpenDALException(error);
next.Release();
throw exception;
}
if (next.Buffer.Handle == IntPtr.Zero || next.Buffer.Len == 0)
{
next.Release();
return false;
}
currentChunk = next;
chunkOffset = 0;
return true;
}
private int CopyFromCurrentChunk(Span<byte> destination)
{
var copied = currentChunk!.Value.Buffer.CopyTo(chunkOffset, destination);
chunkOffset += (nuint)copied;
return copied;
}
private void ReleaseCurrentChunk()
{
if (currentChunk is { } chunk)
{
chunk.Release();
currentChunk = null;
chunkOffset = 0;
}
}
public override void Flush()
{
}
public override Task FlushAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.CompletedTask;
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
private void ThrowIfDisposed()
{
ObjectDisposedException.ThrowIf(disposed || handle == IntPtr.Zero, this);
if (abandoned)
{
throw new InvalidOperationException(
"An abandoned read left the native stream position unknown; dispose this stream and open a new one.");
}
}
protected override void Dispose(bool disposing)
{
if (!disposed)
{
ReleaseCurrentChunk();
NativeMethods.operator_input_stream_free(handle);
handle = IntPtr.Zero;
disposed = true;
}
base.Dispose(disposing);
}
}