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

Kris Van Hees kris.van.hees at oracle.com
Wed Jan 24 21:04:48 UTC 2024


On Thu, Dec 21, 2023 at 01:11:25PM -0500, eugene.loh at oracle.com wrote:
> 
> 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:
> 

Some comments on this (after reviewing the documentation again):

>     input string         current patch     legacy DTrace
> 
>     "/."                 "/"               "/."
>     "a/."                "a"               "a/."
>     "./a"                "a"               "./a"

These three seem correct to me in how the current patch handles it.

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

A case could be made for all of them being "", given that cleanpath() is
supposed to remove redundant elements (. and ..) and no context is known
for relative paths.  But I do agree that there is also a case to be made
to not get rid of .. elements that would apply to the root of the relative
path, becausae discarding them effectively causes information loss.

Except for the case of "a/..".  That one should be "" because the .. should
be considered redundant in view of having a present path element, and thus
it is possible to back up one path element, which would yield "".  I do not
see how the result of "." would be considered valid because that in itself
is a redundant element (and thus should still be removed).

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

I agree.

> 
> In the first set, legacy DTrace has unnecessary trailing "." or "/."
> 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                       | 478 ++++++++++++++++++++++++++
>  libdtrace/dt_cg.c                     |   9 +-
>  libdtrace/dt_impl.h                   |   9 +-
>  test/stress/fbtsafety/tst.cleanpath.d |   1 -
>  test/unittest/dif/cleanpath.d         |   1 -
>  test/unittest/funcs/tst.cleanpath.d   |  26 +-
>  test/unittest/funcs/tst.cleanpath.r   |  30 +-
>  8 files changed, 547 insertions(+), 8 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..3d537e2f
> --- /dev/null
> +++ b/bpf/cleanpath.S
> @@ -0,0 +1,478 @@
> +// 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;
> + *
> + *      lastchar = src[strlen(src) - 1];
> + *
> + *      // #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) {
> + *      	if (lastchar != '/')
> + *      		dst = ".";
> + *      	else
> + *      		dst = "./";
> + *      	return;
> + *      }
> + *
> + *      // #18.  check for single-char string
> + *      if (strlen(dst) < 2)
> + *      	return;
> + *
> + *      // #19.  strip off the trailing '/'
> + *      if (lastchar != '/')
> + *      	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("") = "". */
> +	jsgt	%r0, 1, .Lnotempty
> +	stb	[DST+0], 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+-2]
> +#define LASTCHAR [%fp+-1]
> +	/* Save the first char. */
> +	stxb	FIRSTCHAR, %r1
> +
> +	/* Save the last char. */
> +	mov	%r8, %r0
> +	add	%r8, DST
> +	ldxb	%r8, [%r8+-1]
> +	stxb	LASTCHAR, %r8
> +
> +	/* #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" or "./\0" and return. */
> +	jsge	%r0, 2, .Lnotempty2
> +	ldxb	%r1, LASTCHAR
> +	jeq	%r1, '/', .Laddslash
> +	sth	[DST+0], '.' /* little-endian uses "\0.", or just '.' */
> +	ja	.Lret
> +.Laddslash:
> +	sth	[DST+0], (('/' << 8) | '.') /* little-endian uses "/." */
> +	stb	[DST+2], 0
> +	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 '/'. */
> +	ldxb	%r1, LASTCHAR
> +	jeq	%r1, '/', .Lret
> +	add	%r0, DST
> +	stb	[%r0+-2], 0
> +#undef DST
> +#undef LASTCHAR
> +
> +.Lret:
> +	mov	%r0, 0
> +	exit
> +	.size	dt_cleanpath, .-dt_cleanpath
> diff --git a/libdtrace/dt_cg.c b/libdtrace/dt_cg.c
> index b211712c..f50a4702 100644
> --- a/libdtrace/dt_cg.c
> +++ b/libdtrace/dt_cg.c
> @@ -5047,6 +5047,13 @@ dt_cg_subr_basename(dt_node_t *dnp, dt_irlist_t *dlp, dt_regset_t *drp)
>  				  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", 0, DT_IGNOR, 0,
> +				  DT_IGNOR, 0);
> +}
> +
>  static void
>  dt_cg_subr_dirname(dt_node_t *dnp, dt_irlist_t *dlp, dt_regset_t *drp)
>  {
> @@ -6466,7 +6473,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 f123c736..bc96fc40 100644
> --- a/libdtrace/dt_impl.h
> +++ b/libdtrace/dt_impl.h
> @@ -226,10 +226,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..0695148c 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,31 @@ 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++] =  "a/";
> +	path[i++] =  "ab/";
> +	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..76f8f584 100644
> --- a/test/unittest/funcs/tst.cleanpath.r
> +++ b/test/unittest/funcs/tst.cleanpath.r
> @@ -2,7 +2,7 @@ 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/./") = "/foo/bar/baz/"
>  cleanpath("/foo/bar/../../baz/.//") = "/baz/"
>  cleanpath("foo/bar/./././././baz/") = "foo/bar/baz/"
> @@ -12,11 +12,35 @@ cleanpath("/./") = "/"
>  cleanpath("/foo/bar/baz/../../bop/bang/../../bar/baz/") = "/foo/bar/baz/"
>  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("a/") = "a/"
> +cleanpath("ab/") = "ab/"
> +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