Egy hálózati üzenet bájtsorozat, egy C-sztring viszont olyan adat, amelynek végét egy meghatározott érték jelzi. A kettő összekeverése nem pusztán szövegkezelési hiba: határon túli olvasást, csonkolást vagy protokollértelmezési eltérést okozhat.
A Tömbobjektum, decay és függvényparaméter
rész alapján a függvénynek átadott pointerből nem állapítható meg a
mögöttes tömb mérete. A const pointerek, több szintű
indirection és void* rész pedig megmutatta, hogy a
const char * korlátozza az adott hozzáférésen keresztüli
módosítást, de önmagában nem igazolja sem a tároló élettartamát, sem a
terminátor jelenlétét. Most ezekből építünk explicit
sztringszerződést.
ISO C17: a sztring (string) karakterek
összefüggő sorozata, amelyet az első nullkarakter lezár; a sztring ezt a
lezáró karaktert is magában foglalja. A nullkarakter értéke nulla,
szokásos jelölése '\0'. A NUL terminátor nem azonos a
'0' számjeggyel, és nem azonos a null pointer jelölésére
használt NULL makróval.
Három mennyiséget kell külön követni:
Mivel sizeof(char) == 1, egy char tömb
elemszáma egyben a C bájtokban mért mérete. Ez nem jelenti általánosan
azt, hogy egy C bájt nyolc bit: ezt a CHAR_BIT értéke
határozza meg.
Egy kapacitású tárolóban elhelyezett sztring alapinvariánsa:
$$ 0 \le L < C, \qquad \text{\texttt{data[L]} = \texttt{'\backslash 0'}}. $$
A terminátor előtti elemek egyike sem lehet nulla, különben a sztring korábban véget érne. Az üres sztring hossza nulla, de tárolásához akkor is legalább egy C bájt szükséges.
A karakter szó itt különösen veszélyes. A bájtok száma nem
feltétlenül egyezik meg sem a Unicode-kódpontok, sem a felhasználó által
egyetlen jelnek érzékelt egységek számával. A strlen nem
megjelenített karaktereket számol; ezt a különbséget a Szövegkódolás, locale és UTF-8 C-ben rész bontja
ki.
char tömb és
string literalAz alábbi, függvénytörzsbe illeszthető részlet ISO C17-kód, külön platformfeltétel nélkül:
char name[] = "api";
const char *label = "api";
char field[3] = "api";A három deklaráció eltérő szerződést jelent.
| Deklaráció | Tároló és kezelés | Élettartam | Módosíthatóság és sztringállapot |
|---|---|---|---|
name |
Önálló, automatikus tömb; a blokk kezeli | A blokk végrehajtásának elhagyásáig | Elemei írhatók; kezdetben NUL-lal lezárt sztring |
label |
Nem tulajdonló pointer egy literál statikus tárolójára | A literál a program teljes futása alatt él | A literál módosítása UB |
field |
Önálló, automatikus tömb; a blokk kezeli | A blokk végrehajtásának elhagyásáig | Elemei írhatók, de nincs bennük terminátor |
A name inicializálásakor a tömb megkapja az
'a', 'p', 'i', '\0'
értékeket. A name[0] = 'A' megengedett; a
name[3] = '!' szintén érvényes elemírás, de megszünteti a
sztring lezárását.
ISO C17: a char field[3] = "api";
megengedett inicializálás. Ha nincs hely a terminátornak, az kimarad. A
tömb ezért érvényes adat, de nem sztring.
C-ben a char *p = "api"; deklaráció is megengedett, a
literál módosítása azonban nem definiált viselkedés. A
const char * jobb interfész, mert az ilyen írási
kísérleteket fordításkor észlelhetővé teszi.
A tömbre mutató kölcsönzött pointert a tömb élettartamának vége érvényteleníti. A terminátor felülírása másfajta hiba: a tároló és a pointer még érvényes lehet, miközben a sztringszerződés már nem teljesül.
strlen?A <string.h> fejlécben deklarált
strlen egy érvényes sztring hasznos hosszát adja vissza
size_t típussal. A terminátort nem számolja bele.
A függvény előfeltételezi, hogy a pointertől indulva
elérhető egy olvasható, NUL-lal lezárt sztring. Nem kap kapacitást,
ezért nem alkalmas annak ellenőrzésére, hogy egy tetszőleges buffer
sztring-e. A fenti strlen(field) hívás nem definiált
viselkedéshez vezet: a függvény a tömbön belül nem talál
terminátort.
A strlen(name) kezdetben három, a
sizeof name négy. Ezzel szemben a sizeof label
a pointer mérete, nem a literál hossza vagy kapacitása.
A terminátor megkeresése általánosan idejű bejárás; nem igényel allokációt, és nem módosítja a tárolót. A pontos gépi megvalósítás könyvtár- és targetfüggő. Ha minden hozzáfűzés előtt újra megkeressük az egyre hosszabb sztring végét, sok kis művelet összköltsége négyzetessé válhat. Gyakran ezért érdemes a hasznos hosszt külön állapotként nyilvántartani.
ISO C17: a strcpy(dst, src) átmásolja a
forrássztringet a terminátorral együtt. Legyen a forrás hossza
,
a cél kapacitása
.
A szükséges méretfeltétel:
Emellett a forrásnak érvényes sztringnek, a célnak megfelelően nagy, írható tárolónak kell lennie, és a másolt tartományok nem fedhetik át egymást. A függvény nem kap kapacitást, nem végez szerződés szerinti bounds-ellenőrzést, és nem jelez „kevés hely” hibát.
A strcat(dst, src) a cél meglévő terminátorának helyétől
másolja be a forrást. Ha a cél jelenlegi hossza
,
akkor:
Ezt ellenőrző kódban célszerű kivonással megfogalmazni:
Bizonyításvázlat. A
feltétel mellett a
kivonás nem csordul alul. Ez a cél jelenlegi végétől rendelkezésre álló
hely, amelybe a forrás
bájtjának és egy terminátornak kell beférnie. Így elkerülhető a
D + S + 1 esetleges size_t túlcsordulása.
Ez csak akkor valódi védelem, ha
és
már megbízhatóan ismert. Egy lezáratlan bufferre előbb meghívott
strlen hibáját az utólagos méretellenőrzés nem javítja
meg.
strncpy?A strncpy(dst, src, n) nem egyszerűen „kapacitásbiztos
strcpy”.
n karakter között
található, a függvény nullákkal tölti fel a célt összesen n
elemig.n karaktert másol, és
nem tesz hozzá NUL-t.A célban tehát legalább n írható elem szükséges. A
forrásban vagy elérhető terminátornak, vagy legalább n
olvasható karakternek kell lennie; az átfedés itt sem megengedett.
Ez például nullákkal kitöltött, rögzített szélességű mezők
előállítására használható. Általános sztringmásolásnál viszont könnyen
lezáratlan eredményt ad, és rövid forrás esetén is n
célbájtot ír. A végső NUL kézi beírása lezárhatja az eredményt, de
önmagában nem teszi észlelhetővé az adatvesztést.
snprintf: a
visszatérési érték is része a szerződésnekISO C17: a
snprintf(dst, cap, format, ...) legfeljebb cap
C bájtot ír, a lezáró NUL-t is beleértve. Pozitív kapacitás és
nemnegatív visszatérési érték esetén a tárolt eredmény lezárt.
A visszatérési érték int, és kétféle jelentése van:
Pozitív kapacitásnál tehát:
A negatív értéket a size_t konverzió
előtt kell kezelni. A cap == 0 speciális eset:
nincs írás, és a célpointer lehet null pointer; a hívás méretigényt
számíthat, de nem hoz létre tárolt sztringet.
A korlát kizárólag a célra vonatkozik. A %s
argumentumának továbbra is érvényes sztringnek kell lennie. A forrás és
a cél átfedését sem teszi biztonságossá; a
snprintf(buf, cap, "%s!", buf) hibás megoldás. Külső adatot
pedig %s argumentumként adjunk át, ne
formátumsztringként.
A következő függvény hiba esetén nem ad tovább részleges konfigurációs értéket: pozitív kapacitásnál üres sztringre állítja a célt.
A hívó birtokolja a forrás és a cél tárolóját; a függvény csak a
hívás idejére kölcsönzi őket, pointert nem őriz meg. A name
érvényes sztring, a dst pozitív cap esetén
legalább ennyi írható elemet jelöl, és a két tartomány nem fed át.
Profil: ISO C17, GNU bővítések nélkül. Választott, itt nem futtatással ellenőrzött laborprofil: Linux x86-64 System V, GCC 14.2.0 vagy Clang 19.1.7, glibc 2.40. A kód nem igényel nyolcbites C bájtot.
#include <assert.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
enum text_status {
TEXT_OK,
TEXT_NO_SPACE,
TEXT_ENCODING_ERROR
};
static enum text_status
format_endpoint(char *dst, size_t cap,
const char *name, unsigned port)
{
if (cap == 0) {
return TEXT_NO_SPACE;
}
int result = snprintf(dst, cap, "node=%s;port=%u",
name, port);
if (result < 0) {
dst[0] = '\0';
return TEXT_ENCODING_ERROR;
}
if ((size_t)result >= cap) {
dst[0] = '\0';
return TEXT_NO_SPACE;
}
return TEXT_OK;
}
int main(void)
{
const char name[] = "api";
char output[32];
char small[8];
enum text_status status =
format_endpoint(output, sizeof output, name, 443u);
assert(status == TEXT_OK);
assert(strcmp(output, "node=api;port=443") == 0);
assert(strlen(output) == 17);
status = format_endpoint(small, sizeof small, name, 443u);
assert(status == TEXT_NO_SPACE);
assert(small[0] == '\0');
return 0;
}Fordítás a külön laborban, a megfelelő verziójú eszközökkel:
gcc -std=c17 -Wall -Wextra -Wpedantic -Wconversion \
-O0 -g endpoint.c -o endpoint-gcc
clang -std=c17 -Wall -Wextra -Wpedantic -Wconversion \
-O0 -g endpoint.c -o endpoint-clangA várt teljes eredmény 17 hasznos C bájt, ezért legalább 18 elem kell
hozzá. Nyolcelemű célban a snprintf legfeljebb hét hasznos
bájtot tárol, de továbbra is 17-et ad vissza. A wrapper ezt
csonkolásként kezeli, majd kiüríti a célt. Ezek levezetett elvárások,
nem közölt futtatási eredmények.
A művelet felülírja a cél korábbi tartalmát; annak korábbi hosszadata többé nem használható. A célra mutató pointerek ettől még nem válnak dangling pointerré.
Tekintsük az alábbi ISO C17-adatdeklarációt:
char packet[] = {'O', 'K', '\0', 'X'};A tömb négy C bájtot tartalmaz, de a strlen(packet)
eredménye kettő. A kezdőcímen az "OK" sztring található; az
utána következő 'X' már nem része annak.
Egy protokoll szempontjából azonban mind a négy bájt lehet jelentős. Ha az egyik komponens explicit hosszal, a másik sztringként kezeli ugyanazt az adatot, eltérő üzenetet értelmezhetnek. Bináris adathoz ezért pointer és hossz együtt szükséges; a NUL ott közönséges adatérték lehet.
Hasonlóképpen a snprintf visszatérési értéke sem mindig
egyezik meg a cél strlen-értékével: például a
%c formázás nulla értékkel belső NUL-t írhat. A fenti
endpoint-formátum ilyen karaktert nem állít elő. Bináris másoláshoz a Memóriamásolás, összehasonlítás és byte view rész
ad megfelelő eszközöket.
A char data[] = {'a', '\0', 'b', '\0'}; deklarációnál
add meg a kapacitást, a strlen(data) eredményét és a
kezdőcímen található sztring tárolóigényét. Megőrzi-e a teljes tömb
tartalmát a strcpy?
Írj
bool copy_string(char *dst, size_t cap, const char *src)
függvényt. Kevés hely esetén adjon false értéket, és ne
módosítsa a célt. A forrás érvényes sztring; pozitív kapacitásnál a cél
megfelelően nagy, írható tároló. Átfedés nincs.
Az alábbi ISO C17-részlet nem UB-példa, hanem hibás sikerellenőrzés:
char buf[8];
int n = snprintf(buf, sizeof buf, "%s", "12345678");
if (n <= (int)sizeof buf) {
puts(buf); /* Tévesen teljes eredménynek minősíti. */
}Mi a visszatérési érték és a tárolt sztring? Javítsd az ellenőrzést úgy, hogy a negatív eredményt is kezelje.
A kapacitás négy, a strlen eredménye egy, a kezdőcímen
található sztring tárolóigénye kettő C bájt. A strcpy csak
az 'a', '\0' értékeket másolja át; a teljes
bináris tartalmat nem.
Ellenőrzési feltétel: a tömb méretét, az első sztring hosszát és annak terminátoros méretét külön mennyiségként kell megadni.
ISO C17-függvényrészlet, a szükséges headerekkel; külön tesztprogramba illeszthető:
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
bool copy_string(char *dst, size_t cap, const char *src)
{
if (cap == 0) {
return false;
}
size_t len = strlen(src);
if (len >= cap) {
return false;
}
for (size_t i = 0; i < len; ++i) {
dst[i] = src[i];
}
dst[len] = '\0';
return true;
}A len < cap feltétel biztosítja a terminátor helyét
anélkül, hogy len + 1 összeadást kellene ellenőrizni. A
függvény nem allokál, lineáris költségű, és nem őriz meg kölcsönzött
pointert.
Ellenőrizendő az üres forrás egy- és nullaelemű céllal, a pontosan elegendő kapacitás, valamint az egy bájttal kisebb cél változatlansága.
A visszatérési érték nyolc, a tárolt sztring "1234567".
Az egyenlőség már csonkolást jelent.
Helyes ISO C17-ellenőrzési részlet:
if (n < 0) {
/* Kódolási hiba. */
} else if ((size_t)n >= sizeof buf) {
/* Csonkolás. */
} else {
puts(buf);
}A teljes eredmény ágába csak nemnegatív, a kapacitásnál szigorúan kisebb érték kerülhet.
A C-sztring szerződés: élő, olvasható tárolóban elérhető NUL
terminátort feltételez. A kapacitás, a hasznos bájthossz és a szöveges
karakterek száma külön fogalom. A strlen nem validátor, a
strcpy és strcat nem kapacitásellenőrző, a
strncpy pedig nem garantál általánosan lezárt eredményt. A
snprintf korlátozza az írást, de sikerét és csonkolását
külön ellenőrizni kell. Bináris adatnál mindig explicit hosszra építs,
ne terminátorra.