/* * Copyright (c) 2001 Apple Computer, Inc. All rights reserved. * * @APPLE_LICENSE_HEADER_START@ * * "Portions Copyright (c) 2001 Apple Computer, Inc. All Rights * Reserved. This file contains Original Code and/or Modifications of * Original Code as defined in and that are subject to the Apple Public * Source License Version 1.0 (the 'License'). You may not use this file * except in compliance with the License. Please obtain a copy of the * License at http:www.apple.com/publicsource and read it before using * this file. * * The Original Code and all software distributed under the License are * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT. Please see the * License for the specific language governing rights and limitations * under the License." * * @APPLE_LICENSE_HEADER_END@ */ /* * DSAgent.m * Directory Services agent for lookupd */ #import "DSAgent.h" #ifndef _ENABLE_DSAGENT_ @implementation DSAgent - (DSAgent *)init { [self release]; return nil; } - (LUAgent *)initWithArg:(char *)arg { [self release]; return nil; } @end #else #import "LUGlobal.h" #import #import #import "Thread.h" #import #import #import #import /* * This table MUST be kept up to date with the NetInfo plug-in * in Directory Services */ #ifdef DSDEBUG #define kAttrConsts 45 #else #define kAttrConsts 41 #endif static const char *sAttrMap[kAttrConsts][2] = { /* 1 */ { kDSNAttrRecordName, "name" }, { kDS1AttrDistinguishedName, "realname" }, { kDS1AttrPasswordPlus, "passwd" }, { kDS1AttrPassword, "passwd" }, /* needed when retrieving all so that standard type received gets correctly mapped */ /* 5 */ { kDS1AttrUniqueID, "uid" }, { kDS1AttrPrimaryGroupID, "gid" }, { kDS1AttrUserShell, "shell" }, { kDS1AttrNFSHomeDirectory, "home" }, { kDSNAttrHomeDirectory, "home_loc" }, /* 10 */ { kDS1AttrInternetAlias, "InetAlias" }, { kDS1AttrMailAttribute, "applemail" }, { kDS1AttrAuthenticationHint, "hint" }, { kDS1AttrRARA, "RARA" }, { kDS1AttrGeneratedUID, "GeneratedUID" }, /* 15 */ { kDSNAttrGroupMembership, "users" }, { kDSNAttrEMailAddress, "mail" }, { kDSNAttrURL, "URL" }, { kDSNAttrURLForNSL, "URL" }, { kDSNAttrMIME, "mime" }, /* 20 */ { kDSNAttrHTML, "htmldata" }, { kDSNAttrNBPEntry, "NBPEntry" }, { kDSNAttrDNSName, "dnsname" }, { kDSNAttrIPAddress, "IP_Address" }, { kDS1AttrPrintServiceInfoText, "PrintServiceInfoText" }, /* 25 */ { kDS1AttrPrintServiceInfoXML, "PrintServiceInfoXML" }, { kDS1AttrVFSType, "vfstype" }, { kDS1AttrVFSPassNo, "passno" }, { kDS1AttrVFSDumpFreq, "dump_freq" }, { kDS1AttrVFSLinkDir, "dir" }, /* 30 */ { kDSNAttrVFSOpts, "opts" }, { kDS1AttrAliasData, "alias_data" }, { kDSNAttrPhoneNumber, "phonenumber" }, { kDS1AttrCapabilities, "capabilities" }, { kDSNAttrProtocols, "protocols" }, /* 35 */ { kDSNAttrMember, "users" }, #ifdef DSDEBUG /* 36 */ { kDSNAttrAllNames, "dsAttrTypeStandard:AllNames" }, { kStandardTargetAlias, "dsAttrTypeStandard:AppleMetaAliasTarget" }, { kStandardSourceAlias, "dsAttrTypeStandard:AppleMetaAliasSource" }, { kDSNAttrMetaNodeLocation, "dsAttrTypeStandard:AppleMetaNodeLocation" }, #endif /* 40-36 */{ kDS1AttrComment, "comment" }, { kDS1AttrAdminStatus, "AdminStatus" }, { kDS1AttrPwdAgingPolicy, "PwdAgingPolicy" }, { kDS1AttrChange, "change" }, { kDS1AttrExpire, "expire" }, /* 45-41 */{ kDSNAttrRecordAlias, "RecordAlias" } }; /* Static globals */ /* Static consts */ /* 4096 + 2048 */ static const unsigned long kBuffSize = 6144; /* Globals */ /* Can DS do the work or are we a noop */ BOOL gDSRunState = NO; /* Did we check the DS file indicator ie. we do this only once */ BOOL gCheckDSFileIndicator = YES; /* How long before we check our run state */ time_t gSeconds = 0; /* DS reference and search node reference are globals since DS FW is single threaded */ tDirReference gDirRef = 0; tDirNodeReference gNodeRef = 0; static void LogErrStr(int inLine, char *inObjC, char *inAPICall, tDirStatus inError) { #ifdef DSDEBUG system_log(LOG_DEBUG, "-- DS: %d: *** Error *** %s: %s() error = %d.", inLine , inObjC, inAPICall, inError); #endif } static void LogDbgStr(int inLine, char *inObjC, char *inDbgStr) { #ifdef DSDEBUG system_log(LOG_DEBUG, "-- DS: %d: %s: %s.", inLine, inObjC, inDbgStr); #endif } @implementation DSAgent - (BOOL)doWeUseDS { struct stat statResult; /* * ONLY if custom search policy is set then use DirectoryService */ if (stat("/Library/Preferences/DirectoryService/.DSRunningSP3", &statResult) == 0) return YES; #ifdef NOTDEF /* * check to see if DS is running and then we use it */ if (dsIsDirServiceRunning() == eDSNoErr) return YES; /* * now check if we need to allow the DS framework to restart DS * ie. the system has been rebooted and the search policy HAD been set to either * "custom" or "local netinfo only" so in these two cases we re-start DS * we make this check only once * ie. we check for the presence of the files * "/Library/Preferences/DirectoryService/.DSRunningSP2" or ".DSRunningSP3" */ if (gCheckDSFileIndicator) { gCheckDSFileIndicator = NO; if (stat("/Library/Preferences/DirectoryService/.DSRunningSP2", &statResult) == 0) return YES; if (stat("/Library/Preferences/DirectoryService/.DSRunningSP3", &statResult) == 0) return YES; } #endif return NO; } - (BOOL)canWeWork:(tDirReference*)inDirRef withSearchNode:(tDirNodeReference*)inNodeRef { tDirStatus status = eDSNoErr; tDataList *pDataList = NULL; tDataBuffer *pDataBuff = NULL; tDirReference dirRef = 0; tDirNodeReference nodeRef = 0; unsigned long count = 0; time_t timenow = 0; timenow = time(NULL); /* Is it time to check or continue with past state */ if (gSeconds > timenow) { /* Continuing with current state */ /* If DS was not running then return */ if (!gDSRunState) return NO; /* Else DS is running so we continue below to see if we have proper references */ } else { /* Time to check */ /* Don't bother if DS isn't running or DS should NOT be re-started */ if (![self doWeUseDS]) { gDSRunState = NO; /* Set the time to the future */ gSeconds = timenow + 30; return NO; } /* Else DS is running so we continue below to get the proper references */ } gDSRunState = YES; /* Set the time to the future */ gSeconds = timenow + 30; /* verify that we have a valid dir ref otherwise get a new one */ if ((*inDirRef == 0) || (dsVerifyDirRefNum(*inDirRef) != eDSNoErr)) { *inDirRef = 0; *inNodeRef = 0; /* Open DirectoryService */ status = dsOpenDirService(&dirRef); if (status != eDSNoErr) { #ifdef DSDEBUG LogErrStr(__LINE__, "cww", "dsOpenDirService failed", status); #endif gDSRunState = NO; return NO; } else { *inDirRef = dirRef; } } /* verify that we have a valid search node ref otherwise we get a new one */ if (*inNodeRef == 0) { /* Allocate the data buffer to be used here - 512 chars max for search node name */ pDataBuff = dsDataBufferAllocate(*inDirRef, 512); if (pDataBuff == NULL) { dsCloseDirService(*inDirRef); *inDirRef = 0; gDSRunState = NO; return NO; } /* Get the search node */ status = dsFindDirNodes(*inDirRef, pDataBuff, NULL, eDSSearchNodeName, &count, NULL); /* if error or expecting only one search node returned */ if ((status != eDSNoErr) || (count != 1)) { #ifdef DSDEBUG LogErrStr(__LINE__, "cww", "dsFindDirNodes can't get search node", status); #endif dsDataBufferDeAllocate(*inDirRef, pDataBuff); dsCloseDirService(*inDirRef); *inDirRef = 0; gDSRunState = NO; return NO; } /* Allocate the tDataList to retrieve the search node name */ pDataList = dsDataListAllocate(*inDirRef); if (pDataList == NULL) { dsDataBufferDeAllocate(*inDirRef, pDataBuff); dsCloseDirService(*inDirRef); *inDirRef = 0; gDSRunState = NO; return NO; } /* Now get the search node name so we can open it - index is one based */ status = dsGetDirNodeName(*inDirRef, pDataBuff, 1, &pDataList); if (status != eDSNoErr) { dsDataBufferDeAllocate(*inDirRef, pDataBuff); dsDataListDeallocate(*inDirRef, pDataList); free(pDataList); dsCloseDirService(*inDirRef); *inDirRef = 0; gDSRunState = NO; return NO; } /* Open the search node */ status = dsOpenDirNode(*inDirRef, pDataList, &nodeRef); dsDataListDeallocate(*inDirRef, pDataList); free(pDataList); if (status == eDSNoErr) { #ifdef DSDEBUG LogDbgStr(__LINE__, "cww", "Search node opened"); #endif *inNodeRef = nodeRef; } else { dsDataBufferDeAllocate(*inDirRef, pDataBuff); dsCloseDirService(*inDirRef); *inDirRef = 0; gDSRunState = NO; return NO; } /* Deallocate the temp buff */ dsDataBufferDeAllocate(*inDirRef, pDataBuff); } /* * if DS is running then we use it since in the case of NetInfo default DS can resolve aliases and * lookupd will have consulted NetInfo directly already and not found anything if it makes it here */ gDSRunState = YES; return YES; } - (void)dealloc { #ifdef DSDEBUG char errStr[256]; sprintf(errStr, "Deallocated DSAgent 0x%08x\n", (int)self); LogDbgStr(__LINE__, "alloc", errStr); #endif [super dealloc]; } - (DSAgent *)init { return (DSAgent *)[self initWithArg:NULL]; } - (LUAgent *)initWithArg:(char *)arg { #ifdef DSDEBUG char errStr[256]; #endif [super initWithArg:arg]; if (gSeconds == 0) gSeconds = time(NULL) - 1; #ifdef DSDEBUG sprintf(errStr, "Created new DSAgent: 0x%08x", (int)self); LogDbgStr(__LINE__, "alloc", errStr); #endif return self; } - (const char *)shortName { return("DS"); } - (LUDictionary *)stamp:(LUDictionary *)inDict { time_t now; char scratch[32]; if (inDict == NULL) return nil; now = time(NULL); sprintf(scratch, "%lu", now); [inDict setAgent:self]; [inDict setValue:"DirectoryServices" forKey:"_lookup_info_system"]; [inDict setValue:scratch forKey:"_lookup_DS_timestamp"]; return inDict; } - (void)stampAll:(LUArray *)inArray addToList:(LUArray *)inList { int i; int len; time_t now; char scratch[32]; LUDictionary *dictItem; if (inArray == NULL) return; now = time(NULL); sprintf(scratch, "%lu", now); len = [inArray count]; for (i = 0; i < len; i++) { dictItem = [inArray objectAtIndex:i]; [dictItem setAgent:self]; [dictItem setValue:"DirectoryServices" forKey:"_lookup_info_system"]; [dictItem setValue:scratch forKey:"_lookup_DS_timestamp"]; [inList addObject:dictItem]; } } - (BOOL)isValid:(LUDictionary *)inDict { time_t now = 0; time_t goodUntil = 0; if (inDict == NULL) return NO; goodUntil = [inDict unsignedLongForKey:"_lookup_DS_timestamp"]; /* might need to review this time for live values of the cache */ goodUntil += 600; now = time(0); if (now > goodUntil) return NO; return YES; } - (char *)mapDSAttrToNetInfoType:(const char *)inAttrType { int i = 0; char *outResult = NULL; unsigned long uiStdLen = strlen(kDSStdAttrTypePrefix); if (inAttrType == NULL) return NULL; if (strncmp(inAttrType, kDSStdAttrTypePrefix, uiStdLen) == 0) { for (i = 0; i < kAttrConsts; i++) { if (strcmp(inAttrType, sAttrMap[i][0]) == 0) { outResult = (char *)malloc(strlen(sAttrMap[i][1]) + 1); strcpy(outResult, sAttrMap[i][1]); break; } } } return outResult; } - (char *)mapNetInfoAttrToDSType:(const char *)inAttrType { char *outResult = NULL; int i = 0; if (inAttrType == NULL) return NULL; /* Look for a standard type */ for (i = 0; i < kAttrConsts; i++) { if (strcmp(inAttrType, sAttrMap[i][1]) == 0) { outResult = (char *)malloc(strlen(sAttrMap[i][0]) + 1); strcpy(outResult, sAttrMap[i][0]); break; } } return outResult; } - (char *)mapNetInfoRecToDSType:(LUCategory)inCategory { #ifdef DSDEBUG char errStr[4096]; #endif switch (inCategory) { case LUCategoryUser: return(kDSStdRecordTypeUsers); case LUCategoryGroup: return(kDSStdRecordTypeGroups); case LUCategoryInitgroups: return(kDSStdRecordTypeGroups); case LUCategoryMount: return(kDSStdRecordTypeMounts); case LUCategoryHost: case LUCategoryNetwork: case LUCategoryProtocol: case LUCategoryRpc: case LUCategoryPrinter: case LUCategoryBootparam: case LUCategoryBootp: case LUCategoryAlias: case LUCategoryNetgroup: case LUCategoryHostServices: default: #ifdef DSDEBUG sprintf(errStr, "*** Warning *** Unmapped category: %d", inCategory); LogDbgStr(__LINE__, "mapNetInfoRecToDSType", errStr); #endif return NULL; } return NULL; } - (LUDictionary *)dictFromDS:(tDirNodeReference)node buffer:(tDataBuffer *)buf index:(int)which { LUDictionary *item; tDirStatus status; tAttributeListRef attrListRef = 0; tRecordEntry *pRecEntry = NULL; tAttributeEntry *pAttrEntry = NULL; tAttributeValueListRef valueRef = 0; tAttributeValueEntry *pValueEntry = NULL; char *pNIKey = NULL; char **pValArray = NULL; int i, j, k; attrListRef = 0; pRecEntry = NULL; /* Using "which plus one" since DS indices for retrieval are one-based */ status = dsGetRecordEntry(node, buf, which+1, &attrListRef, &pRecEntry); if (status != eDSNoErr) return nil; item = [[LUDictionary alloc] init]; for (i = 0; i < pRecEntry->fRecordAttributeCount; i++) { pAttrEntry = NULL; valueRef = 0; status = dsGetAttributeEntry(node, buf, attrListRef, i+1, &valueRef, &pAttrEntry); /* Still try for the next attribute if this one fails */ if (status != eDSNoErr) continue; pNIKey = [self mapDSAttrToNetInfoType:pAttrEntry->fAttributeSignature.fBufferData]; if (pNIKey == NULL) { dsCloseAttributeValueList(valueRef); dsDeallocAttributeEntry(gDirRef, pAttrEntry); continue; } if (pAttrEntry->fAttributeValueCount == 0) { [item addValue:NULL forKey:pNIKey]; } else if (pAttrEntry->fAttributeValueCount == 1) { pValueEntry = NULL; status = dsGetAttributeValue(node, buf, 1, valueRef, &pValueEntry); if (status != eDSNoErr) { dsCloseAttributeValueList(valueRef); dsDeallocAttributeEntry(gDirRef, pAttrEntry); continue; } [item addValue:pValueEntry->fAttributeValueData.fBufferData forKey:pNIKey]; dsDeallocAttributeValueEntry(gDirRef, pValueEntry); } else if (pAttrEntry->fAttributeValueCount > 1) { pValArray = (char **)malloc(sizeof(char **) * (pAttrEntry->fAttributeValueCount + 1)); for (k=0, j = 0; j < pAttrEntry->fAttributeValueCount; j++) { pValueEntry = NULL; status = dsGetAttributeValue(node, buf, j+1, valueRef, &pValueEntry); if (status != eDSNoErr) continue; /* keep track of the values actually retrieved */ pValArray[k] = copyString(pValueEntry->fAttributeValueData.fBufferData); k++; dsDeallocAttributeValueEntry(gDirRef, pValueEntry); } pValArray[k] = NULL; [item addValues:pValArray forKey:pNIKey count:k]; for (j = 0; pValArray[j] != NULL; j++) free(pValArray[j]); free(pValArray); } free(pNIKey); pNIKey = NULL; dsCloseAttributeValueList(valueRef); dsDeallocAttributeEntry(gDirRef, pAttrEntry); } dsCloseAttributeList(attrListRef); dsDeallocRecordEntry(gDirRef, pRecEntry); return item; } - (LUDictionary *)itemWithKey:(char *)inKey value:(char *)inValue category:(LUCategory)inCategory { LUDictionary *item; tDirStatus status = eDSNoErr; char *pDSRecord = NULL; char *pDSKey = NULL; tDataList *pRecType = NULL; tDataNode *pAttrType = NULL; tDataNode *pValue = NULL; tDataBuffer *pDataBuffer = NULL; unsigned long count = 0; tContextData pContext = NULL; BOOL bFound = NO; Thread *pThread; #ifdef DSDEBUG char errStr[4096]; #endif pDSRecord = [self mapNetInfoRecToDSType:inCategory]; if (pDSRecord == NULL) return nil; pDSKey = [self mapNetInfoAttrToDSType:inKey]; if (pDSKey == NULL) return nil; if ([self canWeWork:&gDirRef withSearchNode:&gNodeRef] == NO) { /* pDSRecord is a constant that need not be freed */ if (pDSKey != NULL) { free(pDSKey); pDSKey = NULL; } return nil; } #ifdef DSDEBUG sprintf(errStr, "key = %s, val = %s, cat = %d", inKey, inValue, inCategory); LogDbgStr(__LINE__, "iwk", errStr); #endif /* We need this if we need to sit and spin */ pThread = [Thread currentThread]; [pThread setState:ThreadStateActive]; #ifdef DSDEBUG sprintf(errStr, "rec = %s, key = %s, val = %s", pDSRecord, pDSKey, inValue); LogDbgStr(__LINE__, "iwk", errStr); #endif pRecType = dsBuildListFromStrings(gDirRef, pDSRecord, NULL); if (pDSRecord != NULL) { /* DON'T free pDSRecord since it is a constant returned from mapNetInfoRecToDSType */ pDSRecord = NULL; } if (pRecType == NULL) return nil; pAttrType = dsDataNodeAllocateString(gDirRef, pDSKey); if (pDSKey != NULL) { free(pDSKey); pDSKey = NULL; } if (pAttrType == NULL) { dsDataListDeallocate(gDirRef, pRecType); free(pRecType); LogDbgStr(__LINE__, "iwk", "dsDoAttributeValueSearch() item _NOT_ found"); return nil; } pValue = dsDataNodeAllocateString(gDirRef, inValue); if (pValue == NULL) { dsDataNodeDeAllocate(gDirRef, pAttrType); dsDataListDeallocate(gDirRef, pRecType); free(pRecType); LogDbgStr(__LINE__, "iwk", "dsDoAttributeValueSearch() item _NOT_ found"); return nil; } if (status != eDSNoErr) { dsDataNodeDeAllocate(gDirRef, pAttrType); dsDataListDeallocate(gDirRef, pRecType); free(pRecType); LogDbgStr(__LINE__, "iwk", "dsDoAttributeValueSearch() item _NOT_ found"); return nil; } pDataBuffer = dsDataBufferAllocate(gDirRef, kBuffSize); if (pDataBuffer == NULL) { dsDataNodeDeAllocate(gDirRef, pAttrType); dsDataListDeallocate(gDirRef, pRecType); free(pRecType); LogDbgStr(__LINE__, "iwk", "dsDoAttributeValueSearch() item _NOT_ found"); return nil; } count = 0; do { status = dsDoAttributeValueSearch(gNodeRef, pDataBuffer, pRecType, pAttrType, eDSExact, pValue, &count, &pContext); /* If the node ref is invalid then reset it to zero */ if (status == eDSInvalidNodeRef) gNodeRef = 0; if (status == eDSNoErr) { if (count != 0) { #ifdef DSDEBUG sprintf(errStr, "dsDoAttributeValueSearch() returned - %lu - item(s)", count); LogDbgStr(__LINE__, "iwk", errStr); #endif bFound = YES; break; } } #ifdef DSDEBUG sprintf(errStr, "status = %u, pContext = %ld, count = %lu", status, (unsigned long)pContext, count); LogDbgStr(__LINE__, "iwk", errStr); #endif } while ((status == eDSNoErr) && (pContext != NULL) && (count == 0)); dsDataBufferDeAllocate(gDirRef, pValue); pValue = NULL; dsDataNodeDeAllocate(gDirRef, pAttrType); pAttrType = NULL; dsDataListDeallocate(gDirRef, pRecType); free(pRecType); pRecType = NULL; if (bFound == NO) { dsDataBufferDeAllocate(gDirRef, pDataBuffer); pDataBuffer = NULL; return nil; } item = [self dictFromDS:gNodeRef buffer:pDataBuffer index:0]; dsDataBufferDeAllocate(gDirRef, pDataBuffer); pDataBuffer = NULL; return [self stamp:item]; } - (LUArray *)allItemsWithCategory:(LUCategory)inCategory { tDirStatus status; char *pDSRecType = NULL; int i; unsigned long ulRecCount = 0; tDataList *pRecName = NULL; tDataList *pRecType = NULL; tDataList *pAttrType = NULL; tContextData pContext = NULL; LUDictionary *item; LUDictionary *vstamp; LUArray *list = nil; BOOL bFirstFound = NO; tDataBuffer *pDataBuffer = NULL; pDSRecType = [self mapNetInfoRecToDSType:inCategory]; if (pDSRecType == NULL) return nil; if ([self canWeWork:&gDirRef withSearchNode:&gNodeRef] == NO) return nil; /* Get all records */ pRecName = dsBuildListFromStrings(gDirRef, kDSRecordsAll, NULL); if (pRecName == NULL) return nil; /* And all attributes */ pAttrType = dsBuildListFromStrings(gDirRef, kDSAttributesStandardAll, NULL); if (pAttrType == NULL) { dsDataListDeallocate(gDirRef, pRecName); free(pRecName); return nil; } /* Of this type */ pRecType = dsBuildListFromStrings(gDirRef, pDSRecType, NULL); if (pDSRecType != NULL) { /* DON'T free this since it is a constant returned from mapNetInfoRecToDSType */ pDSRecType = NULL; } if (pRecType == NULL) { dsDataListDeallocate(gDirRef, pAttrType); free(pAttrType); dsDataListDeallocate(gDirRef, pRecName); free(pRecName); return nil; } pDataBuffer = dsDataBufferAllocate(gDirRef, kBuffSize); if (pDataBuffer == NULL) { dsDataListDeallocate(gDirRef, pAttrType); free(pAttrType); dsDataListDeallocate(gDirRef, pRecName); free(pRecName); dsDataListDeallocate(gDirRef, pRecType); free(pRecType); return nil; } do { status = dsGetRecordList(gNodeRef, pDataBuffer, pRecName, eDSExact, pRecType, pAttrType, NO, &ulRecCount, &pContext); /* If the node ref is invalid then reset it to zero */ if (status == eDSInvalidNodeRef) gNodeRef = 0; /* Check for error and at least one record found. */ if ((status == eDSNoErr) && (ulRecCount != 0)) { if (bFirstFound == NO) { list = [[LUArray alloc] init]; vstamp = [[LUDictionary alloc] init]; [self stamp:vstamp]; [list addValidationStamp:vstamp]; [vstamp release]; bFirstFound = YES; } for (i = 0; i < ulRecCount; i++) { item = [self dictFromDS:gNodeRef buffer:pDataBuffer index:i]; if (item != nil) { [list addObject:item]; [item release]; } } } } while ((status == eDSNoErr) && (pContext != NULL)); dsDataListDeallocate(gDirRef, pRecType); free(pRecType); dsDataListDeallocate(gDirRef, pAttrType); free(pAttrType); dsDataListDeallocate(gDirRef, pRecName); free(pRecName); dsDataBufferDeAllocate(gDirRef, pDataBuffer); pDataBuffer = NULL; if ([list count] == 0) { [list release]; return nil; } return list; } @end #endif _ENABLE_DSAGENT_