COM Structured Storage in .NET
Written by Harry Fairhead   
Wednesday, 03 February 2010
Article Index
COM Structured Storage in .NET
Opening structured storage
Using IStorage
Reading a stream
Reading JPEG data

 

Reading a stream

The Thumbs.db file always contains a Catalog stream and this contains information about the thumbnails defined within the structured storage. The format of Catalog isn't completely determined but it starts with a header corresponding to the struct:

public struct CatalogHeader
{
public short num1;
public short num2;
public int thumbCount;
public int thumbWidth;
public int thumbHeight;
}

First we need to open the stream using the IStorage function OpenStream:

IStream CatStream;
Is.OpenStream("Catalog", IntPtr.Zero,
(uint)(STGM.READWRITE |
STGM.SHARE_EXCLUSIVE),
0, out CatStream);

This returns an IStream interface which is used to read the raw bytes stored in the stream.

At this point we have the problem of reading in a struct stored as binary data. There are a number of ways of doing this but no single "best". If you have only numerical data in a struct then a common method is to use a BinaryReader. First we need a byte buffer and a variable to hold the number of bytes actually read:

byte[] buf = new byte[1000];
uint count;

Next we create an instance of the structure and read in all of the bytes that correspond to it:

CatalogHeader CH = new CatalogHeader();
CatStream.Read(buf, Marshal.SizeOf(CH),
new IntPtr(&count));

Notice the slightly strange way the final parameter is passed. If you look up the definition in the documentation it says:

pcbRead
A pointer to a ULONG variable that receives the actual number of bytes read from the stream object.

which suggests that you pass a pointer type. However, the Intellisense prompting reveals that the function is actually expecting an IntPtr. There is most definitely a better way of defining this Interface function, e.g. marshal the parameter as (out) ulong, but as it's predefined in ComType it's simpler to just use it "as is".

The slightly unnerving:

new IntPtr(&count));

does the job of passing a "pointer" so that the function can change the value of count. Now that we have a buffer with all of the data relevant to the header we can create a BinaryReader based on it and proceed to read the various data types from it:

BinaryReader head = new BinaryReader(
new MemoryStream(buf));
CH.num1=head.ReadInt16();
CH.num2=head.ReadInt16();
CH.thumbCount=head.ReadInt32();
CH.thumbWidth=head.ReadInt32();
CH.thumbHeight=head.ReadInt32();
head.Close();

This method works reasonably well as long as the structure has a fixed size and is composed of types that the BinaryReader supports. When it comes to reading the rest of the Catalog this isn't the case. After the header there are "thumbCount" records, one for each thumbnail corresponding to the struct:

public struct CatalogItem
{
public int num1;
public int itemID;
public DateTime Modified;
public string filename;
public short num2;
}

The DateTime field is stored as a 64-bit FileTime format value and the filename field is stored as a C-style, variable length, null-terminated string. To read this struct we need a different approach, and what could be simpler than using a BitConvertor:

CatalogItem CI = new CatalogItem();
CatStream.Read(buf, 16,
 new IntPtr(&count));
CI.num1 = BitConverter.ToInt32(buf, 0);
CI.itemID = BitConverter.ToInt32(buf, 4);
CI.Modified = DateTime.FromFileTime(
BitConverter.ToInt64(buf, 8));

The idea is to read in the first 16 bytes, i.e. up to the start of the string, and then simply convert each group into the appropriate type. The DateTime object is constructed directly from the 64 bits that make up the FileTime value using the very useful FromFileTime static method.

Reading in the string can't be done easily in one go because we don't know how many bytes it corresponds to. We are forced to write an explicit loop and test for the null bytes at the end of the string:

StringBuilder temp = 
new StringBuilder(50);
do
{
CatStream.Read(buf, 2,
new IntPtr(&count));
temp.Append(
BitConverter.ToChar(buf, 0));
} while (buf[0] != 0 || buf[1]!=0);

Notice that in this case a character corresponds to two bytes because this is a Unicode string, and indeed ToChar converts a two-byte value to a Unicode character. Now we can store the string in the struct and read the final Int16:

temp.Length = temp.Length - 1;
CI.filename = temp.ToString();
CatStream.Read(buf, 2,
new IntPtr(&count));
CI.num2 = BitConverter.ToInt16(buf, 0);

Notice the need to reduce the size of the string by one to remove the null from the end.

This reads just a single record from the Catalog, but converting it into a function and iterating through all of the records isn't difficult.

<ASIN:0735618755>

<ASIN:067232170X>

<ASIN:0130850322>

<ASIN:0735615470>



Last Updated ( Thursday, 04 February 2010 )