Pure-C ist eine hervorragende Entwicklungsumgebung für C-Programme. Nur schade, daß die Help-Screens nicht in gedruckter Form mitgeliefert werden. Diesem Mißstand kann durch HELP_RC Abhilfe geschaffen werden. Mehr noch. Mit HELP_RC haben Sie die Möglichkeit, die mitgelieferten HLP-Dateien zu ändern und wieder zu übersetzen. Interessiert, wie man das macht? Ja? Dann lesen Sie bitte weiter.*
Um es vorweg zu sagen, das Listing vom Help-Screen-Recompiler ist mit etwa 1600 Programmzeilen viel zu lang, um es in der Rubrik Grundlagen abzudrucken. Auf der zu diesem Heft gehörenden Monatsdiskette werden der Quelltext und das eigentliche Programm erhältlich sein. Im folgenden werde ich den Aufbau der HELP-Dateien, soweit er für die Recompilierung der Help-Screens wichtig ist, erläutern.
Eine jede Help-Datei beginnt mit dem Dateikopf, der bis zur Dateiposition 0x88 reicht. In ihm werden neben dem Copyright, der Versionsnummer des Help-Compilers, der die HLP-Datei erzeugt hat, auch die Lage und Größe einiger Tabellen gespeichert. Hinter diesen Tabellen liegen die lückenlos aneinandergereihten Help-Screens (Abb. 1).
Direkt auf den Kopf folgen die Screen-, die String-, die c_key- und die _s key-Tabelle. Die Screen-Tabelle enthält für jeden sich in der Help-Datei befindlichen Screen dessen Anfangsposition. Der letzte Eintrag der Screen-Tabelle stellt keinen Verweis dar, sondern wird für die Berechnung der Länge eines Screens ausgenutzt. Wer jetzt gedacht hat, er bräuchte nur mit Hilfe der Dateipositionen aus der Screen-Tabelle die Screens anzusteuern und alles, was kommt, auszugeben, wird enttäuscht. Denn jeder Screen wurde durch den Help-Compiler kodiert.
Häufig in den Help-Screens auftretende Zeichenketten werden in der String-Tabelle abgelegt. Das spart u.U. enorm viel Speicherplatz. Die über die Direktiven „sensitive“ und „capsensitive“ definierten Suchwörter werden in der s_key- bzw. c_key-Tabelle gespeichert.
Nachdem jetzt der Grobaufbau geklärt ist, kann man an die Beschreibung der Einzelheiten gehen. Der wesentliche Teil des Dateikopfes beginnt an der Dateiposition 0x58 - vom Dateianfang an gerechnet - und ist 48 Bytes lang. Der Aufbau dieses Bereiches wird durch die Datenstruktur HLPHDR beschrieben.
typedef struct {
long scr_tab_size;
long str_tab_offset;
long str_tab_size;
char char_table[12];
long c_key_tab_offset;
long c_key_tab_size;
long c_key_count;
long s_key_tab_offset;
long s_key_tab_size;
long s_key_count;
} HLPHDR;
Scr_tab_size gibt die Länge der Screen-Tabelle in Bytes an. Sie beginnt immer ab der Position 0x88. Jeder Eintrag hat eine Länge von 4 Bytes (Abb. 2).
Str_tab_offset und str_tab_size beschreiben die Startposition der String-Tabelle und deren Größe. Die String-Tabelle ist zweiteilig. Der erste Teil enthält eine Liste von Verweisen auf die eigentlichen Zeichenketten. Der zweite Teil besteht aus den Zeichenketten selbst. Dabei ist noch eine Besonderheit zu beachten: Die Zeichenketten sind nicht sofort erkennbar, man muß erst noch die XOR-Funktion auf jedes Byte mit dem Wert 0xA3 anwenden. Das ist reinste Verschleierungstaktik (Abb. 3)! Die zwölf häufigsten Zeichen, die in der Help-Datei Vorkommen, werden in der char_table abgespeichert.
C_key_offset, c_key_size und c_key_count geben die Anfangsposition der c_key-Tabelle, deren Länge sowie die Anzahl der „capsensitiven“ Suchwörter an. Die letzten drei Einträge gelten entsprechend für die s_key-Tabelle. Sowohl die c_key- als auch die s_key-Tabelle sind zweigeteilt. Am Anfang steht für jedes S uchwort ein Eintrag vom Typ SEARCH_KEY.
typedef struct {
unsigned long keyword_offset;
unsigned WORD code;
} SEARCH_KEY_ENTRY;
Direkt auf diese Liste folgen die Such wörter im ASCII-Code (Abb. 4). Im Feld code wird der dem Suchwort zugeordnete Screen vermerkt. Addiert man keyword_offset zur Adresse eines Eintrags, erhält man einen Zeiger auf den Anfang des Suchwortes.
Um ein Zeichen, das in der char_table vorkommt, zu verschlüsseln, genügt es, die Position des Zeichens in der Tabelle aufzuschreiben. Dieser Index hat in einem Halb-Byte (= Nibble = 4 Bit) Platz. Dieses Vorgehen reduziert den Speicherplatzbedarf für eines der 12 häufigsten Zeichen auf die Hälfte. Mit vier Bit kann man 16 Objekte unterscheiden. Der Help-Compiler hat davon 12 für die char_table benutzt. Die restlichen Codes 0xC bis 0xF verwendet er wie folgt: Der Code 0xC besagt, daß die beiden nachfolgenden Nibbles das nächste Textzeichen bilden. Die Byte-Folge C4 1C 42 C4 3C 44 steht also für die Zeichenfolge ‘ABCD’.
Die dem Code 0xD folgenden drei Nibbles (= 12 Bit) werden zusammengefaßt. Mit Hilfe dieser 12 Bits wird ein Eintrag der Verweisliste der String-Tabelle adressiert. Dieser Eintrag enthält einen Offset. Addiert man ihn zur Anfangsadresse der String-Tabelle hinzu, erhält man die Anfangsposition der Zeichenkette.
Der Code 0xE steht für die beiden Zeichen Wagenrücklauf (CR) und Zeilenvorschub (LF).
Der letzte verbleibende Code (0xF) kennzeichnet das Ende eines kodierten Screens. Um dieses Ergebnis zu erhalten, war der Artikel [1] sehr hilfreich. Dort wird eine ältere Version des Help-Compilers für MS-DOS-Rechner beschrieben.
Jetzt hat man alles beisammen, um die Help-Screens zu entschlüsseln. Macht man das, stellt man fest, daß die dekodierten Help-Screens noch Byte-Folgen enthalten, die nicht ausgedruckt werden können. Diese Folgen beginnen und enden immer mit dem Code 0x1D. Wissen Sie jetzt, warum die Verwendung des Codes 0x1D in Hilfe-Texten verboten ist? Die zwei Bytes, die auf das erste 0x1D folgen, stellen einen Verweis auf einen Screen dar (Verweiscode). Der Rest bis zum 0x1D ist ein ASCII-String. Dieser String war in der ursprünglichen Help-Datei einmal ein einfacher Querverweis (#verweis#) bzw. ein \link-Querverweis. Der Ausgabe der Help-Screens steht nun nicht mehr viel im Wege (make_txtfile). Man braucht nur die 0x1D-Codes sowie den Verweiscode aus der Byte-Folge zu streichen. Im Help-Recompiler hat man die Möglichkeit, die Ausgabe an eigene Wünsche anzupassen. Dies geschieht durch Verändern der Datei HELP_RC.INF.
Will man nicht nur den reinen Text, sondern auch noch eine wieder compilierbare Hilfe-Text-Datei haben, muß man die Beziehungen zwischen den einzelnen Help-Screens und ihrer Abbildung durch den Help-Compiler erforschen. Bevor ich den Übersetzungsvorgang grob skizziere, will ich die Möglichkeiten, die der Help-Compiler zur Erstellung einer Hilfedatei bietet, auflisten [2]: 1) Jeder Help-Screen kann einen Namen haben. Ja, Sie haben richtig gelesen. Bei meiner Analyse stellte sich nämlich heraus, daß manche Screens keinen Namen hatten. Auf sie wurde aber durch sensitive bzw. capsensitive Suchwörter verwiesen. Ich dachte zuerst, daß ich einen Fehler gemacht hätte, doch als ich eine Help-Datei der Art
screen( sensitive("verweis"))
Dies ist ein Screen ohne Namen.
\end
zu übersetzen versuchte, gab es keinerlei Fehlermeldungen bzw. Warnungen. Da die Entwickler von Turbo-C bzw. Pure-C dieses Verhalten in ihren Hilfedateien genutzt haben, denke ich, daß man auch in eigenen Hilfedateien davon Gebrauch machen kann.
Klammert man einen Screen-Namen <scr_name> mit Hilfe der Zeichenfolge *#’ im laufenden Text, hat man einen einfachen Querverweis auf den Screen <scr_name> erzeugt.
Eine weitere Möglichkeit, einen Querverweis herzustellen, besteht in der Verwendung des link-Konstruktes. Es hat folgende Form:
\link („verweis“) Zeichenfolge, die zum Screen mit Namen ‘verweis’ führen soll#
Beim Übersetzen geht der Compiler so vor, daß er zuerst einen Screen mit dem Namen „Copyright“ erzeugt. Danach gibt er einen weiteren Screen aus. Diesem gibt er den Namen „INDEX“. In diesem werden die Wörter ‘A..’ bis ‘Z..’ sowie das Wort ‘Sonstiges’ eingetragen. Jedes dieser Wörter wird von 0x1D-Zeichen zusammen mit dem Verweiscode eingerahmt. Der Verweiscode dient der Verzweigung. Der Screen ‘A..’ z.B. enthält alle mit ‘A’ bzw. ‘a’ beginnenden Screen-Namen sowie alle mittels der sensitive- bzw. capsensitive-Direktive erzeugten Suchwörter. Die Screens ‘A..’ bis ‘Sonstiges..’ will ich als Subindex-Screens bezeichnen. Die Beziehungen zwischen Index- und Subindex-Screens sind einem baumstrukturierten Inhaltsverzeichnis recht ähnlich. Abbildung 5 zeigt die Struktur des Index- und der Subindex-Screens.
Wie werden nun die Subindex-Screens gefüllt? Der Help-Compiler liest die ASCII-Help-Datei ein und trägt jeden gefundenen Screen-Namen in den entsprechenden Subindex-Screen ein. Stößt er bei der Bearbeitung auf die Direktiven „capsensitive“ bzw. „sensitive“, werden die damit definierten Such Wörter ebenfalls in den zugehörigen Subindex-Screen übernommen. Zusätzlich wird aber noch die c_key- bzw. s_key-Tabelle ergänzt, und zwar so, daß die beiden Tabellen jeweils aufsteigend sortiert sind. Die Sortierung beschleunigt das spätere Auffinden der Screens enorm. Trifft er allerdings auf eine link-Konstruktion, wird der nach dem Querverweis folgende ASCII-String nicht in einen der Subindex-Screens eingetragen. Dieses Verhalten macht die Recompilierung etwas kniffliger. Findet der Help-Compiler eineneinfachen Querverweis#verweis#, rahmt er diesen in 0x1D-Zeichen zusammen mit dem Verweiscode ein.
Jetzt habe ich schon mehrfach vom Verweiscode gesprochen, ohne seinen genauen Aufbau zu erklären. Ein Verweiscode besteht aus einem Wort (= 16 Bit), wobei das höchste Bit immer gesetzt ist. Durch einige arithmetische Operationen wird daraus der Index für die Screen-Tabelle gewonnen (Funktion screen_index). Die ganze Rechnerei hat wahrscheinlich nur den Zweck, daß im kodierten Text kein Null-Byte vorkommt. Denn das würde das vorzeitige Ende des Textes markieren. Der Verweiscode 0xFFFF bildet eine Ausnahme. Er wird verwendet, um anzuzeigen, daß der nachfolgende ASCII-String einen Querverweis in eine andere Hilfetextdatei darstellt. Hier hat das Handbuch wieder etwas verschwiegen.
Ziel der Recompilierung soll sein, daß die Ausgabe von HELP_RC so beschaffen ist, daß ein Übersetzen mittels des Help-Compilers dasselbe Ergebnis produziert wie die ursprüngliche HLP-Datei. Dabei sehe ich davon ab, daß der Help-Compiler im Copyright-Screen immer das Datum des Übersetzungszeitpunkts einträgt.
Wichtig ist auch noch, daß damit nicht ausgesagt ist, daß die Hilfetext-Dateien identisch aufgebaut sind! Nur ihre Übersetzungen verhalten sich gleich. Aber das genügt ja.
Der erste Schritt der Rückübersetzung besteht darin, den Dateikopf (Funktion read_header), die Screen- (Funktion read_screen_table), die String- (Funktion read_string_table), die c_key- und als letztes die s_key-Tabelle (Funktion read_key_table) zu lesen. Danach setzt man den Dateizeiger auf die Position screen_tab[1]. Dort beginnt nämlich der kodierte Index-Screen. Die Position des Index-Screens ist immer der zweite Eintrag der Screen-Tabelle. Der erste Eintrag ist für den Copyright-Screen vorgesehen.
Nachdem die Dekodierung vorgenommen wurde, läuft man durch den Text und sucht die in 0x1D-Zeichen eingeschlossenen Subindex-Screen-Namen (Funktion read_Index). Dabei merkt man sich den Verweiscode. Das sind genau 27. Nachdem man jetzt die Verweiscodes der Sub-index-Screens kennt, sucht man diese der Reihe nach auf (Funktion rd_sidx_names). Alle dabei gefundenen Namen legt man zusammen mit ihrem Verweiscode und dem Namensattribut in einer Liste ab (Funktion ins_name). Ein Array ist für diesen Zweck ungeeignet, da man nie wissen kann, wieviele Namen in einer HLP-Datei sind, und man durch großzügige Dimensionierung Speicherplatz verschenken würde. Das Namensattribut dient der Unterscheidung zwischen Screen-Namen, sensitiven bzw. capsensitiven Suchwörtern und Strings, die bei der Verwendung von Mink-Verweisen entstehen (sogenannte Link-Namen). An dieser Stelle stößt man schon auf ein Problem. Nirgends wird eine Liste mit den Namen der einzelnen Screens geführt und zur Recompilierung sind sie unbedingt erforderlich. Was tun? Man nimmt fürs Erste mal an, daß alle gefundenen Namen Screen-Namen wären. Das stimmt natürlich nicht. Doch mit Hilfe der c_key- bzw. s_key-Tabelle kann man dies korrigieren (Funktion corr_attrs). Denn alle Namen, die nicht in diesen beiden Tabellen Vorkommen, müssen zwangsläufig Screen-Namen sein (vgl.: Was tut der Helpcompiler). Als nächstes sucht man alle Screens außer dem Copyright-, dem Index- und den Subindex-Screens auf, dekodiert sie und untersucht, ob sie Verweise enthalten (Funktion read_Link). Findet man dabei einen Verweis, der nicht in der Namensliste enthalten ist, kann man sicher sein, daß man einen Link-Namen gefunden hat. Dieser wird zusammen mit seinem Verweiscode in die Liste der Link-Namen eingetragen. Diese Prozedur ist aufwendig, und daraus resultiert u.a. die relativ hohe Programmlaufzeit.
Ordnet man nun noch die Namensliste so um, daß alle Namen mit demselben Verweiscode hintereinander stehen und zusätzlich noch die Reihenfolge Screen-Name, capsensitive-, sensitive- und Link-Name einhalten, ist man nicht mehr weit vom Ziel entfernt (Funktion order_nametable). Es genügt nun, die sortierte Liste der Reihe nach zu durchlaufen (Funktion recompile) und dem Namensattribut entsprechend den Namen auszugeben, und zwar solange, bis der Verweiscode wechselt. Bevor man den nächsten Eintrag der Liste verarbeitet, liest man noch den zugehörigen Screen, dekodiert ihn und ersetzt die darin enthaltenen Verweise durch die ihnen zugeordneten Namen aus der Namens- bzw. Link-Liste. Aufpassen muß man nur beim Verweiscode 0xFFFF. Dieser zeigt ja an, daß ein Screen außerhalb der aktuellen Help-Datei angesprochen werden soll. Da man diesen Screen-Namen nicht kennt und er auch für die erneute Übersetzung unwesentlich ist, habe ich dafür immer die Zeichenfolge ’%%-GLOBAL%%’ vorgesehen. Man braucht sich wegen der Warnung des Helpcompilers keine Sorgen zu machen.
Zum Schluß noch einige Bemerkungen über die Bedienung des Recompilers. Werden beim Start keine Parameter angegeben, werden sie beim Benutzer erfragt. Der erste Parameter betrifft die Programmoptionen. Als zweiter Parameter muß der Name der zu recompilierenden HLP-Datei angegeben werden. Es können bis zu drei Optionen angegeben werden. Groß-/Kleinschreibung ist dabei bedeutungslos. Der allgemeine Aufruf lautet also:
HELP_RC.TTP [-LST] helpfile
Ist die Option ‘L’ angegeben, wird eine Log-Datei mit dem Namen helpfile.LOG erzeugt. Sie enthält genaue Informationen über Tabellen, Namen und Warnungen.
Wird die Option ‘S’ gesetzt, wird die Screen-Datei mit dem Namen helpfile.SCR geschrieben. In ihr sind alle Screens in compilierbarer Form enthalten. Vordem Compilieren müssen allerdings noch die Screens Copyright und Index mit Hilfe eines Text-Editors entfernt werden.
Die Option ‘T’ schreibt den Inhalt der Screens mit den entsprechenden Ausgabeattributen in die Datei helpfile.TXT.
Literatur:
[1] Torsten Brand, Marcus Gröber, „Helfer in der Not", c’t-Magazin 11/89, Seite I84ff.
[2] Pure-C Compiler Handbuch, Anhang H, Seite 283ff.