// Copyright 1997-1999 Omni Development, Inc.  All rights reserved.
//
// This software may only be used and reproduced according to the
// terms in the file OmniSourceLicense.html, which should be
// distributed with this project and can also be found at
// http://www.omnigroup.com/DeveloperResources/OmniSourceLicense.html.

#import "NSFileManager-OFExtensions.h"

#import <Foundation/Foundation.h>
#import <OmniBase/OmniBase.h>
#import <OmniBase/system.h>

#ifdef winnt
#import <errno.h>
#else
#import <sys/errno.h>
#endif
#import <sys/param.h>
#import <stdio.h>
#ifndef winnt
#import <sys/mount.h>
#endif

#import "OFUserDefaults.h"

RCS_ID("$Header: /Network/Developer/Source/CVS/OmniGroup/OmniFoundation/OpenStepExtensions.subproj/NSFileManager-OFExtensions.m,v 1.30 1999/03/30 21:25:44 kc Exp $")

@implementation NSFileManager (OFExtensions)

static NSLock *tempFilenameLock;
static NSString *scratchDirectoryPath;
static NSLock *scratchDirectoryLock;
static int permissionsMask = 0022;

+ (void)didLoad;
{
    tempFilenameLock = [[NSLock alloc] init];
    scratchDirectoryPath = nil;
    scratchDirectoryLock = [[NSLock alloc] init];

    permissionsMask = umask(permissionsMask);
    umask(permissionsMask); // Restore the original value
}

// Create a unique temp filename from a template filename, given a range within the template filename which identifies where the unique portion of the filename is to lie.

- (NSString *)tempFilenameFromTemplate:(NSString *)inputString andRange:(NSRange)replaceRange;
{
    NSMutableString *tempFilename = nil;
    NSString *result;
    unsigned int tempFilenameNumber = 1;

    [tempFilenameLock lock];
    NS_DURING {
        do {
            [tempFilename release];
            tempFilename = [inputString mutableCopy];
            [tempFilename replaceCharactersInRange:replaceRange withString:[NSString stringWithFormat:@"%d", tempFilenameNumber++]];
        } while ([self fileExistsAtPath:tempFilename]);
    } NS_HANDLER {
        [tempFilenameLock unlock];
        [tempFilename release];
        [localException raise];
    } NS_ENDHANDLER;
    [tempFilenameLock unlock];

    result = [[tempFilename copy] autorelease]; // Make a nice immutable string
    [tempFilename release];
    return result;
}

// Create a unique temp filename from a template string, given a position within the template filename which identifies where the unique portion of the filename is to begin.

- (NSString *)tempFilenameFromTemplate:(NSString *)inputString
    andPosition:(int)position;
{
    NSRange replaceRange;

    replaceRange.location = position;
    replaceRange.length = 6;
    return [self tempFilenameFromTemplate:inputString andRange:replaceRange];
}

// Create a unique temp filename from a template string, given a substring within the template filename which is to be replaced by the unique portion of the filename.

- (NSString *)tempFilenameFromTemplate:(NSString *)inputString andSubstring:(NSString *)substring;
{
    NSRange replaceRange;

    replaceRange = [inputString rangeOfString:substring];
    return [self tempFilenameFromTemplate:inputString andRange:replaceRange];
}

// Create a unique temp filename from a template string which contains a substring of six hash marks which are to be replaced by the unique portion of the filename.

- (NSString *)tempFilenameFromHashesTemplate:(NSString *)inputString;
{
    return [self tempFilenameFromTemplate:inputString andSubstring:@"######"];
}

// Generate a unique filename based on a suggested name

- (NSString *)uniqueFilenameFromName:(NSString *)filename;
{
    int testFD;
    NSRange lastPathComponentRange, periodRange;

    testFD = open([self fileSystemRepresentationWithPath:filename], O_EXCL | O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (testFD != -1) {
	close(testFD);
	return filename;
    }
#warning -uniqueFilenameFromName: does not work properly on Windows
    lastPathComponentRange = [filename rangeOfString:@"/" options:NSBackwardsSearch];
    if (lastPathComponentRange.length != 0) {
	lastPathComponentRange.location++;
	lastPathComponentRange.length = [filename length] - lastPathComponentRange.location;
    }
    if (lastPathComponentRange.length == 0)
        lastPathComponentRange = NSMakeRange(0, [filename length]);

    periodRange = [filename rangeOfString:@"." options:0 range:lastPathComponentRange];
    if (periodRange.length != 0) {
	filename = [self tempFilenameFromHashesTemplate:[NSString stringWithFormat:@"%@-######.%@", [filename substringToIndex:periodRange.location], [filename substringFromIndex:periodRange.location + 1]]];
    } else {
	filename = [self tempFilenameFromHashesTemplate:[NSString stringWithFormat:@"%@-######", filename]];
    }

    return filename;
}

- (NSString *)scratchDirectoryPath;
{
    OFUserDefaults *defaults;
    NSString *defaultsScratchDirectoryPath;
    NSString *workingScratchDirectoryPath;
    NSMutableDictionary *attributes;
    
    [scratchDirectoryLock lock];

    if (scratchDirectoryPath) {
	if ([self fileExistsAtPath:scratchDirectoryPath]) {
	    [scratchDirectoryLock unlock];
	    return scratchDirectoryPath;
	} else {
	    [scratchDirectoryPath release];
	    scratchDirectoryPath = nil;
	}
    }
    
    defaults = [OFUserDefaults sharedUserDefaults];

    defaultsScratchDirectoryPath = [defaults stringForKey:@"OFScratchDirectory"];
    if ([defaultsScratchDirectoryPath isEqualToString:@"NSTemporaryDirectory"]) {
        defaultsScratchDirectoryPath = NSTemporaryDirectory();
    } else {
        defaultsScratchDirectoryPath = [defaultsScratchDirectoryPath stringByExpandingTildeInPath];
    }
    [self createDirectoryAtPath:defaultsScratchDirectoryPath attributes:nil];
    attributes = [[NSMutableDictionary alloc] initWithCapacity:1];
    [attributes setObject:[NSNumber numberWithInt:0777] forKey:NSFilePosixPermissions];
    [self changeFileAttributes:attributes atPath:defaultsScratchDirectoryPath];
    [attributes release];

    workingScratchDirectoryPath = [defaultsScratchDirectoryPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-%@-######", [defaults ownerName], NSUserName()]];
    workingScratchDirectoryPath = [self tempFilenameFromHashesTemplate:workingScratchDirectoryPath];
    [workingScratchDirectoryPath retain];

    [self createDirectoryAtPath:workingScratchDirectoryPath attributes:nil];
    scratchDirectoryPath = workingScratchDirectoryPath;

    [scratchDirectoryLock unlock];
    return scratchDirectoryPath;
}

- (NSString *)scratchFilenameNamed:(NSString *)aName;
{
    if (!aName || [aName length] == 0)
	aName = @"scratch";
    return [self uniqueFilenameFromName:[[self scratchDirectoryPath] stringByAppendingPathComponent:aName]];
}

- (void)removeScratchDirectory;
{
    if (!scratchDirectoryPath)
	return;
    [self removeFileAtPath:scratchDirectoryPath handler:nil];
    [scratchDirectoryPath release];
    scratchDirectoryPath = nil;
}

- (void)touchFile:(NSString *)filePath;
{
    NSMutableDictionary *attributes;

    attributes = [[NSMutableDictionary alloc] initWithCapacity:1];
    [attributes setObject:[NSDate date] forKey:NSFileModificationDate];
    [self changeFileAttributes:attributes atPath:filePath];
    [attributes release];
}


- (BOOL)directoryExistsAtPath:(NSString *)path;
{
    BOOL isDirectory = NO;

    if (![self fileExistsAtPath:path isDirectory:&isDirectory])
        return NO;
    return isDirectory;
}

- (void)createPathToFile:(NSString *)path attributes:(NSDictionary *)attributes;
    // Creates any directories needed to be able to create a file at the specified path.  Raises an exception on failure.
{
    NSArray *pathComponents;
    unsigned int dirIndex, dirCount;

    pathComponents = [path pathComponents];
    for (dirIndex = 0, dirCount = [pathComponents count] - 1; dirIndex < dirCount; dirIndex++) {
        NSString *partialPath;
        BOOL fileExists, isDirectory;

        partialPath = [NSString pathWithComponents:[pathComponents subarrayWithRange:NSMakeRange(0, dirIndex + 1)]];
        fileExists = [self fileExistsAtPath:partialPath isDirectory:&isDirectory];
        if (!fileExists) {
            if (![self createDirectoryAtPath:partialPath attributes:attributes]) {
                [NSException raise:NSGenericException format:@"Unable to create a directory at path: %@", partialPath];
            }
        } else if (!isDirectory) {
            [NSException raise:NSGenericException format:@"Unable to write to path: %@, because %@ is a file", path, partialPath];
        }
    }
}

- (NSString *)existingPortionOfPath:(NSString *)path;
{
    NSArray *pathComponents;
    unsigned int goodComponentsCount, componentCount;

    pathComponents = [path pathComponents];
    componentCount = [pathComponents count];
    for (goodComponentsCount = 0; goodComponentsCount < componentCount; goodComponentsCount++) {
        BOOL isDirectory;

        if (![self fileExistsAtPath:[NSString pathWithComponents:[pathComponents subarrayWithRange:NSMakeRange(0, goodComponentsCount + 1)]] isDirectory:&isDirectory])
            break;

        // Break early if we hit a non-directory before the end of the path
        if (!isDirectory && (goodComponentsCount < componentCount-1))
            break;
    }

    if (goodComponentsCount == 0)
        return @"";
    else if (goodComponentsCount == 1)
        return @"/";
    else if (goodComponentsCount == componentCount)
        return path;
    else
        return [[NSString pathWithComponents:[pathComponents subarrayWithRange:NSMakeRange(0, goodComponentsCount)]] stringByAppendingString:@"/"];
}


//

- (NSNumber *)posixPermissionsForMode:(unsigned int)mode;
{
    return [NSNumber numberWithUnsignedLong:mode & (~permissionsMask)];
}

- (NSNumber *)defaultFilePermissions;
{
    return [self posixPermissionsForMode:0666];
}

- (NSNumber *)defaultDirectoryPermissions;
{
    return [self posixPermissionsForMode:0777];
}

//

- (int)filesystemStats:(struct statfs *)stats forPath:(NSString *)path;
{
#ifndef winnt
    if ([[[self fileAttributesAtPath:path traverseLink:NO] fileType] isEqualToString:NSFileTypeSymbolicLink])
        // BUG: statfs() will return stats on the file we link to, not the link itself.  We want stats on the link itself, but there is no lstatfs().  As a mostly-correct hackaround, I get the stats on the link's parent directory. This will fail if you NFS-mount a link as the source from a remote machine -- it'll report that the link isn't network mounted, because its local parent dir isn't.  Hopefully, this isn't real common.
        return statfs([self fileSystemRepresentationWithPath:[path stringByDeletingLastPathComponent]], stats);
    else
        return statfs([self fileSystemRepresentationWithPath:path], stats);
#else
#warning -filesystemStats:forPath: not yet implemented for this operating system
    OBRequestConcreteImplementation(self, _cmd);
    return nil;
#endif
}

- (NSString *)networkMountPointForPath:(NSString *)path returnMountSource:(NSString **)mountSource;
{
#ifdef RHAPSODY
    struct statfs stats;

    if ([self filesystemStats:&stats forPath:path] == -1)
        return nil;

    if (strcmp(stats.f_fstypename, "nfs") != 0)
        return nil;

    if (mountSource)
        *mountSource = [self stringWithFileSystemRepresentation:stats.f_mntfromname length:strlen(stats.f_mntfromname)];
    
    return [self stringWithFileSystemRepresentation:stats.f_mntonname length:strlen(stats.f_mntonname)];
#else
#warning -networkMountPointForPath:returnMountSource: not yet implemented for this operating system
    OBRequestConcreteImplementation(self, _cmd);
    return nil;
#endif
}

@end

#if !defined(YELLOW_BOX) && !defined(WIN32) && defined(NeXT)

// OPENSTEP/Mach 4.2 had a bug where -createFileAtPath:contents:attributes: with nil attributes would create a file with the permissions of 0700 (u=rwx), rather than 0666 (a=rw) modified by the umask.  Thus, all files created using this method (by OmniWeb, for example) were made executable, which meant that when opening them in the Workspace tried to execute them rather than view them.

@interface NSFileManager (OFBugFix)
- (BOOL)OFCreateFileAtPath:(NSString *)path contents:(NSData *)data attributes:(NSDictionary *)attr;
@end

@implementation NSFileManager (OFBugFix)

static BOOL (*originalCreateFileAtPathIMP)(id, SEL, NSString *, NSData *, NSDictionary *);

+ (void)didLoad;
{
    originalCreateFileAtPathIMP = (typeof(originalCreateFileAtPathIMP))OBReplaceMethodImplementationWithSelector(self, @selector(createFileAtPath:contents:attributes:), @selector(OFCreateFileAtPath:contents:attributes:));
}

- (BOOL)OFCreateFileAtPath:(NSString *)path contents:(NSData *)data attributes:(NSDictionary *)attributes;
{
    NSMutableDictionary *fixedAttributes;
    BOOL success;

    if ([attributes objectForKey:NSFilePosixPermissions] != nil) {
        return originalCreateFileAtPathIMP(self, _cmd, path, data, attributes);
    }

    fixedAttributes = [[NSMutableDictionary alloc] initWithDictionary:attributes];
    [fixedAttributes setObject:[self defaultFilePermissions] forKey:NSFilePosixPermissions];
    success = originalCreateFileAtPathIMP(self, _cmd, path, data, fixedAttributes);
    [fixedAttributes release];
    return success;
}

@end

#endif
