diff --git a/MODULE.bazel b/MODULE.bazel index e6f1ddb893dd..beb1395f2098 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -280,7 +280,7 @@ go_sdk.download(version = "1.26.4") go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps") go_deps.from_file(go_mod = "//go/extractor:go.mod") -use_repo(go_deps, "com_github_stretchr_testify", "org_golang_x_mod", "org_golang_x_tools") +use_repo(go_deps, "com_github_stretchr_testify", "org_golang_x_mod", "org_golang_x_sys", "org_golang_x_tools") ripunzip_archive = use_repo_rule("//misc/ripunzip:ripunzip.bzl", "ripunzip_archive") diff --git a/csharp/ql/integration-tests/windows/subst/file.expected b/csharp/ql/integration-tests/windows/subst/file.expected index 6421dce41212..d14faca364be 100644 --- a/csharp/ql/integration-tests/windows/subst/file.expected +++ b/csharp/ql/integration-tests/windows/subst/file.expected @@ -1,8 +1,8 @@ -| code/Program.cs:0:0:0:0 | code/Program.cs | | -| code/obj/Debug/net9.0/.NETCoreApp,Version=v9.0.AssemblyAttributes.cs:0:0:0:0 | code/obj/Debug/net9.0/.NETCoreApp,Version=v9.0.AssemblyAttributes.cs | | -| code/obj/Debug/net9.0/dotnet_build.AssemblyInfo.cs:0:0:0:0 | code/obj/Debug/net9.0/dotnet_build.AssemblyInfo.cs | | -| code/obj/Debug/net9.0/dotnet_build.GlobalUsings.g.cs:0:0:0:0 | code/obj/Debug/net9.0/dotnet_build.GlobalUsings.g.cs | | -| code/obj/Debug/net9.0/dotnet_build.dll:0:0:0:0 | code/obj/Debug/net9.0/dotnet_build.dll | | +| code/Program.cs:0:0:0:0 | code/Program.cs | relative | +| code/dotnet_build.csproj:0:0:0:0 | code/dotnet_build.csproj | relative | +| code/obj/Debug/net9.0/.NETCoreApp,Version=v9.0.AssemblyAttributes.cs:0:0:0:0 | code/obj/Debug/net9.0/.NETCoreApp,Version=v9.0.AssemblyAttributes.cs | relative | +| code/obj/Debug/net9.0/dotnet_build.AssemblyInfo.cs:0:0:0:0 | code/obj/Debug/net9.0/dotnet_build.AssemblyInfo.cs | relative | +| code/obj/Debug/net9.0/dotnet_build.GlobalUsings.g.cs:0:0:0:0 | code/obj/Debug/net9.0/dotnet_build.GlobalUsings.g.cs | relative | +| code/obj/Debug/net9.0/dotnet_build.dll:0:0:0:0 | code/obj/Debug/net9.0/dotnet_build.dll | relative | +| code/obj/dotnet_build.csproj.nuget.g.props:0:0:0:0 | code/obj/dotnet_build.csproj.nuget.g.props | relative | | file://:0:0:0:0 | | | -| file://Z:/dotnet_build.csproj:0:0:0:0 | Z:/dotnet_build.csproj | relative | -| file://Z:/obj/dotnet_build.csproj.nuget.g.props:0:0:0:0 | Z:/obj/dotnet_build.csproj.nuget.g.props | relative | diff --git a/go/extractor/extractor.go b/go/extractor/extractor.go index 158f0029704d..45565584dcf1 100644 --- a/go/extractor/extractor.go +++ b/go/extractor/extractor.go @@ -766,7 +766,7 @@ func normalizedPath(ast *ast.File, fset *token.FileSet) string { if err != nil { return 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..6d6c6c7a9933 100644 --- a/go/extractor/util/BUILD.bazel +++ b/go/extractor/util/BUILD.bazel @@ -9,11 +9,20 @@ 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..b31ee08dc225 --- /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") + if err != nil { + return + } + f, _ := d.FindProc("resolve_subst_free") + dll = d + procResolve = p + procFree = f + available = true +} + +// If "path" is an absolute path starting with a "subst"ed drive letter, return an +// equivalent path with the drive letter replaced by its target. Otherwise return +// "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:] +} + +// Given a drive root like "X:\" (or "X:/"), returns the path that drive is +// "subst"ed to. Returns false if the drive is not "subst"ed or an error occurred. +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/go/ql/integration-tests/subst/file.expected b/go/ql/integration-tests/subst/file.expected index b86b34681e44..96cce26cff5e 100644 --- a/go/ql/integration-tests/subst/file.expected +++ b/go/ql/integration-tests/subst/file.expected @@ -1 +1 @@ -| file://Z:/main.go:0:0:0:0 | Z:/main.go | relative | +| code/main.go:0:0:0:0 | code/main.go | relative | 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..19bdc786c5e2 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 @@ -1237,13 +1237,14 @@ public static String relativePathLink (File f, File base) /** * Try to convert a file into a canonical file. Handles the possible IO exception by just making - * the path absolute. + * the path absolute. Also resolves subst drives on Windows. */ public static File tryMakeCanonical (File f) { try { - return f.getCanonicalFile(); - } + // getCanonicalFile does not canonicalize subst drives on Windows, so do this separately. This + // is a no-op on non-Windows platforms. + return SubstResolver.resolve(f.getCanonicalFile()); } catch (IOException ignored) { Exceptions.ignore(ignored, "Can't log error: Could be too verbose."); return new File(simplifyPath(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..3d93c45234dc --- /dev/null +++ b/java/kotlin-extractor/src/main/java/com/semmle/util/files/SubstResolver.java @@ -0,0 +1,77 @@ +package com.semmle.util.files; + +import java.io.File; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +/** + * Resolves Windows {@code subst}ed drive letters to their underlying paths. On non-Windows + * platforms, or when the native library failed to load, resolving will be a no-op. + */ +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:\\"} (or {@code "X:/"}), returns the path that drive is + * {@code subst}ed to, or {@code null} if the drive root was not subst drive or if an internal + * error occurred. + */ + 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; + } + + // Append the remainder of the original path. The native side strips away + // any trailing separator. + return new File(resolved + path.substring(2)); + } + + private static boolean isDriveLetter(char c) { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); + } +} diff --git a/java/ql/integration-tests/java/subst/file.expected b/java/ql/integration-tests/java/subst/file.expected index 467876a74450..02d4543cfcae 100644 --- a/java/ql/integration-tests/java/subst/file.expected +++ b/java/ql/integration-tests/java/subst/file.expected @@ -1,5 +1,5 @@ +| code/Test.class:0:0:0:0 | Test | relative | +| code/test1.java:0:0:0:0 | test1 | relative | +| code/test2.kt:0:0:0:0 | test2 | relative | | file://:0:0:0:0 | | | | file://:0:0:0:0 | | | -| file://Z:/Test.class:0:0:0:0 | Test | relative | -| file://Z:/test1.java:0:0:0:0 | test1 | relative | -| file://Z:/test2.kt:0:0:0:0 | test2 | relative | diff --git a/javascript/ql/integration-tests/subst/file.expected b/javascript/ql/integration-tests/subst/file.expected index 10416eb5e733..6d3663acac81 100644 --- a/javascript/ql/integration-tests/subst/file.expected +++ b/javascript/ql/integration-tests/subst/file.expected @@ -1,2 +1,2 @@ -| file://Z:/main.js:0:0:0:0 | Z:/main.js | relative | -| file://Z:/test.ts:0:0:0:0 | Z:/test.ts | relative | +| code/main.js:0:0:0:0 | code/main.js | relative | +| code/test.ts:0:0:0:0 | code/test.ts | relative | diff --git a/python/ql/integration-tests/subst/file.expected b/python/ql/integration-tests/subst/file.expected index 1bc4d03914a1..e001d4959bb5 100644 --- a/python/ql/integration-tests/subst/file.expected +++ b/python/ql/integration-tests/subst/file.expected @@ -1 +1 @@ -| code/main.py:0:0:0:0 | code/main.py | | +| code/main.py:0:0:0:0 | code/main.py | relative | diff --git a/ruby/ql/integration-tests/subst/file.expected b/ruby/ql/integration-tests/subst/file.expected index 47b324fea9e9..2962c283b484 100644 --- a/ruby/ql/integration-tests/subst/file.expected +++ b/ruby/ql/integration-tests/subst/file.expected @@ -1,2 +1,2 @@ -| code/test.rb:0:0:0:0 | code/test.rb | | +| code/test.rb:0:0:0:0 | code/test.rb | relative | | file://:0:0:0:0 | | | diff --git a/rust/ql/integration-tests/subst/file.expected b/rust/ql/integration-tests/subst/file.expected index c1ff73337f2a..407f50b5c0ea 100644 --- a/rust/ql/integration-tests/subst/file.expected +++ b/rust/ql/integration-tests/subst/file.expected @@ -1,2 +1,2 @@ -| code/test.rs:0:0:0:0 | code/test.rs | | +| code/test.rs:0:0:0:0 | code/test.rs | relative | | file://:0:0:0:0 | | |