Simple Storage: Cosmo

Cosmo (CHAP07\COSMO) requires only a few changes to use compound files instead of traditional files as the Chapter 1 version does (or as does Chapter 5's Component Cosmo). The changes are simple. Instead of opening a regular file with which to read or write data, we open a root storage. Instead of using file I/O functions such as _lread and _lwrite, we obtain a stream pointer and use IStream member functions. These changes affect the functions CCosmoDoc::Load and CCosmoDoc::Save in DOCUMENT.CPP as well as new functions we add to the Polyline object, CPolyline::ReadFromStorage and CPolyline::WriteToStorage (POLYLINE.CPP). These latter functions are similar to the existing CPolyline members of ReadFromFile and WriteToFile.

To write a simple file with a single stream, Cosmo makes the following calls, in which steps 1, 2, and 6 occur in CCosmoDoc::Save, and steps 3, 4, and 5 occur in CPolyline::WriteToStorage:

StgCreateDocfile, using STGM_DIRECT ¦ STGM_CREATE, creates a new compound file, overwriting any file that already exists. This returns an IStorage pointer for this new file. Because we use STGM_DIRECT, there is no need to call IStorage::Commit later.

WriteClassStg and WriteFmtUserTypeStg set various flags on the storage and save standard class information.

IStorage::CreateStream, using the name "CONTENTS", returns an IStream pointer.

IStream::Write saves the data, passing a pointer to the data and the size of the data to go into the stream.

IStream::Release closes the stream, matching IStorage::CreateStream.

IStorage::Release closes the storage, matching StgCreateDocfile.

We can see these steps in the following code. You'll see that Cosmo handles multiple versions of its data (it has a legacy of an OLE 1 version as well as its file-based version of Chapter 1), but the specific code to handle the different cases has been omitted here for brevity:


UINT CCosmoDoc::Save(UINT uType, LPTSTR pszFile)
{
LONG lVer, lRet;
UINT uTemp;
BOOL fRename=TRUE;
HRESULT hr;
LPSTORAGE pIStorage;

if (NULL==pszFile)
{
fRename=FALSE;
pszFile=m_szFile;
}

[Determine version of data to write in lVer.]
[Ask whether user wants to change versions if necessary.]


/*
* Use old WriteToFile code for version 1 data; otherwise,
* use Structured Storage through WriteToStorage.
*/
if (lVer==MAKELONG(0, 1))
lRet=m_pPL->WriteToFile(pszFile, lVer);
else
{
hr=StgCreateDocfile(pszFile, STGM_DIRECT | STGM_READWRITE
| STGM_CREATE | STGM_SHARE_EXCLUSIVE, 0, &pIStorage);

if (FAILED(hr))
return DOCERR_COULDNOTOPEN;

//Mark this as one of our class.
WriteClassStg(pIStorage, CLSID_CosmoFigure);

//Write user-readable class information.
WriteFmtUserTypeStg(pIStorage, m_cf
, PSZ(IDS_CLIPBOARDFORMAT));

lRet=m_pPL->WriteToStorage(pIStorage, lVer);
pIStorage->Release();
}

if (POLYLINE_E_NONE!=lRet)
return DOCERR_WRITEFAILURE;

FDirtySet(FALSE);
m_lVer=lVer;

if (fRename)
Rename(pszFile);

return DOCERR_NONE;
}

LONG CPolyline::WriteToStorage(LPSTORAGE pIStorage, LONG lVer)
{
ULONG cb;
ULONG cbExpect=0;
WORD wVerMaj=HIWORD(lVer);
WORD wVerMin=LOWORD(lVer);
POLYLINEDATA pl;
LPSTREAM pIStream;
HRESULT hr;

if (NULL==pIStorage)
return POLYLINE_E_READFAILURE;

//Get copy of our data in version we're going to save.
DataGet(&pl, lVer);

[Set cbExpect to size of appropriate version.]

hr=pIStorage->CreateStream(SZSTREAM, STGM_DIRECT | STGM_CREATE
| STGM_WRITE | STGM_SHARE_EXCLUSIVE, 0, 0, &pIStream);

if (FAILED(hr))
return POLYLINE_E_WRITEFAILURE;

hr=pIStream->Write(&pl, cbExpect, &cb);
pIStream->Release();

if (FAILED(hr) || cbExpect!=cb)
return POLYLINE_E_WRITEFAILURE;

return POLYLINE_E_NONE;
}

In a similar fashion, Cosmo makes the following calls to open and read the data saved previously during a File Open operation. Steps 1, 2, and 7 occur in CCosmoDoc::Load; and steps 4, 5, and 6 occur in CPolyline::ReadFromStorage:

StgIsStorageFile determines whether the filename refers to a compound file by looking for a specific OLE-generated signature at the beginning of the disk file.

If the file is a compound file, StgOpenStorage opens the storage for reading and returns an IStorage pointer. Otherwise, StgCreateDocfile, using STGM_TRANSACTED ¦ STGM_CONVERT, opens a noncompound file as a storage object and returns an IStorage pointer.

At the application's option, ReadClassStg loads the CLSID previously saved from WriteClassStg, and IsEqualCLSID compares the expected class to the one in the file. If the two don't match, you didn't write this file, and you can read the data any way you want. Cosmo does not perform this step.

IStorage::OpenStream is passed the name "CONTENTS" and returns an IStream pointer to the data.

IStream::Read loads the data from the file into the memory structures.

IStream::Release closes the stream, matching IStorage::OpenStream.

IStorage::Release closes the storage, matching the StgOpenStorage or StgCreateDocfile call.

These steps are apparent in the following code:


UINT CCosmoDoc::Load(BOOL fChangeFile, LPTSTR pszFile)
{
HRESULT hr;
LPSTORAGE pIStorage;

if (NULL==pszFile)
{
//For new untitled document, just rename ourselves.
Rename(NULL);
m_lVer=VERSIONCURRENT;
return DOCERR_NONE;
}

pIStorage=NULL;

if (NOERROR!=StgIsStorageFile(pszFile))
{
hr=StgCreateDocfile(pszFile,STGM_TRANSACTED | STGM_READWRITE
| STGM_CONVERT | STGM_SHARE_EXCLUSIVE, 0, &pIStorage);

if (FAILED(hr))
{
//If denied write access, try to load the old way.
if (STG_E_ACCESSDENIED==GetScode(hr))
m_lVer=m_pPL->ReadFromFile(pszFile);
else
return DOCERR_COULDNOTOPEN;
}
}
else
{
hr=StgOpenStorage(pszFile, NULL, STGM_DIRECT | STGM_READ
| STGM_SHARE_EXCLUSIVE, NULL, 0, &pIStorage);

if (FAILED(hr))
return DOCERR_COULDNOTOPEN;
}

if (NULL!=pIStorage)
{
m_lVer=m_pPL->ReadFromStorage(pIStorage);
pIStorage->Release();
}

if (POLYLINE_E_READFAILURE==m_lVer)
return DOCERR_READFAILURE;

if (POLYLINE_E_UNSUPPORTEDVERSION==m_lVer)
return DOCERR_UNSUPPORTEDVERSION;

if (fChangeFile)
Rename(pszFile);

//Importing a file makes things dirty.
FDirtySet(!fChangeFile);

return DOCERR_NONE;
}


LONG CPolyline::ReadFromStorage(LPSTORAGE pIStorage)
{
POLYLINEDATA pl;
ULONG cb=(ULONG)-1;
ULONG cbExpect=0;
LPSTREAM pIStream;
HRESULT hr;
LARGE_INTEGER li;

if (NULL==pIStorage)
return POLYLINE_E_READFAILURE;

//Open CONTENTS stream.
hr=pIStorage->OpenStream(SZSTREAM, 0, STGM_DIRECT | STGM_READ
| STGM_SHARE_EXCLUSIVE, 0, &pIStream);

if (FAILED(hr))
return POLYLINE_E_READFAILURE;

//Read version numbers and seek back to file beginning.
hr=pIStream->Read(&pl, 2*sizeof(WORD), &cb);

LISet32(li, 0);
pIStream->Seek(li, STREAM_SEEK_SET, NULL);

if (FAILED(hr) || 2*sizeof(WORD)!=cb)
{
pIStream->Release();
return POLYLINE_E_READFAILURE;
}

[Code to set cbExpect according to version omitted.]

if (0==cbExpect)
{
pIStream->Release();
return POLYLINE_E_UNSUPPORTEDVERSION;
}

hr=pIStream->Read(&pl, cbExpect, &cb);
pIStream->Release();

if (cbExpect!=cb)
return POLYLINE_E_READFAILURE;

DataSet(&pl, TRUE, TRUE);
return MAKELONG(pl.wVerMin, pl.wVerMaj);
}

The most interesting aspects of this code are the correspondence between stream operations and traditional file operations and the use of STGM_CONVERT to deal with an old file format. We'll cover these topics in the next two sections. Although Cosmo writes a CLSID to the storage, it does not check it during File Open using ReadClassStg. I do this so Cosmo and Component Cosmo (CoCosmo, modified for storage objects a little later) retain file compatibility. Because the two applications write different CLSIDs into their storages, we skip the ReadClassStg step. Using ReadClassStg is an extra check that you can perform to validate a file before loading potentially large amounts of data.