[DTrace-devel] [PATCH] Add support for cleanpath() subroutine

eugene.loh at oracle.com eugene.loh at oracle.com
Tue Nov 21 04:56:50 UTC 2023


From: Eugene Loh <eugene.loh at oracle.com>

Replace all "//" with "/".
Replace all "/./" with "/".
Replace all "../foo/" with "/".

Notice that the results differ in some cases from what was
done in legacy DTrace:

    input string         current patch     legacy DTrace

    ""                   "."               ""

    "/foo/bar/baz/"      "/foo/bar/baz"    "/foo/bar/baz/"
    "/."                 "/"               "/."
    "a/."                "a"               "a/."
    "./a"                "a"               "./a"

    "../.."              "../.."           "."
    "a/.."               "."               "a"
    "a/../.."            ".."              "a"

    "a/../b"             "b"               "/b"

In the first case, one can argue about which implementation is
"correct."

In the next cases, there are unnecessary trailing '/' or '.' or
both or unnecessary leading "./".  They would seem to be wrong.

In the next cases, the legacy results are clearly wrong, not
handling ".." correctly.

In the final case, a relative path is converted into an absolute
path, which is also clearly incorrect.

Signed-off-by: Eugene Loh <eugene.loh at oracle.com>
---
 bpf/Build                             |   1 +
 bpf/cleanpath.S                       | 457 ++++++++++++++++++++++++++
 libdtrace/dt_cg.c                     |   8 +-
 libdtrace/dt_impl.h                   |   9 +-
 test/stress/fbtsafety/tst.cleanpath.d |   1 -
 test/unittest/dif/cleanpath.d         |   1 -
 test/unittest/funcs/tst.cleanpath.d   |  24 +-
 test/unittest/funcs/tst.cleanpath.r   |  46 ++-
 8 files changed, 530 insertions(+), 17 deletions(-)
 create mode 100644 bpf/cleanpath.S

diff --git a/bpf/Build b/bpf/Build
index a165cbd3..20202d90 100644
--- a/bpf/Build
+++ b/bpf/Build
@@ -23,6 +23,7 @@ bpf_dlib_SRCDEPS = $(objdir)/include/.dir.stamp
 bpf_dlib_SOURCES = \
 	agg_lqbin.c agg_qbin.c \
 	basename.S \
+	cleanpath.S \
 	dirname.S \
 	get_agg.c \
 	get_bvar.c \
diff --git a/bpf/cleanpath.S b/bpf/cleanpath.S
new file mode 100644
index 00000000..8f6dae11
--- /dev/null
+++ b/bpf/cleanpath.S
@@ -0,0 +1,457 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
+ */
+
+/*
+ * Conceptually, the idea behind cleanpath() is relatively simple:
+ * simplify gratuitous "//", "/./" and "/foo/../" sequences in a path name.
+ *
+ * In practice, the algorithm is a little tricky, all the more since we
+ * want to implement this in BPF.
+ *
+ * Significantly, the BPF verifier really does not like conditional
+ * statements, since it has to verify every conceivable execution path,
+ * and the number of such paths can explode exponentially.  So, we go to
+ * great lengths to replace conditional execution with nonconditional
+ * execution.  For example,
+ *      if (nup > 0)
+ *      	nup--;
+ * can be implemented as:
+ *      tmp = (-nup) >> 63;
+ *      nup -= tmp;
+ *
+ * Here is the high-level algorithm.  Milestones are designated with
+ * "#" so that they can be found easily in the code.  Comments like
+ * "execute if TMP==1" indicate which variable is set to 1 (or 0) to
+ * indicate that a code block should be executed (or not).
+ *
+ *      // #1.  handle empty string: cleanpath("") = "."
+ *      if (strlen(src) == 0) {
+ *      	dst = ".";
+ *      	return;
+ *      }
+ *
+ *      // #2.  handle single-char string: cleanpath("x") = "x", even for '.' and '/'
+ *      if (strlen(src) == 1)
+ *      	return;
+ *
+ *      // #3.  regularize processing by prepending and appending a '/'
+ *      dst = '/' + dst + '/';
+ *
+ *      // #4.  loop forward to handle "//" and "/./" strings
+ *      seq = 0;      // word whose bytes are the most recent chars we have seen
+ *      for (to = from = 0; ; to++, from++) {    // "from" advances faster than "to"
+ *      	char chr;
+ *
+ *      	chr = dst[from];
+ *      	dst[to] = chr;
+ *      	if (chr == '\0')
+ *      		break;
+ *      	seq = (seq << 8) | chr;
+ *
+ *      	if (recent two chars in seq are "//") {
+ *      		// #5.  execute if TMP==1
+ *      		to--;
+ *      		seq >>= 8;
+ *      	}
+ *
+ *      	if (recent three chars in seq are "/./") {
+ *      		// #6.  execute if TMP==1
+ *      		to -= 2;
+ *      		seq >>= 16;
+ *      	}
+ *      }
+ *
+ *      // #7.  loop backward to handle "/../" strings
+ *      nup = 0;      // how many ".." are unresolved
+ *      brk = ito;    // index of last '/' we have seen
+ *      seq = 0;      // word whose bytes are the most recent chars we have seen
+ *      for (to = from = brk - 1; from >= 0; to--, from--) { // "from" descends faster than "to"
+ *      	char chr;
+ *
+ *      	chr = dst[from];
+ *      	dst[to] = chr;
+ *      	seq = (seq << 8) | chr;
+ *
+ *      	if (recent four chars in seq are "/../") {  // drop 3 chars and increment nup
+ *      		// #8.  execute if TMP==1
+ *      		nup++;
+ *      		to += 3;
+ *      		seq >>= 24;
+ *      	} else if (chr == '/') {        // completed a dir name that is not ".."
+ *      		// #9.  execute if TMP==1
+ *      		if (nup > 0) {          // we have to drop the dir name due to a ".."
+ *      			// #10.  execute if TM4==1
+ *      			to = brk;       // rewind to last brk index
+ *      			nup--;          // decrement nup
+ *      		} else {                // normal case, keep this dir name
+ *      			// #11.  execute if TM2==1
+ *      			brk = ito;      // note brk for future use
+ *      		}
+ *      	}
+ *      }
+ *
+ *      // #12.  point to the first char in the output
+ *      to++;
+ *
+ *      // #13.  for absolute paths, forget about nup:  "/.." is equivalent to "/"
+ *      if (src[0] == '/')
+ *      	goto Lout;
+ *
+ *      // #14.  for relative paths, advance past the initial, artificial '/'
+ *      to++;
+ *
+ *      // #15.  for each nup, prepend a "../"
+ *      while (nup > 0) {
+ *      	ito -= 3;
+ *      	dst[ITO + 0] = '.';
+ *      	dst[ITO + 1] = '.';
+ *      	dst[ITO + 2] = '/';
+ *      	nup--;
+ *      }
+ *
+ *      Lout:
+ *
+ *      // #16.  move the result string to the beginning of the dst buffer
+ *      strcpy(dst, &dst[to]);
+ *
+ *      // #17.  check for empty string
+ *      if (strlen(dst) < 1) {
+ *      	dst = ".\0";
+ *      	return;
+ *      }
+ *
+ *      // #18.  check for single-char string
+ *      if (strlen(dst) < 2)
+ *      	return;
+ *
+ *      // #19.  strip off the trailing '/'
+ *      dst[strlen(dst) - 1] = '\0';
+ */
+
+#define BPF_FUNC_probe_read	4
+#define BPF_FUNC_probe_read_str	45
+
+/*
+ * void dt_cleanpath(const dt_dctx_t *dctx, char *src, char *dst);
+ */
+	.text
+	.align	4
+	.global	dt_cleanpath
+	.type	dt_cleanpath, @function
+dt_cleanpath :
+
+#define DST %r9
+	/* Cache dst pointer. */
+	mov	DST, %r3
+
+	/*
+	 * Copy src to &dst[1].  Then we can directly dereference individual chars.
+	 * The first byte, dst[0], is reserved for prepending a '/'.
+	 */
+	mov	%r3, %r2
+	lddw	%r2, STRSZ
+	add	%r2, 1
+	mov	%r1, DST
+	add	%r1, 1
+	call	BPF_FUNC_probe_read_str
+	/* At this point, %r0 has strlen(src) + 1. */
+
+	/* #1.  Handle empty string: cleanpath("") = ".". */
+	jgt	%r0, 1, .Lnotempty
+	stb	[DST+0], '.'
+	stb	[DST+1], 0
+	mov	%r0, 0
+	exit
+.Lnotempty:
+
+	/*
+	 * #2.  Handle single-char string: cleanpath("x") = "x", even for '.' and '/'.
+	 * Load the first char (which we had put in dst[1]) and stuff it in dst[0].
+	 */
+	ldxb	%r1, [DST+1]
+	jgt	%r0, 2, .Lnotsinglechar
+	stxb	[DST+0], %r1
+	stb	[DST+1], 0
+	mov	%r0, 0
+	exit
+.Lnotsinglechar:
+
+#define FIRSTCHAR [%fp+-1]
+	/* Save the first char. */
+	stxb	FIRSTCHAR, %r1
+
+	/* #3.  Prepend a '/'. */
+	stb	[DST+0], '/'
+
+	/* #3.  Append a '/'. */
+	add	%r0, DST
+	stb	[%r0+0], '/'
+	stb	[%r0+1], 0
+
+#define TMP %r8		/* temp reg */
+#define ITO %r7		/* index to copy "to" */
+#define IFR %r6		/* index to copy "from" */
+#define SEQ %r5		/* sequence of most-recent chars */
+#define CHR %r4		/* char being copied */
+#define SIZ %r3		/* STRSZ */
+
+	/* #4.  Loop forward through the string. */
+	mov	ITO, 0
+	mov	IFR, 0
+	mov	SEQ, 0
+	lddw	SIZ, STRSZ
+.Lforward:
+	jge	IFR, SIZ, .Lret		/* reassure the BPF verifier */
+
+	/* Load the char from the "from" index. */
+	mov	TMP, IFR
+	add	TMP, DST
+	ldxb	CHR, [TMP + 0]
+
+	/* Store the char to the "to" index. */
+	mov	TMP, ITO
+	jlt	TMP, 0, .Lret		/* reassure the BPF verifier */
+	jgt	TMP, IFR, .Lret		/* reassure the BPF verifier */
+	add	TMP, DST
+	stxb	[TMP + 0], CHR
+
+	/* If the char is '\0', break out of the loop. */
+	jeq	CHR, 0, .Lforward_brk
+
+	/* Add this char to the sequence of chars. */
+	lsh	SEQ, 8
+	or	SEQ, CHR
+
+	/* #5.  Compute TMP = 1 (if we should execute), else 0. */
+	mov	TMP, (('/' << 8) | '/')
+	xor	TMP, SEQ
+	and	TMP, 0xffff
+	neg	TMP
+	rsh	TMP, 63
+	xor	TMP, 1
+
+	/* Use this to back up one char if "//". */
+	sub	ITO, TMP
+
+	/* Update SEQ accordingly. */
+	mul	TMP, 8
+	rsh	SEQ, TMP
+
+	/* #6.  Compute TMP = 1 (if we should execute), else 0. */
+	mov	TMP, (('/' << 16) | ('.' << 8) | '/')
+	xor	TMP, SEQ
+	and	TMP, 0xffffff
+	neg	TMP
+	rsh	TMP, 63
+	xor	TMP, 1
+
+	/* Use this to back up two chars if "/./". */
+	mul	TMP, 2
+	sub	ITO, TMP
+
+	/* Update SEQ accordingly. */
+	mul	TMP, 8
+	rsh	SEQ, TMP
+
+	/* Advance the indices and iterate. */
+	add	ITO, 1
+	add	IFR, 1
+	ja	.Lforward
+#undef SIZ
+
+.Lforward_brk:
+
+#define TM2 %r3		/* another temp reg */
+#define TM3 %r2		/* another temp reg */
+#define BRK %r1		/* index of last directory-level break '/' */
+#define NUP %r0		/* num of parent directories to go up due to "../" */
+
+	/* #7.  Handle double-dot "/../", looping backward. */
+	mov	NUP, 0
+	mov	SEQ, 0
+	mov	BRK, ITO
+	jslt	BRK, 1, .Lret		/* reassure the BPF verifier */
+	lddw	TMP, STRSZ
+	jge	BRK, TMP, .Lret		/* reassure the BPF verifier */
+	mov	IFR, BRK
+	sub	IFR, 1
+	mov	ITO, IFR
+.Lbackward:
+	/* Load the char from the "from" index. */
+	mov	TMP, IFR
+	add	TMP, DST
+	ldxb	CHR, [TMP + 0]
+
+	/* Store the char to the "to" index. */
+	mov	TMP, ITO
+	lddw	TM2, STRSZ
+	jge	TMP, TM2, .Lret		/* reassure the BPF verifier */
+	jlt	TMP, IFR, .Lret		/* reassure the BPF verifier */
+	add	TMP, DST
+	stxb	[TMP + 0], CHR
+
+	/* Add this char to the sequence of chars. */
+	lsh	SEQ, 8
+	or	SEQ, CHR
+
+	/* #8.  Compute TMP = 1 (if we should execute), else 0. */
+	mov	TMP, ('/' | ('.' << 8) | ('.' << 16) | ('/' << 24))
+	xor	TMP, SEQ
+	lsh	TMP, 32
+	rsh	TMP, 32
+	neg	TMP
+	rsh	TMP, 63
+	xor	TMP, 1
+
+	/* Use this to increase NUP if "/../". */
+	add	NUP, TMP
+
+	/* Use this to back up three chars if "/../". */
+	mov	TM2, TMP
+	mul	TM2, 3
+	add	ITO, TM2
+
+	/* Update SEQ accordingly. */
+	mul	TM2, 8
+	rsh	SEQ, TM2
+
+	/* Now switch to the not-"/.../" case. */
+	xor	TMP, 1
+
+/* Rename CHR's reg TM4;  TM4 is yet another temp reg and starts with CHR's value. */
+#undef CHR
+#define TM4 %r4
+
+	/* If the last char is "/", TM4 = 1.  Else, TM4 = 0. */
+	xor	TM4, '/'
+	neg	TM4
+	rsh	TM4, 63
+	xor	TM4, 1
+
+	/* #9.  Compute TMP = 1 (if we should execute), else 0. */
+	mul	TMP, TM4
+
+	/* If NUP > 0, TM2 = 1.  Else, TM2 = 0. */
+	mov	TM2, NUP
+	neg	TM2
+	rsh	TM2, 63
+
+	/* #10.  Compute TM4 = 1 (if we should execute), else 0. */
+	mov	TM4, TMP
+	mul	TM4, TM2
+
+	/* If TM4 is 1, then ITO = BRK (revert to last BRK index). */
+	mov	TM3, BRK
+	sub	TM3, ITO
+	mul	TM3, TM4
+	add	ITO, TM3
+
+	/* If TM4 is 1, then NUP--. */
+	sub	NUP, TM4
+
+	/* #11.  Compute TM2 = 1 (if we should execute), else 0. */
+	xor	TM2, 1
+	mul	TM2, TMP
+
+	/* If TM2 is 1, then BRK = ITO. */
+	mov	TM4, ITO
+	sub	TM4, BRK
+	mul	TM4, TM2
+	add	BRK, TM4
+
+	/* Advance the indices and iterate. */
+	sub	ITO, 1
+	sub	IFR, 1
+	jsge	IFR, 0, .Lbackward
+#undef BRK
+#undef TM4
+#undef TM2
+#undef SEQ
+#undef TM3
+#undef IFR
+
+	/*
+	 * At this point, we've finished the backward loop.
+	 */
+
+	/* #12.  Increment ITO to point to the beginning of the output. */
+	add	ITO, 1
+
+	/* #13.  If we have an absolute path (src[0] == '/'), go to Lout. */
+	ldxb	TMP, FIRSTCHAR
+	jeq	TMP, '/', .Lout
+#undef FIRSTCHAR
+
+	/* #14.  Otherwise, advance past the initial, spurious '/'. */
+	add	ITO, 1
+
+	/* #15.  Add a "../" prefix NUP times. */
+	/*
+	 * FIXME:  The BPF verifier needs some assurance that this loop
+	 * is not endless.  We arbitrarily assume NUP is at most 15,
+	 * equivalent to some clean path of the form
+	 * "../../../../../../../../../../../../../../../foo/bar".
+	 */
+	and	NUP, 15
+
+	lddw	TMP, STRSZ
+	jgt	ITO, TMP, .Lret		/* reassure the BPF verifier */
+.Lprefix:
+	jsle	NUP, 0, .Lout
+
+	sub	ITO, 3
+	jslt	ITO, 0, .Lret		/* reassure the BPF verifier */
+
+	mov	TMP, ITO
+	add	TMP, DST
+	stb	[TMP+0], '.'
+	stb	[TMP+1], '.'
+	stb	[TMP+2], '/'
+
+	sub	NUP, 1
+	ja	.Lprefix
+#undef NUP
+#undef TMP
+
+.Lout:
+	jslt	ITO, 0, .Lret		/* reassure the BPF verifier */
+	lddw	%r1, STRSZ
+	jgt	ITO, %r1, .Lret		/* reassure the BPF verifier */
+
+	/* #16.  Copy from index "ito" to the beginning of the DST buffer. */
+	/*
+	 * FIXME:  Is this okay?  We are copying a string in the DST
+	 * array from offset ITO down to offset 0.  The input and output
+	 * strings might (indeed probably will) overlap.  Can we use
+	 * the BPF helper function in this way?
+	 */
+	mov	%r1, DST
+	lddw	%r2, STRSZ
+	add	%r2, 1
+	sub	%r2, ITO
+	mov	%r3, DST
+	add	%r3, ITO
+	call	BPF_FUNC_probe_read_str
+	/* At this point, %r0 has strlen(dst) + 1. */
+#undef ITO
+
+	/* #17.  If the string is empty, then make it ".\0" and return. */
+	jge	%r0, 2, .Lnotempty2
+	sth	[DST+0], '.' /* little-endian uses "\0.", or just '.' */
+	ja	.Lret
+.Lnotempty2:
+
+	/* #18.  If the string is a single char (plus NUL char), then return. */
+	jle	%r0, 2, .Lret
+
+	/* #19.  Strip off the trailing '/'. */
+	add	%r0, DST
+	stb	[%r0+-2], 0
+#undef DST
+
+.Lret:
+	mov	%r0, 0
+	exit
+	.size	dt_cleanpath, .-dt_cleanpath
diff --git a/libdtrace/dt_cg.c b/libdtrace/dt_cg.c
index c4e046bd..6051b1cf 100644
--- a/libdtrace/dt_cg.c
+++ b/libdtrace/dt_cg.c
@@ -4868,6 +4868,12 @@ dt_cg_subr_basename(dt_node_t *dnp, dt_irlist_t *dlp, dt_regset_t *drp)
 	dt_cg_subr_arg_to_tstring(dnp, dlp, drp, "dt_basename", DT_IGNOR, 0);
 }
 
+static void
+dt_cg_subr_cleanpath(dt_node_t *dnp, dt_irlist_t *dlp, dt_regset_t *drp)
+{
+	dt_cg_subr_arg_to_tstring(dnp, dlp, drp, "dt_cleanpath", DT_IGNOR, 0);
+}
+
 static void
 dt_cg_subr_dirname(dt_node_t *dnp, dt_irlist_t *dlp, dt_regset_t *drp)
 {
@@ -6300,7 +6306,7 @@ static dt_cg_subr_f *_dt_cg_subr[DIF_SUBR_MAX + 1] = {
 	[DIF_SUBR_LLTOSTR]		= &dt_cg_subr_lltostr,
 	[DIF_SUBR_BASENAME]		= &dt_cg_subr_basename,
 	[DIF_SUBR_DIRNAME]		= &dt_cg_subr_dirname,
-	[DIF_SUBR_CLEANPATH]		= NULL,
+	[DIF_SUBR_CLEANPATH]		= &dt_cg_subr_cleanpath,
 	[DIF_SUBR_STRCHR]		= &dt_cg_subr_strchr,
 	[DIF_SUBR_STRRCHR]		= &dt_cg_subr_strrchr,
 	[DIF_SUBR_STRSTR]		= &dt_cg_subr_strstr,
diff --git a/libdtrace/dt_impl.h b/libdtrace/dt_impl.h
index 444dd35a..d2af68e4 100644
--- a/libdtrace/dt_impl.h
+++ b/libdtrace/dt_impl.h
@@ -225,10 +225,17 @@ typedef struct dt_ahash {
  *
  * Each tstring needs to be large enough to hold the largest possible string
  * and accomodate the largest known need for tstring space in subroutines.
+ *
+ * For example:
+ *
+ * - inet_ntoa6() stores its output and 2 copies of the input (40 + 2 * 16 = 72)
+ *
+ * - cleanpath() holds a prepended '/' char, a string, an appended '/' char,
+ *   and a terminating NUL char, or STRSZ + 3 chars altogether
  */
 #define DT_TSTRING_SLOTS	4
 #define DT_TSTRING_SIZE(dtp)	\
-		MAX(P2ROUNDUP((dtp)->dt_options[DTRACEOPT_STRSIZE] + 1, 8), \
+		MAX(P2ROUNDUP((dtp)->dt_options[DTRACEOPT_STRSIZE] + 3, 8), \
 		    72)
 
 typedef struct dt_tstring {
diff --git a/test/stress/fbtsafety/tst.cleanpath.d b/test/stress/fbtsafety/tst.cleanpath.d
index d3b934dc..1723a4a7 100644
--- a/test/stress/fbtsafety/tst.cleanpath.d
+++ b/test/stress/fbtsafety/tst.cleanpath.d
@@ -4,7 +4,6 @@
  * Licensed under the Universal Permissive License v 1.0 as shown at
  * http://oss.oracle.com/licenses/upl.
  */
-/* @@xfail: dtv2 */
 
 #pragma D option bufsize=1000
 #pragma D option bufpolicy=ring
diff --git a/test/unittest/dif/cleanpath.d b/test/unittest/dif/cleanpath.d
index 0e6c39f9..3fc3d4c5 100644
--- a/test/unittest/dif/cleanpath.d
+++ b/test/unittest/dif/cleanpath.d
@@ -1,4 +1,3 @@
-/* @@xfail: dtv2 */
 BEGIN
 {
 	exit(cleanpath("/foo/bar/.././../sys/foo") == "/sys/foo" ? 0 : 1);
diff --git a/test/unittest/funcs/tst.cleanpath.d b/test/unittest/funcs/tst.cleanpath.d
index bdcde04b..2b3d1dfb 100644
--- a/test/unittest/funcs/tst.cleanpath.d
+++ b/test/unittest/funcs/tst.cleanpath.d
@@ -4,7 +4,6 @@
  * Licensed under the Universal Permissive License v 1.0 as shown at
  * http://oss.oracle.com/licenses/upl.
  */
-/* @@xfail: dtv2 */
 
 #pragma D option quiet
 
@@ -32,6 +31,29 @@ BEGIN
 	path[i++] = "/////";
 	path[i++] = "";
 
+	path[i++] = "a/.";
+	path[i++] = "a/./.";
+	path[i++] = "./a";
+	path[i++] = "././a";
+	path[i++] = "..";
+	path[i++] = "../..";
+	path[i++] = "../../..";
+	path[i++] = "../../../..";
+	path[i++] = "a/..";
+	path[i++] =  "abc";
+	path[i++] = "/abc";
+	path[i++] =  "z/a/../b";
+	path[i++] = "/z/a/../b";
+	path[i++] =  "z/a/b/../../././c///d/e";
+	path[i++] = "/z/a/b/../../././c///d/e";
+	path[i++] = "/a/../b";
+	path[i++] = "/a/..";
+	path[i++] = "/a/../..";
+	path[i++] = "/a/b/../../././c///d/e";
+	path[i++] =  "a/../b";
+	path[i++] =  "a/../..";
+	path[i++] =  "a/b/../../././c///d/e";
+
 	end = i;
 	i = 0;
 }
diff --git a/test/unittest/funcs/tst.cleanpath.r b/test/unittest/funcs/tst.cleanpath.r
index b8bdc099..8a7860d6 100644
--- a/test/unittest/funcs/tst.cleanpath.r
+++ b/test/unittest/funcs/tst.cleanpath.r
@@ -1,22 +1,44 @@
 cleanpath("/foo/bar/baz") = "/foo/bar/baz"
-cleanpath("/foo/bar///baz/") = "/foo/bar/baz/"
-cleanpath("/foo/bar/baz/") = "/foo/bar/baz/"
-cleanpath("/foo/bar/baz//") = "/foo/bar/baz/"
-cleanpath("/foo/bar/baz/.") = "/foo/bar/baz/."
-cleanpath("/foo/bar/baz/./") = "/foo/bar/baz/"
-cleanpath("/foo/bar/../../baz/.//") = "/baz/"
-cleanpath("foo/bar/./././././baz/") = "foo/bar/baz/"
+cleanpath("/foo/bar///baz/") = "/foo/bar/baz"
+cleanpath("/foo/bar/baz/") = "/foo/bar/baz"
+cleanpath("/foo/bar/baz//") = "/foo/bar/baz"
+cleanpath("/foo/bar/baz/.") = "/foo/bar/baz"
+cleanpath("/foo/bar/baz/./") = "/foo/bar/baz"
+cleanpath("/foo/bar/../../baz/.//") = "/baz"
+cleanpath("foo/bar/./././././baz/") = "foo/bar/baz"
 cleanpath("/foo/bar/baz/../../../../../../") = "/"
 cleanpath("/../../../../../../") = "/"
 cleanpath("/./") = "/"
-cleanpath("/foo/bar/baz/../../bop/bang/../../bar/baz/") = "/foo/bar/baz/"
-cleanpath("./") = "./"
+cleanpath("/foo/bar/baz/../../bop/bang/../../bar/baz/") = "/foo/bar/baz"
+cleanpath("./") = "."
 cleanpath("//") = "/"
-cleanpath("/.") = "/."
+cleanpath("/.") = "/"
 cleanpath("/./") = "/"
-cleanpath("/./.") = "/."
+cleanpath("/./.") = "/"
 cleanpath("/.//") = "/"
 cleanpath(".") = "."
 cleanpath("/////") = "/"
-cleanpath("") = ""
+cleanpath("") = "."
+cleanpath("a/.") = "a"
+cleanpath("a/./.") = "a"
+cleanpath("./a") = "a"
+cleanpath("././a") = "a"
+cleanpath("..") = ".."
+cleanpath("../..") = "../.."
+cleanpath("../../..") = "../../.."
+cleanpath("../../../..") = "../../../.."
+cleanpath("a/..") = "."
+cleanpath("abc") = "abc"
+cleanpath("/abc") = "/abc"
+cleanpath("z/a/../b") = "z/b"
+cleanpath("/z/a/../b") = "/z/b"
+cleanpath("z/a/b/../../././c///d/e") = "z/c/d/e"
+cleanpath("/z/a/b/../../././c///d/e") = "/z/c/d/e"
+cleanpath("/a/../b") = "/b"
+cleanpath("/a/..") = "/"
+cleanpath("/a/../..") = "/"
+cleanpath("/a/b/../../././c///d/e") = "/c/d/e"
+cleanpath("a/../b") = "b"
+cleanpath("a/../..") = ".."
+cleanpath("a/b/../../././c///d/e") = "c/d/e"
 
-- 
2.18.4




More information about the DTrace-devel mailing list