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.CompressionMode trapCompression) => TrapWriter.TrapPath(logger, TRAP_FOLDER, srcFile, trapCompression); /// /// 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, TrapWriter.CompressionMode trapCompression) => new TrapWriter(logger, srcFile, TRAP_FOLDER, SOURCE_ARCHIVE, discardDuplicates, trapCompression); } 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("CODEQL_EXTRACTOR_CSHARP_TRAP_DIR") ?? Environment.GetEnvironmentVariable("TRAP_FOLDER"), Environment.GetEnvironmentVariable("CODEQL_EXTRACTOR_CSHARP_SOURCE_ARCHIVE_DIR") ?? 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; } } }