/*

Copyright (c) 1992  Microsoft Corporation

Module Name:

	nwtrash.c

Abstract:

	This module contains the routines for performing Network Trash Folder
	operations.

Author:

	Sue Adams (microsoft!suea)


Revision History:
	06 Aug 1992		Initial Version

Notes:	Tab stop: 4
--*/

#define	NWTRASH_LOCALS
#define	FILENUM	FILE_NWTRASH

#include <afp.h>
#include <fdparm.h>
#include <pathmap.h>
#include <nwtrash.h>
#include <afpinfo.h>
#include <access.h>

#ifdef ALLOC_PRAGMA
#pragma alloc_text( PAGE, AfpCreateNetworkTrash)
#pragma alloc_text( PAGE, AfpDeleteNetworkTrash)
#pragma alloc_text( PAGE, afpCleanNetworkTrash)
#pragma alloc_text( PAGE, AfpWalkDirectoryTree)
#pragma alloc_text( PAGE, afpPushDirNode)
#pragma alloc_text( PAGE, afpPopDirNode)
#pragma alloc_text( PAGE, AfpGetNextDirectoryInfo)
#pragma alloc_text( PAGE, afpNwtDeleteFileEntity)
#endif

/***	AfpCreateNetworkTrash
 *
 *	Create the network trash folder for a newly added volume.
 *  Make sure it is hidden and make sure the streams are intact.
 *  This routine may only be called for NTFS volumes.  Note that even
 *  ReadOnly NTFS volumes will have a trash created.  This is because
 *  if someone is going to toggle the volume ReadOnly bit, we don't need
 *  to worry about creating/deleting the trash on the fly.
 *  We keep an open handle to the network trash stored in the volume
 *  descriptor so that nobody can come in behind our backs and delete
 *  it.
 */
NTSTATUS
AfpCreateNetworkTrash(
	IN	PVOLDESC	pVolDesc
)
{
	FILESYSHANDLE	hNWT;
	PDFENTRY		pDfEntry;
	NTSTATUS 		Status;
	ULONG	 		info, Attr;
	AFPINFO			afpInfo;
	BOOLEAN			ReleaseSwmr = False;
	PISECURITY_DESCRIPTOR pSecDesc;
	FILEDIRPARM		fdparm;		// This is used to set the hidden attribute
								// of the FinderInfo for network trash folder

	PAGED_CODE( );

	DBGPRINT(DBG_COMP_IDINDEX,DBG_LEVEL_INFO,("\tCreating Network Trash...\n"));
	ASSERT(pVolDesc->vds_Flags & VOLUME_NTFS);

	hNWT.fsh_FileHandle = NULL;

	Status = AfpMakeSecurityDescriptorForUser(&AfpSidWorld, &AfpSidWorld, &pSecDesc);

	if (!NT_SUCCESS(Status))
		return Status;

	ASSERT (pSecDesc != NULL);
	ASSERT (pSecDesc->Dacl != NULL);

	do
	{
		// NOTE: NTFS allows me to open a Readonly directory for
		// delete access.
		Status = AfpIoCreate(&pVolDesc->vds_hRootDir,
							 AFP_STREAM_DATA,
							 &AfpNetworkTrashNameU,
							 AFP_NWT_ACCESS | AFP_OWNER_ACCESS,
							 AFP_NWT_SHAREMODE,
							 AFP_NWT_OPTIONS,
							 AFP_NWT_DISPOSITION,
							 AFP_NWT_ATTRIBS, // makes it hidden
							 False,
							 pSecDesc,
							 &hNWT,
							 &info,
							 NULL,
							 NULL,
							 NULL);

		// Free the memory allocated for the security descriptor
		AfpFreeMemory(pSecDesc->Dacl);
		AfpFreeMemory(pSecDesc);

		if (!NT_SUCCESS(Status))
			break;

		ASSERT(info == FILE_CREATED);

		// Add the AfpInfo stream
		Status = AfpSlapOnAfpInfoStream(NULL,
										NULL,
										&hNWT,
										NULL,
										AFP_ID_NETWORK_TRASH,
										True,
										NULL,
										&afpInfo);
		if (!NT_SUCCESS(Status))
			break;

		// it does not exist in the ID index database, add it
		AfpSwmrAcquireExclusive(&pVolDesc->vds_IdDbAccessLock);
		ReleaseSwmr = True;

		ASSERT(pVolDesc->vds_pDfeRoot != NULL);

		pDfEntry = AfpAddDfEntry(pVolDesc,
								 pVolDesc->vds_pDfeRoot,
								 &AfpNetworkTrashNameU,
								 True,
								 AFP_ID_NETWORK_TRASH);

		if (pDfEntry == NULL)
		{
			Status = STATUS_UNSUCCESSFUL;
			break;
		}

		// NOTE: pDfEntry now points to the "Network Trash Folder"

		// Get directory info to cache
		Status = AfpIoQueryTimesnAttr(&hNWT,
									  &pDfEntry->dfe_CreateTime,
									  &pDfEntry->dfe_LastModTime,
									  &Attr);
	
		ASSERT(NT_SUCCESS(Status));

		ASSERT(Attr & FILE_ATTRIBUTE_HIDDEN);
		pDfEntry->dfe_NtAttr = (USHORT)Attr & FILE_ATTRIBUTE_VALID_FLAGS;
		pDfEntry->dfe_AfpAttr = afpInfo.afpi_Attributes;
		pDfEntry->dfe_FinderInfo = afpInfo.afpi_FinderInfo;
		pDfEntry->dfe_BackupTime = afpInfo.afpi_BackupTime;
		DFE_OWNER_ACCESS(pDfEntry) = (DIR_ACCESS_SEARCH | DIR_ACCESS_READ);
		DFE_GROUP_ACCESS(pDfEntry) = (DIR_ACCESS_SEARCH | DIR_ACCESS_READ);
		DFE_WORLD_ACCESS(pDfEntry) = (DIR_ACCESS_SEARCH | DIR_ACCESS_READ);

		// ok, we know it now exists both on disk and in the database
		if (NT_SUCCESS(Status))
		{
			RtlZeroMemory(&fdparm, sizeof(fdparm));
			fdparm._fdp_Flags = DFE_FLAGS_DIR;
			fdparm._fdp_AfpId = AFP_ID_NETWORK_TRASH;
			fdparm._fdp_FinderInfo = afpInfo.afpi_FinderInfo;

			// We must set the invisible flag in the finder info, because
			// System 6 seems to ignore the hidden attribute.
			pDfEntry->dfe_FinderInfo.fd_Attr1 |= FINDER_FLAG_INVISIBLE;
			fdparm._fdp_FinderInfo.fd_Attr1 |= FINDER_FLAG_INVISIBLE;
			Status = AfpSetAfpInfo(&hNWT,
								   FD_BITMAP_FINDERINFO,
								   &fdparm,
								   NULL,
								   NULL);
		}
	} while (False);

	if (hNWT.fsh_FileHandle != NULL)
		AfpIoClose(&hNWT);

	if (!NT_SUCCESS(Status))
	{
		AFPLOG_ERROR(AFPSRVMSG_CREATE_NWTRASH,
					 Status,
					 NULL,
					 0,
					 &pVolDesc->vds_Name);
	}
	else
	{
		// Open a Network Trash handle to keep around so that no one can
		// come in and delete the Network Trash dir out from under us
		Status = AfpIoOpen(&pVolDesc->vds_hRootDir,
						   AFP_STREAM_DATA,
						   AFP_NWT_OPTIONS,
						   &AfpNetworkTrashNameU,
						   FILEIO_ACCESS_READ,
						   AFP_NWT_SHAREMODE,
						   False,
						   &pVolDesc->vds_hNWT);
	
		if (!NT_SUCCESS(Status))
		{
			AFPLOG_ERROR(AFPSRVMSG_CREATE_NWTRASH, Status, NULL, 0,
						 &pVolDesc->vds_Name);
		}
	}

	if (ReleaseSwmr)
	{
		AfpSwmrRelease(&pVolDesc->vds_IdDbAccessLock);
	}

	return Status;
}

/*** AfpDeleteNetworkTrash
 *
 *	Delete the network trash folder from disk when a volume is being added,
 *  deleted or stopped.
 *
 * 	NOTE: this must be called in the server's context to ensure that we have
 * 	LOCAL_SYSTEM access to all the trash directories created by users
 */
NTSTATUS
AfpDeleteNetworkTrash(
	IN	PVOLDESC	pVolDesc,
	IN	BOOLEAN		VolumeStart 	// Is volume starting or is it stopping
)
{
	FILESYSHANDLE	hNWT;
	NTSTATUS 		Status;

	PAGED_CODE( );

	DBGPRINT(DBG_COMP_IDINDEX, DBG_LEVEL_INFO,
										("\tDeleting Network Trash...\n") );
	ASSERT(pVolDesc->vds_Flags & VOLUME_NTFS);

	if (!VolumeStart)
	{
		// Close the handle to Network Trash that we keep open so PC users can't
		// delete the directory out from under us.
		if (pVolDesc->vds_hNWT.fsh_FileHandle != NULL)
		{
			AfpIoClose(&pVolDesc->vds_hNWT);
			pVolDesc->vds_hNWT.fsh_FileHandle = NULL;
		}
	}

	do
	{
		AfpSwmrAcquireExclusive(&pVolDesc->vds_IdDbAccessLock);

		// Open for delete access
		Status = AfpIoOpen(&pVolDesc->vds_hRootDir,
						   AFP_STREAM_DATA,
						   AFP_NWT_OPTIONS,
						   &AfpNetworkTrashNameU,
						   AFP_NWT_ACCESS,
						   AFP_NWT_SHAREMODE,
						   False,
						   &hNWT);
		// there is no network trash folder to delete
		if (Status == STATUS_OBJECT_NAME_NOT_FOUND)
		{
			Status = STATUS_SUCCESS;
			break;
		}
	
		if (NT_SUCCESS(Status))
		{
			Status = afpCleanNetworkTrash(pVolDesc, &hNWT, NULL);
	
			if (NT_SUCCESS(Status) || !VolumeStart)
			{
	
				// NOTE: NTFS will allow me to open the directory for
				// DELETE access if it is marked Readonly, but I cannot delete it.
				// Clear the Readonly Bit on the Network Trash Folder
				AfpIoSetTimesnAttr(&hNWT,
								   NULL,
								   NULL,
								   0,
								   FILE_ATTRIBUTE_READONLY,
								   NULL,
								   NULL);
				Status = AfpIoMarkFileForDelete(&hNWT,
												NULL,
												NULL,
												NULL);
		
			}
	
			AfpIoClose(&hNWT);
		}
	} while (False);

	AfpSwmrRelease(&pVolDesc->vds_IdDbAccessLock);

	if ((!NT_SUCCESS(Status)) && (Status != STATUS_OBJECT_NAME_NOT_FOUND))
	{
		AFPLOG_ERROR(AFPSRVMSG_DELETE_NWTRASH,
					 Status,
					 NULL,
					 0,
					 &pVolDesc->vds_Name);
		Status = STATUS_UNSUCCESSFUL;
	}

	return Status;
}

/*** afpCleanNetworkTrash
 *
 *	Delete the contents of the network trash folder referenced by hNWT.
 *  If pDfeNWT is non-null, then delete the file/dir entries from the IdIndex
 *  database.  If pDfeNWT is null, the volume is being deleted and the
 *  IdIndex database is getting blown away too, so don't bother removing the
 *  entries.
 */
LOCAL
NTSTATUS
afpCleanNetworkTrash(
	IN	PVOLDESC		pVolDesc,
	IN	PFILESYSHANDLE	phNWT,
	IN	PDFENTRY		pDfeNWT OPTIONAL
)
{
	NTSTATUS Status;

	PAGED_CODE( );

	DBGPRINT(DBG_COMP_IDINDEX,DBG_LEVEL_INFO,("afpCleanNetworkTrash entered\n"));

	if (pDfeNWT != NULL)
	{
		ASSERT(pDfeNWT->dfe_AfpId == AFP_ID_NETWORK_TRASH);
		// clean out all child DFEntries belonging to the network trash
		AfpPruneIdDb(pVolDesc,pDfeNWT);
	}

	Status = AfpWalkDirectoryTree(phNWT, afpNwtDeleteFileEntity);

	return Status;
}

NTSTATUS
AfpWalkDirectoryTree(
	IN	PFILESYSHANDLE	phTargetDir,
	IN	WALKDIR_WORKER	NodeWorker
)
{
	PFILE_DIRECTORY_INFORMATION	tmpptr;
	PWALKDIR_NODE	DirNodeStacktop = NULL, pcurrentnode;
	NTSTATUS		rc, status = STATUS_SUCCESS;
	PBYTE			enumbuf;
	PWCHAR			nodename;
	ULONG			nodenamelen;
	BOOLEAN			isdir;
	UNICODE_STRING	udirname;

	PAGED_CODE( );

	//
	// allocate the buffer that will hold enumerated files and dirs
	//
	if ((enumbuf = (PBYTE)AfpAllocPANonPagedMemory(AFP_ENUMBUF_SIZE)) == NULL)
	{
		return STATUS_INSUFFICIENT_RESOURCES;
	}

	do	// error handling loop
	{
		//
		// prime the pump with the top level (target dir) directory handle
		//
		if ((rc = afpPushDirNode(&DirNodeStacktop, NULL, NULL))
															!= STATUS_SUCCESS)
		{
			status = rc;
			break;
		}
		else
		{
			DirNodeStacktop->wdn_Handle = *phTargetDir;
		}

		//
		// keep popping enumerated directories off the stack until stack empty
		//
		while ((pcurrentnode = DirNodeStacktop) != NULL)
		{
			if (pcurrentnode->wdn_Enumerated == False)
			{
				//
				// get a handle to the directory so it can be enumerated
				//
				if (pcurrentnode->wdn_Handle.fsh_FileHandle == NULL)
				{
					RtlInitUnicodeString(&udirname,
										 pcurrentnode->wdn_RelativePath.Buffer);
					// open a handle to the thing relative to the phTargetDir
					rc = AfpIoOpen(phTargetDir,
								 AFP_STREAM_DATA,
								 FILEIO_OPEN_DIR,
								 &udirname,
								 FILEIO_ACCESS_READ,
								 FILEIO_DENY_NONE,
								 False,
								 &pcurrentnode->wdn_Handle);

					if (!NT_SUCCESS(rc))
					{
						status = rc;
						break;
					}
				}

				//
				// keep enumerating till we get all the entries
				//
				while (True)
				{
					rc = AfpIoQueryDirectoryFile(&pcurrentnode->wdn_Handle,
												 (PFILE_DIRECTORY_INFORMATION)enumbuf,
												 AFP_ENUMBUF_SIZE,
												 FileDirectoryInformation,
												 False,	// return multiple entries
												 False,	// don't restart scan
												 NULL);

					ASSERT(rc != STATUS_PENDING);

					if ((rc == STATUS_NO_MORE_FILES) ||
						(rc == STATUS_NO_SUCH_FILE))
					{
						pcurrentnode->wdn_Enumerated = True;
						break; // that's it, we've seen everything there is
					}
					//
					// NOTE: if we get STATUS_BUFFER_OVERFLOW, the IO status
					// information field does NOT tell us the required size
					// of the buffer, so we wouldn't know how big to realloc
					// the enum buffer if we wanted to retry, so don't bother
					else if (!NT_SUCCESS(rc))
					{
						status = rc;
						break;	// enumerate failed, bail out
					}

					//
					// process the enumerated files and dirs
					//
					tmpptr = (PFILE_DIRECTORY_INFORMATION)enumbuf;
					while (True)
					{
						rc = AfpGetNextDirectoryInfo(&tmpptr,
													 &nodename,
													 &nodenamelen,
													 &isdir);

						if (rc == STATUS_NO_MORE_ENTRIES)
						{
							break;
						}

						if (isdir == True)
						{
							AfpInitUnicodeStringWithNonNullTerm(&udirname,
													   (USHORT)nodenamelen,
													   nodename);

							if (RtlEqualUnicodeString(&Dot,&udirname,False) ||
								RtlEqualUnicodeString(&DotDot,&udirname,False))
							{
								continue;
							}

							//
							// push it onto the dir node stack
							//
							rc = afpPushDirNode(&DirNodeStacktop,
												&pcurrentnode->wdn_RelativePath,
												&udirname);
							if (rc != STATUS_SUCCESS)
							{
								status = rc;
								break;
							}
						}
						else
						{
							//
							// its a file, call worker with its relative handle
							// and path
							//
							rc = NodeWorker(&pcurrentnode->wdn_Handle,
											nodename,
											nodenamelen,
											False);
							if (!NT_SUCCESS(rc))
							{
								status = rc;
								break;
							}
						}

					} // while more entries in the enumbuf


					if (!NT_SUCCESS(status))
					{
						break;
					}

				} // while there are more files to enumerate

				if (pcurrentnode->wdn_Handle.fsh_FileHandle != phTargetDir->fsh_FileHandle)
				{
					AfpIoClose(&pcurrentnode->wdn_Handle);
				}
			}
			else	// we have already enumerated this directory
			{
				if (pcurrentnode->wdn_RelativePath.Length != 0)
				{
					// call the worker routine on this directory node
					rc = NodeWorker(phTargetDir,
									pcurrentnode->wdn_RelativePath.Buffer,
									pcurrentnode->wdn_RelativePath.Length,
									True);
				}
				else rc = STATUS_SUCCESS;

				afpPopDirNode(&DirNodeStacktop);
				if (!NT_SUCCESS(rc))
				{
					status = rc;
					break;
				}

			}


			if (!NT_SUCCESS(status))
			{
				break;
			}

		} // while there are directories to pop

	} while (False); // error handling loop

	while (DirNodeStacktop != NULL)
	{
		DBGPRINT(DBG_COMP_IDINDEX, DBG_LEVEL_WARN,
				 ("AfpWalkDirectoryTree: WARNING: cleaning up dir stack\n") );
		// clean up in case of error
		afpPopDirNode(&DirNodeStacktop);
	}
	AfpFreePANonPagedMemory(enumbuf, AFP_ENUMBUF_SIZE);
	return status;
}

/***	afpPushDirNode
 *
 *	Keep a record of all the directories we have encountered so far during
 *  enumeration of the tree.  We need to process directories from the
 *  bottom up because the WalkTree node worker routine does a delete
 *	on all the items in a tree, and we certainly cant be deleting directories that
 *  are not empty.
 *
 */
LOCAL
NTSTATUS
afpPushDirNode(
	IN OUT	PWALKDIR_NODE	*ppStacktop,
	IN		PUNICODE_STRING pParentPath,	// path to parent (NULL iff walk root)
	IN		PUNICODE_STRING	pDirName		// name of current node directory
)
{
	PWALKDIR_NODE	tempptr;
	UNICODE_STRING	ubackslash;
	ULONG			memsize;
	USHORT			namesize = 0;

	PAGED_CODE( );

	if (pParentPath != NULL)
	{
		namesize = pParentPath->Length + sizeof(WCHAR) +
				   pDirName->Length + sizeof(UNICODE_NULL);
	}
	memsize = namesize + sizeof(WALKDIR_NODE);

	if ((tempptr = (PWALKDIR_NODE)AfpAllocNonPagedMemory(memsize)) == NULL)
	{
		return STATUS_INSUFFICIENT_RESOURCES;
	}

	tempptr->wdn_Enumerated = False;
	tempptr->wdn_Handle.fsh_FileHandle = NULL;
	tempptr->wdn_RelativePath.Length = 0;
	tempptr->wdn_RelativePath.MaximumLength = namesize;

	if (pParentPath != NULL)
	{
		tempptr->wdn_RelativePath.Buffer = (LPWSTR)((PBYTE)tempptr +
														  sizeof(WALKDIR_NODE));
		if (pParentPath->Length != 0)
		{
			RtlInitUnicodeString(&ubackslash,L"\\");
			AfpCopyUnicodeString(&tempptr->wdn_RelativePath,pParentPath);
			RtlAppendUnicodeStringToString(&tempptr->wdn_RelativePath,
										   &ubackslash);
		}

		RtlAppendUnicodeStringToString(&tempptr->wdn_RelativePath,pDirName);
		tempptr->wdn_RelativePath.Buffer[tempptr->wdn_RelativePath.Length/sizeof(WCHAR)] = UNICODE_NULL;

	}
	else
	{
		tempptr->wdn_RelativePath.Buffer = NULL;
	}

	// push it on the stack
	tempptr->wdn_Next = *ppStacktop;
	*ppStacktop = tempptr;

	return STATUS_SUCCESS;
}

/*** afpPopDirNode
 *
 * Pop the top DirNode off of the stack and free it
 *
 ***/
LOCAL
VOID
afpPopDirNode(
	IN OUT PWALKDIR_NODE	*ppStackTop
)
{
	PWALKDIR_NODE	tempptr;

	PAGED_CODE( );

	ASSERT(*ppStackTop != NULL);

	tempptr = (*ppStackTop)->wdn_Next;
	AfpFreeMemory(*ppStackTop);
	*ppStackTop = tempptr;

}

/***	AfpGetNextDirectoryInfo
 *
 * Given a buffer full of FILE_DIRECTORY_INFORMATION entries as returned
 * from a directory enumerate, find the next structure in the buffer and
 * return the name information out of it, and whether or not the item
 * is a file or directory. Also update the ppInfoBuf to point to the next
 * available entry to return for the next time this routine is called.
 *
 */
NTSTATUS
AfpGetNextDirectoryInfo(
	IN OUT	PFILE_DIRECTORY_INFORMATION	*ppInfoBuf,
	OUT		PWCHAR		*pNodeName,
	OUT		PULONG		pNodeNameLen,
	OUT		PBOOLEAN	pIsDir
)
{
	PFILE_DIRECTORY_INFORMATION		tempdirinfo;

	PAGED_CODE( );

	if (*ppInfoBuf == NULL)
	{
		return STATUS_NO_MORE_ENTRIES;
	}

	tempdirinfo = *ppInfoBuf;
	if (tempdirinfo->NextEntryOffset == 0)
	{
		*ppInfoBuf = NULL;
	}
	else
	{
		(PBYTE)*ppInfoBuf += tempdirinfo->NextEntryOffset;
	}

	*pIsDir = (tempdirinfo->FileAttributes & FILE_ATTRIBUTE_DIRECTORY) ?
																True : False;
	*pNodeNameLen = tempdirinfo->FileNameLength;
	*pNodeName = tempdirinfo->FileName;

	return STATUS_SUCCESS;
}

/***	afpNwtDeleteFileEntity
 *
 * Delete a file or directory opening it with the name relative to phRelative
 * handle.
 * NOTE: can we use NtDeleteFile here since we dont really care about
 * any security checking?  Then we wouldn't even have to open a handle,
 * although that routine opens one for DELETE_ON_CLOSE for us, then
 * closes it.
 */
LOCAL
NTSTATUS
afpNwtDeleteFileEntity(
	IN	PFILESYSHANDLE	phRelative,
	IN	PWCHAR			Name,
	IN	ULONG			Namelen,
	IN 	BOOLEAN			IsDir
)
{
	ULONG			OpenOptions;
	FILESYSHANDLE	hEntity;
	NTSTATUS		rc;
	UNICODE_STRING	uname;

	PAGED_CODE( );

	DBGPRINT(DBG_COMP_IDINDEX, DBG_LEVEL_INFO,
									("\nafpNwtDeleteFileEntity entered\n"));

	OpenOptions = IsDir ? FILEIO_OPEN_DIR : FILEIO_OPEN_FILE;

	AfpInitUnicodeStringWithNonNullTerm(&uname,(USHORT)Namelen,Name);
	rc = AfpIoOpen(phRelative,
				   AFP_STREAM_DATA,
				   OpenOptions,
				   &uname,
				   FILEIO_ACCESS_DELETE,
				   FILEIO_DENY_ALL,
				   False,
				   &hEntity);

	if (!NT_SUCCESS(rc))
	{
		return rc;
	}

	rc = AfpIoMarkFileForDelete(&hEntity, NULL, NULL, NULL);

	if (!NT_SUCCESS(rc))
	{
		// If the file is marked readonly, try clearing the RO attribute
		if (((rc == STATUS_ACCESS_DENIED) || (rc == STATUS_CANNOT_DELETE)) &&
			(NT_SUCCESS(AfpIoSetTimesnAttr(&hEntity,
										   NULL,
										   NULL,
										   0,
										   FILE_ATTRIBUTE_READONLY,
										   NULL,
										   NULL))))

		{
			rc = AfpIoMarkFileForDelete(&hEntity, NULL, NULL, NULL);
		}
		if (!NT_SUCCESS(rc))
		{
			DBGPRINT(DBG_COMP_IDINDEX, DBG_LEVEL_ERR,
			("\nafpNwtDeleteFileEntity: could not delete file/dir (rc=0x%lx)\n",rc));
			DBGBRK(DBG_LEVEL_ERR);
		}
	}
	// NOTE: if marking it for delete fails, at least we could try deleting
	// the afpId stream so that we wouldn't find it at some future point...

	AfpIoClose(&hEntity);

	return rc;
}