printf format strings prüfen?

#7825188
Lesenswert?

Mahlzeit!

-Wformat und besonders -Wformat-security sind ja nicht verkehrt. Ein nicht-konstanter format string wird dann angemeckert. Seht gut, aber es gibt eine Grauzone, zum Beispiel finde ich, dass diese format strings durchaus konstant sind. Kann ich dem gcc außer "const" noch einen Tipp geben?

1
int
2
my_mount (unsigned long flags, const char *source)
3
{
4
  static const char  *opttab[] = {
5
    "errors=remount-ro,check=strict,showexec,uid=%d,gid=%d,time_offset=%ld",
6
    "errors=remount-ro,uid=%d,gid=%d,time_offset=%ld",
7
    "errors=remount-ro",
8
    NULL
9
  };
10
  const char  *fsopts;
11
// blkid usw. findet msdos, exfat, ext4 und dann...
12
  fsopts = opttab[type];
13
  asprintf (&options, fsopts, uid, uid, timezone / 60L);
14
  err = mount (source, default_mnt, fstype, flags, options);

Im nächsten Beispiel ist im Endeffekt auch alles konstant, aber da ist die Warnung wohl unvermeidlich. Gibt es vielleicht "eine lib", mit der ich den format string prüfen könnte? Und wie sage ich dann dem strftime(), dass der ok ist?

1
char *
2
isotime (const char *format, time_t utc)
3
{
4
  static char  text[40];
5
  time_t       unixtime;
6

7
  if (utc == 0) {
8
    return (char *)"";
9
  }
10
  if (utc == 1) {
11
    unixtime = time (NULL);
12
  } else {
13
    unixtime = utc;
14
  }
15
  if (format == NULL || *format == '\0') {
16
    format = "%F %T";
17
  }
18
  strftime (text, sizeof text, format, localtime (&unixtime));
19
  return text;
20
}

Ja, natürlich kann ich das mount auch anders formulieren, da gibt es nur vier bekannte Dateisysteme. Aber wie man ein Datum schreibt, ist ja leider völlig undefiniert.

Persönliche Seite #7825217
Lesenswert?

Bauform B. schrieb:

zum Beispiel finde ich, dass diese format strings durchaus konstant sind. Kann ich dem gcc außer "const" noch einen Tipp geben?

Damit ein Format-String gecheckt werden kann, muss er natürlich zur Compilezeit bekannt sein, und das ist

1
>   const char  *fsopts = opttab[type];

Nun mal nicht, weil type nicht bekannt ist zur Laufzeit.

Ich nehme mal an, dass die Obfuskation über das Array nicht super Performance-kritisch ist, also einfach die 3 Fälle per if+else oder switch+case abhandeln mit FMT-Strings die zur Compilezeit bekannt sind.

#7825307
Lesenswert?

Johann L. schrieb:

Damit ein Format-String gecheckt werden kann, muss er natürlich zur Compilezeit bekannt sein, und das ist

const char *fsopts = opttab[type];

Nun mal nicht, weil type nicht bekannt ist zur Laufzeit.

Ach was, natürlich ist type bekannt ;) Wenn es zu groß würde, wäre das ja undefined! Aber an der Stelle geht if/else problemlos, also gut.

Und jetzt auf einmal geht's für das Datumsformat genauso einfach: ich erlaube nur den Default oder %x "The preferred date representation for the current locale without the time". Wer sich ein Fantasieformat ausdenkt, oder in Italien die norwegische Schreibweise will, bekommt automatisch RFC 3339. Das muss reichen, manche Formate gehören sowieso verboten.

Haben wir wieder mal ein Problem elegant? umgangen. Trotzdem, so eine Format-Prüf-Funktion sollte es eigentlich geben, braucht man das nicht, wenn man gettext mit fremden Übersetzungen benutzt?

Persönliche Seite #7825329
Lesenswert?

Bauform B. schrieb:

Johann L. schrieb:

Damit ein Format-String gecheckt werden kann, muss er natürlich zur Compilezeit bekannt sein, und das ist

const char *fsopts = opttab[type];

Nun mal nicht, weil type nicht bekannt ist zur Laufzeit.

Ach was, natürlich ist type bekannt

Was aber nicht aus deinem Code ersichtlich ist.

#7825407
Lesenswert?

Bauform B. schrieb:

Johann L. schrieb:

Damit ein Format-String gecheckt werden kann, muss er natürlich zur Compilezeit bekannt sein, und das ist

const char *fsopts = opttab[type];

Nun mal nicht, weil type nicht bekannt ist zur Laufzeit.

Ach was, natürlich ist type bekannt ;)

Wenn type schon zur Compilezeit feststeht, wozu brauchst du es dann?

#7825529
Lesenswert?

Bauform B. schrieb:

Ach, keiner versteht mich :( Natürlich ist das erst zur Laufzeit bekannt. Aber sonst heißt es immer, UB gibt es nicht. Also könnte der gcc sagen, es muss eins von den drei Formaten sein und die sind alle ok.

Das könnte er möglicherweise (Wo type herkommt und wie es zu seinem Wert kommt, sieht man deinem Code oben nicht), aber das wäre recht komplex, und so ein Verhalten müsste extra da reinprogrammiert werden, für einen äußerst seltenen Anwendungsfall.

Ich benutze allerdings auch -O, vielleicht würde er es mit -O3 sogar finden.

Höhere Optimierungsstufen sind für sowas definitv besser, weil der Compiler dann eine ausführliche Codeanalyse machen muss, durch die solche Sonderfälle eher gefunden werden können.

Moderator Persönliche Seite #7826712
Lesenswert?

Bauform B. schrieb:

Natürlich ist das erst zur Laufzeit bekannt.

Da type nur die Werte 0, 1, 2 annehmen kann, mache einen switch über die Variable type und schreibe das printf jedesmal explizit mit dem konkreten Format-String hin.

Dann kannst Du Dir das array "opttab" sparen und der Code wird lesbarer. Außerdem kann der Compiler dann zur Compilezeit die drei Formatstrings prüfen.

#7827191
Lesenswert?

Frank M. schrieb:

mache einen switch über die Variable type und schreibe das printf jedesmal explizit mit dem konkreten Format-String hin.

Ja, natürlich ist das in diesem Fall besser. Rein optisch sieht es jetzt garnicht so übel aus. Aber wenn es mehr Fälle werden, ist mir ein Liste mit structs (hier fstype und options) doch lieber.

1
static const char dosopts[] = "check=strict,showexec";
2

3
snprintf (fatopts, sizeof fatopts, "time_offset=%ld,uid=%d,gid=%d",
4
          timezone / 60L, uid, uid);
5
if (strcmp (fstype, "vfat") == 0 || strcmp (fstype, "msdos") == 0) {
6
   asprintf (&options, "%s,%s,%s", errors, fatopts, dosopts);
7
} else {
8
   if (strcmp (fstype, "exfat") == 0) {
9
      asprintf (&options, "%s,%s", errors, fatopts);
10
   } else {
11
      if (strcmp (fstype, "ext4") == 0) {
12
         asprintf (&options, "%s", errors);
13
      } else {
14
         fail (__func__, 0, "Unknown fstype '%s'", fstype);
15
      }
16
   }
17
}
Persönliche Seite #7827266
Lesenswert?

Bauform B. schrieb:

Rein optisch

Viele Editoren unterstützen auch folgende Einrückung:

1
if (strcmp (fstype, "vfat") == 0 || strcmp (fstype, "msdos") == 0) {
2
   asprintf (&options, "%s,%s,%s", errors, fatopts, dosopts);
3
} else if (strcmp (fstype, "exfat") == 0) {
4
   asprintf (&options, "%s,%s", errors, fatopts);
5
} else if (strcmp (fstype, "ext4") == 0) {
6
   asprintf (&options, "%s", errors);
7
} else {
8
   fail (__func__, 0, "Unknown fstype '%s'", fstype);
9
}

Wobei der grauslige Stil, } anders einzurücken als das zugehörige {, grauslig ist.

Moderator Persönliche Seite #7827280
Lesenswert?

Johann L. schrieb:

Wobei der grauslige Stil, } anders einzurücken als das zugehörige {, grauslig ist.

Deshalb schreibe ich die { immer auf eine extra Zeile. Dann ist rechts daneben auch noch jede Menge Platz für einen passenden Kommentar. :-)

P.S.

Auch für die } gebe ich mir eine extra Zeile und schreibe auch das "else if" darunter. Also:

1
if (! strcmp (fstype, "vfat") || ! strcmp (fstype, "msdos"))
2
{
3
   asprintf (&options, "%s,%s,%s", errors, fatopts, dosopts);
4
}
5
else if (! strcmp (fstype, "exfat"))
6
{
7
   asprintf (&options, "%s,%s", errors, fatopts);
8
}
9
else if (! strcmp (fstype, "ext4"))
10
{
11
   asprintf (&options, "%s", errors);
12
}
13
else
14
{
15
   fail (__func__, 0, "Unknown fstype '%s'", fstype);
16
}

Aber das ist Geschmackssache. Bei den heutigen Monitorgrößen muss man aber nicht unbedingt mit Platz geizen. Noch ein Vorteil: Ich kann mit dem Editor jederzeit einen else-if-Block zeilenweise rausschneiden oder einfügen, ohne die Formatierung in unmittelbarer Umgebung anpassen zu müssen.

EDIT:

strcmp-Vergleich gekürzt.

#7827381
Lesenswert?

Bruno V. schrieb:

Bauform B. schrieb:

Euch fehlen ein paar { { {.

Nein. Zu else if gibt es einen breiten Konsens, dass nur hier auf {} verzichtet werden darf. Dafür ist das abschließende else Pflicht.

Ach wo, wenn es legal ist darf man es schreiben. Ich habe auch schon so Sachen wie "do if(){ }while();" oder "if(){}else for(;;){}" hingeschrieben. Das ist völlig OK.

#7827606
Lesenswert?

Bruno V. schrieb:

Zu else if gibt es einen breiten Konsens, dass nur hier auf {} verzichtet werden darf, ja sogar sollte.

Natürlich. Genauso wie einen breiten Konsens darüber gibt, wo man öffnende und schließende Klammern zu setzen hat...

Immerhin werfen gcc und Clang inzwischen eine Warnung, wenn die auf else ohne Klammern und potentiell missverständliche Formatierungen treffen.

Klammern. Immer. Ausnahmen bestätigen die Regel.

Oliver

Moderator Persönliche Seite #7827631
Lesenswert?

Oliver S. schrieb:

Immerhin werfen gcc und Clang inzwischen eine Warnung, wenn die auf else ohne Klammern und potentiell missverständliche Formatierungen treffen.

Das ist ja auch sinnvoll. Aber hier wird garantiert nicht gewarnt:

1
if (foo)
2
{
3
    bar();
4
}
5
else if (foo2)
6
{
7
    bar2();
8
}
9
else if (foo3)
10
{
11
    bar3();
12
}
13
else
14
{
15
    baz();
16
}

Grund: Das auf else folgende if() mit geklammerten Block dahinter ist für den Compiler grundsätzlich ein Statement, so dass auf die geschweifte Klammer zwischen else und if immer(!) verzichtet werden kann - wie man es vom switch() gewohnt ist. Das ist auch keine C-Sonderregel oder ähnliches, sondern ergibt sich klar aus der C-Grammatik.

Auf diese Klammer- bzw. Einrückungs-Orgie hier:

1
if (foo)
2
{
3
    bar();
4
}
5
else
6
{
7
    if (foo2)
8
    {
9
        bar2();
10
    }
11
    else
12
    {
13
        if (foo3)
14
        {
15
            bar3();
16
        }
17
        else
18
        {
19
            baz();
20
        }
21
    }
22
}

kann also immer verzichtet werden. Es reicht eine lineare Aufzählung aller Bedingungen. Die obere lineare Lösung ist auch wesentlich lesbarer als ein derart ineinander geschachtelter Block. Der wird nämlich, wenn eine neue Bedingung hinzukommt, immer weiter verschachtelt und nach rechts immer weiter eingerückt. Bei 20 Bedingungen ist man dann "ganz weit draußen".

#7827738
Lesenswert?

Oliver S. schrieb:

Immerhin werfen gcc und Clang inzwischen eine Warnung, wenn die auf else ohne Klammern und potentiell missverständliche Formatierungen treffen.

Wirklich bei else if -Reihen mit abschließendem else? (Ich nehme an, Du hast das ausprobiert und mit MISRA und Co bisher nichts zu tun gehabt. Da ist dann wohl was schiefgelaufen)

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren