2007-11-22 Dick Porter <dick@ximian.com>
[mono.git] / mono / io-layer / versioninfo.c
1 /*
2  * versioninfo.c:  Version information support
3  *
4  * Author:
5  *      Dick Porter (dick@ximian.com)
6  *
7  * (C) 2007 Novell, Inc.
8  */
9
10 #include <config.h>
11 #include <glib.h>
12 #include <string.h>
13 #include <pthread.h>
14 #include <sys/mman.h>
15 #include <sys/types.h>
16 #include <sys/stat.h>
17 #include <unistd.h>
18 #include <fcntl.h>
19 #include <errno.h>
20
21 #include <mono/io-layer/wapi.h>
22 #include <mono/io-layer/wapi-private.h>
23 #include <mono/io-layer/versioninfo.h>
24 #include <mono/io-layer/io-portability.h>
25 #include <mono/io-layer/error.h>
26 #include <mono/utils/strenc.h>
27
28 #undef DEBUG
29
30 static WapiImageSectionHeader *get_enclosing_section_header (guint32 rva, WapiImageNTHeaders *nt_headers)
31 {
32         WapiImageSectionHeader *section = IMAGE_FIRST_SECTION (nt_headers);
33         guint32 i;
34         
35         for (i = 0; i < nt_headers->FileHeader.NumberOfSections; i++, section++) {
36                 guint32 size = section->Misc.VirtualSize;
37                 if (size == 0) {
38                         size = section->SizeOfRawData;
39                 }
40                 
41                 if ((rva >= section->VirtualAddress) &&
42                     (rva < (section->VirtualAddress + size))) {
43                         return(section);
44                 }
45         }
46         
47         return(NULL);
48 }
49
50 static gpointer get_ptr_from_rva (guint32 rva, WapiImageNTHeaders *ntheaders,
51                                   gpointer file_map)
52 {
53         WapiImageSectionHeader *section_header;
54         guint32 delta;
55         
56         section_header = get_enclosing_section_header (rva, ntheaders);
57         if (section_header == NULL) {
58                 return(NULL);
59         }
60         
61         delta = (guint32)(section_header->VirtualAddress -
62                           section_header->PointerToRawData);
63         
64         return((guint8 *)file_map + rva - delta);
65 }
66
67 static gpointer scan_resource_dir (WapiImageResourceDirectory *root,
68                                    WapiImageNTHeaders *nt_headers,
69                                    gpointer file_map,
70                                    WapiImageResourceDirectoryEntry *entry,
71                                    int level, guint32 res_id, guint32 lang_id,
72                                    guint32 *size)
73 {
74         gboolean is_string = entry->NameIsString;
75         gboolean is_dir = entry->DataIsDirectory;
76         guint32 name_offset = GUINT32_FROM_LE (entry->NameOffset);
77         guint32 dir_offset = GUINT32_FROM_LE (entry->OffsetToDirectory);
78         guint32 data_offset = GUINT32_FROM_LE (entry->OffsetToData);
79         
80         if (level == 0) {
81                 /* Normally holds a directory entry for each type of
82                  * resource
83                  */
84                 if ((is_string == FALSE &&
85                      name_offset != res_id) ||
86                     (is_string == TRUE)) {
87                         return(NULL);
88                 }
89         } else if (level == 1) {
90                 /* Normally holds a directory entry for each resource
91                  * item
92                  */
93         } else if (level == 2) {
94                 /* Normally holds a directory entry for each language
95                  */
96                 if ((is_string == FALSE &&
97                      name_offset != lang_id &&
98                      lang_id != 0) ||
99                     (is_string == TRUE)) {
100                         return(NULL);
101                 }
102         } else {
103                 g_assert_not_reached ();
104         }
105         
106         if (is_dir == TRUE) {
107                 WapiImageResourceDirectory *res_dir = (WapiImageResourceDirectory *)((guint8 *)root + dir_offset);
108                 WapiImageResourceDirectoryEntry *sub_entries = (WapiImageResourceDirectoryEntry *)(res_dir + 1);
109                 guint32 entries, i;
110                 
111                 entries = GUINT16_FROM_LE (res_dir->NumberOfNamedEntries) + GUINT16_FROM_LE (res_dir->NumberOfIdEntries);
112                 
113                 for (i = 0; i < entries; i++) {
114                         WapiImageResourceDirectoryEntry *sub_entry = &sub_entries[i];
115                         gpointer ret;
116                         
117                         ret = scan_resource_dir (root, nt_headers, file_map,
118                                                  sub_entry, level + 1, res_id,
119                                                  lang_id, size);
120                         if (ret != NULL) {
121                                 return(ret);
122                         }
123                 }
124                 
125                 return(NULL);
126         } else {
127                 WapiImageResourceDataEntry *data_entry = (WapiImageResourceDataEntry *)((guint8 *)root + data_offset);
128                 *size = GUINT32_FROM_LE (data_entry->Size);
129                 
130                 return(get_ptr_from_rva (data_entry->OffsetToData, nt_headers, file_map));
131         }
132 }
133
134 static gpointer find_pe_file_resources (gpointer file_map, guint32 map_size,
135                                         guint32 res_id, guint32 lang_id,
136                                         guint32 *size)
137 {
138         WapiImageDosHeader *dos_header;
139         WapiImageNTHeaders *nt_headers;
140         WapiImageResourceDirectory *resource_dir;
141         WapiImageResourceDirectoryEntry *resource_dir_entry;
142         guint32 resource_rva, entries, i;
143         gpointer ret = NULL;
144
145         dos_header = (WapiImageDosHeader *)file_map;
146         if (dos_header->e_magic != IMAGE_DOS_SIGNATURE) {
147 #ifdef DEBUG
148                 g_message ("%s: Bad dos signature 0x%x", __func__,
149                            dos_header->e_magic);
150 #endif
151
152                 SetLastError (ERROR_INVALID_DATA);
153                 return(NULL);
154         }
155         
156         if (map_size < sizeof(WapiImageNTHeaders) + GUINT32_FROM_LE (dos_header->e_lfanew)) {
157 #ifdef DEBUG
158                 g_message ("%s: File is too small: %d", __func__, map_size);
159 #endif
160
161                 SetLastError (ERROR_BAD_LENGTH);
162                 return(NULL);
163         }
164         
165         nt_headers = (WapiImageNTHeaders *)((guint8 *)file_map + GUINT32_FROM_LE (dos_header->e_lfanew));
166         if (nt_headers->Signature != IMAGE_NT_SIGNATURE) {
167 #ifdef DEBUG
168                 g_message ("%s: Bad NT signature 0x%x", __func__,
169                            nt_headers->Signature);
170 #endif
171
172                 SetLastError (ERROR_INVALID_DATA);
173                 return(NULL);
174         }
175         
176         if (nt_headers->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) {
177                 /* Do 64-bit stuff */
178                 resource_rva = GUINT32_FROM_LE (((WapiImageNTHeaders64 *)nt_headers)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].VirtualAddress);
179         } else {
180                 resource_rva = GUINT32_FROM_LE (nt_headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].VirtualAddress);
181         }
182
183         if (resource_rva == 0) {
184 #ifdef DEBUG
185                 g_message ("%s: No resources in file!", __func__);
186 #endif
187                 SetLastError (ERROR_INVALID_DATA);
188                 return(NULL);
189         }
190         
191         resource_dir = (WapiImageResourceDirectory *)get_ptr_from_rva (resource_rva, nt_headers, file_map);
192         if (resource_dir == NULL) {
193 #ifdef DEBUG
194                 g_message ("%s: Can't find resource directory", __func__);
195 #endif
196                 SetLastError (ERROR_INVALID_DATA);
197                 return(NULL);
198         }
199         
200         entries = GUINT16_FROM_LE (resource_dir->NumberOfNamedEntries) + GUINT16_FROM_LE (resource_dir->NumberOfIdEntries);
201         resource_dir_entry = (WapiImageResourceDirectoryEntry *)(resource_dir + 1);
202         
203         for (i = 0; i < entries; i++) {
204                 WapiImageResourceDirectoryEntry *direntry = &resource_dir_entry[i];
205                 ret = scan_resource_dir (resource_dir, nt_headers, file_map,
206                                          direntry, 0, res_id, lang_id, size);
207                 if (ret != NULL) {
208                         return(ret);
209                 }
210         }
211         
212         return(NULL);
213 }
214
215 static gpointer map_pe_file (gunichar2 *filename, guint32 *map_size)
216 {
217         gchar *filename_ext;
218         int fd;
219         struct stat statbuf;
220         gpointer file_map;
221         
222         /* According to the MSDN docs, a search path is applied to
223          * filename.  FIXME: implement this, for now just pass it
224          * straight to fopen
225          */
226
227         filename_ext = mono_unicode_to_external (filename);
228         if (filename_ext == NULL) {
229 #ifdef DEBUG
230                 g_message ("%s: unicode conversion returned NULL", __func__);
231 #endif
232
233                 SetLastError (ERROR_INVALID_NAME);
234                 return(NULL);
235         }
236         
237         fd = _wapi_open (filename_ext, O_RDONLY, 0);
238         if (fd == -1) {
239 #ifdef DEBUG
240                 g_message ("%s: Error opening file %s: %s", __func__,
241                            filename_ext, strerror (errno));
242 #endif
243
244                 SetLastError (_wapi_get_win32_file_error (errno));
245                 g_free (filename_ext);
246                 
247                 return(NULL);
248         }
249
250         if (fstat (fd, &statbuf) == -1) {
251 #ifdef DEBUG
252                 g_message ("%s: Error stat()ing file %s: %s", __func__,
253                            filename_ext, strerror (errno));
254 #endif
255
256                 SetLastError (_wapi_get_win32_file_error (errno));
257                 g_free (filename_ext);
258                 close (fd);
259                 return(NULL);
260         }
261         *map_size = statbuf.st_size;
262
263         /* Check basic file size */
264         if (statbuf.st_size < sizeof(WapiImageDosHeader)) {
265 #ifdef DEBUG
266                 g_message ("%s: File %s is too small: %ld", __func__,
267                            filename_ext, statbuf.st_size);
268 #endif
269
270                 SetLastError (ERROR_BAD_LENGTH);
271                 g_free (filename_ext);
272                 close (fd);
273                 return(NULL);
274         }
275         
276         file_map = mmap (NULL, statbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
277         if (file_map == MAP_FAILED) {
278 #ifdef DEBUG
279                 g_message ("%s: Error mmap()int file %s: %s", __func__,
280                            filename_ext, strerror (errno));
281 #endif
282
283                 SetLastError (_wapi_get_win32_file_error (errno));
284                 g_free (filename_ext);
285                 close (fd);
286                 return(NULL);
287         }
288
289         /* Don't need the fd any more */
290         close (fd);
291
292         return(file_map);
293 }
294
295 static void unmap_pe_file (gpointer file_map, guint32 map_size)
296 {
297         munmap (file_map, map_size);
298 }
299
300 guint32 GetFileVersionInfoSize (gunichar2 *filename, guint32 *handle)
301 {
302         gpointer file_map;
303         gpointer versioninfo;
304         guint32 map_size;
305         guint32 size;
306         
307         /* This value is unused, but set to zero */
308         *handle = 0;
309         
310         file_map = map_pe_file (filename, &map_size);
311         if (file_map == NULL) {
312                 return(0);
313         }
314         
315         versioninfo = find_pe_file_resources (file_map, map_size, RT_VERSION,
316                                               0, &size);
317         if (versioninfo == NULL) {
318                 /* Didn't find the resource, so set the return value
319                  * to 0
320                  */
321                 size = 0;
322         }
323
324         unmap_pe_file (file_map, map_size);
325
326         return(size);
327 }
328
329 gboolean GetFileVersionInfo (gunichar2 *filename, guint32 handle G_GNUC_UNUSED,
330                              guint32 len, gpointer data)
331 {
332         gpointer file_map;
333         gpointer versioninfo;
334         guint32 map_size;
335         guint32 size;
336         gboolean ret = FALSE;
337         
338         file_map = map_pe_file (filename, &map_size);
339         if (file_map == NULL) {
340                 return(FALSE);
341         }
342         
343         versioninfo = find_pe_file_resources (file_map, map_size, RT_VERSION,
344                                               0, &size);
345         if (versioninfo != NULL) {
346                 /* This could probably process the data so that
347                  * VerQueryValue() doesn't have to follow the data
348                  * blocks every time.  But hey, these functions aren't
349                  * likely to appear in many profiles.
350                  */
351                 memcpy (data, versioninfo, len < size?len:size);
352                 ret = TRUE;
353         }
354
355         unmap_pe_file (file_map, map_size);
356         
357         return(ret);
358 }
359
360 static guint32 unicode_chars (const gunichar2 *str)
361 {
362         guint32 len = 0;
363         
364         do {
365                 if (str[len] == '\0') {
366                         return(len);
367                 }
368                 len++;
369         } while(1);
370 }
371
372 static gboolean unicode_compare (const gunichar2 *str1, const gunichar2 *str2)
373 {
374         while (*str1 && *str2) {
375                 if (GUINT16_TO_LE (*str1) != GUINT16_TO_LE (*str2)) {
376                         return(FALSE);
377                 }
378                 ++str1;
379                 ++str2;
380         }
381         
382         return(*str1 == *str2);
383 }
384
385 /* compare a little-endian null-terminated utf16 string and a normal string.
386  * Can be used only for ascii or latin1 chars.
387  */
388 static gboolean unicode_string_equals (const gunichar2 *str1, const gchar *str2)
389 {
390         while (*str1 && *str2) {
391                 if (GUINT16_TO_LE (*str1) != *str2) {
392                         return(FALSE);
393                 }
394                 ++str1;
395                 ++str2;
396         }
397         
398         return(*str1 == *str2);
399 }
400
401 typedef struct 
402 {
403         guint16 data_len;
404         guint16 value_len;
405         guint16 type;
406         gunichar2 *key;
407 } version_data;
408
409 /* Returns a pointer to the value data, because there's no way to know
410  * how big that data is (value_len is set to zero for most blocks :-( )
411  */
412 static gconstpointer get_versioninfo_block (gconstpointer data,
413                                             version_data *block)
414 {
415         block->data_len = GUINT16_FROM_LE (*((guint16 *)data));
416         data = (char *)data + sizeof(guint16);
417         block->value_len = GUINT16_FROM_LE (*((guint16 *)data));
418         data = (char *)data + sizeof(guint16);
419         
420         /* No idea what the type is supposed to indicate */
421         block->type = GUINT16_FROM_LE (*((guint16 *)data));
422         data = (char *)data + sizeof(guint16);
423         block->key = ((gunichar2 *)data);
424         
425         /* Skip over the key (including the terminator) */
426         data = ((gunichar2 *)data) + (unicode_chars (block->key) + 1);
427         
428         /* align on a 32-bit boundary */
429         data = (gpointer)((char *)data + 3);
430         data = (gpointer)((char *)data - (GPOINTER_TO_INT (data) & 3));
431         
432         return(data);
433 }
434
435 static gconstpointer get_fixedfileinfo_block (gconstpointer data,
436                                               version_data *block)
437 {
438         gconstpointer data_ptr;
439         gint32 data_len; /* signed to guard against underflow */
440         WapiFixedFileInfo *ffi;
441
442         data_ptr = get_versioninfo_block (data, block);
443         data_len = block->data_len;
444                 
445         if (block->value_len != sizeof(WapiFixedFileInfo)) {
446 #ifdef DEBUG
447                 g_message ("%s: FIXEDFILEINFO size mismatch", __func__);
448 #endif
449                 return(NULL);
450         }
451
452         if (!unicode_string_equals (block->key, "VS_VERSION_INFO")) {
453 #ifdef DEBUG
454                 g_message ("%s: VS_VERSION_INFO mismatch", __func__);
455 #endif
456                 return(NULL);
457         }
458
459         ffi = ((WapiFixedFileInfo *)data_ptr);
460         if ((ffi->dwSignature != VS_FFI_SIGNATURE) ||
461             (ffi->dwStrucVersion != VS_FFI_STRUCVERSION)) {
462 #ifdef DEBUG
463                 g_message ("%s: FIXEDFILEINFO bad signature", __func__);
464 #endif
465                 return(NULL);
466         }
467
468         return(data_ptr);
469 }
470
471 static gconstpointer get_varfileinfo_block (gconstpointer data_ptr,
472                                             version_data *block)
473 {
474         /* data is pointing at a Var block
475          */
476         data_ptr = get_versioninfo_block (data_ptr, block);
477
478         return(data_ptr);
479 }
480
481 static gconstpointer get_string_block (gconstpointer data_ptr,
482                                        const gunichar2 *string_key,
483                                        gpointer *string_value,
484                                        guint32 *string_value_len,
485                                        version_data *block)
486 {
487         guint16 data_len = block->data_len;
488         guint16 string_len = 28; /* Length of the StringTable block */
489         
490         /* data_ptr is pointing at an array of one or more String blocks
491          * with total length (not including alignment padding) of
492          * data_len
493          */
494         while (string_len < data_len) {
495                 gunichar2 *value;
496                 
497                 /* align on a 32-bit boundary */
498                 data_ptr = (gpointer)((char *)data_ptr + 3);
499                 data_ptr = (gpointer)((char *)data_ptr - (GPOINTER_TO_INT (data_ptr) & 3));
500                 
501                 data_ptr = get_versioninfo_block (data_ptr, block);
502                 if (block->data_len == 0) {
503                         /* We must have hit padding, so give up
504                          * processing now
505                          */
506 #ifdef DEBUG
507                         g_message ("%s: Hit 0-length block, giving up",
508                                    __func__);
509 #endif
510                         return(NULL);
511                 }
512                 
513                 string_len = string_len + block->data_len;
514                 value = (gunichar2 *)data_ptr;
515                 
516                 if (string_key != NULL &&
517                     string_value != NULL &&
518                     string_value_len != NULL &&
519                     unicode_compare (string_key, block->key) == TRUE) {
520                         *string_value = (gpointer)data_ptr;
521                         *string_value_len = block->value_len;
522                 }
523                 
524                 /* Skip over the value */
525                 data_ptr = ((gunichar2 *)data_ptr) + block->value_len;
526         }
527         
528         return(data_ptr);
529 }
530
531 /* Returns a pointer to the byte following the Stringtable block, or
532  * NULL if the data read hits padding.  We can't recover from this
533  * because the data length does not include padding bytes, so it's not
534  * possible to just return the start position + length
535  */
536 static gconstpointer get_stringtable_block (gconstpointer data_ptr,
537                                             gunichar2 lang[8],
538                                             const gunichar2 *string_key,
539                                             gpointer *string_value,
540                                             guint32 *string_value_len,
541                                             version_data *block)
542 {
543         guint16 data_len = block->data_len;
544         guint16 string_len = 36; /* length of the StringFileInfo block */
545         
546         /* data_ptr is pointing at an array of StringTable blocks,
547          * with total length (not including alignment padding) of
548          * data_len
549          */
550
551         while(string_len < data_len) {
552                 /* align on a 32-bit boundary */
553                 data_ptr = (gpointer)((char *)data_ptr + 3);
554                 data_ptr = (gpointer)((char *)data_ptr - (GPOINTER_TO_INT (data_ptr) & 3));
555                 
556                 data_ptr = get_versioninfo_block (data_ptr, block);
557                 if (block->data_len == 0) {
558                         /* We must have hit padding, so give up
559                          * processing now
560                          */
561 #ifdef DEBUG
562                         g_message ("%s: Hit 0-length block, giving up",
563                                    __func__);
564 #endif
565                         return(NULL);
566                 }
567                 
568                 string_len = string_len + block->data_len;
569                 
570                 if (!memcmp (block->key, lang, 8 * sizeof(gunichar2))) {
571                         /* Got the one we're interested in */
572                         data_ptr = get_string_block (data_ptr, string_key,
573                                                      string_value,
574                                                      string_value_len, block);
575                 } else {
576                         data_ptr = get_string_block (data_ptr, NULL, NULL,
577                                                      NULL, block);
578                 }
579                 
580                 if (data_ptr == NULL) {
581                         /* Child block hit padding */
582 #ifdef DEBUG
583                         g_message ("%s: Child block hit 0-length block, giving up", __func__);
584 #endif
585                         return(NULL);
586                 }
587         }
588         
589         return(data_ptr);
590 }
591
592 gboolean VerQueryValue (gconstpointer datablock, const gunichar2 *subblock,
593                         gpointer *buffer, guint32 *len)
594 {
595         gchar *subblock_utf8;
596         gboolean ret = FALSE;
597         version_data block;
598         gconstpointer data_ptr;
599         gint32 data_len; /* signed to guard against underflow */
600         gboolean want_var = FALSE;
601         gboolean want_string = FALSE;
602         gunichar2 lang[8];
603         const gunichar2 *string_key = NULL;
604         gpointer string_value = NULL;
605         guint32 string_value_len = 0;
606         
607         subblock_utf8 = g_utf16_to_utf8 (subblock, -1, NULL, NULL, NULL);
608         if (subblock_utf8 == NULL) {
609                 return(FALSE);
610         }
611
612         if (!strcmp (subblock_utf8, "\\VarFileInfo\\Translation")) {
613                 want_var = TRUE;
614         } else if (!strncmp (subblock_utf8, "\\StringFileInfo\\", 16)) {
615                 want_string = TRUE;
616                 memcpy (lang, subblock + 16, 8 * sizeof(gunichar2));
617                 string_key = subblock + 25;
618         }
619         
620         if (!strcmp (subblock_utf8, "\\")) {
621                 data_ptr = get_fixedfileinfo_block (datablock, &block);
622                 if (data_ptr != NULL) {
623                         *buffer = (gpointer)data_ptr;
624                         *len = block.value_len;
625                 
626                         ret = TRUE;
627                 }
628         } else if (want_var || want_string) {
629                 data_ptr = get_fixedfileinfo_block (datablock, &block);
630                 if (data_ptr != NULL) {
631                         /* The FFI and header occupies the first 92
632                          * bytes
633                          */
634                         data_ptr = (char *)data_ptr + sizeof(WapiFixedFileInfo);
635                         data_len = block.data_len - 92;
636                         
637                         /* There now follow zero or one StringFileInfo
638                          * blocks and zero or one VarFileInfo blocks
639                          */
640                         while (data_len > 0) {
641                                 /* align on a 32-bit boundary */
642                                 data_ptr = (gpointer)((char *)data_ptr + 3);
643                                 data_ptr = (gpointer)((char *)data_ptr - (GPOINTER_TO_INT (data_ptr) & 3));
644                                 
645                                 data_ptr = get_versioninfo_block (data_ptr,
646                                                                   &block);
647                                 if (block.data_len == 0) {
648                                         /* We must have hit padding,
649                                          * so give up processing now
650                                          */
651 #ifdef DEBUG
652                                         g_message ("%s: Hit 0-length block, giving up", __func__);
653 #endif
654                                         goto done;
655                                 }
656                                 
657                                 data_len = data_len - block.data_len;
658                                 
659                                 if (unicode_string_equals (block.key, "VarFileInfo")) {
660                                         data_ptr = get_varfileinfo_block (data_ptr, &block);
661                                         if (want_var) {
662                                                 *buffer = (gpointer)data_ptr;
663                                                 *len = block.value_len;
664                                                 ret = TRUE;
665                                                 goto done;
666                                         } else {
667                                                 /* Skip over the Var block */
668                                                 data_ptr = ((guchar *)data_ptr) + block.value_len;
669                                         }
670                                 } else if (unicode_string_equals (block.key, "StringFileInfo")) {
671                                         data_ptr = get_stringtable_block (data_ptr, lang, string_key, &string_value, &string_value_len, &block);
672                                         if (want_string &&
673                                             string_value != NULL &&
674                                             string_value_len != 0) {
675                                                 *buffer = string_value;
676                                                 *len = string_value_len;
677                                                 ret = TRUE;
678                                                 goto done;
679                                         }
680                                 } else {
681                                         /* Bogus data */
682 #ifdef DEBUG
683                                         g_message ("%s: Not a valid VERSIONINFO child block", __func__);
684 #endif
685                                         goto done;
686                                 }
687                                 
688                                 if (data_ptr == NULL) {
689                                         /* Child block hit padding */
690 #ifdef DEBUG
691                                         g_message ("%s: Child block hit 0-length block, giving up", __func__);
692 #endif
693                                         goto done;
694                                 }
695                         }
696                 }
697         }
698
699   done:
700         g_free (subblock_utf8);
701         return(ret);
702 }
703
704 guint32 VerLanguageName (guint32 lang, gunichar2 *lang_out, guint32 lang_len)
705 {
706         return(0);
707 }