diff --git a/src/Lucene.Net.Misc/Lucene.Net.Misc.csproj b/src/Lucene.Net.Misc/Lucene.Net.Misc.csproj
index 688a05ae46..41794edb7c 100644
--- a/src/Lucene.Net.Misc/Lucene.Net.Misc.csproj
+++ b/src/Lucene.Net.Misc/Lucene.Net.Misc.csproj
@@ -36,14 +36,9 @@
$(PackageTags);miscellaneous
bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml
$(NoWarn);1591;1573
+ true
-
-
-
-
-
-
diff --git a/src/Lucene.Net.Misc/Store/NativePosixUtil.cs b/src/Lucene.Net.Misc/Store/NativePosixUtil.cs
index a1d0a8ed86..272f6ed808 100644
--- a/src/Lucene.Net.Misc/Store/NativePosixUtil.cs
+++ b/src/Lucene.Net.Misc/Store/NativePosixUtil.cs
@@ -1,8 +1,15 @@
+using Lucene.Net.Util;
+using Microsoft.Win32.SafeHandles;
using System;
+using System.ComponentModel;
+using System.IO;
+using System.Runtime.InteropServices;
+#if FEATURE_SUPPORTEDOSPLATFORMATTRIBUTE
+using System.Runtime.Versioning;
+#endif
-namespace org.apache.lucene.store
+namespace Lucene.Net.Store
{
-
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
@@ -20,45 +27,227 @@ namespace org.apache.lucene.store
* limitations under the License.
*/
- ignore
-
///
- /// Provides JNI access to native methods such as madvise() for
- ///
+ /// Provides access to native POSIX methods such as madvise() for
+ /// .
+ ///
+ /// LUCENENET specific: the original Lucene implementation called these through a JNI
+ /// shim compiled from NativePosixUtil.cpp. This port replaces that native build
+ /// step with direct P/Invoke into libc, so it can only be used on Unix-like
+ /// platforms (Linux and macOS); it is not supported on Microsoft Windows.
///
- public final class NativePosixUtil
+#if FEATURE_SUPPORTEDOSPLATFORMATTRIBUTE
+ [UnsupportedOSPlatform("windows")]
+#endif
+ public static class NativePosixUtil
{
- public final static int NORMAL = 0;
- public final static int SEQUENTIAL = 1;
- public final static int RANDOM = 2;
- public final static int WILLNEED = 3;
- public final static int DONTNEED = 4;
- public final static int NOREUSE = 5;
-
-//JAVA TO C# CONVERTER NOTE: This static initializer block is converted to a static constructor, but there is no current class:
- static ImpliedClass()
- {
-//JAVA TO C# CONVERTER TODO TASK: The library is specified in the 'DllImport' attribute for .NET:
-// System.loadLibrary("NativePosixUtil");
- }
-
- private static native int posix_fadvise(FileDescriptor fd, long offset, long len, int advise) throws IOException;
- public static native int posix_madvise(ByteBuffer buf, int advise) throws IOException;
- public static native int madvise(ByteBuffer buf, int advise) throws IOException;
- public static native FileDescriptor open_direct(string filename, bool read) throws IOException;
- public static native long pread(FileDescriptor fd, long pos, ByteBuffer byteBuf) throws IOException;
-
- public static void advise(FileDescriptor fd, long offset, long len, int advise) throws IOException
- {
-//JAVA TO C# CONVERTER WARNING: The original Java variable was marked 'final':
-//ORIGINAL LINE: final int code = posix_fadvise(fd, offset, len, advise);
- int code = posix_fadvise(fd, offset, len, advise);
- if (code != 0)
+ // These constants mirror the Java NativePosixUtil ordering. Note this is NOT the same
+ // ordering as the OS POSIX_FADV_*/POSIX_MADV_* values (SEQUENTIAL/RANDOM are swapped);
+ // MapAdvice() translates to the OS values.
+ public const int NORMAL = 0;
+ public const int SEQUENTIAL = 1;
+ public const int RANDOM = 2;
+ public const int WILLNEED = 3;
+ public const int DONTNEED = 4;
+ public const int NOREUSE = 5;
+
+ ///
+ /// Opens a file for direct (un-cached) I/O.
+ ///
+ /// On Linux this uses O_DIRECT | O_NOATIME; on macOS it opens normally and then
+ /// applies fcntl(F_NOCACHE), matching the original native implementation.
+ ///
+ /// the file to open
+ /// true to open read-only; false to open read-write (creating if needed)
+ /// a wrapping the open file descriptor
+ /// If the file could not be opened
+ /// If running on Microsoft Windows
+ public static SafeFileHandle OpenDirect(string filename, bool read)
{
- throw new Exception("posix_fadvise failed code=" + code);
+ EnsureUnix();
+
+ // Upstream's C++ called open(fname, O_RDWR | O_CREAT | DIRECT_FLAG, 0666) for the write path.
+ // We cannot create-with-mode through P/Invoke: open() is variadic (int open(const char*, int, ...))
+ // and the mode argument rides the varargs ABI. On platforms where varargs are not passed in the
+ // same registers as fixed args (notably macOS, including Apple Silicon arm64), a fixed-signature
+ // P/Invoke delivers garbage for mode, producing a file without owner-read permission, so the
+ // subsequent O_RDONLY open of the same file fails with EACCES. To stay correct and ABI-agnostic
+ // we never pass mode: the file is created up front by the BCL (which applies the normal mode and
+ // umask), and we then open it with the plain two-argument open() - no O_CREAT, no varargs.
+ if (!read)
+ {
+ // mirrors O_CREAT: ensure the file exists with a sane mode before opening it
+ using (System.IO.File.Open(filename, System.IO.FileMode.OpenOrCreate, System.IO.FileAccess.ReadWrite)) { }
+ }
+
+ int fd;
+ if (Constants.MAC_OS_X)
+ {
+ fd = read
+ ? NativeMethods.open(filename, NativeMethods.O_RDONLY)
+ : NativeMethods.open(filename, NativeMethods.O_RDWR);
+ if (fd < 0)
+ {
+ throw NewIOException("open", filename);
+ }
+
+ // macOS has no O_DIRECT; disable the page cache for this descriptor instead.
+ if (NativeMethods.fcntl(fd, NativeMethods.F_NOCACHE, 1) < 0)
+ {
+ int err = Marshal.GetLastWin32Error();
+ NativeMethods.close(fd);
+ throw NewIOException("fcntl(F_NOCACHE)", filename, err);
+ }
+ }
+ else // Linux (and other Unixes, best-effort)
+ {
+ fd = read
+ ? NativeMethods.open(filename, NativeMethods.O_RDONLY | NativeMethods.O_DIRECT | NativeMethods.O_NOATIME)
+ : NativeMethods.open(filename, NativeMethods.O_RDWR | NativeMethods.O_DIRECT | NativeMethods.O_NOATIME);
+ if (fd < 0)
+ {
+ throw NewIOException("open", filename);
+ }
+ }
+
+ return new SafeFileHandle((IntPtr)fd, ownsHandle: true);
}
- }
- }
+ ///
+ /// Positioned read of up to bytes from at
+ /// absolute offset into the native buffer at ,
+ /// without changing the file's current offset.
+ ///
+ /// the number of bytes read (may be less than near EOF)
+ /// If the read failed
+ public static long Pread(SafeFileHandle fd, long pos, IntPtr buffer, int length)
+ {
+ EnsureUnix();
+ long n = (long)NativeMethods.pread(Fd(fd), buffer, (nuint)length, pos);
+ if (n < 0)
+ {
+ throw NewIOException("pread", null);
+ }
+ return n;
+ }
+
+ ///
+ /// Issues a posix_madvise() hint over the native buffer at .
+ public static int PosixMAdvise(IntPtr buffer, int length, int advise)
+ {
+ EnsureUnix();
+ return NativeMethods.posix_madvise(buffer, (nuint)length, MapAdvice(advise));
+ }
+
+ ///
+ /// Issues a madvise() hint over the native buffer at .
+ public static int MAdvise(IntPtr buffer, int length, int advise)
+ {
+ EnsureUnix();
+ return NativeMethods.madvise(buffer, (nuint)length, MapAdvice(advise));
+ }
+
+ ///
+ /// Issues a posix_fadvise() hint for the given range of ,
+ /// throwing if the call reports a non-zero error code.
+ ///
+ /// If posix_fadvise returned a non-zero code
+ public static void Advise(SafeFileHandle fd, long offset, long len, int advise)
+ {
+ EnsureUnix();
+ int code = NativeMethods.posix_fadvise(Fd(fd), offset, len, MapAdvice(advise));
+ if (code != 0)
+ {
+ // LUCENENET: upstream throws RuntimeException; we use IOException as this is an I/O failure.
+ // posix_fadvise returns the error number directly (it does not set errno); surface it as
+ // the HResult to match the errno-as-HResult convention used elsewhere here.
+ throw new IOException("posix_fadvise failed code=" + code, code);
+ }
+ }
+
+ ///
+ /// Translates the Java-style advice ordinal (see the public constants) to the OS
+ /// POSIX_FADV_*/POSIX_MADV_* value. Only SEQUENTIAL and RANDOM
+ /// differ in ordering between the two.
+ ///
+ private static int MapAdvice(int advise)
+ {
+ switch (advise)
+ {
+ case SEQUENTIAL: return 2; // POSIX_*_SEQUENTIAL
+ case RANDOM: return 1; // POSIX_*_RANDOM
+ default: return advise; // NORMAL=0, WILLNEED=3, DONTNEED=4, NOREUSE=5 are identical
+ }
+ }
+
+ private static int Fd(SafeFileHandle handle) => (int)handle.DangerousGetHandle();
+
+ private static IOException NewIOException(string operation, string filename)
+ => NewIOException(operation, filename, Marshal.GetLastWin32Error());
+
+ private static IOException NewIOException(string operation, string filename, int errno)
+ {
+ string where = filename is null ? operation : $"{operation} {filename}";
+ // On Unix with SetLastError, GetLastWin32Error() returns errno. Win32Exception's message
+ // is not meaningful on Unix, so include the raw errno for diagnosis.
+ // LUCENENET: surface the errno as the IOException's HResult. On Unix the BCL sets HResult to
+ // the raw errno (e.g. a FileStream share violation surfaces HResult == EWOULDBLOCK), and
+ // NativeFSLock inspects IOException.HResult, so we match that convention here.
+ return new IOException($"{where} failed (errno {errno})", errno);
+ }
+
+ internal static void EnsureUnix()
+ {
+ if (Constants.WINDOWS)
+ {
+ throw new PlatformNotSupportedException(
+ $"{nameof(NativePosixUtil)} requires Linux or macOS direct I/O and is not supported on Microsoft Windows.");
+ }
+ }
+
+ ///
+ /// P/Invoke declarations for the libc functions used here. These replace the
+ /// JNI/C++ native methods of the original implementation.
+ ///
+ private static class NativeMethods
+ {
+ private const string LIBC = "libc";
+ // open() flags. O_DIRECT/O_NOATIME are Linux-only. We never pass O_CREAT/mode: see OpenDirect.
+ internal const int O_RDONLY = 0x0;
+ internal const int O_RDWR = 0x2;
+
+ // O_DIRECT is architecture-dependent on Linux: most arches (x86/x86-64) use the asm-generic
+ // value 0x4000, but Arm/Arm64 (and a few others) swap it with O_DIRECTORY and use 0x10000.
+ // Using the wrong value silently means "O_DIRECTORY", which makes open() of a regular file
+ // fail with ENOTDIR. O_NOATIME (0x40000) is the same across these arches.
+ internal static readonly int O_DIRECT =
+ RuntimeInformation.OSArchitecture is Architecture.Arm or Architecture.Arm64
+ ? 0x10000
+ : 0x4000;
+ internal const int O_NOATIME = 0x40000; // Linux
+ internal const int F_NOCACHE = 48; // macOS
+
+ [DllImport(LIBC, SetLastError = true, EntryPoint = "open")]
+ internal static extern int open([MarshalAs(UnmanagedType.LPStr)] string pathname, int flags);
+
+ [DllImport(LIBC, SetLastError = true)]
+ internal static extern int close(int fd);
+
+ [DllImport(LIBC, SetLastError = true)]
+ internal static extern int fcntl(int fd, int cmd, int arg);
+
+ [DllImport(LIBC, SetLastError = true)]
+ internal static extern nint pread(int fd, IntPtr buf, nuint count, long offset);
+
+ [DllImport(LIBC, SetLastError = true)]
+ internal static extern int posix_fadvise(int fd, long offset, long len, int advice);
+
+ [DllImport(LIBC, SetLastError = true)]
+ internal static extern int posix_madvise(IntPtr addr, nuint length, int advice);
+
+ [DllImport(LIBC, SetLastError = true)]
+ internal static extern int madvise(IntPtr addr, nuint length, int advice);
+ }
+ }
}
diff --git a/src/Lucene.Net.Misc/Store/NativeUnixDirectory.cs b/src/Lucene.Net.Misc/Store/NativeUnixDirectory.cs
index 3cb38e6939..5d7c1f479a 100644
--- a/src/Lucene.Net.Misc/Store/NativeUnixDirectory.cs
+++ b/src/Lucene.Net.Misc/Store/NativeUnixDirectory.cs
@@ -1,32 +1,32 @@
+using Lucene.Net.Support;
+using Lucene.Net.Util;
+using Microsoft.Win32.SafeHandles;
using System;
-using System.Diagnostics;
+using System.IO;
+using System.Runtime.InteropServices;
+#if FEATURE_SUPPORTEDOSPLATFORMATTRIBUTE
+using System.Runtime.Versioning;
+#endif
-namespace org.apache.lucene.store
+namespace Lucene.Net.Store
{
-
/*
* 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
+ * 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
+ * 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.
+ * 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.
*/
- ignore
- import java.nio.channels.FileChannel;
-
- import org.apache.lucene.store.Directory; // javadoc
- import org.apache.lucene.store.IOContext.Context;
-
// TODO
// - newer Linux kernel versions (after 2.6.29) have
// improved MADV_SEQUENTIAL (and hopefully also
@@ -35,493 +35,606 @@ namespace org.apache.lucene.store
// IO when context is merge
///
- /// A implementation for all Unixes that uses
+ /// A implementation for all Unixes that uses
/// DIRECT I/O to bypass OS level IO caching during
/// merging. For all other cases (searching, writing) we delegate
- /// to the provided Directory instance.
+ /// to the provided instance.
///
- /// See Overview
- /// for more details.
+ /// LUCENENET specific: the original Lucene implementation reached the OS through a JNI
+ /// shim (NativePosixUtil.cpp) plus Java NIO FileChannels. This port replaces the
+ /// native build step with direct P/Invoke into libc (see ),
+ /// so no native compilation is required, but it can only be used on Unix-like platforms
+ /// (Linux and macOS); it is not supported on Microsoft Windows.
///
- ///
- /// To use this you must compile
- /// NativePosixUtil.cpp (exposes Linux-specific APIs through
- /// JNI) for your platform, by running ant
- /// build-native-unix, and then putting the resulting
- /// libNativePosixUtil.so (from
- /// lucene/build/native) onto your dynamic
- /// linker search path.
- ///
- ///
/// WARNING: this code is very new and quite easily
/// could contain horrible bugs. For example, here's one
- /// known issue: if you use seek in IndexOutput, and then
+ /// known issue: if you use seek in , and then
/// write more than one buffer's worth of bytes, then the
/// file will be wrong. Lucene does not do this today (only writes
- /// small number of bytes after seek), but that may change.
+ /// small number of bytes after seek), but that may change.
///
- ///
- /// This directory passes Solr and Lucene tests on Linux
- /// and OS X; other Unixes should work but have not been
- /// tested! Use at your own risk.
+ /// Direct I/O requires that reads and writes are aligned to the device block size
+ /// (here 512 bytes) and that the underlying filesystem supports it; some filesystems
+ /// (e.g. tmpfs) and overlay/virtual mounts do not.
///
/// @lucene.experimental
- ///
///
- public class NativeUnixDirectory extends FSDirectory
+#if FEATURE_SUPPORTEDOSPLATFORMATTRIBUTE
+ [UnsupportedOSPlatform("windows")]
+#endif
+ public class NativeUnixDirectory : FSDirectory
{
-
- // TODO: this is OS dependent, but likely 512 is the LCD
- private final static long ALIGN = 512;
- private final static long ALIGN_NOT_MASK = ~(ALIGN - 1);
-
- ///
- /// Default buffer size before writing to disk (256 KB);
- /// larger means less IO load but more RAM and direct
- /// buffer storage space consumed during merging.
- ///
-
- public final static int DEFAULT_MERGE_BUFFER_SIZE = 262144;
-
- ///
- /// Default min expected merge size before direct IO is
- /// used (10 MB):
- ///
- public final static long DEFAULT_MIN_BYTES_DIRECT = 10 * 1024 * 1024;
-
- private final int mergeBufferSize;
- private final long minBytesDirect;
- private final Directory @delegate;
-
- ///
- /// Create a new NIOFSDirectory for the named location.
- ///
- /// the path of the directory
- /// Size of buffer to use for
- /// merging. See .
- /// Merges, or files to be opened for
- /// reading, smaller than this will
- /// not use direct IO. See {@link
- /// #DEFAULT_MIN_BYTES_DIRECT}
- /// fallback Directory for non-merges
- /// If there is a low-level I/O error
- public NativeUnixDirectory(File path, int mergeBufferSize, long minBytesDirect, Directory @delegate) throws IOException
- {
- base(path, @delegate.LockFactory);
- if ((mergeBufferSize & ALIGN) != 0)
+ // TODO: this is OS dependent, but likely 512 is the LCD
+ private const long ALIGN = 512;
+ private const long ALIGN_NOT_MASK = ~(ALIGN - 1);
+
+ ///
+ /// Default buffer size before writing to disk (256 KB);
+ /// larger means less IO load but more RAM and direct
+ /// buffer storage space consumed during merging.
+ ///
+ public const int DEFAULT_MERGE_BUFFER_SIZE = 262144;
+
+ ///
+ /// Default min expected merge size before direct IO is
+ /// used (10 MB):
+ ///
+ public const long DEFAULT_MIN_BYTES_DIRECT = 10 * 1024 * 1024;
+
+ private readonly int mergeBufferSize;
+ private readonly long minBytesDirect;
+ private readonly Directory m_delegate;
+
+ ///
+ /// Create a new for the named location.
+ ///
+ /// the path of the directory
+ /// Size of buffer to use for
+ /// merging. See .
+ /// Merges, or files to be opened for
+ /// reading, smaller than this will
+ /// not use direct IO. See
+ /// fallback for non-merges
+ /// If there is a low-level I/O error
+ /// If running on Microsoft Windows
+ public NativeUnixDirectory(DirectoryInfo path, int mergeBufferSize, long minBytesDirect, Directory @delegate)
+ : base(path, @delegate.LockFactory)
{
- throw new System.ArgumentException("mergeBufferSize must be 0 mod " + ALIGN + " (got: " + mergeBufferSize + ")");
- }
- this.mergeBufferSize = mergeBufferSize;
- this.minBytesDirect = minBytesDirect;
- this.@delegate = @delegate;
- }
-
- ///
- /// Create a new NIOFSDirectory for the named location.
- ///
- /// the path of the directory
- /// fallback Directory for non-merges
- /// If there is a low-level I/O error
- public NativeUnixDirectory(File path, Directory @delegate) throws IOException
- {
- this(path, DEFAULT_MERGE_BUFFER_SIZE, DEFAULT_MIN_BYTES_DIRECT, @delegate);
- }
-
- public IndexInput openInput(string name, IOContext context) throws IOException
- {
- ensureOpen();
- if (context.context != Context.MERGE || context.mergeInfo.estimatedMergeBytes < minBytesDirect || fileLength(name) < minBytesDirect)
- {
- return @delegate.openInput(name, context);
+ EnsureUnix();
+ if ((mergeBufferSize & ALIGN) != 0)
+ {
+ throw new ArgumentException("mergeBufferSize must be 0 mod " + ALIGN + " (got: " + mergeBufferSize + ")");
+ }
+ this.mergeBufferSize = mergeBufferSize;
+ this.minBytesDirect = minBytesDirect;
+ this.m_delegate = @delegate;
}
- else
+
+ ///
+ /// Create a new for the named location.
+ ///
+ /// the path of the directory
+ /// fallback for non-merges
+ /// If there is a low-level I/O error
+ /// If running on Microsoft Windows
+ public NativeUnixDirectory(DirectoryInfo path, Directory @delegate)
+ : this(path, DEFAULT_MERGE_BUFFER_SIZE, DEFAULT_MIN_BYTES_DIRECT, @delegate)
{
- return new NativeUnixIndexInput(new File(Directory, name), mergeBufferSize);
}
- }
- public IndexOutput createOutput(string name, IOContext context) throws IOException
- {
- ensureOpen();
- if (context.context != Context.MERGE || context.mergeInfo.estimatedMergeBytes < minBytesDirect)
+ ///
+ /// Create a new for the named location.
+ ///
+ /// LUCENENET specific overload for convenience using string instead of .
+ ///
+ public NativeUnixDirectory(string path, int mergeBufferSize, long minBytesDirect, Directory @delegate)
+ : this(new DirectoryInfo(path), mergeBufferSize, minBytesDirect, @delegate)
{
- return @delegate.createOutput(name, context);
}
- else
+
+ ///
+ /// Create a new for the named location.
+ ///
+ /// LUCENENET specific overload for convenience using string instead of .
+ ///
+ public NativeUnixDirectory(string path, Directory @delegate)
+ : this(new DirectoryInfo(path), @delegate)
{
- ensureCanWrite(name);
- return new NativeUnixIndexOutput(new File(Directory, name), mergeBufferSize);
}
- }
-
- private final static class NativeUnixIndexOutput extends IndexOutput
- {
- private final ByteBuffer buffer;
- private final FileOutputStream fos;
- private final FileChannel channel;
- private final int bufferSize;
-
- //private final File path;
-
- private int bufferPos;
- private long filePos;
- private long fileLength;
- private bool isOpen;
- public NativeUnixIndexOutput(File path, int bufferSize) throws IOException
+ private static void EnsureUnix()
{
- //this.path = path;
-//JAVA TO C# CONVERTER WARNING: The original Java variable was marked 'final':
-//ORIGINAL LINE: final java.io.FileDescriptor fd = NativePosixUtil.open_direct(path.toString(), false);
- FileDescriptor fd = NativePosixUtil.open_direct(path.ToString(), false);
- fos = new FileOutputStream(fd);
- //fos = new FileOutputStream(path);
- channel = fos.Channel;
- buffer = ByteBuffer.allocateDirect(bufferSize);
- this.bufferSize = bufferSize;
- isOpen = true;
+ if (Constants.WINDOWS)
+ {
+ throw new PlatformNotSupportedException(
+ $"{nameof(NativeUnixDirectory)} requires Linux or macOS direct I/O and is not supported on Microsoft Windows.");
+ }
}
- public void writeByte(sbyte b) throws IOException
+ public override IndexInput OpenInput(string name, IOContext context)
{
- Debug.Assert(bufferPos == buffer.position(), "bufferPos=" + bufferPos + " vs buffer.position()=" + buffer.position());
- buffer.put(b);
- if (++bufferPos == bufferSize)
- {
- dump();
- }
+ EnsureOpen();
+ if (context.Context != IOContext.UsageContext.MERGE || context.MergeInfo.EstimatedMergeBytes < minBytesDirect || FileLength(name) < minBytesDirect)
+ {
+ return m_delegate.OpenInput(name, context);
+ }
+ else
+ {
+ return new NativeUnixIndexInput(Path.Combine(Directory.FullName, name), mergeBufferSize);
+ }
}
- public void writeBytes(sbyte[] src, int offset, int len) throws IOException
+ public override IndexOutput CreateOutput(string name, IOContext context)
{
- int toWrite = len;
- while (true)
- {
-//JAVA TO C# CONVERTER WARNING: The original Java variable was marked 'final':
-//ORIGINAL LINE: final int left = bufferSize - bufferPos;
- int left = bufferSize - bufferPos;
- if (left <= toWrite)
- {
- buffer.put(src, offset, left);
- toWrite -= left;
- offset += left;
- bufferPos = bufferSize;
- dump();
+ EnsureOpen();
+ if (context.Context != IOContext.UsageContext.MERGE || context.MergeInfo.EstimatedMergeBytes < minBytesDirect)
+ {
+ return m_delegate.CreateOutput(name, context);
}
else
{
- buffer.put(src, offset, toWrite);
- bufferPos += toWrite;
- break;
+ EnsureCanWrite(name);
+ return new NativeUnixIndexOutput(Path.Combine(Directory.FullName, name), mergeBufferSize);
}
- }
}
- //@Override
- //public void setLength() throws IOException {
- // TODO -- how to impl this? neither FOS nor
- // FileChannel provides an API?
- //}
+ // ----- libc helpers for the operations the original code performed via FileChannel -----
- public void flush()
- {
- // TODO -- I don't think this method is necessary?
- }
+ private static int Fd(SafeFileHandle handle) => (int)handle.DangerousGetHandle();
- private void dump() throws IOException
+ private static void PWrite(SafeFileHandle fd, IntPtr buffer, int length, long pos)
{
- buffer.flip();
-//JAVA TO C# CONVERTER WARNING: The original Java variable was marked 'final':
-//ORIGINAL LINE: final long limit = filePos + buffer.limit();
- long limit = filePos + buffer.limit();
- if (limit > fileLength)
- {
- // this dump extends the file
- fileLength = limit;
- }
- else
- {
- // we had seek'd back & wrote some changes
- }
-
- // must always round to next block
- buffer.limit((int)((buffer.limit() + ALIGN - 1) & ALIGN_NOT_MASK));
-
- assert(buffer.limit() & ALIGN_NOT_MASK) == buffer.limit() : "limit=" + buffer.limit() + " vs " + (buffer.limit() & ALIGN_NOT_MASK);
- assert(filePos & ALIGN_NOT_MASK) == filePos;
- //System.out.println(Thread.currentThread().getName() + ": dump to " + filePos + " limit=" + buffer.limit() + " fos=" + fos);
- channel.write(buffer, filePos);
- filePos += bufferPos;
- bufferPos = 0;
- buffer.clear();
- //System.out.println("dump: done");
-
- // TODO: the case where we'd seek'd back, wrote an
- // entire buffer, we must here read the next buffer;
- // likely Lucene won't trip on this since we only
- // write smallish amounts on seeking back
+ long written = (long)NativeMethods.pwrite(Fd(fd), buffer, (nuint)length, pos);
+ if (written < 0)
+ {
+ int errno = Marshal.GetLastWin32Error();
+ // LUCENENET: surface errno as the HResult (see NativePosixUtil.NewIOException) so callers
+ // such as NativeFSLock, which inspect IOException.HResult, still see the underlying error.
+ throw new IOException($"pwrite failed (errno {errno})", errno);
+ }
}
- public long FilePointer
+ private static void FTruncate(SafeFileHandle fd, long length)
{
- return filePos + bufferPos;
+ if (NativeMethods.ftruncate(Fd(fd), length) < 0)
+ {
+ int errno = Marshal.GetLastWin32Error();
+ // LUCENENET: surface errno as the HResult (see PWrite).
+ throw new IOException($"ftruncate failed (errno {errno})", errno);
+ }
}
- // TODO: seek is fragile at best; it can only properly
- // handle seek & then change bytes that fit entirely
- // within one buffer
- public void seek(long pos) throws IOException
+ private static long FileSize(SafeFileHandle fd)
{
- if (pos != FilePointer)
- {
- dump();
-//JAVA TO C# CONVERTER WARNING: The original Java variable was marked 'final':
-//ORIGINAL LINE: final long alignedPos = pos & ALIGN_NOT_MASK;
- long alignedPos = pos & ALIGN_NOT_MASK;
- filePos = alignedPos;
- int n = (int) NativePosixUtil.pread(fos.FD, filePos, buffer);
- if (n < bufferSize)
- {
- buffer.limit(n);
- }
- //System.out.println("seek refill=" + n);
-//JAVA TO C# CONVERTER WARNING: The original Java variable was marked 'final':
-//ORIGINAL LINE: final int delta = (int)(pos - alignedPos);
- int delta = (int)(pos - alignedPos);
- buffer.position(delta);
- bufferPos = delta;
- }
+ // The original used FileChannel.size(). All reads here are positioned (pread), so
+ // moving the file offset with lseek(SEEK_END) to learn the size is harmless.
+ long size = NativeMethods.lseek(Fd(fd), 0, NativeMethods.SEEK_END);
+ if (size < 0)
+ {
+ int errno = Marshal.GetLastWin32Error();
+ // LUCENENET: surface errno as the HResult (see PWrite).
+ throw new IOException($"lseek failed (errno {errno})", errno);
+ }
+ return size;
}
- public long length()
+ ///
+ /// A small native, block-aligned buffer that mimics the subset of Java's direct
+ /// ByteBuffer behavior used by the index input/output (position/limit, relative
+ /// get/put, flip/clear/rewind). Direct I/O requires the buffer to be aligned to the
+ /// device block size, which does not guarantee, so
+ /// we over-allocate and align manually.
+ ///
+ private sealed unsafe class AlignedByteBuffer : IDisposable
{
- return fileLength + bufferPos;
- }
+ private IntPtr basePtr;
+ private readonly byte* alignedPtr;
+ private readonly int capacity;
+ private int position;
+ private int limit;
- public long Checksum throws IOException
- {
- throw new System.NotSupportedException("this directory currently does not work at all!");
+ public AlignedByteBuffer(int capacity, int alignment)
+ {
+ this.capacity = capacity;
+ basePtr = Marshal.AllocHGlobal(capacity + alignment - 1);
+ long addr = basePtr.ToInt64();
+ long aligned = (addr + alignment - 1) & ~((long)alignment - 1);
+ alignedPtr = (byte*)aligned;
+ Clear();
+ }
+
+ ~AlignedByteBuffer()
+ {
+ // Clones are never disposed by Lucene, so free native memory as a backstop.
+ Free();
+ }
+
+ public int Capacity => capacity;
+ public int Position { get => position; set => position = value; }
+ public int Limit { get => limit; set => limit = value; }
+ public IntPtr Pointer => (IntPtr)alignedPtr;
+
+ public void Clear() { position = 0; limit = capacity; }
+ public void Flip() { limit = position; position = 0; }
+ public void Rewind() { position = 0; }
+
+ public void Put(byte b) => alignedPtr[position++] = b;
+ public byte Get() => alignedPtr[position++];
+
+ public void Put(ReadOnlySpan src)
+ {
+ src.CopyTo(new Span(alignedPtr + position, src.Length));
+ position += src.Length;
+ }
+
+ public void Get(Span dst)
+ {
+ new ReadOnlySpan(alignedPtr + position, dst.Length).CopyTo(dst);
+ position += dst.Length;
+ }
+
+ private void Free()
+ {
+ if (basePtr != IntPtr.Zero)
+ {
+ Marshal.FreeHGlobal(basePtr);
+ basePtr = IntPtr.Zero;
+ }
+ }
+
+ public void Dispose()
+ {
+ Free();
+ GC.SuppressFinalize(this);
+ }
}
- public void close() throws IOException
+ private sealed class NativeUnixIndexOutput : IndexOutput
{
- if (isOpen)
- {
- isOpen = false;
- try
- {
- dump();
- }
- finally
- {
- try
- {
- //System.out.println("direct close set len=" + fileLength + " vs " + channel.size() + " path=" + path);
- channel.truncate(fileLength);
- //System.out.println(" now: " + channel.size());
- }
- finally
- {
- try
+ private readonly AlignedByteBuffer buffer;
+ private readonly SafeFileHandle fd;
+ private readonly int bufferSize;
+ // LUCENENET specific: track a running CRC32 over the bytes written (in logical order), so
+ // Checksum can return it like FSIndexOutput does. Upstream left getChecksum() unsupported.
+ private readonly CRC32 crc = new CRC32();
+
+ private int bufferPos;
+ private long filePos;
+ private long fileLength;
+ private bool isOpen;
+
+ public NativeUnixIndexOutput(string path, int bufferSize)
+ {
+ fd = NativePosixUtil.OpenDirect(path, read: false);
+ buffer = new AlignedByteBuffer(bufferSize, (int)ALIGN);
+ this.bufferSize = bufferSize;
+ isOpen = true;
+ }
+
+ public override void WriteByte(byte b)
+ {
+ crc.Update(b);
+ buffer.Put(b);
+ if (++bufferPos == bufferSize)
{
- channel.close();
+ Dump();
}
- finally
+ }
+
+ public override void WriteBytes(ReadOnlySpan source)
+ {
+ crc.Update(source);
+ int offset = 0;
+ int toWrite = source.Length;
+ while (true)
{
- fos.close();
- //System.out.println(" final len=" + path.length());
+ int left = bufferSize - bufferPos;
+ if (left <= toWrite)
+ {
+ buffer.Put(source.Slice(offset, left));
+ toWrite -= left;
+ offset += left;
+ bufferPos = bufferSize;
+ Dump();
+ }
+ else
+ {
+ buffer.Put(source.Slice(offset, toWrite));
+ bufferPos += toWrite;
+ break;
+ }
}
- }
}
- }
- }
- }
- private final static class NativeUnixIndexInput extends IndexInput
- {
- private final ByteBuffer buffer;
- private final FileInputStream fis;
- private final FileChannel channel;
- private final int bufferSize;
+ public override void Flush()
+ {
+ // TODO -- I don't think this method is necessary?
+ }
- private bool isOpen;
- private bool isClone;
- private long filePos;
- private int bufferPos;
+ private void Dump()
+ {
+ buffer.Flip();
+ long limit = filePos + buffer.Limit;
+ if (limit > fileLength)
+ {
+ // this dump extends the file
+ fileLength = limit;
+ }
+ // else: we had seek'd back & wrote some changes
- public NativeUnixIndexInput(File path, int bufferSize) throws IOException
- {
- base("NativeUnixIndexInput(path=\"" + path.Path + "\")");
-//JAVA TO C# CONVERTER WARNING: The original Java variable was marked 'final':
-//ORIGINAL LINE: final java.io.FileDescriptor fd = NativePosixUtil.open_direct(path.toString(), true);
- FileDescriptor fd = NativePosixUtil.open_direct(path.ToString(), true);
- fis = new FileInputStream(fd);
- channel = fis.Channel;
- this.bufferSize = bufferSize;
- buffer = ByteBuffer.allocateDirect(bufferSize);
- isOpen = true;
- isClone = false;
- filePos = -bufferSize;
- bufferPos = bufferSize;
- //System.out.println("D open " + path + " this=" + this);
- }
+ // must always round to next block
+ buffer.Limit = (int)((buffer.Limit + ALIGN - 1) & ALIGN_NOT_MASK);
- // for clone
- public NativeUnixIndexInput(NativeUnixIndexInput other) throws IOException
- {
- base(other.ToString());
- this.fis = null;
- channel = other.channel;
- this.bufferSize = other.bufferSize;
- buffer = ByteBuffer.allocateDirect(bufferSize);
- filePos = -bufferSize;
- bufferPos = bufferSize;
- isOpen = true;
- isClone = true;
- //System.out.println("D clone this=" + this);
- seek(other.FilePointer);
- }
+ PWrite(fd, buffer.Pointer, buffer.Limit, filePos);
+ filePos += bufferPos;
+ bufferPos = 0;
+ buffer.Clear();
- public void close() throws IOException
- {
- if (isOpen && !isClone)
- {
- try
+ // TODO: the case where we'd seek'd back, wrote an
+ // entire buffer, we must here read the next buffer;
+ // likely Lucene won't trip on this since we only
+ // write smallish amounts on seeking back
+ }
+
+ public override long Position => filePos + bufferPos;
+
+ // TODO: seek is fragile at best; it can only properly
+ // handle seek & then change bytes that fit entirely
+ // within one buffer
+ [Obsolete("(4.1) this method will be removed in Lucene 5.0")]
+ public override void Seek(long pos)
{
- channel.close();
+ if (pos != Position)
+ {
+ Dump();
+ long alignedPos = pos & ALIGN_NOT_MASK;
+ filePos = alignedPos;
+ int n = (int)NativePosixUtil.Pread(fd, filePos, buffer.Pointer, bufferSize);
+ if (n < bufferSize)
+ {
+ buffer.Limit = n;
+ }
+ int delta = (int)(pos - alignedPos);
+ buffer.Position = delta;
+ bufferPos = delta;
+ }
}
- finally
+
+ public override long Length
{
- if (!isClone)
- {
- fis.close();
- }
+ get => fileLength + bufferPos;
+ set { /* not supported: length is managed internally */ }
}
- }
- }
- public long FilePointer
- {
- return filePos + bufferPos;
+ // LUCENENET specific: upstream left getChecksum() unsupported; we maintain a running CRC32
+ // over the written bytes (like FSIndexOutput) so the checksum footer can be written.
+ public override long Checksum => crc.Value;
+
+ protected override void Dispose(bool disposing)
+ {
+ if (isOpen)
+ {
+ isOpen = false;
+ try
+ {
+ Dump();
+ }
+ finally
+ {
+ try
+ {
+ FTruncate(fd, fileLength);
+ }
+ finally
+ {
+ fd.Dispose();
+ buffer.Dispose();
+ }
+ }
+ }
+ }
}
- public void seek(long pos) throws IOException
+ private sealed class NativeUnixIndexInput : IndexInput
{
- if (pos != FilePointer)
- {
-//JAVA TO C# CONVERTER WARNING: The original Java variable was marked 'final':
-//ORIGINAL LINE: final long alignedPos = pos & ALIGN_NOT_MASK;
- long alignedPos = pos & ALIGN_NOT_MASK;
- filePos = alignedPos - bufferSize;
-
-//JAVA TO C# CONVERTER WARNING: The original Java variable was marked 'final':
-//ORIGINAL LINE: final int delta = (int)(pos - alignedPos);
- int delta = (int)(pos - alignedPos);
- if (delta != 0)
- {
- refill();
- buffer.position(delta);
- bufferPos = delta;
+ private readonly AlignedByteBuffer buffer;
+ private readonly SafeFileHandle fd; // owned descriptor; null for clones
+ private readonly SafeFileHandle sharedFd; // descriptor used for reads (own, or the original's)
+ private readonly int bufferSize;
+
+ private bool isOpen;
+ private readonly bool isClone;
+ private long filePos;
+ private int bufferPos;
+
+ public NativeUnixIndexInput(string path, int bufferSize)
+ : base("NativeUnixIndexInput(path=\"" + path + "\")")
+ {
+ fd = NativePosixUtil.OpenDirect(path, read: true);
+ sharedFd = fd;
+ this.bufferSize = bufferSize;
+ buffer = new AlignedByteBuffer(bufferSize, (int)ALIGN);
+ isOpen = true;
+ isClone = false;
+ filePos = -bufferSize;
+ bufferPos = bufferSize;
}
- else
+
+ // for clone
+ private NativeUnixIndexInput(NativeUnixIndexInput other)
+ : base(other.ToString())
{
- // force refill on next read
- bufferPos = bufferSize;
+ fd = null;
+ sharedFd = other.sharedFd;
+ bufferSize = other.bufferSize;
+ buffer = new AlignedByteBuffer(bufferSize, (int)ALIGN);
+ filePos = -bufferSize;
+ bufferPos = bufferSize;
+ isOpen = true;
+ isClone = true;
+ Seek(other.Position);
}
- }
- }
- public long length()
- {
- try
- {
- return channel.size();
- }
- catch (IOException ioe)
- {
- throw new Exception("IOException during length(): " + this, ioe);
- }
- }
+ public override long Position => filePos + bufferPos;
- public sbyte readByte() throws IOException
- {
- // NOTE: we don't guard against EOF here... ie the
- // "final" buffer will typically be filled to less
- // than bufferSize
- if (bufferPos == bufferSize)
- {
- refill();
- }
- Debug.Assert(bufferPos == buffer.position(), "bufferPos=" + bufferPos + " vs buffer.position()=" + buffer.position());
- bufferPos++;
- return buffer.get();
- }
+ public override void Seek(long pos)
+ {
+ if (pos != Position)
+ {
+ long alignedPos = pos & ALIGN_NOT_MASK;
+ filePos = alignedPos - bufferSize;
+
+ int delta = (int)(pos - alignedPos);
+ if (delta != 0)
+ {
+ Refill();
+ buffer.Position = delta;
+ bufferPos = delta;
+ }
+ else
+ {
+ // force refill on next read
+ bufferPos = bufferSize;
+ }
+ }
+ }
- private void refill() throws IOException
- {
- buffer.clear();
- filePos += bufferSize;
- bufferPos = 0;
- assert(filePos & ALIGN_NOT_MASK) == filePos : "filePos=" + filePos + " anded=" + (filePos & ALIGN_NOT_MASK);
- //System.out.println("X refill filePos=" + filePos);
- int n;
- try
- {
- n = channel.read(buffer, filePos);
- }
- catch (IOException ioe)
- {
- throw new IOException(ioe.Message + ": " + this, ioe);
- }
- if (n < 0)
- {
- throw new EOFException("read past EOF: " + this);
- }
- buffer.rewind();
- }
+ public override long Length
+ {
+ get
+ {
+ try
+ {
+ return FileSize(sharedFd);
+ }
+ catch (Exception ioe) when (ioe.IsIOException())
+ {
+ // LUCENENET: upstream wraps in a RuntimeException here (IndexInput.Length cannot
+ // throw a checked IOException in Java). Preserve the original HResult so callers
+ // that inspect it (e.g. NativeFSLock) still see the underlying error. The HResult
+ // setter is only public on modern TFMs; on net462/netstandard2.0 it is protected,
+ // so the detail is retained in the message there instead.
+ Exception wrapped = RuntimeException.Create("IOException during Length: " + this, ioe);
+#if !(NETSTANDARD2_0 || NET462)
+ wrapped.HResult = ioe.HResult;
+#endif
+ throw wrapped;
+ }
+ }
+ }
- public void readBytes(sbyte[] dst, int offset, int len) throws IOException
- {
- int toRead = len;
- //System.out.println("\nX readBytes len=" + len + " fp=" + getFilePointer() + " size=" + length() + " this=" + this);
- while (true)
- {
-//JAVA TO C# CONVERTER WARNING: The original Java variable was marked 'final':
-//ORIGINAL LINE: final int left = bufferSize - bufferPos;
- int left = bufferSize - bufferPos;
- if (left < toRead)
- {
- //System.out.println(" copy " + left);
- buffer.get(dst, offset, left);
- toRead -= left;
- offset += left;
- refill();
+ public override byte ReadByte()
+ {
+ // NOTE: we don't guard against EOF here... ie the
+ // "final" buffer will typically be filled to less
+ // than bufferSize
+ if (bufferPos == bufferSize)
+ {
+ Refill();
+ }
+ bufferPos++;
+ return buffer.Get();
}
- else
+
+ private void Refill()
+ {
+ buffer.Clear();
+ filePos += bufferSize;
+ bufferPos = 0;
+ long n;
+ try
+ {
+ n = NativePosixUtil.Pread(sharedFd, filePos, buffer.Pointer, bufferSize);
+ }
+ catch (Exception ioe) when (ioe.IsIOException())
+ {
+ // LUCENENET: surface the original HResult (via the ctor that accepts it, which is
+ // public on all target frameworks) so callers such as NativeFSLock still see the
+ // underlying error. The inner detail is retained in the message.
+ throw new IOException(ioe.Message + ": " + this, ioe.HResult);
+ }
+ // Upstream used FileChannel.read(), which returns -1 at EOF. Here Pread() wraps libc
+ // pread(), which returns 0 at EOF (a genuine error already threw above), so a refill that
+ // reads nothing means we have stepped entirely past the end of the file.
+ if (n <= 0)
+ {
+ throw EOFException.Create("read past EOF: " + this);
+ }
+ buffer.Rewind();
+ }
+
+ public override void ReadBytes(Span destination)
{
- //System.out.println(" copy " + toRead);
- buffer.get(dst, offset, toRead);
- bufferPos += toRead;
- //System.out.println(" readBytes done");
- break;
+ int offset = 0;
+ int toRead = destination.Length;
+ while (true)
+ {
+ int left = bufferSize - bufferPos;
+ if (left < toRead)
+ {
+ buffer.Get(destination.Slice(offset, left));
+ toRead -= left;
+ offset += left;
+ Refill();
+ }
+ else
+ {
+ buffer.Get(destination.Slice(offset, toRead));
+ bufferPos += toRead;
+ break;
+ }
+ }
+ }
+
+ public override object Clone()
+ {
+ try
+ {
+ return new NativeUnixIndexInput(this);
+ }
+ catch (Exception ioe) when (ioe.IsIOException())
+ {
+ // LUCENENET: as in Length above, upstream wraps in a RuntimeException (Clone cannot
+ // throw a checked IOException in Java). Preserve the original HResult where the setter
+ // is public (modern TFMs); on net462/netstandard2.0 the detail stays in the message.
+ Exception wrapped = RuntimeException.Create("IOException during clone: " + this, ioe);
+#if !(NETSTANDARD2_0 || NET462)
+ wrapped.HResult = ioe.HResult;
+#endif
+ throw wrapped;
+ }
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (isOpen && !isClone)
+ {
+ isOpen = false;
+ fd.Dispose();
+ }
+ // each instance (including clones) owns its own native buffer
+ buffer.Dispose();
}
- }
}
- public NativeUnixIndexInput MemberwiseClone()
+ ///
+ /// P/Invoke declarations for the libc functions that the original implementation
+ /// reached through Java NIO FileChannels (positioned write, truncate, size).
+ ///
+ private static class NativeMethods
{
- try
- {
- return new NativeUnixIndexInput(this);
- }
- catch (IOException ioe)
- {
- throw new Exception("IOException during clone: " + this, ioe);
- }
+ private const string LIBC = "libc";
+
+ internal const int SEEK_END = 2;
+
+ [DllImport(LIBC, SetLastError = true)]
+ internal static extern nint pwrite(int fd, IntPtr buf, nuint count, long offset);
+
+ [DllImport(LIBC, SetLastError = true)]
+ internal static extern int ftruncate(int fd, long length);
+
+ [DllImport(LIBC, SetLastError = true)]
+ internal static extern long lseek(int fd, long offset, int whence);
}
- }
}
-
}
diff --git a/src/Lucene.Net.Misc/Store/WindowsDirectory.cs b/src/Lucene.Net.Misc/Store/WindowsDirectory.cs
index 60a8698c4e..adccc3173a 100644
--- a/src/Lucene.Net.Misc/Store/WindowsDirectory.cs
+++ b/src/Lucene.Net.Misc/Store/WindowsDirectory.cs
@@ -1,9 +1,16 @@
+using Lucene.Net.Util;
+using Microsoft.Win32.SafeHandles;
using System;
+using System.ComponentModel;
+using System.IO;
using System.Runtime.InteropServices;
+#if FEATURE_SUPPORTEDOSPLATFORMATTRIBUTE
+using System.Runtime.Versioning;
+#endif
+using System.Threading;
-namespace org.apache.lucene.store
+namespace Lucene.Net.Store
{
-
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with this
@@ -21,161 +28,305 @@ namespace org.apache.lucene.store
* the License.
*/
-
-
///
- /// Native implementation for Microsoft Windows.
- ///
- /// Steps:
- ///
- /// - Compile the source code to create WindowsDirectory.dll:
- ///
- /// c:\mingw\bin\g++ -Wall -D_JNI_IMPLEMENTATION_ -Wl,--kill-at
- /// -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -static-libgcc
- /// -static-libstdc++ -shared WindowsDirectory.cpp -o WindowsDirectory.dll
- ///
- /// For 64-bit JREs, use mingw64, with the -m64 option.
- /// - Put WindowsDirectory.dll into some directory in your windows PATH
- ///
- Open indexes with WindowsDirectory and use it.
- ///
- ///
+ /// Native implementation for Microsoft Windows.
+ ///
+ /// This uses CreateFile with the FILE_FLAG_RANDOM_ACCESS cache hint via
+ /// P/Invoke, so that the operating system can optimize its caching for the random-access
+ /// pattern that searching produces. Each read supplies its own file offset through the
+ /// OVERLAPPED structure; because the handle is synchronous (not opened with
+ /// FILE_FLAG_OVERLAPPED), these positioned reads are serialized by the OS on the
+ /// shared handle rather than running in parallel, but no explicit seek state is needed and
+ /// clones can read independently.
+ ///
+ /// NOTE: Unlike the original Lucene implementation, which
+ /// required compiling a native WindowsDirectory.dll with the JNI sources,
+ /// this implementation calls the Win32 APIs directly through P/Invoke. No native
+ /// build step is required, but it can only be used on Microsoft Windows.
+ ///
/// @lucene.experimental
///
+#if FEATURE_SUPPORTEDOSPLATFORMATTRIBUTE
+ [SupportedOSPlatform("windows")]
+#endif
public class WindowsDirectory : FSDirectory
{
- private const int DEFAULT_BUFFERSIZE = 4096; // default pgsize on ia32/amd64
-
- static WindowsDirectory()
- {
-//JAVA TO C# CONVERTER TODO TASK: The library is specified in the 'DllImport' attribute for .NET:
-// System.loadLibrary("WindowsDirectory");
- }
-
- ///
- /// Create a new WindowsDirectory for the named location.
- ///
- /// the path of the directory
- /// the lock factory to use, or null for the default
- /// ();
- /// If there is a low-level I/O error
-//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in .NET:
-//ORIGINAL LINE: public WindowsDirectory(java.io.File path, LockFactory lockFactory) throws java.io.IOException
- public WindowsDirectory(File path, LockFactory lockFactory) : base(path, lockFactory)
- {
- }
-
- ///
- /// Create a new WindowsDirectory for the named location and .
- ///
- /// the path of the directory
- /// If there is a low-level I/O error
-//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in .NET:
-//ORIGINAL LINE: public WindowsDirectory(java.io.File path) throws java.io.IOException
- public WindowsDirectory(File path) : base(path, null)
- {
- }
-
-//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in .NET:
-//ORIGINAL LINE: @Override public IndexInput openInput(String name, IOContext context) throws java.io.IOException
- public override IndexInput openInput(string name, IOContext context)
- {
- ensureOpen();
- return new WindowsIndexInput(new File(Directory, name), Math.Max(BufferedIndexInput.bufferSize(context), DEFAULT_BUFFERSIZE));
- }
-
- internal class WindowsIndexInput : BufferedIndexInput
- {
- internal readonly long fd;
- internal readonly long length_Renamed;
- internal bool isClone;
- internal bool isOpen;
-
-//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in .NET:
-//ORIGINAL LINE: public WindowsIndexInput(java.io.File file, int bufferSize) throws java.io.IOException
- public WindowsIndexInput(File file, int bufferSize) : base("WindowsIndexInput(path=\"" + file.Path + "\")", bufferSize)
+ private const int DEFAULT_BUFFERSIZE = 4096; // default pgsize on ia32/amd64
+
+ ///
+ /// Create a new for the named location.
+ ///
+ /// the path of the directory
+ /// the lock factory to use, or null for the default
+ /// ();
+ /// If there is a low-level I/O error
+ /// If not running on Microsoft Windows
+ public WindowsDirectory(DirectoryInfo path, LockFactory lockFactory)
+ : base(path, lockFactory)
{
- fd = WindowsDirectory.open(file.Path);
- length_Renamed = WindowsDirectory.length(fd);
- isOpen = true;
+ EnsureWindows();
}
-//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in .NET:
-//ORIGINAL LINE: @Override protected void readInternal(byte[] b, int offset, int length) throws java.io.IOException
- protected internal override void readInternal(sbyte[] b, int offset, int length)
+ ///
+ /// Create a new for the named location and .
+ ///
+ /// the path of the directory
+ /// If there is a low-level I/O error
+ /// If not running on Microsoft Windows
+ public WindowsDirectory(DirectoryInfo path)
+ : base(path, null)
{
- int bytesRead;
- try
- {
- bytesRead = WindowsDirectory.read(fd, b, offset, length, FilePointer);
- }
- catch (IOException ioe)
- {
- throw new IOException(ioe.Message + ": " + this, ioe);
- }
-
- if (bytesRead != length)
- {
- throw new EOFException("read past EOF: " + this);
- }
+ EnsureWindows();
}
-//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in .NET:
-//ORIGINAL LINE: @Override protected void seekInternal(long pos) throws java.io.IOException
- protected internal override void seekInternal(long pos)
+ ///
+ /// Create a new for the named location.
+ ///
+ /// LUCENENET specific overload for convenience using string instead of .
+ ///
+ /// the path of the directory
+ /// the lock factory to use, or null for the default
+ /// ();
+ /// If there is a low-level I/O error
+ /// If not running on Microsoft Windows
+ public WindowsDirectory(string path, LockFactory lockFactory)
+ : this(new DirectoryInfo(path), lockFactory)
{
}
-//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in .NET:
-//ORIGINAL LINE: @Override public synchronized void close() throws java.io.IOException
- public override void close()
+ ///
+ /// Create a new for the named location and .
+ ///
+ /// LUCENENET specific overload for convenience using string instead of .
+ ///
+ /// the path of the directory
+ /// If there is a low-level I/O error
+ /// If not running on Microsoft Windows
+ public WindowsDirectory(string path)
+ : this(path, null)
{
- lock (this)
+ }
+
+ private static void EnsureWindows()
+ {
+ if (!Constants.WINDOWS)
{
- // NOTE: we synchronize and track "isOpen" because Lucene sometimes closes IIs twice!
- if (!isClone && isOpen)
- {
- WindowsDirectory.close(fd);
- isOpen = false;
- }
+ throw new PlatformNotSupportedException($"{nameof(WindowsDirectory)} is only supported on Microsoft Windows.");
}
}
- public override long length()
+ public override IndexInput OpenInput(string name, IOContext context)
{
- return length_Renamed;
+ EnsureOpen();
+ var path = Path.Combine(Directory.FullName, name);
+ return new WindowsIndexInput(path, Math.Max(BufferedIndexInput.GetBufferSize(context), DEFAULT_BUFFERSIZE));
}
- public override WindowsIndexInput clone()
+ internal class WindowsIndexInput : BufferedIndexInput
{
- WindowsIndexInput clone = (WindowsIndexInput)base.clone();
- clone.isClone = true;
- return clone;
+ private readonly SafeFileHandle fd;
+ private readonly long length;
+ private bool isClone;
+ private bool isOpen;
+ private int disposed = 0; // LUCENENET specific - allow double-dispose
+
+ public WindowsIndexInput(string path, int bufferSize)
+ : base("WindowsIndexInput(path=\"" + path + "\")", bufferSize)
+ {
+ fd = WindowsDirectory.OpenFile(path);
+ try
+ {
+ length = WindowsDirectory.Length(fd);
+ }
+ catch
+ {
+ // LUCENENET specific: avoid leaking the handle if reading the length fails;
+ // the ctor throws so Dispose() will never be called on this instance.
+ fd.Dispose();
+ throw;
+ }
+ isOpen = true;
+ }
+
+ protected override void ReadInternal(Span b)
+ {
+ int bytesRead;
+ try
+ {
+ bytesRead = WindowsDirectory.Read(fd, b, Position); // LUCENENET: Position is the file pointer (renamed from getFilePointer())
+ }
+ catch (Exception ioe) when (ioe.IsIOException())
+ {
+ // LUCENENET: surface the original HResult (via the ctor that accepts it, which is
+ // public on all target frameworks) so callers such as NativeFSLock, which inspect
+ // IOException.HResult, still see the underlying error. The inner detail is retained
+ // in the message.
+ throw new IOException(ioe.Message + ": " + this, ioe.HResult);
+ }
+
+ if (bytesRead != b.Length)
+ {
+ throw EOFException.Create("read past EOF: " + this);
+ }
+ }
+
+ protected override void SeekInternal(long pos)
+ {
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ // NOTE: we track "isOpen" because Lucene sometimes closes IndexInputs twice!
+ if (0 != Interlocked.CompareExchange(ref this.disposed, 1, 0)) return; // LUCENENET specific - allow double-dispose
+
+ if (disposing && !isClone && isOpen)
+ {
+ fd.Dispose(); // closes the underlying Win32 handle (CloseHandle)
+ isOpen = false;
+ }
+ }
+
+ public override sealed long Length => length;
+
+ public override object Clone()
+ {
+ WindowsIndexInput clone = (WindowsIndexInput)base.Clone();
+ clone.isClone = true;
+ return clone;
+ }
}
- }
-
- ///
- /// Opens a handle to a file.
-//JAVA TO C# CONVERTER TODO TASK: Replace 'unknown' with the appropriate dll name:
- [DllImport("unknown")]
- private static extern long open(string filename);
-
- ///
- /// Reads data from a file at pos into bytes
-//JAVA TO C# CONVERTER TODO TASK: Replace 'unknown' with the appropriate dll name:
- [DllImport("unknown")]
- private static extern int read(long fd, sbyte[] bytes, int offset, int length, long pos);
-
- ///
- /// Closes a handle to a file
-//JAVA TO C# CONVERTER TODO TASK: Replace 'unknown' with the appropriate dll name:
- [DllImport("unknown")]
- private static extern void close(long fd);
-
- ///
- /// Returns the length of a file
-//JAVA TO C# CONVERTER TODO TASK: Replace 'unknown' with the appropriate dll name:
- [DllImport("unknown")]
- private static extern long length(long fd);
- }
+ ///
+ /// Opens a handle to a file.
+ private static SafeFileHandle OpenFile(string filename)
+ {
+ // LUCENENET specific: include FILE_SHARE_DELETE (upstream used only READ|WRITE) so that,
+ // like SimpleFSDirectory's read path (#1283), an open read handle does not block deletion
+ // of the underlying file on Windows.
+ IntPtr handle = NativeMethods.CreateFileW(
+ filename,
+ NativeMethods.GENERIC_READ,
+ NativeMethods.FILE_SHARE_READ | NativeMethods.FILE_SHARE_WRITE | NativeMethods.FILE_SHARE_DELETE,
+ IntPtr.Zero,
+ NativeMethods.OPEN_EXISTING,
+ NativeMethods.FILE_FLAG_RANDOM_ACCESS,
+ IntPtr.Zero);
+ int lastError = Marshal.GetLastWin32Error();
+ // LUCENENET: surface the Win32 error as the IOException's HResult (matching what the BCL
+ // sets on a FileStream IOException) so callers such as NativeFSLock can recognize it.
+ int hresult = Marshal.GetHRForLastWin32Error();
+
+ if (handle == NativeMethods.INVALID_HANDLE_VALUE)
+ {
+ throw new IOException("Could not open file " + filename + ": " + new Win32Exception(lastError).Message, hresult);
+ }
+
+ return new SafeFileHandle(handle, ownsHandle: true);
+ }
+
+ ///
+ /// Reads data from a file at into .
+ ///
+ /// The position is supplied via the OVERLAPPED structure. The handle is
+ /// synchronous (no FILE_FLAG_OVERLAPPED), so the read completes synchronously and
+ /// is serialized by the OS on the handle; the explicit offset is what lets clones share
+ /// one handle without seeking.
+ private static unsafe int Read(SafeFileHandle fd, Span bytes, long pos)
+ {
+ NativeOverlapped overlapped = default;
+ overlapped.OffsetLow = (int)(pos & 0xFFFFFFFFL);
+ overlapped.OffsetHigh = (int)((pos >> 0x20) & 0x7FFFFFFFL);
+
+ int numRead;
+ bool success;
+ fixed (byte* p = bytes)
+ {
+ success = NativeMethods.ReadFile(fd, p, bytes.Length, out numRead, &overlapped);
+ }
+
+ if (!success)
+ {
+ int lastError = Marshal.GetLastWin32Error();
+ // LUCENENET: surface the Win32 error as the IOException's HResult (see OpenFile).
+ throw new IOException(new Win32Exception(lastError).Message, Marshal.GetHRForLastWin32Error());
+ }
+
+ return numRead;
+ }
+
+ ///
+ /// Returns the length of a file.
+ private static long Length(SafeFileHandle fd)
+ {
+ if (!NativeMethods.GetFileInformationByHandle(fd, out NativeMethods.BY_HANDLE_FILE_INFORMATION info))
+ {
+ int lastError = Marshal.GetLastWin32Error();
+ // LUCENENET: surface the Win32 error as the IOException's HResult (see OpenFile).
+ throw new IOException(new Win32Exception(lastError).Message, Marshal.GetHRForLastWin32Error());
+ }
+
+ return ((long)info.nFileSizeHigh << 0x20) | info.nFileSizeLow;
+ }
+
+ ///
+ /// P/Invoke declarations for the Win32 APIs used by .
+ ///
+ /// These replace the JNI/C++ native methods (open, read, close,
+ /// length) in the original Lucene implementation. Closing is handled by
+ /// , which calls CloseHandle.
+ ///
+ private static class NativeMethods
+ {
+ internal const uint GENERIC_READ = 0x80000000;
+ internal const uint FILE_SHARE_READ = 0x00000001;
+ internal const uint FILE_SHARE_WRITE = 0x00000002;
+ internal const uint FILE_SHARE_DELETE = 0x00000004;
+ internal const uint OPEN_EXISTING = 3;
+ internal const uint FILE_FLAG_RANDOM_ACCESS = 0x10000000;
+
+ internal static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);
+
+ // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+ internal static extern IntPtr CreateFileW(
+ string lpFileName,
+ uint dwDesiredAccess,
+ uint dwShareMode,
+ IntPtr lpSecurityAttributes,
+ uint dwCreationDisposition,
+ uint dwFlagsAndAttributes,
+ IntPtr hTemplateFile);
+
+ // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-readfile
+ [DllImport("kernel32.dll", SetLastError = true)]
+ internal static extern unsafe bool ReadFile(
+ SafeFileHandle hFile,
+ byte* lpBuffer,
+ int nNumberOfBytesToRead,
+ out int lpNumberOfBytesRead,
+ NativeOverlapped* lpOverlapped);
+
+ // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfileinformationbyhandle
+ [DllImport("kernel32.dll", SetLastError = true)]
+ internal static extern bool GetFileInformationByHandle(
+ SafeFileHandle hFile,
+ out BY_HANDLE_FILE_INFORMATION lpFileInformation);
+
+ // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/ns-fileapi-by_handle_file_information
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct BY_HANDLE_FILE_INFORMATION
+ {
+ public uint dwFileAttributes;
+ public System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime;
+ public System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime;
+ public System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime;
+ public uint dwVolumeSerialNumber;
+ public uint nFileSizeHigh;
+ public uint nFileSizeLow;
+ public uint nNumberOfLinks;
+ public uint nFileIndexHigh;
+ public uint nFileIndexLow;
+ }
+ }
+ }
}
diff --git a/src/Lucene.Net.Tests.Misc/Support/Store/TestNativeUnixDirectory.cs b/src/Lucene.Net.Tests.Misc/Support/Store/TestNativeUnixDirectory.cs
new file mode 100644
index 0000000000..82aa3fcf94
--- /dev/null
+++ b/src/Lucene.Net.Tests.Misc/Support/Store/TestNativeUnixDirectory.cs
@@ -0,0 +1,334 @@
+using Lucene.Net.Attributes;
+using Lucene.Net.Util;
+using NUnit.Framework;
+using RandomizedTesting.Generators;
+using System;
+using System.Collections.Generic;
+using System.IO;
+#if FEATURE_SUPPORTEDOSPLATFORMATTRIBUTE
+using System.Runtime.Versioning;
+#endif
+using System.Threading;
+using Assert = Lucene.Net.TestFramework.Assert;
+
+namespace Lucene.Net.Store
+{
+ /*
+ * 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.
+ */
+
+ ///
+ /// Cross-platform tests for the platform guard on and
+ /// . This fixture runs on every platform; on Microsoft Windows it
+ /// verifies that the types refuse to operate (they require Linux/macOS direct I/O).
+ ///
+ /// LUCENENET specific: the original Lucene types had no test coverage (GH-1342).
+ ///
+ [TestFixture]
+ [LuceneNetSpecific]
+ public class TestNativeUnixDirectoryPlatformGuard : LuceneTestCase
+ {
+ [Test]
+ public virtual void TestDirectoryThrowsOnWindows()
+ {
+ DirectoryInfo path = CreateTempDir("nativeUnixGuard");
+#pragma warning disable CA1416 // Validate platform compatibility - intentionally exercising the guard on all platforms
+ using Directory @delegate = new RAMDirectory();
+ if (Constants.WINDOWS)
+ {
+ Assert.Throws(() => new NativeUnixDirectory(path, @delegate));
+ }
+ else
+ {
+ // On Unix the constructor must succeed (no direct files are opened until a merge-context
+ // CreateOutput/OpenInput); functional behavior is covered by TestRoundTrip below.
+ using Directory dir = new NativeUnixDirectory(path, @delegate);
+ Assert.IsNotNull(dir);
+ }
+#pragma warning restore CA1416 // Validate platform compatibility
+ }
+
+ [Test]
+ public virtual void TestOpenDirectThrowsOnWindows()
+ {
+ if (Constants.WINDOWS)
+ {
+#pragma warning disable CA1416 // Validate platform compatibility - intentionally exercising the guard
+ Assert.Throws(() => NativePosixUtil.OpenDirect("anything", read: true));
+#pragma warning restore CA1416 // Validate platform compatibility
+ }
+ else
+ {
+ AssumeTrue("OpenDirect's open path is exercised by the Unix functional tests.", false);
+ }
+ }
+ }
+
+ ///
+ /// Functional tests for 's direct-I/O read/write path. These
+ /// only run on Unix-like platforms and additionally require a filesystem that supports direct
+ /// I/O (O_DIRECT); they are skipped on Microsoft Windows.
+ ///
+ /// Unlike TestWindowsDirectory, this does not subclass :
+ /// only uses its direct-I/O input/output in a MERGE
+ /// for files larger than minBytesDirect (otherwise it delegates),
+ /// and the experimental direct-I/O input does not throw on read-past-EOF the way the generic
+ /// suite expects, so the randomized generic suite would be non-deterministic. These targeted
+ /// tests instead exercise the direct path in its designed usage (sequential write, full read,
+ /// clone, and concurrent clones).
+ ///
+ /// LUCENENET specific (GH-1342): the original Lucene type had no test coverage.
+ ///
+ [TestFixture]
+ [LuceneNetSpecific]
+#if FEATURE_SUPPORTEDOSPLATFORMATTRIBUTE
+ [UnsupportedOSPlatform("windows")]
+#endif
+ public class TestNativeUnixDirectory : LuceneTestCase
+ {
+ // A MERGE context with estimatedMergeBytes >= minBytesDirect(0) forces the direct path.
+ private static readonly IOContext MERGE_CONTEXT = new IOContext(new MergeInfo(1, 1024 * 1024, false, 1));
+
+ public override void SetUp()
+ {
+ base.SetUp();
+ AssumeTrue("NativeUnixDirectory requires Linux or macOS direct I/O.", !Constants.WINDOWS);
+ }
+
+ ///
+ /// Creates a (plus its delegate) configured so that the
+ /// direct-I/O path is always taken for MERGE-context I/O. The caller must dispose both the
+ /// returned directory and .
+ ///
+ private NativeUnixDirectory NewDirectDirectory(DirectoryInfo path, out Directory @delegate)
+ {
+ @delegate = new SimpleFSDirectory(path);
+ // mergeBufferSize must be a multiple of 512 (and, per the upstream check, have bit 9 clear).
+ return new NativeUnixDirectory(path, mergeBufferSize: 1024, minBytesDirect: 0, @delegate);
+ }
+
+ ///
+ /// Writes a file through the direct-I/O merge path and reads it back (in bulk), verifying the
+ /// bytes round-trip across many buffer dumps/refills, plus a clone read.
+ ///
+ [Test]
+ public virtual void TestRoundTrip()
+ {
+ DirectoryInfo path = CreateTempDir("nativeUnixRoundTrip");
+ using var dir = NewDirectDirectory(path, out Directory @delegate);
+ using (@delegate)
+ {
+ int len = TestUtil.NextInt32(Random, 2000, 20000);
+ byte[] expected = new byte[len];
+ Random.NextBytes(expected);
+
+ using (IndexOutput output = dir.CreateOutput("test", MERGE_CONTEXT))
+ {
+ output.WriteBytes(expected, expected.Length);
+ }
+
+ using IndexInput input = dir.OpenInput("test", MERGE_CONTEXT);
+ Assert.AreEqual(len, input.Length);
+ byte[] actual = new byte[len];
+ input.ReadBytes(actual, 0, actual.Length);
+ Assert.AreEqual(expected, actual);
+
+ // random positioned reads
+ for (int i = 0; i < 50; i++)
+ {
+ int pos = Random.Next(len);
+ int count = Math.Min(TestUtil.NextInt32(Random, 1, 500), len - pos);
+ input.Seek(pos);
+ byte[] buf = new byte[count];
+ input.ReadBytes(buf, 0, count);
+ for (int j = 0; j < count; j++)
+ {
+ Assert.AreEqual(expected[pos + j], buf[j], "mismatch at " + (pos + j));
+ }
+ }
+
+ // a clone reads independently from the same shared descriptor
+ input.Seek(0);
+ using IndexInput clone = (IndexInput)input.Clone();
+ int clonePos = Random.Next(len);
+ clone.Seek(clonePos);
+ Assert.AreEqual(expected[clonePos], clone.ReadByte());
+ }
+ }
+
+ ///
+ /// Disposing an input (and a clone) more than once must be a no-op; clones must not close the
+ /// shared descriptor. Mirrors the equivalent WindowsDirectory test.
+ ///
+ [Test]
+ public virtual void TestCloneDisposeDoesNotCloseSharedDescriptor()
+ {
+ DirectoryInfo path = CreateTempDir("nativeUnixDoubleDispose");
+ using var dir = NewDirectDirectory(path, out Directory @delegate);
+ using (@delegate)
+ {
+ byte[] data = new byte[2048];
+ Random.NextBytes(data);
+ IndexOutput output = dir.CreateOutput("data", MERGE_CONTEXT);
+ output.WriteBytes(data, data.Length);
+ Assert.DoesNotThrow(() => output.Dispose());
+ Assert.DoesNotThrow(() => output.Dispose()); // double dispose is a no-op
+
+ IndexInput input = dir.OpenInput("data", MERGE_CONTEXT);
+ IndexInput clone = (IndexInput)input.Clone();
+
+ // disposing the clone must not close the shared descriptor: the original still reads
+ Assert.DoesNotThrow(() => clone.Dispose());
+ Assert.DoesNotThrow(() => clone.Dispose());
+ input.Seek(0);
+ byte[] check = new byte[data.Length];
+ input.ReadBytes(check, 0, check.Length);
+ Assert.AreEqual(data, check);
+
+ Assert.DoesNotThrow(() => input.Dispose());
+ Assert.DoesNotThrow(() => input.Dispose()); // double dispose is a no-op
+ }
+ }
+
+ ///
+ /// Many threads reading random positions through independent clones of the same input (and
+ /// thus the same shared descriptor) must each get correct data. Positioned pread is
+ /// atomic and each clone has its own buffer, so this must be safe under concurrency. Mirrors
+ /// the equivalent WindowsDirectory test.
+ ///
+ [Test]
+ public virtual void TestConcurrentCloneReads()
+ {
+ DirectoryInfo path = CreateTempDir("nativeUnixConcurrent");
+ using var dir = NewDirectDirectory(path, out Directory @delegate);
+ using (@delegate)
+ {
+ int len = TestUtil.NextInt32(Random, 10000, 50000);
+ byte[] expected = new byte[len];
+ Random.NextBytes(expected);
+ using (IndexOutput output = dir.CreateOutput("data", MERGE_CONTEXT))
+ {
+ output.WriteBytes(expected, expected.Length);
+ }
+
+ using IndexInput input = dir.OpenInput("data", MERGE_CONTEXT);
+
+ int numThreads = Math.Max(2, Environment.ProcessorCount);
+ var threads = new Thread[numThreads];
+ Exception failure = null;
+ using (var start = new ManualResetEventSlim(false))
+ {
+ for (int t = 0; t < numThreads; t++)
+ {
+ int seed = t;
+ threads[t] = new Thread(() =>
+ {
+ try
+ {
+ var rnd = new J2N.Randomizer(seed + 1);
+ using IndexInput clone = (IndexInput)input.Clone();
+ start.Wait();
+ for (int i = 0; i < 1000; i++)
+ {
+ int pos = rnd.Next(len);
+ int count = Math.Min(rnd.Next(1, 256), len - pos);
+ clone.Seek(pos);
+ byte[] buf = new byte[count];
+ clone.ReadBytes(buf, 0, count);
+ for (int j = 0; j < count; j++)
+ {
+ if (buf[j] != expected[pos + j])
+ {
+ throw new Exception($"mismatch at {pos + j}: got {buf[j]}, expected {expected[pos + j]}");
+ }
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ Interlocked.CompareExchange(ref failure, e, null);
+ }
+ });
+ threads[t].Start();
+ }
+
+ start.Set(); // release all threads at once to maximize contention
+ foreach (var thread in threads)
+ {
+ thread.Join();
+ }
+ }
+
+ Assert.IsNull(failure, "concurrent clone reads failed: " + failure);
+ }
+ }
+ }
+
+ ///
+ /// Runs the full suite against
+ /// with minBytesDirect == 0, so the direct-I/O path is taken for all MERGE-context I/O.
+ /// Unix-gated and requires an O_DIRECT-capable filesystem.
+ ///
+ /// LUCENENET specific (GH-1342).
+ ///
+ [TestFixture]
+ [LuceneNetSpecific]
+#if FEATURE_SUPPORTEDOSPLATFORMATTRIBUTE
+ [UnsupportedOSPlatform("windows")]
+#endif
+ public class TestNativeUnixDirectoryBase : BaseDirectoryTestCase
+ {
+ private readonly List delegates = new List();
+
+ public override void SetUp()
+ {
+ base.SetUp();
+ AssumeTrue("NativeUnixDirectory requires Linux or macOS direct I/O.", !Constants.WINDOWS);
+ }
+
+ public override void TearDown()
+ {
+ foreach (var d in delegates)
+ {
+ try { d.Dispose(); } catch { /* ignore: best-effort cleanup of the delegate */ }
+ }
+ delegates.Clear();
+ base.TearDown();
+ }
+
+ protected override Directory GetDirectory(DirectoryInfo path)
+ {
+ var del = new SimpleFSDirectory(path);
+ delegates.Add(del);
+ // minBytesDirect = 0 so the direct-I/O path is used for all MERGE-context I/O.
+ return new NativeUnixDirectory(path, mergeBufferSize: 1024, minBytesDirect: 0, del);
+ }
+
+ ///
+ /// Not applicable to . It is a composite/delegating directory:
+ /// files written in a non-MERGE context go through the delegate and are tracked in the
+ /// delegate's staleFiles, not this directory's. Since only
+ /// fsyncs files in its own staleFiles (toSync.IntersectWith(m_staleFiles)), it does
+ /// not throw for a file written through the delegate, so this backdoor-the-filesystem test does
+ /// not hold here. The base test itself notes (TODO) that it does not handle composite/two-FSDir
+ /// directories.
+ ///
+ public override void TestFsyncDoesntCreateNewFiles()
+ {
+ AssumeTrue("TestFsyncDoesntCreateNewFiles does not apply to the composite/delegating NativeUnixDirectory; see override remarks.", false);
+ }
+ }
+}
diff --git a/src/Lucene.Net.Tests.Misc/Support/Store/TestWindowsDirectory.cs b/src/Lucene.Net.Tests.Misc/Support/Store/TestWindowsDirectory.cs
new file mode 100644
index 0000000000..6dc5be95ba
--- /dev/null
+++ b/src/Lucene.Net.Tests.Misc/Support/Store/TestWindowsDirectory.cs
@@ -0,0 +1,281 @@
+using Lucene.Net.Attributes;
+using Lucene.Net.Util;
+using NUnit.Framework;
+using RandomizedTesting.Generators;
+using System;
+using System.IO;
+#if FEATURE_SUPPORTEDOSPLATFORMATTRIBUTE
+using System.Runtime.Versioning;
+#endif
+using System.Threading;
+using Assert = Lucene.Net.TestFramework.Assert;
+
+namespace Lucene.Net.Store
+{
+ /*
+ * 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.
+ */
+
+ ///
+ /// Tests for .
+ ///
+ /// This runs the full suite against
+ /// . Because uses the
+ /// Win32 file APIs via P/Invoke, these tests are only run on Microsoft Windows; on
+ /// other platforms the entire fixture is skipped (see ).
+ ///
+ /// LUCENENET specific: the original Lucene WindowsDirectory had no test coverage (GH-1342).
+ ///
+ [TestFixture]
+ [LuceneNetSpecific]
+#if FEATURE_SUPPORTEDOSPLATFORMATTRIBUTE
+ [SupportedOSPlatform("windows")]
+#endif
+ public class TestWindowsDirectory : BaseDirectoryTestCase
+ {
+ public override void SetUp()
+ {
+ base.SetUp();
+ AssumeTrue("WindowsDirectory is only supported on Microsoft Windows.", Constants.WINDOWS);
+ }
+
+ protected override Directory GetDirectory(DirectoryInfo path)
+ {
+ return new WindowsDirectory(path);
+ }
+
+ ///
+ /// Exercises the positioned reads that back : random seeks
+ /// plus an independent clone reading from a different position, all sharing the same
+ /// underlying Win32 file handle.
+ ///
+ [Test]
+ public virtual void TestRandomAccessAndClones()
+ {
+ using Directory dir = GetDirectory(CreateTempDir("testWindowsRandomAccess"));
+
+ int len = TestUtil.NextInt32(Random, 100, 100000);
+ byte[] expected = new byte[len];
+ Random.NextBytes(expected);
+
+ using (IndexOutput output = dir.CreateOutput("data", NewIOContext(Random)))
+ {
+ output.WriteBytes(expected, expected.Length);
+ }
+
+ using IndexInput input = dir.OpenInput("data", NewIOContext(Random));
+ Assert.AreEqual(len, input.Length);
+
+ // sequential read of the whole file
+ byte[] actual = new byte[len];
+ input.ReadBytes(actual, 0, actual.Length);
+ Assert.AreEqual(expected, actual);
+
+ // random positioned reads
+ for (int i = 0; i < 100; i++)
+ {
+ int pos = Random.Next(len);
+ int count = Math.Min(TestUtil.NextInt32(Random, 1, 1000), len - pos);
+ input.Seek(pos);
+ byte[] buf = new byte[count];
+ input.ReadBytes(buf, 0, count);
+ for (int j = 0; j < count; j++)
+ {
+ Assert.AreEqual(expected[pos + j], buf[j], "mismatch at " + (pos + j));
+ }
+ }
+
+ // a clone shares the same handle but must read independently of the original
+ input.Seek(0);
+ using (IndexInput clone = (IndexInput)input.Clone())
+ {
+ int clonePos = Random.Next(len);
+ clone.Seek(clonePos);
+ Assert.AreEqual(expected[clonePos], clone.ReadByte());
+
+ // the original's position is unaffected by the clone's reads
+ Assert.AreEqual(0, input.Position);
+ Assert.AreEqual(expected[0], input.ReadByte());
+ }
+ }
+
+ ///
+ /// Reading past the end of the file must throw an EOF exception, not silently return
+ /// garbage or hang.
+ ///
+ [Test]
+ public virtual void TestReadPastEOF()
+ {
+ using Directory dir = GetDirectory(CreateTempDir("testWindowsEOF"));
+
+ const int len = 100;
+ using (IndexOutput output = dir.CreateOutput("eof", NewIOContext(Random)))
+ {
+ output.WriteBytes(new byte[len], len);
+ }
+
+ using IndexInput input = dir.OpenInput("eof", NewIOContext(Random));
+ Assert.AreEqual(len, input.Length);
+
+ // read right up to EOF: fine
+ input.Seek(len);
+ try
+ {
+ input.ReadByte();
+ fail("did not hit expected EOF exception");
+ }
+ catch (Exception e) when (e.IsEOFException())
+ {
+ // expected
+ }
+
+ // a read that straddles EOF must also throw
+ input.Seek(len - 4);
+ try
+ {
+ byte[] buf = new byte[8];
+ input.ReadBytes(buf, 0, buf.Length);
+ fail("did not hit expected EOF exception");
+ }
+ catch (Exception e) when (e.IsEOFException())
+ {
+ // expected
+ }
+ }
+
+ ///
+ /// Disposing an input (and a clone) more than once must be a no-op, never a double
+ /// CloseHandle. Clones must not close the shared handle.
+ ///
+ [Test]
+ public virtual void TestCloneDisposeDoesNotCloseSharedHandle()
+ {
+ using Directory dir = GetDirectory(CreateTempDir("testWindowsDoubleDispose"));
+ using (IndexOutput output = dir.CreateOutput("data", NewIOContext(Random)))
+ {
+ output.WriteInt64(42);
+ }
+
+ IndexInput input = dir.OpenInput("data", NewIOContext(Random));
+ IndexInput clone = (IndexInput)input.Clone();
+
+ // disposing the clone must not close the shared handle: original still reads
+ Assert.DoesNotThrow(() => clone.Dispose());
+ Assert.DoesNotThrow(() => clone.Dispose());
+ input.Seek(0);
+ Assert.AreEqual(42L, input.ReadInt64());
+
+ // original double-dispose is a no-op
+ Assert.DoesNotThrow(() => input.Dispose());
+ Assert.DoesNotThrow(() => input.Dispose());
+ }
+
+ ///
+ /// Many threads reading random positions through independent clones of the same input
+ /// (and thus the same shared Win32 handle) must each get correct data. This proves the
+ /// positioned reads are safe under concurrency, not just single-threaded.
+ ///
+ [Test]
+ public virtual void TestConcurrentCloneReads()
+ {
+ using Directory dir = GetDirectory(CreateTempDir("testWindowsConcurrent"));
+
+ int len = TestUtil.NextInt32(Random, 10000, 50000);
+ byte[] expected = new byte[len];
+ Random.NextBytes(expected);
+ using (IndexOutput output = dir.CreateOutput("data", NewIOContext(Random)))
+ {
+ output.WriteBytes(expected, expected.Length);
+ }
+
+ using IndexInput input = dir.OpenInput("data", NewIOContext(Random));
+
+ int numThreads = Math.Max(2, Environment.ProcessorCount);
+ var threads = new Thread[numThreads];
+ Exception failure = null;
+ using (var start = new ManualResetEventSlim(false))
+ {
+ for (int t = 0; t < numThreads; t++)
+ {
+ int seed = t;
+ threads[t] = new Thread(() =>
+ {
+ try
+ {
+ var rnd = new J2N.Randomizer(seed + 1);
+ using IndexInput clone = (IndexInput)input.Clone();
+ start.Wait();
+ for (int i = 0; i < 2000; i++)
+ {
+ int pos = rnd.Next(len);
+ int count = Math.Min(rnd.Next(1, 256), len - pos);
+ clone.Seek(pos);
+ byte[] buf = new byte[count];
+ clone.ReadBytes(buf, 0, count);
+ for (int j = 0; j < count; j++)
+ {
+ if (buf[j] != expected[pos + j])
+ {
+ throw new Exception($"mismatch at {pos + j}: got {buf[j]}, expected {expected[pos + j]}");
+ }
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ Interlocked.CompareExchange(ref failure, e, null);
+ }
+ });
+ threads[t].Start();
+ }
+
+ start.Set(); // release all threads at once to maximize contention
+ foreach (var thread in threads)
+ {
+ thread.Join();
+ }
+ }
+
+ Assert.IsNull(failure, "concurrent clone reads failed: " + failure);
+ }
+ }
+
+ ///
+ /// Verifies the cross-platform guard on . Unlike
+ /// , this fixture runs on every platform.
+ ///
+ [TestFixture]
+ [LuceneNetSpecific]
+ public class TestWindowsDirectoryPlatformGuard : LuceneTestCase
+ {
+ [Test]
+ public virtual void TestThrowsOnNonWindows()
+ {
+ DirectoryInfo path = CreateTempDir("testWindowsGuard");
+#pragma warning disable CA1416 // Validate platform compatibility - intentionally exercising the guard on all platforms
+ if (Constants.WINDOWS)
+ {
+ using Directory dir = new WindowsDirectory(path);
+ Assert.IsNotNull(dir);
+ }
+ else
+ {
+ Assert.Throws(() => new WindowsDirectory(path));
+ }
+#pragma warning restore CA1416 // Validate platform compatibility
+ }
+ }
+}