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.
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)
. Astrcpy()
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 astrncpy()
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:
Tudva persze, hogy ha nem fér bele a tömbbe a sztring, akkor le lesz vágva. A másik furcsaság, amit astrncpy(cel, forras, cel_tomb_merete); cel[cel_tomb_merete-1] = '\0';
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);
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.
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.
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.)