using System;
using System.Collections.Generic;
using System.IO;
using System.Diagnostics.CodeAnalysis;
using Semmle.Util;
using Semmle.Util.Logging;
namespace Semmle.Extraction
{
///
/// A class for interpreting path transformers specified using the environment
/// variable `CODEQL_PATH_TRANSFORMER`.
///
public sealed class PathTransformer
{
public class InvalidPathTransformerException : Exception
{
public InvalidPathTransformerException(string message) :
base($"Invalid path transformer specification: {message}")
{ }
}
///
/// A transformed path.
///
public interface ITransformedPath
{
string Value { get; }
string Extension { get; }
string NameWithoutExtension { get; }
ITransformedPath? ParentDirectory { get; }
ITransformedPath WithSuffix(string suffix);
string DatabaseId { get; }
///
/// Gets the name of the trap file for this file.
///
/// The full filepath of the trap file.
public string GetTrapPath(ILogger logger, TrapWriter.CompressionMode trapCompression) =>
TrapWriter.TrapPath(logger, Environment.GetEnvironmentVariable("CODEQL_EXTRACTOR_CSHARP_TRAP_DIR"), this, trapCompression);
///
/// Creates a trap writer for this file.
///
/// A newly created TrapWriter.
public TrapWriter CreateTrapWriter(ILogger logger, TrapWriter.CompressionMode trapCompression, bool discardDuplicates) =>
new(logger, this, Environment.GetEnvironmentVariable("CODEQL_EXTRACTOR_CSHARP_TRAP_DIR"), Environment.GetEnvironmentVariable("CODEQL_EXTRACTOR_CSHARP_SOURCE_ARCHIVE_DIR"), trapCompression, discardDuplicates);
}
private struct TransformedPath : ITransformedPath
{
public TransformedPath(string value) { this.value = value; }
private readonly string value;
public string Value => value;
public string Extension
{
get
{
var extension = Path.GetExtension(value);
return string.IsNullOrEmpty(extension) ? "" : extension.Substring(1);
}
}
public string NameWithoutExtension => Path.GetFileNameWithoutExtension(value);
public ITransformedPath? ParentDirectory
{
get
{
var dir = Path.GetDirectoryName(value);
if (dir is null)
return null;
var isWindowsDriveLetter = dir.Length == 2 && char.IsLetter(dir[0]) && dir[1] == ':';
if (isWindowsDriveLetter)
return null;
return new TransformedPath(FileUtils.ConvertToUnix(dir));
}
}
public ITransformedPath WithSuffix(string suffix) => new TransformedPath(value + suffix);
public string DatabaseId
{
get
{
var ret = value;
if (ret.Length >= 2 && ret[1] == ':' && Char.IsLower(ret[0]))
ret = Char.ToUpper(ret[0]) + "_" + ret.Substring(2);
return ret.Replace('\\', '/').Replace(":", "_");
}
}
public override int GetHashCode() => 11 * value.GetHashCode();
public override bool Equals(object? obj) => obj is TransformedPath tp && tp.value == value;
public override string ToString() => value;
}
private readonly Func transform;
///
/// Returns the path obtained by transforming `path`.
///
public ITransformedPath Transform(string path) => new TransformedPath(transform(path));
///
/// Default constructor reads parameters from the environment.
///
public PathTransformer(IPathCache pathCache) :
this(pathCache, Environment.GetEnvironmentVariable("CODEQL_PATH_TRANSFORMER") is string file ? File.ReadAllLines(file) : null)
{
}
///
/// Creates a path transformer based on the specification in `lines`.
/// Throws `InvalidPathTransformerException` for invalid specifications.
///
public PathTransformer(IPathCache pathCache, string[]? lines)
{
if (lines is null)
{
transform = path => FileUtils.ConvertToUnix(pathCache.GetCanonicalPath(path));
return;
}
var sections = ParsePathTransformerSpec(lines);
transform = path =>
{
path = FileUtils.ConvertToUnix(pathCache.GetCanonicalPath(path));
foreach (var section in sections)
{
if (section.Matches(path, out var transformed))
return transformed;
}
return path;
};
}
private static IEnumerable ParsePathTransformerSpec(string[] lines)
{
var sections = new List();
try
{
var i = 0;
while (i < lines.Length && !lines[i].StartsWith("#"))
i++;
while (i < lines.Length)
{
var section = new TransformerSection(lines, ref i);
sections.Add(section);
}
if (sections.Count == 0)
throw new InvalidPathTransformerException("contains no sections.");
}
catch (InvalidFilePatternException ex)
{
throw new InvalidPathTransformerException(ex.Message);
}
return sections;
}
}
internal sealed class TransformerSection
{
private readonly string name;
private readonly List filePatterns = new List();
public TransformerSection(string[] lines, ref int i)
{
name = lines[i++].Substring(1); // skip the '#'
for (; i < lines.Length && !lines[i].StartsWith("#"); i++)
{
var line = lines[i];
if (!string.IsNullOrWhiteSpace(line))
filePatterns.Add(new FilePattern(line));
}
}
public bool Matches(string path, [NotNullWhen(true)] out string? transformed)
{
if (FilePattern.Matches(filePatterns, path, out var suffix))
{
transformed = FileUtils.ConvertToUnix(name) + suffix;
return true;
}
transformed = null;
return false;
}
}
}