diff --git a/go/extractor/extractor.go b/go/extractor/extractor.go index 158f0029704d..8310a97c56b7 100644 --- a/go/extractor/extractor.go +++ b/go/extractor/extractor.go @@ -764,9 +764,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 util.ResolvePath(path) } // extractFile extracts AST information for the given file diff --git a/go/extractor/go.mod b/go/extractor/go.mod index cea20f1bd4a4..e7d35a8c0843 100644 --- a/go/extractor/go.mod +++ b/go/extractor/go.mod @@ -13,7 +13,10 @@ require ( golang.org/x/tools v0.47.0 ) -require github.com/stretchr/testify v1.11.1 +require ( + github.com/stretchr/testify v1.11.1 + golang.org/x/sys v0.46.0 +) require ( github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/go/extractor/go.sum b/go/extractor/go.sum index a928a105f684..5cba90917b2f 100644 --- a/go/extractor/go.sum +++ b/go/extractor/go.sum @@ -10,6 +10,8 @@ golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/go/extractor/util/BUILD.bazel b/go/extractor/util/BUILD.bazel index ccebf5ebd865..5735e65cf9f4 100644 --- a/go/extractor/util/BUILD.bazel +++ b/go/extractor/util/BUILD.bazel @@ -9,11 +9,18 @@ go_library( "logging.go", "overlays.go", "semver.go", + "subst_other.go", + "subst_windows.go", "util.go", ], importpath = "github.com/github/codeql-go/extractor/util", visibility = ["//visibility:public"], - deps = ["@org_golang_x_mod//semver"], + deps = ["@org_golang_x_mod//semver"] + select({ + "@rules_go//go/platform:windows": [ + "@org_golang_x_sys//windows", + ], + "//conditions:default": [], + }), ) go_test( diff --git a/go/extractor/util/subst_other.go b/go/extractor/util/subst_other.go new file mode 100644 index 000000000000..b32420a7746e --- /dev/null +++ b/go/extractor/util/subst_other.go @@ -0,0 +1,6 @@ +//go:build !windows + +package util + +// ResolvePath is a no-op on non-Windows platforms. +func ResolvePath(path string) string { return path } diff --git a/go/extractor/util/subst_windows.go b/go/extractor/util/subst_windows.go new file mode 100644 index 000000000000..c6c5efc68a43 --- /dev/null +++ b/go/extractor/util/subst_windows.go @@ -0,0 +1,86 @@ +//go:build windows + +package util + +import ( + "os" + "path/filepath" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +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 +} + +// 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 { + 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, ok := resolveDrive(path[:3]) + if !ok { + return path + } + return resolved + path[2:] +} + +// resolveDrive resolves a subst'd drive root (e.g. "X:\") to its backing path. +// The second return value is false if the drive is not subst'd or on error. +func resolveDrive(driveRoot string) (string, bool) { + if !available { + return "", false + } + driveBytes, err := windows.ByteSliceFromString(driveRoot) + if err != nil { + return "", false + } + ret, _, _ := procResolve.Call(uintptr(unsafe.Pointer(&driveBytes[0]))) + if ret == 0 { + return "", false + } + result := windows.BytePtrToString((*byte)(unsafe.Pointer(ret))) + if procFree != nil { + procFree.Call(ret) + } + return result, true +} 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 79ce2d8d8d3d..974a72be4b06 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 static String relativePathLink (File f, File base) 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 000000000000..f7cf6364b64e --- /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; + } +}