diff --git a/go/extractor/BUILD.bazel b/go/extractor/BUILD.bazel index 23158e25b15..c8e66c98d21 100644 --- a/go/extractor/BUILD.bazel +++ b/go/extractor/BUILD.bazel @@ -19,6 +19,7 @@ go_library( "//go/extractor/dbscheme", "//go/extractor/diagnostics", "//go/extractor/srcarchive", + "//go/extractor/subst", "//go/extractor/toolchain", "//go/extractor/trap", "//go/extractor/util", diff --git a/go/extractor/extractor.go b/go/extractor/extractor.go index 158f0029704..e3d134a522d 100644 --- a/go/extractor/extractor.go +++ b/go/extractor/extractor.go @@ -25,6 +25,7 @@ import ( "github.com/github/codeql-go/extractor/dbscheme" "github.com/github/codeql-go/extractor/diagnostics" "github.com/github/codeql-go/extractor/srcarchive" + "github.com/github/codeql-go/extractor/subst" "github.com/github/codeql-go/extractor/toolchain" "github.com/github/codeql-go/extractor/trap" "github.com/github/codeql-go/extractor/util" @@ -764,9 +765,9 @@ func normalizedPath(ast *ast.File, fset *token.FileSet) string { file := fset.File(ast.Package).Name() path, err := filepath.EvalSymlinks(file) if err != nil { - return file + path = file } - return path + return subst.ResolvePath(path) } // extractFile extracts AST information for the given file diff --git a/go/extractor/subst/BUILD.bazel b/go/extractor/subst/BUILD.bazel new file mode 100644 index 00000000000..a1975f3312a --- /dev/null +++ b/go/extractor/subst/BUILD.bazel @@ -0,0 +1,18 @@ +load("@rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "subst", + srcs = [ + "subst.go", + "subst_other.go", + "subst_windows.go", + ], + importpath = "github.com/github/codeql-go/extractor/subst", + visibility = ["//go:__subpackages__"], +) + +go_test( + name = "subst_test", + srcs = ["subst_test.go"], + embed = [":subst"], +) diff --git a/go/extractor/subst/subst.go b/go/extractor/subst/subst.go new file mode 100644 index 00000000000..885f9f09a81 --- /dev/null +++ b/go/extractor/subst/subst.go @@ -0,0 +1,30 @@ +package subst + +// ResolvePath resolves subst'd drive letters in a full path. +// If the path starts with a subst'd drive letter, replaces it with the backing path. +// Otherwise returns the path unchanged. +func ResolvePath(path string) string { + return resolvePath(path, ResolveDrive) +} + +func resolvePath(path string, resolveDrive func(string) string) string { + if len(path) < 3 { + return path + } + if path[1] != ':' { + return path + } + if path[2] != '\\' && path[2] != '/' { + return path + } + c := path[0] + if !((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) { + return path + } + + resolved := resolveDrive(path[:3]) + if resolved == "" { + return path + } + return resolved + path[2:] +} diff --git a/go/extractor/subst/subst_other.go b/go/extractor/subst/subst_other.go new file mode 100644 index 00000000000..8e153bf3bd6 --- /dev/null +++ b/go/extractor/subst/subst_other.go @@ -0,0 +1,6 @@ +//go:build !windows + +package subst + +// ResolveDrive is a no-op on non-Windows platforms. +func ResolveDrive(driveRoot string) string { return "" } diff --git a/go/extractor/subst/subst_test.go b/go/extractor/subst/subst_test.go new file mode 100644 index 00000000000..28a23849a8a --- /dev/null +++ b/go/extractor/subst/subst_test.go @@ -0,0 +1,86 @@ +package subst + +import "testing" + +func TestResolvePath(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + path string + resolveRoot string + resolved string + expected string + }{ + { + name: "resolved backslash path", + path: `X:\dir\file.go`, + resolveRoot: `X:\`, + resolved: `C:\target`, + expected: `C:\target\dir\file.go`, + }, + { + name: "resolved slash path", + path: `X:/dir/file.go`, + resolveRoot: `X:/`, + resolved: `C:\target`, + expected: `C:\target/dir/file.go`, + }, + { + name: "lowercase drive letter", + path: `x:\dir\file.go`, + resolveRoot: `x:\`, + resolved: `C:\target`, + expected: `C:\target\dir\file.go`, + }, + { + name: "unresolved drive", + path: `X:\dir\file.go`, + resolveRoot: `X:\`, + expected: `X:\dir\file.go`, + }, + { + name: "relative path", + path: `dir\file.go`, + expected: `dir\file.go`, + }, + { + name: "non drive prefix", + path: `\\server\share\file.go`, + expected: `\\server\share\file.go`, + }, + { + name: "missing separator after colon", + path: `X:file.go`, + expected: `X:file.go`, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + resolveCalls := 0 + actual := resolvePath(test.path, func(driveRoot string) string { + resolveCalls++ + if driveRoot != test.resolveRoot { + t.Fatalf("resolvePath passed drive root %q, want %q", driveRoot, test.resolveRoot) + } + return test.resolved + }) + + if actual != test.expected { + t.Fatalf("resolvePath(%q) = %q, want %q", test.path, actual, test.expected) + } + + wantCalls := 0 + if test.resolveRoot != "" { + wantCalls = 1 + } + if resolveCalls != wantCalls { + t.Fatalf("resolvePath(%q) made %d resolve calls, want %d", test.path, resolveCalls, wantCalls) + } + }) + } +} diff --git a/go/extractor/subst/subst_windows.go b/go/extractor/subst/subst_windows.go new file mode 100644 index 00000000000..cb6a60984c0 --- /dev/null +++ b/go/extractor/subst/subst_windows.go @@ -0,0 +1,67 @@ +//go:build windows + +package subst + +import ( + "os" + "path/filepath" + "syscall" + "unsafe" +) + +var ( + dll *syscall.DLL + procResolve *syscall.Proc + procFree *syscall.Proc + available bool +) + +func init() { + dist := os.Getenv("CODEQL_DIST") + if dist == "" { + return + } + dllPath := filepath.Join(dist, "tools", "win64", "canonicalize.dll") + d, err := syscall.LoadDLL(dllPath) + if err != nil { + return + } + p, err := d.FindProc("resolve_subst_u8") + if err != nil { + return + } + f, _ := d.FindProc("resolve_subst_free_u8") + dll = d + procResolve = p + procFree = f + available = true +} + +// ResolveDrive resolves a subst'd drive root (e.g. "X:\") to its backing path. +// Returns "" if the drive is not subst'd or on error. +func ResolveDrive(driveRoot string) string { + if !available { + return "" + } + driveBytes := append([]byte(driveRoot), 0) + ret, _, _ := procResolve.Call(uintptr(unsafe.Pointer(&driveBytes[0]))) + if ret == 0 { + return "" + } + result := goString((*byte)(unsafe.Pointer(ret))) + if procFree != nil { + procFree.Call(ret) + } + return result +} + +func goString(p *byte) string { + if p == nil { + return "" + } + var n int + for ptr := unsafe.Pointer(p); *(*byte)(ptr) != 0; n++ { + ptr = unsafe.Add(ptr, 1) + } + return string(unsafe.Slice(p, n)) +} diff --git a/java/kotlin-extractor/src/main/java/com/semmle/util/files/FileUtil.java b/java/kotlin-extractor/src/main/java/com/semmle/util/files/FileUtil.java index 79ce2d8d8d3..974a72be4b0 100644 --- a/java/kotlin-extractor/src/main/java/com/semmle/util/files/FileUtil.java +++ b/java/kotlin-extractor/src/main/java/com/semmle/util/files/FileUtil.java @@ -1242,12 +1242,13 @@ public class FileUtil public static File tryMakeCanonical (File f) { try { - return f.getCanonicalFile(); + f = f.getCanonicalFile(); } catch (IOException ignored) { Exceptions.ignore(ignored, "Can't log error: Could be too verbose."); - return new File(simplifyPath(f)); + f = new File(simplifyPath(f)); } + return SubstResolver.resolve(f); } diff --git a/java/kotlin-extractor/src/main/java/com/semmle/util/files/SubstResolver.java b/java/kotlin-extractor/src/main/java/com/semmle/util/files/SubstResolver.java new file mode 100644 index 00000000000..f7cf6364b64 --- /dev/null +++ b/java/kotlin-extractor/src/main/java/com/semmle/util/files/SubstResolver.java @@ -0,0 +1,76 @@ +package com.semmle.util.files; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Resolves Windows {@code subst}ed drive letters to their underlying paths. On non-Windows + * platforms, or when the native library failed to load, {@link #resolve(File)} is a no-op that + * returns its argument unchanged. + */ +public class SubstResolver { + private static final boolean available; + + static { + boolean loaded = false; + if (File.separatorChar == '\\') { + String dist = System.getenv("CODEQL_DIST"); + if (dist != null && !dist.isEmpty()) { + try { + Path library = Paths.get(dist).resolve("tools").resolve("win64") + .resolve("canonicalize.dll").toAbsolutePath(); + System.load(library.toString()); + loaded = true; + } catch (RuntimeException | UnsatisfiedLinkError ignored) { + } + } + } + available = loaded; + } + + private SubstResolver() {} + + /** + * Given a drive root like {@code "X:\\"}, returns the path that drive is + * {@code subst}ed to, or {@code null} if the letter isn't a subst mapping. + */ + private static native String nativeResolveSubst(String driveRoot); + + /** + * If {@code f} is an absolute path starting with a {@code subst}ed drive letter, return an + * equivalent path with the drive letter replaced by its target. Otherwise return {@code f} + * unchanged. + */ + public static File resolve(File f) { + if (!available) { + return f; + } + String path = f.getPath(); + if (path.length() < 3 || path.charAt(1) != ':') { + return f; + } + char sep = path.charAt(2); + if (sep != '\\' && sep != '/') { + return f; + } + if (!isDriveLetter(path.charAt(0))) { + return f; + } + + String resolved = nativeResolveSubst(path.substring(0, 3)); + if (resolved == null) { + return f; + } + + return new File(resolved + path.substring(2)); + } + + private static boolean isDriveLetter(char c) { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); + } + + public static boolean isAvailable() { + return available; + } +}