Merge pull request #4315 from hvitved/merge-rc-1.25

Merge rc/1.25 into main
This commit is contained in:
Tom Hvitved 2020-09-24 08:27:01 +02:00 коммит произвёл GitHub
Родитель 9b14a70eef b6d93ae81d
Коммит 7970fef3e4
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
22 изменённых файлов: 643 добавлений и 301 удалений

Просмотреть файл

@ -4,20 +4,26 @@ The following changes in version 1.25 affect Java analysis in all applications.
## General improvements
## New queries
| **Query** | **Tags** | **Purpose** |
|-----------------------------|-----------|--------------------------------------------------------------------|
The Java autobuilder has been improved to detect more Gradle Java versions.
## Changes to existing queries
| **Query** | **Expected impact** | **Change** |
|------------------------------|------------------------|-----------------------------------|
| Hard-coded credential in API call (`java/hardcoded-credential-api-call`) | More results | The query now recognizes the `BasicAWSCredentials` class of the Amazon client SDK library with hardcoded access key/secret key. |
| Deserialization of user-controlled data (`java/unsafe-deserialization`) | Fewer false positive results | The query no longer reports results using `org.apache.commons.io.serialization.ValidatingObjectInputStream`. |
| Use of a broken or risky cryptographic algorithm (`java/weak-cryptographic-algorithm`) | More results | The query now recognizes the `MessageDigest.getInstance` method. |
| Use of a potentially broken or risky cryptographic algorithm (`java/potentially-weak-cryptographic-algorithm`) | More results | The query now recognizes the `MessageDigest.getInstance` method. |
| Reading from a world writable file (`java/world-writable-file-read`) | More results | The query now recognizes more JDK file operations. |
## Changes to libraries
* The data-flow library has been improved with more taint flow modeling for the
Collections framework and other classes of the JDK. This affects all security
queries using data flow and can yield additional results.
* The data-flow library has been improved with more taint flow modeling for the
Spring framework. This affects all security queries using data flow and can
yield additional results on project that rely on the Spring framework.
* The data-flow library has been improved, which affects most security queries by potentially
adding more results. Flow through methods now takes nested field reads/writes into account.
For example, the library is able to track flow from `"taint"` to `sink()` via the method
@ -39,3 +45,5 @@ The following changes in version 1.25 affect Java analysis in all applications.
}
}
```
* The library has been extended with more support for Java 14 features
(`switch` expressions and pattern-matching for `instanceof`).

Просмотреть файл

@ -1,22 +1,9 @@
# Improvements to Python analysis
The following changes in version 1.25 affect Python analysis in all applications.
## General improvements
## New queries
| **Query** | **Tags** | **Purpose** |
|-----------------------------|-----------|--------------------------------------------------------------------|
## Changes to existing queries
| **Query** | **Expected impact** | **Change** |
|----------------------------|------------------------|------------------------------------------------------------------|
## Changes to libraries
* Importing `semmle.python.web.HttpRequest` will no longer import `UntrustedStringKind` transitively. `UntrustedStringKind` is the most commonly used non-abstract subclass of `ExternalStringKind`. If not imported (by one mean or another), taint-tracking queries that concern `ExternalStringKind` will not produce any results. Please ensure such queries contain an explicit import (`import semmle.python.security.strings.Untrusted`).
* Added model of taint sources for HTTP servers using `http.server`.
* Added taint modeling of routed parameters in Flask.
* Improved modeling of built-in methods on strings for taint tracking.
* Improved classification of test files.
* New class `BoundMethodValue` represents a bound method during runtime.
* The query `py/command-line-injection` now recognizes command execution with the `fabric` and `invoke` Python libraries.

Просмотреть файл

@ -43,7 +43,7 @@ namespace Semmle.Extraction.CIL
namespaceFactory = new CachedFunction<StringHandle, Entities.Namespace>(n => CreateNamespace(mdReader.GetString(n)));
namespaceDefinitionFactory = new CachedFunction<NamespaceDefinitionHandle, Entities.Namespace>(CreateNamespace);
sourceFiles = new CachedFunction<PDB.ISourceFile, Entities.PdbSourceFile>(path => new Entities.PdbSourceFile(this, path));
folders = new CachedFunction<string, Entities.Folder>(path => new Entities.Folder(this, path));
folders = new CachedFunction<PathTransformer.ITransformedPath, Entities.Folder>(path => new Entities.Folder(this, path));
sourceLocations = new CachedFunction<PDB.Location, Entities.PdbSourceLocation>(location => new Entities.PdbSourceLocation(this, location));
defaultGenericContext = new EmptyContext(this);

Просмотреть файл

@ -5,6 +5,7 @@ using Semmle.Util.Logging;
using System;
using Semmle.Extraction.Entities;
using System.IO;
using Semmle.Util;
namespace Semmle.Extraction.CIL.Entities
{
@ -134,9 +135,12 @@ namespace Semmle.Extraction.CIL.Entities
extracted = false;
try
{
var extractor = new Extractor(false, assemblyPath, logger);
var project = layout.LookupProjectOrDefault(assemblyPath);
using (var trapWriter = project.CreateTrapWriter(logger, assemblyPath + ".cil", true, trapCompression))
var canonicalPathCache = CanonicalPathCache.Create(logger, 1000);
var pathTransformer = new PathTransformer(canonicalPathCache);
var extractor = new Extractor(false, assemblyPath, logger, pathTransformer);
var transformedAssemblyPath = pathTransformer.Transform(assemblyPath);
var project = layout.LookupProjectOrDefault(transformedAssemblyPath);
using (var trapWriter = project.CreateTrapWriter(logger, transformedAssemblyPath.WithSuffix(".cil"), true, trapCompression))
{
trapFile = trapWriter.TrapFile;
if (nocache || !System.IO.File.Exists(trapFile))

Просмотреть файл

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.IO;
namespace Semmle.Extraction.CIL.Entities
@ -13,37 +13,38 @@ namespace Semmle.Extraction.CIL.Entities
public class File : LabelledEntity, IFile
{
protected readonly string path;
protected readonly string OriginalPath;
protected readonly PathTransformer.ITransformedPath TransformedPath;
public File(Context cx, string path) : base(cx)
{
this.path = Semmle.Extraction.Entities.File.PathAsDatabaseString(path);
this.OriginalPath = path;
TransformedPath = cx.cx.Extractor.PathTransformer.Transform(OriginalPath);
}
public override void WriteId(TextWriter trapFile)
{
trapFile.Write(Semmle.Extraction.Entities.File.PathAsDatabaseId(path));
trapFile.Write(TransformedPath.DatabaseId);
}
public override bool Equals(object? obj)
{
return GetType() == obj?.GetType() && path == ((File)obj).path;
return GetType() == obj?.GetType() && OriginalPath == ((File)obj).OriginalPath;
}
public override int GetHashCode() => 11 * path.GetHashCode();
public override int GetHashCode() => 11 * OriginalPath.GetHashCode();
public override IEnumerable<IExtractionProduct> Contents
{
get
{
var directoryName = System.IO.Path.GetDirectoryName(path);
if (directoryName is null)
throw new InternalError($"Directory name for path '{path}' is null.");
var parent = cx.CreateFolder(directoryName);
yield return parent;
yield return Tuples.containerparent(parent, this);
yield return Tuples.files(this, path, System.IO.Path.GetFileNameWithoutExtension(path), System.IO.Path.GetExtension(path).Substring(1));
if (TransformedPath.ParentDirectory is PathTransformer.ITransformedPath dir)
{
var parent = cx.CreateFolder(dir);
yield return parent;
yield return Tuples.containerparent(parent, this);
}
yield return Tuples.files(this, TransformedPath.Value, TransformedPath.NameWithoutExtension, TransformedPath.Extension);
}
}
@ -69,9 +70,9 @@ namespace Semmle.Extraction.CIL.Entities
var text = file.Contents;
if (text == null)
cx.cx.Extractor.Logger.Log(Util.Logging.Severity.Warning, string.Format("PDB source file {0} could not be found", path));
cx.cx.Extractor.Logger.Log(Util.Logging.Severity.Warning, string.Format("PDB source file {0} could not be found", OriginalPath));
else
cx.cx.TrapWriter.Archive(path, text);
cx.cx.TrapWriter.Archive(TransformedPath, text);
yield return Tuples.file_extraction_mode(this, 2);
}

Просмотреть файл

@ -9,16 +9,16 @@ namespace Semmle.Extraction.CIL.Entities
public sealed class Folder : LabelledEntity, IFolder
{
readonly string path;
readonly PathTransformer.ITransformedPath TransformedPath;
public Folder(Context cx, string path) : base(cx)
public Folder(Context cx, PathTransformer.ITransformedPath path) : base(cx)
{
this.path = path;
this.TransformedPath = path;
}
public override void WriteId(TextWriter trapFile)
{
trapFile.Write(Semmle.Extraction.Entities.File.PathAsDatabaseId(path));
trapFile.Write(TransformedPath.DatabaseId);
}
public override string IdSuffix => ";folder";
@ -27,25 +27,21 @@ namespace Semmle.Extraction.CIL.Entities
{
get
{
// On Posix, we could get a Windows directory of the form "C:"
bool windowsDriveLetter = path.Length == 2 && char.IsLetter(path[0]) && path[1] == ':';
var parent = Path.GetDirectoryName(path);
if (parent != null && !windowsDriveLetter)
if (TransformedPath.ParentDirectory is PathTransformer.ITransformedPath parent)
{
var parentFolder = cx.CreateFolder(parent);
yield return parentFolder;
yield return Tuples.containerparent(parentFolder, this);
}
yield return Tuples.folders(this, Semmle.Extraction.Entities.File.PathAsDatabaseString(path), Path.GetFileName(path));
yield return Tuples.folders(this, TransformedPath.Value, TransformedPath.NameWithoutExtension);
}
}
public override bool Equals(object? obj)
{
return obj is Folder folder && path == folder.path;
return obj is Folder folder && TransformedPath == folder.TransformedPath;
}
public override int GetHashCode() => path.GetHashCode();
public override int GetHashCode() => TransformedPath.GetHashCode();
}
}

Просмотреть файл

@ -201,7 +201,7 @@ namespace Semmle.Extraction.CIL
#region Locations
readonly CachedFunction<PDB.ISourceFile, PdbSourceFile> sourceFiles;
readonly CachedFunction<string, Folder> folders;
readonly CachedFunction<PathTransformer.ITransformedPath, Folder> folders;
readonly CachedFunction<PDB.Location, PdbSourceLocation> sourceLocations;
/// <summary>
@ -216,7 +216,7 @@ namespace Semmle.Extraction.CIL
/// </summary>
/// <param name="path">The path of the folder.</param>
/// <returns>A folder entity.</returns>
public Folder CreateFolder(string path) => folders[path];
public Folder CreateFolder(PathTransformer.ITransformedPath path) => folders[path];
/// <summary>
/// Creates a source location.

Просмотреть файл

@ -27,13 +27,16 @@ namespace Semmle.Extraction.CSharp
public readonly bool AddAssemblyTrapPrefix;
public Analyser(IProgressMonitor pm, ILogger logger, bool addAssemblyTrapPrefix)
public readonly PathTransformer PathTransformer;
public Analyser(IProgressMonitor pm, ILogger logger, bool addAssemblyTrapPrefix, PathTransformer pathTransformer)
{
Logger = logger;
AddAssemblyTrapPrefix = addAssemblyTrapPrefix;
Logger.Log(Severity.Info, "EXTRACTION STARTING at {0}", DateTime.Now);
stopWatch.Start();
progressMonitor = pm;
PathTransformer = pathTransformer;
}
CSharpCompilation compilation;
@ -67,7 +70,7 @@ namespace Semmle.Extraction.CSharp
layout = new Layout();
this.options = options;
this.compilation = compilation;
extractor = new Extraction.Extractor(false, GetOutputName(compilation, commandLineArguments), Logger);
extractor = new Extraction.Extractor(false, GetOutputName(compilation, commandLineArguments), Logger, PathTransformer);
LogDiagnostics();
SetReferencePaths();
@ -117,7 +120,7 @@ namespace Semmle.Extraction.CSharp
{
compilation = compilationIn;
layout = new Layout();
extractor = new Extraction.Extractor(true, null, Logger);
extractor = new Extraction.Extractor(true, null, Logger, PathTransformer);
this.options = options;
LogExtractorInfo(Extraction.Extractor.Version);
SetReferencePaths();
@ -230,9 +233,10 @@ namespace Semmle.Extraction.CSharp
try
{
var assemblyPath = extractor.OutputPath;
var transformedAssemblyPath = PathTransformer.Transform(assemblyPath);
var assembly = compilation.Assembly;
var projectLayout = layout.LookupProjectOrDefault(assemblyPath);
var trapWriter = projectLayout.CreateTrapWriter(Logger, assemblyPath, true, options.TrapCompression);
var projectLayout = layout.LookupProjectOrDefault(transformedAssemblyPath);
var trapWriter = projectLayout.CreateTrapWriter(Logger, transformedAssemblyPath, true, options.TrapCompression);
compilationTrapFile = trapWriter; // Dispose later
var cx = extractor.CreateContext(compilation.Clone(), trapWriter, new AssemblyScope(assembly, assemblyPath, true), AddAssemblyTrapPrefix);
@ -260,8 +264,9 @@ namespace Semmle.Extraction.CSharp
stopwatch.Start();
var assemblyPath = r.FilePath;
var projectLayout = layout.LookupProjectOrDefault(assemblyPath);
using (var trapWriter = projectLayout.CreateTrapWriter(Logger, assemblyPath, true, options.TrapCompression))
var transformedAssemblyPath = PathTransformer.Transform(assemblyPath);
var projectLayout = layout.LookupProjectOrDefault(transformedAssemblyPath);
using (var trapWriter = projectLayout.CreateTrapWriter(Logger, transformedAssemblyPath, true, options.TrapCompression))
{
var skipExtraction = options.Cache && File.Exists(trapWriter.TrapFile);
@ -360,16 +365,17 @@ namespace Semmle.Extraction.CSharp
var stopwatch = new Stopwatch();
stopwatch.Start();
var sourcePath = tree.FilePath;
var transformedSourcePath = PathTransformer.Transform(sourcePath);
var projectLayout = layout.LookupProjectOrNull(sourcePath);
var projectLayout = layout.LookupProjectOrNull(transformedSourcePath);
bool excluded = projectLayout == null;
string trapPath = excluded ? "" : projectLayout.GetTrapPath(Logger, sourcePath, options.TrapCompression);
string trapPath = excluded ? "" : projectLayout.GetTrapPath(Logger, transformedSourcePath, options.TrapCompression);
bool upToDate = false;
if (!excluded)
{
// compilation.Clone() is used to allow symbols to be garbage collected.
using (var trapWriter = projectLayout.CreateTrapWriter(Logger, sourcePath, false, options.TrapCompression))
using (var trapWriter = projectLayout.CreateTrapWriter(Logger, transformedSourcePath, false, options.TrapCompression))
{
upToDate = options.Fast && FileIsUpToDate(sourcePath, trapWriter.TrapFile);

Просмотреть файл

@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Semmle.Util;
namespace Semmle.Extraction.CSharp.Entities
{
@ -22,7 +23,7 @@ namespace Semmle.Extraction.CSharp.Entities
{
Extraction.Entities.Assembly.CreateOutputAssembly(cx);
trapFile.compilations(this, Extraction.Entities.File.PathAsDatabaseString(cwd));
trapFile.compilations(this, FileUtils.ConvertToUnix(cwd));
// Arguments
int index = 0;

Просмотреть файл

@ -76,16 +76,16 @@ namespace Semmle.Extraction.CSharp
return ExitCode.Ok;
}
using (var analyser = new Analyser(new LogProgressMonitor(logger), logger, commandLineArguments.AssemblySensitiveTrap))
var canonicalPathCache = CanonicalPathCache.Create(logger, 1000);
var pathTransformer = new PathTransformer(canonicalPathCache);
using (var analyser = new Analyser(new LogProgressMonitor(logger), logger, commandLineArguments.AssemblySensitiveTrap, pathTransformer))
using (var references = new BlockingCollection<MetadataReference>())
{
try
{
var compilerVersion = new CompilerVersion(commandLineArguments);
bool preserveSymlinks = Environment.GetEnvironmentVariable("SEMMLE_PRESERVE_SYMLINKS") == "true";
var canonicalPathCache = CanonicalPathCache.Create(logger, 1000, preserveSymlinks ? CanonicalPathCache.Symlinks.Preserve : CanonicalPathCache.Symlinks.Follow);
if (compilerVersion.SkipExtraction)
{
logger.Log(Severity.Warning, " Unrecognized compiler '{0}' because {1}", compilerVersion.SpecifiedCompiler, compilerVersion.SkipReason);
@ -318,7 +318,10 @@ namespace Semmle.Extraction.CSharp
ILogger logger,
CommonOptions options)
{
using (var analyser = new Analyser(pm, logger, false))
var canonicalPathCache = CanonicalPathCache.Create(logger, 1000);
var pathTransformer = new PathTransformer(canonicalPathCache);
using (var analyser = new Analyser(pm, logger, false, pathTransformer))
using (var references = new BlockingCollection<MetadataReference>())
{
try

Просмотреть файл

@ -0,0 +1,48 @@
using Xunit;
namespace Semmle.Extraction.Tests
{
public class FilePatternTests
{
[Fact]
public void TestRegexCompilation()
{
var fp = new FilePattern("/hadoop*");
Assert.Equal("^hadoop[^/]*.*", fp.RegexPattern);
fp = new FilePattern("**/org/apache/hadoop");
Assert.Equal("^.*/org/apache/hadoop.*", fp.RegexPattern);
fp = new FilePattern("hadoop-common/**/test// ");
Assert.Equal("^hadoop-common/.*/test(?<doubleslash>/).*", fp.RegexPattern);
fp = new FilePattern(@"-C:\agent\root\asdf//");
Assert.Equal("^C:/agent/root/asdf(?<doubleslash>/).*", fp.RegexPattern);
fp = new FilePattern(@"-C:\agent+\[root]\asdf//");
Assert.Equal(@"^C:/agent\+/\[root]/asdf(?<doubleslash>/).*", fp.RegexPattern);
}
[Fact]
public void TestMatching()
{
var fp1 = new FilePattern(@"C:\agent\root\abc//");
var fp2 = new FilePattern(@"C:\agent\root\def//ghi");
var patterns = new[] { fp1, fp2 };
var success = FilePattern.Matches(patterns, @"C:\agent\root\abc\file.cs", out var s);
Assert.True(success);
Assert.Equal("/file.cs", s);
success = FilePattern.Matches(patterns, @"C:\agent\root\def\ghi\file.cs", out s);
Assert.True(success);
Assert.Equal("/ghi/file.cs", s);
success = FilePattern.Matches(patterns, @"C:\agent\root\def\file.cs", out s);
Assert.False(success);
}
[Fact]
public void TestInvalidPatterns()
{
Assert.Throws<InvalidFilePatternException>(() => new FilePattern("/abc//def//ghi"));
Assert.Throws<InvalidFilePatternException>(() => new FilePattern("/abc**def"));
}
}
}

Просмотреть файл

@ -1,10 +1,30 @@
using System.IO;
using System.IO;
using Xunit;
using Semmle.Util.Logging;
using System.Runtime.InteropServices;
namespace Semmle.Extraction.Tests
{
struct TransformedPathStub : PathTransformer.ITransformedPath
{
readonly string value;
public TransformedPathStub(string value) => this.value = value;
public string Value => value;
public string Extension => throw new System.NotImplementedException();
public string NameWithoutExtension => throw new System.NotImplementedException();
public PathTransformer.ITransformedPath ParentDirectory => throw new System.NotImplementedException();
public string DatabaseId => throw new System.NotImplementedException();
public PathTransformer.ITransformedPath WithSuffix(string suffix)
{
throw new System.NotImplementedException();
}
}
public class Layout
{
readonly ILogger Logger = new LoggerMock();
@ -13,12 +33,12 @@ namespace Semmle.Extraction.Tests
public void TestDefaultLayout()
{
var layout = new Semmle.Extraction.Layout(null, null, null);
var project = layout.LookupProjectOrNull("foo.cs");
var project = layout.LookupProjectOrNull(new TransformedPathStub("foo.cs"));
Assert.NotNull(project);
// All files are mapped when there's no layout file.
Assert.True(layout.FileInLayout("foo.cs"));
Assert.True(layout.FileInLayout(new TransformedPathStub("foo.cs")));
// Test trap filename
var tmpDir = Path.GetTempPath();
@ -30,13 +50,13 @@ namespace Semmle.Extraction.Tests
Assert.NotEqual(Directory.GetCurrentDirectory(), tmpDir);
return;
}
var f1 = project!.GetTrapPath(Logger, "foo.cs", TrapWriter.CompressionMode.Gzip);
var g1 = TrapWriter.NestPaths(Logger, tmpDir, "foo.cs.trap.gz", TrapWriter.InnerPathComputation.ABSOLUTE);
var f1 = project!.GetTrapPath(Logger, new TransformedPathStub("foo.cs"), TrapWriter.CompressionMode.Gzip);
var g1 = TrapWriter.NestPaths(Logger, tmpDir, "foo.cs.trap.gz");
Assert.Equal(f1, g1);
// Test trap file generation
var trapwriterFilename = project.GetTrapPath(Logger, "foo.cs", TrapWriter.CompressionMode.Gzip);
using (var trapwriter = project.CreateTrapWriter(Logger, "foo.cs", false, TrapWriter.CompressionMode.Gzip))
var trapwriterFilename = project.GetTrapPath(Logger, new TransformedPathStub("foo.cs"), TrapWriter.CompressionMode.Gzip);
using (var trapwriter = project.CreateTrapWriter(Logger, new TransformedPathStub("foo.cs"), false, TrapWriter.CompressionMode.Gzip))
{
trapwriter.Emit("1=*");
Assert.False(File.Exists(trapwriterFilename));
@ -65,25 +85,24 @@ namespace Semmle.Extraction.Tests
var layout = new Semmle.Extraction.Layout(null, null, "layout.txt");
// Test general pattern matching
Assert.True(layout.FileInLayout("bar.cs"));
Assert.False(layout.FileInLayout("foo.cs"));
Assert.False(layout.FileInLayout("goo.cs"));
Assert.False(layout.FileInLayout("excluded/bar.cs"));
Assert.True(layout.FileInLayout("excluded/foo.cs"));
Assert.True(layout.FileInLayout("included/foo.cs"));
Assert.True(layout.FileInLayout(new TransformedPathStub("bar.cs")));
Assert.False(layout.FileInLayout(new TransformedPathStub("foo.cs")));
Assert.False(layout.FileInLayout(new TransformedPathStub("goo.cs")));
Assert.False(layout.FileInLayout(new TransformedPathStub("excluded/bar.cs")));
Assert.True(layout.FileInLayout(new TransformedPathStub("excluded/foo.cs")));
Assert.True(layout.FileInLayout(new TransformedPathStub("included/foo.cs")));
// Test the trap file
var project = layout.LookupProjectOrNull("bar.cs");
var project = layout.LookupProjectOrNull(new TransformedPathStub("bar.cs"));
Assert.NotNull(project);
var trapwriterFilename = project!.GetTrapPath(Logger, "bar.cs", TrapWriter.CompressionMode.Gzip);
Assert.Equal(TrapWriter.NestPaths(Logger, Path.GetFullPath("snapshot\\trap"), "bar.cs.trap.gz", TrapWriter.InnerPathComputation.ABSOLUTE),
var trapwriterFilename = project!.GetTrapPath(Logger, new TransformedPathStub("bar.cs"), TrapWriter.CompressionMode.Gzip);
Assert.Equal(TrapWriter.NestPaths(Logger, Path.GetFullPath("snapshot\\trap"), "bar.cs.trap.gz"),
trapwriterFilename);
// Test the source archive
var trapWriter = project.CreateTrapWriter(Logger, "bar.cs", false, TrapWriter.CompressionMode.Gzip);
trapWriter.Archive("layout.txt", System.Text.Encoding.ASCII);
var writtenFile = TrapWriter.NestPaths(Logger, Path.GetFullPath("snapshot\\archive"), "layout.txt", TrapWriter.InnerPathComputation.ABSOLUTE);
var trapWriter = project.CreateTrapWriter(Logger, new TransformedPathStub("bar.cs"), false, TrapWriter.CompressionMode.Gzip);
trapWriter.Archive("layout.txt", new TransformedPathStub("layout.txt"), System.Text.Encoding.ASCII);
var writtenFile = TrapWriter.NestPaths(Logger, Path.GetFullPath("snapshot\\archive"), "layout.txt");
Assert.True(File.Exists(writtenFile));
File.Delete("layout.txt");
}
@ -93,11 +112,11 @@ namespace Semmle.Extraction.Tests
{
// When you specify both a trap file and a layout, use the trap file.
var layout = new Semmle.Extraction.Layout(Path.GetFullPath("snapshot\\trap"), null, "something.txt");
Assert.True(layout.FileInLayout("bar.cs"));
var subProject = layout.LookupProjectOrNull("foo.cs");
Assert.True(layout.FileInLayout(new TransformedPathStub("bar.cs")));
var subProject = layout.LookupProjectOrNull(new TransformedPathStub("foo.cs"));
Assert.NotNull(subProject);
var f1 = subProject!.GetTrapPath(Logger, "foo.cs", TrapWriter.CompressionMode.Gzip);
var g1 = TrapWriter.NestPaths(Logger, Path.GetFullPath("snapshot\\trap"), "foo.cs.trap.gz", TrapWriter.InnerPathComputation.ABSOLUTE);
var f1 = subProject!.GetTrapPath(Logger, new TransformedPathStub("foo.cs"), TrapWriter.CompressionMode.Gzip);
var g1 = TrapWriter.NestPaths(Logger, Path.GetFullPath("snapshot\\trap"), "foo.cs.trap.gz");
Assert.Equal(f1, g1);
}
@ -123,30 +142,30 @@ namespace Semmle.Extraction.Tests
var layout = new Semmle.Extraction.Layout(null, null, "layout.txt");
// Use Section 2
Assert.True(layout.FileInLayout("bar.cs"));
var subProject = layout.LookupProjectOrNull("bar.cs");
Assert.True(layout.FileInLayout(new TransformedPathStub("bar.cs")));
var subProject = layout.LookupProjectOrNull(new TransformedPathStub("bar.cs"));
Assert.NotNull(subProject);
var f1 = subProject!.GetTrapPath(Logger, "bar.cs", TrapWriter.CompressionMode.Gzip);
var g1 = TrapWriter.NestPaths(Logger, Path.GetFullPath("snapshot\\trap2"), "bar.cs.trap.gz", TrapWriter.InnerPathComputation.ABSOLUTE);
var f1 = subProject!.GetTrapPath(Logger, new TransformedPathStub("bar.cs"), TrapWriter.CompressionMode.Gzip);
var g1 = TrapWriter.NestPaths(Logger, Path.GetFullPath("snapshot\\trap2"), "bar.cs.trap.gz");
Assert.Equal(f1, g1);
// Use Section 1
Assert.True(layout.FileInLayout("foo.cs"));
subProject = layout.LookupProjectOrNull("foo.cs");
Assert.True(layout.FileInLayout(new TransformedPathStub("foo.cs")));
subProject = layout.LookupProjectOrNull(new TransformedPathStub("foo.cs"));
Assert.NotNull(subProject);
var f2 = subProject!.GetTrapPath(Logger, "foo.cs", TrapWriter.CompressionMode.Gzip);
var g2 = TrapWriter.NestPaths(Logger, Path.GetFullPath("snapshot\\trap1"), "foo.cs.trap.gz", TrapWriter.InnerPathComputation.ABSOLUTE);
var f2 = subProject!.GetTrapPath(Logger, new TransformedPathStub("foo.cs"), TrapWriter.CompressionMode.Gzip);
var g2 = TrapWriter.NestPaths(Logger, Path.GetFullPath("snapshot\\trap1"), "foo.cs.trap.gz");
Assert.Equal(f2, g2);
// boo.dll is not in the layout, so use layout from first section.
Assert.False(layout.FileInLayout("boo.dll"));
var f3 = layout.LookupProjectOrDefault("boo.dll").GetTrapPath(Logger, "boo.dll", TrapWriter.CompressionMode.Gzip);
var g3 = TrapWriter.NestPaths(Logger, Path.GetFullPath("snapshot\\trap1"), "boo.dll.trap.gz", TrapWriter.InnerPathComputation.ABSOLUTE);
Assert.False(layout.FileInLayout(new TransformedPathStub("boo.dll")));
var f3 = layout.LookupProjectOrDefault(new TransformedPathStub("boo.dll")).GetTrapPath(Logger, new TransformedPathStub("boo.dll"), TrapWriter.CompressionMode.Gzip);
var g3 = TrapWriter.NestPaths(Logger, Path.GetFullPath("snapshot\\trap1"), "boo.dll.trap.gz");
Assert.Equal(f3, g3);
// boo.cs is not in the layout, so return null
Assert.False(layout.FileInLayout("boo.cs"));
Assert.Null(layout.LookupProjectOrNull("boo.cs"));
Assert.False(layout.FileInLayout(new TransformedPathStub("boo.cs")));
Assert.Null(layout.LookupProjectOrNull(new TransformedPathStub("boo.cs")));
}
[Fact]

Просмотреть файл

@ -0,0 +1,45 @@
using Semmle.Util;
using Xunit;
namespace Semmle.Extraction.Tests
{
class PathCacheStub : IPathCache
{
public string GetCanonicalPath(string path) => path;
}
public class PathTransformerTests
{
[Fact]
public void TestTransformerFile()
{
var spec = new string[]
{
@"#D:\src",
@"C:\agent*\src//",
@"-C:\agent*\src\external",
@"",
@"#empty",
@"",
@"#src2",
@"/agent*//src",
@"",
@"#optsrc",
@"opt/src//"
};
var pathTransformer = new PathTransformer(new PathCacheStub(), spec);
// Windows-style matching
Assert.Equal(@"C:/bar.cs", pathTransformer.Transform(@"C:\bar.cs").Value);
Assert.Equal("D:/src/file.cs", pathTransformer.Transform(@"C:\agent42\src\file.cs").Value);
Assert.Equal("D:/src/file.cs", pathTransformer.Transform(@"C:\agent43\src\file.cs").Value);
Assert.Equal(@"C:/agent43/src/external/file.cs", pathTransformer.Transform(@"C:\agent43\src\external\file.cs").Value);
// Linux-style matching
Assert.Equal(@"src2/src/file.cs", pathTransformer.Transform(@"/agent/src/file.cs").Value);
Assert.Equal(@"src2/src/file.cs", pathTransformer.Transform(@"/agent42/src/file.cs").Value);
Assert.Equal(@"optsrc/file.cs", pathTransformer.Transform(@"/opt/src/file.cs").Value);
}
}
}

Просмотреть файл

@ -14,7 +14,7 @@ namespace Semmle.Extraction.Tests
string tempDir = System.IO.Path.GetTempPath();
string root1, root2, root3;
if(Win32.IsWindows())
if (Win32.IsWindows())
{
root1 = "E:";
root2 = "e:";
@ -27,32 +27,21 @@ namespace Semmle.Extraction.Tests
root3 = "/";
}
string formattedTempDir = tempDir.Replace('/', '\\').Replace(':', '_').Trim('\\');
var logger = new LoggerMock();
System.IO.Directory.SetCurrentDirectory(tempDir);
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
// `Directory.SetCurrentDirectory()` doesn't seem to work on macOS,
// so disable this test on macOS, for now
Assert.NotEqual(Directory.GetCurrentDirectory(), tempDir);
return;
}
Assert.Equal($@"C:\Temp\source_archive\def.cs", TrapWriter.NestPaths(logger, @"C:\Temp\source_archive", "def.cs").Replace('/', '\\'));
Assert.Equal($@"C:\Temp\source_archive\{formattedTempDir}\def.cs", TrapWriter.NestPaths(logger, @"C:\Temp\source_archive", "def.cs", TrapWriter.InnerPathComputation.ABSOLUTE).Replace('/','\\'));
Assert.Equal(@"C:\Temp\source_archive\def.cs", TrapWriter.NestPaths(logger, @"C:\Temp\source_archive", "def.cs").Replace('/', '\\'));
Assert.Equal(@"C:\Temp\source_archive\def.cs", TrapWriter.NestPaths(logger, @"C:\Temp\source_archive", "def.cs", TrapWriter.InnerPathComputation.RELATIVE).Replace('/', '\\'));
Assert.Equal(@"C:\Temp\source_archive\E_\source\def.cs", TrapWriter.NestPaths(logger, @"C:\Temp\source_archive", $@"{root1}\source\def.cs").Replace('/', '\\'));
Assert.Equal(@"C:\Temp\source_archive\E_\source\def.cs", TrapWriter.NestPaths(logger, @"C:\Temp\source_archive", $@"{root1}\source\def.cs", TrapWriter.InnerPathComputation.ABSOLUTE).Replace('/', '\\'));
Assert.Equal(@"C:\Temp\source_archive\e_\source\def.cs", TrapWriter.NestPaths(logger, @"C:\Temp\source_archive", $@"{root2}\source\def.cs").Replace('/', '\\'));
Assert.Equal(@"C:\Temp\source_archive\e_\source\def.cs", TrapWriter.NestPaths(logger, @"C:\Temp\source_archive", $@"{root2}\source\def.cs", TrapWriter.InnerPathComputation.RELATIVE).Replace('/', '\\'));
Assert.Equal(@"C:\Temp\source_archive\source\def.cs", TrapWriter.NestPaths(logger, @"C:\Temp\source_archive", $@"{root3}source\def.cs").Replace('/', '\\'));
Assert.Equal(@"C:\Temp\source_archive\source\def.cs", TrapWriter.NestPaths(logger, @"C:\Temp\source_archive", $@"{root3}source\def.cs", TrapWriter.InnerPathComputation.ABSOLUTE).Replace('/', '\\'));
Assert.Equal(@"C:\Temp\source_archive\source\def.cs", TrapWriter.NestPaths(logger, @"C:\Temp\source_archive", $@"{root3}source\def.cs").Replace('/', '\\'));
Assert.Equal(@"C:\Temp\source_archive\source\def.cs", TrapWriter.NestPaths(logger, @"C:\Temp\source_archive", $@"{root3}source\def.cs", TrapWriter.InnerPathComputation.RELATIVE).Replace('/', '\\'));
Assert.Equal(@"C:\Temp\source_archive\diskstation\share\source\def.cs", TrapWriter.NestPaths(logger, @"C:\Temp\source_archive", $@"{root3}{root3}diskstation\share\source\def.cs", TrapWriter.InnerPathComputation.ABSOLUTE).Replace('/', '\\'));
Assert.Equal(@"C:\Temp\source_archive\diskstation\share\source\def.cs", TrapWriter.NestPaths(logger, @"C:\Temp\source_archive", $@"{root3}{root3}diskstation\share\source\def.cs").Replace('/', '\\'));
}
class LoggerMock : ILogger

Просмотреть файл

@ -10,93 +10,55 @@ namespace Semmle.Extraction.Entities
File(Context cx, string path)
: base(cx, path)
{
Path = path;
OriginalPath = path;
TransformedPathLazy = new Lazy<PathTransformer.ITransformedPath>(() => Context.Extractor.PathTransformer.Transform(OriginalPath));
}
public string Path
{
get;
private set;
}
readonly string OriginalPath;
readonly Lazy<PathTransformer.ITransformedPath> TransformedPathLazy;
PathTransformer.ITransformedPath TransformedPath => TransformedPathLazy.Value;
public string DatabasePath => PathAsDatabaseId(Path);
public override bool NeedsPopulation => Context.DefinesFile(Path) || Path == Context.Extractor.OutputPath;
public override bool NeedsPopulation => Context.DefinesFile(OriginalPath) || OriginalPath == Context.Extractor.OutputPath;
public override void Populate(TextWriter trapFile)
{
if (Path == null)
trapFile.files(this, TransformedPath.Value, TransformedPath.NameWithoutExtension, TransformedPath.Extension);
if (TransformedPath.ParentDirectory is PathTransformer.ITransformedPath dir)
trapFile.containerparent(Folder.Create(Context, dir), this);
var fromSource = TransformedPath.Extension.ToLowerInvariant().Equals("cs");
if (fromSource)
{
trapFile.files(this, "", "", "");
}
else
{
var fi = new FileInfo(Path);
string extension = fi.Extension ?? "";
string name = fi.Name;
name = name.Substring(0, name.Length - extension.Length);
int fromSource = extension.ToLowerInvariant().Equals(".cs") ? 1 : 2;
// remove the dot from the extension
if (extension.Length > 0)
extension = extension.Substring(1);
trapFile.files(this, PathAsDatabaseString(Path), name, extension);
trapFile.containerparent(Folder.Create(Context, fi.Directory), this);
if (fromSource == 1)
foreach (var text in Context.Compilation.SyntaxTrees.
Where(t => t.FilePath == OriginalPath).
Select(tree => tree.GetText()))
{
foreach (var text in Context.Compilation.SyntaxTrees.
Where(t => t.FilePath == Path).
Select(tree => tree.GetText()))
{
var rawText = text.ToString() ?? "";
var lineCounts = LineCounter.ComputeLineCounts(rawText);
if (rawText.Length > 0 && rawText[rawText.Length - 1] != '\n') lineCounts.Total++;
var rawText = text.ToString() ?? "";
var lineCounts = LineCounter.ComputeLineCounts(rawText);
if (rawText.Length > 0 && rawText[rawText.Length - 1] != '\n') lineCounts.Total++;
trapFile.numlines(this, lineCounts);
Context.TrapWriter.Archive(fi.FullName, text.Encoding ?? System.Text.Encoding.Default);
}
trapFile.numlines(this, lineCounts);
Context.TrapWriter.Archive(OriginalPath, TransformedPath, text.Encoding ?? System.Text.Encoding.Default);
}
trapFile.file_extraction_mode(this, Context.Extractor.Standalone ? 1 : 0);
}
trapFile.file_extraction_mode(this, Context.Extractor.Standalone ? 1 : 0);
}
public override void WriteId(System.IO.TextWriter trapFile)
{
if (Path is null)
trapFile.Write("GENERATED;sourcefile");
else
{
trapFile.Write(DatabasePath);
trapFile.Write(";sourcefile");
}
trapFile.Write(TransformedPath.DatabaseId);
trapFile.Write(";sourcefile");
}
/// <summary>
/// Converts a path string into a string to use as an ID
/// in the QL database.
/// </summary>
/// <param name="path">An absolute path.</param>
/// <returns>The database ID.</returns>
public static string PathAsDatabaseId(string path)
{
if (path.Length >= 2 && path[1] == ':' && Char.IsLower(path[0]))
path = Char.ToUpper(path[0]) + "_" + path.Substring(2);
return path.Replace('\\', '/').Replace(":", "_");
}
public static string PathAsDatabaseString(string path) => path.Replace('\\', '/');
public static File Create(Context cx, string path) => FileFactory.Instance.CreateEntity(cx, (typeof(File), path), path);
public static File CreateGenerated(Context cx) => GeneratedFile.Create(cx);
class GeneratedFile : File
{
GeneratedFile(Context cx)
: base(cx, "") { }
GeneratedFile(Context cx) : base(cx, "") { }
public override bool NeedsPopulation => true;

Просмотреть файл

@ -2,65 +2,44 @@ using System.IO;
namespace Semmle.Extraction.Entities
{
sealed class Folder : CachedEntity<DirectoryInfo>
sealed class Folder : CachedEntity<PathTransformer.ITransformedPath>
{
Folder(Context cx, DirectoryInfo init)
: base(cx, init)
{
Path = init.FullName;
}
public string Path
{
get;
private set;
}
public string DatabasePath => File.PathAsDatabaseId(Path);
Folder(Context cx, PathTransformer.ITransformedPath init) : base(cx, init) { }
public override void Populate(TextWriter trapFile)
{
// Ensure that the name of the root directory is consistent
// with the XmlTrapWriter.
// Linux/Windows: java.io.File.getName() returns ""
// On Linux: System.IO.DirectoryInfo.Name returns "/"
// On Windows: System.IO.DirectoryInfo.Name returns "L:\"
string shortName = symbol.Parent == null ? "" : symbol.Name;
trapFile.folders(this, File.PathAsDatabaseString(Path), shortName);
if (symbol.Parent != null)
{
trapFile.containerparent(Create(Context, symbol.Parent), this);
}
trapFile.folders(this, symbol.Value, symbol.NameWithoutExtension);
if (symbol.ParentDirectory is PathTransformer.ITransformedPath parent)
trapFile.containerparent(Create(Context, parent), this);
}
public override bool NeedsPopulation => true;
public override void WriteId(System.IO.TextWriter trapFile)
{
trapFile.Write(DatabasePath);
trapFile.Write(symbol.DatabaseId);
trapFile.Write(";folder");
}
public static Folder Create(Context cx, DirectoryInfo folder) =>
public static Folder Create(Context cx, PathTransformer.ITransformedPath folder) =>
FolderFactory.Instance.CreateEntity(cx, folder, folder);
public override Microsoft.CodeAnalysis.Location? ReportingLocation => null;
class FolderFactory : ICachedEntityFactory<DirectoryInfo, Folder>
class FolderFactory : ICachedEntityFactory<PathTransformer.ITransformedPath, Folder>
{
public static readonly FolderFactory Instance = new FolderFactory();
public Folder Create(Context cx, DirectoryInfo init) => new Folder(cx, init);
public Folder Create(Context cx, PathTransformer.ITransformedPath init) => new Folder(cx, init);
}
public override TrapStackBehaviour TrapStackBehaviour => TrapStackBehaviour.NoLabel;
public override int GetHashCode() => Path.GetHashCode();
public override int GetHashCode() => symbol.GetHashCode();
public override bool Equals(object? obj)
{
return obj is Folder folder && folder.Path == Path;
return obj is Folder folder && Equals(folder.symbol, symbol);
}
}
}

Просмотреть файл

@ -81,6 +81,11 @@ namespace Semmle.Extraction
/// </summary>
ILogger Logger { get; }
/// <summary>
/// The path transformer to apply.
/// </summary>
PathTransformer PathTransformer { get; }
/// <summary>
/// Creates a new context.
/// </summary>
@ -112,11 +117,14 @@ namespace Semmle.Extraction
/// </summary>
/// <param name="standalone">If the extraction is standalone.</param>
/// <param name="outputPath">The name of the output DLL/EXE, or null if not specified (standalone extraction).</param>
public Extractor(bool standalone, string outputPath, ILogger logger)
/// <param name="logger">The object used for logging.</param>
/// <param name="pathTransformer">The object used for path transformations.</param>
public Extractor(bool standalone, string outputPath, ILogger logger, PathTransformer pathTransformer)
{
Standalone = standalone;
OutputPath = outputPath;
Logger = logger;
PathTransformer = pathTransformer;
}
// Limit the number of error messages in the log file
@ -206,5 +214,7 @@ namespace Semmle.Extraction
public ILogger Logger { get; private set; }
public static string Version => $"{ThisAssembly.Git.BaseTag} ({ThisAssembly.Git.Sha})";
public PathTransformer PathTransformer { get; }
}
}

Просмотреть файл

@ -0,0 +1,131 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Diagnostics.CodeAnalysis;
using Semmle.Util;
namespace Semmle.Extraction
{
public sealed class InvalidFilePatternException : Exception
{
public InvalidFilePatternException(string pattern, string message) :
base($"Invalid file pattern '{pattern}': {message}")
{ }
}
/// <summary>
/// A file pattern, as used in either an extractor layout file or
/// a path transformer file.
/// </summary>
public sealed class FilePattern
{
/// <summary>
/// Whether this is an inclusion pattern.
/// </summary>
public bool Include { get; }
public FilePattern(string pattern)
{
Include = true;
if (pattern.StartsWith("-"))
{
pattern = pattern.Substring(1);
Include = false;
}
pattern = FileUtils.ConvertToUnix(pattern.Trim()).TrimStart('/');
RegexPattern = BuildRegex(pattern).ToString();
}
/// <summary>
/// Constructs a regex string from a file pattern. Throws
/// `InvalidFilePatternException` for invalid patterns.
/// </summary>
static StringBuilder BuildRegex(string pattern)
{
bool HasCharAt(int i, Predicate<char> p) =>
i >= 0 && i < pattern.Length && p(pattern[i]);
var sb = new StringBuilder();
var i = 0;
var seenDoubleSlash = false;
sb.Append('^');
while (i < pattern.Length)
{
if (pattern[i] == '/')
{
if (HasCharAt(i + 1, c => c == '/'))
{
if (seenDoubleSlash)
throw new InvalidFilePatternException(pattern, "'//' is allowed at most once.");
sb.Append("(?<doubleslash>/)");
i += 2;
seenDoubleSlash = true;
}
else
{
sb.Append('/');
i++;
}
}
else if (pattern[i] == '*')
{
if (HasCharAt(i + 1, c => c == '*'))
{
if (HasCharAt(i - 1, c => c != '/'))
throw new InvalidFilePatternException(pattern, "'**' preceeded by non-`/` character.");
if (HasCharAt(i + 2, c => c != '/'))
throw new InvalidFilePatternException(pattern, "'**' succeeded by non-`/` character");
sb.Append(".*");
i += 2;
}
else
{
sb.Append("[^/]*");
i++;
}
}
else
sb.Append(Regex.Escape(pattern[i++].ToString()));
}
return sb.Append(".*");
}
/// <summary>
/// The regex pattern compiled from this file pattern.
/// </summary>
public string RegexPattern { get; }
/// <summary>
/// Returns `true` if the set of file patterns `patterns` match the path `path`.
/// If so, `transformerSuffix` will contain the part of `path` that needs to be
/// suffixed when using path transformers.
/// </summary>
public static bool Matches(IEnumerable<FilePattern> patterns, string path, [NotNullWhen(true)] out string? transformerSuffix)
{
path = FileUtils.ConvertToUnix(path).TrimStart('/');
foreach (var pattern in patterns.Reverse())
{
var m = new Regex(pattern.RegexPattern).Match(path);
if (m.Success)
{
if (pattern.Include)
{
transformerSuffix = m.Groups.TryGetValue("doubleslash", out var group)
? path.Substring(group.Index)
: path;
return true;
}
transformerSuffix = null;
return false;
}
}
transformerSuffix = null;
return false;
}
}
}

Просмотреть файл

@ -54,14 +54,15 @@ namespace Semmle.Extraction
/// </summary>
/// <param name="srcFile">The source file.</param>
/// <returns>The full filepath of the trap file.</returns>
public string GetTrapPath(ILogger logger, string srcFile, TrapWriter.CompressionMode trapCompression) => TrapWriter.TrapPath(logger, TRAP_FOLDER, srcFile, trapCompression);
public string GetTrapPath(ILogger logger, PathTransformer.ITransformedPath srcFile, TrapWriter.CompressionMode trapCompression) =>
TrapWriter.TrapPath(logger, TRAP_FOLDER, srcFile, trapCompression);
/// <summary>
/// Creates a trap writer for a given source/assembly file.
/// </summary>
/// <param name="srcFile">The source file.</param>
/// <returns>A newly created TrapWriter.</returns>
public TrapWriter CreateTrapWriter(ILogger logger, string srcFile, bool discardDuplicates, TrapWriter.CompressionMode trapCompression) =>
public TrapWriter CreateTrapWriter(ILogger logger, PathTransformer.ITransformedPath srcFile, bool discardDuplicates, TrapWriter.CompressionMode trapCompression) =>
new TrapWriter(logger, srcFile, TRAP_FOLDER, SOURCE_ARCHIVE, discardDuplicates, trapCompression);
}
@ -73,7 +74,7 @@ namespace Semmle.Extraction
/// </summary>
/// <param name="sourceFile">The file to look up.</param>
/// <returns>The relevant subproject, or null if not found.</returns>
public SubProject? LookupProjectOrNull(string sourceFile)
public SubProject? LookupProjectOrNull(PathTransformer.ITransformedPath sourceFile)
{
if (!useLayoutFile) return DefaultProject;
@ -89,7 +90,7 @@ namespace Semmle.Extraction
/// </summary>
/// <param name="sourceFile">The file to look up.</param>
/// <returns>The relevant subproject, or DefaultProject if not found.</returns>
public SubProject LookupProjectOrDefault(string sourceFile)
public SubProject LookupProjectOrDefault(PathTransformer.ITransformedPath sourceFile)
{
return LookupProjectOrNull(sourceFile) ?? DefaultProject;
}
@ -134,7 +135,7 @@ namespace Semmle.Extraction
/// </summary>
/// <param name="path">The absolute path of the file to query.</param>
/// <returns>True iff there is no layout file or the layout file specifies the file.</returns>
public bool FileInLayout(string path) => LookupProjectOrNull(path) != null;
public bool FileInLayout(PathTransformer.ITransformedPath path) => LookupProjectOrNull(path) != null;
void ReadLayoutFile(string layout)
{
@ -167,33 +168,7 @@ namespace Semmle.Extraction
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<Condition> conditions = new List<Condition>();
private readonly List<FilePattern> filePatterns = new List<FilePattern>();
public readonly Layout.SubProject Directories;
@ -219,22 +194,10 @@ namespace Semmle.Extraction
ReadVariable("ODASA_BUILD_ERROR_DIR", lines[i++]);
while (i < lines.Length && !lines[i].StartsWith("#"))
{
conditions.Add(new Condition(lines[i++]));
filePatterns.Add(new FilePattern(lines[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;
}
public bool Matches(PathTransformer.ITransformedPath path) => FilePattern.Matches(filePatterns, path.Value, out var _);
}
}

Просмотреть файл

@ -0,0 +1,177 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Diagnostics.CodeAnalysis;
using Semmle.Util;
namespace Semmle.Extraction
{
/// <summary>
/// A class for interpreting path transformers specified using the environment
/// variable `CODEQL_PATH_TRANSFORMER`.
/// </summary>
public sealed class PathTransformer
{
public class InvalidPathTransformerException : Exception
{
public InvalidPathTransformerException(string message) :
base($"Invalid path transformer specification: {message}")
{ }
}
/// <summary>
/// A transformed path.
/// </summary>
public interface ITransformedPath
{
string Value { get; }
string Extension { get; }
string NameWithoutExtension { get; }
ITransformedPath? ParentDirectory { get; }
ITransformedPath WithSuffix(string suffix);
string DatabaseId { get; }
}
struct TransformedPath : ITransformedPath
{
public TransformedPath(string value) { this.value = value; }
readonly string value;
public string Value => value;
public string Extension => Path.GetExtension(value)?.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;
}
readonly Func<string, string> transform;
/// <summary>
/// Returns the path obtained by transforming `path`.
/// </summary>
public ITransformedPath Transform(string path) => new TransformedPath(transform(path));
/// <summary>
/// Default constructor reads parameters from the environment.
/// </summary>
public PathTransformer(IPathCache pathCache) :
this(pathCache, Environment.GetEnvironmentVariable("CODEQL_PATH_TRANSFORMER") is string file ? File.ReadAllLines(file) : null)
{
}
/// <summary>
/// Creates a path transformer based on the specification in `lines`.
/// Throws `InvalidPathTransformerException` for invalid specifications.
/// </summary>
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;
};
}
static IEnumerable<TransformerSection> ParsePathTransformerSpec(string[] lines)
{
var sections = new List<TransformerSection>();
try
{
int 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;
}
}
sealed class TransformerSection
{
readonly string name;
readonly List<FilePattern> filePatterns = new List<FilePattern>();
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;
}
}
}

Просмотреть файл

@ -14,12 +14,6 @@ namespace Semmle.Extraction
public sealed class TrapWriter : IDisposable
{
public enum InnerPathComputation
{
ABSOLUTE,
RELATIVE
}
public enum CompressionMode
{
None,
@ -45,7 +39,7 @@ namespace Semmle.Extraction
readonly CompressionMode TrapCompression;
public TrapWriter(ILogger logger, string outputfile, string? trap, string? archive, bool discardDuplicates, CompressionMode trapCompression)
public TrapWriter(ILogger logger, PathTransformer.ITransformedPath outputfile, string? trap, string? archive, bool discardDuplicates, CompressionMode trapCompression)
{
Logger = logger;
TrapCompression = trapCompression;
@ -107,16 +101,17 @@ namespace Semmle.Extraction
/// Adds the specified input file to the source archive. It may end up in either the normal or long path area
/// of the source archive, depending on the length of its full path.
/// </summary>
/// <param name="inputPath">The path to the input file.</param>
/// <param name="originalPath">The path to the input file.</param>
/// <param name="transformedPath">The transformed path to the input file.</param>
/// <param name="inputEncoding">The encoding used by the input file.</param>
public void Archive(string inputPath, Encoding inputEncoding)
public void Archive(string originalPath, PathTransformer.ITransformedPath transformedPath, Encoding inputEncoding)
{
if (string.IsNullOrEmpty(archive)) return;
// Calling GetFullPath makes this use the canonical capitalisation, if the file exists.
string fullInputPath = Path.GetFullPath(inputPath);
string fullInputPath = Path.GetFullPath(originalPath);
ArchivePath(fullInputPath, inputEncoding);
ArchivePath(fullInputPath, transformedPath, inputEncoding);
}
/// <summary>
@ -124,14 +119,11 @@ namespace Semmle.Extraction
/// </summary>
/// <param name="inputPath">The path of the file.</param>
/// <param name="contents">The contents of the file.</param>
public void Archive(string inputPath, string contents)
public void Archive(PathTransformer.ITransformedPath inputPath, string contents)
{
if (string.IsNullOrEmpty(archive)) return;
// Calling GetFullPath makes this use the canonical capitalisation, if the file exists.
string fullInputPath = Path.GetFullPath(inputPath);
ArchiveContents(fullInputPath, contents);
ArchiveContents(inputPath, contents);
}
/// <summary>
@ -210,18 +202,19 @@ namespace Semmle.Extraction
/// source archive less than the system path limit of 260 characters.
/// </summary>
/// <param name="fullInputPath">The full path to the input file.</param>
/// <param name="transformedPath">The transformed path to the input file.</param>
/// <param name="inputEncoding">The encoding used by the input file.</param>
/// <exception cref="PathTooLongException">If the output path in the source archive would
/// exceed the system path limit of 260 characters.</exception>
private void ArchivePath(string fullInputPath, Encoding inputEncoding)
private void ArchivePath(string fullInputPath, PathTransformer.ITransformedPath transformedPath, Encoding inputEncoding)
{
string contents = File.ReadAllText(fullInputPath, inputEncoding);
ArchiveContents(fullInputPath, contents);
ArchiveContents(transformedPath, contents);
}
private void ArchiveContents(string fullInputPath, string contents)
private void ArchiveContents(PathTransformer.ITransformedPath transformedPath, string contents)
{
string dest = NestPaths(Logger, archive, fullInputPath, InnerPathComputation.ABSOLUTE);
string dest = NestPaths(Logger, archive, transformedPath.Value);
string tmpSrcFile = Path.GetTempFileName();
File.WriteAllText(tmpSrcFile, contents, UTF8);
try
@ -236,14 +229,11 @@ namespace Semmle.Extraction
}
}
public static string NestPaths(ILogger logger, string? outerpath, string innerpath, InnerPathComputation innerPathComputation)
public static string NestPaths(ILogger logger, string? outerpath, string innerpath)
{
string nested = innerpath;
if (!string.IsNullOrEmpty(outerpath))
{
if (!Path.IsPathRooted(innerpath) && innerPathComputation == InnerPathComputation.ABSOLUTE)
innerpath = Path.GetFullPath(innerpath);
// Remove all leading path separators / or \
// For example, UNC paths have two leading \\
innerpath = innerpath.TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
@ -276,13 +266,13 @@ namespace Semmle.Extraction
}
}
public static string TrapPath(ILogger logger, string? folder, string filename, TrapWriter.CompressionMode trapCompression)
public static string TrapPath(ILogger logger, string? folder, PathTransformer.ITransformedPath path, TrapWriter.CompressionMode trapCompression)
{
filename = $"{Path.GetFullPath(filename)}.trap{TrapExtension(trapCompression)}";
var filename = $"{path.Value}.trap{TrapExtension(trapCompression)}";
if (string.IsNullOrEmpty(folder))
folder = Directory.GetCurrentDirectory();
return NestPaths(logger, folder, filename, InnerPathComputation.ABSOLUTE); ;
return NestPaths(logger, folder, filename);
}
}
}

Просмотреть файл

@ -222,6 +222,29 @@ namespace Semmle.Util
this.pathStrategy = pathStrategy;
}
/// <summary>
/// Create a CanonicalPathCache.
/// </summary>
///
/// <remarks>
/// Creates the appropriate PathStrategy object which encapsulates
/// the correct algorithm. Falls back to different implementations
/// depending on platform.
/// </remarks>
///
/// <param name="maxCapacity">Size of the cache.</param>
/// <param name="symlinks">Policy for following symlinks.</param>
/// <returns>A new CanonicalPathCache.</returns>
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);
}
/// <summary>
/// Create a CanonicalPathCache.
/// </summary>