【问题标题】:Is there a way to canonicalize file path of a non-existing file?有没有办法规范化不存在文件的文件路径?
【发布时间】:2019-12-16 11:54:54
【问题描述】:

我想解析当前不存在的文件的文件名中的任何 "." 和 ".." 引用。

所以,我想做的类似于realpath(3),但有以下例外:

  • 文件名可能引用了不存在的文件
  • 不得解析符号链接(主要是因为路径组件可能不存在)

是否有我可以调用的现有库代码来执行此操作,还是我必须编写新代码?

所以,基本上:

  • a/../b/c.txt 将变为 b/c.txt
  • a/../../b/c.txt 将变为 ../b/c.txt
  • a/./b/./c.txt 将变为 a/b/c.txt

如果有人想知道,我这样做不是出于安全目的:我正在编写一个允许任意 shell 命令执行的工具,因此安全性不是问题。我只需要一种方法来获得用于比较它们的路径的规范表示。

【问题讨论】:

  • 我很确定这是另一个问题的重复。基本思想是在路径名上调用realpath,如果最终组件不存在,则将其删除。
  • 但是realpath解析符号链接,我指定符号链接一定不能解析。
  • 所以你想要引用同一个文件的路径名,但是通过不同的符号链接路径,而不是规范化到同一个东西?
  • 这是我的计划。
  • 那么这甚至不是文件系统操作,只是一个即使使用sed也可以完成的字符串转换。

标签: c file path posix filepath


【解决方案1】:

这是我 30 多年前首次编写的代码。您需要的代码有两种变体。

代码在 Stack Overflow 使用的 CC-by-SA 3.0 许可下可用 — 您可以在注明出处的情况下使用它。

/*
@(#)File:           $RCSfile: clnpath.c,v $
@(#)Version:        $Revision: 2.19 $
@(#)Last changed:   $Date: 2017/03/26 06:32:49 $
@(#)Purpose:        Clean up pathname (lexical analysis only)
@(#)Author:         J Leffler
@(#)Copyright:      (C) JLSS 1987-2017
*/

/*TABSTOP=4*/

#include "clnpath.h"

#include "jlss.h"
#include "tokenise.h"
#include <string.h>

#define MAX_PATH_ELEMENTS   64  /* Number of levels of directory */
#define strequal(a,b)       (strcmp((a),(b)) == 0)
#define DIM(x)              (sizeof(x)/sizeof(*(x)))

#if !defined(lint)
/* Prevent over-aggressive optimizers from eliminating ID string */
extern const char jlss_id_clnpath_c[];
const char jlss_id_clnpath_c[] = "@(#)$Id: clnpath.c,v 2.19 2017/03/26 06:32:49 jleffler Exp $";
#endif /* lint */

void clnpath(char *path)
{
    char           *src;
    char           *dst;
    char            c;
    int             slash = 0;

    /* Convert multiple adjacent slashes to single slash */
    src = dst = path;
    while ((c = *dst++ = *src++) != '\0')
    {
        if (c == '/')
        {
            slash = 1;
            while (*src == '/')
                src++;
        }
    }

    if (slash == 0)
        return;

    /* Remove "./" from "./xxx" but leave "./" alone. */
    /* Remove "/." from "xxx/." but reduce "/." to "/". */
    /* Reduce "xxx/./yyy" to "xxx/yyy" */
    src = dst = (*path == '/') ? path + 1 : path;
    while (src[0] == '.' && src[1] == '/' && src[2] != '\0')
        src += 2;
    while ((c = *dst++ = *src++) != '\0')
    {
        if (c == '/' && src[0] == '.' && (src[1] == '\0' || src[1] == '/'))
        {
            src++;
            dst--;
        }
    }
    if (path[0] == '/' && path[1] == '.' &&
        (path[2] == '\0' || (path[2] == '/' && path[3] == '\0')))
        path[1] = '\0';

    /* Remove trailing slash, if any.  There is at most one! */
    /* dst is pointing one beyond terminating null */
    if ((dst -= 2) > path && *dst == '/')
        *dst++ = '\0';
}

/*
** clnpath2() is not part of the basic clnpath() function because it can
** change the meaning of a path name if there are symbolic links on the
** system.  For example, suppose /usr/tmp is a symbolic link to /var/tmp.
** If the user supplies /usr/tmp/../abcdef as the directory name, clnpath
** would transform that to /usr/abcdef, not to /var/abcdef which is what
** the kernel would interpret it as.
*/

void clnpath2(char *path)
{
    char *token[MAX_PATH_ELEMENTS];
    int   ntok;

    clnpath(path);

    /* Reduce "<name>/.." to "/" */
    if ((ntok = tokenise(path, "/", token, MAX_PATH_ELEMENTS, 0)) > 1)
    {
        for (int i = 0; i < ntok - 1; i++)
        {
            if (!strequal(token[i], "..") && strequal(token[i + 1], ".."))
            {
                if (*token[i] == '\0')
                    continue;
                while (i < ntok - 1)
                {
                    token[i] = token[i + 2];
                    i++;
                }
                ntok -= 2;
                i = -1;     /* Restart enclosing for loop */
            }
        }
    }

    /* Reassemble string */
    char *dst = path;
    if (ntok == 0)
    {
        *dst++ = '.';
        *dst = '\0';
    }
    else
    {
        if (token[0][0] == '\0')
        {
            int   i;
            for (i = 1; i < ntok && strequal(token[i], ".."); i++)
                ;
            if (i > 1)
            {
                int j;
                for (j = 1; i < ntok; i++)
                    token[j++] = token[i];
                ntok = j;
            }
        }
        if (ntok == 1 && token[0][0] == '\0')
        {
            *dst++ = '/';
            *dst = '\0';
        }
        else
        {
            for (int i = 0; i < ntok; i++)
            {
                char *src = token[i];
                while ((*dst++ = *src++) != '\0')
                    ;
                *(dst - 1) = '/';
            }
            *(dst - 1) = '\0';
        }
    }
}

#if defined(TEST)

#include <stdio.h>

#include "phasedtest.h"

/* -- PHASE 1 TESTING -- */

/* -- Phase 1 - Testing clnpath() -- */
typedef struct p1_test_case
{
    const char *input;
    const char *output;
} p1_test_case;

/* This stress tests the cleaning, concentrating on the boundaries. */
static const p1_test_case p1_tests[] =
{
    { "/",                                  "/",            },
    { "//",                                 "/",            },
    { "///",                                "/",            },
    { "/.",                                 "/",            },
    { "/./",                                "/",            },
    { "/./.",                               "/",            },
    { "/././.profile",                      "/.profile",    },
    { "./",                                 ".",            },
    { "./.",                                ".",            },
    { "././",                               ".",            },
    { "./././.profile",                     ".profile",     },
    { "abc/.",                              "abc",          },
    { "abc/./def",                          "abc/def",      },
    { "./abc",                              "abc",          },

    { "//abcd///./abcd////",                "/abcd/abcd",                   },
    { "//abcd///././../defg///ddd//.",      "/abcd/../defg/ddd",            },
    { "/abcd/./../././defg/./././ddd",      "/abcd/../defg/ddd",            },
    { "//abcd//././../defg///ddd//.///",    "/abcd/../defg/ddd",            },

    /* Most of these are minimal interest in phase 1 */
    { "/usr/tmp/clnpath.c",                 "/usr/tmp/clnpath.c",           },
    { "/usr/tmp/",                          "/usr/tmp",                     },
    { "/bin/..",                            "/bin/..",                      },
    { "bin/..",                             "bin/..",                       },
    { "/bin/.",                             "/bin",                         },
    { "sub/directory",                      "sub/directory",                },
    { "sub/directory/file",                 "sub/directory/file",           },
    { "/part1/part2/../.././../",           "/part1/part2/../../..",        },
    { "/.././../usr//.//bin/./cc",          "/../../usr/bin/cc",            },
};

static void p1_tester(const void *data)
{
    const p1_test_case *test = (const p1_test_case *)data;
    char  buffer[256];

    strcpy(buffer, test->input);
    clnpath(buffer);
    if (strcmp(buffer, test->output) == 0)
        pt_pass("<<%s>> cleans to <<%s>>\n", test->input, buffer);
    else
    {
        pt_fail("<<%s>> - unexpected output from clnpath()\n", test->input);
        pt_info("Wanted <<%s>>\n", test->output);
        pt_info("Actual <<%s>>\n", buffer);
    }
}

/* -- PHASE 2 TESTING -- */

/* -- Phase 2 - Testing clnpath2() -- */
typedef struct p2_test_case
{
    const char *input;
    const char *output;
} p2_test_case;

static const p2_test_case p2_tests[] =
{
    { "/abcd/../defg/ddd",              "/defg/ddd"         },
    { "/bin/..",                        "/"                 },
    { "bin/..",                         "."                 },
    { "/usr/bin/..",                    "/usr"              },
    { "/usr/bin/../..",                 "/"                 },
    { "usr/bin/../..",                  "."                 },
    { "../part/of/../the/way",          "../part/the/way"   },
    { "/../part/of/../the/way",         "/part/the/way"     },
    { "part1/part2/../../part3",        "part3"             },
    { "part1/part2/../../../part3",     "../part3"          },
    { "/part1/part2/../../../part3",    "/part3"            },
    { "/part1/part2/../../../",         "/"                 },
    { "/../../usr/bin/cc",              "/usr/bin/cc"       },
    { "../../usr/bin/cc",               "../../usr/bin/cc"  },
    { "part1/./part2/../../part3",      "part3"             },
    { "./part1/part2/../../../part3",   "../part3"          },
    { "/part1/part2/.././../../part3",  "/part3"            },
    { "/part1/part2/../.././../",       "/"                 },
    { "/.././..//./usr///bin/cc/",      "/usr/bin/cc"       },
};

static void p2_tester(const void *data)
{
    const p2_test_case *test = (const p2_test_case *)data;
    char  buffer[256];

    strcpy(buffer, test->input);
    clnpath2(buffer);
    if (strcmp(buffer, test->output) == 0)
        pt_pass("<<%s>> cleans to <<%s>>\n", test->input, buffer);
    else
    {
        pt_fail("<<%s>> - unexpected output from clnpath2()\n", test->input);
        pt_info("Wanted <<%s>>\n", test->output);
        pt_info("Actual <<%s>>\n", buffer);
    }
}

/* -- Phased Test Infrastructure -- */

static pt_auto_phase phases[] =
{
    { p1_tester, PT_ARRAYINFO(p1_tests), 0, "Phase 1 - Testing clnpath()" },
    { p2_tester, PT_ARRAYINFO(p2_tests), 0, "Phase 2 - Testing clnpath2()" },
};

int main(int argc, char **argv)
{

#if 0
    /* Interactive testing */
    printf("Enter pathname: ");
    while (fgets(buffer, sizeof(buffer), stdin) != NULL)
    {
        buffer[strlen(buffer) - 1] = '\0';  /* Zap newline */
        printf("Unclean: <<%s>>\n", buffer);
        clnpath(buffer);
        printf("Clean 1: <<%s>>\n", buffer);
        clnpath2(buffer);
        printf("Clean 2: <<%s>>\n", buffer);
        printf("Enter pathname: ");
    }
    putchar('\n');
#endif /* 0 */

    return(pt_auto_harness(argc, argv, phases, DIM(phases)));
}

#endif /* TEST */

第二个变体使用了一个函数tokenise(),它没有包含在上面的源代码中。如果需要,可以提供。

代码包含测试示例,尽管它使用了一个名为“Phased Test”的库,我还没有正式发布它,因为我有一些打包问题要解决。如果认为需要,可以在短时间内提供。

【讨论】:

  • 谢谢!昨天我确实设法自己编写了一些代码,但我肯定会在我的代码上运行你的测试用例,如果它准备好了,我也会发布我的代码。我主要对测试字符串感兴趣,而不是测试库;我可以很好地 printf() 失败编号和 abort() 失败。
猜你喜欢
  • 1970-01-01
  • 2016-11-10
  • 1970-01-01
  • 2022-11-22
  • 1970-01-01
  • 2016-01-09
  • 2014-06-28
  • 1970-01-01
  • 2012-06-27
相关资源
最近更新 更多