VHDHDDCore.cpp revision c4416f84c6e2ffa9549354c4d196858b455500b3
/* $Id$ */
/** @file
* VHD Disk image, Core Code.
*/
/*
* Copyright (C) 2006-2010 Oracle Corporation
*
* This file is part of VirtualBox Open Source Edition (OSE), as
* available from http://www.virtualbox.org. This file is free software;
* General Public License (GPL) as published by the Free Software
* Foundation, in version 2 as it comes in the "COPYING" file of the
* VirtualBox OSE distribution. VirtualBox OSE is distributed in the
* hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
*/
/*******************************************************************************
* Header Files *
*******************************************************************************/
#define LOG_GROUP LOG_GROUP_VD_VHD
#include <VBox/VBoxHDD-Plugin.h>
#define VHD_RELATIVE_MAX_PATH 512
#define VHD_ABSOLUTE_MAX_PATH 512
#define VHD_SECTOR_SIZE 512
/* This is common to all VHD disk types and is located at the end of the image */
#pragma pack(1)
typedef struct VHDFooter
{
char Cookie[8];
char UniqueID[16];
} VHDFooter;
#pragma pack()
#define VHD_FOOTER_COOKIE "conectix"
#define VHD_FOOTER_COOKIE_SIZE 8
#define VHD_FOOTER_FEATURES_NOT_ENABLED 0
#define VHD_FOOTER_FEATURES_TEMPORARY 1
#define VHD_FOOTER_FEATURES_RESERVED 2
#define VHD_FOOTER_FILE_FORMAT_VERSION 0x00010000
#define VHD_FOOTER_DISK_TYPE_FIXED 2
#define VHD_FOOTER_DISK_TYPE_DYNAMIC 3
#define VHD_FOOTER_DISK_TYPE_DIFFERENCING 4
#define VHD_MAX_LOCATOR_ENTRIES 8
#define VHD_PLATFORM_CODE_NONE 0
#define VHD_PLATFORM_CODE_WI2R 0x57693272
#define VHD_PLATFORM_CODE_WI2K 0x5769326B
#define VHD_PLATFORM_CODE_W2RU 0x57327275
#define VHD_PLATFORM_CODE_W2KU 0x57326B75
#define VHD_PLATFORM_CODE_MAC 0x4D163220
#define VHD_PLATFORM_CODE_MACX 0x4D163258
/* Header for expanding disk images. */
#pragma pack(1)
typedef struct VHDParentLocatorEntry
{
typedef struct VHDDynamicDiskHeader
{
char Cookie[8];
#pragma pack()
#define VHD_DYNAMIC_DISK_HEADER_COOKIE "cxsparse"
#define VHD_DYNAMIC_DISK_HEADER_COOKIE_SIZE 8
#define VHD_DYNAMIC_DISK_HEADER_VERSION 0x00010000
/**
* Complete VHD image data structure.
*/
typedef struct VHDIMAGE
{
/** Image file name. */
const char *pszFilename;
/** Opaque storage handle. */
/** I/O interface. */
/** I/O interface callbacks. */
/** Pointer to the per-disk VD interface list. */
/** Pointer to the per-image VD interface list. */
/** Error interface. */
/** Error interface callback table. */
/** Open flags passed by VBoxHDD layer. */
unsigned uOpenFlags;
/** Image flags defined during creation or determined during open. */
unsigned uImageFlags;
/** Total size of the image. */
/** Physical geometry of this image. */
/** Logical geometry of this image. */
/** Image UUID. */
/** Parent image UUID. */
/** Parent's time stamp at the time of image creation. */
/** Relative path to the parent image. */
char *pszParentFilename;
/** The Block Allocation Table. */
/** Number of entries in the table. */
/** Size of one data block. */
/** Sectors per data block. */
/** Length of the sector bitmap in bytes. */
/** A copy of the disk footer. */
/** Current end offset of the file (without the disk footer). */
/** Size of the data block bitmap in sectors. */
/** Start of the block allocation table. */
/** Buffer to hold block's bitmap for bit search operations. */
/** Offset to the next data structure (dynamic disk header). */
/** Flag to force dynamic disk header update. */
bool fDynHdrNeedsUpdate;
/**
* Structure tracking the expansion process of the image
* for async access.
*/
typedef struct VHDIMAGEEXPAND
{
/** Flag indicating the status of each step. */
/** The index in the block allocation table which is written. */
/** Big endian representation of the block index
* which is written in the BAT. */
/** Old end of the file - used for rollback in case of an error. */
/** Sector bitmap written to the new block - variable in size. */
/**
* Flag defines
*/
#define VHDIMAGEEXPAND_STEP_IN_PROGRESS (0x0)
#define VHDIMAGEEXPAND_STEP_FAILED (0x2)
#define VHDIMAGEEXPAND_STEP_SUCCESS (0x3)
/** All steps completed successfully. */
#define VHDIMAGEEXPAND_ALL_SUCCESS (0xff)
/** All steps completed (no success indicator) */
#define VHDIMAGEEXPAND_ALL_COMPLETE (0xaa)
/** Every status field has 2 bits so we can encode 4 steps in one byte. */
#define VHDIMAGEEXPAND_STATUS_MASK 0x03
#define VHDIMAGEEXPAND_BLOCKBITMAP_STATUS_SHIFT 0x00
#define VHDIMAGEEXPAND_USERBLOCK_STATUS_SHIFT 0x02
#define VHDIMAGEEXPAND_FOOTER_STATUS_SHIFT 0x04
#define VHDIMAGEEXPAND_BAT_STATUS_SHIFT 0x06
/**
* Helper macros to get and set the status field.
*/
/*******************************************************************************
* Static Variables *
*******************************************************************************/
/** NULL-terminated array of supported file extensions. */
static const char *const s_apszVhdFileExtensions[] =
{
"vhd",
};
/*******************************************************************************
* Internal Functions *
*******************************************************************************/
/**
* Internal: signal an error to the frontend.
*/
const char *pszFormat, ...)
{
return rc;
}
/**
* Internal: signal an informational message to the frontend.
*/
{
int rc = VINF_SUCCESS;
return rc;
}
{
}
{
}
{
}
{
}
{
}
const char *pszFilename,
{
}
{
}
{
}
{
}
{
}
{
}
{
cbRead);
}
void *pvCompleteUser)
{
}
void *pvCompleteUser)
{
}
void *pvCompleteUser)
{
}
void *pvCompleteUser)
{
}
{
}
/**
* Internal: Compute and update header checksum.
*/
{
return ~checksum;
}
/**
* Internal: Convert filename to UTF16 with appropriate endianness.
*/
bool fBigEndian)
{
int rc;
if (RT_FAILURE(rc))
goto out;
{
goto out;
}
if (fBigEndian)
for (unsigned i = 0; i < cTmp16Len; i++)
else
if (pcbActualSize)
out:
if (tmp16)
return rc;
}
/**
* Internal: Update one locator entry.
*/
{
int rc;
char *pszTmp;
if (!pvBuf)
{
rc = VERR_NO_MEMORY;
goto out;
}
{
case VHD_PLATFORM_CODE_WI2R:
/* Update plain relative name. */
{
goto out;
}
break;
case VHD_PLATFORM_CODE_WI2K:
/* Update plain absolute name. */
if (RT_FAILURE(rc))
goto out;
break;
case VHD_PLATFORM_CODE_W2RU:
/* Update unicode relative name. */
if (RT_FAILURE(rc))
goto out;
break;
case VHD_PLATFORM_CODE_W2KU:
/* Update unicode absolute name. */
if (!pszTmp)
{
rc = VERR_NO_MEMORY;
goto out;
}
if (RT_FAILURE(rc))
{
goto out;
}
if (RT_FAILURE(rc))
goto out;
break;
default:
goto out;
}
NULL);
out:
if (pvBuf)
return rc;
}
/**
* Internal: Update dynamic disk header from VHDIMAGE.
*/
{
int rc, i;
if (!pImage)
return VERR_VD_NOT_OPENED;
if (RT_FAILURE(rc))
return rc;
return VERR_VD_VHD_INVALID_HEADER;
return VERR_VD_VHD_INVALID_HEADER;
/* Update parent's timestamp. */
/* Update parent's filename. */
if (pImage->pszParentFilename)
{
if (RT_FAILURE(rc))
return rc;
}
/* Update parent's locators. */
for (i = 0; i < VHD_MAX_LOCATOR_ENTRIES; i++)
{
/* Skip empty locators */
{
if (pImage->pszParentFilename)
{
if (RT_FAILURE(rc))
return rc;
}
else
{
/* The parent was deleted. */
}
}
}
/* Update parent's UUID */
/* Update data offset and number of table entries. */
return rc;
}
/**
* Internal: Update the VHD footer.
*/
{
int rc = VINF_SUCCESS;
/* Update fields which can change. */
pImage->vhdFooterCopy.Checksum = RT_H2BE_U32(vhdChecksum(&pImage->vhdFooterCopy, sizeof(VHDFooter)));
if (pImage->pBlockAllocationTable)
if (RT_SUCCESS(rc))
return rc;
}
/**
* Internal. Flush image data to disk.
*/
{
int rc = VINF_SUCCESS;
return VINF_SUCCESS;
if (pImage->pBlockAllocationTable)
{
/*
* This is an expanding image. Write the BAT and copy of the disk footer.
*/
return VERR_NO_MEMORY;
/*
* The BAT entries have to be stored in big endian format.
*/
for (unsigned i = 0; i < pImage->cBlockAllocationTableEntries; i++)
/*
* Write the block allocation table after the copy of the disk footer and the dynamic disk header.
*/
if (pImage->fDynHdrNeedsUpdate)
}
if (RT_SUCCESS(rc))
if (RT_SUCCESS(rc))
return rc;
}
/**
* Internal. Free all allocated space for representing an image except pImage,
* and optionally delete the image from disk.
*/
{
int rc = VINF_SUCCESS;
/* Freeing a never allocated image (e.g. because the open failed) is
* not signalled as an error. After all nothing bad happens. */
if (pImage)
{
{
/* No point updating the file that is deleted anyway. */
if (!fDelete)
}
if (pImage->pszParentFilename)
{
}
if (pImage->pBlockAllocationTable)
{
}
{
}
}
return rc;
}
/* 946684800 is the number of seconds between 1/1/1970 and 1/1/2000 */
{
}
{
}
/**
* Internal: Allocates the block bitmap rounding up to the next 32bit or 64bit boundary.
* Can be freed with RTMemFree. The memory is zeroed.
*/
{
#ifdef RT_ARCH_AMD64
#else
#endif
}
/**
* Internal: called when the async expansion process completed (failure or success).
* Will do the necessary rollback if an error occurred.
*/
{
int rc = VINF_SUCCESS;
bool fIoInProgress = false;
/* Quick path, check if everything succeeded. */
if (fFlags == VHDIMAGEEXPAND_ALL_SUCCESS)
{
}
else
{
if ( uStatus == VHDIMAGEEXPAND_STEP_FAILED
{
/* Undo and restore the old value. */
/* Restore the old value on the disk.
* No need for a completion callback because we can't
* do anything if this fails. */
if (uStatus == VHDIMAGEEXPAND_STEP_SUCCESS)
{
}
}
/* Restore old size (including the footer because another application might
* fill up the free space making it impossible to add the footer)
* and add the footer at the right place again. */
}
}
static int vhdAsyncExpansionStepCompleted(void *pBackendData, PVDIOCTX pIoCtx, void *pvUser, int rcReq, unsigned iStep)
{
LogFlowFunc(("pBackendData=%#p pIoCtx=%#p pvUser=%#p rcReq=%Rrc iStep=%u\n",
if (RT_SUCCESS(rcReq))
else
return VERR_VD_ASYNC_IO_IN_PROGRESS;
}
static int vhdAsyncExpansionDataBlockBitmapComplete(void *pBackendData, PVDIOCTX pIoCtx, void *pvUser, int rcReq)
{
return vhdAsyncExpansionStepCompleted(pBackendData, pIoCtx, pvUser, rcReq, VHDIMAGEEXPAND_BLOCKBITMAP_STATUS_SHIFT);
}
static int vhdAsyncExpansionDataComplete(void *pBackendData, PVDIOCTX pIoCtx, void *pvUser, int rcReq)
{
return vhdAsyncExpansionStepCompleted(pBackendData, pIoCtx, pvUser, rcReq, VHDIMAGEEXPAND_USERBLOCK_STATUS_SHIFT);
}
static int vhdAsyncExpansionBatUpdateComplete(void *pBackendData, PVDIOCTX pIoCtx, void *pvUser, int rcReq)
{
return vhdAsyncExpansionStepCompleted(pBackendData, pIoCtx, pvUser, rcReq, VHDIMAGEEXPAND_BAT_STATUS_SHIFT);
}
static int vhdAsyncExpansionFooterUpdateComplete(void *pBackendData, PVDIOCTX pIoCtx, void *pvUser, int rcReq)
{
return vhdAsyncExpansionStepCompleted(pBackendData, pIoCtx, pvUser, rcReq, VHDIMAGEEXPAND_FOOTER_STATUS_SHIFT);
}
{
int rc = VINF_SUCCESS;
unsigned i = 0;
Log(("Open a dynamic disk.\n"));
/*
* Read the dynamic disk header.
*/
&vhdDynamicDiskHeader, sizeof(VHDDynamicDiskHeader),
NULL);
if (memcmp(vhdDynamicDiskHeader.Cookie, VHD_DYNAMIC_DISK_HEADER_COOKIE, VHD_DYNAMIC_DISK_HEADER_COOKIE_SIZE))
return VERR_INVALID_PARAMETER;
AssertMsg(!(pImage->cbDataBlock % VHD_SECTOR_SIZE), ("%s: Data block size is not a multiple of %!\n", __FUNCTION__, VHD_SECTOR_SIZE));
/*
* Every block starts with a bitmap indicating which sectors are valid and which are not.
* We store the size of it to be able to calculate the real offset.
*/
/* Round up to full sector size */
return VERR_NO_MEMORY;
pBlockAllocationTable = (uint32_t *)RTMemAllocZ(pImage->cBlockAllocationTableEntries * sizeof(uint32_t));
if (!pBlockAllocationTable)
return VERR_NO_MEMORY;
/*
* Read the table.
*/
NULL);
/*
* Because the offset entries inside the allocation table are stored big endian
* we need to convert them into host endian.
*/
pImage->pBlockAllocationTable = (uint32_t *)RTMemAllocZ(pImage->cBlockAllocationTableEntries * sizeof(uint32_t));
if (!pImage->pBlockAllocationTable)
{
return VERR_NO_MEMORY;
}
for (i = 0; i < pImage->cBlockAllocationTableEntries; i++)
return rc;
}
{
if (pImage->pInterfaceError)
/* Get I/O interface. */
/*
* Open the image.
*/
false /* fCreate */));
if (RT_FAILURE(rc))
{
/* Do NOT signal an appropriate error here, as the VD layer has the
* choice of retrying the open if it failed. */
return rc;
}
return VERR_VD_VHD_INVALID_HEADER;
{
{
}
break;
{
}
break;
{
}
break;
default:
return VERR_NOT_IMPLEMENTED;
}
/*
* Copy of the disk footer.
* If we allocate new blocks in differencing disks on write access
* the footer is overwritten. We need to write it at the end of the file.
*/
/*
* Is there a better way?
*/
if (RT_FAILURE(rc))
vhdFreeImage(pImage, false);
return rc;
}
/**
* Internal: Checks if a sector in the block bitmap is set
*/
{
/*
* The index of the bit in the byte of the data block bitmap.
* The most signifcant bit stands for a lower sector number.
*/
("VHD: Current bitmap position exceeds maximum size of the bitmap\n"));
}
/**
* Internal: Sets the given sector in the sector bitmap.
*/
DECLINLINE(bool) vhdBlockBitmapSectorSet(PVHDIMAGE pImage, uint8_t *pu8Bitmap, uint32_t cBlockBitmapEntry)
{
/*
* The index of the bit in the byte of the data block bitmap.
* The most significant bit stands for a lower sector number.
*/
("VHD: Current bitmap position exceeds maximum size of the bitmap\n"));
}
/**
* Internal: Derive drive geometry from its size.
*/
{
{
/* ATA disks limited to 127 GB. */
}
{
u32SectorsPerTrack = 255;
u32Heads = 16;
}
else
{
u32SectorsPerTrack = 17;
if (u32Heads < 4)
{
u32Heads = 4;
}
{
u32SectorsPerTrack = 31;
u32Heads = 16;
}
{
u32SectorsPerTrack = 63;
u32Heads = 16;
}
}
}
static uint32_t vhdAllocateParentLocators(PVHDIMAGE pImage, VHDDynamicDiskHeader *pDDH, uint64_t u64Offset)
{
/* Relative Windows path. */
pLocator++;
/* Absolute Windows path. */
pLocator++;
/* Unicode relative Windows path. */
pLocator++;
/* Unicode absolute Windows path. */
}
/**
* Internal: Additional code for dynamic VHD image creation.
*/
{
int rc;
/* Align to sector boundary */
return vhdError(pImage, VERR_NO_MEMORY, RT_SRC_POS, N_("VHD: cannot allocate memory for bitmap storage"));
/* Initialize BAT. */
pImage->cBlockAllocationTableEntries = (uint32_t)((cbSize + pImage->cbDataBlock - 1) / pImage->cbDataBlock); /* Align table to the block size. */
u32BlockAllocationTableSectors = (pImage->cBlockAllocationTableEntries * sizeof(uint32_t) + VHD_SECTOR_SIZE - 1) / VHD_SECTOR_SIZE;
pImage->pBlockAllocationTable = (uint32_t *)RTMemAllocZ(pImage->cBlockAllocationTableEntries * sizeof(uint32_t));
if (!pImage->pBlockAllocationTable)
for (unsigned i = 0; i < pImage->cBlockAllocationTableEntries; i++)
{
}
/* Round up to the sector size. */
else
pImage->uCurrentEndOfFile = pImage->uBlockAllocationTableOffset + u32BlockAllocationTableSectors * VHD_SECTOR_SIZE;
/* Set dynamic image size. */
if (!pvTmp)
return vhdError(pImage, VERR_NO_MEMORY, RT_SRC_POS, N_("VHD: cannot set the file size for '%s'"), pImage->pszFilename);
if (RT_FAILURE(rc))
{
return vhdError(pImage, rc, RT_SRC_POS, N_("VHD: cannot set the file size for '%s'"), pImage->pszFilename);
}
/* Initialize and write the dynamic disk header. */
/* Compute and update checksum. */
DynamicDiskHeader.Checksum = RT_H2BE_U32(vhdChecksum(&DynamicDiskHeader, sizeof(DynamicDiskHeader)));
sizeof(DynamicDiskHeader), NULL);
if (RT_FAILURE(rc))
return vhdError(pImage, rc, RT_SRC_POS, N_("VHD: cannot write dynamic disk header to image '%s'"), pImage->pszFilename);
/* Write BAT. */
NULL);
if (RT_FAILURE(rc))
return vhdError(pImage, rc, RT_SRC_POS, N_("VHD: cannot write BAT to image '%s'"), pImage->pszFilename);
return rc;
}
/**
* Internal: The actual code for VHD image creation, both fixed and dynamic.
*/
unsigned uImageFlags, const char *pszComment,
unsigned uOpenFlags,
unsigned uPercentStart, unsigned uPercentSpan)
{
int rc;
if (pImage->pInterfaceError)
true /* fCreate */));
if (RT_FAILURE(rc))
/* Initialize the footer. */
#ifdef RT_OS_DARWIN
#else /* Virtual PC supports only two platforms atm, so everything else will be Wi2k. */
#endif
Footer.SavedState = 0;
if (uImageFlags & VD_IMAGE_FLAGS_FIXED)
{
/*
* Initialize fixed image.
* "The size of the entire file is the size of the hard disk in
* the guest operating system plus the size of the footer."
*/
/** @todo r=klaus replace this with actual data writes, see the experience
* with VDI files on Windows, can cause long freezes when writing. */
if (RT_FAILURE(rc))
{
vhdError(pImage, rc, RT_SRC_POS, N_("VHD: cannot set the file size for '%s'"), pImage->pszFilename);
goto out;
}
}
else
{
/*
* Initialize dynamic image.
*
* The overall structure of dynamic disk is:
*
* [Copy of hard disk footer (512 bytes)]
* [Dynamic disk header (1024 bytes)]
* [BAT (Block Allocation Table)]
* [Parent Locators]
* [Data block 1]
* [Data block 2]
* ...
* [Data block N]
* [Hard disk footer (512 bytes)]
*/
/* We are half way thourgh with creation of image, let the caller know. */
if (pfnProgress)
if (RT_FAILURE(rc))
goto out;
}
/* Compute and update the footer checksum. */
/* Store the footer */
if (RT_FAILURE(rc))
{
vhdError(pImage, rc, RT_SRC_POS, N_("VHD: cannot write footer to image '%s'"), pImage->pszFilename);
goto out;
}
/* Dynamic images contain a copy of the footer at the very beginning of the file. */
if (!(uImageFlags & VD_IMAGE_FLAGS_FIXED))
{
/* Write the copy of the footer. */
if (RT_FAILURE(rc))
{
vhdError(pImage, rc, RT_SRC_POS, N_("VHD: cannot write a copy of footer to image '%s'"), pImage->pszFilename);
goto out;
}
}
out:
if (RT_FAILURE(rc))
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnCheckIfValid */
{
LogFlowFunc(("pszFilename=\"%s\" pVDIfsDisk=%#p pVDIfsImage=%#p\n", pszFilename, pVDIfsDisk, pVDIfsImage));
int rc;
/* Get I/O interface. */
false /* fCreate */),
&pStorage);
if (RT_FAILURE(rc))
goto out;
&cbFile);
if (RT_FAILURE(rc))
{
goto out;
}
else
rc = VINF_SUCCESS;
out:
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnOpen */
void **ppBackendData)
{
LogFlowFunc(("pszFilename=\"%s\" uOpenFlags=%#x pVDIfsDisk=%#p pVDIfsImage=%#p ppBackendData=%#p\n", pszFilename, uOpenFlags, pVDIfsDisk, pVDIfsImage, ppBackendData));
int rc = VINF_SUCCESS;
/* Check open flags. All valid flags are supported. */
if (uOpenFlags & ~VD_OPEN_FLAGS_MASK)
{
goto out;
}
/* Check remaining arguments. */
if ( !VALID_PTR(pszFilename)
|| !*pszFilename)
{
goto out;
}
if (!pImage)
{
rc = VERR_NO_MEMORY;
goto out;
}
if (RT_SUCCESS(rc))
*ppBackendData = pImage;
else
out:
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnCreate */
unsigned uImageFlags, const char *pszComment,
unsigned uPercentStart, unsigned uPercentSpan,
{
LogFlowFunc(("pszFilename=\"%s\" cbSize=%llu uImageFlags=%#x pszComment=\"%s\" pPCHSGeometry=%#p pLCHSGeometry=%#p Uuid=%RTuuid uOpenFlags=%#x uPercentStart=%u uPercentSpan=%u pVDIfsDisk=%#p pVDIfsImage=%#p pVDIfsOperation=%#p ppBackendData=%#p", pszFilename, cbSize, uImageFlags, pszComment, pPCHSGeometry, pLCHSGeometry, pUuid, uOpenFlags, uPercentStart, uPercentSpan, pVDIfsDisk, pVDIfsImage, pVDIfsOperation, ppBackendData));
int rc = VINF_SUCCESS;
if (pIfProgress)
{
if (pCbProgress)
}
/* Check open flags. All valid flags are supported. */
if (uOpenFlags & ~VD_OPEN_FLAGS_MASK)
{
return rc;
}
/* @todo Check the values of other params */
if (!pImage)
{
rc = VERR_NO_MEMORY;
return rc;
}
/* Get I/O interface. */
{
return VERR_INVALID_PARAMETER;
}
{
return VERR_INVALID_PARAMETER;
}
if (RT_SUCCESS(rc))
{
* image is opened in read-only mode if the caller requested that. */
if (uOpenFlags & VD_OPEN_FLAGS_READONLY)
{
vhdFreeImage(pImage, false);
if (RT_FAILURE(rc))
{
goto out;
}
}
*ppBackendData = pImage;
}
else
out:
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnRename */
{
int rc = VINF_SUCCESS;
/* Check arguments. */
if ( !pImage
|| !pszFilename
|| !*pszFilename)
{
goto out;
}
/* Close the image. */
if (RT_FAILURE(rc))
goto out;
/* Rename the file. */
if (RT_FAILURE(rc))
{
/* The move failed, try to reopen the original image. */
if (RT_FAILURE(rc2))
goto out;
}
/* Update pImage with the new information. */
/* Open the old file with new name. */
if (RT_FAILURE(rc))
goto out;
out:
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnClose */
{
int rc;
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnRead */
{
LogFlowFunc(("pBackendData=%p uOffset=%#llx pvBuf=%p cbBuf=%u pcbActuallyRead=%p\n", pBackendData, uOffset, pvBuf, cbBuf, pcbActuallyRead));
int rc = VINF_SUCCESS;
{
goto out;
}
/*
* If we have a dynamic disk image, we need to find the data block and sector to read.
*/
if (pImage->pBlockAllocationTable)
{
/*
* Get the data block first.
*/
LogFlowFunc(("cBlockAllocationTableEntry=%u cBatEntryIndex=%u\n", cBlockAllocationTableEntry, cBATEntryIndex));
LogFlowFunc(("BlockAllocationEntry=%u\n", pImage->pBlockAllocationTable[cBlockAllocationTableEntry]));
/*
* If the block is not allocated the content of the entry is ~0
*/
{
/* Return block size as read. */
goto out;
}
uVhdOffset = ((uint64_t)pImage->pBlockAllocationTable[cBlockAllocationTableEntry] + pImage->cDataBlockBitmapSectors + cBATEntryIndex) * VHD_SECTOR_SIZE;
/*
* Clip read range to remain in this data block.
*/
/* Read in the block's bitmap. */
NULL);
if (RT_SUCCESS(rc))
{
{
cSectors = 1;
/*
* The first sector being read is marked dirty, read as much as we
* can from child. Note that only sectors that are marked dirty
* must be read from child.
*/
{
cSectors++;
}
}
else
{
/*
* The first sector being read is marked clean, so we should read from
* our parent instead, but only as much as there are the following
* clean sectors, because the block may still contain dirty sectors
* further on. We just need to compute the number of clean sectors
* and pass it to our caller along with the notification that they
* should be read from the parent.
*/
cSectors = 1;
{
cSectors++;
}
}
}
else
}
else
{
}
if (RT_SUCCESS(rc))
{
if (pcbActuallyRead)
*pcbActuallyRead = cbBuf;
Log2(("vhdRead: off=%#llx pvBuf=%p cbBuf=%d\n"
"%.*Rhxd\n",
}
out:
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnWrite */
{
LogFlowFunc(("pBackendData=%#p uOffset=%llu pvBuf=%#p cbBuf=%zu pcbWriteProcess=%#p\n", pBackendData, uOffset, pvBuf, cbBuf, pcbWriteProcess));
int rc = VINF_SUCCESS;
LogFlowFunc(("pBackendData=%p uOffset=%llu pvBuf=%p cbBuf=%u pcbWriteProcess=%p pcbPreRead=%p pcbPostRead=%p fWrite=%u\n",
if (pImage->pBlockAllocationTable)
{
/*
* Get the data block first.
*/
/*
* Clip write range.
*/
/*
* If the block is not allocated the content of the entry is ~0
* and we need to allocate a new block. Note that while blocks are
* allocated with a relatively big granularity, each sector has its
* own bitmap entry, indicating whether it has been written or not.
* So that means for the purposes of the higher level that the
* granularity is invisible. This means there's no need to return
* VERR_VD_BLOCK_FREE unless the block hasn't been allocated yet.
*/
{
/* Check if the block allocation should be suppressed. */
if (fWrite & VD_WRITE_NO_ALLOC)
{
if (pcbWriteProcess)
*pcbWriteProcess = cbBuf;
goto out;
}
if (!pNewBlock)
{
rc = VERR_NO_MEMORY;
goto out;
}
/*
* Write the new block at the current end of the file.
*/
/*
* Set the new end of the file and link the new block into the BAT.
*/
pImage->pBlockAllocationTable[cBlockAllocationTableEntry] = pImage->uCurrentEndOfFile / VHD_SECTOR_SIZE;
/* Write the updated BAT and the footer to remain in a consistent state. */
}
/*
* Calculate the real offset in the file.
*/
uVhdOffset = ((uint64_t)pImage->pBlockAllocationTable[cBlockAllocationTableEntry] + pImage->cDataBlockBitmapSectors + cBATEntryIndex) * VHD_SECTOR_SIZE;
/* Write data. */
/* Read in the block's bitmap. */
NULL);
if (RT_SUCCESS(rc))
{
bool fChanged = false;
/* Set the bits for all sectors having been written. */
{
}
if (fChanged)
{
/* Write the bitmap back. */
NULL);
}
}
}
else
{
}
if (pcbWriteProcess)
*pcbWriteProcess = cbBuf;
/* Stay on the safe side. Do not run the risk of confusing the higher
* level, as that can be pretty lethal to image consistency. */
*pcbPreRead = 0;
*pcbPostRead = 0;
out:
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnFlush */
static int vhdFlush(void *pBackendData)
{
int rc;
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnGetVersion */
static unsigned vhdGetVersion(void *pBackendData)
{
unsigned ver = 0;
if (pImage)
return ver;
}
/** @copydoc VBOXHDDBACKEND::pfnGetSize */
{
return cb;
}
/** @copydoc VBOXHDDBACKEND::pfnGetFileSize */
{
return cb;
}
/** @copydoc VBOXHDDBACKEND::pfnGetPCHSGeometry */
{
int rc;
if (pImage)
{
{
rc = VINF_SUCCESS;
}
else
}
else
LogFlowFunc(("returns %Rrc (CHS=%u/%u/%u)\n", rc, pImage->PCHSGeometry.cCylinders, pImage->PCHSGeometry.cHeads, pImage->PCHSGeometry.cSectors));
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnSetPCHSGeometry */
{
LogFlowFunc(("pBackendData=%#p pPCHSGeometry=%#p PCHS=%u/%u/%u\n", pBackendData, pPCHSGeometry, pPCHSGeometry->cCylinders, pPCHSGeometry->cHeads, pPCHSGeometry->cSectors));
int rc;
if (pImage)
{
{
goto out;
}
rc = VINF_SUCCESS;
}
else
out:
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnGetLCHSGeometry */
{
int rc;
if (pImage)
{
{
rc = VINF_SUCCESS;
}
else
}
else
LogFlowFunc(("returns %Rrc (CHS=%u/%u/%u)\n", rc, pImage->LCHSGeometry.cCylinders, pImage->LCHSGeometry.cHeads, pImage->LCHSGeometry.cSectors));
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnSetLCHSGeometry */
{
int rc;
if (pImage)
{
{
goto out;
}
rc = VINF_SUCCESS;
}
else
out:
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnGetImageFlags */
static unsigned vhdGetImageFlags(void *pBackendData)
{
unsigned uImageFlags;
if (pImage)
else
uImageFlags = 0;
return uImageFlags;
}
/** @copydoc VBOXHDDBACKEND::pfnGetOpenFlags */
static unsigned vhdGetOpenFlags(void *pBackendData)
{
unsigned uOpenFlags;
if (pImage)
else
uOpenFlags = 0;
return uOpenFlags;
}
/** @copydoc VBOXHDDBACKEND::pfnSetOpenFlags */
{
int rc;
/* Image must be opened and the new flags must be valid. */
if (!pImage || (uOpenFlags & ~(VD_OPEN_FLAGS_READONLY | VD_OPEN_FLAGS_INFO | VD_OPEN_FLAGS_ASYNC_IO | VD_OPEN_FLAGS_SHAREABLE | VD_OPEN_FLAGS_SEQUENTIAL)))
{
goto out;
}
/* Implement this operation via reopening the image. */
if (RT_FAILURE(rc))
goto out;
out:
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnGetComment */
{
LogFlowFunc(("pBackendData=%#p pszComment=%#p cbComment=%zu\n", pBackendData, pszComment, cbComment));
int rc;
if (pImage)
else
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnSetComment */
{
int rc;
{
goto out;
}
if (pImage)
else
out:
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnGetUuid */
{
int rc;
if (pImage)
{
rc = VINF_SUCCESS;
}
else
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnSetUuid */
{
int rc;
if (pImage)
{
{
/* Update the footer copy. It will get written to disk when the image is closed. */
/* Update checksum. */
pImage->vhdFooterCopy.Checksum = RT_H2BE_U32(vhdChecksum(&pImage->vhdFooterCopy, sizeof(VHDFooter)));
/* Need to update the dynamic disk header to update the disk footer copy at the beginning. */
pImage->fDynHdrNeedsUpdate = true;
rc = VINF_SUCCESS;
}
else
}
else
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnGetModificationUuid */
{
int rc;
if (pImage)
else
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnSetModificationUuid */
{
int rc;
if (pImage)
{
else
}
else
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnGetParentUuid */
{
int rc;
if (pImage)
{
rc = VINF_SUCCESS;
}
else
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnSetParentUuid */
{
int rc = VINF_SUCCESS;
{
{
pImage->fDynHdrNeedsUpdate = true;
}
else
}
else
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnGetParentModificationUuid */
{
int rc;
if (pImage)
else
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnSetParentModificationUuid */
{
int rc;
if (pImage)
{
else
}
else
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnDump */
static void vhdDump(void *pBackendData)
{
if (pImage)
{
}
}
/** @copydoc VBOXHDDBACKEND::pfnGetTimestamp */
{
int rc = VINF_SUCCESS;
if (pImage)
else
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnGetParentTimeStamp */
{
int rc = VINF_SUCCESS;
if (pImage)
else
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnSetParentTimeStamp */
{
int rc = VINF_SUCCESS;
if (pImage)
{
else
{
pImage->fDynHdrNeedsUpdate = true;
}
}
else
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnGetParentFilename */
{
int rc = VINF_SUCCESS;
if (pImage)
else
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnSetParentFilename */
{
int rc = VINF_SUCCESS;
if (pImage)
{
else
{
if (pImage->pszParentFilename)
if (!pImage->pszParentFilename)
rc = VERR_NO_MEMORY;
else
pImage->fDynHdrNeedsUpdate = true;
}
}
else
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnIsAsyncIOSupported */
static bool vhdIsAsyncIOSupported(void *pBackendData)
{
return true;
}
/** @copydoc VBOXHDDBACKEND::pfnAsyncRead */
{
int rc = VINF_SUCCESS;
LogFlowFunc(("pBackendData=%p uOffset=%#llx pIoCtx=%#p cbRead=%u pcbActuallyRead=%p\n", pBackendData, uOffset, pIoCtx, cbRead, pcbActuallyRead));
return VERR_INVALID_PARAMETER;
/*
* If we have a dynamic disk image, we need to find the data block and sector to read.
*/
if (pImage->pBlockAllocationTable)
{
/*
* Get the data block first.
*/
LogFlowFunc(("cBlockAllocationTableEntry=%u cBatEntryIndex=%u\n", cBlockAllocationTableEntry, cBATEntryIndex));
LogFlowFunc(("BlockAllocationEntry=%u\n", pImage->pBlockAllocationTable[cBlockAllocationTableEntry]));
/*
* If the block is not allocated the content of the entry is ~0
*/
{
/* Return block size as read. */
return VERR_VD_BLOCK_FREE;
}
uVhdOffset = ((uint64_t)pImage->pBlockAllocationTable[cBlockAllocationTableEntry] + pImage->cDataBlockBitmapSectors + cBATEntryIndex) * VHD_SECTOR_SIZE;
/*
* Clip read range to remain in this data block.
*/
/* Read in the block's bitmap. */
if (RT_SUCCESS(rc))
{
{
cSectors = 1;
/*
* The first sector being read is marked dirty, read as much as we
* can from child. Note that only sectors that are marked dirty
* must be read from child.
*/
{
cSectors++;
}
}
else
{
/*
* The first sector being read is marked clean, so we should read from
* our parent instead, but only as much as there are the following
* clean sectors, because the block may still contain dirty sectors
* further on. We just need to compute the number of clean sectors
* and pass it to our caller along with the notification that they
* should be read from the parent.
*/
cSectors = 1;
{
cSectors++;
}
}
}
else
}
else
{
}
if (pcbActuallyRead)
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnAsyncWrite */
{
int rc = VINF_SUCCESS;
LogFlowFunc(("pBackendData=%p uOffset=%llu pIoCtx=%#p cbWrite=%u pcbWriteProcess=%p pcbPreRead=%p pcbPostRead=%p fWrite=%u\n",
if (pImage->pBlockAllocationTable)
{
/*
* Get the data block first.
*/
/*
* Clip write range.
*/
/*
* If the block is not allocated the content of the entry is ~0
* and we need to allocate a new block. Note that while blocks are
* allocated with a relatively big granularity, each sector has its
* own bitmap entry, indicating whether it has been written or not.
* So that means for the purposes of the higher level that the
* granularity is invisible. This means there's no need to return
* VERR_VD_BLOCK_FREE unless the block hasn't been allocated yet.
*/
{
/* Check if the block allocation should be suppressed. */
if (fWrite & VD_WRITE_NO_ALLOC)
{
if (pcbWriteProcess)
return VERR_VD_BLOCK_FREE;
}
PVHDIMAGEEXPAND pExpand = (PVHDIMAGEEXPAND)RTMemAllocZ(RT_OFFSETOF(VHDIMAGEEXPAND, au8Bitmap[pImage->cDataBlockBitmapSectors * VHD_SECTOR_SIZE]));
bool fIoInProgress = false;
if (!pExpand)
return VERR_NO_MEMORY;
/* Set the bits for all sectors having been written. */
{
/* No need to check for a changed value because this is an initial write. */
}
do
{
/*
* Start with the sector bitmap.
*/
pExpand);
if (RT_SUCCESS(rc))
VHDIMAGEEXPAND_STATUS_SET(pExpand->fFlags, VHDIMAGEEXPAND_BLOCKBITMAP_STATUS_SHIFT, VHDIMAGEEXPAND_STEP_SUCCESS);
else if (rc == VERR_VD_ASYNC_IO_IN_PROGRESS)
fIoInProgress = true;
else
{
VHDIMAGEEXPAND_STATUS_SET(pExpand->fFlags, VHDIMAGEEXPAND_BLOCKBITMAP_STATUS_SHIFT, VHDIMAGEEXPAND_STEP_FAILED);
VHDIMAGEEXPAND_STATUS_SET(pExpand->fFlags, VHDIMAGEEXPAND_USERBLOCK_STATUS_SHIFT, VHDIMAGEEXPAND_STEP_FAILED);
VHDIMAGEEXPAND_STATUS_SET(pExpand->fFlags, VHDIMAGEEXPAND_BAT_STATUS_SHIFT, VHDIMAGEEXPAND_STEP_FAILED);
VHDIMAGEEXPAND_STATUS_SET(pExpand->fFlags, VHDIMAGEEXPAND_FOOTER_STATUS_SHIFT, VHDIMAGEEXPAND_STEP_FAILED);
break;
}
/*
* Write the new block at the current end of the file.
*/
pExpand);
if (RT_SUCCESS(rc))
VHDIMAGEEXPAND_STATUS_SET(pExpand->fFlags, VHDIMAGEEXPAND_USERBLOCK_STATUS_SHIFT, VHDIMAGEEXPAND_STEP_SUCCESS);
else if (rc == VERR_VD_ASYNC_IO_IN_PROGRESS)
fIoInProgress = true;
else
{
VHDIMAGEEXPAND_STATUS_SET(pExpand->fFlags, VHDIMAGEEXPAND_USERBLOCK_STATUS_SHIFT, VHDIMAGEEXPAND_STEP_FAILED);
VHDIMAGEEXPAND_STATUS_SET(pExpand->fFlags, VHDIMAGEEXPAND_BAT_STATUS_SHIFT, VHDIMAGEEXPAND_STEP_FAILED);
VHDIMAGEEXPAND_STATUS_SET(pExpand->fFlags, VHDIMAGEEXPAND_FOOTER_STATUS_SHIFT, VHDIMAGEEXPAND_STEP_FAILED);
break;
}
/*
* Write entry in the BAT.
*/
pExpand);
if (RT_SUCCESS(rc))
VHDIMAGEEXPAND_STATUS_SET(pExpand->fFlags, VHDIMAGEEXPAND_BAT_STATUS_SHIFT, VHDIMAGEEXPAND_STEP_SUCCESS);
else if (rc == VERR_VD_ASYNC_IO_IN_PROGRESS)
fIoInProgress = true;
else
{
VHDIMAGEEXPAND_STATUS_SET(pExpand->fFlags, VHDIMAGEEXPAND_BAT_STATUS_SHIFT, VHDIMAGEEXPAND_STEP_FAILED);
VHDIMAGEEXPAND_STATUS_SET(pExpand->fFlags, VHDIMAGEEXPAND_FOOTER_STATUS_SHIFT, VHDIMAGEEXPAND_STEP_FAILED);
break;
}
/*
* Set the new end of the file and link the new block into the BAT.
*/
pImage->pBlockAllocationTable[cBlockAllocationTableEntry] = pImage->uCurrentEndOfFile / VHD_SECTOR_SIZE;
/* Update the footer. */
pExpand);
if (RT_SUCCESS(rc))
VHDIMAGEEXPAND_STATUS_SET(pExpand->fFlags, VHDIMAGEEXPAND_FOOTER_STATUS_SHIFT, VHDIMAGEEXPAND_STEP_SUCCESS);
else if (rc == VERR_VD_ASYNC_IO_IN_PROGRESS)
fIoInProgress = true;
else
{
VHDIMAGEEXPAND_STATUS_SET(pExpand->fFlags, VHDIMAGEEXPAND_FOOTER_STATUS_SHIFT, VHDIMAGEEXPAND_STEP_FAILED);
break;
}
} while (0);
if (!fIoInProgress)
else
}
else
{
/*
* Calculate the real offset in the file.
*/
uVhdOffset = ((uint64_t)pImage->pBlockAllocationTable[cBlockAllocationTableEntry] + pImage->cDataBlockBitmapSectors + cBATEntryIndex) * VHD_SECTOR_SIZE;
/* Read in the block's bitmap. */
if (RT_SUCCESS(rc))
{
/* Write data. */
{
bool fChanged = false;
/* Set the bits for all sectors having been written. */
{
}
/* Only write the bitmap if it was changed. */
if (fChanged)
{
/*
* Write the bitmap back.
*
* @note We don't have a completion callback here because we
* can't do anything if the write fails for some reason.
* by the generic VD layer already and we don't need
* to rollback anything here.
*/
}
}
}
}
}
else
{
}
if (pcbWriteProcess)
/* Stay on the safe side. Do not run the risk of confusing the higher
* level, as that can be pretty lethal to image consistency. */
*pcbPreRead = 0;
*pcbPostRead = 0;
return rc;
}
/** @copydoc VBOXHDDBACKEND::pfnAsyncFlush */
{
/* No need to write anything here. Data is always updated on a write. */
}
/** @copydoc VBOXHDDBACKEND::pfnResize */
unsigned uPercentStart, unsigned uPercentSpan,
{
int rc = VINF_SUCCESS;
if (pIfProgress)
{
if (pCbProgress)
}
/* Making the image smaller is not supported at the moment. */
{
unsigned cBlocksAllocated = 0;
size_t cbBlock = pImage->cbDataBlock + pImage->cbDataBlockBitmap; /** < Size of a block including the sector bitmap. */
uint32_t cBlocksNew = cbSize / pImage->cbDataBlock; /** < New number of blocks in the image after the resize */
cBlocksNew++;
uint32_t cBlocksOld = pImage->cBlockAllocationTableEntries; /** < Number of blocks before the resize. */
uint64_t cbBlockspaceNew = RT_ALIGN_32(cBlocksNew * sizeof(uint32_t), VHD_SECTOR_SIZE); /** < Required space for the block array after the resize. */
uint64_t offStartDataNew = RT_ALIGN_32(pImage->uBlockAllocationTableOffset + cbBlockspaceNew, VHD_SECTOR_SIZE); /** < New start offset for block data after the resize */
/* Go through the BAT and finde the data start offset. */
{
{
if (offStartBlock < offStartDataOld)
}
}
if ( offStartDataOld != offStartDataNew
&& cBlocksAllocated > 0)
{
/* Calculate how many sectors nee to be relocated. */
if (cbOverlapping % cbBlock)
cBlocksReloc++;
/* Do the relocation. */
/*
* Get the blocks we need to relocate first, they are appended to the end
* of the image.
*/
do
{
/* Allocate data buffer. */
if (!pvBuf)
{
rc = VERR_NO_MEMORY;
break;
}
/* Allocate buffer for overwrting with zeroes. */
if (!pvZero)
{
rc = VERR_NO_MEMORY;
break;
}
for (unsigned i = 0; i < cBlocksReloc; i++)
{
/* Search the index in the block table. */
{
{
/* Read data and append to the end of the image. */
if (RT_FAILURE(rc))
break;
if (RT_FAILURE(rc))
break;
/* Zero out the old block area. */
if (RT_FAILURE(rc))
break;
/* Update block counter. */
/* Continue with the next block. */
break;
}
}
if (RT_FAILURE(rc))
break;
}
} while (0);
if (pvBuf)
if (pvZero)
}
/*
* Relocation done, expand the block array and update the header with
* the new data.
*/
if (RT_SUCCESS(rc))
{
uint32_t *paBlocksNew = (uint32_t *)RTMemRealloc(pImage->pBlockAllocationTable, cBlocksNew * sizeof(uint32_t));
if (paBlocksNew)
{
/* Mark the new blocks as unallocated. */
}
else
rc = VERR_NO_MEMORY;
if (RT_SUCCESS(rc))
{
/* Write the block array before updating the rest. */
}
if (RT_SUCCESS(rc))
{
/* Update size and new block count. */
/* Update geometry. */
}
}
/* Update header information in base image file. */
pImage->fDynHdrNeedsUpdate = true;
}
/* Same size doesn't change the image at all. */
return rc;
}
{
/* pszBackendName */
"VHD",
/* cbSize */
sizeof(VBOXHDDBACKEND),
/* uBackendCaps */
/* papszFileExtensions */
/* paConfigInfo */
NULL,
/* hPlugin */
/* pfnCheckIfValid */
/* pfnOpen */
/* pfnCreate */
/* pfnRename */
/* pfnClose */
/* pfnRead */
/* pfnWrite */
/* pfnFlush */
/* pfnGetVersion */
/* pfnGetSize */
/* pfnGetFileSize */
/* pfnGetPCHSGeometry */
/* pfnSetPCHSGeometry */
/* pfnGetLCHSGeometry */
/* pfnSetLCHSGeometry */
/* pfnGetImageFlags */
/* pfnGetOpenFlags */
/* pfnSetOpenFlags */
/* pfnGetComment */
/* pfnSetComment */
/* pfnGetUuid */
/* pfnSetUuid */
/* pfnGetModificationUuid */
/* pfnSetModificationUuid */
/* pfnGetParentUuid */
/* pfnSetParentUuid */
/* pfnGetParentModificationUuid */
/* pfnSetParentModificationUuid */
/* pfnDump */
/* pfnGetTimeStamp */
/* pfnGetParentTimeStamp */
/* pfnSetParentTimeStamp */
/* pfnGetParentFilename */
/* pfnSetParentFilename */
/* pfnIsAsyncIOSupported */
/* pfnAsyncRead */
/* pfnAsyncWrite */
/* pfnAsyncFlush */
/* pfnComposeLocation */
/* pfnComposeName */
/* pfnCompact */
NULL,
/* pfnResize */
};