//
//  NSWorkspace_RBAdditions.m
//  PathProps
//
//  Created by Rainer Brockerhoff on 10/04/2007.
//  Copyright 2007 Rainer Brockerhoff. All rights reserved.
//

#import "NSWorkspace_RBAdditions.h"
#include <IOKit/IOKitLib.h>
#include <sys/mount.h>
#include <mach/mach.h>

NSString* NSWorkspace_RBfstypename = @"NSWorkspace_RBfstypename";
NSString* NSWorkspace_RBmntonname = @"NSWorkspace_RBmntonname";
NSString* NSWorkspace_RBmntfromname = @"NSWorkspace_RBmntfromname";
NSString* NSWorkspace_RBdeviceinfo = @"NSWorkspace_RBdeviceinfo";
NSString* NSWorkspace_RBimagefilepath = @"NSWorkspace_RBimagefilepath";
NSString* NSWorkspace_RBconnectiontype = @"NSWorkspace_RBconnectiontype";
NSString* NSWorkspace_RBpartitionscheme = @"NSWorkspace_RBpartitionscheme";
NSString* NSWorkspace_RBserverURL = @"NSWorkspace_RBserverURL";

// This static funtion concatenates two strings, but first checks several possibilities...
// like one or the other nil, or one containing the other already.

static NSString* AddPart(NSString* first,NSString* second) {
    if (!second) {
        return first;
    }
    second = [second stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
    if (first) {
        if ([first rangeOfString:second options:NSCaseInsensitiveSearch].location==NSNotFound) {
            if ([second rangeOfString:first options:NSCaseInsensitiveSearch].location==NSNotFound) {
                return [NSString stringWithFormat:@"%@; %@",first,second];
            }
            return second;
        }
        return first;
    }
    return second;
}

// This static functions recurses "upwards" over the IO registry. Returns strings that are concatenated
// and ultimately end up under the NSWorkspace_RBdeviceinfo key.
// This isn't too robust in that it assumes that objects returned by the objectForKey methods are
// either strings or dictionaries. A "standard" implementations would use either only CoreFoundation and
// IOKit calls for this, or do more robust type checking on the returned objects.
//
// Also notice that this works as determined experimentally in 10.4.9, there's no official docs I could find.
// YMMV, and it may stop working in any new version of Mac OS X.

static NSString* CheckParents(io_object_t thing,NSString* part,NSMutableDictionary* dict) {
    NSString* result = part;
    io_iterator_t parentsIterator = 0;
    kern_return_t kernResult = IORegistryEntryGetParentIterator(thing,kIOServicePlane,&parentsIterator);
    if ((kernResult==KERN_SUCCESS)&&parentsIterator) {
        io_object_t nextParent = 0;
        while ((nextParent = IOIteratorNext(parentsIterator))) {
            NSDictionary* props = nil;
            NSString* image = nil;
            NSString* partition = nil;
            NSString* connection = nil;
            kernResult = IORegistryEntryCreateCFProperties(nextParent,(CFMutableDictionaryRef*)&props,kCFAllocatorDefault,0);
            if (IOObjectConformsTo(nextParent,"IOApplePartitionScheme")) {
                partition = [props objectForKey:@"Content Mask"];
            } else if (IOObjectConformsTo(nextParent,"IOMedia")) {
                partition = [props objectForKey:@"Content"];
            } else if (IOObjectConformsTo(nextParent,"IODiskImageBlockStorageDeviceOutKernel")) {
                NSData* data = nil;
                                if ((data = [[props objectForKey:@"Protocol Characteristics"] objectForKey:@"Virtual Interface Location Path"])) {
                    image = [[[NSString alloc] initWithBytes:[data bytes] length:[data length] encoding:NSUTF8StringEncoding] autorelease];
                }
            } else if (IOObjectConformsTo(nextParent,"IOHDIXHDDriveInKernel")) {
                image = [props objectForKey:@"KDIURLPath"];
            }
            NSDictionary* subdict;
                        if ((subdict = [props objectForKey:@"Protocol Characteristics"])) {
                connection = [subdict objectForKey:@"Physical Interconnect"];
            } else {
                connection = [props objectForKey:@"Physical Interconnect"];
            }
            if (connection) {
                [dict setObject:AddPart([dict objectForKey:NSWorkspace_RBconnectiontype],connection) forKey:NSWorkspace_RBconnectiontype];
            }
            if (partition) {
                [dict setObject:partition forKey:NSWorkspace_RBpartitionscheme];
            }
            if (image) {
                [dict setObject:image forKey:NSWorkspace_RBimagefilepath];
            }
            NSString* value;
                        if ((subdict = [props objectForKey:@"Device Characteristics"])) {
                                if ((value = [subdict objectForKey:@"Product Name"])) {
                    result = AddPart(result,value);
                }
                                if ((value = [subdict objectForKey:@"Product Revision Level"])) {
                    result = AddPart(result,value);
                }
                                if ((value = [subdict objectForKey:@"Vendor Name"])) {
                    result = AddPart(result,value);
                }
            }
                        if ((value = [props objectForKey:@"USB Serial Number"])) {
                result = AddPart(result,value);
            }
                        if ((value = [props objectForKey:@"USB Vendor Name"])) {
                result = AddPart(result,value);
            }
            NSString* cls = [(NSString*)IOObjectCopyClass(nextParent) autorelease];
            if (![cls isEqualToString:@"IOPCIDevice"]) {

// Uncomment the following line to have the device tree dumped to the console.
//              NSLog(@"=================================> %@:%@\n",cls,props);

                result = CheckParents(nextParent,result,dict);
            }
            IOObjectRelease(nextParent);
        }
    }
    if (parentsIterator) {
        IOObjectRelease(parentsIterator);
    }
    return result;
}

// This formats the (partially undocumented) AFPXMountInfo info into a string.

/*
static NSString* FormatAFPURL(AFPXVolMountInfoPtr mountInfo,NSString** devdesc) {
    UInt8* work = ((UInt8*)mountInfo)+mountInfo->serverNameOffset;
    if (devdesc) {
        *devdesc = [[[NSString alloc] initWithBytes:&work[1] length:work[0] encoding:NSUTF8StringEncoding] autorelease];
    }
    work = ((UInt8*)mountInfo)+mountInfo->volNameOffset;
    NSString* volname = [[[NSString alloc] initWithBytes:&work[1] length:work[0] encoding:NSUTF8StringEncoding] autorelease];
    work = ((UInt8*)mountInfo)+mountInfo->alternateAddressOffset;
    AFPAlternateAddress* afpa = (AFPAlternateAddress*)work;
    AFPTagData* afpta = (AFPTagData*)(&afpa->fAddressList);
    NSString* ip = nil;
    NSString* dns = nil;
    int i = afpa->fAddressCount;
    while ((i-->0)) {
        switch (afpta->fType) {
            case kAFPTagTypeIP:
                if (!ip) {
                    ip = [[[NSString alloc] initWithBytes:&afpta->fData[0] length:afpta->fLength-2 encoding:NSUTF8StringEncoding] autorelease];
                }
                break;
            case kAFPTagTypeIPPort:
                ip = [NSString stringWithFormat:@"%u.%u.%u.%u:%u",afpta->fData[0],afpta->fData[1],afpta->fData[2],afpta->fData[3],OSSwapBigToHostInt16(*(UInt16*)&afpta->fData[4])];
                break;
            case kAFPTagTypeDNS:
                dns = [[[NSString alloc] initWithBytes:&afpta->fData[0] length:afpta->fLength-2 encoding:NSUTF8StringEncoding] autorelease];
                break;
            case 0x07:
                ip = [NSString stringWithFormat:@"[%x:%x:%x:%x:%x:%x:%x:%x]",OSSwapBigToHostInt16(*(UInt16*)&afpta->fData[0]),
                    OSSwapBigToHostInt16(*(UInt16*)&afpta->fData[2]),OSSwapBigToHostInt16(*(UInt16*)&afpta->fData[4]),
                    OSSwapBigToHostInt16(*(UInt16*)&afpta->fData[6]),OSSwapBigToHostInt16(*(UInt16*)&afpta->fData[8]),
                    OSSwapBigToHostInt16(*(UInt16*)&afpta->fData[10]),OSSwapBigToHostInt16(*(UInt16*)&afpta->fData[12]),
                    OSSwapBigToHostInt16(*(UInt16*)&afpta->fData[14])];
                break;
        }
        afpta = (AFPTagData*)((char*)afpta+afpta->fLength);
    }
    return [NSString stringWithFormat:@"afp://%@/%@",dns?:(ip?:@""),volname];
}
*/

@implementation NSWorkspace (NSWorkspace_RBAdditions)

// Returns a NSDictionary with properties for the path. See details in the .h file.
// This assumes that the length of path is less than PATH_MAX (currently 1024 characters).

- (NSDictionary*)propertiesForPath:(NSString*)path {
    const char* ccpath = (const char*)[path fileSystemRepresentation];
    NSMutableDictionary* result = nil;
    struct statfs fs;
    if (!statfs(ccpath,&fs)) {
        NSString* from = [NSString stringWithUTF8String:fs.f_mntfromname];
        result = [NSMutableDictionary dictionaryWithObjectsAndKeys:
            [NSString stringWithUTF8String:fs.f_fstypename],NSWorkspace_RBfstypename,
            [NSString stringWithUTF8String:fs.f_mntonname],NSWorkspace_RBmntonname,
            nil];
        if (strncmp(fs.f_mntfromname,"/dev/",5)==0) {
// For a local volume,get the IO registry tree and search it for further info.
            mach_port_t masterPort = 0;
            io_iterator_t mediaIterator = 0;
            kern_return_t kernResult = IOMasterPort(bootstrap_port,&masterPort);
            if (kernResult==KERN_SUCCESS) {
                CFMutableDictionaryRef classesToMatch = IOBSDNameMatching(masterPort,0,&fs.f_mntfromname[5]);
                if (classesToMatch) {
                    kernResult = IOServiceGetMatchingServices(masterPort,classesToMatch,&mediaIterator);
                    if ((kernResult==KERN_SUCCESS)&&mediaIterator) {
                        io_object_t firstMedia = 0;
                        while ((firstMedia = IOIteratorNext(mediaIterator))) {
                            NSString* stuff = CheckParents(firstMedia,nil,result);
                            if (stuff) {
                                [result setObject:stuff forKey:NSWorkspace_RBdeviceinfo];
                            }
                            IOObjectRelease(firstMedia);
                        }
                    }
                }
            }
            if (mediaIterator) {
                IOObjectRelease(mediaIterator);
            }
            if (masterPort) {
                mach_port_deallocate(mach_task_self(),masterPort);
            }
        }
        //Don't need this for disk images, gets around warnings for some deprecated functions

        /* else {
// For a network volume, get the volume reference number and use to get the server URL.
            FSRef ref;
            if (FSPathMakeRef((const UInt8*)ccpath,&ref,NULL)==noErr) {
                FSCatalogInfo info;
                if (FSGetCatalogInfo(&ref,kFSCatInfoVolume,&info,NULL,NULL,NULL)==noErr) {
                    ParamBlockRec pb;
                    UInt16 vmisize = 0;
                    VolumeMountInfoHeaderPtr mountInfo = NULL;
                    pb.ioParam.ioCompletion = NULL;
                    pb.ioParam.ioNamePtr = NULL;
                    pb.ioParam.ioVRefNum = info.volume;
                    pb.ioParam.ioBuffer = (Ptr)&vmisize;
                    pb.ioParam.ioReqCount = sizeof(vmisize);
                    if ((PBGetVolMountInfoSize(&pb)==noErr)&&vmisize) {
                        mountInfo = (VolumeMountInfoHeaderPtr)malloc(vmisize);
                        if (mountInfo) {
                            pb.ioParam.ioBuffer = (Ptr)mountInfo;
                            pb.ioParam.ioReqCount = vmisize;
                            if (PBGetVolMountInfo(&pb)==noErr) {
                                NSString* url = nil;
                                switch (mountInfo->media) {
                                case AppleShareMediaType:
                                    url = FormatAFPURL((AFPXVolMountInfoPtr)mountInfo,&from);
                                    break;
                                case 'http':
                                    url = from;
                                    break;
                                case 'crbm':
                                case 'nfs_':
                                case 'cifs':
                                    url = [NSString stringWithUTF8String:(char*)mountInfo+sizeof(VolumeMountInfoHeader)+sizeof(OSType)];
                                    break;
                                }
                                if (url) {
                                    [result setObject:url forKey:NSWorkspace_RBserverURL];
                                }
                            }
                        }
                        free(mountInfo);
                    }
                }
            }
        }*/
        [result setObject:from forKey:NSWorkspace_RBmntfromname];
    }
    return result;
}

@end
