PATH_MAX


C и C++ программисты в какой-то момент могут столкнуться с ограниченным размером PATH_MAX и задаться вопросом – какого размера создавать буфер, чтобы отследить путь к директориям или файлам?


Большинство операционных и файловых систем, которые я встречал, чаще всего выделяют около 255 байтов на имя файла или путь. Но как же тогда узнать полный путь к файлу или папке?


Примечание переводчика. Ссылка на оригинальную статью "PATH_MAX simply isn't" всплыла в обсуждении на сайте linux.org.ru. Статья своеобразная, но она привлекла наше внимание по следующим соображениям. Если PATH_MAX не всегда достаточно, то возможно стоит научить анализатор PVS-Studio искать какие-то связанные с этим проблемы. Но, если честно, что-то ничего не придумалось, да и рассматриваемая в статье проблематика выглядит неубедительно. Тем не менее, раз уж мы обратили внимание на эту тему, наша команда решила перевести статью и поделиться ею с аудиторией Хабра. Возможно, кто-то поделится схожим опытом и это наведёт нас на мысли, как подстраховать программистов, выдавая новые предупреждения в PVS-Studio.

Многие подумают, что им достаточно буфера размера в PATH_MAX или PATH_MAX+1. Некоторые вместо него додумываются использовать строки C++ (std::string или их аналоги из других библиотек). Но динамические строки в вашей программе – это решение лишь половины всех проблем.


Даже C++ программисту может в какой-то момент понадобиться вызов функций getcwd или realpath (fullpath в Windows), которые принимают не std::string, а указатель на буфер. Однако, согласно стандарту, эти функции не выделяют память самостоятельно.


Функция getcwd служит для возврата текущей рабочей директории. Функция realpath может принимать относительный или абсолютный путь к любому имени файла, содержащему '..', '/././.' или дополнительные слеши, символические ссылки (symlinks) и тому подобное, и возвращает полный путь без дополнительного мусора.


Однако у этих функций есть недостаток – значение PATH_MAX может меняться в зависимости от используемой платформы. Каждая операционная система способна определить PATH_MAX любого размера, который ей захочется. На моей системе Linux он равен 4096, в системе OpenBSD – 1024, а в Windows – 260.


При выполнении теста на своей системе Linux, я заметил, что на файловой системе ext3 компонент пути ограничен 255 символами, но это не мешает мне создавать столько вложенных путей, сколько я захочу. Я смог создать путь длиной в 6000 символов. Linux никак не препятствует мне создавать такой большой путь или монтировать ещё несколько путей. Запуск функции getcwd на таком большом пути, даже с огромным буфером, не работает. Он не работает ни с чем, превышающим максимальное значение PATH_MAX.


Даже такая ОС, как macOS X, ограничивает путь 1024 символами, но тесты показывают, что можно создать путь длиной в несколько тысяч символов. Примечательно, что, если вы передадите в функцию getcwd достаточно большой буфер под macOS X, она правильно определит путь, превышающий PATH_MAX. Возможно, потому что объявление для функции getcwd выглядит примерно так:


char *getcwd(char *buf, size_t size);

Таким образом, умная getcwd сможет работать при достаточном количестве байтов. Но, к сожалению, нельзя заранее узнать сколько байтов вам понадобится. Поэтому аллоцировать память заранее не получится. Придётся выделять всё новые и новые буферы большего размера в надежде, что один из них подойдёт. Это весьма нерационально.


Мы уже знаем, что размер пути может превышать значение PATH_MAX. Однако его переопределение будет ошибочно, так как его используют другие функции. Работа этих функций будет нарушена.


Исключением является Windows. Она не позволяет создавать пути размером более 260 символов. Если такой путь будет создан в другой ОС, то в Windows открыть его будет невозможно. Весьма странно, что Windows так ограничил размер пути, учитывая, что архитектура файловой системы FAT не имеет такого ограничения, а в NTFS можно создать пути длиной до 32768 символов. С лёгкостью могу себе представить человека с внушительной коллекцией аудиофайлов с путём длиной в 300+ символов:


"C:\Documents and Settings\Jonathan Ezekiel Cornflour\My Documents\My Music\My Personal Rips\2007\Technological\Operating System Symphony Orchestra\The GNOME Musical Men\I Married Her For Her File System\You Don't Appreciate Marriage Until You've Noticed Tax Pro's Wizard For Married Couples.Track 01.MP5"


Пока не забыл, вот вам объявление realpath:


char *realpath(const char *file_name, char *resolved_name);

Глядя на это объявление, вас, должно быть, сразу заинтересовало: а где же параметр, задающий размер resolved_name? Мы же не хотим переполнения буфера! Если заглянуть в документацию, мы увидим, что операционная система задаёт его на основе PATH_MAX.


Это значит, что realpath не может работать с путём большего размера. Более того, ни одна умная ОС не сможет обойти это ограничение, если только она не проверит размер полученного буфера.


Поэтому я решил реализовать getcwd и realpath самостоятельно. Мы обсудим специфику realpath в следующий раз, а пока сосредоточимся на том, как можно сделать свой собственный getcwd.


Идея заключается в том, чтобы идти от рабочего каталога к корню, по пути отмечая, какой компонент пути мы только что прошли.


В современных ОС есть функция stat, которая может взять часть пути и вернуть о нём информацию, например, когда он был создан, на каком устройстве находится и т.п. Большинство ОС, исключая Windows, возвращают поля st_dev и st_ino, на основе которых можно точно распознать любой файл или папку. Если эти два поля совпадают с данными, полученными другим способом в той же системе, вы можете быть уверены, что это один и тот же файл или папка.


Наш алгоритм будет обрабатывать символы '.' и '/' особым образом – при каждой итерации цикла, когда current_id не равен root_id, мы можем заменить текущую папку на '.'. Затем мы сканируем директорию (используя opendir + readdir + closedir) на наличие компонента с таким же id. Как только id будет найден, мы можем обозначить его как правильное имя для текущего уровня и перейти на уровень выше.


Ниже приведён код, демонстрирующий вышесказанное на C++:


bool getcwd(std::string& path)
{
  typedef std::pair<dev_t, ino_t> file_id;

  bool success = false;
  //Keep track of start directory, so can jump back to it later
  int start_fd = open(".", O_RDONLY);
  if (start_fd != -1)
  {
    struct stat sb;
    if (!fstat(start_fd, &sb))
    {
      file_id current_id(sb.st_dev, sb.st_ino);
      //Get info for root directory, so we can determine when we hit it
      if (!stat("/", &sb))
      {
        std::vector<std::string> path_components;
        file_id root_id(sb.st_dev, sb.st_ino);

        //If they're equal, we've obtained enough info to build the path
        while (current_id != root_id)
        {
          bool pushed = false;

          if (!chdir("..")) //Keep recursing towards root each iteration
          {
            DIR* dir = opendir(".");
            if (dir)
            {
              dirent* entry;
              //We loop through each entry trying to find where we came from
              while ((entry = readdir(dir)))
              {
                if ((strcmp(entry->d_name, ".")
                     && strcmp(entry->d_name, "..")
                     && !lstat(entry->d_name, &sb)))
                {
                  file_id child_id(sb.st_dev, sb.st_ino);
                  //We found where we came from, add its name to the list
                  if (child_id == current_id)
                  {
                    path_components.push_back(entry->d_name);
                    pushed = true;
                    break;
                  }
                }
              }
              closedir(dir);
              //If we have a reason to contiue, we update the current dir id
              if (pushed && !stat(".", &sb))
              {
                current_id = file_id(sb.st_dev, sb.st_ino);
              }
              //Else, Uh oh, can't read information at this level
            }
          }
          if (!pushed)
          {
            //If we didn't obtain any info this pass, no reason to continue
            break;
          }
        }
        if (current_id == root_id) //Unless they're equal, we failed above
        {
          //Built the path, will always end with a slash
          path = "/";
          for (auto i = path_components.rbegin();
               i != path_components.rend(); ++i)
          {
            path += *i + "/";
          }
          success = true;
        }
        fchdir(start_fd);
      }
    }
    close(start_fd);
  }
  return(success);
}

Перед использованием, давайте обсудим недостатки этого метода:


  • Как упоминалось выше, наша реализация getcwd не работает для Windows, однако, это можно поправить, добавив встроенный буфер размера PATH_MAX под #ifdef.
  • Мы назвали функцию getcwd. Из-за этого возникнет конфликт со встроенной функцией на языке C. Чтобы исправить это, нужно переименовать функцию или поместить её в собственное пространство имён.
  • Во встроенные реализации getcwd в конце кода я поставил слеш. Я обычно объединяю его с именем файла, но имейте в виду, что если вы используете его при передаче таким функциям, как access, stat, opendir, chdir и т.п., ОС может не понравиться выполнение вызова. Я заметил эту проблему только в DJGPP и некоторых функциях. Поэтому, если для вас это важно, цикл в конце функции можно легко изменить так, чтобы не добавлялся слеш в конец, за исключением случая, когда корневой каталог – это весь путь к файлу.
  • Эта функция также изменяет директорию в процессе работы, поэтому она не является потокобезопасной. Но, опять же, многие встроенные реализации тоже могут являться не потокобезопасными. Если вы используете потоки, вычисляйте все необходимые пути до процесса их создания. Вероятно, стоит подумать и над тем, чтобы и дальше использовать абсолютные пути и не модифицировать их из других мест программы. Или же вам придётся оборачивать её вызов в mutex, что также вполне допустимо.
  • Также некоторые уровни пути могут быть недоступны для чтения. Такая проблема может возникнуть в UNIX-системах, где для входа в папку требуется разрешение на выполнение. В этом случае, можно реализовать fallback механизм, который поможет обойти это. Если кто-то сталкивался с похожим, пожалуйста, напишите в комментариях, что вы делали в таком случае.
  • И последнее – эта функция написана на C++, что может быть неудобно для пользователей C. Класс std::vector можно заменить на односвязный список, а в конце выделить необходимый размер буфера и вернуть его. В этом случае потребуется освободить буфер снаружи.

В качестве альтернативы вместо списка можно использовать буфер, который постоянно реаллоцируется во время построения пути, используя memmove для перемещения построенной части пути в начало буфера.


Во время работы остальной части программы все манипуляции с путями должны осуществляться с использованием безопасных строк, управляющих распределением памяти, таких как std::string. Также можно использовать описанные выше функции, реализовав свой аллокатор. Поскольку невозможно получать информацию о размере пути заранее, внимательно следите за тем, чтобы размер пути не выходил за пределы возможности буфера.


Надеюсь, разработчики понимают, что если PATH_MAX определение ошибочно, то лучше его не использовать, за исключением Windows. Также надеюсь, что многие после прочтения этой статьи исправят ошибки в своих приложениях.

Комментарии (22)


  1. DistortNeo
    25.08.2022 14:38
    +7

    В Windows есть ещё UNC-пути и API для них, где длина ограничена уже 32767 двухбайтовыми символами.


  1. xi-tauw
    25.08.2022 15:08
    +2

    "C:\Documents and Settings\Jonathan Ezekiel Cornflour\My Documents\My Music\My Personal Rips\2007\Technological\Operating System Symphony Orchestra\The GNOME Musical Men\I Married Her For Her File System\You Don't Appreciate Marriage Until You've Noticed Tax Pro's Wizard For Married Couples.Track 01.MP5"

    Я сначала не понял, а потом как понял. Статья-то 2007 года. Вроде уже в Windows 8 в дефолтной поставке были пути больше 260 символов, из-за чего она не хотела вставать на FAT32.


    1. fedorro
      25.08.2022 15:26
      +2

      Шел 2022, а виндовый Проводник так и не умеет длинные пути ????


      1. Einherjar
        25.08.2022 16:25
        +1

        Создавать не умеет, но открывает без проблем


        1. fedorro
          25.08.2022 16:30
          +1

          А также не умеет копировать, перемещать и удалять:

          Мем


          1. Einherjar
            25.08.2022 16:42

            Ради любопытства скопировал файл из папки с очень длинным путем, скопировался. Попробовал удалить эту папку - удалилась, переименование как и прочие изменения внутри не работает. Но кривизна работы проводника с длинными путями напрямую следует из полного отсутствия пользователей которым бы это было нужно.


            1. fedorro
              25.08.2022 16:55
              +1

              Удалять научился, действительно, но остальное так и нет:

              Hidden text

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


              1. Einherjar
                25.08.2022 17:06
                -3

                Можно конкретный пример? Столь длинные пути уже сильно человеконечитаемые чтобы через UI файлового менеджера, тем более такого базового как проводник, туда пробиться. Соответственно если приложение их создало то оно с ними и должно работать, а лезть в такое тыкая мышкой из проводника как минимум очень неудобно.


                1. fedorro
                  25.08.2022 17:19
                  +3

                  У меня название диссера 227 символов (само название, без всего остального), приходится около корня хранить. Многие книги в электронном виде имеют длинное название, и оно в папке вписано, т.к. там ещё приложения со структурой папок. Раньше npm постоянно создавал не удаляемую из проводника node_modeules. В пути проектов как впишут название организации, проекта, релиза тд, тп - постоянно приходится придумывать куда покороче смапить, чтобы не посыпались ошибки. C:\User\Projects\GIT\ уже стал C:\G\ . и т.д.

                  Я понимаю когда ресурсы ПК были ограничены, ФС, не умели, АПИ не было, но сейчас то что мешает не создавать пользователям лишние проблемы? 640 КБ 260 "буков" хватит всем?


                  1. Einherjar
                    25.08.2022 17:30

                    Вам реально удобно работать с файлом имя которого 227 символов? Если назвать короче проще и понятнее, вы не найдете его среди других, или какие еще трудности это вызовет?

                    Hidden text

                    Дело не в том хватит 640 или не хватит а в том что корень юзабилити проблем которые вы описываете вовсе не в проводнике. Ну вот как вы себе представляете отображение на экране пути к файлу длиной 10000 символов?


                    1. fedorro
                      25.08.2022 17:37
                      +3

                      Именно что не найду, т.к. там ещё примеры и прочие такие-же файлы лежат. Иногда надо найти по ключевому слову в названии, а его нет, под сокращение попало... Приходится индексаторы всякие прикручивать. К тому большинство таких длинных файлов появляется извне.


                      1. Einherjar
                        25.08.2022 17:55

                        Ну а когда надо искать по ключевому слову, которого и так изначально не было в названии, вы что делаете?


                      1. fedorro
                        25.08.2022 19:11
                        +3

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


                      1. Einherjar
                        25.08.2022 19:33

                        Полноценная каталогизация это теги, индексы и это вот все, а не проводник и обфусцированные имена файлов в килобайт длиной.


              1. qw1
                26.08.2022 13:44

                Удалять научился, действительно, но остальное так и нет:
                Это может быть продуманной фичей от MS: большинство программ выделяют буфер в MAX_PATH, например, для диалога открытия файла. И таким образом, ОС сигнализирует пользователю, что будут проблемы, чтобы он не создавал такие файлы. А удалять — пожалуйста.


                1. fedorro
                  26.08.2022 13:54
                  +1

                  Лучше бы она подала сигнал этому большинству программ что не стоит использовать АПИ с ограничениями времен DOS.


                  1. qw1
                    26.08.2022 14:15

                    Хипстеры пусть идут в свои макоси и линуксы, а Windows известна своей поддержкой legacy. Так что это Windows-юзеры подают сигнал, куда развивать систему.

                    Какая-нибудь CRM, написанная в середине 2000-х на delphi 5, её уже невозможно перевести на другую платформу, даже если разработчики активно развивают эту CRM. Приходится мириться с тем, что есть.


                    1. fedorro
                      26.08.2022 14:24

                      Ну и пусть старьё хранит свои файл в C:\Data\, а когда одни программы в винде создают файлы, которые в других программах открываются, и всё хорошо, а проводник, вдруг, не может.

                      По вашей логике надо было и ту прорву опций что на вкладке "Совместимость" у экзешников оставить по умолчанию, и сидеть в разрешении 640x480 на 256 цветов, а то вдруг CRM-ка не заведется...


                      1. qw1
                        26.08.2022 19:49

                        А так и есть… На 4к мониторе старые программы апскейлятся с мылом (но зато максимально безопасно), и нужно каждую вручную переключать индивидуально на режим качественного рендеринга.


            1. tzlom
              26.08.2022 09:42
              +2

              Наоборот, это отсутствие пользователей следует из того, что стандартные инструменты не умеют в длинные пути


  1. a-tk
    25.08.2022 16:54
    +2

    А в винде ещё можно переключаться между коротким (260 символов) и длинным режимом (32к символов), меняя ключ в реестре, но при этом надо ещё затащить в манифест специальное значение.


    1. screwer
      25.08.2022 21:20
      +2

      Подозреваю что не "в Винде переклбчаться", а менять ограничения Win32 API, по большей части вызванные вопросами своместимости. NT со своего рождения (1993) прекрасно умеет открывать файлы с длиной пути до 32767. Через свое же родное Native API.