You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucenenet.apache.org by ni...@apache.org on 2025/10/01 06:13:55 UTC

(lucenenet) 02/09: Lucene.Net.Support.IO.StreamExtensions: Added Read(Span) and Read(Span, long) overloads to patch missing APIs on .NET Framework and .NET Standard. The latter uses the BCL RandomAccess class to synchronize (.NET Core only), which is a better match to Lucene than maintaining our own lock object per stream.

This is an automated email from the ASF dual-hosted git repository.

nightowl888 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/lucenenet.git

commit 189b4ea00a2452b5107bb368cf491d9a80719eff
Author: Shad Storhaug <sh...@shadstorhaug.com>
AuthorDate: Sun Sep 28 22:35:01 2025 +0700

    Lucene.Net.Support.IO.StreamExtensions: Added Read(Span<byte>) and Read(Span<byte>, long) overloads to patch missing APIs on .NET Framework and .NET Standard. The latter uses the BCL RandomAccess class to synchronize (.NET Core only), which is a better match to Lucene than maintaining our own lock object per stream.
---
 Directory.Build.targets                            |   1 +
 .../Support/IO/TestStreamExtensions.cs             |  29 ++++++
 src/Lucene.Net/Support/IO/StreamExtensions.cs      | 113 +++++++++++++++++++++
 3 files changed, 143 insertions(+)

diff --git a/Directory.Build.targets b/Directory.Build.targets
index d848684b4..900057ebc 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -54,6 +54,7 @@
   <PropertyGroup Condition=" $(TargetFramework.StartsWith('net6.')) Or $(TargetFramework.StartsWith('net7.')) Or $(TargetFramework.StartsWith('net8.')) Or $(TargetFramework.StartsWith('net9.')) ">
 
     <DefineConstants>$(DefineConstants);FEATURE_RANDOM_NEXTINT64_NEXTSINGLE</DefineConstants>
+    <DefineConstants>$(DefineConstants);FEATURE_RANDOMACCESS_READ</DefineConstants>
     <DefineConstants>$(DefineConstants);FEATURE_SPANFORMATTABLE</DefineConstants>
     <DefineConstants>$(DefineConstants);FEATURE_SUPPORTEDOSPLATFORMATTRIBUTE</DefineConstants>
 
diff --git a/src/Lucene.Net.Tests/Support/IO/TestStreamExtensions.cs b/src/Lucene.Net.Tests/Support/IO/TestStreamExtensions.cs
index 2d0b34a5a..481ac0fe7 100644
--- a/src/Lucene.Net.Tests/Support/IO/TestStreamExtensions.cs
+++ b/src/Lucene.Net.Tests/Support/IO/TestStreamExtensions.cs
@@ -55,6 +55,35 @@ public void TestRead()
             Assert.IsTrue(Encoding.UTF8.GetString(buffer.Array).Equals(fileString));
         }
 
+        [Test]
+        // LUCENENET note: adapted from test_read$BII() for ByteBuffer-based Read extension method
+        public void TestRead_Span_Int64()
+        {
+            FileInfo file = LuceneTestCase.CreateTempFile("TestStreamExtensions", ".tmp");
+            using FileStream fileStream = file.Open(FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite);
+            byte[] bytes = Encoding.UTF8.GetBytes(fileString);
+            fileStream.Write(bytes, 0, bytes.Length);
+            fileStream.Flush(flushToDisk: true);
+
+            Span<byte> buffer = stackalloc byte[Encoding.UTF8.GetByteCount(fileString)];
+            Assert.Greater(fileStream.Read(buffer, 0L), 0);
+            Assert.IsTrue(Encoding.UTF8.GetString(buffer).Equals(fileString));
+        }
+
+        [Test]
+        // LUCENENET note: adapted from test_read$BII() for ByteBuffer-based Read extension method
+        public void TestRead_Span()
+        {
+            byte[] bytes = Encoding.UTF8.GetBytes(fileString);
+            stream.Write(bytes, 0, bytes.Length);
+            // stream.Dispose(); // LUCENENET - we will reuse stream
+            ResetStreamForReading();
+
+            Span<byte> buffer = stackalloc byte[Encoding.UTF8.GetByteCount(fileString)];
+            Assert.Greater(stream.Read(buffer), 0);
+            Assert.IsTrue(Encoding.UTF8.GetString(buffer).Equals(fileString));
+        }
+
         [Test]
         public void TestWrite_Span()
         {
diff --git a/src/Lucene.Net/Support/IO/StreamExtensions.cs b/src/Lucene.Net/Support/IO/StreamExtensions.cs
index 862bdcc6c..7ab4562c1 100644
--- a/src/Lucene.Net/Support/IO/StreamExtensions.cs
+++ b/src/Lucene.Net/Support/IO/StreamExtensions.cs
@@ -126,6 +126,113 @@ public static int Read(this Stream stream, ByteBuffer destination, long position
             return read;
         }
 
+        /// <summary>
+        /// Reads a sequence of bytes from a <see cref="Stream"/> to the given <see cref="Span{Byte}"/>,
+        /// starting at the given <paramref name="position"/>. Prior to .NET Core, the <paramref name="stream"/>
+        /// must be both seekable and readable.
+        /// </summary>
+        /// <param name="stream">The stream to read.</param>
+        /// <param name="destination">The span to write to.</param>
+        /// <param name="position">The file position at which the transfer is to begin; must be non-negative.</param>
+        /// <returns>The number of bytes read, possibly zero.</returns>
+        /// <exception cref="ArgumentNullException"><paramref name="stream"/> is <c>null</c></exception>
+        /// <exception cref="NotSupportedException">
+        /// <paramref name="stream"/> is not readable.
+        /// <para/>
+        /// -or-
+        /// <para/>
+        /// <paramref name="stream"/> is not seekable.
+        /// </exception>
+        /// <exception cref="ArgumentOutOfRangeException">
+        /// <paramref name="position"/> is less than 0.
+        /// <para/>
+        /// -or-
+        /// <para/>
+        /// <paramref name="position"/> is greater than the <see cref="Stream.Length"/> of the stream.
+        /// </exception>
+        /// <exception cref="IOException">An I/O error occurs.</exception>
+        /// <exception cref="ObjectDisposedException"><paramref name="stream"/> has already been disposed.</exception>
+        /// <remarks>
+        /// On .NET 6+, this method uses the RandomAccess class to synchronize, so it is completely threadsafe
+        /// and does not require the stream to be seekable or readable.
+        /// <para/>
+        /// On older target frameworks, this method is atomic when used by itself, but does not synchronize with
+        /// the rest of the stream methods.
+        /// </remarks>
+        public static int Read(this FileStream stream, Span<byte> destination, long position)
+        {
+            if (stream is null)
+                throw new ArgumentNullException(nameof(stream));
+            if (position < 0)
+                throw new ArgumentOutOfRangeException(nameof(position));
+            if (position > stream.Length)
+                return 0;
+
+#if FEATURE_RANDOMACCESS_READ
+            return RandomAccess.Read(stream.SafeFileHandle, destination, position);
+#else
+            if (!stream.CanSeek)
+                throw new NotSupportedException("Stream does not support seeking.");
+            if (!stream.CanRead)
+                throw new NotSupportedException("Stream does not support reading.");
+
+            int read;
+            object readLock = lockCache.GetOrCreateValue(stream);
+            UninterruptableMonitor.Enter(readLock);
+            try
+            {
+                long originalPosition = stream.Position;
+                stream.Seek(position, SeekOrigin.Begin);
+
+                read = stream.Read(destination);
+
+                // Per Java's FileChannel.Read(), we don't want to alter the position
+                // of the stream, so we return it as it was originally.
+                stream.Seek(originalPosition, SeekOrigin.Begin);
+            }
+            finally
+            {
+                UninterruptableMonitor.Exit(readLock);
+            }
+
+            return read;
+#endif
+        }
+
+        /// <summary>
+        /// Reads a sequence of bytes from the current stream and advances
+        /// the position within the stream by the number of bytes read.
+        /// </summary>
+        /// <param name="stream"></param>
+        /// <param name="buffer">A region of memory. When this method returns,
+        /// the contents of this region are replaced by the bytes read from
+        /// the current source.</param>
+        /// <returns>The total number of bytes read into the buffer. This can be
+        /// less than the size of the buffer if that many bytes are not currently
+        /// available, or zero (0) if the buffer's length is zero or the end of
+        /// the stream has been reached.</returns>
+        /// <exception cref="IOException">An I/O error occurs.</exception>
+        /// <remarks>This is to patch .NET Standard and .NET Framework.</remarks>
+        public static int Read(this Stream stream, Span<byte> buffer)
+        {
+            byte[] sharedBuffer = ArrayPool<byte>.Shared.Rent(buffer.Length);
+            try
+            {
+                int numRead = stream.Read(sharedBuffer, 0, buffer.Length);
+                if ((uint)numRead > (uint)buffer.Length)
+                {
+                    throw new IOException(SR.IO_StreamTooLong);
+                }
+
+                new ReadOnlySpan<byte>(sharedBuffer, 0, numRead).CopyTo(buffer);
+                return numRead;
+            }
+            finally
+            {
+                ArrayPool<byte>.Shared.Return(sharedBuffer);
+            }
+        }
+
         /// <summary>
         /// Writes a sequence of bytes to the current stream and advances the current
         /// position within this stream by the number of bytes written.
@@ -411,5 +518,11 @@ private static int WriteUTFBytesToBuffer(string value, long count, byte[] buffer
             }
             return offset;
         }
+
+        private static class SR
+        {
+            public const string IO_StreamTooLong = "Stream was too long.";
+        }
+
     }
 }