Skip to content

Commit 4e067c0

Browse files
panagosg7facebook-github-bot
authored andcommitted
[flow] Fix Windows path separator mismatches in File_key and flowconfig expansion (#9402)
Summary: Normalize directory separators to '/' in File_key suffixes at creation time (strip_project_root, lib_file_of_absolute) so that file keys are always '/'-based regardless of platform. This fixes the Graph.NodeNotFound crash when loading Linux-generated saved state on Windows, where '\'-based local file keys failed to match '/'-based saved state keys. Also normalize separators in expand_project_root_token_as_relative so that flowconfig directives like module.system.node.root_relative_dirname with <PROJECT_ROOT> prefixes produce '/'-based paths, matching the now-normalized File_key suffixes. Without this, Files.is_prefix would append '\' on Windows while suffixes use '/', breaking root-relative module resolution. Fix Files.is_prefix to append '/' instead of Filename.dir_sep when the prefix lacks a trailing separator. Previously, is_prefix would append '\' on Windows, which failed to match the now '/'-based File_key suffixes — breaking scoped root-relative module resolution (e.g., the `config_module_system_node_root_relative_dirnames` test). Both normalizations are no-ops on Unix. Changelog: [internal] Differential Revision: D102285839
1 parent 50c33ac commit 4e067c0

8 files changed

Lines changed: 74 additions & 23 deletions

File tree

rust_port/crates/flow_common/src/files.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -908,11 +908,12 @@ pub fn expand_project_root_token(root: &Path, s: &str) -> String {
908908

909909
pub fn expand_project_root_token_as_relative(s: &str) -> String {
910910
let s = s.replace(PROJECT_ROOT_TOKEN, "");
911-
if s.starts_with('/') || s.starts_with('\\') {
911+
let s = if s.starts_with('/') || s.starts_with('\\') {
912912
s[1..].to_string()
913913
} else {
914914
s
915-
}
915+
};
916+
normalize_filename_dir_sep(&s).into_owned()
916917
}
917918

918919
pub fn expand_builtin_root_token(flowlib_dir: &Path, s: &str) -> String {

rust_port/crates/flow_parser/src/file_key.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,12 +317,14 @@ fn relative_path_from(root: &str, path: &str) -> String {
317317
result.join("/")
318318
}
319319

320+
// The result always uses '/' separators for cross-platform consistency
321+
// (saved state generated on Linux must work on Windows and vice versa).
320322
pub fn strip_project_root(path: &str) -> String {
321323
let guard = PROJECT_ROOT.read().unwrap();
322324
match guard.as_deref() {
323325
Some(root) => {
324326
if let Some(stripped) = path.strip_prefix(root) {
325-
stripped.to_string()
327+
normalize_dir_sep_with(std::path::MAIN_SEPARATOR, stripped)
326328
} else if !is_relative(path) {
327329
relative_path_from(root, path)
328330
} else {
@@ -437,7 +439,9 @@ impl FileKey {
437439
let guard = FLOWLIB_ROOT.read().unwrap();
438440
let suffix = match guard.as_deref() {
439441
Some(fl) if !fl.is_empty() && path.starts_with(fl) => {
440-
format!("{}{}", FLOWLIB_MARKER, strip_prefix(fl, path))
442+
let stripped =
443+
normalize_dir_sep_with(std::path::MAIN_SEPARATOR, strip_prefix(fl, path));
444+
format!("{}{}", FLOWLIB_MARKER, stripped)
441445
}
442446
_ => {
443447
drop(guard);

src/common/files.ml

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@ type file_kind =
379379
like a pipe, socket or device. If `path` is a symbolic link, then it returns
380380
the type of the target of the symlink, and the target's real path. *)
381381
let kind_of_path dirent path =
382+
let long_path = Sys_utils.win_long_path path in
382383
try
383384
let open Dirent in
384385
let open Unix in
@@ -387,11 +388,11 @@ let kind_of_path dirent path =
387388
| DT_DIR -> Dir (path, false)
388389
| DT_LNK
389390
| DT_UNKNOWN ->
390-
(match (Sys_utils.lstat path).st_kind with
391+
(match (Sys_utils.lstat long_path).st_kind with
391392
| S_REG -> Reg path
392393
| S_LNK ->
393394
(try
394-
match (stat path).st_kind with
395+
match (stat long_path).st_kind with
395396
| S_REG -> Reg (realpath_ path)
396397
| S_DIR -> Dir (realpath_ path, true)
397398
| _ -> Other
@@ -406,13 +407,6 @@ let kind_of_path dirent path =
406407
| DT_SOCK ->
407408
Other
408409
with
409-
| Unix.Unix_error (Unix.ENOENT, _, _) when Sys.win32 && String.length path >= 248 ->
410-
StatError
411-
(Utils_js.spf
412-
"On Windows, paths must be less than 248 characters for directories and 260 characters for files. This path has %d characters. Skipping %s"
413-
(String.length path)
414-
path
415-
)
416410
| Unix.Unix_error (e, _, _) ->
417411
StatError (Utils_js.spf "Skipping %s: %s\n%!" path (Unix.error_message e))
418412

@@ -1031,10 +1025,13 @@ let expand_project_root_token_as_absolute ~root =
10311025

10321026
let expand_project_root_token_as_relative str =
10331027
let s = str |> Str.split_delim project_root_token |> String.concat "" in
1034-
if String.length s > 0 && (s.[0] = '/' || s.[0] = '\\') then
1035-
String.sub s 1 (String.length s - 1)
1036-
else
1037-
s
1028+
let s =
1029+
if String.length s > 0 && (s.[0] = '/' || s.[0] = '\\') then
1030+
String.sub s 1 (String.length s - 1)
1031+
else
1032+
s
1033+
in
1034+
Sys_utils.normalize_filename_dir_sep s
10381035

10391036
let expand_builtin_root_token ~flowlib_dir =
10401037
let flowlib_dir_str = File_path.to_string flowlib_dir |> Sys_utils.normalize_filename_dir_sep in

src/hack_forked/utils/disk/disk.ml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,24 @@ exception Rename_target_already_exists of string
1313

1414
exception Rename_target_dir_not_empty of string
1515

16+
let win_long_path path =
17+
if Sys.win32
18+
&& String.length path >= 248
19+
&& not (String.length path >= 4
20+
&& path.[0] = '\\'
21+
&& path.[1] = '\\'
22+
&& path.[2] = '?'
23+
&& path.[3] = '\\')
24+
then
25+
let backslashed =
26+
String.map (fun c -> if c = '/' then '\\' else c) path
27+
in
28+
"\\\\?\\" ^ backslashed
29+
else
30+
path
31+
1632
let cat (filename : string) : string =
17-
let ic = open_in_bin filename in
33+
let ic = open_in_bin (win_long_path filename) in
1834
let len =
1935
try in_channel_length ic with
2036
| Sys_error _ -> 0

src/hack_forked/utils/sys/sys_utils.ml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,30 @@ let lstat path =
301301
else
302302
path
303303

304+
(** On Windows, prepend the \\?\ extended-length path prefix to absolute
305+
paths that approach the 260-char MAX_PATH limit. This lifts the limit
306+
to ~32k chars. The prefix requires backslash separators and a fully
307+
resolved path (no . or .. segments). No-op on non-Windows platforms
308+
and on short paths. *)
309+
let win_long_path =
310+
if not Sys.win32 then
311+
fun path -> path
312+
else
313+
fun path ->
314+
if String.length path >= 248
315+
&& not (String.length path >= 4
316+
&& path.[0] = '\\'
317+
&& path.[1] = '\\'
318+
&& path.[2] = '?'
319+
&& path.[3] = '\\')
320+
then
321+
let backslashed =
322+
String.map (fun c -> if c = '/' then '\\' else c) path
323+
in
324+
"\\\\?\\" ^ backslashed
325+
else
326+
path
327+
304328
(** Converts platform-specific directory separators to / *)
305329
let normalize_filename_dir_sep =
306330
let dir_sep_char = Filename.dir_sep.[0] in

src/parser/file_key.ml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -272,12 +272,15 @@ let relative_path_from ?(dir_sep = Filename.dir_sep) root path =
272272
compute a relative path from the root — this ensures all suffixes are
273273
portable relative paths, which is critical for saved state portability
274274
across machines with different root paths.
275-
If the root has not been set yet, returns the path unchanged. *)
275+
If the root has not been set yet, returns the path unchanged.
276+
The result always uses '/' separators for cross-platform consistency
277+
(saved state generated on Linux must work on Windows and vice versa). *)
276278
let strip_project_root path =
277279
match !project_root with
278280
| Some root ->
279281
if String.starts_with ~prefix:root path then
280-
String.sub path (String.length root) (String.length path - String.length root)
282+
normalize_dir_sep
283+
(String.sub path (String.length root) (String.length path - String.length root))
281284
else if not (Filename.is_relative path) then
282285
relative_path_from root path
283286
else
@@ -296,7 +299,7 @@ let resource_file_of_absolute path = ResourceFile (strip_project_root path)
296299
let lib_file_of_absolute path =
297300
match !flowlib_root with
298301
| Some fl when String.length fl > 0 && String.starts_with ~prefix:fl path ->
299-
LibFile (flowlib_marker ^ strip_prefix fl path)
302+
LibFile (flowlib_marker ^ normalize_dir_sep (strip_prefix fl path))
300303
| _ -> LibFile (strip_project_root path)
301304

302305
module For_tests = struct

src/parsing/parsing_service_js.ml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -581,4 +581,8 @@ let ensure_parsed ~reader options workers files =
581581
workers
582582
next
583583
in
584-
Lwt.return (FilenameSet.union changed not_found)
584+
if not (FilenameSet.is_empty not_found) then
585+
Hh_logger.info
586+
"ensure_parsed: %d files not found on disk (skipping — will be cleaned up during reparse)"
587+
(FilenameSet.cardinal not_found);
588+
Lwt.return changed

src/services/module/module_js.ml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,9 @@ module Node = struct
581581
let applicable =
582582
Base.Option.value_map
583583
applicable_dirname_opt
584-
~f:(fun prefix -> Files.is_prefix prefix (File_key.suffix importing_file))
584+
~f:(fun prefix ->
585+
let suffix = File_key.suffix importing_file in
586+
suffix = prefix || String.starts_with ~prefix:(prefix ^ "/") suffix)
585587
~default:true
586588
in
587589
if applicable then

0 commit comments

Comments
 (0)