From fa5b11f2f06765f31a724ca2e9a9b300e3f7cc80 Mon Sep 17 00:00:00 2001
From: "carpentry-heartbeat[bot]"
computes the path to Both paths are normalized first, so
target relative to base, purely
lexically. It is the companion to absolute.., .., and redundant separators are
-resolved before comparing. Returns Nothing when the paths cannot be related
-lexically: one absolute and one relative, or, on Windows, rooted at different
-drives. Equal paths yield Just ".".Just ".".
Returns Nothing when the paths cannot be related lexically: one absolute and
+one relative; on Windows, rooted at different drives; or a base that still
+starts with .. once the shared prefix is gone, because naming target from
+there would take the name of a directory neither path mentions.
Examples on POSIX:
+(relative "a/b/c" "a/b") ; => (Maybe.Just "c")
+(relative "a" ".") ; => (Maybe.Just "a")
+(relative "." "a") ; => (Maybe.Just "..")
+(relative "../a" "b") ; => (Maybe.Just "../../a")
+(relative "a" "../b") ; => (Maybe.Nothing)
+
diff --git a/docs/index.html b/docs/index.html
index 861a19d..838a4f2 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -453,9 +453,18 @@ computes the path to target relative to base, purely
lexically. It is the companion to absolute.
Both paths are normalized first, so ., .., and redundant separators are
-resolved before comparing. Returns Nothing when the paths cannot be related
-lexically: one absolute and one relative, or, on Windows, rooted at different
-drives. Equal paths yield Just ".".
Just ".".
+Returns Nothing when the paths cannot be related lexically: one absolute and
+one relative; on Windows, rooted at different drives; or a base that still
+starts with .. once the shared prefix is gone, because naming target from
+there would take the name of a directory neither path mentions.
Examples on POSIX:
+(relative "a/b/c" "a/b") ; => (Maybe.Just "c")
+(relative "a" ".") ; => (Maybe.Just "a")
+(relative "." "a") ; => (Maybe.Just "..")
+(relative "../a" "b") ; => (Maybe.Just "../../a")
+(relative "a" "../b") ; => (Maybe.Nothing)
+
diff --git a/path.carp b/path.carp
index 401599f..a712281 100644
--- a/path.carp
+++ b/path.carp
@@ -199,9 +199,21 @@ an extension if there previously was none.")
lexically. It is the companion to [absolute](#absolute).
Both paths are normalized first, so `.`, `..`, and redundant separators are
-resolved before comparing. Returns `Nothing` when the paths cannot be related
-lexically: one absolute and one relative, or, on Windows, rooted at different
-drives. Equal paths yield `Just \".\"`.")
+resolved before comparing. Equal paths yield `Just \".\"`.
+
+Returns `Nothing` when the paths cannot be related lexically: one absolute and
+one relative; on Windows, rooted at different drives; or a `base` that still
+starts with `..` once the shared prefix is gone, because naming `target` from
+there would take the name of a directory neither path mentions.
+
+Examples on POSIX:
+```
+(relative \"a/b/c\" \"a/b\") ; => (Maybe.Just \"c\")
+(relative \"a\" \".\") ; => (Maybe.Just \"a\")
+(relative \".\" \"a\") ; => (Maybe.Just \"..\")
+(relative \"../a\" \"b\") ; => (Maybe.Just \"../../a\")
+(relative \"a\" \"../b\") ; => (Maybe.Nothing)
+```")
(defn relative [target base]
(let [nt (normalize target)
nb (normalize base)
@@ -212,9 +224,9 @@ drives. Equal paths yield `Just \".\"`.")
bc (split &nb)]
(if (and abs (/= (Array.unsafe-nth &tc 0) (Array.unsafe-nth &bc 0)))
(Maybe.Nothing)
- (let [non-empty? (fn [s] (not (String.empty? s)))
- tf (Array.copy-filter &non-empty? &tc)
- bf (Array.copy-filter &non-empty? &bc)
+ (let [component? (fn [s] (and (not (String.empty? s)) (/= s ".")))
+ tf (Array.copy-filter &component? &tc)
+ bf (Array.copy-filter &component? &bc)
tn (Array.length &tf)
bn (Array.length &bf)
common (let-do [k 0]
@@ -223,11 +235,13 @@ drives. Equal paths yield `Just \".\"`.")
(= (Array.unsafe-nth &tf k)
(Array.unsafe-nth &bf k)))
(set! k (inc k)))
- k)
- ups (Array.replicate (- bn common) "..")
- comps (Array.concat &[ups (Array.suffix &tf common)])
- joined (join &comps)]
- (Maybe.Just (if (String.empty? &joined) @"." joined))))))))
+ k)]
+ (if (and (< common bn) (= (Array.unsafe-nth &bf common) ".."))
+ (Maybe.Nothing)
+ (let [ups (Array.replicate (- bn common) "..")
+ comps (Array.concat &[ups (Array.suffix &tf common)])
+ joined (join &comps)]
+ (Maybe.Just (if (String.empty? &joined) @"." joined))))))))))
(defn- class-end [pcs i]
(let-do [n (Array.length pcs)
diff --git a/test/path.carp b/test/path.carp
index e809bd7..12ab3b2 100644
--- a/test/path.carp
+++ b/test/path.carp
@@ -127,6 +127,34 @@
&(Maybe.Nothing)
&(relative "a/b" "/a/b")
"relative returns Nothing for a relative target and absolute base")
+ (assert-equal test
+ &(Maybe.Just @"a")
+ &(relative "a" ".")
+ "relative descends from a . base")
+ (assert-equal test
+ &(Maybe.Just @"..")
+ &(relative "." "a")
+ "relative walks up to a . target")
+ (assert-equal test
+ &(Maybe.Just @"..")
+ &(relative ".." ".")
+ "relative walks up from a . base")
+ (assert-equal test
+ &(Maybe.Just @"../../a")
+ &(relative "../a" "b")
+ "relative resolves a target above the base")
+ (assert-equal test
+ &(Maybe.Just @"../a")
+ &(relative "../a" "../b")
+ "relative cancels leading .. shared by both paths")
+ (assert-equal test
+ &(Maybe.Nothing)
+ &(relative "a" "..")
+ "relative returns Nothing for a base above the current directory")
+ (assert-equal test
+ &(Maybe.Nothing)
+ &(relative "a" "../b")
+ "relative returns Nothing for a base whose residual starts with ..")
(assert-equal test
&(Maybe.Just @"file.txt")
&(filename "path/to/file.txt")