Dump-sicheres Parsen



  • anti-freak schrieb:

    Absichtlich crashen lassen Ist nie eine gute Idee. Wirk unfertig. Dann lieber ne fette auffällige Fehlermeldung. Optimalerweise mit Fehler Korrektur Funktion.

    Fehler machen und absichtlich Formatfehler einbauen sind zwei verschiedene Dinge. Mal eine andere Frage. Musst/Willst du Streams benutzen?


  • Mod

    Eisflamme schrieb:

    Oder lasst ihr das Programm auch einfach Mal crashen, wenn der einzige Inputfehler sein könnte, dass der Benutzer absichtlich manipuliert hat?

    Kommt auf das Format an :p . Wie willst du eine allgemeine Antwort auf diese Frage haben? Manchmal macht es keinen Sinn, das Programm fortzusetzen, weil wichtige Daten fehlen. Dann muss man eben abbrechen. Manchmal hat man einfach einen Haufen Daten und ein Satz daraus ist falsch, dann kann man je nach Datenformat den Fehler ignorieren und dahinter weiter lesen. Oder man kann zumindest versuchen zu retten, was zu retten ist und mit den bisher gelesenen Daten weiter arbeiten.

    In den meisten Fällen benutze ich übrigens die dritte Variante, außer die Logik verlangt ausdrücklich nach der ersten. Die zweite Variante ist mir meistens zu umständlich, außer ich habe sowieso schon ein dafür geeignetes Format vorgegeben.

    Okay, aber prüft ihr bei jedem get, bei jedem getline und bei jedem Stringparsen, ob auch an dieser Stelle das Format korrekt ist?

    Bei dir klingt das so, als ob das ein großer Aufwand wäre. In der Regel hat man doch die komplette Leseaktion irgendwo weggekapselt und macht dann nur noch while(Leseaktion) verarbeiten; oder if(!Leseaktion) abbrechen; . Was genau innerhalb der Leseaktion geschieht ist egal, wenn diese einen Fehler findet, dann bricht sie eben ab (im einfachsten Fall, weil einfach alle weiteren Aktionen auf dem Stream automatisch fehlschlagen und daher nichts mehr gelesen wird), die Prüfung darauf findet aber nur einmal nach Abschluss der Aktion auf der übergeordneten Programmlogikebene statt.



  • anti-freak schrieb:

    Absichtlich crashen lassen Ist nie eine gute Idee.

    Nicht? Lieber alles so lange mit Errorhandlern vollstopfen, bis der eigentliche Code unauffindbar ist und 90% der Performance für irrsinnige was-wäre-wenn-Abfragen drauf geht?

    Wenn das, was geparst werden soll, generiert wird, dann bedeutet das, wie du schon sagst, dass ein halbwegs zurechnungsfähiger Nutzer nicht willkürlich am lebenden System rumpfuscht. Wenn also jemand tatsächlich an den Inputdaten rumspielt, dann macht er das testweise, und dann reicht es völlig aus, einen allgemeinen Parse-Fehler anzugeben. Sprich: Aus welcher Ecke des Parsers eine Exception fliegt ist wurscht. Höchstens gegen UB solltest du dich vielleicht schützen (Segfaults etc.), allerdings ohne große Suche - eher á la if (ptr == nullptr) throw runtime_error("nullptr");
    Die einzige Stelle, wo du wirklich Fehler abfängst ist dann beim Aufruf des Parsers - wo genau der Parser krepiert sollte völlig irrelevant sein, der User braucht die Information nicht. Du selbst als Entwickler hast mit dem Debugger genügend tools, um die Herkunft solcher Exceptions zu finden - falls tatsächlich ein bug im Parser oder im generator vorliegen sollte, den du durch deine Tests noch nicht gefunden hast.

    Let It Crash!



  • pumuckl schrieb:

    Nicht? Lieber alles so lange mit Errorhandlern vollstopfen, bis der eigentliche Code unauffindbar ist und 90% der Performance für irrsinnige was-wäre-wenn-Abfragen drauf geht?

    Es gibt auch eine Alternative: dont write sucky software.

    Stell dir mal vor jede Anwendung würde crashen wenn etwas unerwartetes passiert. Du schreibst gerade ein Dokument, jetzt willst du etwas aus einem anderen Dokument referenzieren, klickst aus versehen die falsche Datei an -> bäm, dein Texteditor crasht.

    Schlecht.

    Eine Anwendung sollte nie Aufgrund externen Faktoren crashen. Never EVER!
    Crashen nur wenn invarianten der Anwendung verletzt werden - weil dann ein fortsetzen nicht mehr sicher ist.

    Aber crash weil zB die Netzwerkverbindung abgebrochen ist, oder der User die falsche Datei angeklickt hat, ist keine gute Idee.



  • Ich glaube ihr verwechselt hier Benutzerfehler die passieren können und abgefangen werden sollten und Fehlern, die eigentlich nicht passieren sollten. Wenn ich eine TextBox habe, welche nur Buchstaben entgegen nimmt, wird der normale Benutzer auch nur Buchstaben eingeben können. Wenn jemand aber absichtlich Zahlen schreiben will (OP schrieb, man könnte Zugriff auf die Formatbehandlung der zweiten Software haben, sollte aber die Finger weglassen), macht das eigentlich keinen Sinn, der "Hacker" möchte es aber. Soll ich jetzt bei der Eingabe in die TextBox auf Zahlen prüfen und dann nochmal, wenn ich mit dem String der TextBox arbeiten will, obwohl das eigentlich sowieso nicht möglich sein sollte?



  • D.h. ihr beendet eure Programme, nur weil in einer Datei, die eingelesen wird, etwas nicht stimmt? O.o
    Und der User bekommt nichtmal ne Meldung, Na herzlichen dank... Ich finde das echt nicht schön. Fehlerbehebung muss ja nicht unbedingt sein, aber ne fehlermeldung is imo das mindeste.



  • Man kann ein Programm auch mit Fehlermeldung crashen lassen. 😉



  • anti-freak schrieb:

    D.h. ihr beendet eure Programme, nur weil in einer Datei, die eingelesen wird, etwas nicht stimmt?

    Wer hat das geschrieben, und wo? Es hat denke ich auch keiner geschrieben, dass bei einem Parsefehler das komplette Programm abrauchen soll.

    Wie FreakY<3Cpp schon sagt muss man zwischen Nutzerfehlern und allgemeinen Systemfehlern unterscheiden. Bei Nutzereingaben muss ich dem user richtig auf die Finger gucken, dass er auch ja eine Zahl eintippt, wenn ich eine Zahl haben möchte usw. Dann muss ich dem Nutzer auch explizit sagen, was er falsch gemacht hat "Deine Schuhgröße muss eine Zahl sein!" Bei Systemfehlern ist das was ganz anderes. Dem Nutzer ist in keinster Weise geholfen, wenn ich an jeder Ecke Handler einbaue und ihm dann genau sagen kann, dass ich übers Netzwerk nur 22 byte bekommen habe, obwohl ich eigentlich 28 brauche. Da reicht es, wenn es einen allgemeinen "Netzwerk-Übertragungsfehler" gibt. Bei dem hier gegebenen Problem ist es so, dass der Input im Normalfall generiert und damit keine Nutzereingabe ist. Ein Fehler beim Parsen ist deshalb ein Systemfehler, kein Fehler in der Nutzereingabe. Ich brauch deshalb nicht bei jedem get, getline oder was auch immer eine Fehlerabfrage einbauen und dann am Ende ausspucken "Fehler beim Parsen in Zeile 21, Spalte 4, Zahl erwartet, Buchstabe gefunden" - es reicht ein allgemeiner "Fehler beim Parsen der Inputdatei". Ich muss nicht wissen, wann das failbit beim ifstream gesetzt worden ist, es reicht, wenn ich irgendwann merke, dass etwas schiefgelaufen ist, und dafür muss ich nicht den ganzen Parsercode mit Abfragen vollmüllen.
    Ja, der User kann am Input rumeditieren. Aber trotzdem ist das nicht der Zweck des Parsers, manuell erstellten Input sicher zu analysieren und zu parsen. Der User kann aber auch mitten während der Übertragung den Netzwerkstecker ziehen. Würdet ihr deshalb sowas schreiben?

    while (needMoreData())
    {
      if (!isStillConnectedToNetwork())
         throw DAUException("Plug that network cable back in, moron!");
      recv(sock, buf, len, flags);
      //...
    }
    


  • *hier stand nonsense*



  • also man kann aus einer fremdsoftware über zwischenablage eben eine datei im textformat exportieren. Die ist dafür gedacht ohne änderungen in meine software importiert zu werden. Wenn jemand etwas vollkommen anderes einfügt, wird das auch verstanden und ein fehler ausgegeben.
    Blöd ist nur, wenn mein programm eine bestimmte zeile in der mitte parst, in der normalerweise immer ein doppelpunkt vorkommt. Wenn nicht, gibt das jetzt probleme beim find und substr. Soll ich das prüfen?



  • Oder anders gefragt: Sollte man an jeder Stelle beim Parsen, bei jedem Zeichen und bei jeder Zeile damit rechnen, dass jeder noch so willkürliche Formatfehler auftritt, auch wenn der nur durch böswilligste Manipulation eintreffen kann?

    Einen Fehler auszugeben ist okay. Wenn aber find string::npos ergibt und ich das für substr nutze, ist das Verhalten vermutlich undefiniert. Das ist nicht Mal ein geordneter Crash. Andererseits hat der Benutzer dann wohl wirklich absichtlich versucht das Programm zu zerstören, das wäre also fast "gerecht".

    Aufwand ist es schon ein wenig. Ich muss halt bei jedem get, getline, find, substr prüfen, ob das Resultat auch einem korrekten Format entspricht. Und bei operator>> natürlich auch und das sind halt so meine Mittel.

    Trotzdem bereinige ich Derartiges wohl komplett, erscheint besser. 🙂



  • anti-freak schrieb:

    D.h. ihr beendet eure Programme, nur weil in einer Datei, die eingelesen wird, etwas nicht stimmt? O.o

    Exakt. Wenn es die Konfigurationsdatei ist, die die exakten Parameter enthält, die ich für eine Experimentalreihe ist, dann möchte ich unter überhaupt gar keinen Umständen, dass das Programm einfach mal macht, wenn fehler in der Datei sind. Es soll härtestmöglich crashen und weder kostbare Rechenzeit noch meine Zeit verschwenden.

    Und der User bekommt nichtmal ne Meldung, Na herzlichen dank... Ich finde das echt nicht schön. Fehlerbehebung muss ja nicht unbedingt sein, aber ne fehlermeldung is imo das mindeste.

    Wer sagt, dass wir das nicht haben? An irgendeiner Stelle kommt halt ein throw() das nirgenwo gefangen wird. Das Programm crasht und der nutzer erhält auf der Konsole noch den Inhalt von what(). Hier hat niemand von terminate gesprochen, nur davon, dass man nicht versuchen sollte, jeden möglichen Fehler zu korrigieren.



  • Eisflamme schrieb:

    Hoffe, jemand erkennt mein Problem

    Hallo Eisflamme,

    Dein Problem hast Du schon selber erkannt - nämlich:

    Eisflamme schrieb:

    ich habe jetzt einen gut funktionierenden Parser gebastelt. Ich bin jedoch mit der Sicherheit von Streams nicht sonderlich vertraut.

    Wenn Du konsequent Streams benutzt, so gibt es nur zwei Zustände, die hier eine Bedeutung haben. Der Stream ist im Fehlerzustand oder eben nicht. Im ersten Fall kannst Du anschließend beliebig viele Streamoperationen aufrufen - sie machen dann alle dasselbe - nämlich gar nichts.
    Damit dies funktioniert steht am Anfang jeder Streamoperation ein Objekt der Klasse std:istream::sentry, ist dies nicht ok, so wird jede Funktion sofort wieder verlassen. Und da crasht auch nichts - und ganz am Schluss fragt man dann den Stream, ob der Fehlerzustand gesetzt ist oder eben nicht. Im zweiten Fall ist alles gut.

    Eisflamme schrieb:

    Einen Fehler auszugeben ist okay. Wenn aber find string::npos ergibt und ich das für substr nutze, ist das Verhalten vermutlich undefiniert. Das ist nicht Mal ein geordneter Crash.

    Und wieder ein Grund, warum das Pärchen getline und Stringfrickelei ein Graus ist - lass es einfach sein. Mache Dich mit den Fähigkeiten von std::istream und Co vertraut. Die sind dafür gemacht - std::string ist es nicht.

    Eisflamme schrieb:

    Ich muss halt bei jedem get, getline, find, substr prüfen, ob das Resultat auch einem korrekten Format entspricht. Und bei operator>> natürlich auch ..

    Nein bei operator>> muss man eben nicht (s.o.) - man kann fröhlich weitermachen/-Lesen/-Parsen. Der Stream bleibt quasi einfach 'stehen', sobald ein Fehler auftritt.
    Und wenn Du einen geeigneten Streambuf 'einschiebst', der Zeichen und Zeilenumbrüche mit zählt, so kannst Du am Ende sogar genau sagen, in welcher Zeile und Spalte der Fehler aufgetreten ist.

    Und wenn ein Fehler auftritt sollte man alle Infos darüber an den werten User weitergeben und ihm die Entscheidung überlassen, was jetzt zu tun ist. Vielleicht hat er bloß die falsche Konfig-Datei ausgewählt - oder der Memory-Stick war nicht eingesteckt - oder das Laufwerk nicht gemountet - oder ... .

    Gruß
    Werner



  • Hallo,

    oh, okay, die Info, dass ein Stream nach Misslingen weiter benutzbar ist, ist mir neu und äußert wertvoll! Danke schon Mal dafür!

    Sagen wir, ich bin jetzt davon überzeugt, dass Streams toll sind (bin ich eh, aber ich kann damit noch nicht alles so lösen, wie ich will).

    Folgende zwei Probleme stellten sich mir. Wenn mir da jemand helfen könnte, wäre ich sehr froh und bräuchte find/substr wohl nicht mehr. 🙂

    Zu einem bestimmten Zeitpunkt in meinem Parsevorgang suche ich nur nach Zeilen, die mit "Beat" anfangen. Kommt keines vor, würde ich gerne den Rest der Zeile ignorieren (da hätte ich jetzt so was wie stream >> firstWord; if(firstWord != Beat) getline(stream, &dummy); gemacht)

    Dann möchte ich gerne in der aktuellen Zeile so viele Zeichen überspringen, bis das Zeichen ':' kommt. Der zu überspringende Teil soll auch Leerzeichen enthalten. Kommt jedoch kein ':' in der aktuellen Zeile vor, möchte ich das auch gerne irgendwie erfahren.

    Vermutlich braucht es da nur irgendein Schlüsselelement. Ich könnte auch eine Funktion schreiben, die einfach solange get aufruft, bis ein ':' kommt und eben throwt (o.ä. Fehlerbehandlung), wenn vorher ein '\n' gefunden wurde. Ist das die Lösung?

    Herzlichen Dank!



  • hola

    // von Werner geklaut und umgeschrieben
    template< char C >
    bool skip_to_char( std::istream& in )
    {
        char c;
        while( in >> c && c != C && c != '\n' && !in.eof());
        return c == C;
    }
    

    ungetestet, aber ungefaehr so koennte es aussehen

    Meep Meep



  • Ah, sehr gut sieht das aus. Das, was ich eben Schlüsselelement nannte, ist also im Endeffekt, dass man immer Zeichen pro Zeichen einliest. Das ist zwar kein get aber "stream >> c" ist ja so ähnlich (ist das nur syntaktisch ähnlich oder tatsächlich dasselbe?)

    Jedenfalls sollte ich damit alles lösen können, danke. 🙂 Falls noch jemand einen Nachtrag hat, gerne.



  • Meep Meep schrieb:

    // von Werner geklaut und umgeschrieben
    template< char C >
    bool skip_to_char( std::istream& in )
    {
        char c;
        while( in >> c && c != C && c != '\n' && !in.eof());
        return c == C;
    }
    

    ungetestet

    Hallo Meep Meep,

    ich bemühe mich immer, jeden Code Schnipsel, den ich hier rein stelle, auch zu testen. Weil - ich mache auch Fehler 😉 . Vielleicht hat Eisflamme inzwischen gemerkt, dass es nicht funktioniert, gerührt hat er sich ja noch nicht.

    Eisflamme schrieb:

    Das ist zwar kein get aber "stream >> c" ist ja so ähnlich (ist das nur syntaktisch ähnlich oder tatsächlich dasselbe?)

    'stream >> c' liest - so nichts anderes eingestellt ist - immer das nächste druckbare Zeichen. Oder anders ausgedrückt: alle White Space Character werden vorher überlesen. Der Zeilenvorschub '\n' gehört auch zu den White Space Characters und wird somit auch überlesen. Folglich ist die Bedienung "c != '\n'" immer erfüllt.

    Im Gegensatz dazu liefert isteam::get() jedes Zeichen, aber eben auch EOF. Deshalb ist der Return-Typ auch int und nicht char. Streng genommen müsste man das jedes mal prüfen bevor man es zum char konvertiert und vergleicht. Also etwa so:

    #include <iostream>
    #include <limits>
    
    template< char C >
    bool skip_to_char_in_line( std::istream& in )
    {
        typedef std::istream::traits_type Traits;
        for( std::istream::int_type m; (m = in.get()) != Traits::eof(); )
        {
            const char c = Traits::to_char_type( m );
            if( c == C )
                return true;
            if( c == '\n' )
                break;
        }
        return false;
    } 
    std::istream& skipline( std::istream& in )
    {
        return in.ignore( std::numeric_limits< std::streamsize >::max(), '\n' );
    }
    
    int main()
    {
        using namespace std;
        while( cin )
        {
            cout << "--> ";
            if( skip_to_char_in_line<':'>( cin ) )
            {
                int value;
                if( cin >> value )
                {
                    cin >> skipline; // Wichtig! Rest der Zeile überlesen
                    cout << "Gelesen: " << value << endl;
                }
                else
                    cerr << "Lesefehler" << endl;
            }
            else
                cout << "diese Zeile enthaelt kein ':'" << endl;
        }
        return 0;
    }
    

    ermöglicht diesen Dialog auf der Konsole:

    --> egal was : 42
    Gelesen: 42
    --> ohne Doppelpunkt
    diese Zeile enthaelt kein ':'
    --> mal sehn:123  rest der Zeile
    Gelesen: 123
    --> sdjksk sds
    diese Zeile enthaelt kein ':'
    --> :keine Zahl
    Lesefehler
    

    Gruß
    Werner



  • Eisflamme schrieb:

    Zu einem bestimmten Zeitpunkt in meinem Parsevorgang suche ich nur nach Zeilen, die mit "Beat" anfangen. Kommt keines vor, würde ich gerne den Rest der Zeile ignorieren (da hätte ich jetzt so was wie stream >> firstWord; if(firstWord != Beat) getline(stream, &dummy); gemacht)

    Könnte damit gehen:

    std::istream& search_beat( std::istream& in )
    {
        for( std::string token; in >> token && token != "Beat"; )
            in.ignore( std::numeric_limits< std::streamsize >::max(), '\n' );
        return in;
    }
    

    setzt aber voraus, dass sich hinter "Beat" garantiert ein Leerzeichen befindet. Falls nicht, macht man sich std::setw zu nutze.

    #include <istream>
    #include <limits>
    #include <string>
    #include <iomanip> // std::setw
    
    std::istream& search_beat( std::istream& in )
    {
        for( std::string token; in >> std::setw(4) >> token && token != "Beat"; )
            in.ignore( std::numeric_limits< std::streamsize >::max(), '\n' );
        return in;
    }
    

    Und falls es nicht immer "Beat" ist, kann man das Suchwort auch mitgeben.

    // includes wie oben
    struct search_text_at_beginline
    {
        explicit search_text_at_beginline( const std::string& key )
            : key_( key )
        {}
        friend std::istream& operator>>( std::istream& in, search_text_at_beginline st )
        {
            for( std::string token; in >> std::setw(st.key_.length()) >> token && token != st.key_; )
                in.ignore( std::numeric_limits< std::streamsize >::max(), '\n' );
            return in;
        }
    private:
        std::string key_;
    };
    

    Anwendung

    cin >> search_text_at_beginline("Beat")
    

    Eisflamme schrieb:

    Dann möchte ich gerne in der aktuellen Zeile so viele Zeichen überspringen, bis das Zeichen ':' kommt. Der zu überspringende Teil soll auch Leerzeichen enthalten. Kommt jedoch kein ':' in der aktuellen Zeile vor, möchte ich das auch gerne irgendwie erfahren.

    siehe Posting oben mit skip_to_char_in_line.

    Gruß
    Werner



  • Okay, das sieht super aus, herzlichen Dank. 🙂



  • Kann mir jemand erklären, warum Meep Meep skip_char als Template-Funktion deklariert hat? Was spricht dagegen den Buchstaben 'C' nicht einfach als Argument zu übergeben?


Anmelden zum Antworten