using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using Semmle.Util.Logging;
using Mono.Unix;
namespace Semmle.Util
{
///
/// Interface for obtaining canonical paths.
///
public interface IPathCache
{
string GetCanonicalPath(string path);
}
///
/// Algorithm for determining a canonical path.
/// For example some strategies may preserve symlinks
/// or only work on certain platforms.
///
public abstract class PathStrategy
{
///
/// Obtain a canonical path.
///
/// The path to canonicalise.
/// A cache for making subqueries.
/// The canonical path.
public abstract string GetCanonicalPath(string path, IPathCache cache);
///
/// Constructs a canonical path for a file
/// which doesn't yet exist.
///
/// Path to canonicalise.
/// The PathCache.
/// A canonical path.
protected static string ConstructCanonicalPath(string path, IPathCache cache)
{
DirectoryInfo parent = Directory.GetParent(path);
return parent != null ?
Path.Combine(cache.GetCanonicalPath(parent.FullName), Path.GetFileName(path)) :
path.ToUpperInvariant();
}
}
///
/// Determine canonical paths using the Win32 function
/// GetFinalPathNameByHandle(). Follows symlinks.
///
class GetFinalPathNameByHandleStrategy : PathStrategy
{
///
/// Call GetFinalPathNameByHandle() to get a canonical filename.
/// Follows symlinks.
///
///
///
/// GetFinalPathNameByHandle() only works on open file handles,
/// so if the path doesn't yet exist, construct the path
/// by appending the filename to the canonical parent directory.
///
///
/// The path to canonicalise.
/// Subquery cache.
/// The canonical path.
public override string GetCanonicalPath(string path, IPathCache cache)
{
using (var hFile = Win32.CreateFile( // lgtm[cs/call-to-unmanaged-code]
path,
0,
Win32.FILE_SHARE_READ | Win32.FILE_SHARE_WRITE,
IntPtr.Zero,
Win32.OPEN_EXISTING,
Win32.FILE_FLAG_BACKUP_SEMANTICS,
IntPtr.Zero))
{
if (hFile.IsInvalid)
{
// File/directory does not exist.
return ConstructCanonicalPath(path, cache);
}
else
{
StringBuilder outPath = new StringBuilder(Win32.MAX_PATH);
int length = Win32.GetFinalPathNameByHandle(hFile, outPath, outPath.Capacity, 0); // lgtm[cs/call-to-unmanaged-code]
if (length >= outPath.Capacity)
{
// Path length exceeded MAX_PATH.
// Possible if target has a long path.
outPath = new StringBuilder(length + 1);
length = Win32.GetFinalPathNameByHandle(hFile, outPath, outPath.Capacity, 0); // lgtm[cs/call-to-unmanaged-code]
}
const int PREAMBLE = 4; // outPath always starts \\?\
if (length <= PREAMBLE)
{
// Failed. GetFinalPathNameByHandle() failed somehow.
return ConstructCanonicalPath(path, cache);
}
string result = outPath.ToString(PREAMBLE, length - PREAMBLE); // Trim off leading \\?\
return result.StartsWith("UNC") ?
@"\" + result.Substring(3) :
result;
}
}
}
}
///
/// Determine file case by querying directory contents.
/// Preserves symlinks.
///
class QueryDirectoryStrategy : PathStrategy
{
public override string GetCanonicalPath(string path, IPathCache cache)
{
DirectoryInfo parent = Directory.GetParent(path);
if (parent != null)
{
var name = Path.GetFileName(path);
var parentPath = cache.GetCanonicalPath(parent.FullName);
try
{
string[] entries = Directory.GetFileSystemEntries(parentPath, name);
return entries.Length == 1 ?
entries[0] :
Path.Combine(parentPath, name);
}
catch // lgtm[cs/catch-of-all-exceptions]
{
// IO error or security error querying directory.
return Path.Combine(parentPath, name);
}
}
else
{
// We are at a root of the filesystem.
// Convert drive letters, UNC paths etc. to uppercase.
// On UNIX, this should be "/" or "".
return path.ToUpperInvariant();
}
}
}
///
/// Uses Mono.Unix.UnixPath to resolve symlinks.
/// Not available on Windows.
///
class PosixSymlinkStrategy : PathStrategy
{
public PosixSymlinkStrategy()
{
GetRealPath("."); // Test that it works
}
string GetRealPath(string path)
{
path = UnixPath.GetFullPath(path);
return UnixPath.GetCompleteRealPath(path);
}
public override string GetCanonicalPath(string path, IPathCache cache)
{
try
{
return GetRealPath(path);
}
catch // lgtm[cs/catch-of-all-exceptions]
{
// File does not exist
return ConstructCanonicalPath(path, cache);
}
}
}
///
/// Class which computes canonical paths.
/// Contains a simple thread-safe cache of files and directories.
///
public class CanonicalPathCache : IPathCache
{
///
/// The maximum number of items in the cache.
///
readonly int maxCapacity;
///
/// How to handle symlinks.
///
public enum Symlinks
{
Follow,
Preserve
}
///
/// Algorithm for computing the canonical path.
///
readonly PathStrategy pathStrategy;
///
/// Create cache with a given capacity.
///
/// The algorithm for determining the canonical path.
/// The size of the cache.
public CanonicalPathCache(int maxCapacity, PathStrategy pathStrategy)
{
if (maxCapacity <= 0)
throw new ArgumentOutOfRangeException("Invalid cache size specified");
this.maxCapacity = maxCapacity;
this.pathStrategy = pathStrategy;
}
///
/// Create a CanonicalPathCache.
///
///
///
/// Creates the appropriate PathStrategy object which encapsulates
/// the correct algorithm. Falls back to different implementations
/// depending on platform.
///
///
/// Size of the cache.
/// Policy for following symlinks.
/// A new CanonicalPathCache.
public static CanonicalPathCache Create(ILogger logger, int maxCapacity)
{
var preserveSymlinks =
Environment.GetEnvironmentVariable("CODEQL_PRESERVE_SYMLINKS") == "true" ||
Environment.GetEnvironmentVariable("SEMMLE_PRESERVE_SYMLINKS") == "true";
return Create(logger, maxCapacity, preserveSymlinks ? CanonicalPathCache.Symlinks.Preserve : CanonicalPathCache.Symlinks.Follow);
}
///
/// Create a CanonicalPathCache.
///
///
///
/// Creates the appropriate PathStrategy object which encapsulates
/// the correct algorithm. Falls back to different implementations
/// depending on platform.
///
///
/// Size of the cache.
/// Policy for following symlinks.
/// A new CanonicalPathCache.
public static CanonicalPathCache Create(ILogger logger, int maxCapacity, Symlinks symlinks)
{
PathStrategy pathStrategy;
switch (symlinks)
{
case Symlinks.Follow:
try
{
pathStrategy = Win32.IsWindows() ?
(PathStrategy)new GetFinalPathNameByHandleStrategy() :
(PathStrategy)new PosixSymlinkStrategy();
}
catch // lgtm[cs/catch-of-all-exceptions]
{
// Failed to late-bind a suitable library.
logger.Log(Severity.Warning, "Preserving symlinks in canonical paths");
pathStrategy = new QueryDirectoryStrategy();
}
break;
case Symlinks.Preserve:
pathStrategy = new QueryDirectoryStrategy();
break;
default:
throw new ArgumentOutOfRangeException("Invalid symlinks option");
}
return new CanonicalPathCache(maxCapacity, pathStrategy);
}
///
/// Map of path to canonical path.
///
readonly IDictionary cache = new Dictionary();
///
/// Used to evict random cache items when the cache is full.
///
readonly Random random = new Random();
///
/// The current number of items in the cache.
///
public int CacheSize
{
get
{
lock (cache)
return cache.Count;
}
}
///
/// Adds a path to the cache.
/// Removes items from cache as needed.
///
/// The path.
/// The canonical form of path.
void AddToCache(string path, string canonical)
{
if (cache.Count >= maxCapacity)
{
/* A simple strategy for limiting the cache size, given that
* C# doesn't have a convenient dictionary+list data structure.
*/
cache.Remove(cache.ElementAt(random.Next(maxCapacity)));
}
cache[path] = canonical;
}
///
/// Retrieve the canonical path for a given path.
/// Caches the result.
///
/// The path.
/// The canonical path.
public string GetCanonicalPath(string path)
{
lock (cache)
{
if (!cache.TryGetValue(path, out var canonicalPath))
{
canonicalPath = pathStrategy.GetCanonicalPath(path, this);
AddToCache(path, canonicalPath);
}
return canonicalPath;
}
}
}
}