A konfigurációs érték feldolgozása nem attól sikeres, hogy a bemenet
elejéből előállt egy szám. A "443xyz" szövegnek van számmá
alakítható kezdete, mégsem érvényes portszám. Megbízható feldolgozáskor
külön vizsgáljuk a bemenet nyelvtanát, a konverzió sikerét, a
reprezentálhatóságot, az alkalmazási tartományt és a teljes bemenet
elfogyasztását.
A variadikus függvényeknél megismert típusszerződés itt közvetlen biztonsági követelmény. A stdio és pufferelés adja az I/O hátterét, a locale és szövegkódolás pedig megmagyarázza, miért nem feltétlenül környezetfüggetlen egy szöveges konverzió. Az alábbi nyelvi és könyvtári szabályok ISO C17-re vonatkoznak; a POSIX-kiegészítéseket külön jelöljük.
A printf formátumsztringje meghatározza, milyen típusú
argumentumot olvas ki a függvény. A variadikus argumentumokon
végrehajtott alapértelmezett promóciókat figyelembe kell venni: például
a float argumentum double lesz.
| Kiírandó típus | printf-formátum |
|---|---|
int |
%d |
unsigned int |
%u |
long |
%ld |
unsigned long long |
%llu |
size_t |
%zu |
double, illetve argumentumként átadott
float |
%f, %e vagy %g |
long double |
%Lf, %Le vagy %Lg |
int32_t, ha rendelkezésre áll |
"%" PRId32 |
Az utolsó sorhoz a <stdint.h> és
<inttypes.h> fejlécek tartoznak. Az exact-width
típusok opcionálisak: például az int32_t elérhetőségét az
INT32_MAX makró meglétével ellenőrizhetjük. Ha
count ilyen típusú, a helyes alak:
/* ISO C17-részlet; <stdint.h>, <inttypes.h>, <stdio.h> mellett.
Előfeltétel: int32_t elérhető, count inicializált int32_t. */
printf("count=%" PRId32 "\n", count);A típusnévben szereplő szélesség nem határozza meg, hogy a típus
int, long vagy más megfelelő egész típus
aliasa. A hibás formátum–argumentum kapcsolat nem hordozható konverzió,
hanem nem definiált viselkedést okozhat.
Külső szöveg soha ne legyen közvetlenül formátumsztring:
printf("%s", text) használható, printf(text)
nem megfelelő. Ettől még a sortörésekkel történő loginjektálás ellen
külön védekezés kell.
snprintf
eredményének értelmezéseA snprintf(dst, capacity, ...) legfeljebb
capacity C bájtot használ, a lezáró nullkarakterrel együtt.
Pozitív kapacitásnál, érvényes célterület esetén nullterminál. A
visszatérési érték:
Ezért a helyes ellenőrzési sorrend:
/* ISO C17-részlet; dst valódi, írható tömb, value típusa long. */
int n = snprintf(dst, sizeof dst, "value=%ld", value);
if (n < 0) {
/* Kódolási hiba. */
} else if ((size_t)n >= sizeof dst) {
/* Csonkolás: a teljes eredmény nem fért el. */
}Előbb vizsgáljuk a negatív eredményt, csak utána konvertálunk
size_t-ra. A sizeof dst itt tömbméret;
pointerparaméteren nem a mögöttes tároló kapacitását adná.
scanf vagy az atoi?A scanf-család visszatérési értéke a sikeresen
hozzárendelt bemeneti elemek száma. A hozzárendelést elnyomó
* és a %n nem növeli ezt a számot. Illesztési
hibánál az eredmény lehet nulla; az első konverzió befejezése előtti
bemeneti hibánál EOF.
A mezőszélesség kötelező védelem a sztringeknél. A %15s
legfeljebb 15 karaktert tárol, majd nullkaraktert ír, tehát legalább 16
elemű char tömb kell hozzá. A %c ezzel szemben
nem tesz lezáró nullkaraktert.
A szélességkorlát azonban nem bizonyítja a teljes mező elfogadását. Egy hosszabb token maradéka a bemenetben maradhat, vagy a következő konverzióhoz kerülhet. Két sikeres hozzárendelés sem bizonyítja, hogy nincs további szemét a sorban.
Numerikus bemenetnél súlyosabb korlát, hogy ha az eredmény nem
reprezentálható a célobjektumban, a C17 scanf-szerződése
szerint nem definiált viselkedés következhet be. A return count tehát
nem általános túlcsordulás-ellenőrzés. Arra is figyeljünk, hogy itt
%f float *, %lf pedig
double * argumentumot vár.
Az atoi még kevesebb ellenőrzési lehetőséget ad: nincs
végpointere, és a nulla eredményből nem állapítható meg a siker. A nem
reprezentálható eredmény sem kezelhető megbízhatóan. Ellenőrizetlen
konfigurációhoz használjunk strto... függvényt.
A strtol, strtoull és strtod a
bemenet elején álló megfelelő alakot dolgozza fel. Az
endptr az első fel nem dolgozott karakterre mutat.
A teljes elfogadás feltétele:
A vizsgálat lépései:
errno értékét nullára, majd végezzük el a
konverziót.end == text feltétel azt jelzi, hogy nem történt
konverzió.ERANGE konverziós tartományhibát jelez.Az errno itt csak a konverzióhoz kapcsolódó jelzés:
sikeres hívás nem köteles nullára állítani. A végjel vizsgálatát nem
helyettesíti.
A következő program egy port=443 alakú argumentumot
fogad. A szerződés szándékosan szigorú: csak decimális számjegyek, nincs
előjel vagy szóköz, a tartomány pedig
.
Hiba esetén a kimeneti érték változatlan.
ISO C17, teljes program —
parse_config.c:
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
enum parse_status {
PARSE_OK,
PARSE_SYNTAX,
PARSE_OVERFLOW,
PARSE_TRAILING,
PARSE_DOMAIN
};
static enum parse_status
parse_port(const char *text, long *out)
{
if (text[0] < '0' || text[0] > '9') {
return PARSE_SYNTAX;
}
char *end;
errno = 0;
long value = strtol(text, &end, 10);
int conversion_errno = errno;
if (end == text) {
return PARSE_SYNTAX;
}
if (conversion_errno == ERANGE) {
return PARSE_OVERFLOW;
}
if (*end != '\0') {
return PARSE_TRAILING;
}
if (value < 1 || value > 65535) {
return PARSE_DOMAIN;
}
*out = value;
return PARSE_OK;
}
int main(int argc, char **argv)
{
if (argc != 2 || strncmp(argv[1], "port=", 5) != 0) {
fputs("expected: port=DECIMAL\n", stderr);
return EXIT_FAILURE;
}
long port = 0;
enum parse_status status = parse_port(argv[1] + 5, &port);
if (status != PARSE_OK) {
fprintf(stderr, "invalid port: status=%d\n", (int)status);
return EXIT_FAILURE;
}
char record[64];
int n = snprintf(record, sizeof record,
"event=config port=%ld\n", port);
if (n < 0 || (size_t)n >= sizeof record) {
fputs("log formatting failed\n", stderr);
return EXIT_FAILURE;
}
if (fputs(record, stdout) == EOF || fflush(stdout) == EOF) {
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}Reprodukáláshoz választott laborprofil: Linux, x86-64 System V ABI, LP64, glibc 2.40; GCC 14.2.0 vagy Clang 19.1.7. A következő parancsok fordítási útmutatók, nem itt elvégzett fordítás bizonyítékai:
gcc -std=c17 -Wall -Wextra -Wpedantic -Wconversion -Wformat=2 \
parse_config.c -o parse-config-gcc
clang -std=c17 -Wall -Wextra -Wpedantic -Wconversion -Wformat=2 \
parse_config.c -o parse-config-clangA várt besorolások:
| Bemenet | Eredmény |
|---|---|
port=443 |
Siker |
port=-1 |
Szintaktikai hiba |
port=443xyz |
Hátramaradt bemenet |
port=70000 |
Alkalmazási tartományhiba |
port= |
Szintaktikai hiba |
A LONG_MAX decimális alakja után még egy
0 |
Konverziós túlcsordulás |
Ha több hiba is fennáll, a program ellenőrzési sorrendje határozza meg az elsődleges hibát: a túlcsordulás például megelőzi a hátramaradt bemenet jelentését.
Helyességi bizonyításvázlat. A kezdőkarakter-vizsgálat kizárja az előjelet és a kezdő szóközt. A tízes alapú konverzió és a nullkarakterre mutató végpointer együtt biztosítja a teljes számjegysor feldolgozását. A konverziós és alkalmazási tartományvizsgálat után az érték érvényes portszám. Az egyetlen kimeneti értékadás ezek után történik.
| Objektum vagy hozzáférés | Tulajdonos és élettartam | Érvénytelenítés |
|---|---|---|
argv[1], illetve text |
A programindítási környezet tárolója; a parser kölcsönzi | A program végéig rendelkezésre áll |
end |
Nem tulajdonló pointer ugyanabba a sztringbe | Az alapul szolgáló tároló megszűnése |
out |
A hívó írható long objektuma; itt
port |
A hívó objektumának élettartamvége |
record |
A main automatikus tömbje |
Újraíráskor a tartalom, kilépéskor a tároló |
A parser előfeltétele érvényes nullterminált bemenet és érvényes, írható kimeneti objektum; pointereket nem őriz meg.
A strtoull elfogad mínuszjelet is: a "-1"
eredménye lehet ULLONG_MAX tartományhiba nélkül. Ezért a
„nemnegatív kvóta” szerződést nem biztosítja önmagában az előjel nélküli
céltípus. Követeljünk kezdő számjegyet, használjunk tízes alapot, majd
ellenőrizzük az ERANGE, a végpointer és az alkalmazási
felső korlát feltételeit.
A strtod esetén az ERANGE túlcsordulást
vagy alulcsordulást is jelezhet. A nulla eredmény önmagában nem hiba. A
függvény végtelent, NaN-t és hexadecimális lebegőpontos alakot is
elfogadhat; ezek engedélyezése külön döntés.
Egy véges, másodperces időköznél például:
/* ISO C17-részlet: <errno.h>, <math.h>, <stdlib.h>.
text: érvényes, kölcsönzött nullterminált sztring.
out: a hívó írható double objektuma.
A lexikai szabályokat a hívó már ellenőrizte.
A hibás ágakban out változatlan. */
char *end;
errno = 0;
double value = strtod(text, &end);
int conversion_errno = errno;
if (end == text) {
/* Nincs szám. */
} else if (conversion_errno == ERANGE) {
/* Konverziós tartományhiba. */
} else if (*end != '\0') {
/* Részleges feldolgozás. */
} else if (!isfinite(value) || value < 0.0 || value > 3600.0) {
/* Alkalmazási tartományhiba. */
} else {
*out = value;
}Ez a szemantikai ellenőrzés nem tiltja a hexadecimális alakot. Ha csak decimális protokollformátum engedélyezett, annak nyelvtanát külön ellenőrizni kell.
ISO C17: az fgets legfeljebb a megadott
kapacitásnál eggyel kevesebb karaktert olvas, megtartja a beolvasott
sortörést és siker esetén nullterminál. A hiányzó sortörés nem bizonyít
hibát: lehet hosszú sor részlete vagy az utolsó, sortörés nélküli sor. A
teljes sorhatár eldöntéséhez további olvasás kellhet. Túl hosszú
rekordnál a maradékot is kezeljük, különben a következő olvasás hamis új
rekordot kap.
A NULL eredménynél ferror és
feof alapján különítsük el a hibát és a bemenet végét. Az
olvasási hibával érintett buffert ne dolgozzuk fel.
POSIX: a getline szükség szerint
allokálja vagy növeli a hívó által kezelt buffert. Glibc alatt a
deklarációhoz használható a fejlécek előtt definiált
_POSIX_C_SOURCE=200809L. Visszatérési típusa
ssize_t; siker esetén a hossz tartalmazza a beolvasott
elválasztót, de nem a lezáró nullkaraktert. -1 jelzi a
bemenet végét vagy hibát.
A buffer tulajdonosa a hívó: végül free szükséges,
hibakimenet után is, ha történt allokáció. Új hívás felülírhatja a
tartalmat, áthelyezés pedig érvényteleníti a korábbi belső pointereket.
A korlátlan sorhossz memóriafogyasztási kockázat; szigorú
erőforráskorláthoz korlátozott sorolvasó kell.
Beágyazott nullkarakter esetén a strlen nem a teljes
beolvasott hosszt adja. A getline visszaadott hosszával ez
felismerhető; az fgets-re épülő egyszerű
sztringfeldolgozásnál a nullkarakter-mentesség legyen explicit bemeneti
feltétel.
A <ctype.h> függvényeinek argumentuma csak
EOF vagy unsigned char-ként reprezentálható
érték lehet:
/* ISO C17-részlet, <ctype.h>; ch egy char érték. */
int whitespace = isspace((unsigned char)ch);Egy negatív char közvetlen átadása UB lehet. A
getchar eredményét viszont először int-ként,
az EOF megőrzésével kezeljük.
A strtod tizedesjele a numerikus locale-tól függ. Gépi
konfigurációnál rögzítsük a nyelvtant és a locale-feltételeket;
könyvtári segédfüggvény ne módosítsa váratlanul a folyamat globális
locale-ját. Ha csak ASCII szóköz és tabulátor megengedett, ezek explicit
vizsgálata pontosabb szerződés, mint minden locale-függő whitespace
elfogadása.
Magyarázat: mi a hiba a
size_t length érték %u formátumú kiírásában?
Miért jelent csonkolást a snprintf esetén a kapacitással
egyenlő eredmény?
Kódolás: a portparser mintájára készíts
parse_quota függvényt unsigned long long
kimenettel és paraméterként kapott felső korláttal. A nulla legyen
érvényes, a mínuszjel ne.
Diagnosztika: egy logfeldolgozó
%7s %u formátummal olvas char level[8] és
unsigned int count objektumba. Két hozzárendelést teljes
sikernek tekint. Sorolj fel három problémát, és javasolj
javítást.
A %u unsigned int argumentumot vár, a
size_t nem szükségképpen ez a típus. A helyes formátum
%zu.
Ha a visszaadott hossz , akkor a szükséges kapacitás:
mert a nullkarakter is helyet foglal. Ezért
már csonkolást jelent. Ellenőrzési feltétel: teljes sikerhez
n >= 0 és (size_t)n < capacity kell.
A korábbi programhoz illeszthető ISO C17-függvényrészlet, azonos pointer- és hibaszerződéssel:
static enum parse_status
parse_quota(const char *text, unsigned long long limit,
unsigned long long *out)
{
if (text[0] < '0' || text[0] > '9')
return PARSE_SYNTAX;
char *end;
errno = 0;
unsigned long long value = strtoull(text, &end, 10);
int conversion_errno = errno;
if (end == text)
return PARSE_SYNTAX;
if (conversion_errno == ERANGE)
return PARSE_OVERFLOW;
if (*end != '\0')
return PARSE_TRAILING;
if (value > limit)
return PARSE_DOMAIN;
*out = value;
return PARSE_OK;
}Ellenőrzési feltétel: "0" és a korlát értéke sikeres;
"-1" szintaktikai hiba; "12x" részleges
feldolgozás; minden hibánál a korábban beállított kimenet
változatlan.
A hét karakternél hosszabb első mező maradéka a következő konverzióhoz kerülhet. A numerikus túlcsordulás nem kezelhető biztonságosan return counttal. Két sikeres hozzárendelés után további szemét is maradhat.
Először teljes, hosszkorlátozott rekordot olvassunk. Ezután
különítsük el és ellenőrizzük a mezőhatárokat, a számot pedig
strtoull és az UINT_MAX korlát segítségével
dolgozzuk fel. Ellenőrzési eset legyen hosszú szintnév, túl nagy szám és
érvényes rekord utáni extra token.
A formátumsztring típusszerződés, a buffer kapacitása tárolási szerződés, a parser pedig nyelvtani és értéktartomány-szerződés. Megbízható feldolgozás csak akkor történik, ha mindhárom teljesül. A „kaptam egy számot” helyett mindig azt bizonyítsuk: pontosan a megengedett teljes bemenetből kaptam megengedett értéket.