@@ -392,6 +392,43 @@ Path := [
392392 to_hash : Path , Hasher -> Hasher
393393 to_hash = |path, hasher| to_os_str(path). to_hash (hasher)
394394
395+ ## Remove the final path component without accessing the filesystem. The
396+ ## returned path preserves the input representation. UTF-8 and Unix paths
397+ ## use `/`. Windows paths recognize drive and share roots and use both
398+ ## separators, except inside verbatim paths, which use `\`.
399+ ##
400+ ## Trailing separators and `.` components are ignored, except for a leading
401+ ## relative `.` and for `.` in a Windows verbatim path. `..` is kept as a
402+ ## component. An empty path, root, or Windows prefix alone has `NoParent`.
403+ ## The parent of a one-component relative path is the empty path.
404+ ##
405+ ## ```roc
406+ ## expect Path.parent("src/main.roc") == Ok(Path.utf8("src"))
407+ ## expect Path.parent("/src/") == Ok(Path.utf8("/"))
408+ ## expect Path.parent("/") == Err(NoParent)
409+ ## ```
410+ parent : Path -> Try (Path , [NoParent ])
411+ parent = |path|
412+ match path {
413+ Utf8 (str) => parent_slash_units(Str . to_utf8 (str )).map_ok (|bytes | Utf8 (str_from_valid_utf8(bytes)))
414+ UnixBytes (bytes) => parent_slash_units(bytes). map_ok (|parent| UnixBytes (parent))
415+ WindowsU16s (units) => parent_windows_units(units). map_ok (|parent| WindowsU16s (parent))
416+ }
417+
418+ ## Iterate over this path and its successive [parent] paths, starting with
419+ ## the original path and ending when there is no parent.
420+ ancestors : Path -> Iter (Path )
421+ ancestors = |path|
422+ Iter . custom (
423+ Ok (path),
424+ Err (Unknown ),
425+ |next|
426+ match next {
427+ Ok (item) => Ok ((item, parent(item)))
428+ Err (NoParent ) => Err (NoMore )
429+ },
430+ )
431+
395432 ## Returns everything after the last directory separator.
396433 filename : Path -> Try (Path , [IsDirPath , EndsInDots ])
397434 filename = |path|
@@ -499,6 +536,135 @@ str_from_valid_utf8 = |bytes|
499536 }
500537 }
501538
539+ parent_slash_units : List (U8 ) -> Try (List (U8 ), [NoParent ])
540+ parent_slash_units = |units| {
541+ is_sep = |unit| unit == ' /'
542+ root_end = if is_sep(units. get (0 ) ?? ' .' ) {
543+ 1
544+ } else {
545+ 0
546+ }
547+ parent_units(units, { prefix_end: 0 , root_end, verbatim: False }, is_sep, ' .' )
548+ }
549+
550+ parent_windows_units : List (U16 ) -> Try (List (U16 ), [NoParent ])
551+ parent_windows_units = |units| {
552+ { end: prefix_end, verbatim } = windows_prefix(units)
553+ # A slash immediately after a verbatim drive prefix still denotes the root.
554+ root_end = if windows_separator(units. get (prefix_end) ?? ' .' ) {
555+ prefix_end + 1
556+ } else {
557+ prefix_end
558+ }
559+ is_sep = |unit| unit == ' \\ ' or (Bool . not (verbatim ) and unit == ' /' )
560+ parent_units(units, { prefix_end, root_end, verbatim }, is_sep, ' .' )
561+ }
562+
563+ # Look backward for the last component, then trim the separators and ignored
564+ # dots before it. Keep the root or prefix intact, as well as a leading `.`.
565+ parent_units : List (unit), { prefix_end : U64 , root_end : U64 , verbatim : Bool }, (unit -> Bool ), unit -> Try (List (unit), [NoParent ])
566+ where [unit. is_eq : unit, unit -> Bool ]
567+ parent_units = |units, { prefix_end, root_end, verbatim }, is_sep, dot| {
568+ is_sep_at = |index| is_sep(units. get (index) ?? dot)
569+ is_dot_at = |index| units. get (index) == Ok (dot)
570+ len = units. len ()
571+ leading_dot = root_end == prefix_end and is_dot_at(root_end) and (len == root_end + 1 or is_sep_at(root_end + 1 ))
572+ body_start = if leading_dot {
573+ root_end + 1
574+ } else {
575+ root_end
576+ }
577+ trim_end = |end| {
578+ var $end = end
579+ while $end > body_start {
580+ last = $end - 1
581+ if is_sep_at(last) {
582+ $end = last
583+ } else if Bool . not (verbatim ) and is_dot_at(last) and (last == body_start or is_sep_at(last - 1 )) {
584+ $end = last
585+ } else {
586+ break
587+ }
588+ }
589+ $end
590+ }
591+
592+ var $end = trim_end(len)
593+ if $end == body_start {
594+ if leading_dot and prefix_end == 0 {
595+ Ok ([])
596+ } else {
597+ Err (NoParent )
598+ }
599+ } else {
600+ while $end > body_start and Bool . not (is_sep_at ($end - 1 )) {
601+ $end = $end - 1
602+ }
603+ Ok (units. take_first (trim_end($end)))
604+ }
605+ }
606+
607+ windows_separator : U16 -> Bool
608+ windows_separator = |unit| unit == ' /' or unit == ' \\ '
609+
610+ windows_prefix : List (U16 ) -> { end : U64 , verbatim : Bool }
611+ windows_prefix = |units|
612+ match units {
613+ [' \\ ' , ' \\ ' , ' ?' , ' \\ ' , ..] => {
614+ backslash = |unit| unit == ' \\ '
615+ end = match units {
616+ [' \\ ' , ' \\ ' , ' ?' , ' \\ ' , ' U' , ' N' , ' C' , sep, ..] if windows_separator(sep) =>
617+ windows_share_end(units, 8 , backslash). end
618+ [' \\ ' , ' \\ ' , ' ?' , ' \\ ' , drive, ' :' , ..] if is_drive_letter(drive) and (units. len () == 6 or windows_separator(units. get (6 ) ?? ' .' )) => 6
619+ _ => windows_component_end(units, 4 , backslash)
620+ }
621+ { end, verbatim: True }
622+ }
623+ [first, second, ' .' , fourth, ..] if windows_separator(first) and windows_separator(second) and windows_separator(fourth) =>
624+ { end: windows_component_end(units, 4 , windows_separator), verbatim: False }
625+ [first, second, ..] if windows_separator(first) and windows_separator(second) => {
626+ { end, complete } = windows_share_end(units, 2 , windows_separator)
627+ {
628+ end: if complete {
629+ end
630+ } else {
631+ 0
632+ },
633+ verbatim: False ,
634+ }
635+ }
636+ [drive, ' :' , ..] if is_drive_letter(drive) => { end: 2 , verbatim: False }
637+ _ => { end: 0 , verbatim: False }
638+ }
639+
640+ is_drive_letter : U16 -> Bool
641+ is_drive_letter = |unit| (unit >= ' A' and unit <= ' Z' ) or (unit >= ' a' and unit <= ' z' )
642+
643+ windows_share_end : List (U16 ), U64 , (U16 -> Bool ) -> { end : U64 , complete : Bool }
644+ windows_share_end = |units, start, is_sep| {
645+ server_end = windows_component_end(units, start, is_sep)
646+ if server_end == units. len () {
647+ { end: server_end, complete: False }
648+ } else {
649+ share_start = server_end + 1
650+ share_end = windows_component_end(units, share_start, is_sep)
651+ if share_end == share_start {
652+ { end: server_end, complete: False }
653+ } else {
654+ { end: share_end, complete: server_end > start }
655+ }
656+ }
657+ }
658+
659+ windows_component_end : List (U16 ), U64 , (U16 -> Bool ) -> U64
660+ windows_component_end = |units, start, is_sep| {
661+ var $end = start
662+ while $end < units. len () and Bool . not (is_sep (units .get ($end ) ?? ' \\ ' )) {
663+ $end = $end + 1
664+ }
665+ $end
666+ }
667+
502668ends_with_u8 : List (U8 ), U8 -> Bool
503669ends_with_u8 = |list, suffix|
504670 match list {
@@ -850,6 +1016,97 @@ expect Path.to_raw(Path.join(Path.unix("a"), "b")) == UnixBytes([97, 47, 98])
8501016expect Path . to_raw (Path . join (Path . windows ("a "), "b ")) == WindowsU16s ([97 , 92 , 98 ])
8511017expect Path . to_raw (Path . join (Path . utf8 ("a "), "b ")) == Utf8 (" a/b" )
8521018
1019+ ## Parent traversal preserves roots, relative components, and retained spelling.
1020+ expect Path . parent ("src /main .roc ") == Ok (Path . utf8 ("src "))
1021+ expect Path . parent ("main .roc ") == Ok (Path . utf8 (""))
1022+ expect Path . parent ("") == Err (NoParent )
1023+ expect Path . parent ("/") == Err (NoParent )
1024+ expect Path . parent ("///") == Err (NoParent )
1025+ expect Path . parent ("/src ") == Ok (Path . utf8 ("/"))
1026+ expect Path . parent ("///src /main .roc ") == Ok (Path . utf8 ("///src "))
1027+ expect Path . parent ("src ///main .roc ///") == Ok (Path . utf8 ("src "))
1028+ expect Path . parent ("src /./main .roc /.//") == Ok (Path . utf8 ("src "))
1029+ expect Path . parent ("src /./lib /main .roc ") == Ok (Path . utf8 ("src /./lib "))
1030+ expect Path . parent (".") == Ok (Path . utf8 (""))
1031+ expect Path . parent ("././/") == Ok (Path . utf8 (""))
1032+ expect Path . parent ("./main .roc ") == Ok (Path . utf8 ("."))
1033+ expect Path . parent ("..") == Ok (Path . utf8 (""))
1034+ expect Path . parent ("src /..") == Ok (Path . utf8 ("src "))
1035+ expect Path . parent ("src /../main .roc ") == Ok (Path . utf8 ("src /.."))
1036+ expect Path . parent ("/../main .roc ") == Ok (Path . utf8 ("/.."))
1037+ expect Path . parent ("src /dir ../main .roc ") == Ok (Path . utf8 ("src /dir .."))
1038+ expect Path . parent ("src \\main .roc ") == Ok (Path . utf8 (""))
1039+ expect Path . parent (Path . unix ("C :\\src \\main .roc ")) == Ok (Path . unix (""))
1040+
1041+ expect {
1042+ checked_parent = |path| {
1043+ if path == " " {
1044+ return Err (EmptyInput )
1045+ }
1046+ parent = Path . parent
1047+ Ok (parent(path)?)
1048+ }
1049+ checked_parent(" " ) == Err (EmptyInput ) and checked_parent(" /" ) == Err (NoParent ) and checked_parent(" src/main.roc" ) == Ok (Path . utf8 ("src "))
1050+ }
1051+
1052+ ## UTF-8 boundaries, arbitrary Unix bytes, and unpaired Windows surrogates survive.
1053+ expect Path . parent (Path . utf8 ("🐦/é.roc ")).map_ok (Path . to_raw ) == Ok (Utf8 ("🐦"))
1054+ expect Path . parent (Path . unix_bytes ([0xFF, '/', 0xFE])). map_ok (Path . to_raw ) == Ok (UnixBytes ([0xFF]))
1055+ expect Path . parent (Path . unix_bytes ([0xFF])).map_ok (Path . to_raw ) == Ok (UnixBytes ([]))
1056+ expect Path . parent (Path . windows_u16s ([0xD800, '\\', 0xDC00])). map_ok (Path . to_raw ) == Ok (WindowsU16s ([0xD800]))
1057+ expect Path . parent (Path . windows_u16s ([0xD800])).map_ok (Path . to_raw ) == Ok (WindowsU16s ([]))
1058+ expect Path . parent (Path . windows ("🐦\\é.roc ")).map_ok (Path . to_raw ) == Ok (WindowsU16s ([0xD83D, 0xDC26]))
1059+
1060+ ## Windows grammar is determined by the representation on every operating system.
1061+ expect Path . parent (Path . windows ("")) == Err (NoParent )
1062+ expect Path . parent (Path . windows ("\\")) == Err (NoParent )
1063+ expect Path . parent (Path . windows ("src \\main .roc ")) == Ok (Path . windows ("src "))
1064+ expect Path . parent (Path . windows ("src \\lib /main .roc \\.//")) == Ok (Path . windows ("src \\lib "))
1065+ expect Path . parent (Path . windows (".\\main .roc ")) == Ok (Path . windows ("."))
1066+ expect Path . parent (Path . windows ("C :")) == Err (NoParent )
1067+ expect Path . parent (Path . windows ("C :\\")) == Err (NoParent )
1068+ expect Path . parent (Path . windows ("C :/")) == Err (NoParent )
1069+ expect Path . parent (Path . windows ("C :main .roc ")) == Ok (Path . windows ("C :"))
1070+ expect Path . parent (Path . windows ("C :.\\main .roc ")) == Ok (Path . windows ("C :."))
1071+ expect Path . parent (Path . windows ("C :.")) == Err (NoParent )
1072+ expect Path . parent (Path . windows ("C :\\src ")) == Ok (Path . windows ("C :\\"))
1073+ expect Path . parent (Path . windows ("c :/src ")) == Ok (Path . windows ("c :/"))
1074+ expect Path . parent (Path . windows ("\\src ")) == Ok (Path . windows ("\\"))
1075+ expect Path . parent (Path . windows ("\\\\server \\share ")) == Err (NoParent )
1076+ expect Path . parent (Path . windows ("\\\\server \\share \\")) == Err (NoParent )
1077+ expect Path . parent (Path . windows ("\\\\server \\share \\src ")) == Ok (Path . windows ("\\\\server \\share \\"))
1078+ expect Path . parent (Path . windows ("//server /share /src ")) == Ok (Path . windows ("//server /share /"))
1079+ expect Path . parent (Path . windows ("\\\\server ")) == Ok (Path . windows ("\\"))
1080+ expect Path . parent (Path . windows ("\\\\.\\pipe ")) == Err (NoParent )
1081+ expect Path . parent (Path . windows ("\\\\.\\pipe \\name ")) == Ok (Path . windows ("\\\\.\\pipe \\"))
1082+ expect Path . parent (Path . windows ("//./pipe /name ")) == Ok (Path . windows ("//./pipe /"))
1083+
1084+ ## Windows verbatim prefixes preserve dots and treat forward slashes as filename units.
1085+ expect Path . parent (Path . windows ("\\\\?\\C :")) == Err (NoParent )
1086+ expect Path . parent (Path . windows ("\\\\?\\C :\\")) == Err (NoParent )
1087+ expect Path . parent (Path . windows ("\\\\?\\C :\\src ")) == Ok (Path . windows ("\\\\?\\C :\\"))
1088+ expect Path . parent (Path . windows ("\\\\?\\C :\\src \\.")) == Ok (Path . windows ("\\\\?\\C :\\src "))
1089+ expect Path . parent (Path . windows ("\\\\?\\C :\\src \\.\\file ")) == Ok (Path . windows ("\\\\?\\C :\\src \\."))
1090+ expect Path . parent (Path . windows ("\\\\?\\C :\\src /file ")) == Ok (Path . windows ("\\\\?\\C :\\"))
1091+ expect Path . parent (Path . windows ("\\\\?\\C :/src /file ")) == Ok (Path . windows ("\\\\?\\C :/"))
1092+ expect Path . parent (Path . windows ("\\\\?\\UNC \\server \\share ")) == Err (NoParent )
1093+ expect Path . parent (Path . windows ("\\\\?\\UNC \\server \\share \\src ")) == Ok (Path . windows ("\\\\?\\UNC \\server \\share \\"))
1094+ expect Path . parent (Path . windows ("\\\\?\\UNC \\server ")) == Err (NoParent )
1095+ expect Path . parent (Path . windows ("\\\\?\\Volume {abc }\\src ")) == Ok (Path . windows ("\\\\?\\Volume {abc }\\"))
1096+ expect Path . parent (Path . windows ("\\\\?\\Volume {abc }")) == Err (NoParent )
1097+
1098+ ## Ancestors starts with the original path, includes an empty relative parent,
1099+ ## and stops at roots and prefixes. It can also be consumed incrementally.
1100+ expect Path . ancestors ("src /lib /main .roc ").collect () == [Path . utf8 ("src /lib /main .roc "), Path . utf8 ("src /lib "), Path . utf8 ("src "), Path . utf8 ("")]
1101+ expect Path . ancestors ("/src ").collect () == [Path . utf8 ("/src "), Path . utf8 ("/")]
1102+ expect Path . ancestors ("").collect () == [Path . utf8 ("")]
1103+ expect Path . ancestors ("/").collect () == [Path . utf8 ("/")]
1104+ expect Path . ancestors ("src ///").collect () == [Path . utf8 ("src ///"), Path . utf8 ("")]
1105+ expect Path . ancestors ("src /../file ").take_first (2).collect () == [Path . utf8 ("src /../file "), Path . utf8 ("src /..")]
1106+ expect Path . ancestors (Path . windows ("C :\\src ")).collect () == [Path . windows ("C :\\src "), Path . windows ("C :\\")]
1107+ expect Path . ancestors (Path . windows ("\\\\server \\share \\src ")).collect () == [Path . windows ("\\\\server \\share \\src "), Path . windows ("\\\\server \\share \\")]
1108+ expect Path . ancestors (Path . unix_bytes ([0xFF, '/', 0xFE])). map (Path . to_raw ).collect () == [UnixBytes ([0xFF, ' /' , 0xFE]), UnixBytes ([0xFF]), UnixBytes ([])]
1109+
8531110## Errors carry the path they were about, and successes pass through untouched.
8541111expect {
8551112 result : Try ({}, [PathErr (IOErr , Path )])
0 commit comments