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. /// readonly 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, PathTransformer.ITransformedPath 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, PathTransformer.ITransformedPath 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(PathTransformer.ITransformedPath 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(PathTransformer.ITransformedPath 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); blocks = new List(); 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(PathTransformer.ITransformedPath path) => LookupProjectOrNull(path) != null; void ReadLayoutFile(string layout) { try { var lines = File.ReadAllLines(layout); int i = 0; while (!lines[i].StartsWith("#")) i++; while (i < lines.Length) { LayoutBlock block = new LayoutBlock(lines, ref 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 { private readonly List filePatterns = new List(); public readonly 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 LayoutBlock(string[] lines, ref int i) { // first line: #name i++; string? TRAP_FOLDER = ReadVariable("TRAP_FOLDER", lines[i++]); // Don't care about ODASA_DB. ReadVariable("ODASA_DB", lines[i++]); string? SOURCE_ARCHIVE = ReadVariable("SOURCE_ARCHIVE", lines[i++]); Directories = new 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("#")) { filePatterns.Add(new FilePattern(lines[i++])); } } public bool Matches(PathTransformer.ITransformedPath path) => FilePattern.Matches(filePatterns, path.Value, out var _); } }