Kulturált sztringmásoló függvény

A C nyelvnek egyszerre előnye és hátránya is az, hogy a tömbök használatakor nem ellenőrzi futási időben a túlindexelést. Előnye azért, mert gyors: ha tudjuk, hogy a program helyes, akkor felesleges futási időben minden indexeléskor ellenőrizni a határokat. (Nehéz is lenne, tekintve a nyelv tömbmodelljét.) Hátránya pedig azért, mert ha hibázunk, az nem mindig derül ki egyértelműen, determinisztikusan. Olyankor a legváltozatosabb hibajelenségeket tapasztalhatjuk: memóriaelérési hibák, helytelen eredmények, program lefagyások, a rendszer biztonságának sérülése. Vagy egyszerűen az is előfordulhat, hogy semmi hatását nem észleljük a hibának.

Bár a gondolat csábító, hogy a program írásakor mindig biztosítani lehessen valahogy a helyes tömbméret megválasztását, ez sokszor nem egyszerű. A probléma talán a sztringeknél érzékelhető a legjobban: bármekkorára is választjuk a karaktertömb méretét, mindig előfordulhat, hogy a felhasználó hosszabb sort vagy szót ad meg, mint amire gondoltunk. Emiatt a programokban a tömbök túlindexelése egyébként gyakoribb szokott lenni, mint az alulindexelés. A két leghírhedtebb, Interneten terjedő féregprogram, a Code Red és a Slammer is éppen ilyen túlindexelési problémát – tulajdonképpen figyelmetlenül megírt programot – használt ki.

Nincs persze szó arról, hogy elvi akadály miatt megoldhatatlan probléma lenne ez. A félév második felében szereplő dinamikus memóriakezelés és dinamikus adatszerkezetek témakör éppen azt mutatja be, hogyan léphetünk túl a fix méretű tömbökön. Ez is egy megoldás lehet, csak éppen ezt sokszor egyszerűen a lustaság akadályozza meg. Ha maradunk a rögzített méretű tömböknél, akkor is el tudjuk kerülni az ilyen jellegű hibákat. Azonban sajnos a szabványos C függvénykönyvtár sztringkezelő függvényei sem túl szerencsésen lettek megtervezve. Nézzük meg, miért; utána pedig azt, hogyan lehetne javítani a helyzeten.

1 A C könyvtár sztringkezelő függvényei

Idézzük csak fel a C sztringek lelkivilágát! A C sztringek nullával lezárt karaktertömbök. Adott a következő, száz elemű tömb:

char sztring[100];

Ebben a tömbben egy legfeljebb 99 karakterből álló szöveget tárolhatunk, hiszen legkésőbb a tömb utolsó elemének, sztring[99]-nek a lezáró nullát kell tartalmaznia. Ezt az elvárást a C sztringkezelő függvényei néha nem teljesítik, máskor pedig nagyon körülményesen kell felparaméterezni őket:

2 Sztringmásoló függvény – elvárások

Mit várnánk egy sztringet másoló függvénytől?

Ilyen tulajdonságokkal rendelkezik a könyvtári snprintf() függvény. Ennek első paramétere a cél tömb, a második paramétere pedig a cél tömb mérete kell legyen. (A többi paramétere a printf()-éhez hasonló.) Figyelembe veszi az írás közben azt, hogy csak méret-1 karaktert írhat, és akármi is történik, lezárja nullával a tömböt. A visszatérési értéke pedig annak a sztringnek a hossza, aminek az előállítására kértük – még akkor is, ha az a cél tömbbe nem fért bele. Ehhez hasonlóan viselkedő 'cpy és 'cat függvényeket több operációs rendszer C könyvtára is tartalmaz, általában strlcpy() és strlcat() néven.

3 Miért kell az ilyen paraméterezés?

Tegyük fel a fent linkelt cikk nyomán, hogy egy fájl elérési útját szeretnénk összebarkácsolni egy sztringben, saját_mappa + / + fájl.txt módon:

char eleresiut[100];

strcpy(eleresiut, sajat_mappa);
strcat(eleresiut, "/");
strcat(eleresiut, "fajl.txt");

Ez potenciálisan több túlindexelést is tartalmaz. A saját mappa neve különböző lehet, hiszen a felhasználó határozza meg. A könyvtári strncpy() és strncat() függvényekkel a következőképpen lehetne biztonságossá tenni a kódrészletet:

char eleresiut[100];

strncpy(eleresiut, sajat_mappa, sizeof(eleresiut) - 1);
eleresiut[sizeof(eleresiut) - 1] = '\0';
strncat(eleresiut, "/", sizeof(eleresiut) - strlen(eleresiut) - 1);
strncat(eleresiut, "fajl.txt", sizeof(eleresiut) - strlen(eleresiut) - 1);

Ezen már minden látszik, csak az nem, hogy mit csinál. Ha a sztringmásoló és -összefűző függvényünk az előbb említett tulajdonságokkal rendelkezik, akkor a biztonságossá tétel sokkal egyszerűbb, sőt triviális:

char eleresiut[100];

strlcpy(eleresiut, sajat_mappa, sizeof(eleresiut));
strlcat(eleresiut, "/", sizeof(eleresiut));
strlcat(eleresiut, "fajl.txt", sizeof(eleresiut));

Ezek a függvények egyébként a snprintf()-hez hasonlóan azt a sztringhosszt szokták visszaadni, amekkora a másolt/keletkező sztring lett volna. Így bármelyik pillanatban, ha azt látjuk, hogy a visszatérési értékük ≥ sizeof(elérésiút), akkor tudjuk, hogy le kellett vágni a sztringet, mert nem fért bele a cél tömbbe.

4 Sztringmásoló függvény – egy konkrét implementáció

Az OpenBSD strlcpy() függvénye így néz ki, magyarra fordított megjegyzésekkel:

/*
 * src-t dst sztringbe másolja, ahol az utóbbi tömb mérete siz.
 * Legfeljebb siz-1 karakter kerül másolásra.
 * Mindig lezárja 0-val a dst tömböt (kivétel ha siz == 0).
 * strlen(src)-vel tér vissza; ha ez >= siz, akkor a cél sztring
 * le lett vágva.
 */
size_t
strlcpy(char *dst, const char *src, size_t siz)
{
    char *d = dst;
    const char *s = src;
    size_t n = siz;

    /* Annyi bájtot másol, amennyi belefér */
    if (n != 0) {
        while (--n != 0) {
            if ((*d++ = *s++) == '\0')
                break;
        }
    }

    /* Nincs elég hely dst-ben: nullával lezárás és src végének megkeresése */
    if (n == 0) {
        if (siz != 0)
            *d = '\0';      /* dst 0-val lezárása */
        while (*s++)
            ;
    }

    return s - src - 1;    /* a méretbe nem számít bele a lezáró 0 */
}

Így néz ki az a függvény, amely figyel a sztringek játékszabályaira, és egyúttal könnyű is használni.