【问题标题】:Check if a fstream is either a file or directory检查 fstream 是文件还是目录
【发布时间】:2015-06-01 08:10:54
【问题描述】:

我正在使用 C++ fstream 来读取配置文件。

#include <fstream>
std::ifstream my_file(my_filename);

现在,如果我传递一个目录的路径,它会默默地忽略它。例如。 my_file.good() 返回 true,即使 my_filename 是一个目录。由于这是我的程序的意外输入,我喜欢检查它并抛出异常。

如何检查刚刚打开的 fstream 是否为常规文件、目录或流?

我似乎也找不到办法:

  • 从给定的 ifstream 中获取文件描述符。
  • 使用其他机制在 ifstream 中查找此信息。

some forum discussion 中建议两者都不可能,因为这取决于操作系统,因此永远不可能成为 fstream C++ 标准的一部分。

我能想到的唯一选择是重写我的代码以完全摆脱 ifstream 并使用文件描述符的 C 方法 (*fp) 以及 fstat():

#include <stdio.h>
#include <sys/stat.h>
FILE *fp = fopen(my_filename.c_str(), "r");
// skip code to check if fp is not NULL, and if fstat() returns != -1
struct stat fileInfo;
fstat(fileno(fp), &fileInfo);
if (!S_ISREG(fileInfo.st_mode)) {
    fclose(fp);
    throw std::invalid_argument(std::string("Not a regular file ") + my_filename);
}

我更喜欢 fstream。因此,我的问题。

【问题讨论】:

  • 什么标准库实现,在什么操作系统上?我很难相信事情会像你描述的那样出错。如果myfilename 是一个目录,good() 应该返回 false。
  • 我使用的是 OS X、10.10、C++11。
  • 我相信旧的(或者似乎不那么旧的)Unix 衍生版本允许将目录作为文件打开以读取其内容,因此虽然这令人惊讶,但并不令人惊讶。顺便说一句:如果您使用fstream 阅读该目录的内容是什么?
  • 编辑:使用普通的旧 open(),它似乎读取了该目录中文件的内容。我没有检查是否会读取所有文件。使用 ifstream,它看起来就像是一个空文件。
  • 如果有帮助:gist.github.com/macfreek/5bfab1be77784cc048d7 测试 ifstream 的行为。

标签: c++ c++11 fstream ifstream fstat


【解决方案1】:

解决这个问题有不同的方法:

  1. 忽略它。说真的,如果目录内容作为有效配置通过,我会感到惊讶。如果没有,解析无论如何都会失败,因此您不会冒导入错误数据的风险。此外,您不会阻止用户提供管道或类似的不是文件的东西。
  2. 在打开之前检查路径。您可以使用 stat() 或直接使用 Boost.Filesystem 或一些类似的库。我不是 100% 确定 C++11 中是否添加了类似的东西。请注意,这会产生竞争条件,因为在您检查之后但在打开之前,一些攻击者可能会使用目录切换文件。
  3. 通常,有一些方法可以从fstream 检索低级句柄,在您的情况下可能是FILE*。还有一些方法可以从FILE* 创建iostream(不一定是fstream!)。这些总是特定于实现的扩展,因此您需要一些#ifdef 魔术来定制您的代码,以针对使用的标准库实现。我敢于依靠他们的存在,即使没有,如果您需要移植到一些无法提供更简单方法的晦涩系统,您仍然可以在 FILE* 上创建 streambuf

【讨论】:

  • 它绝对没有被添加到 C++11 或 C++14 中,我也不知道在不久的将来会包含它的严肃计划。在 cmets 中他说open() 也成功了,并读取了文件夹中文件的内容。
【解决方案2】:
void assertGoodFile(const char* fileName) {
   ifstream fileOrDir(fileName);
   //This will set the fail bit if fileName is a directory (or do nothing if it is already set  
   fileOrDir.seekg(0, ios::end);
   if( !fileOrDir.good()) {
      throw BadFile();
   };
}

【讨论】:

  • 不幸的是,这对我在 Linux 2.6.32-042stab092.3 i686 和 Darwin 14.1.0 x86_64 上都不起作用。
  • 我的错,它没有为我重新编译 ios::beg 测试,所以我仍在运行使用 ios::end 编译的二进制文件。但是seekg(0, ios::end) 确实对我有用,可以区分目录和文件。不幸的是,那个也会探测到管道。
【解决方案3】:

由于 IO 操作依赖于操作系统,因此想法很复杂。

我在 OS X 10.10.2、Linux 2.6.32 和 FreeBSD 8.2-RELEASE 上尝试了一些技术(后两者是稍旧的操作系统,我使用了一些旧的 VirtualBox VM)。

  • 我还没有找到万无一失的方法。如果您真的想检查,请在路径上使用stat(),或者使用老式C open()fstat()
  • PSkocik 建议的 seekg(0, std::ios::beg); 方法对我不起作用。
  • 对于 fstream,最好的方法是打开并读取文件,然后等待错误。
  • badbitfailbit 都必须设置,特别是在 OS X 上,以引发所有必需的异常。例如。 my_file.exceptions(std::ios::failbit | std::ios::badbit);
  • 这还会在读取常规文件后导致文件结束 (EOF) 异常,这需要代码忽略这些正常异常。
  • my_file.eof() 也可能在更严重的错误时设置,因此对EOF 条件的检查效果不佳。
  • errno 是一个更好的指标:如果引发异常,但 errno 仍为 0,则很可能是 EOF 条件。
  • 这并不总是正确的。在 FreeBSD 8.2 上,打开目录路径只会返回二进制 gobbledygook,而不会引发异常。

在我测试过的 3 个平台上,这个实现似乎在某种程度上可以处理它。

#include < iostream>
#include < fstream>
#include < cerrno>
#include < cstring>

int main(int argc, char *argv[]) {
   for (int i = 1; i < argc; i++) {

      std::ifstream my_file;
      try {
         // Ensure that my_file throws an exception if the fail or bad bit is set.
         my_file.exceptions(std::ios::failbit | std::ios::badbit);
         std::cout << "Read file '" << argv[i] << "'" << std::endl;
         my_file.open(argv[i]);
         my_file.seekg(0, std::ios::end);
      } catch (std::ios_base::failure& err) {
         std::cerr << "  Exception during open(): " << argv[i] << ": " << strerror(errno) << std::endl;
         continue;
      }
      
      try {
         errno = 0; // reset errno before I/O operation.
         std::string line;
         while (std::getline(my_file, line))
         {
            std::cout << "  read line" << std::endl;
            // ...
         }
      } catch (std::ios_base::failure& err) {
         if (errno == 0) {
            std::cerr << "  Exception during read(), but errno is 0. No real error." << std::endl;
            continue; // exception is likely raised due to EOF, no real error.
         }
         std::cerr << "  Exception during read(): " << argv[i] << ": " << strerror(errno) << std::endl;
      }
   }
}

【讨论】:

    【解决方案4】:

    从 C++17 开始,我们可以使用 filesystem(基于 boost::filesystem)。这将比 C++ 中的其他任何东西都更便携(尽管我认为stat() 也很有效)。

    您有两个可用的函数,一个在需要时返回错误代码。

    bool is_regular_file( const path& p );
    bool is_regular_file( const path& p, error_code& ec );
    

    您可以将这些与以下内容一起使用:

    #include <filesystem>
    
    ...
        if(!std::filesystem::is_regular_file(path))
        {
            throw std::runtime_error(path + " was expected to be a regular file.");
        }
    ...
    

    cppreference 上查找更多详细信息。

    (警告: 一些编译器说他们有 c++17,但文件系统可能仍然是实验性的;即 GNU C++ 只有从 g++ v8.0 开始才有完整版本,有关详细信息,请参阅此问题:Link errors using <filesystem> members in C++17)

    【讨论】:

    • 另一个警告是 Xcode 支持取决于部署目标,MacOS 为 10.15,iOS 为 13.0 等。因此即使在 MacOS 10.15 上编译为 C++17 标准,如果您的最低部署目标是 10.14 或更早版本,您不能使用 std::filesystem
    • 这个解决方案是最好的。
    猜你喜欢
    • 1970-01-01
    • 2011-05-31
    • 2013-03-15
    • 1970-01-01
    • 2018-12-18
    • 1970-01-01
    • 1970-01-01
    • 2011-04-20
    • 1970-01-01
    相关资源
    最近更新 更多