2 * CMD - Wine-compatible command line interface - Directory functions.
4 * Copyright (C) 1999 D A Pickles
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2.1 of the License, or (at your option) any later version.
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this library; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
23 * On entry, global variables quals, param1, param2 contain
24 * the qualifiers (uppercased and concatenated) and parameters entered, with
25 * environment-variable and batch parameter substitution already done.
28 #define WIN32_LEAN_AND_MEAN
31 #include "wine/debug.h"
33 WINE_DEFAULT_DEBUG_CHANNEL(cmd
);
35 int WCMD_dir_sort (const void *a
, const void *b
);
36 void WCMD_list_directory (char *path
, int level
);
37 char * WCMD_filesize64 (ULONGLONG free
);
38 char * WCMD_strrev (char *buff
);
39 static void WCMD_getfileowner(char *filename
, char *owner
, int ownerlen
);
42 extern char quals
[MAX_PATH
], param1
[MAX_PATH
], param2
[MAX_PATH
];
43 extern DWORD errorlevel
;
45 typedef enum _DISPLAYTIME
52 typedef enum _DISPLAYORDER
60 struct directory_stack
62 struct directory_stack
*next
;
66 static int file_total
, dir_total
, recurse
, wide
, bare
, max_width
, lower
;
67 static int shortname
, usernames
;
68 static ULONGLONG byte_total
;
69 static DISPLAYTIME dirTime
;
70 static DISPLAYORDER dirOrder
;
71 static BOOL orderReverse
, orderGroupDirs
, orderGroupDirsReverse
, orderByCol
;
72 static BOOL separator
;
73 static ULONG showattrs
, attrsbits
;
75 /*****************************************************************************
78 * List a file directory.
82 void WCMD_directory (void) {
84 char path
[MAX_PATH
], drive
[8];
85 int status
, paged_mode
;
86 ULARGE_INTEGER avail
, total
, free
;
87 CONSOLE_SCREEN_BUFFER_INFO consoleInfo
;
89 char string
[MAXSTRING
];
93 /* Prefill Quals with (uppercased) DIRCMD env var */
94 if (GetEnvironmentVariable ("DIRCMD", string
, sizeof(string
))) {
96 while ( (*p
= toupper(*p
)) ) ++p
;
98 strcpy(quals
, string
);
102 file_total
= dir_total
= 0;
104 /* Initialize all flags to their defaults as if no DIRCMD or quals */
116 orderReverse
= FALSE
;
117 orderGroupDirs
= FALSE
;
118 orderGroupDirsReverse
= FALSE
;
122 /* Handle args - Loop through so right most is the effective one */
123 /* Note: /- appears to be a negate rather than an off, eg. dir
124 /-W is wide, or dir /w /-w /-w is also wide */
126 while (*p
&& (*p
=='/' || *p
==' ')) {
128 if (*p
++==' ') continue; /* Skip / and blanks introduced through DIRCMD */
135 WINE_TRACE("Processing arg '%c' (in %s)\n", *p
, quals
);
137 case 'P': if (negate
) paged_mode
= !paged_mode
;
138 else paged_mode
= TRUE
;
140 case 'S': if (negate
) recurse
= !recurse
;
143 case 'W': if (negate
) wide
= !wide
;
146 case 'B': if (negate
) bare
= !bare
;
149 case 'L': if (negate
) lower
= !lower
;
152 case 'X': if (negate
) shortname
= !shortname
;
153 else shortname
= TRUE
;
155 case 'Q': if (negate
) usernames
= !usernames
;
156 else usernames
= TRUE
;
158 case 'D': if (negate
) orderByCol
= !orderByCol
;
159 else orderByCol
= TRUE
;
161 case 'C': if (negate
) separator
= !separator
;
162 else separator
= TRUE
;
165 if (*p
==':') p
++; /* Skip optional : */
167 if (*p
== 'A') dirTime
= Access
;
168 else if (*p
== 'C') dirTime
= Creation
;
169 else if (*p
== 'W') dirTime
= Written
;
171 /* Support /T and /T: with no parms, default to written */
172 else if (*p
== 0x00 || *p
== '/') {
174 p
= p
- 1; /* So when step on, move to '/' */
176 SetLastError(ERROR_INVALID_PARAMETER
);
183 if (*p
==':') p
++; /* Skip optional : */
184 while (*p
&& *p
!= '/') {
185 WINE_TRACE("Processing subparm '%c' (in %s)\n", *p
, quals
);
187 case 'N': dirOrder
= Name
; break;
188 case 'E': dirOrder
= Extension
; break;
189 case 'S': dirOrder
= Size
; break;
190 case 'D': dirOrder
= Date
; break;
191 case '-': if (*(p
+1)=='G') orderGroupDirsReverse
=TRUE
;
192 else orderReverse
= TRUE
;
194 case 'G': orderGroupDirs
= TRUE
; break;
196 SetLastError(ERROR_INVALID_PARAMETER
);
203 p
= p
- 1; /* So when step on, move to '/' */
208 if (*p
==':') p
++; /* Skip optional : */
209 while (*p
&& *p
!= '/') {
210 BOOL anegate
= FALSE
;
213 /* Note /A: - options are 'offs' not toggles */
219 WINE_TRACE("Processing subparm '%c' (in %s)\n", *p
, quals
);
221 case 'D': mask
= FILE_ATTRIBUTE_DIRECTORY
; break;
222 case 'H': mask
= FILE_ATTRIBUTE_HIDDEN
; break;
223 case 'S': mask
= FILE_ATTRIBUTE_SYSTEM
; break;
224 case 'R': mask
= FILE_ATTRIBUTE_READONLY
; break;
225 case 'A': mask
= FILE_ATTRIBUTE_ARCHIVE
; break;
227 SetLastError(ERROR_INVALID_PARAMETER
);
233 /* Keep running list of bits we care about */
236 /* Mask shows what MUST be in the bits we care about */
237 if (anegate
) showattrs
= showattrs
& ~mask
;
238 else showattrs
|= mask
;
242 p
= p
- 1; /* So when step on, move to '/' */
243 WINE_TRACE("Result: showattrs %x, bits %x\n", showattrs
, attrsbits
);
246 SetLastError(ERROR_INVALID_PARAMETER
);
254 /* Handle conflicting args and initialization */
255 if (bare
|| shortname
) wide
= FALSE
;
256 if (bare
) shortname
= FALSE
;
257 if (wide
) usernames
= FALSE
;
258 if (orderByCol
) wide
= TRUE
;
261 if (GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE
), &consoleInfo
))
262 max_width
= consoleInfo
.dwSize
.X
;
267 WCMD_enter_paged_mode();
270 if (param1
[0] == '\0') strcpy (param1
, ".");
271 status
= GetFullPathName (param1
, sizeof(path
), path
, NULL
);
274 if (paged_mode
) WCMD_leave_paged_mode();
278 lstrcpyn (drive
, path
, 3);
281 status
= WCMD_volume (0, drive
);
283 if (paged_mode
) WCMD_leave_paged_mode();
289 WCMD_list_directory (path
, 0);
290 lstrcpyn (drive
, path
, 4);
291 GetDiskFreeSpaceEx (drive
, &avail
, &total
, &free
);
293 if (errorlevel
==0 && !bare
) {
295 WCMD_output ("\n Total files listed:\n%8d files%25s bytes\n",
296 file_total
, WCMD_filesize64 (byte_total
));
297 WCMD_output ("%8d directories %18s bytes free\n\n",
298 dir_total
, WCMD_filesize64 (free
.QuadPart
));
300 WCMD_output (" %18s bytes free\n\n", WCMD_filesize64 (free
.QuadPart
));
303 if (paged_mode
) WCMD_leave_paged_mode();
306 /*****************************************************************************
307 * WCMD_list_directory
309 * List a single file directory. This function (and those below it) can be called
310 * recursively when the /S switch is used.
312 * FIXME: Entries sorted by name only. Should we support DIRCMD??
313 * FIXME: Assumes 24-line display for the /P qualifier.
314 * FIXME: Other command qualifiers not supported.
315 * FIXME: DIR /S FILENAME fails if at least one matching file is not found in the top level.
318 void WCMD_list_directory (char *search_path
, int level
) {
320 char string
[1024], datestring
[32], timestring
[32];
322 char real_path
[MAX_PATH
];
327 int status
, dir_count
, file_count
, entry_count
, i
, widest
, cur_width
, tmp_width
;
328 int numCols
, numRows
;
330 ULARGE_INTEGER byte_count
, file_size
;
335 byte_count
.QuadPart
= 0;
340 * If the path supplied does not include a wildcard, and the endpoint of the
341 * path references a directory, we need to list the *contents* of that
342 * directory not the directory file itself. However, only do this on first
343 * entry and not subsequent recursion
347 (strchr(search_path
, '*') == NULL
) && (strchr(search_path
, '%') == NULL
)) {
348 status
= GetFileAttributes (search_path
);
349 if ((status
!= INVALID_FILE_ATTRIBUTES
) && (status
& FILE_ATTRIBUTE_DIRECTORY
)) {
350 if (search_path
[strlen(search_path
)-1] == '\\') {
351 strcat (search_path
, "*");
354 strcat (search_path
, "\\*");
359 /* Work out the actual current directory name */
360 p
= strrchr (search_path
, '\\');
361 memset(real_path
, 0x00, sizeof(real_path
));
362 lstrcpyn (real_path
, search_path
, (p
-search_path
+2));
364 /* Load all files into an in memory structure */
365 fd
= HeapAlloc(GetProcessHeap(),0,sizeof(WIN32_FIND_DATA
));
366 WINE_TRACE("Looking for matches to '%s'\n", search_path
);
367 hff
= FindFirstFile (search_path
, fd
);
368 if (hff
== INVALID_HANDLE_VALUE
) {
372 /* Skip any which are filtered out by attribute */
373 if (((fd
+entry_count
)->dwFileAttributes
& attrsbits
) != showattrs
) continue;
377 /* Keep running track of longest filename for wide output */
378 if (wide
|| orderByCol
) {
379 int tmpLen
= strlen((fd
+(entry_count
-1))->cFileName
) + 3;
380 if ((fd
+(entry_count
-1))->dwFileAttributes
& FILE_ATTRIBUTE_DIRECTORY
) tmpLen
= tmpLen
+ 2;
381 if (tmpLen
> widest
) widest
= tmpLen
;
384 fd
= HeapReAlloc(GetProcessHeap(),0,fd
,(entry_count
+1)*sizeof(WIN32_FIND_DATA
));
387 WCMD_output ("Memory Allocation Error");
391 } while (FindNextFile(hff
, (fd
+entry_count
)) != 0);
395 /* Output the results */
397 if (level
!= 0 && (entry_count
> 0)) WCMD_output ("\n");
398 if ((entry_count
> 0) || (!recurse
&& level
== 0)) WCMD_output ("Directory of %s\n\n", real_path
);
401 /* Handle case where everything is filtered out */
402 if (entry_count
> 0) {
404 /* Sort the list of files */
405 qsort (fd
, entry_count
, sizeof(WIN32_FIND_DATA
), WCMD_dir_sort
);
407 /* Work out the number of columns */
408 WINE_TRACE("%d entries, maxwidth=%d, widest=%d\n", entry_count
, max_width
, widest
);
409 if (wide
|| orderByCol
) {
410 numCols
= max(1, (int)max_width
/ widest
);
411 numRows
= entry_count
/ numCols
;
412 if (entry_count
% numCols
) numRows
++;
415 numRows
= entry_count
;
417 WINE_TRACE("cols=%d, rows=%d\n", numCols
, numRows
);
419 for (rows
=0; rows
<numRows
; rows
++) {
420 BOOL addNewLine
= TRUE
;
421 for (cols
=0; cols
<numCols
; cols
++) {
424 /* Work out the index of the entry being pointed to */
426 i
= (cols
* numRows
) + rows
;
427 if (i
>= entry_count
) continue;
429 i
= (rows
* numCols
) + cols
;
430 if (i
>= entry_count
) continue;
433 /* /L convers all names to lower case */
435 char *p
= (fd
+i
)->cFileName
;
436 while ( (*p
= tolower(*p
)) ) ++p
;
439 /* /Q gets file ownership information */
441 p
= strrchr (search_path
, '\\');
442 lstrcpyn (string
, search_path
, (p
-search_path
+2));
443 lstrcat (string
, (fd
+i
)->cFileName
);
444 WCMD_getfileowner(string
, username
, sizeof(username
));
447 if (dirTime
== Written
) {
448 FileTimeToLocalFileTime (&(fd
+i
)->ftLastWriteTime
, &ft
);
449 } else if (dirTime
== Access
) {
450 FileTimeToLocalFileTime (&(fd
+i
)->ftLastAccessTime
, &ft
);
452 FileTimeToLocalFileTime (&(fd
+i
)->ftCreationTime
, &ft
);
454 FileTimeToSystemTime (&ft
, &st
);
455 GetDateFormat (0, DATE_SHORTDATE
, &st
, NULL
, datestring
,
457 GetTimeFormat (0, TIME_NOSECONDS
, &st
,
458 NULL
, timestring
, sizeof(timestring
));
462 tmp_width
= cur_width
;
463 if ((fd
+i
)->dwFileAttributes
& FILE_ATTRIBUTE_DIRECTORY
) {
464 WCMD_output ("[%s]", (fd
+i
)->cFileName
);
466 tmp_width
= tmp_width
+ strlen((fd
+i
)->cFileName
) + 2;
468 WCMD_output ("%s", (fd
+i
)->cFileName
);
469 tmp_width
= tmp_width
+ strlen((fd
+i
)->cFileName
) ;
471 file_size
.u
.LowPart
= (fd
+i
)->nFileSizeLow
;
472 file_size
.u
.HighPart
= (fd
+i
)->nFileSizeHigh
;
473 byte_count
.QuadPart
+= file_size
.QuadPart
;
475 cur_width
= cur_width
+ widest
;
477 if ((cur_width
+ widest
) > max_width
) {
480 WCMD_output ("%*.s", (tmp_width
- cur_width
) ,"");
483 } else if ((fd
+i
)->dwFileAttributes
& FILE_ATTRIBUTE_DIRECTORY
) {
487 WCMD_output ("%10s %8s <DIR> ", datestring
, timestring
);
488 if (shortname
) WCMD_output ("%-13s", (fd
+i
)->cAlternateFileName
);
489 if (usernames
) WCMD_output ("%-23s", username
);
490 WCMD_output("%s",(fd
+i
)->cFileName
);
492 if (!((strcmp((fd
+i
)->cFileName
, ".") == 0) ||
493 (strcmp((fd
+i
)->cFileName
, "..") == 0))) {
494 WCMD_output ("%s%s", recurse
?real_path
:"", (fd
+i
)->cFileName
);
502 file_size
.u
.LowPart
= (fd
+i
)->nFileSizeLow
;
503 file_size
.u
.HighPart
= (fd
+i
)->nFileSizeHigh
;
504 byte_count
.QuadPart
+= file_size
.QuadPart
;
506 WCMD_output ("%10s %8s %10s ", datestring
, timestring
,
507 WCMD_filesize64(file_size
.QuadPart
));
508 if (shortname
) WCMD_output ("%-13s", (fd
+i
)->cAlternateFileName
);
509 if (usernames
) WCMD_output ("%-23s", username
);
510 WCMD_output("%s",(fd
+i
)->cFileName
);
512 WCMD_output ("%s%s", recurse
?real_path
:"", (fd
+i
)->cFileName
);
516 if (addNewLine
) WCMD_output ("\n");
521 if (file_count
== 1) {
522 WCMD_output (" 1 file %25s bytes\n", WCMD_filesize64 (byte_count
.QuadPart
));
525 WCMD_output ("%8d files %24s bytes\n", file_count
, WCMD_filesize64 (byte_count
.QuadPart
));
528 byte_total
= byte_total
+ byte_count
.QuadPart
;
529 file_total
= file_total
+ file_count
;
530 dir_total
= dir_total
+ dir_count
;
532 if (!bare
&& !recurse
) {
533 if (dir_count
== 1) WCMD_output ("%8d directory ", 1);
534 else WCMD_output ("%8d directories", dir_count
);
537 HeapFree(GetProcessHeap(),0,fd
);
539 /* When recursing, look in all subdirectories for matches */
541 struct directory_stack
*dirStack
= NULL
;
542 struct directory_stack
*lastEntry
= NULL
;
543 WIN32_FIND_DATA finddata
;
545 /* Build path to search */
546 strcpy(string
, search_path
);
547 p
= strrchr (string
, '\\');
550 WINE_TRACE("Recursive, looking for '%s'\n", string
);
551 hff
= FindFirstFile (string
, &finddata
);
552 if (hff
!= INVALID_HANDLE_VALUE
) {
554 if ((finddata
.dwFileAttributes
& FILE_ATTRIBUTE_DIRECTORY
) &&
555 (strcmp(finddata
.cFileName
, "..") != 0) &&
556 (strcmp(finddata
.cFileName
, ".") != 0)) {
558 struct directory_stack
*thisDir
;
560 /* Work out search parameter in sub dir */
561 p
= strrchr (search_path
, '\\');
562 lstrcpyn (string
, search_path
, (p
-search_path
+2));
563 string
[(p
-search_path
+2)] = 0x00;
564 lstrcat (string
, finddata
.cFileName
);
566 WINE_TRACE("Recursive, Adding to search list '%s'\n", string
);
568 /* Allocate memory, add to list */
569 thisDir
= (struct directory_stack
*) HeapAlloc(GetProcessHeap(),0,sizeof(struct directory_stack
));
570 if (dirStack
== NULL
) dirStack
= thisDir
;
571 if (lastEntry
!= NULL
) lastEntry
->next
= thisDir
;
573 thisDir
->next
= NULL
;
574 thisDir
->name
= HeapAlloc(GetProcessHeap(),0,(strlen(string
)+1));
575 strcpy(thisDir
->name
, string
);
577 } while (FindNextFile(hff
, &finddata
) != 0);
580 while (dirStack
!= NULL
) {
581 struct directory_stack
*thisDir
= dirStack
;
582 dirStack
= thisDir
->next
;
584 WCMD_list_directory (thisDir
->name
, 1);
585 HeapFree(GetProcessHeap(),0,thisDir
->name
);
586 HeapFree(GetProcessHeap(),0,thisDir
);
591 /* Handle case where everything is filtered out */
592 if ((file_total
+ dir_total
== 0) && (level
== 0)) {
593 SetLastError (ERROR_FILE_NOT_FOUND
);
601 /*****************************************************************************
604 * Convert a 64-bit number into a character string, with commas every three digits.
605 * Result is returned in a static string overwritten with each call.
606 * FIXME: There must be a better algorithm!
609 char * WCMD_filesize64 (ULONGLONG n
) {
614 static char buff
[32];
619 if (separator
&& ((++i
)%3 == 1)) *p
++ = ',';
630 /*****************************************************************************
633 * Reverse a character string in-place (strrev() is not available under unixen :-( ).
636 char * WCMD_strrev (char *buff
) {
642 for (i
=0; i
<r
/2; i
++) {
644 buff
[i
] = buff
[r
-i
-1];
651 /*****************************************************************************
654 * Sort based on the /O options supplied on the command line
656 int WCMD_dir_sort (const void *a
, const void *b
)
658 WIN32_FIND_DATA
*filea
= (WIN32_FIND_DATA
*)a
;
659 WIN32_FIND_DATA
*fileb
= (WIN32_FIND_DATA
*)b
;
662 /* If /OG or /O-G supplied, dirs go at the top or bottom, ignoring the
663 requested sort order for the directory components */
664 if (orderGroupDirs
&&
665 ((filea
->dwFileAttributes
& FILE_ATTRIBUTE_DIRECTORY
) ||
666 (fileb
->dwFileAttributes
& FILE_ATTRIBUTE_DIRECTORY
)))
668 BOOL aDir
= filea
->dwFileAttributes
& FILE_ATTRIBUTE_DIRECTORY
;
669 if (aDir
) result
= -1;
671 if (orderGroupDirsReverse
) result
= -result
;
675 } else if (dirOrder
== Name
) {
676 result
= lstrcmpi(filea
->cFileName
, fileb
->cFileName
);
679 } else if (dirOrder
== Size
) {
680 ULONG64 sizea
= (((ULONG64
)filea
->nFileSizeHigh
) << 32) + filea
->nFileSizeLow
;
681 ULONG64 sizeb
= (((ULONG64
)fileb
->nFileSizeHigh
) << 32) + fileb
->nFileSizeLow
;
682 if( sizea
< sizeb
) result
= -1;
683 else if( sizea
== sizeb
) result
= 0;
686 /* Order by Date: (Takes into account which date (/T option) */
687 } else if (dirOrder
== Date
) {
690 ULONG64 timea
, timeb
;
692 if (dirTime
== Written
) {
693 ft
= &filea
->ftLastWriteTime
;
694 timea
= (((ULONG64
)ft
->dwHighDateTime
) << 32) + ft
->dwLowDateTime
;
695 ft
= &fileb
->ftLastWriteTime
;
696 timeb
= (((ULONG64
)ft
->dwHighDateTime
) << 32) + ft
->dwLowDateTime
;
697 } else if (dirTime
== Access
) {
698 ft
= &filea
->ftLastAccessTime
;
699 timea
= (((ULONG64
)ft
->dwHighDateTime
) << 32) + ft
->dwLowDateTime
;
700 ft
= &fileb
->ftLastAccessTime
;
701 timeb
= (((ULONG64
)ft
->dwHighDateTime
) << 32) + ft
->dwLowDateTime
;
703 ft
= &filea
->ftCreationTime
;
704 timea
= (((ULONG64
)ft
->dwHighDateTime
) << 32) + ft
->dwLowDateTime
;
705 ft
= &fileb
->ftCreationTime
;
706 timeb
= (((ULONG64
)ft
->dwHighDateTime
) << 32) + ft
->dwLowDateTime
;
708 if( timea
< timeb
) result
= -1;
709 else if( timea
== timeb
) result
= 0;
712 /* Order by Extension: (Takes into account which date (/T option) */
713 } else if (dirOrder
== Extension
) {
716 char fname
[MAX_PATH
];
720 /* Split into components */
721 WCMD_splitpath(filea
->cFileName
, drive
, dir
, fname
, extA
);
722 WCMD_splitpath(fileb
->cFileName
, drive
, dir
, fname
, extB
);
723 result
= lstrcmpi(extA
, extB
);
726 if (orderReverse
) result
= -result
;
730 /*****************************************************************************
733 * Reverse a character string in-place (strrev() is not available under unixen :-( ).
735 void WCMD_getfileowner(char *filename
, char *owner
, int ownerlen
) {
737 ULONG sizeNeeded
= 0;
739 char name
[MAXSTRING
];
740 char domain
[MAXSTRING
];
742 /* In case of error, return empty string */
745 /* Find out how much space we need for the owner security descritpor */
746 GetFileSecurity(filename
, OWNER_SECURITY_INFORMATION
, 0, 0, &sizeNeeded
);
749 if(rc
== ERROR_INSUFFICIENT_BUFFER
&& sizeNeeded
> 0) {
753 BOOL defaulted
= FALSE
;
754 ULONG nameLen
= MAXSTRING
;
755 ULONG domainLen
= MAXSTRING
;
756 SID_NAME_USE nameuse
;
758 secBuffer
= (LPBYTE
) HeapAlloc(GetProcessHeap(),0,sizeNeeded
* sizeof(BYTE
));
759 if(!secBuffer
) return;
761 /* Get the owners security descriptor */
762 if(!GetFileSecurity(filename
, OWNER_SECURITY_INFORMATION
, secBuffer
,
763 sizeNeeded
, &sizeNeeded
)) {
764 HeapFree(GetProcessHeap(),0,secBuffer
);
768 /* Get the SID from the SD */
769 if(!GetSecurityDescriptorOwner(secBuffer
, &pSID
, &defaulted
)) {
770 HeapFree(GetProcessHeap(),0,secBuffer
);
774 /* Convert to a username */
775 if (LookupAccountSid(NULL
, pSID
, name
, &nameLen
, domain
, &domainLen
, &nameuse
)) {
776 snprintf(owner
, ownerlen
, "%s%c%s", domain
, '\\', name
);
778 HeapFree(GetProcessHeap(),0,secBuffer
);