/****************************************************************************** * * BigBlockFile * * This is the implementation of a file that consists of blocks of * a predetermined size. * This class is used in the Compound File implementation of the * IStorage and IStream interfaces. It provides the functionality * to read and write any blocks in the file as well as setting and * obtaining the size of the file. * The blocks are indexed sequentially from the start of the file * starting with -1. * * TODO: * - Support for a transacted mode * * Copyright 1999 Thuy Nguyen * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA */ #include <assert.h> #include <stdlib.h> #include <stdarg.h> #include <stdio.h> #include <string.h> #include <limits.h> #define COBJMACROS #define NONAMELESSUNION #define NONAMELESSSTRUCT #include "windef.h" #include "winbase.h" #include "winuser.h" #include "winerror.h" #include "objbase.h" #include "ole2.h" #include "storage32.h" #include "wine/debug.h" WINE_DEFAULT_DEBUG_CHANNEL(storage); /*********************************************************** * Data structures used internally by the BigBlockFile * class. */ /* We map in PAGE_SIZE-sized chunks. Must be a multiple of 4096. */ #define PAGE_SIZE 131072 #define BLOCKS_PER_PAGE (PAGE_SIZE / BIG_BLOCK_SIZE) /* We keep a list of recently-discarded pages. This controls the * size of that list. */ #define MAX_VICTIM_PAGES 16 /* This structure provides one bit for each block in a page. * Use BIGBLOCKFILE_{Test,Set,Clear}Bit to manipulate it. */ typedef struct { unsigned int bits[BLOCKS_PER_PAGE / (CHAR_BIT * sizeof(unsigned int))]; } BlockBits; /*** * This structure identifies the paged that are mapped * from the file and their position in memory. It is * also used to hold a reference count to those pages. * * page_index identifies which PAGE_SIZE chunk from the * file this mapping represents. (The mappings are always * PAGE_SIZE-aligned.) */ typedef struct MappedPage MappedPage; struct MappedPage { MappedPage *next; MappedPage *prev; DWORD page_index; DWORD mapped_bytes; LPVOID lpBytes; LONG refcnt; BlockBits readable_blocks; BlockBits writable_blocks; }; struct BigBlockFile { BOOL fileBased; ULARGE_INTEGER filesize; ULONG blocksize; HANDLE hfile; HANDLE hfilemap; DWORD flProtect; MappedPage *maplist; MappedPage *victimhead, *victimtail; ULONG num_victim_pages; ILockBytes *pLkbyt; }; /*********************************************************** * Prototypes for private methods */ /* Note that this evaluates a and b multiple times, so don't * pass expressions with side effects. */ #define ROUND_UP(a, b) ((((a) + (b) - 1)/(b))*(b)) /*********************************************************** * Blockbits functions. */ static inline BOOL BIGBLOCKFILE_TestBit(const BlockBits *bb, unsigned int index) { unsigned int array_index = index / (CHAR_BIT * sizeof(unsigned int)); unsigned int bit_index = index % (CHAR_BIT * sizeof(unsigned int)); return bb->bits[array_index] & (1 << bit_index); } static inline void BIGBLOCKFILE_SetBit(BlockBits *bb, unsigned int index) { unsigned int array_index = index / (CHAR_BIT * sizeof(unsigned int)); unsigned int bit_index = index % (CHAR_BIT * sizeof(unsigned int)); bb->bits[array_index] |= (1 << bit_index); } static inline void BIGBLOCKFILE_ClearBit(BlockBits *bb, unsigned int index) { unsigned int array_index = index / (CHAR_BIT * sizeof(unsigned int)); unsigned int bit_index = index % (CHAR_BIT * sizeof(unsigned int)); bb->bits[array_index] &= ~(1 << bit_index); } static inline void BIGBLOCKFILE_Zero(BlockBits *bb) { memset(bb->bits, 0, sizeof(bb->bits)); } /****************************************************************************** * BIGBLOCKFILE_FileInit * * Initialize a big block object supported by a file. */ static BOOL BIGBLOCKFILE_FileInit(LPBIGBLOCKFILE This, HANDLE hFile) { This->pLkbyt = NULL; This->hfile = hFile; if (This->hfile == INVALID_HANDLE_VALUE) return FALSE; This->filesize.u.LowPart = GetFileSize(This->hfile, &This->filesize.u.HighPart); if( This->filesize.u.LowPart || This->filesize.u.HighPart ) { /* create the file mapping object */ This->hfilemap = CreateFileMappingA(This->hfile, NULL, This->flProtect, 0, 0, NULL); if (!This->hfilemap) { CloseHandle(This->hfile); return FALSE; } } else This->hfilemap = NULL; This->maplist = NULL; TRACE("file len %u\n", This->filesize.u.LowPart); return TRUE; } /****************************************************************************** * BIGBLOCKFILE_LockBytesInit * * Initialize a big block object supported by an ILockBytes. */ static BOOL BIGBLOCKFILE_LockBytesInit(LPBIGBLOCKFILE This, ILockBytes* plkbyt) { This->hfile = 0; This->hfilemap = 0; This->pLkbyt = plkbyt; ILockBytes_AddRef(This->pLkbyt); /* We'll get the size directly with ILockBytes_Stat */ This->filesize.QuadPart = 0; TRACE("ILockBytes %p\n", This->pLkbyt); return TRUE; } /****************************************************************************** * BIGBLOCKFILE_FindPageInList [PRIVATE] * */ static MappedPage *BIGBLOCKFILE_FindPageInList(MappedPage *head, ULONG page_index) { for (; head != NULL; head = head->next) { if (head->page_index == page_index) { InterlockedIncrement(&head->refcnt); break; } } return head; } static void BIGBLOCKFILE_UnlinkPage(MappedPage *page) { if (page->next) page->next->prev = page->prev; if (page->prev) page->prev->next = page->next; } static void BIGBLOCKFILE_LinkHeadPage(MappedPage **head, MappedPage *page) { if (*head) (*head)->prev = page; page->next = *head; page->prev = NULL; *head = page; } static BOOL BIGBLOCKFILE_MapPage(BigBlockFile *This, MappedPage *page) { DWORD lowoffset = PAGE_SIZE * page->page_index; DWORD numBytesToMap; DWORD desired_access; assert(This->fileBased); if( !This->hfilemap ) return FALSE; if (lowoffset + PAGE_SIZE > This->filesize.u.LowPart) numBytesToMap = This->filesize.u.LowPart - lowoffset; else numBytesToMap = PAGE_SIZE; if (This->flProtect == PAGE_READONLY) desired_access = FILE_MAP_READ; else desired_access = FILE_MAP_WRITE; page->lpBytes = MapViewOfFile(This->hfilemap, desired_access, 0, lowoffset, numBytesToMap); page->mapped_bytes = numBytesToMap; TRACE("mapped page %u to %p\n", page->page_index, page->lpBytes); return page->lpBytes != NULL; } static MappedPage *BIGBLOCKFILE_CreatePage(BigBlockFile *This, ULONG page_index) { MappedPage *page; page = HeapAlloc(GetProcessHeap(), 0, sizeof(MappedPage)); if (page == NULL) return NULL; page->page_index = page_index; page->refcnt = 1; page->next = NULL; page->prev = NULL; if (!BIGBLOCKFILE_MapPage(This, page)) { HeapFree(GetProcessHeap(),0,page); return NULL; } BIGBLOCKFILE_Zero(&page->readable_blocks); BIGBLOCKFILE_Zero(&page->writable_blocks); return page; } /****************************************************************************** * BIGBLOCKFILE_GetMappedView [PRIVATE] * * Gets the page requested if it is already mapped. * If it's not already mapped, this method will map it */ static void * BIGBLOCKFILE_GetMappedView( LPBIGBLOCKFILE This, DWORD page_index) { MappedPage *page; page = BIGBLOCKFILE_FindPageInList(This->maplist, page_index); if (!page) { page = BIGBLOCKFILE_FindPageInList(This->victimhead, page_index); if (page) { This->num_victim_pages--; BIGBLOCKFILE_Zero(&page->readable_blocks); BIGBLOCKFILE_Zero(&page->writable_blocks); } } if (page) { /* If the page is not already at the head of the list, move * it there. (Also moves pages from victim to main list.) */ if (This->maplist != page) { if (This->victimhead == page) This->victimhead = page->next; if (This->victimtail == page) This->victimtail = page->prev; BIGBLOCKFILE_UnlinkPage(page); BIGBLOCKFILE_LinkHeadPage(&This->maplist, page); } return page; } page = BIGBLOCKFILE_CreatePage(This, page_index); if (!page) return NULL; BIGBLOCKFILE_LinkHeadPage(&This->maplist, page); return page; } static void BIGBLOCKFILE_UnmapPage(LPBIGBLOCKFILE This, MappedPage *page) { TRACE("%d at %p\n", page->page_index, page->lpBytes); assert(This->fileBased); if (page->refcnt > 0) ERR("unmapping inuse page %p\n", page->lpBytes); if (page->lpBytes) UnmapViewOfFile(page->lpBytes); page->lpBytes = NULL; } static void BIGBLOCKFILE_DeletePage(LPBIGBLOCKFILE This, MappedPage *page) { BIGBLOCKFILE_UnmapPage(This, page); HeapFree(GetProcessHeap(), 0, page); } /****************************************************************************** * BIGBLOCKFILE_ReleaseMappedPage [PRIVATE] * * Decrements the reference count of the mapped page. */ static void BIGBLOCKFILE_ReleaseMappedPage( LPBIGBLOCKFILE This, MappedPage *page) { assert(This != NULL); assert(page != NULL); /* If the page is no longer refenced, move it to the victim list. * If the victim list is too long, kick somebody off. */ if (!InterlockedDecrement(&page->refcnt)) { if (This->maplist == page) This->maplist = page->next; BIGBLOCKFILE_UnlinkPage(page); if (MAX_VICTIM_PAGES > 0) { if (This->num_victim_pages >= MAX_VICTIM_PAGES) { MappedPage *victim = This->victimtail; if (victim) { This->victimtail = victim->prev; if (This->victimhead == victim) This->victimhead = victim->next; BIGBLOCKFILE_UnlinkPage(victim); BIGBLOCKFILE_DeletePage(This, victim); } } else This->num_victim_pages++; BIGBLOCKFILE_LinkHeadPage(&This->victimhead, page); if (This->victimtail == NULL) This->victimtail = page; } else BIGBLOCKFILE_DeletePage(This, page); } } static void BIGBLOCKFILE_DeleteList(LPBIGBLOCKFILE This, MappedPage *list) { while (list != NULL) { MappedPage *next = list->next; BIGBLOCKFILE_DeletePage(This, list); list = next; } } /****************************************************************************** * BIGBLOCKFILE_FreeAllMappedPages [PRIVATE] * * Unmap all currently mapped pages. * Empty mapped pages list. */ static void BIGBLOCKFILE_FreeAllMappedPages( LPBIGBLOCKFILE This) { BIGBLOCKFILE_DeleteList(This, This->maplist); BIGBLOCKFILE_DeleteList(This, This->victimhead); This->maplist = NULL; This->victimhead = NULL; This->victimtail = NULL; This->num_victim_pages = 0; } static void BIGBLOCKFILE_UnmapList(LPBIGBLOCKFILE This, MappedPage *list) { for (; list != NULL; list = list->next) { BIGBLOCKFILE_UnmapPage(This, list); } } static void BIGBLOCKFILE_UnmapAllMappedPages(LPBIGBLOCKFILE This) { BIGBLOCKFILE_UnmapList(This, This->maplist); BIGBLOCKFILE_UnmapList(This, This->victimhead); } static void BIGBLOCKFILE_RemapList(LPBIGBLOCKFILE This, MappedPage *list) { while (list != NULL) { MappedPage *next = list->next; if (list->page_index * PAGE_SIZE > This->filesize.u.LowPart) { TRACE("discarding %u\n", list->page_index); /* page is entirely outside of the file, delete it */ BIGBLOCKFILE_UnlinkPage(list); BIGBLOCKFILE_DeletePage(This, list); } else { /* otherwise, remap it */ BIGBLOCKFILE_MapPage(This, list); } list = next; } } static void BIGBLOCKFILE_RemapAllMappedPages(LPBIGBLOCKFILE This) { BIGBLOCKFILE_RemapList(This, This->maplist); BIGBLOCKFILE_RemapList(This, This->victimhead); } /**************************************************************************** * BIGBLOCKFILE_GetProtectMode * * This function will return a protection mode flag for a file-mapping object * from the open flags of a file. */ static DWORD BIGBLOCKFILE_GetProtectMode(DWORD openFlags) { switch(STGM_ACCESS_MODE(openFlags)) { case STGM_WRITE: case STGM_READWRITE: return PAGE_READWRITE; } return PAGE_READONLY; } /* ILockByte Interfaces */ /****************************************************************************** * This method is part of the ILockBytes interface. * * It reads a block of information from the byte array at the specified * offset. * * See the documentation of ILockBytes for more info. */ static HRESULT ImplBIGBLOCKFILE_ReadAt( BigBlockFile* const This, ULARGE_INTEGER ulOffset, /* [in] */ void* pv, /* [length_is][size_is][out] */ ULONG cb, /* [in] */ ULONG* pcbRead) /* [out] */ { ULONG first_page = ulOffset.u.LowPart / PAGE_SIZE; ULONG offset_in_page = ulOffset.u.LowPart % PAGE_SIZE; ULONG bytes_left = cb; ULONG page_index = first_page; ULONG bytes_from_page; LPVOID writePtr = pv; HRESULT rc = S_OK; TRACE("(%p)-> %i %p %i %p\n",This, ulOffset.u.LowPart, pv, cb, pcbRead); /* verify a sane environment */ if (!This) return E_FAIL; if (offset_in_page + bytes_left > PAGE_SIZE) bytes_from_page = PAGE_SIZE - offset_in_page; else bytes_from_page = bytes_left; if (pcbRead) *pcbRead = 0; while (bytes_left) { LPBYTE readPtr; BOOL eof = FALSE; MappedPage *page = BIGBLOCKFILE_GetMappedView(This, page_index); if (!page || !page->lpBytes) { rc = STG_E_READFAULT; break; } TRACE("page %i, offset %u, bytes_from_page %u, bytes_left %u\n", page->page_index, offset_in_page, bytes_from_page, bytes_left); if (page->mapped_bytes < bytes_from_page) { eof = TRUE; bytes_from_page = page->mapped_bytes; } readPtr = (BYTE*)page->lpBytes + offset_in_page; memcpy(writePtr,readPtr,bytes_from_page); BIGBLOCKFILE_ReleaseMappedPage(This, page); if (pcbRead) *pcbRead += bytes_from_page; bytes_left -= bytes_from_page; if (bytes_left && !eof) { writePtr = (LPBYTE)writePtr + bytes_from_page; page_index ++; offset_in_page = 0; if (bytes_left > PAGE_SIZE) bytes_from_page = PAGE_SIZE; else bytes_from_page = bytes_left; } if (eof) { rc = STG_E_READFAULT; break; } } TRACE("finished\n"); return rc; } /****************************************************************************** * This method is part of the ILockBytes interface. * * It writes the specified bytes at the specified offset. * position. If the file is too small, it will be resized. * * See the documentation of ILockBytes for more info. */ static HRESULT ImplBIGBLOCKFILE_WriteAt( BigBlockFile* const This, ULARGE_INTEGER ulOffset, /* [in] */ const void* pv, /* [size_is][in] */ ULONG cb, /* [in] */ ULONG* pcbWritten) /* [out] */ { ULONG size_needed = ulOffset.u.LowPart + cb; ULONG first_page = ulOffset.u.LowPart / PAGE_SIZE; ULONG offset_in_page = ulOffset.u.LowPart % PAGE_SIZE; ULONG bytes_left = cb; ULONG page_index = first_page; ULONG bytes_to_page; LPCVOID readPtr = pv; HRESULT rc = S_OK; TRACE("(%p)-> %i %p %i %p\n",This, ulOffset.u.LowPart, pv, cb, pcbWritten); /* verify a sane environment */ if (!This) return E_FAIL; if (This->flProtect != PAGE_READWRITE) return STG_E_ACCESSDENIED; if (size_needed > This->filesize.u.LowPart) { ULARGE_INTEGER newSize; newSize.u.HighPart = 0; newSize.u.LowPart = size_needed; BIGBLOCKFILE_SetSize(This, newSize); } if (offset_in_page + bytes_left > PAGE_SIZE) bytes_to_page = PAGE_SIZE - offset_in_page; else bytes_to_page = bytes_left; if (pcbWritten) *pcbWritten = 0; while (bytes_left) { LPBYTE writePtr; MappedPage *page = BIGBLOCKFILE_GetMappedView(This, page_index); TRACE("page %i, offset %u, bytes_to_page %u, bytes_left %u\n", page ? page->page_index : 0, offset_in_page, bytes_to_page, bytes_left); if (!page) { ERR("Unable to get a page to write. This should never happen\n"); rc = E_FAIL; break; } if (page->mapped_bytes < bytes_to_page) { ERR("Not enough bytes mapped to the page. This should never happen\n"); rc = E_FAIL; break; } writePtr = (BYTE*)page->lpBytes + offset_in_page; memcpy(writePtr,readPtr,bytes_to_page); BIGBLOCKFILE_ReleaseMappedPage(This, page); if (pcbWritten) *pcbWritten += bytes_to_page; bytes_left -= bytes_to_page; if (bytes_left) { readPtr = (const BYTE *)readPtr + bytes_to_page; page_index ++; offset_in_page = 0; if (bytes_left > PAGE_SIZE) bytes_to_page = PAGE_SIZE; else bytes_to_page = bytes_left; } } return rc; } /****************************************************************************** * BIGBLOCKFILE_Construct * * Construct a big block file. Create the file mapping object. * Create the read only mapped pages list, the writable mapped page list * and the blocks in use list. */ BigBlockFile *BIGBLOCKFILE_Construct(HANDLE hFile, ILockBytes* pLkByt, DWORD openFlags, ULONG blocksize, BOOL fileBased) { BigBlockFile *This; This = HeapAlloc(GetProcessHeap(), 0, sizeof(BigBlockFile)); if (This == NULL) return NULL; This->fileBased = fileBased; This->flProtect = BIGBLOCKFILE_GetProtectMode(openFlags); This->blocksize = blocksize; This->maplist = NULL; This->victimhead = NULL; This->victimtail = NULL; This->num_victim_pages = 0; if (This->fileBased) { if (!BIGBLOCKFILE_FileInit(This, hFile)) { HeapFree(GetProcessHeap(), 0, This); return NULL; } } else { if (!BIGBLOCKFILE_LockBytesInit(This, pLkByt)) { HeapFree(GetProcessHeap(), 0, This); return NULL; } } return This; } /****************************************************************************** * BIGBLOCKFILE_Destructor * * Destructor. Clean up, free memory. */ void BIGBLOCKFILE_Destructor(BigBlockFile *This) { BIGBLOCKFILE_FreeAllMappedPages(This); if (This->fileBased) { CloseHandle(This->hfilemap); CloseHandle(This->hfile); } else { ILockBytes_Release(This->pLkbyt); } HeapFree(GetProcessHeap(), 0, This); } /****************************************************************************** * BIGBLOCKFILE_ReadAt */ HRESULT BIGBLOCKFILE_ReadAt(BigBlockFile *This, ULARGE_INTEGER offset, void* buffer, ULONG size, ULONG* bytesRead) { if (This->fileBased) return ImplBIGBLOCKFILE_ReadAt(This,offset,buffer,size,bytesRead); else return ILockBytes_ReadAt(This->pLkbyt,offset,buffer,size,bytesRead); } /****************************************************************************** * BIGBLOCKFILE_WriteAt */ HRESULT BIGBLOCKFILE_WriteAt(BigBlockFile *This, ULARGE_INTEGER offset, const void* buffer, ULONG size, ULONG* bytesRead) { if (This->fileBased) return ImplBIGBLOCKFILE_WriteAt(This,offset,buffer,size,bytesRead); else return ILockBytes_WriteAt(This->pLkbyt,offset,buffer,size,bytesRead); } /****************************************************************************** * BIGBLOCKFILE_SetSize * * Sets the size of the file. * */ HRESULT BIGBLOCKFILE_SetSize(BigBlockFile *This, ULARGE_INTEGER newSize) { HRESULT hr = S_OK; LARGE_INTEGER newpos; if (!This->fileBased) return ILockBytes_SetSize(This->pLkbyt, newSize); if (This->filesize.u.LowPart == newSize.u.LowPart) return hr; TRACE("from %u to %u\n", This->filesize.u.LowPart, newSize.u.LowPart); /* * Unmap all views, must be done before call to SetEndFile. * * Just ditch the victim list because there is no guarantee we will need them * and it is not worth the performance hit to unmap and remap them all. */ BIGBLOCKFILE_DeleteList(This, This->victimhead); This->victimhead = NULL; This->victimtail = NULL; This->num_victim_pages = 0; BIGBLOCKFILE_UnmapAllMappedPages(This); newpos.QuadPart = newSize.QuadPart; if (SetFilePointerEx(This->hfile, newpos, NULL, FILE_BEGIN)) { if( This->hfilemap ) CloseHandle(This->hfilemap); SetEndOfFile(This->hfile); /* re-create the file mapping object */ This->hfilemap = CreateFileMappingA(This->hfile, NULL, This->flProtect, 0, 0, NULL); } This->filesize = newSize; BIGBLOCKFILE_RemapAllMappedPages(This); return hr; } /****************************************************************************** * BIGBLOCKFILE_GetSize * * Gets the size of the file. * */ static HRESULT BIGBLOCKFILE_GetSize(BigBlockFile *This, ULARGE_INTEGER *size) { HRESULT hr = S_OK; if(This->fileBased) *size = This->filesize; else { STATSTG stat; hr = ILockBytes_Stat(This->pLkbyt, &stat, STATFLAG_NONAME); if(SUCCEEDED(hr)) *size = stat.cbSize; } return hr; } /****************************************************************************** * BIGBLOCKFILE_EnsureExists * * Grows the file if necessary to make sure the block is valid. */ HRESULT BIGBLOCKFILE_EnsureExists(BigBlockFile *This, ULONG index) { ULARGE_INTEGER size; HRESULT hr; /* Block index starts at -1 translate to zero based index */ if (index == 0xffffffff) index = 0; else index++; hr = BIGBLOCKFILE_GetSize(This, &size); if(FAILED(hr)) return hr; /* make sure that the block physically exists */ if ((This->blocksize * (index + 1)) > size.QuadPart) { ULARGE_INTEGER newSize; newSize.QuadPart = This->blocksize * (index + 1); hr = BIGBLOCKFILE_SetSize(This, newSize); } return hr; }