<<
Demo "ExtractMSOfficeFiles"
OLE2-Objekte in OLE-Feldern von Microsoft Access
(Demo-MDB)

In dieser Demo-MDB werden Microsoft Office Dateien entpackt, die als OLE-Objekte in OLE-Feldern gespeichert wurden. Das Beispiel zeigt, wie OLE2-Objekte aufgebaut sind, wo sie im OLE1Stream abgelegt werden und wie man auf sie zugreifen kann. 

Diese Dokumentation behandelt die Microsoft Object Linking and Embedding (OLE) Data Structures nach OLE2 und das Microsoft Compound Binary File Format. Diese Formate sind hervorragend von Microsoft dokumentiert. Um eine vollständige Darstellung dieser Formate zu erhalten, ist es ratsam, sich mit der originalen Dokumentation von Microsoft zu beschäftigen. Diese Dokumentation hier dient nur dem besseren Verständnis der Dateninhalte der OLE-Felder von Microsoft Access. 

Bei meinen Erklärungen gehe ich davon aus, dass die Informationen, die meine Beispiele DocfileViewer (Compound File) und ReduceToPresentationPicture (OLE1-Objekte) vermitteln, bereits bekannt sind. 

OLE2-Objekte verwenden das Compound File Format. Die Daten der OLE-Objekte werden dabei als Streams in einem Compound File gespeichert. Es gibt dort prinzipiell dieselben Bereiche wie bei den OLE1-Objekten. Jeder Bereich erhält (min.) einen eigenen Stream. Das gilt auf jeden Fall für die Bereiche Container-Application-Data, Creating-Application-Data und Presentation-Data. Ob es einen einzelnen Stream gibt, der dem Bereich der nativen Daten entspricht, ist vor allem abhängig von der Ausgangssituation. Verschiedene Programme speichern ihre Daten selber im Compound File Format. Wird eine derartige Datei als OLE-Objekt eingefügt, dann werden die zusätzlichen OLE-Streams in das bereits bestehende Compound File eingetragen. Wenn aber eine Datei eingefügt wird, die nicht auf dem Compound File Format basiert, dann muss für das OLE2-Objekt ein eigenes Compound File angelegt werden. Dort könnten dann die nativen Daten in einem einzelnen Stream abgelegt sein, der dem Bereich der nativen Daten der OLE1-Objekte entspricht. Wie dieser Stream aufgebaut ist und welche Daten er enthält, wird aber von der Ersteller-Anwendung vorgegeben. Nur für den Fall, dass OLE1-Objekte zu OLE2-Objekten konvertiert werden müssen, gibt es einen speziellen Stream im OLE-Format, der direkt dem Bereich der nativen Daten der OLE1-Objekte entspricht. 

Snapshot- und PDF-Dateien basieren z.B. nicht auf dem Compound File Format. Werden solche Dateien als OLE2-Objekte gespeichert, dann wird ein Stream erstellt, der als native Daten den Inhalt der eingefügten Datei enthält. Bei komprimierten Grafikformaten kann man dagegen nicht davon ausgehen, dass als native Daten der Inhalt der eingefügten Datei gespeichert wird. Komprimierte Grafikformate existieren nicht auf dem Bildschirm, dort gibt es immer nur Bitmaps. Eine JPG-Datei ist im Prinzip ein Bitmap, das sich in einem Zip-Archiv befindet. So etwas kann man gut speichern oder transferieren, aber man kann es sich nicht ansehen. Bevor man das Bitmap ansehen kann, muss es erst wieder entpackt werden. Wenn ein Grafikprogramm ein OLE-Objekt bearbeiten soll, dann ist das ein Vorgang der immer auf dem Bildschirm stattfindet. Da man dort kein komprimiertes Format gebrauchen kann, wird in den OLE-Objekten meistens ein unkomprimiertes Format verwendet. Wenn man z.B. eine JPG-Datei mit Hilfe der OLE-Schnittstelle des Microsoft Photo Editor als OLE-Objekt speichert, dann wird im Stream der nativen Daten nicht der Inhalt der JPG-Datei abgelegt, sondern man findet dort die Grafik als Bitmap vor. 

Die Programme Word, Excel und Power Point verwenden selber das Compound File Format zum Speichern ihrer Daten. Als native Daten der Ersteller-Anwendung wird das gesamte Compound File übergeben, in das dann die zusätzlichen Streams des OLE-Objekts eingetragen werden. Der Aufbau der nativen Daten in solchen OLE2-Objekten entspricht dem speziellen Format der jeweiligen Ersteller-Anwendung. Ggf. kann man dort auch weitere OLE2-Objekte vorfinden, die aber zu den nativen Daten gehören. 

Möchte man eine Datei entpacken, die nicht auf dem Compound File Format basiert, dann muss man den Stream der nativern Daten isolieren und ihn als Datei speichern. Möchte man dagegen eine Datei entpacken, die selber das Compound File Format verwendet, dann muss man die zusätzlichen OLE-Streams entfernen und den Rest als Datei speichern. Da die Streams durch ihre Namen identifiziert werden, müssen diese natürlich bekannt sein. Teilweise werden die Namen durch das OLE-Format vorgegeben. Da aber die Ersteller-Anwendung und die Container-Anwendung ihre eigenen Streams in das Compound File des OLE2-Objekts eintragen, muss auch immer der spezielle Aufbau jeder einzelnen Klasse bekannt sein. Am wichtigsten sind aber natürlich die Standardbereiche des OLE-Formats. 

Dem Bereich der Container-Application-Data entspricht der Stream mit dem Namen chr(3) & "AccessObjSiteData". Der Stream besteht aus 56 Bytes. Der Inhalt scheint immer gleich (uninteressant) zu sein. 

Dem Bereich der Creating-Application-Data entspricht der Stream mit dem Namen chr(1) & "Ole". In diesem Stream wird angegeben, ob das Objekt verknüpft oder eingebettet ist. Bei eingebetteten Objekten werden dort keine weiteren Informationen gespeichert. Eine Größenangabe zu den nativen Daten macht für ein Compound File an dieser Stelle keinen Sinn und der Klassenname wird als CLSID im Root-Directory-Entry des Compound Files hinterlegt. Bei verknüpften Objekten enthält der Stream dagegen alle Informationen über das verknüpfte Objekt. 

Dem Bereich der Presentation-Data entsprechen die Streams mit den Namen chr(2) & "OlePres000" und chr(2) & "OlePres001". Gemäß dem OLE-Format wären bis zu 999 dieser OlePresentationStreams zulässig. In den OLE-Feldern von Access habe ich aber bisher nur diese beiden Varianten vorgefunden. Der Stream "OlePres000" enthält das Präsentationsbild im Windows Metafile Format. Der Stream "OlePres001" enthält das Präsentationsbild im Enhanced Metafile Format. Bei den meisten Klassen sind beide Streams vorhanden. 

Dem Bereich der Nativen Daten entspricht eigentlich nur der Stream mit dem Namen chr(1) & "Ole10Native", den es aber nur für die Konvertierung von OLE1- zu OLE2-Objekten gibt. In allen anderen Fällen werden die nativen Daten von der Ersteller-Anwendung in einem oder mehreren Streams im Compound File abgelegt. Werden Dateien eingefügt, die nicht auf dem Compound File Format basieren, dann werden die nativen Daten z.B. in einem Stream mit dem Namen "CONTENTS" gespeichert. Bei Dateien, die selber auf dem Compound File Format basieren, muss man wissen welche Streams zu den nativen Daten gehören. Ein Stream mit dem Namen chr(1) & "CompObj" könnte z.B. sowohl zu den OLE-Objekt-Daten, als auch zu den nativen Daten der eingefügten Datei gehören. Man muss auch beachten, dass solche Dateien möglicherweise selber OLE-Objekte beinhalten. Die zusätzlichen Streams eines OLE-Objekts befinden sich immer im Root-Verzeichnis des Compound Files. 

Dieses Demo zeigt, wie man verschiedene Microsoft Office Dateien entpacken kann. Word, Excel und Power Point Dateien basieren (bis Office 2003) auf dem Compound File Format und werden als OLE2-Objekte ausgetauscht. Access speichert OLE-Objekte im OLE1-Format, OLE2-Objekte werden dabei im Bereich der nativen Daten des OLE1Streams eingebettet. Zuerst muss man also das OLE2-Objekt aus diesem Bereich isolieren. Das OLE2-Objekt ist dann ein Compound File (Structured Storage), in dem sich neben den ursprünglichen Streams der eingefügten Datei die zusätzlichen Streams des OLE2-Objekts befinden. Die zusätzlichen Streams müssen gelöscht werden, was letztlich bedeutet, dass man mit den restlichen Streams ein neues Compound File erstellt. Das reduzierte Compound File entspricht dann wieder der eingefügten Datei und kann gespeichert werden. Es müssen die Streams "\1Ole", "\2OlePres000", "\2OlePres001" und "\3AccessObjSiteData" entfernt werden, die sich im Root-Verzeichnis des OLE2-Objekts befinden. 

Die Vorgänge im ersten Teil des Codes wurden bereits in meinen Beispielen DocfileViewer und ReduceToPresentationPicture angesprochen. An Stelle der Presentation-Data werden hier jetzt die nativen Daten des OLE1-Objekts als Bytestream geladen. Der Bytestream beinhaltet damit das Compound File des OLE2-Objekts auf dessen Inhalt dann zugegriffen wird. Während im DocfileViewer-Demo gezeigt wurde, wie Compound Files aufgebaut sind und wie man sie auslesen kann, wird in diesem Demo nun gezeigt, wie man selber ein Compound File erstellen kann. Die verwendeten Strukturen wurden bereits in den beiden anderen Beispielen beschrieben, der benötigte Deklarationsbereich sieht wie folgt aus. 

' Werte die in der FAT verwendet werden.
Private Const DIFSECT As Long = &HFFFFFFFC
Private Const FATSECT As Long = &HFFFFFFFD
Private Const ENDOFCHAIN As Long = &HFFFFFFFE
Private Const FREESECT As Long = &HFFFFFFFF
 
' Arten der Verzeichniseinträge.
Private Const STGTY_INVALID As Long = 0
Private Const STGTY_STORAGE As Long = 1
Private Const STGTY_STREAM As Long = 2
Private Const STGTY_LOCKBYTES As Long = 3
Private Const STGTY_PROPERTY As Long = 4
Private Const STGTY_ROOT As Long = 5
 
' Farbwerte im red-black-tree
Private Const DECOLOR_RED As Long = 0
Private Const DECOLOR_BLACK As Long = 1
 
Private Type POINTS
X As Integer
Y As Integer
End Type
 
' Container-Application-Data (MS-Access)
Private Type OLEOBJHDR
typ As Integer ' Identifier des OLE-Streams / Der Wert 7189 kennzeichnet die vordefinierten Formate
cbHdr As Integer ' Größe der Container-Application-Data (Dazu gehören auch die Strings Class u. Name)
lObjTyp As Long ' FormatID / OLE-Objekt ist 1=verknüpft, 2=eingebettet, 3=statisches Bildobjekt
cchName As Integer ' Länge des AnsiStrings Name (Anzahl der Zeichen)
cchClass As Integer ' Länge des AnsiStrings Class (Anzahl der Zeichen)
ibName As Integer ' Offset zum AnsiString Name (vom Anfang des Bereichs)
ibClass As Integer ' Offset zum AnsiString Class (vom Anfang des Bereichs)
ptSize As POINTS ' ? / Werte sind immer -1
End Type
 
' Creating-Application-Data (OLE1.0)
' Erster Teil des ObjectHeaders (für linked und embedded)
Private Type ole1ObjectHeaderShort
lOLEVersion As Long ' Versions-Nr. (ist nur für die Ersteller-Anwendung interessant)
lFormatID As Long ' Das OLE1-Objekt ist 1 = verknüpft, 2 = eingebettet
lClassNameLength As Long ' Long-Wert des LengthPrefixedAnsiStings ClassName (Anzahl der Bytes in ClassName)
End Type
 
' Zweiter Teil des ObjectHeaders (optimiert für eingebettete Objekte)
Private Type ole1ObjectHeaderEmbd2
lTopicNameLength As Long ' Anzahl der Zeichen im TopicName (erwarteter Wert ist 0)
lItemNameLength As Long ' Anzahl der Zeichen im ItemName (erwarteter Wert ist 0)
lNativeDataSize As Long ' Größe der nativen Daten die im OLE1Stream eingebettet sind
End Type
 
Private Type GUID
Data1 As Long
Data2 As Integer
Data3 As Integer
Data4(7) As Byte
End Type
 
' Header des Compound File
Private Type StructuredStorageHeader
abSig(7) As Byte ' Compound File Kennzeichnung
clid As GUID
uMinorVersion As Integer
uDllVersion As Integer
uByteOrder As Integer
uSectorShift As Integer ' Angabe zur Sektorgröße (2^x)
uMiniSectorShift As Integer ' Angabe zur Sektorengröße im Ministream (2^x)
usReserved As Integer
ulReserved1 As Long
ulReserved2 As Long
csectFat As Long ' Anzahl der FAT-Sektoren
sectDirStart As Long ' Pointer zum ersten Verzeichnis-Sektor
signature As Long
ulMiniSectorCutoff As Long ' Schwellenwert des Ministreams
sectMiniFatStart As Long ' Pointer zum ersten MiniFAT-Sektor
csectMiniFat As Long ' Anzahl der MiniFAT-Sektoren
sectDifStart As Long ' Pointer zum ersen zusätzlichen DIF-Sektor
csectDif As Long ' Anzahl der zusätzlichen DIF-Sektoren
'sectFat(108) As Long ' Die ersten 109 Elemente der DIF
End Type
 
' Element des Verzeichnis-Arrays
Private Type StructuredStorageDirectoryEntry
ab(63) As Byte ' Name des Eintrags
cb As Integer ' Anzahl der Zeichen im Namen
mse As Byte ' Art des Eintrags (STGTY-Konstanten)
bflags As Byte ' Farbwert red-black-tree
sidLeftSib As Long ' Pointer zum kleinerern Geschwisterknoten (Binärbaum)
sidRightSib As Long ' Pointer zum größeren Geschwisterknoten (Binärbaum)
sidChild As Long ' Pointer zum Kindknoten (wenn Storage)
CLSID As GUID
dwUserFlags As Long
timeCreate(3) As Integer
timeModify(3) As Integer
sectStart As Long ' Pointer zum ersten Sektor des Streams
ulSize As Long ' Anzahl der Bytes im Stream
dptPropType As Integer
wFiller As Integer
End Type
 
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
(pDest As Any, pSource As Any, ByVal cbLength As Long)
 

Die nachfolgende Funktion isoliert das Compound File eines OLE2-Objekts aus einem OLE1Stream, der in einem OLE-Feld gespeichert wurde. Als Parameter wird das OLE-Feld mit dem OLE-Objekt übergeben. Außerdem wird der Dateiname erwartet, unter dem die entpackte Office Datei gespeichert werden soll. 

Public Function SaveOLEObjectToFile(sFileName As String, oField As DAO.Field) As Long
Dim tOLE1OHS As ole1ObjectHeaderShort, tOLE1OHE2 As ole1ObjectHeaderEmbd2
Dim tOH As OLEOBJHDR, bDocFile() As Byte, bOLEStream() As Byte
Dim sCName As String, lPointer1 As Long, lLength As Long
 
' Container-Application-Data laden
If GetAccOLEInfo(oField, sCName, "", tOH) = False Then
Exit Function
End If
 
' Nur eingebettete Objekte zulassen
If (tOH.lobjTyp <> 2) Then
SaveOLEObjectToFile = 1
Exit Function
End If
 
' Nur bestimmte Klassen berücksichtigen
Select Case Left(sCName, InStr(sCName & ".", ".") - 1)
Case "Excel", "Word", "PowerPoint"
' OLE1Stream laden
lLength = oField.FieldSize
ReDim bOLEStream(lLength - 1)
bOLEStream() = oField.GetChunk(0, lLength)
 
' Objektheader laden (OLE1)
CopyMemory ByVal VarPtr(tOLE1OHS), bOLEStream(tOH.cbHdr), Len(tOLE1OHS)
CopyMemory ByVal VarPtr(tOLE1OHE2), bOLEStream(tOH.cbHdr + Len(tOLE1OHS) + _
tOLE1OHS.lClassNameLength), Len(tOLE1OHE2)
 
' Pointer auf die nativen Daten
lPointer1 = tOH.cbHdr + Len(tOLE1OHS) + tOLE1OHS.lClassNameLength + Len(tOLE1OHE2)
 
' Compound File des OLE2-Objekts isolieren
ReDim bDocFile(tOLE1OHE2.lNativeDataSize - 1)
CopyMemory bDocFile(0), bOLEStream(lPointer1), tOLE1OHE2.lNativeDataSize
 
If Dir(sFileName) > "" Then
Kill sFileName
End If
 
' Compound File zur Wiederherstellung weitergeben
RecoverMSOffDocFile sFileName, bDocFile()
SaveOLEObjectToFile = 2
 
Case Else
MsgBox "Format wird nicht unterstützt."
SaveOLEObjectToFile = 1
 
End Select
 
End Function
 
 

Mit der folgenden Funktion werden die Container-Application-Data von Microsoft Access geladen, wenn sie vorhanden sind. 

Private Function GetAccOLEInfo(oField As DAO.Field, sOClass As String, _
sOName As String, tOH As OLEOBJHDR) As Boolean
Dim bStream() As Byte
 
If Nz(oField.FieldSize, 0) < Len(tOH) Then
Exit Function
End If
 
' Container-Application-Header füllen
ReDim bStream(Len(tOH) - 1)
bStream() = oField.GetChunk(0, Len(tOH))
CopyMemory ByVal VarPtr(tOH), bStream(0), Len(tOH)
 
If tOH.typ <> 7189 Then
Exit Function
End If
 
If oField.FieldSize < tOH.ibName + tOH.cchName Then
Exit Function
End If
 
' Klassennamen laden wenn vorhanden (null-terminierter Ansi-String)
If tOH.cchClass > 1 Then
sOClass = StrConv(oField.GetChunk(tOH.ibClass, tOH.cchClass - 1), vbUnicode)
End If
 
' Objektnamen laden wenn vorhanden (null-terminierter Ansi-String)
If tOH.cchName > 1 Then
sOName = StrConv(oField.GetChunk(tOH.ibName, tOH.cchName - 1), vbUnicode)
End If
 
GetAccOLEInfo = True
 
End Function
 
 

Bevor ich zur eigentlichen Funktion zum Wiederherstellen der Office Dateien komme, möchte ich zuerst noch eine Unterfunktion ansprechen. Bei der Wiederherstellung wird das Verzeichnis-Array des Compound Files des OLE2-Objekts eingelesen, wobei die Einträge der zusätzlichen OLE-Streams ignoriert werden. Das Verzeichnis-Array enthält danach alle Einträge, die in das neue Compound File übernommen werden sollen. Da jetzt aber einige der ursprünglichen Einträge fehlen, sind Lücken in der Binärbaum-Verkettung entstanden, mit deren Hilfe die Verzeichnisstruktur codiert wurde. Bevor das neue Verzeichnis-Array verwendet werden kann, muss erst noch die Binärbaum-Verkettung reorganisiert werden. Das Thema "Binärbaum" ist sehr interessant und umfangreich, wer sich damit beschäftigen will, sollte sich an anderer Stelle informieren. Ich behandle das Ganze hier nur sehr spartanisch. Die Verkettung der Einträge erfolgt gemäß dem Compound File Format als Red-Black-Tree. Ich ignoriere die Farbwerte und setze sie beim Schreiben auf black. Das ist zulässig und entspricht der Behandlung eines einfachen Binärbaums. 

Für die Verkettung der Verzeichnis-Array-Einträge stehen 3 Variablen pro Eintrag zur Verfügung. Zwei dieser Variablen verweisen auf Geschwister-Einträge, die dritte Variable verweist auf einen Kind-Eintrag. Die Verzeichnis-Array-Einträge (die ich behandle) stehen entweder für einen Stream oder für ein Verzeichnis. Der erste Eintrag im Verzeichnis-Array steht für das Root-Verzeichnis des Compound Files. Das Root-Verzeichnis ist die Wurzel der Baumstruktur und besitzt selber keine Geschwister, kann aber Kinder beinhalten. In der Kind-Variablen des Root-Eintrags wird der Index des ersten Kind-Eintrags gespeichert, der dem Root-Verzeichnis untergeordnet ist. Natürlich kann das Root-Verzeichnis auch mehrere Kinder enthalten. Alle weiteren Kinder sind dann Geschwister des ersten Kindes. Besitzt ein Kind-Eintrag Geschwister, dann wird der Index des nächsten Geschwister-Eintrags in einer seiner beiden Geschwister-Variablen abgelegt. Einem Eintrag kann ein linker und ein rechter Geschwister-Eintrag zugeordnet werden. Der rechte Eintrag ist dabei immer größer und der linke Eintrag immer kleiner. Die Größe der Einträge ergibt sich aus ihren Namen. Zuerst ist dafür die Anzahl der Zeichen entscheidend, bei gleicher Länge werden die Namen dann binär verglichen. In einem Verzeichnis dürfen Namen nicht doppelt vorkommen. 

Zusammenfassend kann man sagen, alle Kind-Einträge die sich in einem Verzeichnis befinden sind über ihre Geschwister-Variablen miteinander verkettet. Ein Verzeichnis-Eintrag kennt immer nur einen seiner Kind-Einträge und ist mit ihm über seine Kind-Variable verkettet. Die Verkettung der Ebenen erfolgt also über die Kind-Variablen und die Verkettung der Elemente einer Ebene über die Geschwister-Variablen. Sucht man nach einem Namen in einem Verzeichnis, dann fängt man mit dem Eintrag an, auf den die Kind-Variable des Verzeichnis-Eintrags verweist. Entspricht dieser Eintrag nicht dem gesuchten Namen, dann geht man zum nächsten Geschwister-Eintrag weiter. Da man ja weiß, ob der Name den man sucht, größer oder kleiner ist als der Name des aktuellen Eintrages, weiß man auch, ob man mit dem linken oder mit dem rechten Geschwister-Eintrag weitermachen muss. Man wiederholt dann das Ganze so lange, bis man den gesuchten Namen gefunden hat. Gibt es keinen Geschwister-Eintrag mehr, dann ist die entsprechende Geschwister-Variable als EndOfChain gekennzeichnet. Möchte man einen neuen Eintrag in den Binärbaum aufnehmen, dann sucht man auf dieselbe Weise nach dem ersten freien Platz in der Kette. Man fängt also mit dem ersten Kind-Eintrag an. Da es keine doppelten Namen geben darf, ist der Name des Kind-Eintrags entweder größer oder kleiner als der Name des neuen Eintrags und man weiß damit, ob es mit dem linken oder rechten Geschwister-Eintrag weiter gehen muss. Man folgt der Kette auf diesem Weg so lange, bis man zu einem Eintrag kommt, dessen entsprechende Geschwister-Variable als EndOfChain gekennzeichnet ist. Dort wird dann der Index des neuen Eintrags eingetragen. 

Den Index des Verzeichnis-Arrays nennt man SID, das englische Wort für Geschwister ist Siblings und für Kind natürlich Child. Daraus ergeben sich die Namen der Variablen des Binärbaums in den Verzeichnis-Array-Einträgen. 'sidLeftSib' und 'sidRightSib' sind die Geschwister-Variablen und 'sidChild' ist die Kind-Variable. 

Da ich die Binärbaum-Verkettung nicht besonders vorteilhaft finde, wenn es darum geht gezielt auf Elemente einer Ebenen zuzugreifen, bilde ich das Verzeichnis-Array nochmal in einem zweidimensionalen Typen-Array ab. Die erste Dimension beinhaltet dabei Informationen zu den Verzeichnissen und die zweite Dimension zu den Elementen der Verzeichnisse. 

' Strukturen des zweidimensionalen Typen-Arrays
 
' Infos zu den Elementen
Private Type StorageContentsData
iChild As Integer ' Verweis auf einen Kind-Eintrag (SID)
iChildSC As Integer ' Index im SC-Array, wenn Child ist Ebene
iParent As Integer ' Verweis auf die Parent-Ebene
iSSDEIndex As Integer ' Index im Verzeichnis-Array (SID)
iCB As Integer ' Anzahl der Bytes im Namen
sNName As String ' Name des Elements
lSize As Long ' Größe des Streams
lSectStart As Long ' Erster Sektor des Streams
iSibParent As Integer ' Parent-Geschwister-Eintrag (SID)
End Type
 
' Infos zu den Ebenen (Verzeichnisse)
Private Type StorageContents
iHeighNode As Integer ' Index des ersten Kind-Eintrags
iParent As Integer ' ID der Ebene (als Parent)
sNName As String ' Name der Ebene
tSCD(30) As StorageContentsData ' Infos zu den Elementen der Ebene
End Type
 
 

Zum Einlesen des Binärbaums benutze ich zwei rekursive Prozeduren. Die erste Prozedur liest die Informationen zu den Verzeichnissen ein und die zweite die Informationen zu den Elementen. ReadStorageDirectory() wird zuerst mit dem SID 0 für das Root-Verzeichnis aufgerufen. Das Verzeichnis wird in der 1.Dimension registriert und danach die Prozedur ReadDirectoryNode() aufgerufen, wobei der SID des ersten Kind-Eintrags übergeben wird. Dort wird dann der Kind-Eintrag in der 2.Dimension registriert, wenn er nicht zu den Einträgen gehört, die ignoriert werden sollen. Anschließend wird überprüft, ob der Eintrag Geschwister-Einträge besitzt. Existieren weitere Geschwister-Einträge, dann ruft sich die Prozedur selber auf und übergibt dabei den SID des Geschwister-Eintrags. Der Vorgang wiederholt sich, bis alle Einträge des aktuellen Verzeichnisses eingelesen wurden. Danach geht es wieder in ReadStorageDirectory() weiter. Dort wird nun das Array mit den Einträgen durchlaufen. Handelt es sich bei einem der Einträge um ein Verzeichnis, dann ruft sich die Prozedur erneut selber auf und übergibt dabei den SID dieses Eintrags. Die Vorgänge wiederholen sich so lange, bis die gesamte Baumstruktur durchlaufen wurde. 

' Prozedur zum Einlesen der Ebenen
Private Sub ReadStorageDirectory(ByVal iRoot As Integer, tSSDE() As StructuredStorageDirectoryEntry, _
tSC() As StorageContents, sIgnorStreams() As String)
Dim iCurrentRootIndex As Integer, i As Integer
 
' Anzahl der Verzeichnisse im SC-Array
tSC(0).iParent = tSC(0).iParent + 1
' Aktueller Index im SC-Array
iCurrentRootIndex = tSC(0).iParent
 
' ID der Ebene
tSC(iCurrentRootIndex).iParent = iRoot
' Name der Ebene
tSC(iCurrentRootIndex).sNName = Left(CStr(tSSDE(iRoot).ab), tSSDE(iRoot).cb / 2 - 1)
 
If tSSDE(iRoot).sidChild > 0 Then
' Elemente der Ebene einlesen
Call ReadDirectoryNode(iRoot, tSSDE(iRoot).sidChild, tSSDE(), tSC(iCurrentRootIndex), sIgnorStreams())
 
' Alle Elemente der Ebene durchlaufen
For i = 1 To tSC(iCurrentRootIndex).tSCD(0).lSize
' Kind-Eintrag ist Unterverzeichnis
If tSC(iCurrentRootIndex).tSCD(i).iChild > 0 Then
' Index des Kind-Eintrags als Ebene im SC-Array
tSC(iCurrentRootIndex).tSCD(i).iChildSC = tSC(0).iParent + 1
' Unterverzeichnis einlesen (Rekursion)
Call ReadStorageDirectory(tSC(iCurrentRootIndex).tSCD(i).iSSDEIndex, tSSDE(), tSC(), sIgnorStreams())
End If
Next i
 
End If
 
End Sub
 
' Prozedur zum Einlesen der Elemente
Private Sub ReadDirectoryNode(iRoot As Integer, ByVal iIndex As Integer, tSSDE() As StructuredStorageDirectoryEntry, _
tSC() As StorageContents, sIgnorStreams() As String)
' OLE-Streams aus dem Root-Verzeichnis ignorieren
If (InStr(";" & Join(sIgnorStreams, ";") & ";", ";" & Left(CStr(tSSDE(iIndex).ab), tSSDE(iIndex).cb / 2 - 1) & _
";") = 0) Or (iRoot > 0) Then
tSC.tSCD(0).lSize = tSC.tSCD(0).lSize + 1
tSC.tSCD(tSC.tSCD(0).lSize).iParent = iRoot
tSC.tSCD(tSC.tSCD(0).lSize).iSSDEIndex = iIndex
tSC.tSCD(tSC.tSCD(0).lSize).iCB = tSSDE(iIndex).cb
tSC.tSCD(tSC.tSCD(0).lSize).lSize = tSSDE(iIndex).ulSize
tSC.tSCD(tSC.tSCD(0).lSize).iChild = tSSDE(iIndex).sidChild
tSC.tSCD(tSC.tSCD(0).lSize).lSectStart = tSSDE(iIndex).sectStart
tSC.tSCD(tSC.tSCD(0).lSize).sNName = Left(CStr(tSSDE(iIndex).ab), tSSDE(iIndex).cb / 2 - 1)
End If
 
' Rechten Geschwister-Eintrag einlesen, wenn vorhanden (Rekursion)
If tSSDE(iIndex).sidRightSib <> -1 Then
Call ReadDirectoryNode(iRoot, tSSDE(iIndex).sidRightSib, tSSDE(), tSC, sIgnorStreams())
End If
 
' Linken Geschwister-Eintrag einlesen, wenn vorhanden (Rekursion)
If tSSDE(iIndex).sidLeftSib <> -1 Then
Call ReadDirectoryNode(iRoot, tSSDE(iIndex).sidLeftSib, tSSDE(), tSC, sIgnorStreams())
End If
 
End Sub
 
 

Mit der folgenden Prozedur wird das neue Verzeichnis-Array erstellt und die Einträge im Binärbaum verkettet. Als Kind-Eintrag verwende ich jeweils das erste Element im Array, die restlichen Einträge werden dann entsprechend ihrer Größe an diesen angehängt. Man könnte natürlich auch die optimale Baumstruktur der Einträge ermitteln, was dann der Fall wäre, wenn so viele Einträge wie möglich je zwei Geschwister-Einträge besitzen würden. Ich nehme hier aber die Einträge so wie sie kommen, das ist für die Office-Dateien ausreichend. Wenn alle Geschwister-Einträge verkettet wurden, werden die SIDs der 1.Kind-Einträge in den Verzeichnis-Array-Einträgen der Ebenen (Verzeichnisse) hinterlegt. 

Private Sub ReorganizeDirectoryEntrys(tSSDE() As StructuredStorageDirectoryEntry, tSC() As StorageContents, _
tSSDE2() As StructuredStorageDirectoryEntry)
Dim iContents As Integer, iStorage As Integer
Dim sTemp As String, i As Integer
 
' Alle Ebenen (Verzeichnisse) durchlaufen
For iStorage = 1 To tSC(0).iParent
' Alle Elemente der Ebene durchlaufen
For iContents = 1 To tSC(iStorage).tSCD(0).lSize
' Verzeichnis-Array-Eintrag anfügen
ReDim Preserve tSSDE2(UBound(tSSDE2) + 1)
tSSDE2(UBound(tSSDE2)) = tSSDE(tSC(iStorage).tSCD(iContents).iSSDEIndex)
 
' Binärbaum-Variablen initialisieren
tSSDE2(UBound(tSSDE2)).bflags = DECOLOR_BLACK
tSSDE2(UBound(tSSDE2)).sidRightSib = -1
tSSDE2(UBound(tSSDE2)).sidLeftSib = -1
 
' Neuen SID hinterlegen
tSC(iStorage).tSCD(iContents).iSSDEIndex = UBound(tSSDE2)
' SID des 1.Kind-Eintrags hinterlegen
tSC(iStorage).iHeighNode = tSC(iStorage).tSCD(1).iSSDEIndex
 
Next iContents
 
' 1.Element ist Root (sidChild), die restlichen Elemente werden verkettet
For iContents = 2 To tSC(iStorage).tSCD(0).lSize
i = tSC(iStorage).tSCD(1).iSSDEIndex
Do
' Ist der neue Eintrag kleiner als der aktuelle Eintrag
If tSSDE2(i).cb > tSC(iStorage).tSCD(iContents).iCB Then
If tSSDE2(i).sidLeftSib = -1 Then
' Neuen Eintrag am freien Platz eintragen
tSSDE2(i).sidLeftSib = tSC(iStorage).tSCD(iContents).iSSDEIndex
Exit Do
Else
' Mit dem linken Geschwister-Eintrag weitermachen
i = tSSDE2(i).sidLeftSib
End If
 
' Ist der neue Eintrag größer als der aktuelle Eintrag
ElseIf tSSDE2(i).cb < tSC(iStorage).tSCD(iContents).iCB Then
If tSSDE2(i).sidRightSib = -1 Then
' Neuen Eintrag am freien Platz eintragen
tSSDE2(i).sidRightSib = tSC(iStorage).tSCD(iContents).iSSDEIndex
Exit Do
Else
' Mit dem rechten Geschwister-Eintrag weitermachen
i = tSSDE2(i).sidRightSib
End If
 
Else
sTemp = Left(CStr(tSSDE2(i).ab), tSSDE2(i).cb / 2 - 1)
' Ist der neue Eintrag kleiner als der aktuelle Eintrag
If StrComp(sTemp, tSC(iStorage).tSCD(iContents).sNName, vbBinaryCompare) = 1 Then
If tSSDE2(i).sidLeftSib = -1 Then
' Neuen Eintrag am freien Platz eintragen
tSSDE2(i).sidLeftSib = tSC(iStorage).tSCD(iContents).iSSDEIndex
Exit Do
Else
' Mit dem linken Geschwister-Eintrag weitermachen
i = tSSDE2(i).sidLeftSib
End If
 
Else
If tSSDE2(i).sidRightSib = -1 Then
' Neuen Eintrag am freien Platz eintragen
tSSDE2(i).sidRightSib = tSC(iStorage).tSCD(iContents).iSSDEIndex
Exit Do
Else
' Mit dem rechten Geschwister-Eintrag weitermachen
i = tSSDE2(i).sidRightSib
End If
 
End If
 
End If
 
Loop
 
Next iContents
 
Next iStorage
 
' Alle Ebenen (Verzeichnisse) durchlaufen und den SID der Kind-Einträge eintragen.
For iStorage = 1 To tSC(0).iParent
For iContents = 1 To tSC(iStorage).tSCD(0).lSize
If tSC(iStorage).tSCD(iContents).iChildSC > 0 Then
tSSDE2(tSC(iStorage).tSCD(iContents).iSSDEIndex).sidChild = _
tSC(tSC(iStorage).tSCD(iContents).iChildSC).iHeighNode
End If
Next iContents
Next iStorage
 
End Sub
 
 

Nun komme ich also zu der Funktion mit der die Office-Dateien wiederhergestellt werden. Das Compound File des OLE2-Objekts wird übergeben und das Verzeichnis-Array wird unter Ausschluss der zusätzlichen OLE-Streams eingelesen und reorganisiert. Dann wird das neue Verzeichnis-Array durchlaufen, alle benötigten Streams aus dem OLE2-Objekt ausgelesen und entsprechend ihrer Größen entweder an den neuen Ministream oder den neuen Datenstream angehängt. Da sich die Sektorengröße nicht geändert hat, können die Streams so wie sie sind verwendet werden. Durch diesen Vorgang werden die Streams auch gleich defragmentiert. Danach wird die benötigte MiniFAT bereitgestellt und der Ministream an die Sektorengröße des Compound Files angepasst. Anschließend wird die FAT erstellt und entsprechend der FAT die DIF. Da die FAT auch die FAT-Sektoren und die zusätzlichen DIF-Sektoren beinhaltet, muss ihre richtige Größe schrittweise ermittelt werden. Das gilt entsprechend für die DIF. Nachdem diese Einheiten alle in der richtigen Größe bereitgestellt wurden, wird mit der Verkettung begonnen. Zuerst wird die DIF verkettet, dann kommt die FAT dran. In die FAT muss die DIF, die FAT selber, das Verzeichnis-Array, die MiniFAT und der Ministream registriert werden. Abschließend werden dann die einzelnen Streams, entweder in der FAT oder der MiniFAT verkettet und jeweils der erste Sektor im zugehörigen Verzeichnis-Array-Eintrag hinterlegt. Die Einzelteile des neuen Compound Files stehen danach in korrekter Form bereit und können in einer Datei gespeichert werden. 

Public Function RecoverMSOffDocFile(sFileName As String, bDocFile() As Byte) As Long
Dim tSSH2 As StructuredStorageHeader, tSSDE2() As StructuredStorageDirectoryEntry
Dim tSSH As StructuredStorageHeader, tSSDE() As StructuredStorageDirectoryEntry
Dim bSSDE2() As Byte, bMiniStream2() As Byte, bDataStream2() As Byte
Dim lSectDIF As Long, lSectFAT As Long, lSectMiniFAT As Long
Dim lDIF2() As Long, lFAT2() As Long, lMiniFAT2() As Long
Dim tSC() As StorageContents, sIgnorStreams() As String
Dim lDIF() As Long, lFAT() As Long, lMiniFAT() As Long
Dim bMiniStream() As Byte, bStream1() As Byte
Dim lSectorSize As Long, cbDataSize As Long
Dim lPointer1 As Long, lPointer2 As Long
Dim i As Integer, l As Long
 
' Compound File Header füllen (Ohne DIF-Daten)
CopyMemory tSSH.abSig(0), bDocfile(0), Len(tSSH)
 
' Sektorengröße des Compound File
lSectorSize = (2 ^ tSSH.uSectorShift)
 
' Größe des DIF-Arrays festlegen (Die Verkettungselemente dürfen nicht ins Array übernommen werden)
ReDim lDIF(109 + (((lSectorSize / 4) - 1) * tSSH.csectDif))
' Die ersten 109 DIF-Elemente füllen
CopyMemory lDIF(0), bDocfile(Len(tSSH)), (109 * 4)
 
' Die zusätzlichen DIF-Sektoren laden (wenn vorhanden)
lPointer1 = tSSH.sectDifStart
For l = 1 To tSSH.csectDif
CopyMemory lDIF(109 + (l - 1) * ((lSectorSize / 4) - 1)), _
bDocfile(Len(tSSH) + (109 * 4) + (lPointer1 * lSectorSize)), lSectorSize
lPointer1 = lDIF(109 + l * ((lSectorSize / 4) - 1))
Next l
 
' Größe des FAT-Arrays festlegen ([Anzahl FAT-Sektoren] * [Elemente pro Sektor])
ReDim lFAT(((lSectorSize / 4) * tSSH.csectFat) - 1)
' FAT-Array füllen (Sektoren beginnen bei (Sektor - 1) * Sektorgröße | Der erste Sektor hat Offset 0)
For l = 1 To tSSH.csectFat
CopyMemory lFAT((l - 1) * (lSectorSize / 4)), _
bDocfile(Len(tSSH) + (109 * 4) + (lDIF(l - 1) * lSectorSize)), lSectorSize
Next l
 
' MiniFAT-Array anlegen (wenn vorhanden)
If tSSH.csectMiniFat > 0 Then
ReDim lMiniFAT(((lSectorSize / 4) * tSSH.csectMiniFat) - 1)
i = 0
l = tSSH.sectMiniFatStart
' MiniFAT-Array füllen bis zum Ende der Sektorenkette
While Not lFAT(l) = ENDOFCHAIN
CopyMemory lMiniFAT(i * (lSectorSize / 4)), _
bDocfile(Len(tSSH) + (109 * 4) + (l * lSectorSize)), lSectorSize
l = lFAT(l)
i = i + 1
Wend
' Letzten Sektor der MiniFAT laden
CopyMemory lMiniFAT(i * (lSectorSize / 4)), _
bDocfile(Len(tSSH) + (109 * 4) + (l * lSectorSize)), lSectorSize
End If
 
' Verzeichnis-Array laden ([Sektorengröße] / 128 (Bytes) = [Anzahl der Elemente pro Sektor])
i = 0
l = tSSH.sectDirStart
While Not lFAT(l) = ENDOFCHAIN
ReDim Preserve tSSDE(i + (lSectorSize / 128) - 1)
CopyMemory tSSDE(i).ab(0), bDocfile(Len(tSSH) + (109 * 4) + (l * lSectorSize)), lSectorSize
l = lFAT(l)
i = i + (lSectorSize / 128)
Wend
' Letzten Verzeichnissektor der Kette laden
ReDim Preserve tSSDE(i + (lSectorSize / 128) - 1)
CopyMemory tSSDE(i).ab(0), bDocfile(Len(tSSH) + (109 * 4) + (l * lSectorSize)), lSectorSize
 
' Ministream laden (wenn vorhanden)
If tSSDE(0).sectStart > 0 Then
l = tSSDE(0).sectStart
i = 0
While Not lFAT(l) = ENDOFCHAIN
ReDim Preserve bMiniStream((i + 1) * lSectorSize)
CopyMemory bMiniStream(i * lSectorSize), _
bDocfile(Len(tSSH) + (109 * 4) + (l * lSectorSize)), lSectorSize
l = lFAT(l)
i = i + 1
Wend
ReDim Preserve bMiniStream((i + 1) * lSectorSize - 1)
CopyMemory bMiniStream(i * lSectorSize), bDocfile(Len(tSSH) + (109 * 4) + (l * lSectorSize)), lSectorSize
End If
 
' Neues Compound File vorbereiten
ReDim bMiniStream2(0)
ReDim bDataStream2(0)
ReDim tSSDE2(0)
tSSDE2(0) = tSSDE(0)
l = 0
 
ReDim tSC(20)
' OLE-Streams ignorieren
ReDim sIgnorStreams(3)
sIgnorStreams(0) = Chr(1) & "Ole"
sIgnorStreams(1) = Chr(2) & "OlePres000"
sIgnorStreams(2) = Chr(2) & "OlePres001"
sIgnorStreams(3) = Chr(3) & "AccessObjSiteData"
 
' Neues Verzeichnis-Array einlesen und Binärbaum reorganisieren
Call ReadStorageDirectory(0, tSSDE(), tSC(), sIgnorStreams())
Call ReorganizeDirectoryEntrys(tSSDE(), tSC(), tSSDE2())
tSSDE2(0).sidChild = tSC(1).tSCD(tSC(1).iHeighNode).iSSDEIndex
 
' Alle Einträge im neuen Verzeichnis-Array durchlaufen
For i = 1 To UBound(tSSDE2)
If tSSDE2(i).mse = STGTY_STREAM Then
' Stream einlesen
If (tSSDE2(i).ulSize > 0) And (tSSDE2(i).sectStart >= 0) Then
ReadStream tSSDE2(i).sectStart, tSSDE2(i).ulSize, bStream1(), _
tSSH, lFAT(), lMiniFAT(), bDocFile(), bMiniStream()
End If
 
If (tSSDE2(i).ulSize > 0) Then
If Left(CStr(tSSDE2(i).ab), tSSDE2(i).cb / 2 - 1) = "Workbook" Then
excelSetWBVisible bStream1()
End If
 
If tSSDE2(i).ulSize < tSSH.ulMiniSectorCutoff Then
' Stream an den Ministream anfügen
ReDim Preserve bMiniStream2(UBound(bMiniStream2) + _
UBound(bStream1) + Abs(UBound(bMiniStream2) <> 0))
CopyMemory bMiniStream2(UBound(bMiniStream2) - UBound(bStream1)), bStream1(0), _
UBound(bStream1) + 1
lSectMiniFAT = lSectMiniFAT + (Int(tSSDE2(l).ulSize / (2 ^ tSSH.uMiniSectorShift)) + _
Abs((tSSDE2(l).ulSize Mod (2 ^ tSSH.uMiniSectorShift)) <> 0))
Else
' Stream an den Datenstream anfügen
ReDim Preserve bDataStream2(UBound(bDataStream2) + _
UBound(bStream1) + Abs(UBound(bDataStream2) <> 0))
CopyMemory bDataStream2(UBound(bDataStream2) - UBound(bStream1)), _
bStream1(0), UBound(bStream1) + 1
End If
 
End If
 
End If
Next i
 
' Länge des Ministreams im Root-Entry hinterlegen
tSSDE2(0).ulSize = lSectMiniFAT * (2 ^ tSSH.uMiniSectorShift)
' MiniFAT-Array anlegen
If lSectMiniFAT Then
' 4 Bytes pro Ministream-Sektor. Die Gesamtgröße des Arrays muss dem Vielfachen
' der Sektorengröße des CF entsprechen
ReDim lMiniFAT2(((Int(lSectMiniFAT * 4 / lSectorSize) + _
Abs((lSectMiniFAT * 4) Mod lSectorSize <> 0)) * lSectorSize) / 4 - 1)
Else
' Das Demo legt in jedem Fall eine MiniStream-Umgebung an. (optional)
ReDim lMiniFAT2(lSectorSize / 4 - 1)
End If
 
If UBound(bMiniStream2) > 0 Then
' Die Gesamtgröße des Ministreams muss dem Vielfachen
' der Sektorengröße des CF entsprechen
ReDim Preserve bMiniStream2((Int((UBound(bMiniStream2) + 1) / lSectorSize) + _
Abs((UBound(bMiniStream2) + 1) Mod lSectorSize <> 0)) * lSectorSize - 1)
Else
' Das Demo legt in jedem Fall eine MiniStream-Umgebung an. (optional)
ReDim bMiniStream2(lSectorSize - 1)
End If
 
' Directory-Stream anlegen. Die Gesamtgröße des Streams muss dem Vielfachen
' der Sektorengröße des CF entsprechen.
ReDim bSSDE2(((Int((UBound(tSSDE2) + 1) * 128 / lSectorSize) + _
Abs(((UBound(tSSDE2) + 1) * 128) Mod lSectorSize <> 0)) * lSectorSize) - 1)
 
' Gesamtgröße der Daten im Compound File
cbDataSize = (UBound(bMiniStream2) + 1) + (UBound(bDataStream2) + 1) + _
(UBound(bSSDE2) + 1) + ((UBound(lMiniFAT2) + 1) * 4)
 
' FAT-Array anlegen. 4 Bytes pro Sektor. Die Gesamtgröße des Arrays muss
' dem Vielfachen der Sektorengröße des CF entsprechen
ReDim lFAT2((Int(cbDataSize / lSectorSize * 4 / lSectorSize) + _
Abs((cbDataSize / lSectorSize * 4 Mod lSectorSize) <> 0)) * lSectorSize / 4 - 1)
 
' Platz für die FAT-Sektoren in der FAT bereitstellen
While (cbDataSize + (UBound(lFAT2) + 1) * 4) / lSectorSize > UBound(lFAT2) + 1
ReDim lFAT2(UBound(lFAT2) + lSectorSize / 4)
Wend
 
' Anzahl der FAT-Sektoren
lSectFAT = (UBound(lFAT2) + 1) * 4 / lSectorSize
 
' DIF-Array anlegen
If lSectFAT > 109 Then
' Anzahl der zusätzlichen DIF-Sektoren
' (4 Bytes pro DIF-Sektor benötigt der DIF-Pointer)
lSectDIF = (Int((lSectFAT - 109) / (lSectorSize / 4 - 1)) + _
Abs((lSectFAT - 109) Mod (lSectorSize / 4 - 1) > 1))
 
' 4 Bytes pro FAT-Sektor
ReDim lDIF2(108 + (lSectDIF * lSectorSize / 4))
 
' Platz für die DIF-Sektoren in der FAT bereitstellen
While (cbDataSize + ((UBound(lFAT2) + 1) * 4) + _
(lSectDIF * lSectorSize)) / lSectorSize > UBound(lFAT2) + 1
ReDim lFAT2(UBound(lFAT2) + lSectorSize / 4)
lSectFAT = lSectFAT + 1
Wend
 
' Platz für die FAT-Sektoren in der DIF bereitstellen
If UBound(lDIF2) + 1 - lSectDIF + 1 < lSectFAT Then
ReDim lDIF2(UBound(lDIF2) + lSectorSize / 4)
lSectDIF = lSectDIF + 1
' Platz für die DIF-Sektoren in der FAT bereitstellen
If (cbDataSize / lSectorSize) + lSectDIF + lSectFAT > UBound(lFAT2) + 1 Then
ReDim lFAT2(UBound(lFAT2) + lSectorSize / 4)
lSectFAT = lSectFAT + 1
End If
End If
 
Else
' Keine zusätzlichen DIF-Sektoren nötig
ReDim lDIF2(108)
 
End If
 
' Inizialisierung der DIF und der FATs
l = 0
Do
If l <= UBound(lDIF2) Then lDIF2(l) = FREESECT
If l <= UBound(lFAT2) Then lFAT2(l) = FREESECT
If l <= UBound(lMiniFAT2) Then lMiniFAT2(l) = FREESECT
l = l + 1
If (l > UBound(lDIF2)) And (l > UBound(lFAT2)) And (l > UBound(lMiniFAT2)) Then
Exit Do
End If
Loop
 
' FAT-Sektoren in die DIF eintragen
l = 0
i = 0
Do
' Schleife verlassen, wenn alle Sektoren eingetragen wurden
If l - i >= lSectFAT Then Exit Do
' FAT-Sektor eintragen (FAT beginnt nach der DIF)
lDIF2(l) = l - i + lSectDIF
l = l + 1
' Das letzte Feld eines DIF-Sektors zeigt auf den nächsten DIF-Sektor
If (l - 108) Mod (lSectorSize / 4) = 0 Then
' Nächsten DIF-Sektor eintragen (DIF beginnt bei Sektor 0)
lDIF2(l) = i
' Nächster DIF-Sektor u. Anzahl der DIF-Felder die nicht für FAT-Sektoren stehen
i = i + 1
l = l + 1
End If
Loop
 
' Neuen Compound File Header inizialisieren
tSSH2 = tSSH
 
' Anzahl der DIF-Sektoren eintragen
tSSH2.csectDif = lSectDIF
If lSectDIF Then
' DIF-Sektoren beginnen bei Sektor 0
tSSH2.sectDifStart = 0
For i = 1 To lSectDIF
' DIF-Sektoren in die FAT eintragen
lFAT2(i - 1) = DIFSECT
Next i
 
Else
' Keine zusätzlichen DIF-Sektoren vorhanden
tSSH2.sectDifStart = ENDOFCHAIN
End If
 
' Anzahl der FAT-Sektoren eintragen
tSSH2.csectFat = lSectFAT
For i = 1 To lSectFAT
' FAT-Sektoren in die FAT eintragen (FAT beginnt nach der DIF)
lFAT2(lSectDIF + i - 1) = FATSECT
Next i
 
' Ersten Sektor des Verzeichnis-Arrays eintragen
tSSH2.sectDirStart = lSectDIF + lSectFAT
For i = 1 To Int((UBound(bSSDE2) + 1) / lSectorSize)
' Sektoren des Verzeichnis-Arrays verketten
lFAT2(tSSH2.sectDirStart + i - 1) = tSSH2.sectDirStart + i
Next i
' Letzten Sektor der Kette als EndOfChain kennzeichnen
lFAT2(tSSH2.sectDirStart + Int((UBound(bSSDE2) + 1) / lSectorSize) - 1) = ENDOFCHAIN
 
' Ersten Sektor der MiniFAT eintragen (Beginnt nach dem Verzeichnis-Array)
tSSH2.sectMiniFatStart = tSSH2.sectDirStart + ((UBound(bSSDE2) + 1) / lSectorSize)
' Anzahl der Sektoren der MiniFAT eintragen
tSSH2.csectMiniFat = (UBound(lMiniFAT2) + 1) * 4 / lSectorSize
For i = 1 To tSSH2.csectMiniFat
' Sektoren der MiniFAT verketten
lFAT2(tSSH2.sectMiniFatStart + i - 1) = tSSH2.sectMiniFatStart + i
Next i
' Letzten Sektor der Kette als EndOfChain kennzeichnen
lFAT2(tSSH2.sectMiniFatStart + tSSH2.csectMiniFat - 1) = ENDOFCHAIN
 
' Ersten Sektor des Mini-Streams eintragen
tSSDE2(0).sectStart = tSSH2.sectMiniFatStart + tSSH2.csectMiniFat
For i = 1 To (UBound(bMiniStream2) + 1) / lSectorSize
' Sektoren des Mini-Streams verketten
lFAT2(tSSDE2(0).sectStart + i - 1) = tSSDE2(0).sectStart + i
Next i
' Letzten Sektor der Kette als EndOfChain kennzeichnen
lFAT2(tSSDE2(0).sectStart + (UBound(bMiniStream2) + 1) / lSectorSize - 1) = ENDOFCHAIN
 
' Verkettung der einzelnen Streams in der FAT bzw. in der Mini-FAT
' Zeiger auf den ersten Sektor im Mini-Stream
lPointer1 = 0
' Zeiger auf den ersten Sektor des Daten-Streams im Compound File
lPointer2 = tSSH2.sectMiniFatStart + tSSH2.csectMiniFat + ((UBound(bMiniStream2) + 1) / lSectorSize)
' Alle Einträge im Verzeichnis-Array durchlaufen
For i = 1 To UBound(tSSDE2)
If tSSDE2(i).ulSize > 0 Then
If tSSDE2(i).ulSize < tSSH.ulMiniSectorCutoff Then
' Ersten Sektor im Mini-Stream eintragen
tSSDE2(i).sectStart = lPointer1
For l = 1 To Int(tSSDE2(i).ulSize / (2 ^ tSSH.uMiniSectorShift))
' Stream in der MiniFAT verketten
lMiniFAT2(lPointer1) = lPointer1 + 1
lPointer1 = lPointer1 + 1
Next l
 
' Letzten Sektor der Kette als EndOfChain kennzeichnen
If tSSDE2(i).ulSize Mod (2 ^ tSSH.uMiniSectorShift) = 0 Then
lMiniFAT2(lPointer1 - 1) = ENDOFCHAIN
Else
lMiniFAT2(lPointer1) = ENDOFCHAIN
lPointer1 = lPointer1 + 1
End If
 
Else
' Ersten Sektor des Streams eintragen
tSSDE2(i).sectStart = lPointer2
For l = 1 To Int(tSSDE2(i).ulSize / lSectorSize)
' Stream in der FAT verketten
lFAT2(lPointer2) = lPointer2 + 1
lPointer2 = lPointer2 + 1
Next l
 
' Letzten Sektor der Kette als EndOfChain kennzeichnen
If tSSDE2(i).ulSize Mod lSectorSize = 0 Then
lFAT2(lPointer2 - 1) = ENDOFCHAIN
Else
lFAT2(lPointer2) = ENDOFCHAIN
lPointer2 = lPointer2 + 1
End If
 
End If
End If
Next i
 
' Verzeichnis-Array als Bytestream bereitstellen
For i = 0 To UBound(tSSDE2)
CopyMemory bSSDE2(i * 128), tSSDE2(i).ab(0), 128
Next i
 
' Das Compound File speichern
i = FreeFile
Open sFileName For Binary Access Write As i
Put i, , tSSH2 ' Compound-File-Header
Put i, , lDIF2 ' DIF-Sektoren
Put i, , lFAT2 ' FAT-Sektoren
Put i, , bSSDE2 ' Verzeichnis-Array
Put i, , lMiniFAT2 ' MiniFAT-Sektoren
Put i, , bMiniStream2 ' Mini-Stream
Put i, , bDataStream2 ' Daten-Stream
Close i
 
End Function
 
 

Mit der nachfolgenden Unterfunktion wird ein Stream aus seinen Sektoren zusammengesetzt. Die Größe des Streams entscheidet, ob die Sektoren im Ministream liegen oder ob es sich um Sektoren des Compound File handelt. 

Private Function ReadStream(ByVal l As Long, lLength As Long, bStream1() As Byte, tSSH As StructuredStorageHeader, _
lFAT() As Long, lMiniFAT() As Long, bDocfile() As Byte, bMiniStream() As Byte)
Dim i As Integer
 
' Prüfen ob die Daten im Ministream liegen oder im Compound File
If lLength >= tSSH.ulMiniSectorCutoff Then
' Stream bis zum Ende der Sektorenkette zusammensetzen
While Not lFAT(l) = ENDOFCHAIN
ReDim Preserve bStream1((i + 1) * (2 ^ tSSH.uSectorShift))
CopyMemory bStream1(i * (2 ^ tSSH.uSectorShift)), _
bDocfile(Len(tSSH) + (109 * 4) + (l * (2 ^ tSSH.uSectorShift))), (2 ^ tSSH.uSectorShift)
l = lFAT(l)
i = i + 1
Wend
' Letzten Sektor der Kette anfügen
ReDim Preserve bStream1((i + 1) * (2 ^ tSSH.uSectorShift) - 1)
CopyMemory bStream1(i * (2 ^ tSSH.uSectorShift)), _
bDocfile(Len(tSSH) + (109 * 4) + (l * (2 ^ tSSH.uSectorShift))), (2 ^ tSSH.uSectorShift)
 
Else
' Stream bis zum Ende der Sektorenkette zusammensetzen (Sektoren liegen im Ministream)
While Not lMiniFAT(l) = ENDOFCHAIN
ReDim Preserve bStream1((i + 1) * (2 ^ tSSH.uMiniSectorShift))
CopyMemory bStream1(i * (2 ^ tSSH.uMiniSectorShift)), _
bMiniStream((l * (2 ^ tSSH.uMiniSectorShift))), (2 ^ tSSH.uMiniSectorShift)
l = lMiniFAT(l)
i = i + 1
Wend
' Letzten Sektor der Kette anfügen (Sektor liegt im Ministream)
ReDim Preserve bStream1((i + 1) * (2 ^ tSSH.uMiniSectorShift) - 1)
CopyMemory bStream1(i * (2 ^ tSSH.uMiniSectorShift)), _
bMiniStream((l * (2 ^ tSSH.uMiniSectorShift))), (2 ^ tSSH.uMiniSectorShift)
 
End If
 
End Function
 
 

Damit bin ich am Ende angekommen und hoffe, dass ich mit meinem Beispiel diesem komplexen Thema einigermaßen gerecht werden konnte. Auch wenn es hier um eine Funktion für Microsoft Access geht, stehen doch im Wesentlichen die OLE-Objekte und das Compound File Format im Vordergrund. Sinnvollerweise verwende ich bei der Neuerstellung des Compound Files verschiedene Daten direkt aus dem Compound File des OLE2-Objekts. Wenn man alle Daten selber bereitstellt, kann man auf dieselbe Weise auch Compound Files (Struktured Storage) mit beliebigem Inhalt erzeugen. Beim Wiederherstellen der eingebetteten Microsoft Office Dateien werden diese automatisch defragmentiert. Bei den Excel-Dateien muss man noch die Mappe einblenden, dafür suche ich die entsprechende Struktur im Workbook-Stream und setze dort ein entsprechendes Bit auf 0. Diese Funktion und der Workbook-Stream gehören aber nicht zu dieser Dokumentation. Sie können sich das vollständige Demo herunterladen, dann steht Ihnen ein lauffähiges Beispiel zur Verfügung. 

Download für Access 2002/2003
ExtractMSOfficeFiles-Demo (Access 2002)
Download für Access 2000
ExtractMSOfficeFiles-Demo (Access 2000)
(downgrade)
Download für Access 97
ExtractMSOfficeFiles-Demo (Access 97)
(downgrade)
Installation:
Einfach runterladen und anschauen.
©   Oliver Straub  -  Fliegenstr. 6  -  80337 München