detecting loops in symbolic links (c programming) - c

I'm looking to detect loops in symbolic links in a C program:
$ ln -s self self
$ ln -s a b
$ ln -s b a
Here's what I've got so far:
#include <sys/stat.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
int
main(int argc, char *argv[])
{
struct stat buffer;
int status;
if (argc != 2) {
fprintf(stderr, "error: file name required\n");
return 0;
}
errno = 0;
status = lstat(argv[1], &buffer);
if (errno == ELOOP) {
fprintf(stderr, "loop found");
}
return 1;
}
I'm running my program like this:
$ findloops self
$ findloops a
Any idea what I'm doing wrong?
This is NOT homework.
This is where I got the idea from.

The trouble is that 'lstat()' looks at the symlink and its properties, and the symlinks actually exist.
If you replace the call with 'stat()', then you will get the ELOOP error. This tries to get the information at the far end of the symlink, and that cannot be found because of the ELOOP condition.
You should only test errno after you have verified that status indicates a failure. With a genuine system call, it is unlikely that errno would be set when the call succeeds, but with library functions, you can find errno is set even though the call succeeds. For example, with some standard I/O library implementations, you can have errno == ENOTTY even after a successful function call; the code checks whether the file descriptor represents a terminal and errno is set to indicate that it isn't, but since the function succeeded, it is not legitimate to check errno.

I would take a look at the buffer returned. According to the documentation of lstat the buffer contains two items that would be relevant:
st_ino - The inode for the file (note that this number is unique to each distinct file and all directories on a Linux file system, but the same inode number can appear in different file systems).
st_dev - The device that the file currently resides on.
If you create a list containing these two items per element+the directory where the link is located as the previously visited elements, you could detect loops. Also don't forget to pop them off when you leave the directory that they were created in.
I'm not convinced that ELOOP is the value that you think it is. According to this, it identifies the maximum links tolerated in the class path, but it won't tell you which link looped first.
The documentation on the page claimed this: "ELOOP: Too many symbolic links were encountered in translating the pathname. "

ELOOP doesn't have to mean that there is a loop. It can also mean that there are too many symbolic links from source to target, as in
a -> b -> c -> d -> e ... -> z
do this enough times and the OS kernel (particularily for some cases on linux) will give up trying to follow the links, even if they are all valid and non-cyclic.
You may also be interested in man 2 readlink.

After some playing with code, it looks like you've found either a feature or a bug with lstat(2). According to the man page on lstat, which is also stat and fstat, the difference between stat and lstat is:
stat() stats the file pointed to by
path and fills in buf.
lstat() is identical to stat(), except
that if path is a symbolic link, then
the link itself is stat-ed, not the
file that it refers to
I took your program and played with it a little. I used lstat, stat, and fopen to check the link. Code is below. The bottom line is that both stat and fopen detected the link properly, while lstat failed. I have no explanation for this.
The program below, executed on file bar created as 'ln -s bar bar', gave the following output:
./foo ./bar
Errno as returned from lstat = 0
Errno as returned from stat = 92
loop found
Errno as returned from fopen = 92
loop found
Code:
#include <sys/stat.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
int
main(int argc, char *argv[])
{
struct stat buffer;
int status;
int savedErrno1;
int savedErrno2;
int savedErrno3;
FILE *theFile;
if (argc != 2) {
printf("error: file name required\n");
return 0;
}
errno = 0;
status = lstat(argv[1], &buffer);
savedErrno1 = errno;
printf("Errno as returned from lstat = %d\n", savedErrno1);
if (savedErrno1 == ELOOP) {
printf("loop found\n");
}
errno = 0;
status = stat(argv[1], &buffer);
savedErrno2 = errno;
printf("Errno as returned from stat = %d\n", savedErrno2);
if (savedErrno2 == ELOOP) {
printf("loop found\n");
}
errno = 0;
theFile = fopen(argv[1], "w");
savedErrno3 = errno;
printf("Errno as returned from fopen = %d\n", savedErrno3);
if (savedErrno3 == ELOOP) {
printf("loop found\n");
}
return 1;
}

Related

How to tell if FILE* is referring to a directory?

I just discovered that a FILE* can not only refer to a regular file, but also to a directory. If the latter is the case, fread will fail with errno set to 21 (Is a directory).
Minimal repro can be tested here
#include <stdio.h>
#include <fcntl.h>
#include <assert.h>
#include <errno.h>
int main() {
char const* sz = ".";
int fd = open(sz, O_RDONLY | O_NOFOLLOW); // all cleanup omitted for brevity
FILE* file = fdopen(fd, "rb");
// I would like to test in this line if it is a directory
char buffer[21];
int const n = fread(buffer, 1, 20, file);
if (0 < n) {
buffer[n] = 0;
printf(buffer);
} else {
printf("Error %d", errno); // 21 = Is a directory
}
}
What is the proper way to detect early that my FILE* is referring to directory without trying to read from it?
EDIT to repel the duplicate flags:
I want to test on the FILE*, not the filename. Testing on filename only and then opening it later is a race condition.
Assuming a POSIX-like environment, if you have just the file stream (FILE *fp), then you are probably reduced to using fileno() and fstat():
#include <sys/stat.h>
struct stat sb;
if (fstat(fileno(fp), &sb) != 0)
…oops…
if (S_ISDIR(sb.st_mode))
…it is a directory…
else
…it is not a directory…
Assuming you are on a POSIX-based system, use stat() (if you wish to use the filename in sz before the call to open()) or fstat() (if you wish to use the descriptor fd after calling open()) to get a file status structure from the OS. The member of the structure named st_mode can be used with the POSIX API S_ISDIR(st_mode) to see if the file is a directory.
For more information, see: http://man7.org/linux/man-pages/man2/stat.2.html
Checking The fcntl.h man page:
header shall define the following symbolic constants as
file creation flags for use in the oflag value to open() and
openat(). The values shall be bitwise-distinct and shall be suitable
for use in #if preprocessing directives.
And the flag :
O_DIRECTORY Fail if not a directory.

What is the method to check if the target of a symlink exists or not?

Using a c program I need to find and delete all symbolic links in a directory, with missing target.
What is the most efficient way to check whether the target of a symbolic link exist or not. Any method other than opening the symlink and check the return value. I'm working with linux and gcc.
stat or access, or indeed open. That's about all you can do.
From the man 3 stat man page
If the named file is a symbolic link, the stat() function shall continue pathname resolution using the
contents of the symbolic link, and shall return information pertaining to the resulting file if the
file exists.
So the following works nice:
#include <sys/stat.h>
#include <stdio.h>
int main() {
struct stat ctx;
int status = stat("test.txt", &ctx);
if(status != 0) {
perror("[stat]");
return 1;
}
else {
puts("works nice");
}
return 0;
}
The access () function with F_OK mode set will follow a symlink path.
The following code will print "Yes!" if both the symlink and the target file exist...
#include <stdio.h>
#include <unistd.h>
int
main (void)
{
if (access ("test.txt", F_OK) != -1) {
puts ("Yes!");
return 0;
}
puts ("No.");
return 1;
}

Linux/ Open directory as a file

I've been reading Brian Kernighan and Dennis Ritchie - The C Programming Language and chapter 8.6 is about directory listing under UNIX OS. They say that everything and even directory is a file. This means that I should be able to open directory as a file? I've tried it using stdio functions and it didn't work. Now, I'm trying it with UNIX system functions. Of course, I'm not using UNIX, I'm using Ubuntu linux. Here is my code:
#include <syscall.h>
#include <fcntl.h>
int main(int argn, char* argv[]) {
int fd;
if (argn!=1) fd=open(argv[1],O_RDONLY,0);
else fd=open(".",O_RDONLY,0);
if (fd==-1) return -1;
char buf[1024];
int n;
while ((n=read(fd,buf,1024))>0)
write(1,buf,n);
close (fd);
return 0;
}
This writes nothing even when argn is 1 (no parameters) and I'm trying to read current directory.
Any ideas/explanations? :)
Files are also called regular files to distinguish them from special files.
Directory or not a regular file. The most common special file is the directory. The layout of a directory file is defined by the filesystem used.
So use opendir to open diretory.
Nachiket's answer is correct (as indeed is sujin) but they don't clear up the mystery as to why open works and not read. Out of curiosity I made some changes to the given code to find out exactly what was going on.
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
int main(int argc, char* argv[]) {
int fd = -1;
if (argc!=1) fd=open(argv[1],O_RDONLY,0);
else fd=open(".",O_RDONLY,0);
if (fd < 0){
perror("file open");
printf("error on open = %d", errno);
return -1;
}
printf("file descriptor is %d\n", fd);
char buf[1024];
int n;
if ((n=read(fd,buf,1024))>0){
write(1,buf,n);
}
else {
printf("n = %d\n", n);
if (n < 0) {
printf("read failure %d\n", errno);
perror("cannot read");
}
}
close (fd);
return 0;
}
The result of compiling and running this:
file descriptor is 3
n = -1
read failure 21
cannot read: Is a directory
That settles it, though I'd have expected open to fail, since the correct system function for opening directories is opendir().
Though everything in unix is a file (directory also) but still filetype is concept is present in unix and applicable to all files.
there are file types like regular file,directory etc and certain operations and functions are allowed/present for every file type.
In your case readdir is applicable for reading contents of directory.
If you want to see the files in a directory you have to use the opendir and readdir functions.
K&R were correct for the original UNIX. I remember doing it back when UNIX file systems had a 14 character length limit for filenames. The opendir(), readdir(), ... stuff happened about the time that longer file names became common (around 1990?)

How to know which file failed during rename?

I have a simple example:
#include <stdio.h>
#include <errno.h>
int main() {
int result = rename("filea", "doesntexist/fileb");
if (result != 0) {
printf("NOOOO %d\n", errno);
}
return 0;
}
and I want to distinguish between 2 of the possible failures:
filea doesn't exist
directory for fileb doesn't exist
but it always returns errno = 2 when either doesn't exist... uhm
Any ideas how can I approach this?
Thanks
EDIT: If possible without manually checking if the files exist.
EDIT2: Not checking if the file exists is a stupid constraint ;) so, I've accepted one of the answers already. Thanks!
I don't know how you're going to check if a file exists without checking if a file exists, but hopefully this function will help you out:
#include <sys/stat.h>
if (!fileExists("foo")) { /* foo does not exist */ }
int fileExists (const char *fn)
{
struct stat buf;
int i = stat(fn, &buf);
if (i == 0)
return 1; /* file found */
return 0;
}
If your goal is to keep the code clean, then just use functions:
int main()
{
if (! renameFiles("fileA", "fileB")) {
fprintf(stderr, "rename failed...\n");
exit EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
int renameFiles(const char *source, const char *destination)
{
int result = -1;
if ( (fileExists(source)) && (!fileExists(destination)) )
result = rename(source, destination);
if (result == 0)
return 1; /* rename succeeded */
/*
Either `source` does not exist, or `destination`
already exists, or there is some other error (take
a look at `errno` and handle appropriately)
*/
return 0;
}
You could return custom error codes from renameFiles() and conditionally handle errors based on which file does or does not exist, or if there is some other problem with the rename() call.
Call access() (unistd.h) first. Or stat(). And you are probably getting an ENOENT error when filea does not exist. Some ways you can get an error on fileB:
path cannot be found
no permissions on the path
fileB exists and you do not have permissions
you have a too long or malformed name
There are others but they are not very common.
There is no case where you should get an error when fileB is not there. You execute a mv filea fileb (what rename does) and all of the errors for mv apply here. Missing destination file is not one of them.
You should also have
#include <errno.h>
since you reference errno.
The ISO C standard does not even require the library function rename to set errno in case of error. All that is guaranteed is a non-zero return value on error (7.19.4.2, §3).
So whether this is possible or not depends on your platform (and it is not portable).
E.g. in Linux there is no way to distinguish which of them is missing by just looking at errno after rename (according to this man page).
If the errno is always 2 ENOENT "No such file or directory" on your system you are going to HAVE to check for the existence of something. On my system I get errno of 2 if old does not existent or if the directory path of new does not exist.
However there is much more then 2 possible errors. The link http://man.chinaunix.net/unix/susv3/functions/rename.html has 20 distinct errno values specified.
I would suggest that if the rename fails and the errno is 2 then check for the existence of old. If found then the problem is that the directory specified in the new doesn't exist.

Regarding checking for file or directory

I have a very simple program here, but it seems to be returning
a "true" value to the query S_ISDIR() even when the directory
entry is not a directory. Can any one pleeas help me. I am using QNX Neurtion RTOS
#include <stdio.h>
#include <dirent.h>
#include <sys/stat.h>
#include <errno.h>
int main(int argc, char *argv[]) {
DIR *dir;
struct dirent *entry;
struct stat eStat;
char *root;
int i;
root = argv[1];
while((entry = readdir(dir)) != NULL) {
lstat(entry->d_name, &eStat);
if(S_ISDIR(eStat.st_mode))
printf("found directory %s\n", entry->d_name);
else
printf("not a dir\n");
}
return 0;
}
sample output:
found directory .
found directory ..
found directory NCURSES-Programming-HOWTO-html.tar.gz
found directory ncurses_programs
found directory ncurses.html
Following information may be helpful.
lstat for file is failing with errno set to 2. I am not sure why, can any one know this.
Just a guess; since you're not checking for an error after your lstat call, the eStat buffer could be containing the result of the last successful call. Try checking if lstat returns -1.
readdir() on Linux is fundamentally different, so I can't fully test on my system. See the sample programs at link text and link text. Modifying the lstat sample code, this seems to work for me:
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
int main( int argc, char **argv )
{
int ecode = 0;
int n;
struct stat sbuf;
for( n = 1; n < argc; ++n ) {
if( lstat( argv[n], &sbuf ) == -1 ) {
perror( argv[n] );
ecode++;
} else if( S_ISDIR( sbuf.st_mode ) ) {
printf( "%s is a dir\n", argv[n] );
} else {
printf( "%s is not a dir\n", argv[n] );
}
}
}
I don't know if that helps any. Note that the readdir() sample code uses opendir() as schot suggested. But I can't explain why your readdir() seems to work regardless.
My compiler says: "warning: 'dir' is used uninitialized in this function" You may want to add dir = opendir(root); after you initialize root. And don't forget to add some error checking.
I doubt this causes your problem, jcomeau_ictx is probably right. If lstat returns -1 it sets errno to a value that signifies the type of error. Look at its man page and the man page for strerror
Even though this question was asked long time ago, and I found it because this quesion. but the answers here didn't really solve the problem, so I decided to post the answer which I wrote on another post, such that if anyone had the same problem and used google to find this page, there is a clear answer.
The real reason of S_ISDIR not working as expected is dp->d_name contains only the name of the file, you need to pass the full path of the file to lstat().

Resources