Hier, j’ai passé un bon moment à lutter avec Scilab et la lecture de données dans un fichier binaire. Au final, le problème venait de la présence du caractère ASCII NUL (valeur 0) dans la chaine de caractères que je lisais avec mgetstr.
Le problème présenté ici affecte la plupart des fonctions concernées : strindex, strcmp, strsubst, part, strtok…
Le problème
Utilisons la fonction strindex pour chercher l’indice du début d’un motif « def » dans une chaine de caractères « abcdef » :
str = "abcdef"
idx = strindex(str, "def")
Renvoi :
Ajoutons un espace, dont la valeur ASCII est 32, au début de la chaine précédente :
str = ascii(32) + str // Ajout du caractère espace
idx = strindex(str, "def")
Renvoi :
Ajoutons une tabulation, dont la valeur ASCII est 8, au début de la chaine précédente :
str = ascii(8) + str // Ajout du caractère tabulation
idx = strindex(str, "def")
Renvoi :
Aucun problème donc jusqu’ici.
Ajoutons maintenant le caractère NUL, dont la valeur ASCII est 0, au début de la chaine précédente :
str = ascii(0) + str // Ajout du caractère NUL
idx = strindex(str, "def")
Renvoi :
Aïe ! Aïe ! Aïe !
Le problème est identique avec d’autres fonctions :
part(str, 1)
ans =
part(str, 7)
ans =
part(str, 8)
ans =
L’explication
Les fonctions de Scilab qui traitent les chaines de caractères sont écrites en C. Or pour stocker une chaine de n caractères, il faut (au minimum) un tableau de n+1 éléments en langage C. Le dernier élément identifie la fin de la chaine de caractères. Et bien entendu, le caractère qui a été retenu pour remplir cette mission est le caractère NUL dont la valeur ASCII est 0 (voir la FAQ C à ce sujet).
Conséquence : si une chaine contient le caractère NUL (0), tous les caractères qui suivent seront ignorés.
Par exemple :
str = "hello world"
substr(1) = part(str, 1:5)
substr(2) = part(str, 7:$)
Renvoi :
Alors que :
str = "hello" + ascii(0) + "world"
substr(1) = part(str, 1:5)
substr(2) = part(str, 7:$)
Renvoi :
Une solution
La solution consiste à remplacer le caractère NUL par un autre avant de traiter la chaine de caractères. Mais attention, il faudra impérativement remplacer la valeur numérique du caractère 0 par une autre (de 1 à 255) car nous avons vu précédemment que les fonctions Scilab n’avaient aucun effet.
str = "hello" + ascii(0) + "world"
str =
hello world
ascii(str)
ans =
104. 101. 108. 108. 111. 0. 119. 111. 114. 108. 100.
newStr = strsubst(str, 0, 32)
!--error 999
strsubst : Type erroné de l'argument d'entrée n°3 : Une chaîne de caractères attendue.
newStr = strsubst(str, ascii(0), ascii(32))
newStr =
hello
Il faut donc faire la conversion implicitement :
numStr = ascii(str)
numStr =
104. 101. 108. 108. 111. 0. 119. 111. 114. 108. 100.
idx = numStr==0
idx =
F F F F F T F F F F F
numStr(idx) = 32 // par exemple
numStr =
104. 101. 108. 108. 111. 32. 119. 111. 114. 108. 100.
newStr = ascii(numStr)
newStr =
hello world
Voila on peut maintenant gérer la chaine de caractères avec les fonctions Scilab :
substr(1) = part(newStr, 1:5)
substr =
!hello !
! !
! !
substr(2) = part(newStr, 7:$)
substr =
!hello !
! !
!world !
idx = strindex(newStr, "world")
idx =
7.
Pourquoi mettre le caractère NUL dans une chaine de caractères ?
Quel est l’intérêt de mettre un caractère NUL dans une chaine ? J’aimerais pouvoir vous répondre : « aucun ». Mais j’ai mentionné la lecture de fichier tout au début de ce billet. Un fichier binaire peut contenir des valeurs numériques et des chaines de caractères.
Prenons l’exemple d’un fichier binaire contenant une valeur 0 suivie d’une chaine de caractères :
fd = mopen("test.dat", "wb")
mput(0, "uc", fd)
mput(ascii("hello world"), "uc", fd)
mclose(fd)
La lecture ne pose pas de problème particulier :
fd = mopen("test.dat", "rb")
str = mgetstr(12, fd)
mclose(fd)
Ce qui donne bien :
str
str =
hello world
ascii(str)
ans =
0. 104. 101. 108. 108. 111. 32. 119. 111. 114. 108. 100.
Par contre :
idx = strindex(str, "world")
idx =
[]
Vous n’êtes pas convaincu par cet exemple trop simpliste, passons alors à un exemple pratique.
Validation de fichier DICOM
Les fichiers DICOM sont des fichiers binaires utilisés pour le stockage d’informations médicales. Ils contiennent des valeurs numériques et des chaines de caractères. Avant de lire un de ces fichiers, il est utile de tester sa validité. Le test le plus robuste consiste à chercher la chaine de caractères « 1.2.840.10008. » dans les premiers octets du fichier. La position de la chaine dans le fichier est inconnue et rien n’empêche que des valeurs nulles soient écrites avant…
La mauvaise solution :
fd = mopen("test.dcm", "rb")
buffer = mgetstr(2000, fd)
chk = strindex(buffer, "1.2.840.10008.")
if isempty(chk) then
disp("Fichier non valide")
else
disp("Fichier valide")
end
mclose(fd)
Le code ci-dessus renverra constamment « Fichier non valide » même si la chaine « 1.2.840.10008. » est bien présente dans le fichier.
La solution plus robuste à utiliser :
fd = mopen("test.dcm", "rb")
buffer = mgeti(2000, "uc", fd)
idx = buffer~=0
chk = strindex(ascii(buffer(idx)), "1.2.840.10008.")
if isempty(chk) then
disp("Fichier non valide")
else
disp("Fichier valide")
end
mclose(fd)
Dans cette version du code, les caractères NUL seront pris en compte avant de chercher la chaine « 1.2.840.10008. » Ce code fonctionne donc comme il faut.