blob: 2138547e01716418ea52f9db55534881c2b3b024 [file] [log] [blame]
using Lucene.Net.Support.Text;
using Lucene.Net.Util;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace Lucene.Net.Support.IO
{
/*
* 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.
*/
/// <summary>
/// Represents the methods to support some operations over files.
/// </summary>
internal static class FileSupport
{
private static readonly char[] INVALID_FILENAME_CHARS = Path.GetInvalidFileNameChars();
// LUCNENENET NOTE: Lookup the HResult value we are interested in for the current OS
// by provoking the exception during initialization and caching its HResult value for later.
// We optimize for Windows because those HResult values are known and documented, but for
// other platforms, this is the only way we can reliably determine the HResult values
// we are interested in.
//
// Reference: https://stackoverflow.com/q/46380483
private const int WIN_HRESULT_FILE_ALREADY_EXISTS = unchecked((int)0x80070050);
private static readonly int? HRESULT_FILE_ALREADY_EXISTS = LoadFileAlreadyExistsHResult();
private static int? LoadFileAlreadyExistsHResult()
{
if (Constants.WINDOWS)
return WIN_HRESULT_FILE_ALREADY_EXISTS;
return GetFileIOExceptionHResult(provokeException: (fileName) =>
{
//Try to create the file again -this should throw an IOException with the correct HResult for the current platform
using var stream = new FileStream(fileName, FileMode.CreateNew, FileAccess.Write, FileShare.Read);
});
}
internal static int? GetFileIOExceptionHResult(Action<string> provokeException)
{
string fileName;
try
{
// This could throw, but we don't care about this HResult value.
fileName = Path.GetTempFileName();
}
catch
{
return null; // We couldn't create a temp file
}
try
{
provokeException(fileName);
}
catch (IOException ex) when (ex.HResult != 0) // Assume 0 means the platform is not completely implemented, thus unknown
{
return ex.HResult;
}
catch
{
return null; // Unknown exception
}
finally
{
try
{
File.Delete(fileName);
}
catch { }
}
return null; // Should never get here
}
/// <summary>
/// Creates a new empty file in a random subdirectory of <see cref="Path.GetTempPath()"/>, using the given prefix and
/// suffix strings to generate its name.
/// </summary>
/// <remarks>
/// If this method returns successfully then it is guaranteed that:
/// <list type="number">
/// <item><description>The file denoted by the returned abstract pathname did not exist before this method was invoked, and</description></item>
/// <item><description>Neither this method nor any of its variants will return the same abstract pathname again in the current invocation of the virtual machine.</description></item>
/// </list>
/// This method provides only part of a temporary-file facility. However, the file will not be deleted automatically,
/// it must be deleted by the caller.
/// <para/>
/// The prefix argument must be at least three characters long. It is recommended that the prefix be a short, meaningful
/// string such as "hjb" or "mail".
/// <para/>
/// The suffix argument may be null, in which case a random suffix will be used.
/// <para/>
/// Both prefix and suffix must be provided with valid characters for the underlying system, as specified by
/// <see cref="Path.GetInvalidFileNameChars()"/>.
/// <para/>
/// If the directory argument is null then the system-dependent default temporary-file directory will be used,
/// with a random subdirectory name. The default temporary-file directory is specified by the
/// <see cref="Path.GetTempPath()"/> method. On UNIX systems the default value of this property is typically
/// "/tmp" or "/var/tmp"; on Microsoft Windows systems it is typically "C:\\Users\\[UserName]\\AppData\Local\Temp".
/// </remarks>
/// <param name="prefix">The prefix string to be used in generating the file's name; must be at least three characters long</param>
/// <param name="suffix">The suffix string to be used in generating the file's name; may be null, in which case a random suffix will be generated</param>
/// <returns>A <see cref="FileInfo"/> instance representing the temp file that was created.</returns>
public static FileInfo CreateTempFile(string prefix, string suffix)
{
return CreateTempFile(prefix, suffix, null);
}
/// <summary>
/// Creates a new empty file in the specified directory, using the given prefix and suffix strings to generate its name.
/// </summary>
/// <remarks>
/// If this method returns successfully then it is guaranteed that:
/// <list type="number">
/// <item><description>The file denoted by the returned abstract pathname did not exist before this method was invoked, and</description></item>
/// <item><description>Neither this method nor any of its variants will return the same abstract pathname again in the current invocation of the virtual machine.</description></item>
/// </list>
/// This method provides only part of a temporary-file facility. However, the file will not be deleted automatically,
/// it must be deleted by the caller.
/// <para/>
/// The prefix argument must be at least three characters long. It is recommended that the prefix be a short, meaningful
/// string such as "hjb" or "mail".
/// <para/>
/// The suffix argument may be null, in which case a random suffix will be used.
/// <para/>
/// Both prefix and suffix must be provided with valid characters for the underlying system, as specified by
/// <see cref="Path.GetInvalidFileNameChars()"/>.
/// <para/>
/// If the directory argument is null then the system-dependent default temporary-file directory will be used,
/// with a random subdirectory name. The default temporary-file directory is specified by the
/// <see cref="Path.GetTempPath()"/> method. On UNIX systems the default value of this property is typically
/// "/tmp" or "/var/tmp"; on Microsoft Windows systems it is typically "C:\\Users\\[UserName]\\AppData\Local\Temp".
/// </remarks>
/// <param name="prefix">The prefix string to be used in generating the file's name; must be at least three characters long</param>
/// <param name="suffix">The suffix string to be used in generating the file's name; may be null, in which case a random suffix will be generated</param>
/// <param name="directory">The directory in which the file is to be created, or null if the default temporary-file directory is to be used</param>
/// <returns>A <see cref="FileInfo"/> instance representing the temp file that was created.</returns>
public static FileInfo CreateTempFile(string prefix, string suffix, DirectoryInfo directory)
{
if (string.IsNullOrEmpty(prefix))
throw new ArgumentNullException(nameof(prefix));
if (prefix.Length < 3)
throw new ArgumentException("Prefix string too short");
// Ensure the strings passed don't contain invalid characters
if (prefix.ContainsAny(INVALID_FILENAME_CHARS))
throw new ArgumentException(string.Format("Prefix contains invalid characters. You may not use any of '{0}'", string.Join(", ", INVALID_FILENAME_CHARS)));
if (suffix != null && suffix.ContainsAny(INVALID_FILENAME_CHARS))
throw new ArgumentException(string.Format("Suffix contains invalid characters. You may not use any of '{0}'", string.Join(", ", INVALID_FILENAME_CHARS)));
// If no directory supplied, create one.
if (directory == null)
{
directory = new DirectoryInfo(Path.Combine(Path.GetTempPath(), Path.GetFileNameWithoutExtension(Path.GetRandomFileName())));
}
// Ensure the directory exists (this does nothing if it already exists, although may throw exceptions in cases where permissions are changed)
directory.Create();
string fileName;
while (true)
{
fileName = NewTempFileName(prefix, suffix, directory);
if (File.Exists(fileName))
{
continue;
}
try
{
// Create the file, and close it immediately
using var stream = new FileStream(fileName, FileMode.CreateNew, FileAccess.Write, FileShare.Read);
break;
}
catch (IOException e) when (IsFileAlreadyExistsException(e, fileName))
{
// If the error was because the file exists, try again.
continue;
}
}
return new FileInfo(fileName);
}
/// <summary>
/// Tests whether the passed in <see cref="Exception"/> is an <see cref="IOException"/>
/// corresponding to the underlying operating system's "File Already Exists" violation.
/// This works by forcing the exception to occur during initialization and caching the
/// <see cref="Exception.HResult"/> value for the current OS.
/// </summary>
/// <param name="ex">An exception, for comparison.</param>
/// <param name="filePath">The path of the file to check. This is used as a fallback in case the
/// current OS doesn't have an HResult (an edge case).</param>
/// <returns><c>true</c> if the exception passed is an <see cref="IOException"/> with an
/// <see cref="Exception.HResult"/> corresponding to the operating system's "File Already Exists" violation, which
/// occurs when an attempt is made to create a file that already exists.</returns>
public static bool IsFileAlreadyExistsException(Exception ex, string filePath)
{
if (!typeof(IOException).Equals(ex))
return false;
else if (HRESULT_FILE_ALREADY_EXISTS.HasValue)
return ex.HResult == HRESULT_FILE_ALREADY_EXISTS;
else
return File.Exists(filePath);
}
/// <summary>
/// Generates a new random file name with the provided <paramref name="directory"/>,
/// <paramref name="prefix"/> and optional <paramref name="suffix"/>.
/// </summary>
/// <param name="prefix">The prefix string to be used in generating the file's name</param>
/// <param name="suffix">The suffix string to be used in generating the file's name; may be null, in which case a random suffix will be generated</param>
/// <param name="directory">A <see cref="DirectoryInfo"/> object containing the temp directory path. Must not be null.</param>
/// <returns>A random file name</returns>
internal static string NewTempFileName(string prefix, string suffix, DirectoryInfo directory)
{
string randomFileName = Path.GetRandomFileName();
if (suffix != null)
{
randomFileName = string.Concat(
Path.GetFileNameWithoutExtension(randomFileName),
suffix.StartsWith(".", StringComparison.Ordinal) ? suffix : '.' + suffix
);
}
return Path.Combine(directory.FullName, string.Concat(prefix, randomFileName));
}
private static readonly ConcurrentDictionary<string, string> fileCanonPathCache = new ConcurrentDictionary<string, string>();
/// <summary>
/// Returns the absolute path of this <see cref="FileSystemInfo"/> with all references resolved and
/// any drive letters normalized to upper case on Windows. An
/// <em>absolute</em> path is one that begins at the root of the file
/// system. The canonical path is one in which all references have been
/// resolved. For the cases of '..' and '.', where the file system supports
/// parent and working directory respectively, these are removed and replaced
/// with a direct directory reference.
/// </summary>
/// <param name="path">This <see cref="FileSystemInfo"/> instance.</param>
/// <returns>The canonical path of this file.</returns>
// LUCENENET NOTE: Implementation ported mostly from Apache Harmony
public static string GetCanonicalPath(this FileSystemInfo path)
{
string absPath = path.FullName; // LUCENENET NOTE: This internally calls GetFullPath(), which resolves relative paths
byte[] result = Encoding.UTF8.GetBytes(absPath);
if (fileCanonPathCache.TryGetValue(absPath, out string canonPath) && canonPath != null)
{
return canonPath;
}
// LUCENENET TODO: On Unix, this resolves symbolic links. Not sure
// if it is safe to assume Path.GetFullPath() does that for us.
//if (Path.DirectorySeparatorChar == '/')
//{
// //// resolve the full path first
// //result = resolveLink(result, result.Length, false);
// //// resolve the parent directories
// //result = resolve(result);
//}
int numSeparators = 1;
for (int i = 0; i < result.Length; i++)
{
if (result[i] == Path.DirectorySeparatorChar)
{
numSeparators++;
}
}
int[] sepLocations = new int[numSeparators];
int rootLoc = 0;
if (Path.DirectorySeparatorChar == '\\')
{
if (result[0] == '\\')
{
rootLoc = (result.Length > 1 && result[1] == '\\') ? 1 : 0;
}
else
{
rootLoc = 2; // skip drive i.e. c:
}
}
byte[] newResult = new byte[result.Length + 1];
int newLength = 0, lastSlash = 0, foundDots = 0;
sepLocations[lastSlash] = rootLoc;
for (int i = 0; i <= result.Length; i++)
{
if (i < rootLoc)
{
// Normalize case of Windows drive letter to upper
newResult[newLength++] = (byte)char.ToUpperInvariant((char)result[i]);
}
else
{
if (i == result.Length || result[i] == Path.DirectorySeparatorChar)
{
if (i == result.Length && foundDots == 0)
{
break;
}
if (foundDots == 1)
{
/* Don't write anything, just reset and continue */
foundDots = 0;
continue;
}
if (foundDots > 1)
{
/* Go back N levels */
lastSlash = lastSlash > (foundDots - 1) ? lastSlash
- (foundDots - 1) : 0;
newLength = sepLocations[lastSlash] + 1;
foundDots = 0;
continue;
}
sepLocations[++lastSlash] = newLength;
newResult[newLength++] = (byte)Path.DirectorySeparatorChar;
continue;
}
if (result[i] == '.')
{
foundDots++;
continue;
}
/* Found some dots within text, write them out */
if (foundDots > 0)
{
for (int j = 0; j < foundDots; j++)
{
newResult[newLength++] = (byte)'.';
}
}
newResult[newLength++] = result[i];
foundDots = 0;
}
}
// remove trailing slash
if (newLength > (rootLoc + 1)
&& newResult[newLength - 1] == Path.DirectorySeparatorChar)
{
newLength--;
}
newResult[newLength] = 0;
//newResult = getCanonImpl(newResult);
newLength = newResult.Length;
canonPath = fileCanonPathCache.GetOrAdd(
absPath,
k => Encoding.UTF8.GetString(newResult, 0, newLength).TrimEnd('\0')); // LUCENENET: Eliminate null terminator char
return canonPath;
}
}
}