/*
* Copyright (c) 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016
* Jonathan Schleifer <js@heap.zone>
*
* All rights reserved.
*
* This file is part of ObjFW. It may be distributed under the terms of the
* Q Public License 1.0, which can be found in the file LICENSE.QPL included in
* the packaging of this file.
*
* Alternatively, it may be distributed under the terms of the GNU General
* Public License, either version 2 or 3, which can be found in the file
* LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this
* file.
*/
#include "config.h"
#include <errno.h>
#include <dirent.h>
#include <unistd.h>
#ifdef HAVE_PWD_H
# include <pwd.h>
#endif
#ifdef HAVE_GRP_H
# include <grp.h>
#endif
#import "OFFileManager.h"
#import "OFFile.h"
#import "OFString.h"
#import "OFArray.h"
#import "OFDate.h"
#import "OFSystemInfo.h"
#ifdef OF_HAVE_THREADS
# import "threading.h"
#endif
#import "OFChangeCurrentDirectoryPathFailedException.h"
#import "OFChangeOwnerFailedException.h"
#import "OFChangePermissionsFailedException.h"
#import "OFCopyItemFailedException.h"
#import "OFCreateDirectoryFailedException.h"
#import "OFCreateSymbolicLinkFailedException.h"
#import "OFInitializationFailedException.h"
#import "OFInvalidArgumentException.h"
#import "OFLinkFailedException.h"
#import "OFLockFailedException.h"
#import "OFMoveItemFailedException.h"
#import "OFOpenItemFailedException.h"
#import "OFOutOfMemoryException.h"
#import "OFReadFailedException.h"
#import "OFRemoveItemFailedException.h"
#import "OFStatItemFailedException.h"
#import "OFUnlockFailedException.h"
#ifdef OF_WINDOWS
# include <windows.h>
# include <direct.h>
#endif
#ifndef S_IRGRP
# define S_IRGRP 0
#endif
#ifndef S_IROTH
# define S_IROTH 0
#endif
#ifndef S_IWGRP
# define S_IWGRP 0
#endif
#ifndef S_IWOTH
# define S_IWOTH 0
#endif
#define DEFAULT_MODE S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH
#define DIR_MODE DEFAULT_MODE | S_IXUSR | S_IXGRP | S_IXOTH
static OFFileManager *defaultManager;
#if defined(OF_HAVE_CHOWN) && defined(OF_HAVE_THREADS)
static of_mutex_t chownMutex;
#endif
#if !defined(HAVE_READDIR_R) && !defined(OF_WINDOWS) && defined(OF_HAVE_THREADS)
static of_mutex_t readdirMutex;
#endif
int
of_stat(OFString *path, of_stat_t *buffer)
{
#if defined(OF_WINDOWS)
return _wstat64([path UTF16String], buffer);
#elif defined(OF_HAVE_OFF64_T)
return stat64([path cStringWithEncoding:
[OFSystemInfo native8BitEncoding]], buffer);
#else
return stat([path cStringWithEncoding:
[OFSystemInfo native8BitEncoding]], buffer);
#endif
}
int
of_lstat(OFString *path, of_stat_t *buffer)
{
#if defined(OF_WINDOWS)
return _wstat64([path UTF16String], buffer);
#elif defined(HAVE_LSTAT)
# ifdef OF_HAVE_OFF64_T
return lstat64([path cStringWithEncoding:
[OFSystemInfo native8BitEncoding]], buffer);
# else
return lstat([path cStringWithEncoding:
[OFSystemInfo native8BitEncoding]], buffer);
# endif
#else
# ifdef OF_HAVE_OFF64_T
return stat64([path cStringWithEncoding:
[OFSystemInfo native8BitEncoding]], buffer);
# else
return stat([path cStringWithEncoding:
[OFSystemInfo native8BitEncoding]], buffer);
# endif
#endif
}
@implementation OFFileManager
+ (void)initialize
{
if (self != [OFFileManager class])
return;
/*
* Make sure OFFile is initialized.
* On some systems, this is needed to initialize the file system driver.
*/
[OFFile class];
#if defined(OF_HAVE_CHOWN) && defined(OF_HAVE_THREADS)
if (!of_mutex_new(&chownMutex))
@throw [OFInitializationFailedException
exceptionWithClass: self];
#endif
#if !defined(HAVE_READDIR_R) && !defined(OF_WINDOWS) && defined(OF_HAVE_THREADS)
if (!of_mutex_new(&readdirMutex))
@throw [OFInitializationFailedException
exceptionWithClass: self];
#endif
defaultManager = [[OFFileManager alloc] init];
}
+ (OFFileManager*)defaultManager
{
return defaultManager;
}
- (OFString*)currentDirectoryPath
{
OFString *ret;
#ifndef OF_WINDOWS
char *buffer = getcwd(NULL, 0);
#else
wchar_t *buffer = _wgetcwd(NULL, 0);
#endif
@try {
#ifndef OF_WINDOWS
ret = [OFString
stringWithCString: buffer
encoding: [OFSystemInfo native8BitEncoding]];
#else
ret = [OFString stringWithUTF16String: buffer];
#endif
} @finally {
free(buffer);
}
return ret;
}
- (bool)fileExistsAtPath: (OFString*)path
{
of_stat_t s;
if (path == nil)
@throw [OFInvalidArgumentException exception];
if (of_stat(path, &s) == -1)
return false;
if (S_ISREG(s.st_mode))
return true;
return false;
}
- (bool)directoryExistsAtPath: (OFString*)path
{
of_stat_t s;
if (path == nil)
@throw [OFInvalidArgumentException exception];
if (of_stat(path, &s) == -1)
return false;
if (S_ISDIR(s.st_mode))
return true;
return false;
}
#ifdef OF_HAVE_SYMLINK
- (bool)symbolicLinkExistsAtPath: (OFString*)path
{
of_stat_t s;
if (path == nil)
@throw [OFInvalidArgumentException exception];
if (of_lstat(path, &s) == -1)
return false;
if (S_ISLNK(s.st_mode))
return true;
return false;
}
#endif
- (void)createDirectoryAtPath: (OFString*)path
{
if (path == nil)
@throw [OFInvalidArgumentException exception];
#ifndef OF_WINDOWS
if (mkdir([path cStringWithEncoding: [OFSystemInfo native8BitEncoding]],
DIR_MODE) != 0)
#else
if (_wmkdir([path UTF16String]) != 0)
#endif
@throw [OFCreateDirectoryFailedException
exceptionWithPath: path
errNo: errno];
}
- (void)createDirectoryAtPath: (OFString*)path
createParents: (bool)createParents
{
OFString *currentPath = nil;
if (!createParents) {
[self createDirectoryAtPath: path];
return;
}
if (path == nil)
@throw [OFInvalidArgumentException exception];
for (OFString *component in [path pathComponents]) {
void *pool = objc_autoreleasePoolPush();
if (currentPath != nil)
currentPath = [currentPath
stringByAppendingPathComponent: component];
else
currentPath = component;
if ([currentPath length] > 0 &&
![self directoryExistsAtPath: currentPath])
[self createDirectoryAtPath: currentPath];
[currentPath retain];
objc_autoreleasePoolPop(pool);
[currentPath autorelease];
}
}
- (OFArray*)contentsOfDirectoryAtPath: (OFString*)path
{
OFMutableArray *files;
#ifndef OF_WINDOWS
of_string_encoding_t encoding;
#endif
if (path == nil)
@throw [OFInvalidArgumentException exception];
files = [OFMutableArray array];
#ifndef OF_WINDOWS
DIR *dir;
encoding = [OFSystemInfo native8BitEncoding];
if ((dir = opendir([path cStringWithEncoding: encoding])) == NULL)
@throw [OFOpenItemFailedException exceptionWithPath: path
errNo: errno];
# if !defined(HAVE_READDIR_R) && defined(OF_HAVE_THREADS)
if (!of_mutex_lock(&readdirMutex))
@throw [OFLockFailedException exception];
# endif
@try {
for (;;) {
struct dirent *dirent;
# ifdef HAVE_READDIR_R
struct dirent buffer;
# endif
void *pool;
OFString *file;
# ifdef HAVE_READDIR_R
if (readdir_r(dir, &buffer, &dirent) != 0)
@throw [OFReadFailedException
exceptionWithObject: self
requestedLength: 0
errNo: errno];
if (dirent == NULL)
break;
# else
if ((dirent = readdir(dir)) == NULL) {
if (errno == 0)
break;
else
@throw [OFReadFailedException
exceptionWithObject: self
requestedLength: 0
errNo: errno];
}
# endif
if (strcmp(dirent->d_name, ".") == 0 ||
strcmp(dirent->d_name, "..") == 0)
continue;
pool = objc_autoreleasePoolPush();
file = [OFString stringWithCString: dirent->d_name
encoding: encoding];
[files addObject: file];
objc_autoreleasePoolPop(pool);
}
} @finally {
closedir(dir);
# if !defined(HAVE_READDIR_R) && defined(OF_HAVE_THREADS)
if (!of_mutex_unlock(&readdirMutex))
@throw [OFUnlockFailedException exception];
# endif
}
#else
void *pool = objc_autoreleasePoolPush();
HANDLE handle;
WIN32_FIND_DATAW fd;
path = [path stringByAppendingString: @"\\*"];
if ((handle = FindFirstFileW([path UTF16String],
&fd)) == INVALID_HANDLE_VALUE) {
int errNo = 0;
if (GetLastError() == ERROR_FILE_NOT_FOUND)
errNo = ENOENT;
@throw [OFOpenItemFailedException exceptionWithPath: path
errNo: errNo];
}
@try {
do {
void *pool2 = objc_autoreleasePoolPush();
OFString *file;
if (!wcscmp(fd.cFileName, L".") ||
!wcscmp(fd.cFileName, L".."))
continue;
file = [OFString stringWithUTF16String: fd.cFileName];
[files addObject: file];
objc_autoreleasePoolPop(pool2);
} while (FindNextFileW(handle, &fd));
if (GetLastError() != ERROR_NO_MORE_FILES)
@throw [OFReadFailedException exceptionWithObject: self
requestedLength: 0];
} @finally {
FindClose(handle);
}
objc_autoreleasePoolPop(pool);
#endif
[files makeImmutable];
return files;
}
- (void)changeCurrentDirectoryPath: (OFString*)path
{
if (path == nil)
@throw [OFInvalidArgumentException exception];
#ifndef OF_WINDOWS
if (chdir([path cStringWithEncoding:
[OFSystemInfo native8BitEncoding]]) != 0)
#else
if (_wchdir([path UTF16String]) != 0)
#endif
@throw [OFChangeCurrentDirectoryPathFailedException
exceptionWithPath: path
errNo: errno];
}
- (of_offset_t)sizeOfFileAtPath: (OFString*)path
{
of_stat_t s;
if (path == nil)
@throw [OFInvalidArgumentException exception];
if (of_stat(path, &s) != 0)
@throw [OFStatItemFailedException exceptionWithPath: path
errNo: errno];
return s.st_size;
}
- (OFDate*)accessTimeOfItemAtPath: (OFString*)path
{
of_stat_t s;
if (path == nil)
@throw [OFInvalidArgumentException exception];
if (of_stat(path, &s) != 0)
@throw [OFStatItemFailedException exceptionWithPath: path
errNo: errno];
/* FIXME: We could be more precise on some OSes */
return [OFDate dateWithTimeIntervalSince1970: s.st_atime];
}
- (OFDate*)modificationTimeOfItemAtPath: (OFString*)path
{
of_stat_t s;
if (path == nil)
@throw [OFInvalidArgumentException exception];
if (of_stat(path, &s) != 0)
@throw [OFStatItemFailedException exceptionWithPath: path
errNo: errno];
/* FIXME: We could be more precise on some OSes */
return [OFDate dateWithTimeIntervalSince1970: s.st_mtime];
}
- (OFDate*)statusChangeTimeOfItemAtPath: (OFString*)path
{
of_stat_t s;
if (path == nil)
@throw [OFInvalidArgumentException exception];
if (of_stat(path, &s) != 0)
@throw [OFStatItemFailedException exceptionWithPath: path
errNo: errno];
/* FIXME: We could be more precise on some OSes */
return [OFDate dateWithTimeIntervalSince1970: s.st_ctime];
}
#ifdef OF_HAVE_CHMOD
- (void)changePermissionsOfItemAtPath: (OFString*)path
permissions: (mode_t)permissions
{
if (path == nil)
@throw [OFInvalidArgumentException exception];
# ifndef OF_WINDOWS
if (chmod([path cStringWithEncoding: [OFSystemInfo native8BitEncoding]],
permissions) != 0)
# else
if (_wchmod([path UTF16String], permissions) != 0)
# endif
@throw [OFChangePermissionsFailedException
exceptionWithPath: path
permissions: permissions
errNo: errno];
}
#endif
#ifdef OF_HAVE_CHOWN
- (void)changeOwnerOfItemAtPath: (OFString*)path
owner: (OFString*)owner
group: (OFString*)group
{
uid_t uid = -1;
gid_t gid = -1;
of_string_encoding_t encoding;
if (path == nil || (owner == nil && group == nil))
@throw [OFInvalidArgumentException exception];
encoding = [OFSystemInfo native8BitEncoding];
# ifdef OF_HAVE_THREADS
if (!of_mutex_lock(&chownMutex))
@throw [OFLockFailedException exception];
@try {
# endif
if (owner != nil) {
struct passwd *passwd;
if ((passwd = getpwnam([owner
cStringWithEncoding: encoding])) == NULL)
@throw [OFChangeOwnerFailedException
exceptionWithPath: path
owner: owner
group: group
errNo: errno];
uid = passwd->pw_uid;
}
if (group != nil) {
struct group *group_;
if ((group_ = getgrnam([group
cStringWithEncoding: encoding])) == NULL)
@throw [OFChangeOwnerFailedException
exceptionWithPath: path
owner: owner
group: group
errNo: errno];
gid = group_->gr_gid;
}
# ifdef OF_HAVE_THREADS
} @finally {
if (!of_mutex_unlock(&chownMutex))
@throw [OFUnlockFailedException exception];
}
# endif
if (chown([path cStringWithEncoding: encoding], uid, gid) != 0)
@throw [OFChangeOwnerFailedException exceptionWithPath: path
owner: owner
group: group
errNo: errno];
}
#endif
- (void)copyItemAtPath: (OFString*)source
toPath: (OFString*)destination
{
void *pool;
of_stat_t s;
if (source == nil || destination == nil)
@throw [OFInvalidArgumentException exception];
pool = objc_autoreleasePoolPush();
if (of_lstat(destination, &s) == 0)
@throw [OFCopyItemFailedException
exceptionWithSourcePath: source
destinationPath: destination
errNo: EEXIST];
if (of_lstat(source, &s) != 0)
@throw [OFCopyItemFailedException
exceptionWithSourcePath: source
destinationPath: destination
errNo: errno];
if (S_ISDIR(s.st_mode)) {
OFArray *contents;
@try {
[self createDirectoryAtPath: destination];
#ifdef OF_HAVE_CHMOD
[self changePermissionsOfItemAtPath: destination
permissions: s.st_mode];
#endif
contents = [self contentsOfDirectoryAtPath: source];
} @catch (id e) {
/*
* Only convert exceptions to OFCopyItemFailedException
* that have an errNo property. This covers all I/O
* related exceptions from the operations used to copy
* an item, all others should be left as is.
*/
if ([e respondsToSelector: @selector(errNo)])
@throw [OFCopyItemFailedException
exceptionWithSourcePath: source
destinationPath: destination
errNo: [e errNo]];
@throw e;
}
for (OFString *item in contents) {
void *pool2 = objc_autoreleasePoolPush();
OFString *sourcePath, *destinationPath;
sourcePath =
[source stringByAppendingPathComponent: item];
destinationPath =
[destination stringByAppendingPathComponent: item];
[self copyItemAtPath: sourcePath
toPath: destinationPath];
objc_autoreleasePoolPop(pool2);
}
} else if (S_ISREG(s.st_mode)) {
size_t pageSize = [OFSystemInfo pageSize];
OFFile *sourceFile = nil;
OFFile *destinationFile = nil;
char *buffer;
if ((buffer = malloc(pageSize)) == NULL)
@throw [OFOutOfMemoryException
exceptionWithRequestedSize: pageSize];
@try {
sourceFile = [OFFile fileWithPath: source
mode: @"rb"];
destinationFile = [OFFile fileWithPath: destination
mode: @"wb"];
while (![sourceFile isAtEndOfStream]) {
size_t length;
length = [sourceFile readIntoBuffer: buffer
length: pageSize];
[destinationFile writeBuffer: buffer
length: length];
}
#ifdef OF_HAVE_CHMOD
[self changePermissionsOfItemAtPath: destination
permissions: s.st_mode];
#endif
} @catch (id e) {
/*
* Only convert exceptions to OFCopyItemFailedException
* that have an errNo property. This covers all I/O
* related exceptions from the operations used to copy
* an item, all others should be left as is.
*/
if ([e respondsToSelector: @selector(errNo)])
@throw [OFCopyItemFailedException
exceptionWithSourcePath: source
destinationPath: destination
errNo: [e errNo]];
@throw e;
} @finally {
[sourceFile close];
[destinationFile close];
free(buffer);
}
#ifdef OF_HAVE_SYMLINK
} else if (S_ISLNK(s.st_mode)) {
@try {
source = [self destinationOfSymbolicLinkAtPath: source];
[self createSymbolicLinkAtPath: destination
withDestinationPath: source];
} @catch (id e) {
/*
* Only convert exceptions to OFCopyItemFailedException
* that have an errNo property. This covers all I/O
* related exceptions from the operations used to copy
* an item, all others should be left as is.
*/
if ([e respondsToSelector: @selector(errNo)])
@throw [OFCopyItemFailedException
exceptionWithSourcePath: source
destinationPath: destination
errNo: [e errNo]];
@throw e;
}
#endif
} else
@throw [OFCopyItemFailedException
exceptionWithSourcePath: source
destinationPath: destination
errNo: ENOTSUP];
objc_autoreleasePoolPop(pool);
}
- (void)moveItemAtPath: (OFString*)source
toPath: (OFString*)destination
{
void *pool;
of_stat_t s;
#ifndef OF_WINDOWS
of_string_encoding_t encoding;
#endif
if (source == nil || destination == nil)
@throw [OFInvalidArgumentException exception];
pool = objc_autoreleasePoolPush();
if (of_lstat(destination, &s) == 0)
@throw [OFCopyItemFailedException
exceptionWithSourcePath: source
destinationPath: destination
errNo: EEXIST];
#ifndef OF_WINDOWS
encoding = [OFSystemInfo native8BitEncoding];
if (rename([source cStringWithEncoding: encoding],
[destination cStringWithEncoding: encoding]) != 0) {
#else
if (_wrename([source UTF16String], [destination UTF16String]) != 0) {
#endif
if (errno != EXDEV)
@throw [OFMoveItemFailedException
exceptionWithSourcePath: source
destinationPath: destination
errNo: errno];
@try {
[self copyItemAtPath: source
toPath: destination];
} @catch (OFCopyItemFailedException *e) {
[self removeItemAtPath: destination];
@throw [OFMoveItemFailedException
exceptionWithSourcePath: source
destinationPath: destination
errNo: [e errNo]];
}
@try {
[self removeItemAtPath: source];
} @catch (OFRemoveItemFailedException *e) {
@throw [OFMoveItemFailedException
exceptionWithSourcePath: source
destinationPath: destination
errNo: [e errNo]];
}
}
objc_autoreleasePoolPop(pool);
}
- (void)removeItemAtPath: (OFString*)path
{
void *pool;
of_stat_t s;
if (path == nil)
@throw [OFInvalidArgumentException exception];
pool = objc_autoreleasePoolPush();
if (of_lstat(path, &s) != 0)
@throw [OFRemoveItemFailedException exceptionWithPath: path
errNo: errno];
if (S_ISDIR(s.st_mode)) {
OFArray *contents;
@try {
contents = [self contentsOfDirectoryAtPath: path];
} @catch (id e) {
/*
* Only convert exceptions to
* OFRemoveItemFailedException that have an errNo
* property. This covers all I/O related exceptions
* from the operations used to remove an item, all
* others should be left as is.
*/
if ([e respondsToSelector: @selector(errNo)])
@throw [OFRemoveItemFailedException
exceptionWithPath: path
errNo: [e errNo]];
@throw e;
}
for (OFString *item in contents) {
void *pool2 = objc_autoreleasePoolPush();
[self removeItemAtPath:
[path stringByAppendingPathComponent: item]];
objc_autoreleasePoolPop(pool2);
}
}
#ifndef OF_WINDOWS
if (remove([path cStringWithEncoding:
[OFSystemInfo native8BitEncoding]]) != 0)
#else
if (_wremove([path UTF16String]) != 0)
#endif
@throw [OFRemoveItemFailedException exceptionWithPath: path
errNo: errno];
objc_autoreleasePoolPop(pool);
}
#ifdef OF_HAVE_LINK
- (void)linkItemAtPath: (OFString*)source
toPath: (OFString*)destination
{
void *pool;
of_string_encoding_t encoding;
if (source == nil || destination == nil)
@throw [OFInvalidArgumentException exception];
pool = objc_autoreleasePoolPush();
encoding = [OFSystemInfo native8BitEncoding];
if (link([source cStringWithEncoding: encoding],
[destination cStringWithEncoding: encoding]) != 0)
@throw [OFLinkFailedException
exceptionWithSourcePath: source
destinationPath: destination
errNo: errno];
objc_autoreleasePoolPop(pool);
}
#endif
#ifdef OF_HAVE_SYMLINK
- (void)createSymbolicLinkAtPath: (OFString*)destination
withDestinationPath: (OFString*)source
{
void *pool;
of_string_encoding_t encoding;
if (source == nil || destination == nil)
@throw [OFInvalidArgumentException exception];
pool = objc_autoreleasePoolPush();
encoding = [OFSystemInfo native8BitEncoding];
if (symlink([source cStringWithEncoding: encoding],
[destination cStringWithEncoding: encoding]) != 0)
@throw [OFCreateSymbolicLinkFailedException
exceptionWithSourcePath: source
destinationPath: destination
errNo: errno];
objc_autoreleasePoolPop(pool);
}
- (OFString*)destinationOfSymbolicLinkAtPath: (OFString*)path
{
char destination[PATH_MAX];
ssize_t length;
of_string_encoding_t encoding;
if (path == nil)
@throw [OFInvalidArgumentException exception];
encoding = [OFSystemInfo native8BitEncoding];
length = readlink([path cStringWithEncoding: encoding],
destination, PATH_MAX);
if (length < 0)
@throw [OFStatItemFailedException exceptionWithPath: path
errNo: errno];
return [OFString stringWithCString: destination
encoding: encoding
length: length];
}
#endif
@end