blob: bf895e927b07137a2af355a9f1cfd52ec85ab2a6 [file] [log] [blame]
/* libnih
*
* file.c - file and directory utility functions
*
* Copyright © 2009 Scott James Remnant <scott@netsplit.com>.
* Copyright © 2009 Canonical Ltd.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
* ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif /* HAVE_CONFIG_H */
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <dirent.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <nih/macros.h>
#include <nih/alloc.h>
#include <nih/list.h>
#include <nih/string.h>
#include <nih/io.h>
#include <nih/file.h>
#include <nih/logging.h>
#include <nih/error.h>
#include <nih/errors.h>
/**
* NihDirEntry:
* @entry: list header,
* @dev: device number,
* @ino: inode number.
*
* This structure is used to detect directory loops, and is stored in a stack
* as we recurse down the directory tree.
**/
typedef struct nih_dir_entry {
NihList entry;
dev_t dev;
ino_t ino;
} NihDirEntry;
/* Prototypes for static functions */
static char **nih_dir_walk_scan (const char *path, NihFileFilter filter,
void *data)
__attribute__ ((warn_unused_result, malloc));
static int nih_dir_walk_visit (const char *dirname, NihList *dirs,
const char *path, NihFileFilter filter,
NihFileVisitor visitor,
NihFileErrorHandler error, void *data)
__attribute__ ((warn_unused_result));
/**
* nih_file_read:
* @parent: parent object for new string,
* @path: path to read,
* @length: pointer to store file length in.
*
* Opens the file at @path and reads the contents into memory, returning
* a newly allocated string. If the file is particularly large, it may
* not be possible to read into memory at all, and you'll need to use
* nih_file_map() instead.
*
* The returned data will NOT be NULL terminated.
*
* If @parent is not NULL, it should be a pointer to another object which
* will be used as a parent for the returned string. When all parents
* of the returned string are freed, the returned string will also be
* freed.
*
* Returns: newly allocated string or NULL if insufficient memory.
**/
char *
nih_file_read (const void *parent,
const char *path,
size_t *length)
{
struct stat statbuf;
FILE *fp;
char *file = NULL;
nih_assert (path != NULL);
nih_assert (length != NULL);
fp = fopen (path, "r");
if (! fp)
nih_return_system_error (NULL);
if (fstat (fileno (fp), &statbuf) < 0)
goto error;
if ((size_t)statbuf.st_size > SIZE_MAX) {
errno = EFBIG;
goto error;
}
*length = statbuf.st_size;
file = nih_alloc (parent, statbuf.st_size);
if (! file)
goto error;
if (fread (file, 1, statbuf.st_size, fp) != (size_t)statbuf.st_size) {
errno = EILSEQ;
goto error;
}
fclose (fp);
return file;
error:
nih_error_raise_system ();
if (file)
nih_free (file);
fclose (fp);
return NULL;
}
/**
* nih_file_map:
* @path: path to open,
* @flags: open mode,
* @length: pointer to store file length.
*
* Opens the file at @path and maps it into memory, returning the mapped
* pointer and the length of the file (required to unmap it later). The
* file is opened with the @flags given.
*
* Returns: memory mapped file or NULL on raised error.
**/
void *
nih_file_map (const char *path,
int flags,
size_t *length)
{
struct stat statbuf;
char *map;
int fd, prot;
nih_assert (path != NULL);
nih_assert (length != NULL);
nih_assert (((flags & O_ACCMODE) == O_RDONLY)
|| ((flags & O_ACCMODE) == O_RDWR));
fd = open (path, flags);
if (fd < 0)
nih_return_system_error (NULL);
prot = PROT_READ;
if ((flags & O_ACCMODE) == O_RDWR)
prot |= PROT_WRITE;
if (fstat (fd, &statbuf) < 0)
goto error;
if ((size_t)statbuf.st_size > SIZE_MAX) {
errno = EFBIG;
goto error;
}
*length = statbuf.st_size;
map = mmap (NULL, *length, prot, MAP_SHARED, fd, 0);
if (map == MAP_FAILED)
goto error;
close (fd);
return map;
error:
nih_error_raise_system ();
close (fd);
return NULL;
}
/**
* nih_file_unmap:
* @map: memory mapped file,
* @length: length of file.
*
* Unmap a file previously mapped with nih_file_map().
*
* Returns: zero on success, NULL on raised error.
**/
int
nih_file_unmap (void *map,
size_t length)
{
nih_assert (map != NULL);
if (munmap (map, length) < 0)
nih_return_system_error (-1);
return 0;
}
/**
* nih_file_is_hidden:
* @path: path to check.
*
* Determines whether @path represents a hidden file, matching it against
* common patterns for that type of file.
*
* Returns: TRUE if it matches, FALSE otherwise.
**/
int
nih_file_is_hidden (const char *path)
{
const char *ptr;
size_t len;
nih_assert (path != NULL);
ptr = strrchr (path, '/');
if (ptr)
path = ptr + 1;
len = strlen (path);
/* Matches .*; standard hidden pattern */
if ((len >= 1) && (path[0] == '.'))
return TRUE;
return FALSE;
}
/**
* nih_file_is_backup:
* @path: path to check.
*
* Determines whether @path represents a backup file, matching it against
* common patterns for that type of file.
*
* Returns: TRUE if it matches, FALSE otherwise.
**/
int
nih_file_is_backup (const char *path)
{
const char *ptr;
size_t len;
nih_assert (path != NULL);
ptr = strrchr (path, '/');
if (ptr)
path = ptr + 1;
len = strlen (path);
ptr = path + len;
/* Matches *~; standard backup style */
if ((len >= 1) && (ptr[-1] == '~'))
return TRUE;
/* Matches *.bak; common backup extension */
if ((len >= 4) && (! strcmp (&ptr[-4], ".bak")))
return TRUE;
/* Matches *.BAK; as above, but on case-insensitive filesystems */
if ((len >= 4) && (! strcmp (&ptr[-4], ".BAK")))
return TRUE;
/* Matches #*#; used by emacs for unsaved files */
if ((len >= 2) && (path[0] == '#') && (ptr[-1] == '#'))
return TRUE;
return FALSE;
}
/**
* nih_file_is_swap:
* @path: path to check.
*
* Determines whether @path represents an editor swap file, matching it
* against common patterns for that type of file.
*
* Returns: TRUE if it matches, FALSE otherwise.
**/
int
nih_file_is_swap (const char *path)
{
const char *ptr;
size_t len;
nih_assert (path != NULL);
ptr = strrchr (path, '/');
if (ptr)
path = ptr + 1;
len = strlen (path);
ptr = path + len;
/* Matches *.swp; used by vi */
if ((len >= 4) && (! strcmp (&ptr[-4], ".swp")))
return TRUE;
/* Matches *.swo; used by vi */
if ((len >= 4) && (! strcmp (&ptr[-4], ".swo")))
return TRUE;
/* Matches *.swn; used by vi */
if ((len >= 4) && (! strcmp (&ptr[-4], ".swn")))
return TRUE;
/* Matches .#*; used by emacs */
if ((len >= 2) && (! strncmp (path, ".#", 2)))
return TRUE;
return FALSE;
}
/**
* nih_file_is_rcs:
* @path: path to check.
*
* Determines whether @path represents a file or directory used by a
* common revision control system, matching it against common patterns
* for known RCSs.
*
* Returns: TRUE if it matches, FALSE otherwise.
**/
int
nih_file_is_rcs (const char *path)
{
const char *ptr;
size_t len;
nih_assert (path != NULL);
ptr = strrchr (path, '/');
if (ptr)
path = ptr + 1;
len = strlen (path);
ptr = path + len;
/* Matches *,v; used by rcs and cvs */
if ((len >= 2) && (! strcmp (&ptr[-2], ",v")))
return TRUE;
/* RCS; used by rcs */
if (! strcmp (path, "RCS"))
return TRUE;
/* CVS; used by cvs */
if (! strcmp (path, "CVS"))
return TRUE;
/* CVS.adm; used by cvs */
if (! strcmp (path, "CVS.adm"))
return TRUE;
/* SCCS; used by sccs */
if (! strcmp (path, "SCCS"))
return TRUE;
/* .bzr; used by bzr */
if (! strcmp (path, ".bzr"))
return TRUE;
/* .bzr.log; used by bzr */
if (! strcmp (path, ".bzr.log"))
return TRUE;
/* .hg; used by hg */
if (! strcmp (path, ".hg"))
return TRUE;
/* .git; used by git */
if (! strcmp (path, ".git"))
return TRUE;
/* .svn; used by subversion */
if (! strcmp (path, ".svn"))
return TRUE;
/* BitKeeper; used by BitKeeper */
if (! strcmp (path, "BitKeeper"))
return TRUE;
/* .arch-ids; used by tla */
if (! strcmp (path, ".arch-ids"))
return TRUE;
/* .arch-inventory; used by tla */
if (! strcmp (path, ".arch-inventory"))
return TRUE;
/* {arch}; used by tla */
if (! strcmp (path, "{arch}"))
return TRUE;
/* _darcs; used by darcs */
if (! strcmp (path, "_darcs"))
return TRUE;
return FALSE;
}
/**
* nih_file_is_packaging:
* @path: path to check.
*
* Determines whether @path represents a file or directory used by a
* common package manager, matching it against common patterns.
*
* Returns: TRUE if it matches, FALSE otherwise.
**/
int
nih_file_is_packaging (const char *path)
{
const char *ptr;
nih_assert (path != NULL);
ptr = strrchr (path, '/');
if (ptr)
path = ptr + 1;
/* Matches *.dpkg-*; used by dpkg */
ptr = strrchr (path, '.');
if (ptr && (! strncmp (ptr, ".dpkg-", 6)))
return TRUE;
/* Matches *.rpm{save,orig,new}; used by rpm */
if (ptr && (! strncmp (ptr, ".rpmsave", 9)))
return TRUE;
if (ptr && (! strncmp (ptr, ".rpmorig", 9)))
return TRUE;
if (ptr && (! strncmp (ptr, ".rpmnew", 8)))
return TRUE;
/* Matches *;[a-fA-F0-9]{8}; used by rpm */
ptr = strrchr (path, ';');
if (ptr && (strspn (ptr + 1, "abcdefABCDEF0123456789") == 8)
&& (! ptr[9]))
return TRUE;
return FALSE;
}
/**
* nih_file_ignore:
* @data: data pointer,
* @path: path to check.
*
* Determines whether @path should normally be ignored when walking a
* directory tree. Files ignored are those that are hidden, represent
* backup files, editor swap files and both files and directories used
* by revision control systems.
*
* Returns: TRUE if it should be ignored, FALSE otherwise.
**/
int
nih_file_ignore (void *data,
const char *path)
{
if (nih_file_is_hidden (path))
return TRUE;
if (nih_file_is_backup (path))
return TRUE;
if (nih_file_is_swap (path))
return TRUE;
if (nih_file_is_rcs (path))
return TRUE;
if (nih_file_is_packaging (path))
return TRUE;
return FALSE;
}
/**
* nih_dir_walk:
* @path: path to walk,
* @filter: path filter,
* @visitor: function to call for each path,
* @error: function to call on error,
* @data: data to pass to @visitor.
*
* Iterates the directory tree starting at @path, calling @visitor for
* each file, directory or other object found. Sub-directories are
* descended into, and the same @visitor called for those.
*
* @visitor is not called for @path itself.
*
* @filter can be used to restrict both the sub-directories iterated and
* the objects that @visitor is called for. It is passed the full path
* of the object, and if it returns TRUE, the object is ignored.
*
* If @visitor returns a negative value, or there's an error obtaining
* the listing for a particular sub-directory, then the @error function
* will be called. This function should handle the error and return zero,
* or raise an error again and return a negative value which causes the
* entire walk to be aborted. If @error is NULL, then a warning is emitted
* instead.
*
* Returns: zero on success, negative value on raised error.
**/
int
nih_dir_walk (const char *path,
NihFileFilter filter,
NihFileVisitor visitor,
NihFileErrorHandler error,
void *data)
{
nih_local NihList *dirs = NULL;
struct stat statbuf;
nih_local char **paths = NULL;
char **subpath;
int ret = 0;
nih_assert (path != NULL);
nih_assert (visitor != NULL);
paths = nih_dir_walk_scan (path, filter, data);
if (! paths)
return -1;
dirs = NIH_MUST (nih_list_new (NULL));
if (stat (path, &statbuf) == 0) {
NihDirEntry *entry;
entry = NIH_MUST (nih_new (dirs, NihDirEntry));
nih_list_init (&entry->entry);
nih_alloc_set_destructor (entry, nih_list_destroy);
entry->dev = statbuf.st_dev;
entry->ino = statbuf.st_ino;
nih_list_add (dirs, &entry->entry);
}
for (subpath = paths; *subpath; subpath++) {
ret = nih_dir_walk_visit (path, dirs, *subpath, filter,
visitor, error, data);
if (ret < 0)
break;
}
return ret;
}
/**
* nih_dir_walk_scan:
* @path: path to scan,
* @filter: path filter,
* @data: data to pass to @filter.
*
* Reads the list of files in @path, removing ".", ".." and any for which
* @filter return TRUE.
*
* Returns: NULL-terminated array of full paths to sub-paths or NULL on
* raised error.
**/
static char **
nih_dir_walk_scan (const char *path,
NihFileFilter filter,
void *data)
{
DIR *dir;
struct dirent *ent;
char **paths;
size_t npaths;
nih_assert (path != NULL);
dir = opendir (path);
if (! dir)
nih_return_system_error (NULL);
npaths = 0;
paths = NIH_MUST (nih_str_array_new (NULL));
while ((ent = readdir (dir)) != NULL) {
nih_local char *subpath = NULL;
/* Always ignore '.' and '..' */
if ((! strcmp (ent->d_name, "."))
|| (! strcmp (ent->d_name, "..")))
continue;
subpath = NIH_MUST (nih_sprintf (NULL, "%s/%s",
path, ent->d_name));
if (filter && filter (data, subpath))
continue;
NIH_MUST (nih_str_array_addp (&paths, NULL, &npaths, subpath));
}
closedir (dir);
qsort (paths, npaths, sizeof (char *), alphasort);
return paths;
}
/**
* nih_dir_walk_visit:
* @dirname: top-level being walked,
* @dirs: stack of visited directories,
* @path: path being visited,
* @filter: path filter,
* @visitor: function to call for each path,
* @error: function to call on error,
* @data: data to pass to @visitor.
*
* Visits an individual @path found while iterating the directory tree
* started at @dirname. Ensures that @visitor is called for @path, and
* if @path is a directory, it is descended into and the same @visitor
* called for each of those.
*
* @filter can be used to restrict both the sub-directories iterated and
* the objects that @visitor is called for. It is passed the full path
* of the object, and if it returns TRUE, the object is ignored.
*
* If @visitor returns a negative value, or there's an error obtaining
* the listing for a particular sub-directory, then the @error function
* will be called. This function should handle the error and return zero,
* or raise an error again and return a negative value which causes the
* entire walk to be aborted. If @error is NULL, then a warning is emitted
* instead.
*
* Returns: zero on success, negative value on raised error.
**/
static int
nih_dir_walk_visit (const char *dirname,
NihList *dirs,
const char *path,
NihFileFilter filter,
NihFileVisitor visitor,
NihFileErrorHandler error,
void *data)
{
struct stat statbuf;
nih_assert (dirname != NULL);
nih_assert (dirs != NULL);
nih_assert (path != NULL);
nih_assert (visitor != NULL);
/* Not much we can do here if we can't at least stat it */
if (stat (path, &statbuf) < 0) {
nih_error_raise_system ();
goto error;
}
/* Call the handler */
if (visitor (data, dirname, path, &statbuf) < 0)
goto error;
/* Iterate into sub-directories; first checking for directory loops.
*/
if (S_ISDIR (statbuf.st_mode)) {
nih_local NihDirEntry *entry = NULL;
nih_local char **paths = NULL;
char **subpath;
int ret = 0;
NIH_LIST_FOREACH (dirs, iter) {
NihDirEntry *entry = (NihDirEntry *)iter;
if ((entry->dev == statbuf.st_dev)
&& (entry->ino == statbuf.st_ino)) {
nih_error_raise (NIH_DIR_LOOP_DETECTED,
_(NIH_DIR_LOOP_DETECTED_STR));
goto error;
}
}
/* Grab the directory contents */
paths = nih_dir_walk_scan (path, filter, data);
if (! paths)
goto error;
/* Record the device and inode numbers in the stack so that
* we can detect directory loops.
*/
entry = NIH_MUST (nih_new (NULL, NihDirEntry));
nih_list_init (&entry->entry);
nih_alloc_set_destructor (entry, nih_list_destroy);
entry->dev = statbuf.st_dev;
entry->ino = statbuf.st_ino;
nih_list_add (dirs, &entry->entry);
/* Iterate the paths found. If these calls return a negative
* value, it means that an error handler decided to abort the
* walk; so just abort right now.
*/
for (subpath = paths; *subpath; subpath++) {
ret = nih_dir_walk_visit (dirname, dirs, *subpath,
filter, visitor, error,
data);
if (ret < 0)
break;
}
if (ret < 0)
return ret;
}
return 0;
error:
if (error) {
return error (data, dirname, path, &statbuf);
} else {
NihError *err;
err = nih_error_get ();
nih_warn ("%s: %s", path, err->message);
nih_free (err);
return 0;
}
}