Making stdin writable in a safe and portable way

2019-09-22 01:34发布

I was trying to run two programs.

Case 1

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

int main()
{
        int n;
        int k = 10;
        int ret_val = 0;
        ret_val = write (0, &k, sizeof(int));
        if (-1 == ret_val)
        {
                printf ("Failed to write");
                exit (EXIT_FAILURE);
        }
        scanf ("%d", &n);
        printf ("Integer read is %d \n", n);
        return 0;
}

Then I tried the next one.

Case 2

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
        int n;
        int k = 10;
        int ret_val = 0;

        /* Open file from which content shall be inserted to stdin_buffer */
        int source_fd = open ("file_in.txt", O_CREAT | O_RDWR, S_IRWXU);
        if (-1 == source_fd)
        {
                printf ("Failed to open file for reading");
                exit (EXIT_FAILURE);
        }
        int stdin_fd;

        /* Close STDIN_FILENO */
        close(0);
        /* dup the source */
        stdin_fd = dup (source_fd);
        if (-1 == stdin_fd)
        {
                printf ("Failed to dup");
                exit (EXIT_FAILURE);
        }

        /* write to stdin_buffer (content will be taken from file_in.txt) */
        ret_val = write (stdin_fd, &k, sizeof(int));
        if (-1 == ret_val)
        {
                printf ("Failed to write to stdin_buffer");
                exit (EXIT_FAILURE);
        }

        scanf ("%d", &n);
        printf ("Integer read is %d \n", n);

        close(source_fd);
        return 0;
}

Now in the first case, I was not able to write to stdin. In the second case, I was able to take the input from a file, "file_in.txt", and send the content to the standard input buffer.

I couldn't get a good explanation for why my first case didn't work out. Can someone explain?

stdin should be like any other file right? If it is write protected, fine. But then when I redirected the input (in the second case), there was no "permission denied" problem. This code seems to be non-portable. Is there a portable and safe way to redirect stdin from a file?


After going through the comments, I have come up with a better working code. I would like some feedback on this code

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>

#define LEN 100

int main()
{
        int n;
        char buffer[LEN];
        memset (buffer, '\0', LEN);
        int ret_val = 0;

        /* Open file from which content shall be inserted to stdin_buffer */
        int source_fd = open ("file_in.txt", O_CREAT | O_RDONLY, S_IRWXU);
        if (-1 == source_fd)
        {
                perror ("Failed to open file for reading");
                exit (EXIT_FAILURE);
        }
        /* Temp stdin_buffer */
        int temp_fd = open ("temp_in.txt", O_CREAT | O_RDWR, S_IRWXU);
        if (-1 == temp_fd)
        {
                perror ("Failed to open temp stdin");
                exit (EXIT_FAILURE);
        }

        int stdin_fd;
        /* Close STDIN_FILENO */
        close(0);
        /* dup the source */
        stdin_fd = dup (temp_fd);
        if (-1 == stdin_fd)
        {
                perror ("Failed to dup");
                exit (EXIT_FAILURE);
        }


        ret_val = read (source_fd, buffer, LEN);
        if (-1 == ret_val)
        {
                perror ("Failed to read from source");
                exit (EXIT_FAILURE);
        }
        else
        {
                printf ("%s read from Source file\n", buffer);
        }

        /* write to stdin_buffer (content taken from file_in.txt) */
        ret_val = write (stdin_fd, buffer, LEN);
        if (-1 == ret_val)
        {
                perror ("Failed to write to stdin_buffer");
                exit (EXIT_FAILURE);
        }


        ret_val = lseek (stdin_fd, 0, SEEK_SET);
        if (-1 == ret_val)
        {
                perror ("Failed lseek");
                exit (EXIT_FAILURE);
        }

        ret_val = scanf ("%d", &n);
        if (-1 == ret_val)
        {
                perror ("Failed to read stdin_buffer");
                exit (EXIT_FAILURE);
        }
        printf ("Integer read is %d \n", n);

        close(source_fd);
        return 0;
}

2条回答
Viruses.
2楼-- · 2019-09-22 01:41

Before the updates

In the first program, 3 null bytes and a newline were (probably) written to the screen (not necessarily in that order); the program then tries to read from the keyboard (assuming that there's no I/O redirection on the command line). Writing to standard input does not load the input buffer. You very often can write to standard input (and read from standard output and standard error) because the classic technique opens a file descriptor with O_RDWR and then connects that to the standard I/O channels. However, there is no guarantee that you can do so. (The first program needs <unistd.h>, incidentally.)

The second program has so much undefined behaviour it is difficult to analyze. The open() call needs three arguments because it includes O_CREAT; the third argument is the mode for the file (e.g. 0644). You don't check that the open() succeeds. You don't check that the write succeeds; it won't, because the file descriptor is opened O_RDONLY (or, rather, the source_fd is opened O_RDONLY, and the dup() will copy that mode to file descriptor 0), which means the write() will fail. The input operation is not checked (you don't ensure that scanf() succeeds). (The second program doesn't really need <sys/types.h> or <sys/stat.h>.)

Basically, you don't know anything about what is going on because you've not checked any of the critical function calls.

After update 1

Note that error messages should be written to standard error and should be terminated with newlines.

I get the first program working as stated (Mac OS X 10.10 Yosemite, GCC 4.8.1), though it is hard to prove that the null bytes got written to standard input (but a newline was written there). I could then type 10 (or 20, or 100, or …) plus Return and that integer would then be printed.

The second program fails on the scanf() because the file pointer is at the end of the file when you try to read. You can see with this variant of your program:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

int main(void)
{    
    /* Open file from which content shall be inserted to stdin_buffer */
    int source_fd = open ("file_in.txt", O_CREAT | O_RDWR, S_IRWXU);
    if (-1 == source_fd)
    {
        printf ("Failed to open file for reading\n");
        exit (EXIT_FAILURE);
    }

    close(0);
    int stdin_fd = dup (source_fd);
    if (-1 == stdin_fd)
    {
        printf("Failed to dup\n");
        exit(EXIT_FAILURE);
    }

    int k = 10;
    int ret_val = write(stdin_fd, &k, sizeof(int));
    if (-1 == ret_val)
    {
        printf("Failed to write to stdin_buffer\n");
        exit(EXIT_FAILURE);
    }

    int rc;
    int n;
    if ((rc = scanf("%d", &n)) != 1)
        printf("Failed to read from standard input: rc = %d\n", rc);
    else
        printf("Integer read is %d (0x%08x)\n", n, n);

    close(source_fd);
    return 0;
}

It produces:

Failed to read from standard input: rc = -1

If you rewind the file before reading, you will get 0 returned; the binary data written to the file is not a valid string representation of an integer.

After update 2

I've written a small function err_exit() because it allows the code to be smaller on the page. I've modified your code in a couple of places to report on the return value from a previous function. The absence of input is not an error. When you get 0 bytes read, that isn't an error; it is EOF. When there is data to be read but it isn't the text format for an integer value, no conversions take place, but that isn't an error.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

#define LEN 100

static void err_exit(const char *msg)
{
    perror(msg);
    exit(EXIT_FAILURE);
}

int main(void)
{
        int n = 99;
        char buffer[LEN];
        memset(buffer, '\0', LEN);
        int ret_val = 0;

        /* Open file from which content shall be inserted to stdin_buffer */
        int source_fd = open("file_in.txt", O_CREAT | O_RDONLY, S_IRWXU);
        if (-1 == source_fd)
                err_exit("Failed to open file for reading");
        /* Temp stdin_buffer */
        int temp_fd = open("temp_in.txt", O_CREAT | O_RDWR, S_IRWXU);
        if (-1 == temp_fd)
                err_exit("Failed to open temp stdin");

        /* Close STDIN_FILENO */
        close(0);
        /* dup the source */
        int stdin_fd = dup(temp_fd);
        if (-1 == stdin_fd)
                err_exit("Failed to dup");

        ret_val = read(source_fd, buffer, LEN);
        if (-1 == ret_val)
                err_exit("Failed to read from source");
        else
                printf("(%d bytes) <<%s>> read from Source file\n", ret_val, buffer);

        /* write to stdin_buffer (content taken from file_in.txt) */
        ret_val = write(stdin_fd, buffer, LEN);
        if (-1 == ret_val)
                err_exit("Failed to write to stdin_buffer");

        ret_val = lseek(stdin_fd, 0, SEEK_SET);
        if (-1 == ret_val)
                err_exit("Failed lseek");

        ret_val = scanf("%d", &n);
        if (-1 == ret_val)
                err_exit("Failed to read stdin_buffer");
        printf("Integer read is %d (ret_val = %d)\n", n, ret_val);

        close(source_fd);
        return 0;
}

Output:

(0 bytes) <<>> read from Source file
Integer read is 99 (ret_val = 0)

When scanf() fails to read a value, it (usually) doesn't write anything into the corresponding variable. That's why the 99 survives. If you want data that can be read by scanf() as an integer, you need:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

#define LEN 100

static void err_exit(const char *msg)
{
    perror(msg);
    exit(EXIT_FAILURE);
}

int main(void)
{
        int n = 99;
        char buffer[LEN] = "";
        int ret_val = 0;

        /* Open file from which content shall be inserted to stdin */
        int source_fd = open("file_in.txt", O_CREAT | O_RDONLY, S_IRWXU);
        if (-1 == source_fd)
                err_exit("Failed to open file for reading");
        /* Temp stdin */
        int temp_fd = open("temp_in.txt", O_CREAT | O_RDWR, S_IRWXU);
        if (-1 == temp_fd)
                err_exit("Failed to open temp stdin");

        /* Close STDIN_FILENO */
        close(0);
        /* dup the source */
        int stdin_fd = dup(temp_fd);
        if (-1 == stdin_fd)
                err_exit("Failed to dup");

        ret_val = read(source_fd, buffer, LEN);
        if (-1 == ret_val)
                err_exit("Failed to read from source");
        else
                printf("(%d bytes) <<%s>> read from Source file\n", ret_val, buffer);

        /* write to stdin (content taken from file_in.txt) */
        ret_val = write(stdin_fd, "10\n", sizeof("10\n")-1);
        if (-1 == ret_val)
                err_exit("Failed to write to stdin");

        ret_val = lseek(stdin_fd, 0, SEEK_SET);
        if (-1 == ret_val)
                err_exit("Failed lseek");

        ret_val = scanf("%d", &n);
        if (-1 == ret_val)
                err_exit("Failed to read stdin");
        printf("Integer read is %d (ret_val = %d)\n", n, ret_val);

        close(source_fd);
        return 0;
}

Output:

(0 bytes) <<>> read from Source file
Integer read is 10 (ret_val = 1)
查看更多
爱情/是我丢掉的垃圾
3楼-- · 2019-09-22 01:48

Let's see what your three programs are doing...

Program 1

The first program writes to the filedescriptor 0. By default, this is stdout, hence the macro STDOUT_FILENO has that value. Since stdout is a unidirectional stream that runs into the process, writing fails.

Program 2

In this program, you open a file, which probably gets FD (filedescriptor) 3 (after 0-2 for the standard streams). Then, you close stdout with FD 0. Then, you dup() FD 3 and since the first open spot is at index 0, that's the new FD to the file. Since scanf() just uses the (unchanged) macro STDIN_FILENO, it will pick up the content of that file there.

Program 3

In program 3, you do pretty much the same as in program 2, only that you open the file twice. The explanation of what is going on pretty much follows the two above.

Question now is what you mean when you say "a better working code". The point is that "better" is impossible to say unless you specify what you actually want. Guessing from your topic and your comment, you want to remote-control a second process, just like using the pipe symbol in bash.

In order to do that, you will have to resort to OS-specific means, just search for "input output redirection" and you should be able to find some info. Since you already mentioned execl(), portability beyond POSIX (e.g. to win32) seems not be an issue though, and for that you should find lots of example code out there, much better that what I can write up here.

In any case, it boils down to roughly these steps:

  • pipe() to create a pair of FDs
  • fork() to create a new process
  • child: dup2() to reassign the input FD from pipe() to stdin
  • child: execl() the new process
  • parent: write to output FD to generate input for the child process

In addition, you should close unused streams or configure them with fcntl(FD_CLOEXEC) to have them closed automatically for you. The important point is that the two FDs remain connected to the same (!) channel, although you have both end in both processes. Closing one end in each leaves a unidirectional channel between the two processes.

查看更多
登录 后发表回答