Sicheres Programmieren mit Perl



Versionskontrolle

Es geht darum, sichere Programme zu schreiben. Das heißt, es sollen möglichst keine Laufzeitfehler auftreten. Das erreicht man u.a. auch durch Versionskontrolle.

Anweisungen im Modul

 package Modulpfad::Modulname;
 use strict;
 use warnings;
 use Modulpfad::Naechstes 17.022_004; # Das Modul benutzt wieder ein anderes
 our $VERSION = 1.005;
 ...
 1;

Anweisungen im Script

 use strict;
 use warnings;
 use Modulpfad::Modulname 1.010;

Ergebnis

Crash zur Compilierungszeit!

Das Script fordert mindestens Version 1.010, 1.005 ist nur installiert. Also entsteht nicht erst dann der Laufzeitfehler, wenn der fehlende neue Code das Script z. B. sterben läßt. Denn das kann eine ganz versteckte Besonderheit im Script sein, muß also nicht sofort auffallen. Wird das durchgängig gemacht, wird beim Scriptaufruf sofort die komplette Modulhierarchie geprüft. Z. B. durch benannte Parameter sind Perl-Subs/-Methoden immer aufwärtskompatibel.

Perl verrechnet sich - nein, nicht wirklich

 our $VERSION = 1.10;

Denn Version 1.2 ist größer als Version 1.10. Wenn man diese Schreibweise benutzt, muß man die Stellenzahl nach dem Dezimalpunkt exakt gleich machen und immer einhalten.

Kann man Versionsnummern weiter untergliedern?

Ja, man hängt einen Unterstrich und den nächsten 3er-Pack Digits an.

 our $VERSION = 5.007_101_022; # Der Unterstrich ist für die Optik,
                               # Perl kann auch damit umgehen.

Warum immer 3 Digits?

Deutsche Zahlen sehen so aus:

 1.234.567,89

amerikanische so:

 1_234_567.89

Außerdem bleibt man so bei der Versionskontrolle kompatibel zu den v-Strings. Diese sind zwar abgekündigt, und ab Perl 5.10 nicht mehr Sprachbestandteil, werden aber mit dem Modul "version" weitergeführt.

 our $VERSION = 5.007_101_022;

ist für die Versionskontrolle das gleiche wie

 our $VERSION = v5.7.101.22;

CPAN ist anders

Für Module, die im CPAN veröffentlich werden, sollte man den Quasistandard benutzen, wobei die Unterversionsnummer immer aus 2 Digits besteht.

CPAN ist eben nicht Perl. :-)

Perl-Versionen erzwingen

 use 5.8.3;

gleichbedeutend mit

 use v5.8.3;

gleichbedeutend mit

 use 5.008_003;

verlangt im Sript mindestens Perl Version 5.8.3, siehe ``perl -v''.


strict und warnings

"use strict;" zwingt zur Deklaration von Variablen (vars) und Subroutinen (subs), läßt symbolische Refenenzierung (refs) nicht zu. "no strict 'refs';" läßt symbolische Referenzierung wieder zu. (Der Geltungsbereich der Aufhebung gegenüber dem globalen "use strict;" am Scriptanfang wird oft durch freistehende Blöcke beschränkt.)

"use warnings;" zwingt den Programmierer dazu, seinen eigenen Code zu verstehen. Es läßt grobe Typkonvertierungsschlampereien nicht zu. Es trägt sehr dazu bei, Fehler schnell zu finden.

Perl ist keine getypte Programmiersprache, wieso nun doch Typen?

Von nicht getypt spricht man nur deshalb, weil Perl nicht die maschinennahen Typen wie Integer, ... verwendet. Perl ist realitätsnäher, denn keiner sagt: "Ich bin Integer 40 Jahre alt." Und "Ich bin Integer -40 Jahre alt." ist Integer-konform, jedoch völlig falsch.

In Perl muß man einerseits zwischen den "Container"-Typen Scalar, Array und Hash und den eigentlichen Datentypen unterscheiden. Letztere sind z.B. Strings, numerische Werte, diverse Referenzen. Dabei stehen sich die ersten beiden sehr nahe. Selbst schon Perl-Scalare ("Container" für 1 Stück Daten und nicht Datentypen) können gleicheitig mehrere gültige Typen beinhalten, siehe Thema Versionsverwaltung. So kann eine Referenz auch gleichzeitig ein Objekt sein, ein String gleichzeitig ein numerischer Wert sein und zudem verseucht (taint).

Warum ist use strict; und use warnings; nicht standardmäßig eingeschaltet?

Perl wird als portable Shellsprache im UNIX, Windows, ... eingesetzt. Schreibt man Einzeiler wie:

 perl -e "$x=$y=1;$z=$x+$y;print qq~$x + $y = $z\n~"

Dann wäre use strict; Horror. Man geht davon aus, daß man bei Einzeilern diese Hilfe des Compilers nicht benötigt. Aber man könnte auch:

 perl -w -Mstrict -e "my $x=my $y=1;my $z=$x+$y;print qq~$x + $y = $z\n~"

schreiben.

In C, Java, ... ist "strict" und "warnings" immer an und läßt sich nicht ausschalten. Warum benutzen manche die beiden Hilfestellungen bei Perl-Projekten nicht? Hat man freie Zeit zur Fehlersuche? In Perl ist Fehlerbehandlung perfektioniert, man es nur nutzen. Die Methoden sind zu vielfältig, um sie hier alle aufzuzählen.

Wie bekommt man auch Warnungen in die Browseranzeige?

 use CGI::Carp 'fatalsToBrowser';
 $SIG{__WARN__} = sub {die 'WARN: '.shift};

Ja gut, ich schalte Warnungen jetzt ein, aber ...

... wie schafft man es, daß beim Einschalten der Warnungen in großen Scripten nicht gleich alles nicht mehr geht?

die weiche Variante:

 { # nur in diesem Scope
   local $SIG{__WARN__} = sub {die 'WARN: '.shift};
 }
 # hier ist alles wieder wie es war

zur Erklärung der äußeren { }:

 { # Das ist ein freistehender Block,
   # der den Geltungsbereich von my-Variablen abgrenzt,
   # genau so aber auch die Ablaufsteuerung mit next, last und redo zuläßt.
   # Ohne redo oder sogar goto läuft er genau einmal ab und dient u.a. auch dazu,
   # unstrukturierte Spaghettiprogramme zu strukturieren.
 }

besser ist aber:

 use warnings;
 $SIG{__WARN__} = sub {die 'WARN: '.shift};
 # fehlerfreier Code
 ...
 { no warnings; # in diesem Scope noch fehlerhaft
   # fehlerhafter Code
   ...
 }
 # hier ist für fehlerfreien Code warnings wieder an
 ...
 no warnings;
 # ab hier wieder fehlerhafter Code möglich
 ...

So grenzt man fremden fehlerhaften Code von eingebundenen Module aus, daß dieser keine Warnungen im eigenen package erzeugen kann:

 { local $SIG{__WARN__} = 'IGNORE';
   $obj->getValue(); # fehlerfafter Code im package
 }

Aufpassen! Scopes, die wie mehrere getrennte aussehen, aber trotzdem nur einer sind!

 if (my $x = shift) {
   ...
 } elsif (my $y = shift) {
   ...
 } else {
   ...
 }
 $x und $y stehen im gemeinsamen Controlblock, der alles umschließt,
 jedoch als Block so nicht sichtbar ist.
 Das heißt, $x ist überall verfügbar, $y ab dem elsif.
 Die 3 Anweisungsblöcke mit Inhalt ... sind dann wieder einzelne
 voneinander getrennte Blöcke.
 for ($x) { # oder while oder until
   ...
 } continue {
   ...
 }
 Das gleiche gilt auch hier.

Typische Operationen gegen Warnungen sind:

 $string ||= 'Defaultwert'; # oft auch '' oder 0
 $never_undef =
   defined $can_be_0_or_string_length_0
   ? $can_be_0_or_string_length_0
   : 'Defaultwert'
 ;
 $boolean = ($var || 0) == 123;
 $boolean = ($var || '') eq 'abc';
 # undef ist keine Zahl und == erzwingt den numerischen Kontext
 if ($var and $var == 5) {
   ...
 }
 unless ($var) { # undef ist kein String und eq erzwingt den String-Kontext
   # kein Code oder $var = 'Defaultwert';
 } elsif ($var eq 'das eigentliche if') {
   ...
 } elsif ($var eq 'eigentlich das 1. elsif') {
   ...
 }
 # damit die Dereferenzierung auch möglich ist
 for (ref $var eq 'HASH' ? %$var : {}) {
   ...
 }


Rückgabecodes prüfen, eval

Prüfe jeden Rückgabewert auf Fehler (mühselig wie in C o.ä.) oder sorge dafür, daß "die" ausgeführt wird und packe den Code in einen eval-Block. Dann reicht die einmalige Prüfung von "$@" für den Block.

Beispiel mit Einzelprüfung

 use strict;
 use warnings;
 use DBI;
 my $statement = 'select ...';
 my $dbh = DBI->connect (
   'prefix:driver:database',
   'user',
   'password',
   { PrintError  => 0,
     RaiseError  => 0,
     AutoCommit  => 1,
   },
 ) or die DBI->errstr;
 my $sth = $dbh->prepare($statement) or die $dbh->errstr;
 $sth->execute or die $sth->errstr;
 my @rows;
 while (local $_ = $sth->fetchrow_hashref) {
   push @rows, {%$_};
 }
 $sth->err and die $sth->errstr;
 $sth->finish or die $dbh->errstr;

Beispiel mit Errorhandler

 use strict;
 use warnings;
 use DBI;
 my $statement = 'select ...';
 my @rows;
 eval {
   my $dbh = DBI->connect (
     'prefix:driver:database',
     'user',
     'password',
     { PrintError  => 0,
       RaiseError  => 1, # <---
       AutoCommit  => 1,
     },
   );
   my $sth = $dbh->prepare($statement);
   $sth->execute;
   while (local $_ = $sth->fetchrow_hashref) {
     push @rows, {%$_};
   }
   $sth->finish;
 }
 $@ and die $@;

Spätestens, wenn der Code komplexer wird, bemerkt man den Vorteil. Der eigentliche Code ist frei von Fehlerbehandlungcode und man kann die Fehlerbehandlung nicht vergessen.

Außerdem kann es bei groben Fehlern sowieso dazu kommen, daß DBI-Methoden nichts anderes übrig bleibt, als "die" aufzurufen. Diese Fehler fängt man dann automatisch mit ab.

Den Inhalt von "$@" bearbeitet man mit einem regulären Ausdruck und extrahiert so wieder die eigentliche Fehlermeldung. Außerdem ist diese Variante dann besser, wenn man im Fehlerfall nicht nur einfach abbrechen will. Man spart solche Konstruktionen wie:

 sub errorhandler {
   ...
 }
 { ...
   $sth = $dbh->prepare($statemnt) or errorhandler(), last;
   $sth->execute or errorhandler(), last;
   ...
 }

Einer Funktion das die aufzwingen

 use strict;
 use warnings;
 use Fatal qw(:void open close);
 eval {
   open FILE, '>', 'myfile';
   print FILE 'Wenn open, print oder close schief geht, endet eval.';
   close FILE;
 };
 $@ and die $@; # alle Fehler hier behandlen


Schreibe gruppierende Klammern, spare bei Funktionsklammern

Klammernhäufungen verkomplizieren den Code, die Verständlichkeit leidet. Fehlen die überflüssigen, können sie sogar Fehler erzeugen.

 # blödes Beispiel, geht zu optimieren, ich weiß
 @array = map(sprintf('%02d', $_), grep($_ and ($_ != 0 or $_ != 9) and $_ < 15,
 sort(values(%{$hashref}))));

Perl-Prgrammierer sind keine Klammernzähler. Einrückungen verdeutlichen Zusammenhänge.

 @array =
   map sprintf('%02d', $_),
   grep $_ and ($_ != 0 or $_ != 9) and $_ < 15,
 sort values %$hashref;

was ist da zu addieren?

 %hash = map +($_, undef), @array;
 %hash = map(($_, undef), @array);

+( ) ist eine nichtFunktionsklammer. Die öffnenden Klammern mutieren. Man kennt so etwas von Klammern in regulären Ausdrücken.


flock - verhindert Datenverlust

 use Fcntl qw(:flock);
 # Fcntl importiert nun zusätzlich folgende Konstanten:
 # LOCK_SH - Shared Lock (Lesezugriff)
 # LOCK_EX - Exclusive Lock (Schreibzugriff)
 # LOCK_NB - Non-Blocking Request (don't stall)
 # LOCK_UN - Lock wieder freigeben
 flock(FH, LOCK_EX); # Exklusiver Lock

Der Lock verfällt, wenn er wieder freigeben oder wenn Sie die Datei mit close (typisch) geschlossen wird. Innerhalb einer kritischen Operation darf die Datei also keinesfalls, auch nur kurzfristig, geschlossen werden.


mehrere Werte in einem Parameter speichern - ohne Leichtsinn

"Das Zeichen kommt in den Daten niemals vor." Diese Aussage stimmt nur solange, bis das Programm komplexer wird. Davon ist auszugehen.

Informiere dich über pack, unpack, use storable und nutze sichere Algorithmen dafür. Es gibt noch weit mehr Methoden zur Datenserialisierung, simples Verketten ist oft untauglich.

 # kein Schlüssel darf "," enthalten, sonst ...
 $alle_keys = join ',', keys %hash;
 # ziemlich kompakt gespeichert,
 # max. Länge der einzelnen keys und Unicode beachten
 $alle_keys = join '', map pack('n/a*', $_), keys %hash;
 # optimal
 use Storable qw(freeze thaw);
 $alle_keys = freeze \keys %hash;


Logging

Es ist sehr einfach, funktionierende Programme zu schreiben.

Es bedarf eines guten Konzeptes, sichere Programme zu schreiben.

Sichere Programme, das heißt auch, daß unkritische Fehler auftreten können. Ggf. hält das Programm auch bei einem kritischen Fehler einfach an. Auch das ist Sicherheit. Sicher heißt auch, schnell wieder produktiv zu sein. Dazu muß man anhand der Fehlermeldung sofort erkennt, wo das Problem liegt und das, ohne den User befragen zu müssen. Oftmals kennt man den nicht einmal.

Logfiles sind ein Mittel dazu.


Erfinde das Rad nicht neu, nutze Programmbibliotheken

Du hast oft nicht die Zeit, ein Thema intensiv zu bearbeiten, weil es dein eigentliches Projekt nur streift. Um trotzdem professionell zu arbeiten, nutze fertige Bibliotheken. Du kannst normalerweise davon ausgehen, daß sich der Autor mit der Gesamtproblematik auseinandergesetzt hat, also Dinge berücksichtigt hat, von denen Du noch nicht einmal geträumt hast.


Code- und Daten-Injektion

Taint

Die Perl-Option -T (-t weiche Variante) verhindert die Injektion unsicherer Daten. Beide Methoden zeigen Fehler bereits im Entwicklungsprozeß an und nicht erst in der Kundenanwendung (die letzte bedingt). Sie bedeuten wiederum, daß man den Programmierer zwingt, seinen eigenen Code zu verstehen.

Bei Windows ist -T in der Shebangzeile nicht mehr möglich. Warum? Perl ist im Gegensatz zu Linux-Systemen schon gestartet. Irgendwo in der Weberserverkonfiguration ist definiert, wie perl aufgerufen wird. Genau an der Stelle wird das -T hinter perl geschrieben. Nur gilt -T dann automatisch für alle Scripte, für die diese Konfiguration gilt.

Überprüfung der Eingaben

Jedes Programm braucht eine Struktur, ein Modell und sei es noch so primitiv. Möglichweise kennt dieses Modell eine Initialisierungs- und Aufräumphase. Auf jeden Fall müssen 4 Struturteile vorhanden sein: Überprüfung der Eingaben, Datenverarbeitung, Datenauslieferung und Logging/Fehlerverarbeitung

Der rein funktional arbeitende Anfänger wird nur den Teil Datenverarbeitung und Datenauslieferung programmieren.

minimale Rechte

root-Rechte scheinen praktisch, sind aber oft völlig ungeeignet.

Denken wir an ein Statistikprogramm. Im allgemeinen wird dieses Select-Statements auf der Datenbank ausführen. Warum braucht es Datenbankschreibrechte? Meistens ist das der Fall, damit wird dieses Programm gefährlich, was nicht sein muß.

minimale Geltungsbereiche von Variablen - Scopes

Programmierer tricksen sich oft selbst aus.

 my $index;
 ...
 for $index (5, 7, 12..17) {
   machwas $index;
 }
 ...
 # ein anderes Problem, wieder mit einem zu verarbeitenden Index
 if ($x == 5) {
  $index = $x;
 }
 ...
 for (1..99) {
   if ($_ == $index) {
     machwasanderesmit $index;
   }
 }

Die Gefahr steckt darin, daß man ggf. nicht merkt, daß $index noch 17 ist, wenn $x == 5 nicht zutrifft.

Besser sind klar definierte Scopes:

 for my $index (5, 7, 12..17) {
   machwas $index;
 }
 ...
 { # ein anderes Problem, wieder mit einem zu verarbeitenden Index
   my $index;
   if ($x == 5) {
     $index = $x;
   }
   ...
   for (1..99) {
     if ($_ == $index) {
       machwasanderesmit $index;
     }
   }
 }

Jedes Scope hat nun seine eigene Variable $index. Nun kommt eine Warnung, weil 1..99 nicht mit undef verglichen werden kann.

open mit 3 Parametern ist immer 1. Wahl

 open FILE, $meine_datei or ...;

ist ein potentiell gefährliche Anweisung.

Deswegen:

 $meine_datei = '>eine_systemdatei';

sicher ist:

 open FILE, '<', $meine_datei or ...;

SQL

Fehlendes Quoting führt möglicherweise zu SQL-Codeinjektion.

 $sth = $dbh->prepare("select abc from def where abc=$code");
 $sth->execute;

besser:

 $code = $dbh->quote($code);
 $sth = $dbh->prepare("select abc from def where abc=$code");
 $sth->execute;

oder auch:

 $sth = $dbh->prepare("select abc from def where abc=?");
 $sth->bind_param(1, $code, SQL_...);
 $sth->execute;

oder auch:

 $sth = $dbh->prepare("select abc from def where abc=?");
 $sth->bind_param(1, 0, SQL_...);
 $sth->execute($code_1);
 $sth->execute($code_2);
 ...
 $sth->execute($code_n);

Regex

 m/^etwas${usereingabe}noch_etwas$/;

besser:

 m/^etwas\Q$usereingabe\Enoch_etwas$/;

Es besteht keine dann keine Chance mehr, Zeichen zu injizieren, die von der Regex bewertet werden.

HTML

 use HTML::Entities;
 ...
 print ..., encode_entities($daten), ...;

User kann somit HTML nicht selbst fortschreiben, weil alle Zeichen so umgewandelt werden, daß sie nicht mehr den HTML-Code entsprechen, z.B. wird aus ">" "&gt;".

Prüfe importierte Parameter, traue niemandem

Parameter, die als %ENV, @ARGV, GET, POST oder COOKIE-Werte in das Programm gekommen sind, sind nicht vertrauenswürdig.

Im schlimmsten Fall werden daraus Dateinamen gebildet und diese zum Schreiben von Dateien benutzt. Benutze bei CGI o.ä. vollständige Pfadangaben und ergänze den linken Teil nicht aus ungeprüften Umgebungsvariablen.

Techniken, die Variablen als GET-Parameter am Ende einer URL durchschleifen oder als HIDDEN-Variablen in Formularen weitergeben (Ping-Pong-Technik), sind daher prinzipbedingt fehlerhaft und können niemals sicher sein.

Bei Parametern, die gegen die Empfehlung per Ping-Pong weitergereicht werden, muß dieser Test jedes Mal (auf jeder Seite) wieder gemacht werden, da ja ein Anwender oder ein böswilliges Programm die Werte inzwischen geändert haben könnte.

Siehe auch Taint.

Eine Webanwendung kontrolliert nur den Bereich bis zur Firewall. Der Browser des Anwenders befindet sich auf der anderen Seite der Firewall und ist daher als Feindesland anzusehen. Daten, die von dort kommen, können unerwartete Werte oder unerwartete Formate haben. Insbesondere kann eine Webanwendung keine Annahmen über bestimmte Eigenschaften des Browsers machen, wie zum Beispiel "JavaScript wird zuverlässig ausgeführt", "Cookies werden akzeptiert und verändern ihre Werte nicht spontan" oder "Referer-Header sind vertrauenswürdig".

Es gelten daher die folgenden Empfehlungen:

Jede Form von Ping-Pong, also Wert-Weitergabe durch GET-Parameter, HIDDEN-Variablen und dergleichen ist zu vermeiden. Auch durch Codierung der Werte ist hier nichts zu erreichen.

Stattdessen sind Sessionvariablen zu verwenden. Nur die Session-ID wird von einer Seite an eine andere Seite weitergereicht.

Keinesfalls darf ein Programm Werte aus einer GET, POST oder COOKIE-Quelle direkt verwenden. Jeder externe Wert ist einer Plausibilitätsprüfung zu unterziehen, bevor er verwendet wird. Validierung von Eingabewerten muß serverseitig geschehen.

JavaScript-Validatoren sind nicht vertrauenswürdig:

Der Browser des Anwenders führt diese Validatoren möglicherweise nicht aus, auch dann, wenn er sich durch die User-Agent-Zeile als JavaScript-fähiger Browser identifiziert.

Ebenso muss angenommen werden, dass die Referer-Header des Browsers möglicherweise gefälscht sind.

Man kann nicht annehmen, dass ein Zugriff tatsächlich von einer bestimmten vorhergehenden Seite hierher vermittelt wurde.

Zusammenfassend:

Traue niemandem. Validiere allen Input oder stirb.

der HTTP-Referer ist unsicher

Der Referer ist eine Variable, in der stehen soll, von welcher Seite der Benutzer kommt, der sich gerade auf der Seite befindet. Hat der User zum Beispiel bei einer Suchmaschine auf einen Link geklickt, so würde der Referer "http://suchmaschine.tld/query?suchwort=german-faq" lauten.

Dies muß allerdings nicht immer so sein:

Die Entwickler des Browsers haben nicht vorgesehen, daß ein Referer-String mitgesendet wird oder der User hat das Mitsenden des Referers im Browser deaktivert.

Der User verwendet einen lokalen Proxyserver (z.B. Webwasher), der die Referer-Information herausfiltert.

Ein Proxyserver bei einem Provider, im Rechenzentrum einer Universität oder in einem Unternehmen ist so konfiguriert, daß er keine Referer-Strings mitsendet.

Neben dem kompletten Entfernen des Referer-Strings aus den Headerdaten kann es auch möglich sein, daß der Referer durch o.g. Quellen modifiziert wird und dadurch unbrauchbar wird.

Diese Punkte sind Argumente dafür, den Referer nicht zu sicherheitsrelevanten Zwecken auf einer Website einzusetzen.

keine Angst vor globalen Variablen

"$_" ist effektiv, weil viele Funktionen "$_" als Defaultvariable benutzen. Jedoch ist das Beschreiben von "$_" mit Vorsicht zu genießen. Nur "for" und "while (<HANDLE>)" rufen "local $_" implizit auf. Anderenfalls muß man selbst dafür sorgen, sein eigenes "$_" zu erzeugen.

Ist es doch einmal passiert, daß der Wert verändert wurde und man einfach nicht weiß, wo das passiert, dann kann die Veränderung mit folgendem Script gefangen werden:

 ...
 package main::Scalar; # das Fang-Modul beginnt
 require Tie::Scalar;
 our @ISA = qw(Tie::Scalar);
 use Carp qw(croak);
 use Data::Dumper;
 sub TIESCALAR {
   my $self = $_[1];
   bless \$self, $_[0];
 }
 sub FETCH {my $self = shift;return $$self} # lesen zulassen
 sub STORE {croak Dumper $_[1]} # Programm abbrechen bei Schreibzugriff,
                                # zeigt den ganzen caller-Stack an
                                # und mit welchem Wert
                                # beschrieben werden sollte
 package main; # zurück zum eigentlichen Programm
 ...
 tie $_, 'main::Scalar', $_; # wir wollen Veränderungen an $_ fangen
 ...

"croak" sorgt dafür, daß der gesamte Aufrufstack bis hin zu der Stelle, wo die Veränderung erfolgt, angezeigt wird.


Perl Objekte

Programme werden sicher, wenn man Daten oder Code über definierte Schnittstellen verwendet. Schnittstellen haben den Vorteil, daß man sie testen kann.

So sieht z.B. ein Testscript für ein Modul aus, Beispiel: "Tie::Sub":

 use 5.6.1;
 use strict;
 use warnings;
 use Test::More tests => 7;
 BEGIN { use_ok('Tie::Sub') }
 { eval {
     tie my %sub, 'Tie::Sub';
     undef = $sub{undef};
   };
   ok
     +($@ || '') =~ /\b\QCall of Config is necessary.\E/,
     'initiating dying, sub is missing'
   ;
 }
 tie my %sub, 'Tie::Sub', sub{$_[0]+1};
 { ok
     $sub{1} == 2,
     'check function'
   ;
 }
 { my $sub = sub {
     my ($p1, $p2) = @_;
     [$p1, $p2];
   };
   tied(%sub)->Config($sub);
   my $cfg = tied(%sub)->Config();
   ok
     $cfg eq $sub,
     'save and get back subroutine, use method Config'
   ;
 }
 { my ($p1, $p2) = @{ $sub{[1, 2]} };
   ok
     $p1 eq '1'
     && $p2 eq '2',
     'check subroutine 2 parmams, 2 returns'
   ;
 }
 { eval { tied(%sub)->Config(undef) };
   my $error1 = $@ || '';
   eval { tied(%sub)->Config([]) };
   my $error2 = $@ || '';
   my $regex = qr/\b\QReference on CODE expects.\E/;
   ok
     $error1 =~ /$regex/
     && $error2 =~ /$regex/,
     'initiating dying by configure wrong reference'
   ;
 }
 { eval { $sub{1} = 2 };
   my $error = $@ || '';
   ok
     $error =~ /\b\Qdoesn't define a STORE method\E\b/,
     'initiating dying by storing into tied hash'
   ;
 }

Das Testscript prüft neben der eigentlichen Funktionalität auch, wie das Modul damit umgeht, wenn es fehlerhaft parametrisiert aufgerufen wird.

Es erzeugt folgende Augabe:

 1..7
 ok 1 - use Tie::Sub;
 ok 2 - initiating dying, sub is missing
 ok 3 - check function
 ok 4 - save and get back subroutine, use method Config
 ok 5 - check subroutine 2 parmams, 2 returns
 ok 6 - initiating dying by configure wrong reference
 ok 7 - initiating dying by storing into tied hash

Objekt-Methoden kennen keine Prototypen

Prototypen machen ein Script nicht sicher. Sie wiederspiegeln nicht den gültigen Wertebereich ab, sondern nur den Typ des "Containers". Das heißt, der gültige Wertebereich und der Wertebereich des Types sind grundverschieden. Ist das nicht so, dann ist das doch eher die Ausnahme.

Beispiel Datum:

 Container-Typ: Scalar
 Daten-Typ: Sting (Wertebereich eines Strings enthält auch 'Hallo')
 tatsächlicher Datentyp: Datum
 Wertebereich: '1.1.0000' bis '31.12.9999',
               aber nur theoretisch, der '32.13.2005',
               wie auch der 29.2.2005 ist ungültig.

Aufgabe dieser Objektmethode ist es, falsche Datumsangaben mit "croak" (use Carp 'croak';) an das aufrufende Script zurückzuweisen.

Der Programmierer der Objektmethode sorgt damit dafür, daß Folgefehler nicht entstehen können. Konsequent angewendet werden Fehler sehr schnell erkannt und nicht erst nach Tagen, Wochen oder Jahren, wenn weiterverarbeitende Programme an den Daten scheitern müssen.

Warum croak und nicht die?

In Subroutinen und erst recht in Modulen macht es wenig Sinn, zu erfahren, daß ein Fehler in einer Subroutine aufgetreten ist. Es fehlt die Information darüber, an welcher Stelle im Anwenderscript der Aufruf diesen Fehler verursacht hat. Manchmal liegen ganze Hierarchien dazwischen. Und genau das macht croak, es meldet wie "die" den Fehler mit der gesamten Aufrufhierarchie.

"CGI::Carp::croak" macht unverständlicherweise das nicht. Man sollte deshalb in der Symboltabelle den Eintrag von confess auf croak kopieren.

 { no strict 'refs';
   *{"CGI::Carp::croak"} = *{"CGI::Carp::confess"};
 }

Damit wird "confess" anstatt "croak" aufgerufen und da klappt es wieder, nur ist die Anzeige dann noch um die Aufrufparameter erweitert, was aber nicht wirklich stört.

Wofür sind Prototypen in Perl dann da?

Wenn man Funktionen schreiben möchte, die sich z.B. wie "push" verhalten sollen, dann geht das ohne Prototypen nicht.

 sub my_push {
   my (@array, @pushdata) = @_; # FALSCH!!!
   ...
 }
 my_push(@wo_es_hineinkommt, 'wo', 'es', 'herkommt');

@pushdata würde nie gefüllt, @array würde alle Daten aufsaugen.

Prototypen würden den Kontext einer Arrayreferenz für das 1. Array erzwingen und den Listenkontext für den Rest.

Ohne Prototypen müßte man my_push folgendermaßen aufrufen.

 my_push(\@wo_es_hinkommt, 'wo', 'es', 'herkommt');

Das ist häßlich.

Und noch einmal zusammengefaßt:

 sub my_push (\@@) {
   my ($array, @pushdata) = @_;
   ...
 }
 my_push(@wo_es_hineinkommt, 'wo', 'es', 'herkommt');