You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucenenet.apache.org by pa...@apache.org on 2026/06/16 21:08:01 UTC
(lucenenet) 01/02: Validate file paths for FSDirectory and Replicator (#1357)
This is an automated email from the ASF dual-hosted git repository.
paulirwin pushed a commit to branch release/4.8.0-beta00018
in repository https://gitbox.apache.org/repos/asf/lucenenet.git
commit f7a62d6d1d9aaa328279da3fcc71f6c2a75611c9
Author: Paul Irwin <pa...@gmail.com>
AuthorDate: Tue Jun 16 14:56:11 2026 -0600
Validate file paths for FSDirectory and Replicator (#1357)
Rewritten from commit 7b7314634f58952d7e2752e589391eeac22fba78 for this release branch.
---
src/Lucene.Net.Replicator/LocalReplicator.cs | 7 ++
.../PerSessionDirectoryFactory.cs | 23 +++-
src/Lucene.Net.Replicator/SessionToken.cs | 13 ++-
.../LocalReplicatorTest.cs | 24 ++++-
.../PerSessionDirectoryFactoryTest.cs | 118 +++++++++++++++++++++
.../SessionTokenTest.cs | 56 ++++++++++
src/Lucene.Net.Tests/Store/TestDirectory.cs | 97 +++++++++++++++++
.../Support/Text/TestStringExtensions.cs | 65 ++++++++++++
src/Lucene.Net/Store/FSDirectory.cs | 18 ++++
src/Lucene.Net/Store/MMapDirectory.cs | 2 +
src/Lucene.Net/Store/NIOFSDirectory.cs | 2 +
src/Lucene.Net/Store/SimpleFSDirectory.cs | 2 +
src/Lucene.Net/Support/Text/StringExtensions.cs | 23 ++++
13 files changed, 447 insertions(+), 3 deletions(-)
diff --git a/src/Lucene.Net.Replicator/LocalReplicator.cs b/src/Lucene.Net.Replicator/LocalReplicator.cs
index a7c06b7df..b4c8019a8 100644
--- a/src/Lucene.Net.Replicator/LocalReplicator.cs
+++ b/src/Lucene.Net.Replicator/LocalReplicator.cs
@@ -1,4 +1,5 @@
using J2N.Threading.Atomic;
+using Lucene.Net.Support.Text;
using Lucene.Net.Support.Threading;
using System;
using System.Collections.Generic;
@@ -254,6 +255,12 @@ public virtual long ExpirationThreshold
public virtual Stream ObtainFile(string sessionId, string source, string fileName)
{
+ // LUCENENET-specific: validate the file name is valid for replication
+ if (!fileName.IsValidSinglePathComponent())
+ {
+ throw new ArgumentException("File name is not valid for replication", nameof(fileName));
+ }
+
UninterruptableMonitor.Enter(syncLock);
try
{
diff --git a/src/Lucene.Net.Replicator/PerSessionDirectoryFactory.cs b/src/Lucene.Net.Replicator/PerSessionDirectoryFactory.cs
index 431bb34e3..894e3abaa 100644
--- a/src/Lucene.Net.Replicator/PerSessionDirectoryFactory.cs
+++ b/src/Lucene.Net.Replicator/PerSessionDirectoryFactory.cs
@@ -1,4 +1,5 @@
using Lucene.Net.Store;
+using Lucene.Net.Support.Text;
using System;
using System.IO;
using Directory = Lucene.Net.Store.Directory;
@@ -42,6 +43,17 @@ public PerSessionDirectoryFactory(string workingDirectory)
public virtual Directory GetDirectory(string sessionId, string source)
{
+ // LUCENENET-specific: validate sessionId and source are valid for paths
+ if (!sessionId.IsValidSinglePathComponent())
+ {
+ throw new ArgumentException("Session ID is not valid for replication", nameof(sessionId));
+ }
+
+ if (!source.IsValidSinglePathComponent())
+ {
+ throw new ArgumentException("Source is not valid for replication", nameof(source));
+ }
+
string sourceDirectory = Path.Combine(workingDirectory, sessionId, source);
System.IO.Directory.CreateDirectory(sourceDirectory);
if (!System.IO.Directory.Exists(sourceDirectory))
@@ -51,7 +63,16 @@ public virtual Directory GetDirectory(string sessionId, string source)
public virtual void CleanupSession(string sessionId)
{
- if (string.IsNullOrEmpty(sessionId)) throw new ArgumentException("sessionID cannot be empty", nameof(sessionId));
+ if (string.IsNullOrEmpty(sessionId))
+ {
+ throw new ArgumentException("sessionID cannot be empty", nameof(sessionId));
+ }
+
+ // LUCENENET-specific: validate sessionId is valid for paths
+ if (!sessionId.IsValidSinglePathComponent())
+ {
+ throw new ArgumentException("Session ID is not valid for replication", nameof(sessionId));
+ }
string sessionDirectory = Path.Combine(workingDirectory, sessionId);
System.IO.Directory.Delete(sessionDirectory, true);
diff --git a/src/Lucene.Net.Replicator/SessionToken.cs b/src/Lucene.Net.Replicator/SessionToken.cs
index ee04a3801..160d506e2 100644
--- a/src/Lucene.Net.Replicator/SessionToken.cs
+++ b/src/Lucene.Net.Replicator/SessionToken.cs
@@ -1,4 +1,6 @@
using J2N.IO;
+using Lucene.Net.Support.Text;
+using System;
using System.Collections.Generic;
using System.IO;
using JCG = J2N.Collections.Generic;
@@ -71,7 +73,16 @@ public SessionToken(IDataInput reader)
IList<RevisionFile> files = new JCG.List<RevisionFile>(numFiles);
for (int i = 0; i < numFiles; i++)
{
- files.Add(new RevisionFile(reader.ReadUTF(), reader.ReadInt64()));
+ string fileName = reader.ReadUTF();
+ long length = reader.ReadInt64();
+
+ // LUCENENET-specific: validate that fileName is valid for replication
+ if (!fileName.IsValidSinglePathComponent())
+ {
+ throw new ArgumentException("File name is not valid for replication", nameof(fileName));
+ }
+
+ files.Add(new RevisionFile(fileName, length));
}
sourceFiles.Add(source, files);
--numSources;
diff --git a/src/Lucene.Net.Tests.Replicator/LocalReplicatorTest.cs b/src/Lucene.Net.Tests.Replicator/LocalReplicatorTest.cs
index f11a4600f..78dc6790f 100644
--- a/src/Lucene.Net.Tests.Replicator/LocalReplicatorTest.cs
+++ b/src/Lucene.Net.Tests.Replicator/LocalReplicatorTest.cs
@@ -1,4 +1,5 @@
-using Lucene.Net.Documents;
+using Lucene.Net.Attributes;
+using Lucene.Net.Documents;
using Lucene.Net.Index;
using Lucene.Net.Support.Threading;
using Lucene.Net.Util;
@@ -90,6 +91,27 @@ public void TestObtainFileAlreadyClosed()
}
}
+ // LUCENENET-specific: covers fileName validation in LocalReplicator.ObtainFile.
+ // Validation runs before session lookup, so a fresh (never-published) replicator is sufficient.
+ [Test, LuceneNetSpecific]
+ [TestCase("../../a.txt")]
+ [TestCase("..\\a.txt")]
+ [TestCase("/a/b")]
+ [TestCase("C:\\folder\\file")]
+ [TestCase("subdir/file.txt")]
+ [TestCase("subdir\\file.txt")]
+ [TestCase("..")]
+ [TestCase(".")]
+ [TestCase("name\0extra")]
+ [TestCase("")]
+ public void TestObtainFileRejectsInvalidFileName(string invalidFileName)
+ {
+ Assert.Throws<ArgumentException>(() =>
+ {
+ using Stream _ = replicator.ObtainFile("session1", "src", invalidFileName);
+ }, $"ObtainFile should reject fileName '{invalidFileName}'");
+ }
+
[Test]
public void TestPublishAlreadyClosed()
{
diff --git a/src/Lucene.Net.Tests.Replicator/PerSessionDirectoryFactoryTest.cs b/src/Lucene.Net.Tests.Replicator/PerSessionDirectoryFactoryTest.cs
new file mode 100644
index 000000000..b1509d669
--- /dev/null
+++ b/src/Lucene.Net.Tests.Replicator/PerSessionDirectoryFactoryTest.cs
@@ -0,0 +1,118 @@
+using Lucene.Net.Attributes;
+using Lucene.Net.Util;
+using NUnit.Framework;
+using System;
+using System.IO;
+using Directory = Lucene.Net.Store.Directory;
+
+namespace Lucene.Net.Replicator
+{
+ /*
+ * 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.
+ */
+
+ // LUCENENET-specific: covers sessionId and source name validation in PerSessionDirectoryFactory
+ // for the GetDirectory and CleanupSession entry points.
+ [LuceneNetSpecific]
+ public class PerSessionDirectoryFactoryTest : ReplicatorTestCase
+ {
+ private static readonly string[] InvalidPathComponents =
+ {
+ "../a",
+ "..\\a",
+ "/a",
+ "C:\\folder",
+ "subdir/segment",
+ "subdir\\segment",
+ "..",
+ ".",
+ "name\0extra",
+ "",
+ };
+
+ [Test]
+ [TestCaseSource(nameof(InvalidPathComponents))]
+ public void TestGetDirectoryRejectsInvalidSessionId(string sessionId)
+ {
+ DirectoryInfo workDir = CreateTempDir("perSessionFactoryGetSession");
+ PerSessionDirectoryFactory factory = new PerSessionDirectoryFactory(workDir.FullName);
+
+ Assert.Throws<ArgumentException>(() =>
+ {
+ using Directory _ = factory.GetDirectory(sessionId, "src");
+ }, $"GetDirectory should reject sessionId '{sessionId}'");
+ }
+
+ [Test]
+ [TestCaseSource(nameof(InvalidPathComponents))]
+ public void TestGetDirectoryRejectsInvalidSource(string source)
+ {
+ DirectoryInfo workDir = CreateTempDir("perSessionFactoryGetSource");
+ PerSessionDirectoryFactory factory = new PerSessionDirectoryFactory(workDir.FullName);
+
+ Assert.Throws<ArgumentException>(() =>
+ {
+ using Directory _ = factory.GetDirectory("session1", source);
+ }, $"GetDirectory should reject source '{source}'");
+ }
+
+ [Test]
+ [TestCaseSource(nameof(InvalidPathComponents))]
+ public void TestCleanupSessionRejectsInvalidSessionId(string sessionId)
+ {
+ DirectoryInfo workDir = CreateTempDir("perSessionFactoryCleanup");
+ PerSessionDirectoryFactory factory = new PerSessionDirectoryFactory(workDir.FullName);
+
+ Assert.Throws<ArgumentException>(() =>
+ {
+ factory.CleanupSession(sessionId);
+ }, $"CleanupSession should reject sessionId '{sessionId}'");
+ }
+
+ [Test]
+ public void TestCleanupSessionDoesNotTouchSiblingDirectory()
+ {
+ // A sessionId that resolves to a sibling of the workingDirectory must be rejected by
+ // validation. The sibling directory and its contents must remain untouched.
+ DirectoryInfo workDir = CreateTempDir("perSessionFactorySiblingWork");
+ DirectoryInfo sibling = CreateTempDir("perSessionFactorySibling");
+ string siblingFile = Path.Combine(sibling.FullName, "marker.txt");
+ File.WriteAllText(siblingFile, "untouched");
+
+ PerSessionDirectoryFactory factory = new PerSessionDirectoryFactory(workDir.FullName);
+ // Path.GetRelativePath is not available on .NET Framework; compute manually.
+ // Both temp dirs share the same parent, so the relative path is "../<siblingName>".
+ string relative = Path.Combine("..", sibling.Name);
+
+ Assert.Throws<ArgumentException>(() => factory.CleanupSession(relative));
+ assertTrue("sibling directory must still exist", System.IO.Directory.Exists(sibling.FullName));
+ assertTrue("sibling file must still exist", File.Exists(siblingFile));
+ }
+
+ [Test]
+ public void TestGetDirectoryAcceptsValidNames()
+ {
+ DirectoryInfo workDir = CreateTempDir("perSessionFactoryValid");
+ PerSessionDirectoryFactory factory = new PerSessionDirectoryFactory(workDir.FullName);
+
+ using Directory dir = factory.GetDirectory("session-abc", "src");
+ assertNotNull(dir);
+
+ string expected = Path.Combine(workDir.FullName, "session-abc", "src");
+ assertTrue("expected session directory to exist on disk", System.IO.Directory.Exists(expected));
+ }
+ }
+}
diff --git a/src/Lucene.Net.Tests.Replicator/SessionTokenTest.cs b/src/Lucene.Net.Tests.Replicator/SessionTokenTest.cs
index c07b11b9c..344ffa61b 100644
--- a/src/Lucene.Net.Tests.Replicator/SessionTokenTest.cs
+++ b/src/Lucene.Net.Tests.Replicator/SessionTokenTest.cs
@@ -1,8 +1,10 @@
using J2N.IO;
+using Lucene.Net.Attributes;
using Lucene.Net.Documents;
using Lucene.Net.Index;
using Lucene.Net.Util;
using NUnit.Framework;
+using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -63,5 +65,59 @@ public void TestSerialization()
IOUtils.Dispose(writer, directory);
}
+ // LUCENENET-specific: covers RevisionFile.FileName validation in the
+ // SessionToken(IDataInput) deserialization constructor.
+ [Test, LuceneNetSpecific]
+ [TestCase("../../../a.txt")]
+ [TestCase("..\\..\\a.txt")]
+ [TestCase("/a/b")]
+ [TestCase("C:\\folder\\file")]
+ [TestCase("subdir/file.txt")]
+ [TestCase("..")]
+ [TestCase(".")]
+ [TestCase("name\0extra")]
+ [TestCase("")]
+ public void TestDeserializeRejectsInvalidFileName(string invalidFileName)
+ {
+ using MemoryStream ms = new MemoryStream();
+ DataOutputStream dos = new DataOutputStream(ms);
+ dos.WriteUTF("session1");
+ dos.WriteUTF("ver1");
+ dos.WriteInt32(1);
+ dos.WriteUTF("source1");
+ dos.WriteInt32(1);
+ dos.WriteUTF(invalidFileName);
+ dos.WriteInt64(123L);
+ dos.Flush();
+ ms.Position = 0;
+
+ Assert.Throws<ArgumentException>(() =>
+ {
+ _ = new SessionToken(new DataInputStream(ms));
+ }, $"SessionToken should reject RevisionFile.FileName '{invalidFileName}'");
+ }
+
+ [Test, LuceneNetSpecific]
+ [TestCase("segments.gen")]
+ [TestCase("_0.cfs")]
+ [TestCase(".hidden")]
+ public void TestDeserializeAcceptsValidFileName(string fileName)
+ {
+ using MemoryStream ms = new MemoryStream();
+ DataOutputStream dos = new DataOutputStream(ms);
+ dos.WriteUTF("session1");
+ dos.WriteUTF("ver1");
+ dos.WriteInt32(1);
+ dos.WriteUTF("source1");
+ dos.WriteInt32(1);
+ dos.WriteUTF(fileName);
+ dos.WriteInt64(123L);
+ dos.Flush();
+ ms.Position = 0;
+
+ SessionToken token = new SessionToken(new DataInputStream(ms));
+ assertEquals(fileName, token.SourceFiles["source1"][0].FileName);
+ }
+
}
}
\ No newline at end of file
diff --git a/src/Lucene.Net.Tests/Store/TestDirectory.cs b/src/Lucene.Net.Tests/Store/TestDirectory.cs
index 6c47cc70a..a7ce69462 100644
--- a/src/Lucene.Net.Tests/Store/TestDirectory.cs
+++ b/src/Lucene.Net.Tests/Store/TestDirectory.cs
@@ -536,5 +536,102 @@ private static void Search(Index.IndexReader r, int times)
}
}
}
+
+ // LUCENENET-specific: covers EnsureCanWrite name validation across FSDirectory subclasses.
+ [Test, LuceneNetSpecific]
+ [TestCase("../../foo.txt")]
+ [TestCase("..\\foo.txt")]
+ [TestCase("/tmp/foo.txt")]
+ [TestCase("subdir/file.txt")]
+ [TestCase("subdir\\file.txt")]
+ [TestCase("..")]
+ [TestCase(".")]
+ [TestCase("")]
+ public void TestCreateOutputRejectsInvalidName(string invalidName)
+ {
+ DirectoryInfo path = CreateTempDir("ensureCanWrite");
+ foreach (FSDirectory dir in new FSDirectory[]
+ {
+ new SimpleFSDirectory(path),
+ new NIOFSDirectory(path),
+ new MMapDirectory(path),
+ })
+ {
+ try
+ {
+ Assert.Throws<ArgumentException>(() =>
+ {
+ using IndexOutput _ = dir.CreateOutput(invalidName, IOContext.DEFAULT);
+ }, $"{dir.GetType().Name}.CreateOutput should reject '{invalidName}'");
+ }
+ finally
+ {
+ dir.Dispose();
+ }
+ }
+ }
+
+ // LUCENENET-specific: covers EnsureCanRead name validation across FSDirectory subclasses.
+ [Test, LuceneNetSpecific]
+ [TestCase("../../foo.txt")]
+ [TestCase("..\\foo.txt")]
+ [TestCase("/tmp/foo.txt")]
+ [TestCase("subdir/file.txt")]
+ [TestCase("subdir\\file.txt")]
+ [TestCase("..")]
+ [TestCase(".")]
+ [TestCase("")]
+ public void TestOpenInputRejectsInvalidName(string invalidName)
+ {
+ DirectoryInfo path = CreateTempDir("ensureCanRead");
+ foreach (FSDirectory dir in new FSDirectory[]
+ {
+ new SimpleFSDirectory(path),
+ new NIOFSDirectory(path),
+ new MMapDirectory(path),
+ })
+ {
+ try
+ {
+ Assert.Throws<ArgumentException>(() =>
+ {
+ using IndexInput _ = dir.OpenInput(invalidName, IOContext.DEFAULT);
+ }, $"{dir.GetType().Name}.OpenInput should reject '{invalidName}'");
+ }
+ finally
+ {
+ dir.Dispose();
+ }
+ }
+ }
+
+ // NIOFSDirectory and SimpleFSDirectory have their own CreateSlicer overload that opens its
+ // own FileStream rather than delegating to OpenInput, so validation must fire there too.
+ [Test, LuceneNetSpecific]
+ [TestCase("../../foo.txt")]
+ [TestCase("..\\foo.txt")]
+ [TestCase("/tmp/foo.txt")]
+ public void TestCreateSlicerRejectsInvalidName(string invalidName)
+ {
+ DirectoryInfo path = CreateTempDir("ensureCanReadSlicer");
+ foreach (FSDirectory dir in new FSDirectory[]
+ {
+ new SimpleFSDirectory(path),
+ new NIOFSDirectory(path),
+ })
+ {
+ try
+ {
+ Assert.Throws<ArgumentException>(() =>
+ {
+ using Directory.IndexInputSlicer _ = dir.CreateSlicer(invalidName, IOContext.DEFAULT);
+ }, $"{dir.GetType().Name}.CreateSlicer should reject '{invalidName}'");
+ }
+ finally
+ {
+ dir.Dispose();
+ }
+ }
+ }
}
}
diff --git a/src/Lucene.Net.Tests/Support/Text/TestStringExtensions.cs b/src/Lucene.Net.Tests/Support/Text/TestStringExtensions.cs
new file mode 100644
index 000000000..b5461b58d
--- /dev/null
+++ b/src/Lucene.Net.Tests/Support/Text/TestStringExtensions.cs
@@ -0,0 +1,65 @@
+using Lucene.Net.Attributes;
+using Lucene.Net.Support.Text;
+using Lucene.Net.Util;
+using NUnit.Framework;
+
+namespace Lucene.Net.Support.Text
+{
+ /*
+ * 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.
+ */
+
+ [LuceneNetSpecific]
+ public class TestStringExtensions : LuceneTestCase
+ {
+ [TestCase("segments.gen")]
+ [TestCase("_0.cfs")]
+ [TestCase("_0_Lucene41_0.tip")]
+ [TestCase(".hidden")]
+ [TestCase("a..b..c")]
+ [TestCase("name-with-dashes_and_underscores.ext")]
+ [LuceneNetSpecific]
+ public void TestIsValidSinglePathComponent_AcceptsValidNames(string path)
+ {
+ Assert.IsTrue(path.IsValidSinglePathComponent(),
+ $"expected '{path}' to be a valid single path component");
+ }
+
+ [TestCase("")] // empty
+ [TestCase(".")] // current-directory literal
+ [TestCase("..")] // parent-directory literal
+ [TestCase("foo/bar")] // forward-slash separator
+ [TestCase("foo\\bar")] // backslash separator (rejected even on POSIX)
+ [TestCase("../../a/b")] // multi-level forward-slash sequence
+ [TestCase("..\\..\\a\\b")] // multi-level backslash sequence
+ [TestCase("..\\a")] // single-level backslash sequence
+ [TestCase("/a/b")] // absolute POSIX path
+ [TestCase("/tmp/file.txt")] // absolute POSIX path
+ [TestCase("C:\\folder\\file")] // path containing a Windows volume separator
+ [TestCase("C:\\file.txt")] // path containing a Windows volume separator
+ [TestCase("name\0extra")] // embedded NUL
+ [TestCase("name/")] // trailing forward slash
+ [TestCase("name\\")] // trailing backslash
+ [TestCase("\\")] // bare backslash
+ [TestCase("/")] // bare forward slash
+ [LuceneNetSpecific]
+ public void TestIsValidSinglePathComponent_RejectsInvalidNames(string path)
+ {
+ Assert.IsFalse(path.IsValidSinglePathComponent(),
+ $"expected '{path}' to be rejected as a single path component");
+ }
+ }
+}
diff --git a/src/Lucene.Net/Store/FSDirectory.cs b/src/Lucene.Net/Store/FSDirectory.cs
index 66950592c..641a591bc 100644
--- a/src/Lucene.Net/Store/FSDirectory.cs
+++ b/src/Lucene.Net/Store/FSDirectory.cs
@@ -1,5 +1,6 @@
using Lucene.Net.Support;
using Lucene.Net.Support.IO;
+using Lucene.Net.Support.Text;
using Lucene.Net.Support.Threading;
using Lucene.Net.Util;
using System;
@@ -362,6 +363,12 @@ public override IndexOutput CreateOutput(string name, IOContext context)
protected virtual void EnsureCanWrite(string name)
{
+ // LUCENENET-specific: validate the name is valid for a FSDirectory
+ if (!name.IsValidSinglePathComponent())
+ {
+ throw new ArgumentException("File name is not valid for a FSDirectory", nameof(name));
+ }
+
if (!m_directory.Exists)
{
try
@@ -388,6 +395,17 @@ protected virtual void EnsureCanWrite(string name)
}
}
+ // LUCENENET-specific: backported method signature (but not implementation, yet)
+ // from Lucene 6.0.0 as extensibility point to override name validation if needed
+ protected virtual void EnsureCanRead(string name)
+ {
+ // LUCENENET-specific: validate the name is valid for a FSDirectory
+ if (!name.IsValidSinglePathComponent())
+ {
+ throw new ArgumentException("File name is not valid for a FSDirectory", nameof(name));
+ }
+ }
+
protected virtual void OnIndexOutputClosed(FSIndexOutput io)
{
// LUCENENET Specific: See remarks for m_staleFiles field.
diff --git a/src/Lucene.Net/Store/MMapDirectory.cs b/src/Lucene.Net/Store/MMapDirectory.cs
index 59e1cccf8..35c9db811 100644
--- a/src/Lucene.Net/Store/MMapDirectory.cs
+++ b/src/Lucene.Net/Store/MMapDirectory.cs
@@ -183,6 +183,7 @@ public MMapDirectory(string path, LockFactory lockFactory, int maxChunkSize)
public override IndexInput OpenInput(string name, IOContext context)
{
EnsureOpen();
+ EnsureCanRead(name); // LUCENENET-specific: backported call site from Lucene 6.0.0
var file = new FileInfo(Path.Combine(Directory.FullName, name));
var fc = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
return new MMapIndexInput(this, "MMapIndexInput(path=\"" + file + "\")", fc);
@@ -190,6 +191,7 @@ public override IndexInput OpenInput(string name, IOContext context)
public override IndexInputSlicer CreateSlicer(string name, IOContext context)
{
+ // LUCENENET NOTE: name is validated in OpenInput call below
var full = (MMapIndexInput)OpenInput(name, context);
return new IndexInputSlicerAnonymousClass(this, full);
}
diff --git a/src/Lucene.Net/Store/NIOFSDirectory.cs b/src/Lucene.Net/Store/NIOFSDirectory.cs
index 684f42b68..6513bed26 100644
--- a/src/Lucene.Net/Store/NIOFSDirectory.cs
+++ b/src/Lucene.Net/Store/NIOFSDirectory.cs
@@ -102,6 +102,7 @@ public NIOFSDirectory(string path)
public override IndexInput OpenInput(string name, IOContext context)
{
EnsureOpen();
+ EnsureCanRead(name); // LUCENENET-specific: backported call site from Lucene 6.0.0
var path = new FileInfo(Path.Combine(Directory.FullName, name));
var fc = new FileStream(path.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
return new NIOFSIndexInput("NIOFSIndexInput(path=\"" + path + "\")", fc, context);
@@ -110,6 +111,7 @@ public override IndexInput OpenInput(string name, IOContext context)
public override IndexInputSlicer CreateSlicer(string name, IOContext context)
{
EnsureOpen();
+ EnsureCanRead(name); // LUCENENET-specific: this method is not in Lucene 6.0.0 but added to match OpenInput above
var path = new FileInfo(Path.Combine(Directory.FullName, name));
var fc = new FileStream(path.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
return new IndexInputSlicerAnonymousClass(context, path, fc);
diff --git a/src/Lucene.Net/Store/SimpleFSDirectory.cs b/src/Lucene.Net/Store/SimpleFSDirectory.cs
index 923ca685b..31a695d83 100644
--- a/src/Lucene.Net/Store/SimpleFSDirectory.cs
+++ b/src/Lucene.Net/Store/SimpleFSDirectory.cs
@@ -98,6 +98,7 @@ public SimpleFSDirectory(string path)
public override IndexInput OpenInput(string name, IOContext context)
{
EnsureOpen();
+ EnsureCanRead(name); // LUCENENET-specific: backported call site from Lucene 6.0.0
var path = new FileInfo(Path.Combine(Directory.FullName, name));
var raf = new FileStream(path.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
return new SimpleFSIndexInput("SimpleFSIndexInput(path=\"" + path.FullName + "\")", raf, context);
@@ -106,6 +107,7 @@ public override IndexInput OpenInput(string name, IOContext context)
public override IndexInputSlicer CreateSlicer(string name, IOContext context)
{
EnsureOpen();
+ EnsureCanRead(name); // LUCENENET-specific: this method is not in Lucene 6.0.0 but added to match OpenInput above
var file = new FileInfo(Path.Combine(Directory.FullName, name));
var descriptor = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
return new IndexInputSlicerAnonymousClass(context, file, descriptor);
diff --git a/src/Lucene.Net/Support/Text/StringExtensions.cs b/src/Lucene.Net/Support/Text/StringExtensions.cs
index eae084223..c7af2b017 100644
--- a/src/Lucene.Net/Support/Text/StringExtensions.cs
+++ b/src/Lucene.Net/Support/Text/StringExtensions.cs
@@ -1,5 +1,6 @@
using J2N.Text;
using System;
+using System.IO;
using System.Runtime.CompilerServices;
namespace Lucene.Net.Support.Text
@@ -48,5 +49,27 @@ public static bool ContainsAny(this string input, char[] charsToCompare)
}
return false;
}
+
+#nullable enable
+ /// <summary>
+ /// Returns <c>true</c> if <paramref name="path"/> is a valid, single path component for use in index
+ /// file system access. A valid value is either a file name or a directory name, without any directory
+ /// or volume separators.
+ /// </summary>
+ /// <param name="path">The file name to check.</param>
+ /// <returns><c>true</c> if <paramref name="path"/> is a valid path component;
+ /// otherwise, <c>false</c>.</returns>
+ public static bool IsValidSinglePathComponent(this string path)
+ {
+ // Check IndexOfAny before Path.GetFileName: on .NET Framework, Path.GetFileName throws
+ // ArgumentException for strings containing NUL or other characters that are illegal in paths.
+ return !string.IsNullOrEmpty(path)
+ && path != "."
+ && path != ".."
+ && path.IndexOfAny(Path.GetInvalidFileNameChars()) < 0
+ && path.IndexOf('\\') < 0 // backslash is not an invalid character on Linux/macOS but is invalid for this purpose
+ && path == Path.GetFileName(path); // NOTE: ensures no directory components (separators, volume names)
+ }
+#nullable restore
}
}