Check if a fstream is either a file or directory
Asked Answered
A

6

14

I'm using C++ fstream to read a config file.

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

Right now, if I pass the path of a directory, it silently ignores this. E.g. my_file.good() returns true, even if my_filename is a directory. Since this is unintended input for my program, I like to check for it, and throw an exception.

How do I check if a just opened fstream is a regular file, directory or stream?

I can't seem to find a way to either:

  • get the file descriptor from a given ifstream.
  • use some other mechanism to find this info in the ifstream.

In some forum discussion it was suggested that neither is possible because this is OS-dependant, and thus could never be part of the fstream C++ standard.

The only alternative I can think of is to rewrite my code to get rid of ifstream altogether and resort to the C-method of a file descriptor (*fp), along with 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);
}

I prefer fstream. Hence, my question.

Arundinaceous answered 27/3, 2015 at 21:3 Comment(5)
What standard library implementation, and on what operating system? I find it hard to believe something could go as awry as you describe. good() should return false if myfilename is a directory.Journal
I'm using OS X, 10.10, C++11.Arundinaceous
I believe that old (or, it seems, not so old) Unix-derivates allowed opening directories as files to read their content, so while this is surprising, it isn't that surprising. BTW: What's the content of that directory if you read it with an fstream?Absorptivity
Edit: with plain old open(), it seems to read the contents of files in that directory. I have not checked if all files would be read. With ifstream, it seems to act as if it was an empty file.Arundinaceous
If it helps: gist.github.com/macfreek/5bfab1be77784cc048d7 to test the behaviour of ifstream.Arundinaceous
C
3
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();
   };
}
Callao answered 27/3, 2015 at 21:55 Comment(2)
Unfortunately, this did not work for me on either Linux 2.6.32-042stab092.3 i686, nor on Darwin 14.1.0 x86_64.Arundinaceous
My bad, it didn't recompile for me for the ios::beg test so I was still running the binary compiled with ios::end. But seekg(0, ios::end) does work for me for telling directories and files apart. Unfortunately, that one will probebly throw for pipes too.Callao
J
3

Starting with C++17, we have the filesystem available to us (based on boost::filesystem). This will be much more portable than anything else in C++ (although the stat() works very well too, I think).

You have two functions available, one returns an error code if you need it.

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

You use these with the following:

#include <filesystem>

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

Find more details on cppreference.

(WARNING: some compilers say they have c++17 but filesystem may still be experimental in them; i.e. GNU C++ has the full version only since g++ v8.0, for details, see this question: Link errors using <filesystem> members in C++17)

Jackscrew answered 28/6, 2020 at 22:24 Comment(2)
Another warning is that Xcode support depends on the deployment target, 10.15 for MacOS, 13.0 for iOS, etc. So even if compiling to the C++17 standard on MacOS 10.15, if your minimum deployment target is 10.14 or earlier you can't use std::filesystem.Tocopherol
This solution is the best.Bellinger
A
2

There are different approaches to solving this issue:

  1. Ignore it. Seriously, if the directory content passes as valid configuration, I'd be surprised. If not, parsing will fail anyway, so you run no risk of importing bad data. Also, you don't impede users from providing a pipe or something similar that is not a file.
  2. Check the path before opening it. You could use stat() or directly use Boost.Filesystem or some similar library. I'm not 100% sure if anything similar was added to C++11. Note that this creates a race condition though, because after you checking but before opening, some attacker could switch the file with a directory.
  3. Typically, there are ways to retrieve a low-level handle from the fstream, in your case probably a FILE*. There are also ways to create an iostream (not necessarily an fstream!) from a FILE*. Those are always implementation-specific extensions, so you need some #ifdef magic to tailor your code specific to the used stdlibrary implementation. I would dare to rely on their presence, even if not you can still create a streambuf on tof of a FILE* if you need to port to some obscure system that doesn't provide an easier way.
Absorptivity answered 27/3, 2015 at 21:34 Comment(1)
It was definitely not added to C++11 nor C++14 nor am I aware of serious plans for it to be included in the near future. In the comments he says open() also succeeds, and reads the contents of the files in the folder.Journal
A
2

Thinks are complicated, due to the OS-dependency of the IO-operations.

I tried a few techniques on OS X 10.10.2, Linux 2.6.32, and FreeBSD 8.2-RELEASE (the later two are slightly older OSes, I used some older VirtualBox VMs).

  • I have not yet found a fool-proof method. If you really want to check, use stat() on the path, or the old-fashioned C open() with fstat().
  • The seekg(0, std::ios::beg); method suggested by PSkocik did not work for me.
  • For fstreams, the best method is just to open and read from the file, and await an error.
  • Both the badbit AND failbit must be set, particular on OS X, in order to raise all required exceptions. E.g. my_file.exceptions(std::ios::failbit | std::ios::badbit);
  • This also causes an end-of-file (EOF) exception after reading a regular file, which requires code to ignore these normal exceptions.
  • my_file.eof() may also be set upon more serious errors, so it is a poor check for the EOF condition.
  • errno is a better indicator: if an exception is raised, but errno is still 0, it is most likely a EOF condition.
  • This is not always true. On FreeBSD 8.2, opening a directory path just returns binary gobbledygook, while no exceptions are ever raised.

This is the implementation that seems to handle it somewhat reasonable across the 3 platforms I've tested.

#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;
      }
   }
}
Arundinaceous answered 28/3, 2015 at 0:37 Comment(0)
L
0

I had the same issue, I replace ifstream by fstream, and it worked

ifstream is only for reading file fstream is for reading and writing

fstream probably try to write to the directory when you open it, which cause an error you can catch with file.good() in an if statement:

Leila answered 15/12, 2022 at 11:28 Comment(0)
D
0

I am learning C++ and faced the same problem, so here is something that worked for me:

    std::ifstream   input_file(av[1]);
    if (!input_file)
        throw (OpenInfileError());

    if (input_file.is_open() && input_file.peek() == std::ifstream::traits_type::eof())
        throw (EmptyInfileError());

The first if statement checks if input_file exists.
The second one checks if input_file is empty. It will also throw the error if it is a folder.

Despond answered 30/3, 2023 at 14:36 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.