Les fonctions d'une bibliothèque C devraient-elles toujours attendre la longueur d'une chaîne?

15

Je travaille actuellement sur une bibliothèque écrite en C. De nombreuses fonctions de cette bibliothèque attendent une chaîne au fur char*et const char*à mesure de leurs arguments. J'ai commencé avec ces fonctions en m'attendant toujours à la longueur de la chaîne size_tafin que la terminaison nulle ne soit pas requise. Cependant, lors de l'écriture des tests, cela a entraîné une utilisation fréquente de strlen(), comme ceci:

const char* string = "Ugh, strlen is tedious";
libFunction(string, strlen(string));

Faire confiance à l'utilisateur pour passer des chaînes correctement terminées conduirait à un code moins sûr, mais plus concis et (à mon avis) lisible:

libFunction("I hope there's a null-terminator there!");

Alors, quelle est la pratique raisonnable ici? Rendre l'API plus compliquée à utiliser, mais forcer l'utilisateur à penser à son entrée, ou documenter l'exigence d'une chaîne terminée par un caractère nul et faire confiance à l'appelant?

Benjamin Kloster
la source

Réponses:

4

Très certainement et absolument porter la longueur autour . La bibliothèque C standard est tristement célèbre de cette façon, ce qui n'a pas causé de douleur en traitant les débordements de tampon. Cette approche est au centre de tant de haine et d'angoisse que les compilateurs modernes avertiront, se plaindront et se plaindront en utilisant ce type de fonctions de bibliothèque standard.

C'est si mauvais que si jamais vous rencontrez cette question lors d'une entrevue - et votre enquêteur technique semble avoir quelques années d'expérience - le pur fanatisme peut décrocher le poste - vous pouvez en fait aller assez loin si vous pouvez citer le précédent de tirer sur quelqu'un qui implémente des API à la recherche du terminateur de chaîne C.

Laissant de côté l'émotion de tout cela, il y a beaucoup de choses qui peuvent mal tourner avec ce NULL à la fin de votre chaîne, à la fois en le lisant et en le manipulant - en plus, il est vraiment en violation directe des concepts de design modernes tels que la défense en profondeur (pas nécessairement appliqué à la sécurité, mais à la conception d'API). Des exemples d'API C qui portent la longueur abondent - ex. l'API Windows.

En fait, ce problème a été résolu dans les années 90, le consensus émergent aujourd'hui est que vous ne devriez même pas toucher à vos cordes .

Édition ultérieure : c'est un débat assez vivant, donc j'ajouterai que faire confiance à tout le monde en dessous et au-dessus de vous pour être gentil et utiliser les fonctions de la bibliothèque str * est OK, jusqu'à ce que vous voyiez des choses classiques comme output = malloc(strlen(input)); strcpy(output, input);ou while(*src) { *dest=transform(*src); dest++; src++; }. J'entends presque le Lacrimosa de Mozart en arrière-plan.

vski
la source
1
Je ne comprends pas votre exemple de l'API Windows exigeant que l'appelant fournisse la longueur des chaînes. Par exemple, une fonction API Win32 typique telle que CreateFileprend un LPTCSTR lpFileNameparamètre en entrée. Aucune longueur de la chaîne n'est attendue de l'appelant. En fait, l'utilisation de chaînes terminées par NUL est tellement ancrée que la documentation ne mentionne même pas que le nom de fichier doit être terminé par NUL (mais bien sûr il doit l'être).
Greg Hewgill
1
En fait , dans Win32, le LPSTRgenre dit que les chaînes peuvent être NUL-fin, et si non , qui sera indiqué dans la spécification associée. Donc, sauf indication contraire, ces chaînes dans Win32 devraient se terminer par NUL.
Greg Hewgill
Grand point, j'étais imprécis. Considérez que CreateFile et son groupe existent depuis Windows NT 3.1 (début des années 90); l'API actuelle (c'est-à-dire depuis l'introduction de Strsafe.h dans XP SP2 - avec les excuses publiques de Microsoft) a déprécié explicitement toutes les choses terminées par NULL qu'elle pouvait. La première fois que Microsoft s'est senti vraiment désolé d'utiliser des chaînes terminées par NULL était en fait beaucoup plus tôt, quand ils ont dû introduire le BSTR dans la spécification OLE 2.0, afin de mettre en quelque sorte VB, COM et l'ancien WINAPI dans le même bateau.
vski
1
Même dans StringCbCatpar exemple, seule la destination a un tampon maximum, ce qui est logique. La source est toujours une chaîne C ordinaire terminée par NUL. Vous pourriez peut-être améliorer votre réponse en clarifiant la différence entre un paramètre d' entrée et un paramètre de sortie . Les paramètres de sortie doivent toujours avoir une longueur de tampon maximale; les paramètres d'entrée sont généralement terminés par NUL (il existe des exceptions, mais rares dans mon expérience).
Greg Hewgill
1
Oui. Les chaînes sont immuables à la fois sur la JVM / Dalvik et le .NET CLR au niveau de la plate-forme, ainsi que dans de nombreuses autres langues. J'irais si loin et je supposerais que le monde natif ne peut pas encore le faire (la norme C ++ 11) à cause de a) l'héritage (vous ne gagnez pas vraiment beaucoup en n'ayant qu'une partie de vos chaînes immuables) et b ) vous avez vraiment besoin d'un GC et d'une table de chaînes pour que cela fonctionne, les allocateurs de portée en C ++ 11 ne peuvent pas tout à fait le couper.
vski
16

En C, l'idiome est que les chaînes de caractères sont terminées par NUL, il est donc logique de respecter la pratique courante - il est en fait relativement peu probable que les utilisateurs de la bibliothèque aient des chaînes non terminées par NUL (car celles-ci nécessitent un travail supplémentaire pour imprimer en utilisant printf et utiliser dans un autre contexte). L'utilisation de tout autre type de chaîne n'est pas naturelle et probablement relativement rare.

De plus, dans les circonstances, vos tests me semblent un peu étranges, car pour fonctionner correctement (en utilisant strlen), vous supposez une chaîne terminée par NUL en premier lieu. Vous devez tester le cas des chaînes non terminées par NUL si vous souhaitez que votre bibliothèque fonctionne avec elles.

James McLeod
la source
-1, je suis désolé, c'est tout simplement mal avisé.
vski
Autrefois, ce n'était pas toujours vrai. J'ai beaucoup travaillé avec des protocoles binaires qui mettent des données de chaîne dans des champs de longueur fixe qui ne sont pas terminés par NULL. Dans de tels cas, il était très pratique de travailler avec des fonctions longues. Je n'ai pas fait C depuis une décennie, cependant.
Gort the Robot
4
@vski, comment le fait de forcer l'utilisateur à appeler «strlen» avant d'appeler la fonction cible fait-il quoi que ce soit pour éviter les problèmes de dépassement de tampon? Au moins, si vous vérifiez vous-même la longueur dans la fonction cible, vous pouvez être sûr du sens de la longueur utilisé (y compris le terminal nul ou non).
Charles E. Grant
@Charles E. Grant: Voir le commentaire ci-dessus sur StringCbCat et StringCbCatN dans Strsafe.h. Si vous avez juste un char * et pas de longueur, alors vous n'avez vraiment pas d'autre choix que d'utiliser les fonctions str *, mais le but est de transporter la longueur, donc cela devient une option entre str * et strn * fonctions dont celles-ci sont préférées.
vski
2
@vski Il n'est pas nécessaire de contourner la longueur d' une chaîne . Il est nécessaire de contourner la longueur d' un tampon . Tous les tampons ne sont pas des chaînes et toutes les chaînes ne sont pas des tampons.
jamesdlin
10

Votre argument "sécurité" ne tient pas vraiment. Si vous ne faites pas confiance à l'utilisateur pour vous remettre une chaîne terminée par un caractère nul lorsque c'est ce que vous avez documenté (et ce qui est "la norme" pour le C ordinaire), vous ne pouvez pas vraiment faire confiance à la longueur qu'il vous donne non plus (ce qu'il obtenir probablement en utilisant strlencomme vous le faites s'ils ne l'ont pas à portée de main, et qui échouera si la "chaîne" n'était pas une chaîne en premier lieu).

Il existe cependant des raisons valables d'exiger une longueur: si vous voulez que vos fonctions fonctionnent sur des sous-chaînes, il est peut-être beaucoup plus facile (et efficace) de passer une longueur que de demander à l'utilisateur de faire de la magie d'avant en arrière pour obtenir l'octet nul. au bon endroit (et risquez des erreurs ponctuelles en cours de route).
Être capable de gérer des encodages où les octets nuls ne sont pas des terminaisons, ou être capable de gérer des chaînes qui ont des valeurs nulles incorporées (exprès) peut être utile dans certaines circonstances (cela dépend de ce que font exactement vos fonctions).
Être capable de gérer des données non nulles (tableaux de longueur fixe) est également pratique.
En bref: cela dépend de ce que vous faites dans votre bibliothèque et du type de données que vous attendez de vos utilisateurs.

Il y a aussi peut-être un aspect de performance à cela. Si votre fonction a besoin de connaître la longueur de la chaîne à l'avance et que vous vous attendez à ce que vos utilisateurs connaissent déjà au moins ces informations, les faire passer (plutôt que de les calculer) pourrait raser quelques cycles.

Mais si votre bibliothèque attend des chaînes de texte ASCII ordinaires et que vous n'avez pas de contraintes de performance atroces et une très bonne compréhension de la façon dont vos utilisateurs interagiront avec votre bibliothèque, l'ajout d'un paramètre de longueur ne semble pas une bonne idée. Si la chaîne n'est pas correctement terminée, il est probable que le paramètre de longueur sera tout aussi faux. Je ne pense pas que vous y gagnerez beaucoup.

Tapis
la source
Fortement en désaccord avec cette approche. Ne faites jamais confiance à vos appelants, en particulier derrière une API de bibliothèque, faites de votre mieux pour remettre en question ce qu'ils vous donnent et échouez gracieusement. Porter la longueur sacrée, travailler avec des chaînes terminées par NULL n'est pas ce que signifie "être lâche avec vos appelants et strict avec vos callees".
vski
2
Je suis d' accord la plupart du temps avec votre position, mais vous semblez mettre beaucoup de confiance dans cet argument de longueur - il n'y a aucune raison pour laquelle il devrait être fiable que la terminaison null. Ma position est que cela dépend de ce que fait la bibliothèque.
Mat
Il y a beaucoup plus de problèmes avec le terminateur NULL dans les chaînes qu'avec la longueur passée par valeur. En C, la seule raison pour laquelle on ferait confiance à la longueur est qu'il serait déraisonnable et peu pratique de ne pas porter la longueur du tampon n'est pas une bonne réponse, c'est juste la meilleure en considérant les alternatives. C'est l'une des raisons pour lesquelles les chaînes (et les tampons en général) sont soigneusement emballées et encapsulées dans les langages RAD.
vski
2

Non. Les chaînes sont toujours terminées par définition, la longueur de la chaîne est redondante.

Les données de caractère non terminées par null ne doivent jamais être appelées une "chaîne". Le traiter (et jeter des longueurs) doit généralement être encapsulé dans une bibliothèque et ne pas faire partie de l'API. Exiger la longueur comme paramètre juste pour éviter les appels strlen () est probablement une optimisation prématurée.

Faire confiance à l'appelant d'une fonction API n'est pas dangereux ; un comportement indéfini est parfaitement correct si les conditions préalables documentées ne sont pas remplies.

Bien sûr, une API bien conçue ne devrait pas contenir d'embûches et devrait faciliter son utilisation correcte. Et cela signifie simplement qu'il devrait être aussi simple et direct que possible, en évitant les redondances et en suivant les conventions du langage.

dpi
la source
non seulement parfaitement bien, mais en fait inévitable à moins que l'on ne passe à une langue à mémoire unique et à thread unique. Il se peut que certaines restrictions supplémentaires aient été supprimées ...
Dédoublonneur
1

Vous devez toujours garder votre longueur. D'une part, vos utilisateurs peuvent souhaiter y contenir des valeurs NULL. Et deuxièmement, n'oubliez pas que strlenc'est O (N) et nécessite de toucher tout le cache de bye bye. Et troisièmement, il est plus facile de contourner les sous-ensembles - par exemple, ils pourraient donner moins que la longueur réelle.

DeadMG
la source
4
La question de savoir si la fonction de bibliothèque traite les valeurs NULL intégrées dans les chaînes doit être très bien documentée. La plupart des fonctions de la bibliothèque C s'arrêtent à NULL ou à la longueur, selon la première éventualité. (Et s'ils sont écrits avec compétence, ceux qui ne prennent pas de longueur ne sont jamais utilisés strlendans un test de boucle.)
Gort the Robot
1

Vous devez faire la distinction entre passer une chaîne et passer un tampon .

En C, les chaînes sont traditionnellement terminées par NUL. Il est tout à fait raisonnable de s’attendre à cela. Par conséquent, il n'est généralement pas nécessaire de contourner la longueur de la chaîne; il peut être calculé avec strlensi nécessaire.

Lorsque vous passez un tampon , en particulier celui qui est écrit, vous devez absolument transmettre la taille du tampon. Pour un tampon de destination, cela permet à l'appelé de s'assurer qu'il ne déborde pas du tampon. Pour un tampon d'entrée, cela permet à l'appelé d'éviter de lire après la fin, surtout si le tampon d'entrée contient des données arbitraires provenant d'une source non fiable.

Il y a peut-être une certaine confusion car les chaînes et les tampons peuvent l'être char*et parce que de nombreuses fonctions de chaîne génèrent de nouvelles chaînes en écrivant dans les tampons de destination. Certaines personnes concluent alors que les fonctions de chaîne doivent prendre des longueurs de chaîne. Cependant, c'est une conclusion inexacte. La pratique d'inclure une taille avec un tampon (que ce tampon soit utilisé pour des chaînes, des tableaux d'entiers, des structures, etc.) est un mantra plus utile et plus général.

(Dans le cas de la lecture d'une chaîne à partir d'une source non fiable (par exemple une prise réseau), il est important de fournir une longueur car l'entrée peut ne pas se terminer par NUL. Cependant , vous ne devez pas considérer l'entrée comme une chaîne. Vous devrait le traiter comme un tampon de données arbitraire qui pourrait contenir une chaîne (mais vous ne savez pas jusqu'à ce que vous le validiez réellement), donc cela suit toujours le principe selon lequel les tampons doivent avoir des tailles associées et que les chaînes n'en ont pas besoin.)

jamesdlin
la source
C'est exactement ce que la question et les autres réponses ont manqué.
Blrfl
0

Si les fonctions sont principalement utilisées avec des littéraux de chaîne, la douleur de traiter des longueurs explicites peut être minimisée en définissant certaines macros. Par exemple, étant donné une fonction API:

void use_string(char *string, int length);

on pourrait définir une macro:

#define use_strlit(x) use_string(x, sizeof ("" x "")-1)

puis l'invoquez comme indiqué dans:

void test(void)
{
  use_strlit("Hello");
}

Bien qu'il soit possible de trouver des choses "créatives" pour passer cette macro qui se compilera mais ne fonctionnera pas réellement, l'utilisation de ""chaque côté de la chaîne dans l'évaluation de "sizeof" devrait intercepter les tentatives accidentelles d'utiliser le caractère pointeurs autres que les littéraux de chaîne décomposés [en l'absence de ceux-ci "", une tentative de passer un pointeur de caractère donnerait à tort la longueur comme la taille d'un pointeur, moins un.

Une approche alternative dans C99 serait de définir un type de structure "pointeur et longueur" et de définir une macro qui convertit un littéral de chaîne en un littéral composé de ce type de structure. Par exemple:

struct lstring { char const *ptr; int length; };
#define as_lstring(x) \
  (( struct lstring const) {x, sizeof("" x "")-1})

Notez que si l'on utilise une telle approche, il faut passer ces structures par valeur plutôt que de passer autour de leurs adresses. Sinon, quelque chose comme:

struct lstring *p;
if (foo)
{
  p = &as_lstring("Hello");
}
else
{
  p = &as_lstring("Goodbye!");
}
use_lstring(p);

peut échouer car la durée de vie des littéraux composés prendrait fin à la fin des instructions qui les entourent.

supercat
la source