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

Czirkos Zoltán · 2021.08.24.

A C nyelv strcpy() függvénye nem biztonságos, a strncpy() pedig nehezen kezelhető. Ez az írás egy olyan biztonságos sztringmásoló függvényt mutat be, amelyet strlcpy() néven több rendszerben is megtalálunk.

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:

  • strcpy(cél, forrás). A strcpy() függvény a cél tömb méretére való tekintet nélkül bemásolja a pointer által mutatótt memóriaterülettől kezdődően a forrás sztringet. Vagyis ha a forrás sztring egy 1000 karakteres szöveget tartalmaz, akkor a függvény 1001 karaktert másol a cél tömbbe, akkor is, ha az csak tíz elemű.
  • strncpy(cél, forrás, maxbájt). Ez kicsit jobbnak tűnik, mint az előző, hiszen meg lehet neki adni, hogy maximum hány bájtot másoljon. Csakhogy van egy kis probléma: a másolandó bájtok számába a strncpy() beleérti a lezáró nullát is, és nem kezeli azt különlegesen. Azaz ha a forrás sztring első maxbájt karaktere között nincsen lezáró nulla, akkor a cél sztring sem lesz nullával lezárva! Ez horror. Így lehetne megbízhatóvá tenni:
    strncpy(cel, forras, cel_tomb_merete);
    cel[cel_tomb_merete-1] = '\0';
    Tudva persze, hogy ha nem fér bele a tömbbe a sztring, akkor le lesz vágva. A másik furcsaság, amit a strncpy() csinál, az az, hogy a tömb fennmaradó részeit (ha a forrás sztring rövidebb, mint a cél tömb) nullákkal tölti ki, ami pedig általában felesleges, és időbe telik.
  • A strcat(cél, forrás) függvénnyel két sztringet fűzhetünk össze, cél+=forrás módon. A cél tömb méretére ez sincs tekintettel. Nem is lehet, hiszen nem kapja paraméterként, nem tudja miből megállapítani – nekünk kell figyelnünk arra, hogy a tömb legalább strlen(cél)+strlen(forrás)+1 bájt méretű legyen.
  • A strncat(cél, forrás, maxbájt) függvény talán egy fokkal jobban használható, mint a 'cpy párja, hiszen ez legalább biztosan lezárja nullával a cél tömböt. A baj csak az, hogy a maxbájt paraméter által megadott méretet a másolt bájtok számára érti, ráadásul a lezáró nulla még efölé jön. Arról nem is beszélve, hogy a maximum másolható bájtok tényleges száma az összefűzés miatt nem csak a forrás sztring méretétől függ, hanem a cél sztring eredeti tartalmától is. Ezt valahogy így lehetne megbízhatóvá tenni:
    strncat(cel, forras, cel_tomb_merete-strlen(forras)-1);

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

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

  • Ne írja túl a cél tömböt.
  • Ha a cél tömbbe nem fér bele az a sztring, amit bele szeretnénk másolni, akkor azt jelezze valahogyan.
  • Könnyű legyen használni.
  • Az utóbbi leginkább azt jelenti, hogy a cél tömb méretét kelljen neki megadni. És pontosan azt, ne −1-et stb., mert azt úgyis előbb-utóbb el fogjuk felejteni.
  • Mindezek vonatkoznak az összefűzést végző függvényre is: ott is a méretet meghatározó paraméter a cél tömb mérete legyen, és ne függjön annak eredeti tartalmától.
  • És végül: garantáltan zárja le a cél tömböt nullával.

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:

enum { MERET = 100 };
char eleresiut[MERET];

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

Ezen már minden látszik, csak az nem, hogy mit csinál. Tele van mindenhol −1-ekkel, amik a lezáró nullák miatt kellenek; néhol pedig figyelembe kell venni azt is, hogy az összefűzésnél a sztring hány karaktert tartalmaz már. 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:

enum { MERET = 100 };
char eleresiut[100];

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

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 ≥ MERET, 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. (A size_t típus egy olyan egész számot jelöl, amely tömb méretét adja meg. Általában egyenértékű a normál egész típussal.)