Альтернатива 'realpath' для разрешения "../" и "./" в пути

realpath делает то, что мне нужно, но работает, только если файлы в пути действительно существуют.

Мне нужна функция, которая возвращает нормализованный путь из строки (например, ../some/./directory/a/b/c/../d в some/directory/a/b/d), независимо от того, существуют ли каталоги / файлы на самом деле

По существу эквивалентно PathCanonicalize на окнах.

Такая функция уже существует?

6 ответов

Решение

Я не думаю, что для этого есть какая-либо стандартная библиотечная функция.

Вы можете использовать функцию ap_getparents() в файле исходного кода Apache httpd server/util.c, Я считаю, что это именно то, что вы хотите: https://github.com/apache/httpd/blob/trunk/server/util.c#L500

#ifdef WIN32
#define IS_SLASH(s) ((s == '/') || (s == '\\'))
#else
#define IS_SLASH(s) (s == '/')
#endif

void ap_getparents(char *name)
{
    char *next;
    int l, w, first_dot;

    /* Four paseses, as per RFC 1808 */
    /* a) remove ./ path segments */
    for (next = name; *next && (*next != '.'); next++) {
    }

    l = w = first_dot = next - name;
    while (name[l] != '\0') {
        if (name[l] == '.' && IS_SLASH(name[l + 1])
            && (l == 0 || IS_SLASH(name[l - 1])))
            l += 2;
        else
            name[w++] = name[l++];
    }

    /* b) remove trailing . path, segment */
    if (w == 1 && name[0] == '.')
        w--;
    else if (w > 1 && name[w - 1] == '.' && IS_SLASH(name[w - 2]))
        w--;
    name[w] = '\0';

    /* c) remove all xx/../ segments. (including leading ../ and /../) */
    l = first_dot;

    while (name[l] != '\0') {
        if (name[l] == '.' && name[l + 1] == '.' && IS_SLASH(name[l + 2])
            && (l == 0 || IS_SLASH(name[l - 1]))) {
            int m = l + 3, n;

            l = l - 2;
            if (l >= 0) {
                while (l >= 0 && !IS_SLASH(name[l]))
                    l--;
                l++;
            }
            else
                l = 0;
            n = l;
            while ((name[n] = name[m]))
                (++n, ++m);
        }
        else
            ++l;
    }

    /* d) remove trailing xx/.. segment. */
    if (l == 2 && name[0] == '.' && name[1] == '.')
        name[0] = '\0';
    else if (l > 2 && name[l - 1] == '.' && name[l - 2] == '.'
             && IS_SLASH(name[l - 3])) {
        l = l - 4;
        if (l >= 0) {
            while (l >= 0 && !IS_SLASH(name[l]))
                l--;
            l++;
        }
        else
            l = 0;
        name[l] = '\0';
    }
}

(Это предполагает, что повторное использование лицензионного кода Apache в вашем проекте приемлемо.)

Исходный код Python имеет реализацию os.path.normpath для нескольких платформ. POSIX (в Lib / posixpath.py, для Python 3, строка 318, или для Python 2, строка 308), к сожалению, в Python, но общая логика может быть легко переопределена в C (функция довольно компактна). Проверено многими годами использования.

Существуют и другие реализации платформы normpath в интерпретаторе Python и в исходном коде стандартной библиотеки, так что переносимое решение может быть их комбинацией.

Возможно, другие системы / библиотеки, написанные на C, имеют такую ​​же реализацию, так как функция normpath является критичной в смысле безопасности.

(И главное преимущество наличия кода Python - это возможность тестировать свою функцию в C с любым, даже случайным, параллельным вводом - и этот вид тестирования важен для обеспечения безопасности функции)

Согласно вашей постановке проблемы, следующее делает именно то, что вы просите. Большая часть кода была от path.c как указано в ссылке в комментарии. Модификация для удаления предыдущего ../ был добавлен в соответствии с вашей постановкой проблемы:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void pathCanonicalize (char *path);

int main (int argc, char **argv)
{
    if (argc < 2) {
        fprintf (stderr, "error: insufficient input, usage: %s <path>\n",
                argv[0]);
        return 1;
    }

    char *fullpath = strdup (argv[1]);
    if (!fullpath) {
        fprintf (stderr, "error: virtual memory exhausted.\n");
        return 1;
    }

    pathCanonicalize (fullpath);

    printf ("\n original : %s\n canonical: %s\n\n", argv[1], fullpath);

    free (fullpath);

    return 0;
}

void pathCanonicalize (char *path)
{
    size_t i;
    size_t j;
    size_t k;

    //Move to the beginning of the string
    i = 0;
    k = 0;

    //Replace backslashes with forward slashes
    while (path[i] != '\0') {
        //Forward slash or backslash separator found?
        if (path[i] == '/' || path[i] == '\\') {
            path[k++] = '/';
            while (path[i] == '/' || path[i] == '\\')
                i++;
        } else {
            path[k++] = path[i++];
        }
    }

    //Properly terminate the string with a NULL character
    path[k] = '\0';

    //Move back to the beginning of the string
    i = 0;
    j = 0;
    k = 0;

    //Parse the entire string
    do {
        //Forward slash separator found?
        if (path[i] == '/' || path[i] == '\0') {
            //"." element found?
            if ((i - j) == 1 && !strncmp (path + j, ".", 1)) {
                //Check whether the pathname is empty?
                if (k == 0) {
                    if (path[i] == '\0') {
                        path[k++] = '.';
                    } else if (path[i] == '/' && path[i + 1] == '\0') {
                        path[k++] = '.';
                        path[k++] = '/';
                    }
                } else if (k > 1) {
                    //Remove the final slash if necessary
                    if (path[i] == '\0')
                        k--;
                }
            }
            //".." element found?
            else if ((i - j) == 2 && !strncmp (path + j, "..", 2)) {
                //Check whether the pathname is empty?
                if (k == 0) {
                    path[k++] = '.';
                    path[k++] = '.';

                    //Append a slash if necessary
                    if (path[i] == '/')
                        path[k++] = '/';
                } else if (k > 1) {
                    //Search the path for the previous slash
                    for (j = 1; j < k; j++) {
                        if (path[k - j - 1] == '/')
                            break;
                    }

                    //Slash separator found?
                    if (j < k) {
                        if (!strncmp (path + k - j, "..", 2)) {
                            path[k++] = '.';
                            path[k++] = '.';
                        } else {
                            k = k - j - 1;
                        }

                        //Append a slash if necessary
                        if (k == 0 && path[0] == '/')
                            path[k++] = '/';
                        else if (path[i] == '/')
                            path[k++] = '/';
                    }
                    //No slash separator found?
                    else {
                        if (k == 3 && !strncmp (path, "..", 2)) {
                            path[k++] = '.';
                            path[k++] = '.';

                            //Append a slash if necessary
                            if (path[i] == '/')
                                path[k++] = '/';
                        } else if (path[i] == '\0') {
                            k = 0;
                            path[k++] = '.';
                        } else if (path[i] == '/' && path[i + 1] == '\0') {
                            k = 0;
                            path[k++] = '.';
                            path[k++] = '/';
                        } else {
                            k = 0;
                        }
                    }
                }
            } else {
                //Copy directory name
                memmove (path + k, path + j, i - j);
                //Advance write pointer
                k += i - j;

                //Append a slash if necessary
                if (path[i] == '/')
                    path[k++] = '/';
            }

            //Move to the next token
            while (path[i] == '/')
                i++;
            j = i;
        }
        else if (k == 0) {
            while (path[i] == '.' || path[i] == '/') {
                 j++,i++;
            }
        }
    } while (path[i++] != '\0');

    //Properly terminate the string with a NULL character
    path[k] = '\0';
}

Использование / вывода

$ ./bin/pathcanonical ../some/./directory/a/b/c/../d

 original : ../some/./directory/a/b/c/../d
 canonical: some/directory/a/b/d

Еще одна попытка. Причуды / особенности этого:

  • не канонизируется в исходную строку; пишет в пространство, предоставленное абонентом
  • имеет понятие абсолютного и относительного пути (исходный путь начинался с '/'?): если присутствует достаточное количество '..', чтобы съесть весь источник, испускает '/' для абсолютного пути и '.' для родственника
  • не имеет понятия, соответствуют ли элементы в исходном пути фактическим объектам файловой системы
  • использует массивы переменной длины C99 и, учитывая его возвращение в пространство, предоставленное вызывающей стороной, не использует malloc, но делает несколько копий под капотом.
  • учитывая эти копии, источник и место назначения могут быть одинаковыми
  • использует strtok_r (3), причуды которого не возвращают токены нулевой длины, похоже, соответствуют желаемому поведению для соседних символов '/'.

Источник:

#include <stdlib.h>
#include <string.h>

int
pathcanon(const char *srcpath, char *dstpath, size_t sz)
{
    size_t plen = strlen(srcpath) + 1, chk;
    char wtmp[plen], *tokv[plen], *s, *tok, *sav;
    int i, ti, relpath;

    relpath = (*srcpath == '/') ? 0 : 1;

    /* make a local copy of srcpath so strtok(3) won't mangle it */

    ti = 0;
    (void) strcpy(wtmp, srcpath);

    tok = strtok_r(wtmp, "/", &sav);
    while (tok != NULL) {
        if (strcmp(tok, "..") == 0) {
            if (ti > 0) {
                ti--;
            }
        } else if (strcmp(tok, ".") != 0) {
            tokv[ti++] = tok;
        }
        tok = strtok_r(NULL, "/", &sav);
    }

    chk = 0;
    s = dstpath;

    /*
     * Construct canonicalized result, checking for room as we
     * go. Running out of space leaves dstpath unusable: written
     * to and *not* cleanly NUL-terminated.
     */
    for (i = 0; i < ti; i++) {
        size_t l = strlen(tokv[i]);

        if (i > 0 || !relpath) {
            if (++chk >= sz) return -1;
            *s++ = '/';
        }

        chk += l;
        if (chk >= sz) return -1;

        strcpy(s, tokv[i]);
        s += l;
    }

    if (s == dstpath) {
        if (++chk >= sz) return -1;
        *s++ = relpath ? '.' : '/';
    }
    *s = '\0';

    return 0;
}

Изменить: пропустил проверку на номер, когда s == dstpath. Легальные абоненты, скорее всего, предоставят более 0 или 1 байт целевого хранилища, но это сложный мир.

Я предполагаю, что ваш хост Windows или Unix (оба поддерживают .., ., а также / означает родительский каталог, текущий каталог и разделитель каталогов соответственно). И что ваша библиотека обеспечивает доступ к указанной в функции posix getcwd() который извлекает текущий рабочий каталог вашей программы (т. е. полный путь, куда будут записываться выходные файлы, если они открыты без указания пути в их имени файла).

Первый звонок getcwd() восстановить рабочий каталог. Если последний символ в этом '/', добавьте этот рабочий каталог к ​​вашей входной строке без изменений. В противном случае добавьте и его, и символ '/' к вашей строке.

Затем просто обработайте строку. Найти первый экземпляр строки "../" и удалите предыдущую часть пути и "../", Например, если строка "/a/b/c/../foo" результат будет "/a/b/foo", Повторять до тех пор, пока не будет "../" в строке.

Единственное предостережение - решать, что делать с такими строками, как "/../" (технически это путь, который не может существовать). Либо оставь это как "/" (таким образом, вы всегда получаете путь, который выполним) или сообщаете об ошибке.

Как только это будет сделано, ищите случаи "/./" и заменить их на "/", Это превратит строки как "/a/b/c/./" в "/a/b/c/" но оставит строки как "/a/b/c./" (которые указывают каталог с именем "c." в "/a/b") в одиночестве.

Все вышеперечисленное просто обрабатывает строку. Помимо использования getcwd(), нет ничего, что зависит от среды хоста. Таким образом, процесс будет одинаковым независимо от того, существует ли путь на самом деле.

Несколько наворотов могут включать улучшение работы с окнами, например, лечение '/' а также '\' как эквивалент, и справиться с такими спецификаторами диска, как "a:",

Если вы не хотите звонить getcwd() (например, если ваша программа не полагается на наличие действующего каталога или если он существует, но не существует), вам необходимо указать начальное условие. Например, где будет строка, как "../x/y/z" завершить?

То, что я предложил, позволяет . символ должен быть частью имен файлов (или имен каталогов), которые вы можете или не хотите. Отрегулируйте по мере необходимости.

Похоже, вы работаете на *nix (например, Linux).

Q: у вашего компилятора есть canonicalize_file_name ()?

В противном случае, если вы программируете на C++, вы можете рассмотреть Boost:

повышение:: файловая система:: канонической

Другие вопросы по тегам