Skip to content
Merged
5 changes: 5 additions & 0 deletions tsc/internal/compiler/fileloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/microsoft/TypeScript/tsc/internal/core"
"github.com/microsoft/TypeScript/tsc/internal/diagnostics"
"github.com/microsoft/TypeScript/tsc/internal/module"
"github.com/microsoft/TypeScript/tsc/internal/nativepath"
"github.com/microsoft/TypeScript/tsc/internal/parser"
"github.com/microsoft/TypeScript/tsc/internal/spanmap"
"github.com/microsoft/TypeScript/tsc/internal/tracing"
Expand Down Expand Up @@ -687,6 +688,10 @@ func (p *fileLoader) getSourceFileFromReference(
allowNonTsExtensions := options.AllowNonTsExtensions.IsTrue()
diagnosticFileName := tspath.NormalizeSlashes(referenceText)

if nativepath.IsNetworkPath(fileName.AsString()) {
return "", "", &sourceFileFromReferenceDiagnostic{message: diagnostics.Network_path_0_is_not_supported, args: []string{diagnosticFileName}}
}

if fileName.HasExtension() {
canonicalFileName := p.caseSensitivity.PathKey(tspath.RootedPath(fileName))
if !allowNonTsExtensions && !p.isSupportedExtension(canonicalFileName) {
Expand Down
3 changes: 3 additions & 0 deletions tsc/internal/compiler/program.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/microsoft/TypeScript/tsc/internal/locale"
"github.com/microsoft/TypeScript/tsc/internal/module"
"github.com/microsoft/TypeScript/tsc/internal/modulespecifiers"
"github.com/microsoft/TypeScript/tsc/internal/nativepath"
"github.com/microsoft/TypeScript/tsc/internal/outputpaths"
"github.com/microsoft/TypeScript/tsc/internal/packagejson"
"github.com/microsoft/TypeScript/tsc/internal/parser"
Expand Down Expand Up @@ -1181,6 +1182,8 @@ func (p *Program) verifyCompilerOptions() {
}
if !tspath.PathIsRelative(subst) && !tspath.PathIsAbsolute(subst) {
createDiagnosticForOptionPathKeyValue(key, i, diagnostics.Non_relative_paths_are_not_allowed_Did_you_forget_a_leading_Slash)
} else if nativepath.IsNetworkPath(tspath.GetNormalizedAbsolutePath(subst, options.GetPathsBasePath(p.GetCurrentDirectory()))) {
createDiagnosticForOptionPathKeyValue(key, i, diagnostics.Network_path_0_is_not_supported, subst)
}
}
}
Expand Down
1 change: 1 addition & 0 deletions tsc/internal/diagnostics/diagnosticMessages.generated.json
Original file line number Diff line number Diff line change
Expand Up @@ -1194,6 +1194,7 @@
"_0_is_only_assignable_to_the_non_distributed_1_but_1_has_been_distributed_here_5113": "'{0}' is only assignable to the non-distributed '{1}', but '{1}' has been distributed here.",
"Instantiations_of_type_0_appear_infinitely_circular_5114": "Instantiations of type '{0}' appear infinitely circular.",
"Instantiations_of_the_following_types_appear_infinitely_circular_Colon_0_5115": "Instantiations of the following types appear infinitely circular: {0}.",
"Network_path_0_is_not_supported_5116": "Network path '{0}' is not supported.",
"Generates_a_sourcemap_for_each_corresponding_d_ts_file_6000": "Generates a sourcemap for each corresponding '.d.ts' file.",
"Concatenate_and_emit_output_to_single_file_6001": "Concatenate and emit output to single file.",
"Generates_corresponding_d_ts_file_6002": "Generates corresponding '.d.ts' file.",
Expand Down
4 changes: 4 additions & 0 deletions tsc/internal/diagnostics/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -4792,6 +4792,10 @@
"category": "Error",
"code": 5115
},
"Network path '{0}' is not supported.": {
"category": "Error",
"code": 5116
},

"Generates a sourcemap for each corresponding '.d.ts' file.": {
"category": "Message",
Expand Down
3 changes: 3 additions & 0 deletions tsc/internal/diagnostics/diagnostics_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 15 additions & 1 deletion tsc/internal/fswatch/watcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ var errRootPath = errors.New("fswatch: cannot watch a root path")
// [Watcher.WatchFile] when the supplied path is not absolute.
var errNotAbsolute = errors.New("fswatch: path must be absolute")

// errNetworkPath is returned by [Watcher.WatchDirectory] and
// [Watcher.WatchFile] when the supplied path may refer to a network
// location. See [nativepath.IsNetworkPath].
var errNetworkPath = errors.New("fswatch: network paths are not supported")

// ErrOverflow indicates that the kernel event queue overflowed and
// some filesystem changes were missed. The watch remains
// active; further events will continue to be delivered. Callers
Expand Down Expand Up @@ -523,6 +528,10 @@ func (w *watcher) WatchDirectories(requests []WatchDirectoryRequest) ([]Watch, e
rollback()
return nil, errNilCallback
}
if nativepath.IsNetworkPath(dir) {
rollback()
return nil, errNetworkPath
}
dir = filepath.Clean(dir)
if !filepath.IsAbs(dir) {
rollback()
Expand Down Expand Up @@ -595,6 +604,9 @@ func (w *watcher) WatchFile(path string, fn WatchCallback) (Watch, error) {
if !w.Available() {
return nil, ErrUnavailable
}
if nativepath.IsNetworkPath(path) {
return nil, errNetworkPath
}
path = filepath.Clean(path)
if !filepath.IsAbs(path) {
return nil, errNotAbsolute
Expand Down Expand Up @@ -856,7 +868,9 @@ func physicalDirFor(dir string) string {
if err != nil {
return dir
}
if realpath == dir {
// A mapped drive resolves to the network share behind it; keep watching
// it through the drive letter rather than opening the share directly.
if realpath == dir || nativepath.IsNetworkPath(realpath) {
return dir
}
return canonicalizePath(filepath.Clean(realpath))
Expand Down
29 changes: 29 additions & 0 deletions tsc/internal/fswatch/watcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1527,6 +1527,35 @@ func TestWatchDirectoriesBatch(t *testing.T) {
})
}

func TestWatchRejectsNetworkPaths(t *testing.T) {
t.Parallel()
runForEachWatcher(t, func(t testingT, watcherImpl Watcher) {
dir := newTmpDir(t)
r := newRecorder(t)
r.watcher = watcherImpl

networkPaths := []string{`\\server\share`, `\??\UNC\server\share`}
if runtime.GOOS != "windows" {
// On POSIX systems, //dir resolves to /dir, so this is an existing
// directory that must still be refused.
networkPaths = append(networkPaths, "/"+dir)
}
for _, networkPath := range networkPaths {
_, err := watcherImpl.WatchDirectories([]WatchDirectoryRequest{
{Dir: dir, Callback: r.callback},
{Dir: networkPath, Callback: r.callback},
})
if !errors.Is(err, errNetworkPath) {
t.Fatalf("WatchDirectories(%q) error = %v, want %v", networkPath, err, errNetworkPath)
}
_, err = watcherImpl.WatchFile(networkPath+"/file.ts", r.callback)
if !errors.Is(err, errNetworkPath) {
t.Fatalf("WatchFile(%q) error = %v, want %v", networkPath, err, errNetworkPath)
}
}
})
}

type countingWatcherImpl struct {
watcherBase
subscribed []*dirWatch
Expand Down
23 changes: 23 additions & 0 deletions tsc/internal/nativepath/network.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package nativepath

// IsNetworkPath reports whether Windows may resolve path over the network.
// This includes UNC paths (`//server/share`), Win32 device paths (`//?/...`,
// `//./...`), and NT object paths (`/??/...`), with either separator. Device
// and object paths are included even when they name local volumes, because
// their `UNC` and `GLOBALROOT` forms reach the network redirector.
//
// Accessing such a path can make Windows authenticate to an arbitrary host,
// leaking credentials, so these paths are never accessed on any platform.
func IsNetworkPath(path string) bool {
if len(path) < 2 || !isAnyDirectorySeparator(path[0]) {
return false
}
if isAnyDirectorySeparator(path[1]) {
return true
}
return len(path) >= 3 && path[1] == '?' && path[2] == '?' && (len(path) == 3 || isAnyDirectorySeparator(path[3]))
}

func isAnyDirectorySeparator(char byte) bool {
return char == '/' || char == '\\'
}
56 changes: 56 additions & 0 deletions tsc/internal/nativepath/network_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package nativepath

import (
"testing"

"gotest.tools/v3/assert"
)

func TestIsNetworkPath(t *testing.T) {
t.Parallel()
for _, path := range []string{
"//server",
"//server/share/file.ts",
"\\\\server\\share\\file.ts",
"/\\server/share",
"\\/server/share",
"//?/UNC/server/share",
"\\\\?\\UNC\\server\\share",
"//./UNC/server/share",
"\\\\.\\GLOBALROOT\\Device\\Mup\\server\\share",
"\\\\?\\c:\\file.ts",
"\\\\wsl$\\Ubuntu\\home",
"//wsl.localhost/Ubuntu/home",
"/??",
"/??/UNC/server/share",
"\\??\\UNC\\server\\share",
"\\??\\GLOBALROOT\\Device\\Mup\\server\\share",
} {
assert.Assert(t, IsNetworkPath(path), path)
}
for _, path := range []string{
"",
"/",
"\\",
"a",
"a//b",
"./a",
"../a",
"/a",
"/a//b",
"/?",
"/???",
"/??a",
"/a/??/b",
"c:",
"c:/",
"c:\\file.ts",
"c:file.ts",
"file:///c:/file.ts",
"file://server/share",
"^/untitled/ts-nul-authority/Untitled-1",
"bundled:///libs/lib.d.ts",
} {
assert.Assert(t, !IsNetworkPath(path), path)
}
}
28 changes: 21 additions & 7 deletions tsc/internal/pprof/pprof.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,22 @@ import (
"runtime/pprof"
"sync"
"time"

"github.com/microsoft/TypeScript/tsc/internal/nativepath"
)

// makeProfileDir creates profileDir, refusing network paths so that
// profiling requests cannot trigger connections to remote hosts.
func makeProfileDir(profileDir string) error {
if nativepath.IsNetworkPath(profileDir) {
return errors.New("network paths are not supported: " + profileDir)
}
if err := os.MkdirAll(profileDir, 0o755); err != nil {
return fmt.Errorf("failed to create profile directory: %w", err)
}
return nil
}

type ProfileSession struct {
cpuFilePath string
memFilePath string
Expand All @@ -21,7 +35,7 @@ type ProfileSession struct {

// BeginProfiling starts CPU and memory profiling, writing the profiles to the specified directory.
func BeginProfiling(profileDir string, logWriter io.Writer) *ProfileSession {
if err := os.MkdirAll(profileDir, 0o755); err != nil {
if err := makeProfileDir(profileDir); err != nil {
panic(err)
}

Expand Down Expand Up @@ -80,8 +94,8 @@ func (c *CPUProfiler) StartCPUProfile(profileDir string) error {
return errors.New("CPU profiling already in progress")
}

if err := os.MkdirAll(profileDir, 0o755); err != nil {
return fmt.Errorf("failed to create profile directory: %w", err)
if err := makeProfileDir(profileDir); err != nil {
return err
}

cpuProfilePath := filepath.Join(profileDir, fmt.Sprintf("%d-%d-cpuprofile.pb.gz", os.Getpid(), time.Now().UnixMilli()))
Expand Down Expand Up @@ -122,8 +136,8 @@ func (c *CPUProfiler) StopCPUProfile() (string, error) {

// SaveHeapProfile saves a heap profile to the specified directory.
func SaveHeapProfile(profileDir string) (string, error) {
if err := os.MkdirAll(profileDir, 0o755); err != nil {
return "", fmt.Errorf("failed to create profile directory: %w", err)
if err := makeProfileDir(profileDir); err != nil {
return "", err
}

heapProfilePath := filepath.Join(profileDir, fmt.Sprintf("%d-%d-heapprofile.pb.gz", os.Getpid(), time.Now().UnixMilli()))
Expand All @@ -144,8 +158,8 @@ func SaveHeapProfile(profileDir string) (string, error) {

// SaveAllocProfile saves an allocation profile to the specified directory.
func SaveAllocProfile(profileDir string) (string, error) {
if err := os.MkdirAll(profileDir, 0o755); err != nil {
return "", fmt.Errorf("failed to create profile directory: %w", err)
if err := makeProfileDir(profileDir); err != nil {
return "", err
}

allocProfilePath := filepath.Join(profileDir, fmt.Sprintf("%d-%d-allocprofile.pb.gz", os.Getpid(), time.Now().UnixMilli()))
Expand Down
6 changes: 6 additions & 0 deletions tsc/internal/project/watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/microsoft/TypeScript/tsc/internal/core"
"github.com/microsoft/TypeScript/tsc/internal/ls/lsconv"
"github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto"
"github.com/microsoft/TypeScript/tsc/internal/nativepath"
"github.com/microsoft/TypeScript/tsc/internal/tspath"
)

Expand Down Expand Up @@ -215,6 +216,11 @@ func (w *WatchedFiles[T]) Watchers() Watchers {
w.mu.Lock()
defer w.mu.Unlock()
result := w.computeGlobPatterns(w.input)
// Never ask the client to watch network locations; see nativepath.IsNetworkPath.
result.patternsInsideWorkspace = slices.DeleteFunc(result.patternsInsideWorkspace, nativepath.IsNetworkPath)
result.directoriesOutsideWorkspace = slices.DeleteFunc(result.directoriesOutsideWorkspace, func(dir tspath.RootedDirectoryPath) bool {
return nativepath.IsNetworkPath(dir.AsString())
})
globs := slices.Compact(slices.Sorted(slices.Values(result.patternsInsideWorkspace)))

ignored := result.ignored
Expand Down
24 changes: 24 additions & 0 deletions tsc/internal/project/watch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"testing"

"github.com/microsoft/TypeScript/tsc/internal/collections"
"github.com/microsoft/TypeScript/tsc/internal/core"
"github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto"
"github.com/microsoft/TypeScript/tsc/internal/tspath"
"gotest.tools/v3/assert"
Expand All @@ -30,6 +31,29 @@ func TestNilWatchedFilesClone(t *testing.T) {
assert.Assert(t, result == nil, "clone on a nil `WatchedFiles` should return nil")
}

func TestWatchedFilesOmitsNetworkPaths(t *testing.T) {
t.Parallel()

files := []tspath.RootedFilePath{
"/workspace/a.ts",
"/external/b.ts",
"//server/share/c.ts",
"/??/UNC/server/share/d.ts",
}
for _, test := range []struct {
workspace tspath.RootedDirectoryPath
expected []string
}{
{"/workspace", []string{"/workspace/a.ts", "file:///external/**/*"}},
{"//server/share", []string{"file:///external/**/*", "file:///workspace/**/*"}},
} {
w := NewWatchedFilesForPaths("test", lsproto.WatchKindCreate, true, test.workspace, tspath.CaseSensitive).Clone(files)
watchers := w.Watchers()
globs := core.Map(append(watchers.WorkspaceWatchers, watchers.OutsideWorkspaceWatchers...), fileSystemWatcherGlobString)
assert.DeepEqual(t, globs, test.expected)
}
}

func TestResolutionLookupWatcherPreservesDirectorySpelling(t *testing.T) {
t.Parallel()

Expand Down
4 changes: 3 additions & 1 deletion tsc/internal/tsoptions/commandlineparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,9 @@ func (p *commandLineParser) parseOptionValue(
}
case "string":
val, err := validateJsonOptionValue(opt, args[i], nil, nil)
if err == nil {
if path, ok := val.(string); ok && err == nil && opt.PathKind.IsRooted() && isNetworkPathValue(path, p.currentDirectory) {
p.errors = append(p.errors, ast.NewCompilerDiagnostic(diagnostics.Network_path_0_is_not_supported, path))
} else if err == nil {
p.options.Set(opt.Name, val)
} else {
p.errors = append(p.errors, err...)
Expand Down
12 changes: 12 additions & 0 deletions tsc/internal/tsoptions/commandlineparser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,18 @@ func TestResponseFileParsing(t *testing.T) {
assert.Assert(t, parsed.CompilerOptions().Strict.IsTrue())
assert.Equal(t, parsed.CompilerOptions().OutDir, tspath.RootedDirectoryPath("/project/dist"))
})

t.Run("network path", func(t *testing.T) {
t.Parallel()
fs := tsoptionstest.NewVFS(map[string]string{
"/project/args.txt": `--pprofDir \\server\share --outDir dist`,
}, tspath.CaseSensitive)
parsed := tsoptions.ParseCommandLine([]string{"@args.txt"}, fs, "/project")
assert.Equal(t, len(parsed.Errors), 1)
assert.Equal(t, parsed.Errors[0].Code(), diagnostics.Network_path_0_is_not_supported.Code())
assert.Equal(t, parsed.CompilerOptions().PprofDir, tspath.RootedDirectoryPath(""))
assert.Equal(t, parsed.CompilerOptions().OutDir, tspath.RootedDirectoryPath("/project/dist"))
})
}

func TestParseCommandLineTypeRootsRelativePath(t *testing.T) {
Expand Down
Loading
Loading