Algorithmen und Datenstrukturen

| Algorithmen & Datenstrukturen Programmieren 1 von Prof. Jürgen Plate

# Programmierübungen in "C"

Zur Vorbereitung der Aufgabe gehören:

- sofern nicht in der Angabe vorgeschrieben, geeignete Datenstrukturen (mit Begründung),

- eine vernünftige Unterteilung in Teilprojekte (mit Beschreibung),

- sauber gezeichnete Struktogramme (sofern der Algorithmus sehr einfach ist, genügt eine verbale Beschreibung),

- ein Terminplan für die Fertigstellung der einzelnen Teilprojekte und

- abschließend vollständige C-Quellprogramme (deren Lauffähigkeit vorzuführen ist).

Je nach Komplexität der Aufgabe können einzelne Punkte stark gekürzt werden.

## Projekt: Web-Lexikon (Wiki)

Zu schreiben ist ein Programm für ein Web-basiertes elektronisches Lexikon mit folgenden Eigenschaften:

1. Jeder Benutzer kann einen Eintrag vornehmen oder korrigieren (Passwortschutz wird später implementiert).

2. Ein Lexikoneintrag ("Artikel") besteht aus:
   - Begriff (Schlagwort)
   - Erklärung mit eingestreuten Bildern und Querverweisen (Links)
   - Autorenangabe
   - Erstellungsdatum

3. Die Eingabe bzw. Änderung eines Artikels erfolgt über ein Webformular. Die vom Formular kommenden Daten müssen aufbereitet werden. Beschreibung siehe unten.

4. Die Speicherung erfolgt in Form von Dateien
   - Je Begriff eine eigene Datei
   - Dateiname = Begriff
   - Für Suche und Zugriff ist ein Index notwendig

5. Auswahl der Artikel über ein Menü der Begriffe.

6. Alternativ soll nach beliebigen Worten im Artikeltext gesucht werden können.

7. Die Ausgabe erfolgt als Webseite (vordefinierte Templates für Seitenkopf und -fuss werden von Datei eingelesen.
   - Bei Suchergebnissen werden nur die Begriffe und Artikelanfänge angezeigt (ca. 2 Zeilen) - es kann dann zum Artikel gesprungen werden.
   - Ein Artikel wird als komplette Webseite angezeigt.

Die Realisierung erfolgt in kleinen Teilaufgaben. Teile des Web-Frontends in HTML werden fertig zur Verfügung gestellt. Der Webserver nimmt uns viel Arbeit ab. Dahinter steckt das Common Gateway Interface.

### Testumgebung Damit Sie Ihre Programme auch testen können, wurde eine Testumgebung auf dem Praktikumsrechner eingerichtet. Es läuft ein Webserver, der ein sogenanntes "User-Directory" erlaubt. Jeder Benutzer kann damit Webseiten und auch CGI-Programme anbieten. Dazu wird im Heimatverzeichnis ein Unterverzeichnis `www` eingerichtet, in das alle Webseiten gelegt werden (Dateiendung `.html`). Das `www`-Verzeichnis enthält ein Unterverzeichnis namens `cgi-bin`, in dem alle Programme untergebracht sind (Dateiendung `.cgi`). Zur Einrichtung des Ganzen öffnen Sie ein Shell-Fenster und rufen Sie dann das Kommando `machwww` auf. Dieses Programm richtet die Verzeichnisse ein und erzeugt auch eine (sehr einfache) Datei `index.html` sowie ein ebenso einfaches CGI-Programm namens `huhu.cgi` (einschl. Quelltext). Zum Testen können im Browser dann die beiden Webadressen `http://localhost/~`username`/index.html` bzw. `http://localhost/~`username`/cgi-bin/huhu.cgi` verwendet werden. Eigene CGI-Programme müssen aus dem Kdevelop-Projektverzeichnis ins CGI-Verzeichnis umkopiert und umbenannt werden. Das geschieht durch das Kommando (aus dem Heimatverzeichnis `/home/`Username heraus). Ausserdem müssen die Zugriffsrechte gesetzt werden: `cp` Projektname`/debug/src/`Projektname `www/cgi-bin/`Projektname`.cgi` `chmod 755 www/cgi-bin/`Projektname`.cgi` Für Projektname müssen Sie natürlich den von Ihnen verwendeten Namen des Projekts (in der Regel `wiki`) eingeben. Damit Sie sich leichter tun, gibt es zwei Hilfskommandos: `copycgi` Projektname kopiert Ihr Binärprogramm ins CGI-Verzeichnis und versieht es dort auch gleich mit der Endung `.cgi`. `gibfrei` setzt die Zugriffsrechte für alle Dateien in Ihrem `www`-Verzeichnis so, dass auf diese per Browser zugegriffen werden kann.

### 1. Aufgabe

Schreiben Sie ein Programm, das von der Standardeingabe beliebig viele und beliebig lange Textzeilen entgegennimmt und diese weiterbehandelt:

- Die Paare "Name=Wert" werden getrennt (Trennstelle "&"),
- die Ersatzzeichen ('+' und "%xx") werden konvertiert,
- die Werte für Begriff, Autor und Datum kommen am Anfang der Eingabe und können gleich verarbeitet werden (s. unten),
- der Rest der Eingabe beinhaltet den beschreibenden Text,
- auch hier weden die Ersatzzeichen ('+' und "%xx") konvertiert.

In die Datei werden die Daten folgendermassen gespeichert:

- 1. Zeile: Begriff:Autor:Datum
- folgende Zeilen: Artikeltext

Hinweise zur Lösung: Verwenden Sie das in der Vorlesung besprochene Rumpfprogramm wiki.c und erweitern Sie es um die Speicherung der Daten in einer Datei. Beachten Sie auch die Kommentare dazu im Programm.

### 2. Aufgabe

In dieser Aufgabe geht es um die Erfassung aller in einem Verzeichnis gespeicherter Dateien. Um ein Verzeichnis auszulesen, wird es zuerst mit dem Aufruf `opendir()` geöffnet, dann können die Einträge nacheinander mit `readdir()` gelesen werden. Mit `closedir()` wird es wieder geschlossen. Analog dem Dateihandle gibt es ein Verzeichnishandle, das vom Datentyp `DIR*` ist. Informationen über den Eintrag liefert die von `readdir()` gelieferte Struktur `dirent`. Die Funktionen sind folgendermassen definiert:

    #include <sys/param.h>
    #include <dirent.h>

    DIR *opendir(const char *pfadname);
    struct dirent *readdir(DIR *dir);
    int closedir(DIR *dir);

Die Funktion `opendir()` erhält als Parameter den Namen des Verzeichnisses. Der Rückgabewert ist ein Zeiger auf das Verzeichnis-Handle. Ein Fehler wird dadurch angezeigt, dass dieser Zeiger den Wert NULL besitzt.

Die Funktion `readdir()` liest den nächsten Eintrag im Verzeichnis und erhält als Rückgabewert einen Zeiger auf einen Wert vom Typ `dirent`:

    struct dirent
      {
      long            d_ino;  /* Inode Nummer */
      off_t           d_off;  /* Offset zum naechsten dirent */
      unsigned short  d_reclen;/* Laenge dieses Eintrags */
      char            d_name[MAXNAMELEN+1]; /* Dateiname */
      };

Für das Anwenderprogramm ist eigentlich nur der Name des Eintrags interessant. Wollen Sie mehr über diesen Eintrag erfahren, beispielsweise, ob es wieder ein Verzeichnis ist, so brauchen Sie dazu andere Systemaufrufe (z. B. `stat()`). Zuletzt wird das Verzeichnis mit `closedir()` wieder geschlossen. Ein Beispielprogramm für das Auslesen eines Verzeichnisses sieht so aus:

    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/param.h>

    // fuer Anzeige des Verzeichnis-Inhalts
    #include <dirent.h>

    int main(void)
      {
      DIR *dir;
      struct dirent *dirzeiger;
      char dateiname[MAXNAMELEN+1];      /* MAXNAMELEN definiert in param.h */
      char dirname[MAXNAMELEN+1] = ".";  /* --> Laenge reicht garantiert  */

      printf("Alle Dateien im aktuellen Verzeichnis: %s\n",dirname);

      dir = opendir(dirname);
      if(dir != NULL)
        {
        while((dirzeiger = readdir(dir)) != NULL)
          {
          strcpy(dateiname,(*dirzeiger).d_name);
          printf("%s\n",dateiname);
          }
        }
      closedir(dir);

      return 0;
      }

Das Programm können Sie unter dir.c herunterladen.

#### Aufgabenstellung:

1. Ändern Sie das Programm der 1. Aufgabe so ab, dass die Dateien nicht im aktuellen Verzeichnis, sondern in einem vorgegebenen Verzeichnis abgelegt werden, das durch

       #define DATADIR "/home/wiki"

   definiert ist (Stringfunktionen `strcat` bzw. `strcpy`!).

2. Schreiben Sie nun eine Funktion, die ein Auswahlformular für alle Dateien im oben angegebenen Verzeichnis ausgibt. Dabei sollen Dateien, die mit einem Punkt beginnen, nicht berücksichtigt werden. Das Formular beginnt mit den Zeilen (Erläuterung in der Vorlesung):

       <HTML>
       <HEAD>
       <TITLE>Lexikon-Ausgabeformular</TITLE>
       </HEAD>
       <BODY>
       <H1>Lexikon-Ausgabe</H1>

       <P>
       <FORM method=POST action="http://localhost/~testuser/www/cgi-bin/wiki.cgi">
       <SELECT NAME="Datei" SIZE="5">

   Danach folgt ihre Dateiausgabe, wobei für jede Datei eine Zeile der Form

       <OPTION>Dateiname</OPTION>

   ausgegeben werden soll. Die Ausgaben schliesst mit den Zeilen:

       </SELECT>
       <P>
       <INPUT TYPE="SUBMIT" NAME="SUBMIT" VALUE="Absenden">
       </FORM>
       </BODY>
       </HTML>

3. Schreiben Sie eine Funktion, welche eine dem Eingabeformular (ppform.html) vergleichbare Ausgabe erzeugt, jedoch die Inhalte einer vorgegenen Datei (Name wird als Parameter übergeben) in das Formular einfügt. Dazu wird bei jedem Eingabefeld vor der schliessenden spitzen Klammer die Angaben `VALUE="....."` eingefügt, aus der Originalzeile `<INPUT TYPE=TEXT SIZE=60 NAME="Begriff">` wird dann beispielsweise `printf("<INPUT TYPE=TEXT SIZE=60 NAME=\"Begriff\" VALUE=\"%s\">\n",Begriff);`. Beim Eingabefeld "Textarea" erfolgt die Ausgabe direkt zwischen den Tags `<TEXTAREA>` und `</TEXTAREA>`.

   Dabei ist es nur nötig, die erste Zeile in die drei Felder "Begriff", "Autor" und "Datum" zu zerlegen. Der Rest der Datei kann an der oben genannten Stelle (Textarea) einfach auf die Standardausgabe kopiert werden. Beachten Sie dazu auch die Erläterungen in der Vorlesung.

Hinweis: Wir brauchen die Webseitensprache HTML zwar nur als Vehikel (weshalb die Sprachelemente in der Regel vorgegeben werden), aber wer sich dafür interessiert, findet eine knappe Einführung unter http://www.netzmafia.de/skripten/easyhtml/index.html.

### 3. Aufgabe

Nun wollen wir das bisher geleistete in den Webserver integrieren. Dazu müssen Sie noch kleine Modufikationen an den Programmen vornehmen, die Quellcodes übersetzen und das ausfübare Programm in Ihr cgi-bin-Verzeichnis kopieren (Hinweis: Programm `copycgi` verwenden). Für die weiten Beschreibungen und Aufgaben gebe ich den Programmen folgende Namen (bei Ihnen heissen sie unter Umständen anders):

- Eingabeprogramm aus Aufgabe 1: `w_ein.cgi`.
- Update-Programm aus Aufgabe 2: `w_upd.cgi`.

Wobei letzteres erst jetzt fertiggestellt wird.

Beim Programm aus Aufgabe 1 müssen Sie nur noch die `#define`-Zeilen ändern, indem Sie bei den folgenden Zeilen die Kommantare ("// ") entfernen:

    // #define REQUEST_METHOD getenv("REQUEST_METHOD")
    // #define CONTENT_LENGTH getenv("CONTENT_LENGTH")
    // #define QUERY_STRING getenv("QUERY_STRING")

Die folgenden Test-Zeilen werden gelöscht.

    /* Fuer's Testen definieren wir die Dinger selbst */
    #define REQUEST_METHOD "GET"
    #define CONTENT_LENGTH "119"
    #define QUERY_STRING "Begriff=G%E4nsegeier&Autor=...

Dann muss das CGI-Programm ja noch eine Antwort geben, z. B. "Ihre Eingabe wurde gespeichert". Das muss natürlich in einem HTML-Umfeld verpackt werden ("Content-Type"-Zeile nicht vergessen!). Das könnte etwa folgendermassen aussehen:

    printf("Content-Type: text/html\r\n\r\n");
    printf("<HTML><HEAD><TITLE>Wiki-Eingabe</TITLE></HEAD>\n");
    printf("<BODY><H1>Wiki-Eingabe</H1>\n");
    .
    .
    .
    printf("Ihre Eingabe wurde gespeichert\n");
    printf("</BODY></HTML>\n");

Nun können Sie das schon mal testen. Dazu übersetzen Sie das Programm und kopieren dann den erzeugten Binärcode in Ihr `cgi-bin`-Verzeichnis (Programm `copycgi`). Anschriessend sind die Execute-Rechte zu setzen (Programm `gibfrei`). Bei eventuell fehlerhaften Verhalten können Sie die Fehlerausgabe des Webservers in einem Shell-Fenster mithilfe des Kommandos `tail -f /var/apache/logs/error_log` beobachten (Abbruch des Programms mit `Strg-C`).

Damit es beim Testen keine "Interferenzen" gibt (53 mal der "Gänsegeier"), nehmen Sie als Begriff Ihren Usernamen oder geben etwas Sinnvolles ein.

Das Programm aus Aufgabe 2 ist noch nicht fertig und wird nun fertiggestellt. Bisher gibt es zwei Funktionen, die bei mir die Namen

`dirlist()` für die Verzeichnisanzeige und `showform()`für die Ausgabe des ausgefüllten Formulars

heissen. Um das Programm zu vervollständigen, fischen Sie sich aus der originalen Datei wiki.c die Funktionen

    char x2c(char hex[]);
    void unescape(char *url);
    void CGIReadInput(void);

heraus und hängen diese an Ihren Programmcode an. Die Funktion `CGIReadInput()` erweitern Sie diesmal so, dass der Formularwert "Datei" herausgeholt wird. Da aber nun die Möglichkeit besteht, dass `CGIReadInput()` auch ohne Parameter aufgerufen wird, ist noch eine weitere Änderung notwendig:

    .
    .
    .
    if (CONTENT_LENGTH == NULL)
    {
    /* Hier Dateiname auf "" setzen o. ä */
    return;   /* statt exit(1) */
    }

Nun gibt es zwei Möglichkeiten:

- Fehlt der Name "Datei" oder hat er keinen vernünftigen Wert (Datei nicht vorhanden), dann wird die Funktion `dirlist()` aufgerufen. Die Funktion stellt auch wieder ein Formular dar, das sich wieder an das Programm selbst richtet (`...action="http://localhost/~testuser/cg-bin/w_upd.cgi...` ).
- Gibt es den Parameter (und die Datei), wird die Funktion `showform()` mit dem entsprechenden Dateiinhalt aufgerufen. Prüfen Sie auf jeden Fall, ob die Datei zum Lesen geöffnet werden kann. Auch diese Funktion stellt ein Formular dar, das sich aber an das erste Programm richtet (`...action="http://localhost/~testuser/cg-bin/w_ein.cgi...`).
- Das Hauptprogramm sieht dann etwas folgendermassen aus:

      int main(void)
        {
        char filename[MAXNAMELEN+1] = "";

        printf("Content-Type: text/html\r\n\r\n");

        CGIReadInput(filename);
        if (strcpy(filename,"") == 0)
          dirlist(dirname);
        else
          {
          strcat(dirname,filename);
          showform(dirname);
          }
        return EXIT_SUCCESS;
        }

Nach dem Erzeugen des Binärcodes können Sie nun testen, ob Sie Ihre vorher erstellte Datei auch Ändern können.

Anmerkung: Alle Programme im `cgi-bin`-Verzeichnis laufen mit der Benutzerberechtigung des Webserver-Programms. Das Verzeichnis `/home/wiki` hat Schreibberechtigung fü diesen Benutzer. Beim lokalen Test in der KDevelop-Umgebung laufen die Programme natürlich mit Ihrer Benuterberechtigung, weshalb Sie auf `/home/wiki` nicht problemlos zugreifen können. Es ist also sinnvoll diesen Pfad als Konstante zu definieren (`#define ...`) und vor dem Erzeugen der Endversion umzustellen.

### 4. Aufgabe

Bleibt noch die Suche nach Dateiinhalten. Auch das soll in kleinen Schritten erarbeitet werden:

1. Beginnen Sie ein neues Projekt und schreiben Sie eine Funktion `void nextword(FILE *fp, char *wort)`, welche aus einer zuvor geöffneten Datei das nächste "Wort" liefert. Als "Wort" gilt alles, was mit einem Buchstaben beginnt und aus Buchstaben und Zahlen besteht. Bedenken Sie dabei, dass die deutschen Umlaute und das scharfe S nicht Bestandteil des ASCII-Zeichensatzes sind (und damit auch nicht von den Bibliotheksfunktionen als Buchstaben erkannt werden). Sie müssen im Programm deshalb getrennt behandelt werden. Andere Zeilen (die ggf. vor dem Wort stehen, werden überlesen. Ist das Dateiende erreicht, wird ein Leerstring zurückgegeben. Das Hauptprogramm zum Testen hat folgendes Aussehen:

       int main(int argc, char *argv[])
         {
         char Dateiname[MAXNAMELEN+1] = "Textdatei";
         char Wort[1000];
         FILE *fp;

         fp = fopen(Dateiname,"r");
         if (fp == NULL)
           {
           printf("Kann Datei %s nicht oeffnen\n",Dateiname);
           exit(1);
           }
         do
           {
           nextword(fp,Wort);
           printf(">>>%s&tl;<<\n",Wort);
           }
         while (Wort[0] != '\0');
         fclose(fp);

         return EXIT_SUCCESS;
         }

2. Ändern Sie nun die Schleife so, dass sie beendet wird, sobald das gefundene Wort mit einem vorgegebenen Suchwort übereinstimmt und "verpacken" Sie den gesamten Code (`fopen ... fclose`) in einer weiteren Funktion `int scanfile(char *Dateiname, char *Suchwort)`, die 0 zurückgibt, wenn das Wort nicht gefunden wurde und 1, wenn es in der Datei enthalten war. Das (Test-)Hauptprogramm lautet nun:

       int main(int argc, char *argv[])
         {
         char Dateiname[MAXNAMELEN+1] = "Textdatei";
         char Suchwort[1000] = "Desoxyribonukleinsäure";
         int Erg;

         Erg = scanfile(Dateiname,Suchwort);
         if (Erg) printf("WORT GEFUNDEN!\n");
         else     printf("NICHTS GEFUNDEN!\n");

         return EXIT_SUCCESS;
         }

3. Verwenden Sie nun das bereits bekannte Programm dir.c, um die Funktion nacheinander `scanfile` auf alle Dateien in einem Verzeichnis anzuwenden.
4. Zuallerletzt wird das Programm wieder Web- oder Browserfähig gemacht. Sie können nun viele Anleihen beim vorhergehenden Programm machen. Wird das Programm`suche.cgi` ohne Parameter aufgerufen, präsentiert es ein einfaches Suchformular. Existiert ein Parameter Namens "Suchwort" wird die neu geschriebene Suchfunktion aktiviert. Sie kön die Funktion `CGIReadInput` komplett übernehen. Es wird lediglich statt "Datei=..." nun "Suchwort=..." genommen. Das Hauptprogramm sieht dann etwa so aus:

          .
          .
          .
       /* #define ... nicht vergessen  */
          .
          .
          .
       #define FILEPATH "/home/wiki/"
          .
          .
       int main(int argc, char *argv[])
         {
         DIR *dir;
         struct dirent *dirzeiger;
         char Dateiname[MAXNAMELEN+1] = "";
         char Dateipfad[MAXNAMELEN+1] = "";
         char Suchwort[1000] = "";
         int Erg;

         printf("Content-Type: text/html\r\n\r\n");

         printf("<HTML>\n<HEAD>\n<TITLE>Lexikon-Suche</TITLE>\n</HEAD>\n<BODY>\n");
         printf("<H1>Lexikon-Suche</H1>\n");

         CGIReadInput(Suchwort);
         if (strcmp(Suchwort,"") == 0)
           {
           printf("<FORM method=POST action=\"http://localhost/~testuser/www/cgi-bin/wiki.cgi\">\n");
           printf("Suchbegriff: <INPUT TYPE=TEXT NAME=\"Suchwort\" SIZE=\"20\">\n");
           printf("<INPUT TYPE=\"SUBMIT\" NAME=\"SUBMIT\" VALUE=\"Suchen\">");
           printf("</FORM>\n</BODY>\n</HTML>");
           }
         else
           {
           dir = opendir(dirname);
           if(dir != NULL)
             {
             printf("<UL>\n");
             while((dirzeiger = readdir(dir)) != NULL)
               {
               strcpy(Dateiname,(*dirzeiger).d_name);
               if (Dateiname[0] != '.')
                 {
                 strcpy(Dateipfad,FILEPATH);
                 strcat(Dateipfad,Dateiname);
                 if(scanfile(Dateipfad,Suchwort))
                   printf("<LI>%s\n",Dateiname);
                 }
               }
             printf("</UL>\n");
             closedir(dir);
             }
           }
         printf("</BODY>\n</HTML>\n");
         return EXIT_SUCCESS;
         }

   In dieser Stufe werden nur die Dateinamen ausgegeben. In einer späteren Erweiterung können dann auch noch die Dateiinhalte ausgegeben werden.

   #### Erweiterungen und Anmerkungen

   In der Vorlesung wurde eine Alternative für die Funktion CGIReadInput besprochen, bei der alle Name-Wert-Paare in einer linearen Liste gespeichert werden und so für den späteren Zugriff zur Verfügung stehen. Diese Variante der Funktion ist wesentlich universeller einsetzbar, da nicht für jeden Anwendungsfall individuelle Anpassungen vorgenommen werden müssen. Diese Variante soll zwar nicht im Praktikum eingesetzt werden, aber der Quellcode steht als Datei liste.c zur Verfügung.

   Im Praktikum und seinem abgeschlossenen System konnten Sicherheitsbetrachtungen zurückgestellt werden, um die Programmierung leichter und einfacher zu gestalten. Bei Verwendung der einzelnen Funktionen in der realen Welt sind aber noch weitere Sicherheitsbetrachtungen anzustellen.

   Zum Inhaltsverzeichnis

   ###### Copyright © FH München, FB 04, Prof. Jürgen Plate

   ###### Letzte Aktualisierung: 03. Jun 2006