[DTrace-devel] [PATCH v4] Add support for cleanpath() subroutine
Kris Van Hees
kris.van.hees at oracle.com
Fri Jan 26 00:31:06 UTC 2024
On Thu, Jan 25, 2024 at 07:17:30PM -0500, eugene.loh--- via DTrace-devel wrote:
> From: Eugene Loh <eugene.loh at oracle.com>
>
> Common simplifications include:
> - replace all "//" with "/"
> - replace all "/./" with "/"
> - replace all "/foo/.." with "/"
> but precise semantics can get tricky in certain cases.
>
> Notice that the results differ in some cases from what was
> done in legacy DTrace:
>
> input string current patch legacy DTrace
>
> "/." "/" "/."
> "a/." "a" "a/."
> "./a" "a" "./a"
>
> "../.." "../.." "."
> "a/.." "." "a"
> "a/../.." ".." "a"
>
> "a/../b" "b" "/b"
>
> 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>
Reviewed-by: Kris Van Hees <kris.van.hees at oracle.com>
> ---
> bpf/Build | 1 +
> bpf/cleanpath.S | 463 ++++++++++++++++++++++++++
> 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 | 46 ++-
> 8 files changed, 540 insertions(+), 16 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..5035ef0a
> --- /dev/null
> +++ b/bpf/cleanpath.S
> @@ -0,0 +1,463 @@
> +// 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) {
> + * dst = ".";
> + * 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("") = "". */
> + 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+-1]
> + /* Save the first char. */
> + stxb FIRSTCHAR, %r1
> +
> + /* Save the last char. */
> + mov %r8, %r0
> + add %r8, DST
> + ldxb %r8, [%r8+-1]
> +
> + /* #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, make it ".\0" and return. */
> + jsge %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 9221e0f6..d312e319 100644
> --- a/libdtrace/dt_cg.c
> +++ b/libdtrace/dt_cg.c
> @@ -5283,6 +5283,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)
> {
> @@ -6719,7 +6726,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 c38d32d5..9912ad65 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..ef603b74 100644
> --- a/test/unittest/funcs/tst.cleanpath.r
> +++ b/test/unittest/funcs/tst.cleanpath.r
> @@ -1,22 +1,46 @@
> 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("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
>
>
> _______________________________________________
> DTrace-devel mailing list
> DTrace-devel at oss.oracle.com
> https://oss.oracle.com/mailman/listinfo/dtrace-devel
More information about the DTrace-devel
mailing list