Skip to content

Commit e34375c

Browse files
authored
Add subst resolution for Kotlin and Go extractors
1 parent 5079680 commit e34375c

9 files changed

Lines changed: 290 additions & 4 deletions

File tree

go/extractor/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ go_library(
1919
"//go/extractor/dbscheme",
2020
"//go/extractor/diagnostics",
2121
"//go/extractor/srcarchive",
22+
"//go/extractor/subst",
2223
"//go/extractor/toolchain",
2324
"//go/extractor/trap",
2425
"//go/extractor/util",

go/extractor/extractor.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/github/codeql-go/extractor/dbscheme"
2626
"github.com/github/codeql-go/extractor/diagnostics"
2727
"github.com/github/codeql-go/extractor/srcarchive"
28+
"github.com/github/codeql-go/extractor/subst"
2829
"github.com/github/codeql-go/extractor/toolchain"
2930
"github.com/github/codeql-go/extractor/trap"
3031
"github.com/github/codeql-go/extractor/util"
@@ -764,9 +765,9 @@ func normalizedPath(ast *ast.File, fset *token.FileSet) string {
764765
file := fset.File(ast.Package).Name()
765766
path, err := filepath.EvalSymlinks(file)
766767
if err != nil {
767-
return file
768+
path = file
768769
}
769-
return path
770+
return subst.ResolvePath(path)
770771
}
771772

772773
// extractFile extracts AST information for the given file

go/extractor/subst/BUILD.bazel

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

go/extractor/subst/subst.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package subst
2+
3+
// ResolvePath resolves subst'd drive letters in a full path.
4+
// If the path starts with a subst'd drive letter, replaces it with the backing path.
5+
// Otherwise returns the path unchanged.
6+
func ResolvePath(path string) string {
7+
return resolvePath(path, ResolveDrive)
8+
}
9+
10+
func resolvePath(path string, resolveDrive func(string) string) string {
11+
if len(path) < 3 {
12+
return path
13+
}
14+
if path[1] != ':' {
15+
return path
16+
}
17+
if path[2] != '\\' && path[2] != '/' {
18+
return path
19+
}
20+
c := path[0]
21+
if !((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) {
22+
return path
23+
}
24+
25+
resolved := resolveDrive(path[:3])
26+
if resolved == "" {
27+
return path
28+
}
29+
return resolved + path[2:]
30+
}

go/extractor/subst/subst_other.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
//go:build !windows
2+
3+
package subst
4+
5+
// ResolveDrive is a no-op on non-Windows platforms.
6+
func ResolveDrive(driveRoot string) string { return "" }

go/extractor/subst/subst_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package subst
2+
3+
import "testing"
4+
5+
func TestResolvePath(t *testing.T) {
6+
t.Parallel()
7+
8+
tests := []struct {
9+
name string
10+
path string
11+
resolveRoot string
12+
resolved string
13+
expected string
14+
}{
15+
{
16+
name: "resolved backslash path",
17+
path: `X:\dir\file.go`,
18+
resolveRoot: `X:\`,
19+
resolved: `C:\target`,
20+
expected: `C:\target\dir\file.go`,
21+
},
22+
{
23+
name: "resolved slash path",
24+
path: `X:/dir/file.go`,
25+
resolveRoot: `X:/`,
26+
resolved: `C:\target`,
27+
expected: `C:\target/dir/file.go`,
28+
},
29+
{
30+
name: "lowercase drive letter",
31+
path: `x:\dir\file.go`,
32+
resolveRoot: `x:\`,
33+
resolved: `C:\target`,
34+
expected: `C:\target\dir\file.go`,
35+
},
36+
{
37+
name: "unresolved drive",
38+
path: `X:\dir\file.go`,
39+
resolveRoot: `X:\`,
40+
expected: `X:\dir\file.go`,
41+
},
42+
{
43+
name: "relative path",
44+
path: `dir\file.go`,
45+
expected: `dir\file.go`,
46+
},
47+
{
48+
name: "non drive prefix",
49+
path: `\\server\share\file.go`,
50+
expected: `\\server\share\file.go`,
51+
},
52+
{
53+
name: "missing separator after colon",
54+
path: `X:file.go`,
55+
expected: `X:file.go`,
56+
},
57+
}
58+
59+
for _, test := range tests {
60+
test := test
61+
t.Run(test.name, func(t *testing.T) {
62+
t.Parallel()
63+
64+
resolveCalls := 0
65+
actual := resolvePath(test.path, func(driveRoot string) string {
66+
resolveCalls++
67+
if driveRoot != test.resolveRoot {
68+
t.Fatalf("resolvePath passed drive root %q, want %q", driveRoot, test.resolveRoot)
69+
}
70+
return test.resolved
71+
})
72+
73+
if actual != test.expected {
74+
t.Fatalf("resolvePath(%q) = %q, want %q", test.path, actual, test.expected)
75+
}
76+
77+
wantCalls := 0
78+
if test.resolveRoot != "" {
79+
wantCalls = 1
80+
}
81+
if resolveCalls != wantCalls {
82+
t.Fatalf("resolvePath(%q) made %d resolve calls, want %d", test.path, resolveCalls, wantCalls)
83+
}
84+
})
85+
}
86+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//go:build windows
2+
3+
package subst
4+
5+
import (
6+
"os"
7+
"path/filepath"
8+
"syscall"
9+
"unsafe"
10+
)
11+
12+
var (
13+
dll *syscall.DLL
14+
procResolve *syscall.Proc
15+
procFree *syscall.Proc
16+
available bool
17+
)
18+
19+
func init() {
20+
dist := os.Getenv("CODEQL_DIST")
21+
if dist == "" {
22+
return
23+
}
24+
dllPath := filepath.Join(dist, "tools", "win64", "canonicalize.dll")
25+
d, err := syscall.LoadDLL(dllPath)
26+
if err != nil {
27+
return
28+
}
29+
p, err := d.FindProc("resolve_subst_u8")
30+
if err != nil {
31+
return
32+
}
33+
f, _ := d.FindProc("resolve_subst_free_u8")
34+
dll = d
35+
procResolve = p
36+
procFree = f
37+
available = true
38+
}
39+
40+
// ResolveDrive resolves a subst'd drive root (e.g. "X:\") to its backing path.
41+
// Returns "" if the drive is not subst'd or on error.
42+
func ResolveDrive(driveRoot string) string {
43+
if !available {
44+
return ""
45+
}
46+
driveBytes := append([]byte(driveRoot), 0)
47+
ret, _, _ := procResolve.Call(uintptr(unsafe.Pointer(&driveBytes[0])))
48+
if ret == 0 {
49+
return ""
50+
}
51+
result := goString((*byte)(unsafe.Pointer(ret)))
52+
if procFree != nil {
53+
procFree.Call(ret)
54+
}
55+
return result
56+
}
57+
58+
func goString(p *byte) string {
59+
if p == nil {
60+
return ""
61+
}
62+
var n int
63+
for ptr := unsafe.Pointer(p); *(*byte)(ptr) != 0; n++ {
64+
ptr = unsafe.Add(ptr, 1)
65+
}
66+
return string(unsafe.Slice(p, n))
67+
}

java/kotlin-extractor/src/main/java/com/semmle/util/files/FileUtil.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1242,12 +1242,13 @@ public static String relativePathLink (File f, File base)
12421242
public static File tryMakeCanonical (File f)
12431243
{
12441244
try {
1245-
return f.getCanonicalFile();
1245+
f = f.getCanonicalFile();
12461246
}
12471247
catch (IOException ignored) {
12481248
Exceptions.ignore(ignored, "Can't log error: Could be too verbose.");
1249-
return new File(simplifyPath(f));
1249+
f = new File(simplifyPath(f));
12501250
}
1251+
return SubstResolver.resolve(f);
12511252
}
12521253

12531254

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.semmle.util.files;
2+
3+
import java.io.File;
4+
import java.nio.file.Path;
5+
import java.nio.file.Paths;
6+
7+
/**
8+
* Resolves Windows {@code subst}ed drive letters to their underlying paths. On non-Windows
9+
* platforms, or when the native library failed to load, {@link #resolve(File)} is a no-op that
10+
* returns its argument unchanged.
11+
*/
12+
public class SubstResolver {
13+
private static final boolean available;
14+
15+
static {
16+
boolean loaded = false;
17+
if (File.separatorChar == '\\') {
18+
String dist = System.getenv("CODEQL_DIST");
19+
if (dist != null && !dist.isEmpty()) {
20+
try {
21+
Path library = Paths.get(dist).resolve("tools").resolve("win64")
22+
.resolve("canonicalize.dll").toAbsolutePath();
23+
System.load(library.toString());
24+
loaded = true;
25+
} catch (RuntimeException | UnsatisfiedLinkError ignored) {
26+
}
27+
}
28+
}
29+
available = loaded;
30+
}
31+
32+
private SubstResolver() {}
33+
34+
/**
35+
* Given a drive root like {@code "X:\\"}, returns the path that drive is
36+
* {@code subst}ed to, or {@code null} if the letter isn't a subst mapping.
37+
*/
38+
private static native String nativeResolveSubst(String driveRoot);
39+
40+
/**
41+
* If {@code f} is an absolute path starting with a {@code subst}ed drive letter, return an
42+
* equivalent path with the drive letter replaced by its target. Otherwise return {@code f}
43+
* unchanged.
44+
*/
45+
public static File resolve(File f) {
46+
if (!available) {
47+
return f;
48+
}
49+
String path = f.getPath();
50+
if (path.length() < 3 || path.charAt(1) != ':') {
51+
return f;
52+
}
53+
char sep = path.charAt(2);
54+
if (sep != '\\' && sep != '/') {
55+
return f;
56+
}
57+
if (!isDriveLetter(path.charAt(0))) {
58+
return f;
59+
}
60+
61+
String resolved = nativeResolveSubst(path.substring(0, 3));
62+
if (resolved == null) {
63+
return f;
64+
}
65+
66+
return new File(resolved + path.substring(2));
67+
}
68+
69+
private static boolean isDriveLetter(char c) {
70+
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
71+
}
72+
73+
public static boolean isAvailable() {
74+
return available;
75+
}
76+
}

0 commit comments

Comments
 (0)