using Semmle.Util.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Semmle.Extraction
{
///
/// An extractor layout file.
/// Represents the layout of projects into trap folders and source archives.
///
public sealed class Layout
{
///
/// Exception thrown when the layout file is invalid.
///
public class InvalidLayoutException : Exception
{
public InvalidLayoutException(string file, string message) :
base("ODASA_CSHARP_LAYOUT " + file + " " + message)
{
}
}
///
/// List of blocks in the layout file.
///
List blocks;
///
/// A subproject in the layout file.
///
public class SubProject
{
///
/// The trap folder, or null for current directory.
///
public readonly string TRAP_FOLDER;
///
/// The source archive, or null to skip.
///
public readonly string SOURCE_ARCHIVE;
public SubProject(string traps, string archive)
{
TRAP_FOLDER = traps;
SOURCE_ARCHIVE = archive;
}
///
/// Gets the name of the trap file for a given source/assembly file.
///
/// The source file.
/// The full filepath of the trap file.
public string GetTrapPath(ILogger logger, string srcFile) => TrapWriter.TrapPath(logger, TRAP_FOLDER, srcFile);
///
/// Creates a trap writer for a given source/assembly file.
///
/// The source file.
/// A newly created TrapWriter.
public TrapWriter CreateTrapWriter(ILogger logger, string srcFile, bool discardDuplicates) => new TrapWriter(logger, srcFile, TRAP_FOLDER, SOURCE_ARCHIVE, discardDuplicates);
}
readonly SubProject DefaultProject;
///
/// Finds the suitable directories for a given source file.
/// Returns null if not included in the layout.
///
/// The file to look up.
/// The relevant subproject, or null if not found.
public SubProject LookupProjectOrNull(string sourceFile)
{
if (!useLayoutFile) return DefaultProject;
return blocks.
Where(block => block.Matches(sourceFile)).
Select(block => block.Directories).
FirstOrDefault();
}
///
/// Finds the suitable directories for a given source file.
/// Returns the default project if not included in the layout.
///
/// The file to look up.
/// The relevant subproject, or DefaultProject if not found.
public SubProject LookupProjectOrDefault(string sourceFile)
{
return LookupProjectOrNull(sourceFile) ?? DefaultProject;
}
readonly bool useLayoutFile;
///
/// Default constructor reads parameters from the environment.
///
public Layout() : this(
Environment.GetEnvironmentVariable("TRAP_FOLDER"),
Environment.GetEnvironmentVariable("SOURCE_ARCHIVE"),
Environment.GetEnvironmentVariable("ODASA_CSHARP_LAYOUT"))
{
}
///
/// Creates the project layout. Reads the layout file if specified.
///
/// Directory for trap files, or null to use layout/current directory.
/// Directory for source archive, or null for layout/no archive.
/// Path of layout file, or null for no layout.
/// Failed to read layout file.
public Layout(string traps, string archive, string layout)
{
useLayoutFile = string.IsNullOrEmpty(traps) && !string.IsNullOrEmpty(layout);
if (useLayoutFile)
{
ReadLayoutFile(layout);
DefaultProject = blocks[0].Directories;
}
else
{
DefaultProject = new SubProject(traps, archive);
}
}
///
/// Is the source file included in the layout?
///
/// The absolute path of the file to query.
/// True iff there is no layout file or the layout file specifies the file.
public bool FileInLayout(string path) => LookupProjectOrNull(path) != null;
void ReadLayoutFile(string layout)
{
try
{
var lines = File.ReadAllLines(layout);
blocks = new List();
int i = 0;
while (!lines[i].StartsWith("#"))
i++;
while (i < lines.Length)
{
LayoutBlock block = new LayoutBlock();
i = block.Read(lines, i);
blocks.Add(block);
}
if (blocks.Count == 0)
throw new InvalidLayoutException(layout, "contains no blocks");
}
catch (IOException ex)
{
throw new InvalidLayoutException(layout, ex.Message);
}
catch (IndexOutOfRangeException)
{
throw new InvalidLayoutException(layout, "is invalid");
}
}
}
sealed class LayoutBlock
{
struct Condition
{
private readonly bool include;
private readonly string prefix;
public bool Include => include;
public string Prefix => prefix;
public Condition(string line)
{
include = false;
if (line.StartsWith("-"))
line = line.Substring(1);
else
include = true;
prefix = Normalise(line.Trim());
}
static public string Normalise(string path)
{
path = Path.GetFullPath(path);
return path.Replace('\\', '/');
}
}
private readonly List conditions = new List();
public Layout.SubProject Directories;
string ReadVariable(string name, string line)
{
string prefix = name + "=";
if (!line.StartsWith(prefix))
return null;
return line.Substring(prefix.Length).Trim();
}
public int Read(string[] lines, int start)
{
// first line: #name
int i = start + 1;
var TRAP_FOLDER = ReadVariable("TRAP_FOLDER", lines[i++]);
// Don't care about ODASA_DB.
ReadVariable("ODASA_DB", lines[i++]);
var SOURCE_ARCHIVE = ReadVariable("SOURCE_ARCHIVE", lines[i++]);
Directories = new Extraction.Layout.SubProject(TRAP_FOLDER, SOURCE_ARCHIVE);
// Don't care about ODASA_BUILD_ERROR_DIR.
ReadVariable("ODASA_BUILD_ERROR_DIR", lines[i++]);
while (i < lines.Length && !lines[i].StartsWith("#"))
{
conditions.Add(new Condition(lines[i++]));
}
return i;
}
public bool Matches(string path)
{
bool matches = false;
path = Condition.Normalise(path);
foreach (Condition condition in conditions)
{
if (condition.Include)
matches |= path.StartsWith(condition.Prefix);
else
matches &= !path.StartsWith(condition.Prefix);
}
return matches;
}
}
}