Skip to content

Commit 6e7f42d

Browse files
committed
Fix Windows \\?\-style paths
In Windows, allow paths prefixed with \\?\ to be supplied as user input for root paths and as link values when following links. https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file In [findWorkingDir], call to [removeTrailingSlashes] is no longer needed because [Filename.basename] is POSIX-compliant since OCaml 4.00.
1 parent 382b298 commit 6e7f42d

File tree

2 files changed

+46
-12
lines changed

2 files changed

+46
-12
lines changed

src/fspath.ml

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,46 @@ let toPrintString (Fspath f) = f
4040
let toDebugString (Fspath f) = String.escaped f
4141

4242
(* Needed to hack around some ocaml/Windows bugs, see comment at stat, below *)
43-
let winRootRx = Rx.rx "(([a-zA-Z]:)?/|//[^/]+/[^/]+/)"
43+
let winRootRx = Rx.rx "(([a-zA-Z]:)?/|//[^?/]+/[^/]+/|//[?]/[Uu][Nn][Cc]/[^/]+/[^/]+/)|//[?]/([^Uu][^/]*|[Uu]|[Uu][^Nn][^/]*|[Uu][Nn]|[Uu][Nn][^Cc][^/]*|[Uu][Nn][Cc][^/]+)/"
4444
(* FIX I think we could just check the last character of [d]. *)
4545
let isRootDir d =
4646
(* We assume all path separators are slashes in d *)
4747
d="/" ||
4848
(Util.osType = `Win32 && Rx.match_string winRootRx d)
49-
let winRootFixRx = Rx.rx "//[^/]+/[^/]+"
49+
(* Here, backslashes are allowed as path separators in Windows *)
50+
let isRootDirLocalString d =
51+
let d =
52+
if Util.osType = `Win32 then Fileutil.backslashes2forwardslashes d else d
53+
in
54+
isRootDir ((Fileutil.removeTrailingSlashes d) ^ "/")
5055
let winRootFix d =
51-
if Rx.match_string winRootFixRx d then d^"/" else d
56+
if Rx.match_string winRootRx (d ^ "/") then d ^ "/" else d
57+
let winFNsPrefixRx = Rx.rx "[\\/][\\/][?][\\/][^\\/]+"
58+
let isInvalidWinPath p =
59+
Rx.match_string winFNsPrefixRx p (* Is there a path after the prefix? *)
60+
let winSafeDirname p =
61+
if Util.osType <> `Win32 then
62+
Filename.dirname p
63+
else
64+
(* [Filename.dirname] can't handle Windows paths prefixed with \\?\
65+
(Win32 file namespace) if [dirname] goes all the way up to the fs root.
66+
Most paths are still processed correctly because they are basically a
67+
DOS path prefixed with \\?\ or something similar to \\server\share\
68+
paths. Only paths right at the fs root are problematic.
69+
70+
\\?\C:\ becomes \\? (correct is \\?\C:\)
71+
\\?\C:\sub becomes \\?\C (correct is \\?\C:\)
72+
\\?\Volume{GUID}\ becomes \\? (correct is \\?\Volume{GUID}\)
73+
\\?\Volume{GUID}\sub becomes \\?\Volume{GUID} (correct is \\?\Volume{GUID}\)
74+
75+
As a workaround, first remove the \\?\ prefix and the first component of
76+
the path (usually this would be the "volume", except for UNC paths).
77+
Then add the removed prefix back to the result of [dirname]. *)
78+
match Rx.match_prefix winFNsPrefixRx p 0 with
79+
| None -> Filename.dirname p
80+
| Some pos ->
81+
String.sub p 0 pos ^
82+
Filename.dirname (String.sub p pos (String.length p - pos))
5283

5384
(* [differentSuffix: fspath -> fspath -> (string * string)] returns the *)
5485
(* least distinguishing suffixes of two fspaths, for displaying in the user *)
@@ -250,11 +281,12 @@ let canonizeFspath p0 =
250281
(* fails, we just quit. This works nicely for most cases of (1), *)
251282
(* it works for (2), and on (3) it may leave a mess for someone *)
252283
(* else to pick up. *)
253-
let p = if Util.osType = `Win32 then Fileutil.backslashes2forwardslashes p else p in
254-
if isRootDir p then raise
284+
if isRootDirLocalString p || isInvalidWinPath p then raise
255285
(Util.Fatal (Printf.sprintf
256-
"Cannot find canonical name of root directory %s\n(%s)" p why));
257-
let parent = Filename.dirname p in
286+
"Cannot find canonical name of root directory %s\n(%s)%s" p why
287+
(if isInvalidWinPath p then "\nMaybe you need to add a "
288+
^ "backslash at end of the root path?" else "")));
289+
let parent = winSafeDirname p in
258290
let parent' = begin
259291
(try System.chdir parent with
260292
Sys_error why2 -> raise (Util.Fatal (Printf.sprintf
@@ -333,7 +365,7 @@ let findWorkingDir fspath path =
333365
334366
[chdir] hack from [canonizeFspath] above seems to be the current
335367
best compromise. *)
336-
Filename.concat (Filename.dirname p) link
368+
Filename.concat (winSafeDirname p) link
337369
|> fun l ->
338370
if Util.osType = `Win32 then
339371
let Fspath l' = canonizeFspath (Some l) in
@@ -345,19 +377,18 @@ let findWorkingDir fspath path =
345377
| Unix.Unix_error _ | Util.Fatal _ -> p
346378
in
347379
followlinks 0 abspath in
348-
if isRootDir realpath then
380+
if isRootDirLocalString realpath then
349381
raise (Util.Transient(Printf.sprintf
350382
"The path %s is a root directory" abspath));
351-
let realpath = Fileutil.removeTrailingSlashes realpath in
352383
let p = Filename.basename realpath in
353384
debug
354385
(fun() ->
355386
Util.msg "Os.findWorkingDir(%s,%s) = (%s,%s)\n"
356387
(toString fspath)
357388
(Path.toString path)
358-
(Filename.dirname realpath)
389+
(winSafeDirname realpath)
359390
p);
360-
(localString2fspath (Filename.dirname realpath), Path.fromString p)
391+
(localString2fspath (winSafeDirname realpath), Path.fromString p)
361392

362393
let quotes (Fspath f) = Uutil.quotes f
363394
let compare (Fspath f1) (Fspath f2) = compare f1 f2

src/system/system_win.ml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,14 @@ include System_generic
2929
let fixPath f = String.map (function '/' -> '\\' | c -> c) f
3030
let winRootRx = Rx.rx "[a-zA-Z]:[/\\].*"
3131
let winUncRx = Rx.rx "[/\\][/\\][^?/\\]+[/\\][^/\\]+[/\\].*"
32+
let winFileNsPathRx = Rx.rx "//[?]/.+"
3233
let extendedPath f =
3334
if Rx.match_string winRootRx f then
3435
fixPath ("\\\\?\\" ^ f)
3536
else if Rx.match_string winUncRx f then
3637
fixPath ("\\\\?\\UNC" ^ String.sub f 1 (String.length f - 1))
38+
else if Rx.match_string winFileNsPathRx f then
39+
fixPath f
3740
else
3841
f
3942

0 commit comments

Comments
 (0)