3 * COPYRIGHT: See COPYING in the top level directory
4 * PROJECT: ReactOS system libraries
5 * FILE: lib/ntdll/rtl/path.c
6 * PURPOSE: Path and current directory functions
11 /* INCLUDES ******************************************************************/
13 #include <ddk/ntddk.h>
14 #include <ntdll/rtl.h>
15 #include <ntos/minmax.h>
20 #include <ddk/obfuncs.h>
23 #include <ntdll/ntdll.h>
25 /* DEFINITONS and MACROS ******************************************************/
27 #define MAX_PFX_SIZE 16
29 #define IS_PATH_SEPARATOR(x) (((x)==L'\\')||((x)==L'/'))
32 /* GLOBALS ********************************************************************/
34 static const WCHAR DeviceRootW
[] = L
"\\\\.\\";
36 static const UNICODE_STRING _condev
= RTL_CONSTANT_STRING(L
"\\\\.\\CON");
38 static const UNICODE_STRING _lpt
= RTL_CONSTANT_STRING(L
"LPT");
40 static const UNICODE_STRING _com
= RTL_CONSTANT_STRING(L
"COM");
42 static const UNICODE_STRING _prn
= RTL_CONSTANT_STRING(L
"PRN");
44 static const UNICODE_STRING _aux
= RTL_CONSTANT_STRING(L
"AUX");
46 static const UNICODE_STRING _con
= RTL_CONSTANT_STRING(L
"CON");
48 static const UNICODE_STRING _nul
= RTL_CONSTANT_STRING(L
"NUL");
50 /* FUNCTIONS *****************************************************************/
56 ULONG STDCALL
RtlGetLongestNtPathLength (VOID
)
58 return (MAX_PATH
+ 9);
67 RtlDetermineDosPathNameType_U(PCWSTR Path
)
69 DPRINT("RtlDetermineDosPathNameType_U %S\n", Path
);
76 if (IS_PATH_SEPARATOR(Path
[0]))
78 if (!IS_PATH_SEPARATOR(Path
[1])) return ABSOLUTE_PATH
; /* \xxx */
79 if (Path
[2] != L
'.') return UNC_PATH
; /* \\xxx */
80 if (IS_PATH_SEPARATOR(Path
[3])) return DEVICE_PATH
; /* \\.\xxx */
81 if (Path
[3]) return UNC_PATH
; /* \\.xxxx */
83 return UNC_DOT_PATH
; /* \\. */
87 /* FIXME: the Wine version of this line reads:
88 * if (!Path[1] || Path[1] != L':') return RELATIVE_PATH
89 * Should we do this too?
92 if (Path
[1] != L
':') return RELATIVE_PATH
; /* xxx */
93 if (IS_PATH_SEPARATOR(Path
[2])) return ABSOLUTE_DRIVE_PATH
; /* x:\xxx */
95 return RELATIVE_DRIVE_PATH
; /* x:xxx */
100 /* returns 0 if name is not valid DOS device name, or DWORD with
101 * offset in bytes to DOS device name from beginning of buffer in high word
102 * and size in bytes of DOS device name in low word */
108 RtlIsDosDeviceName_U(PWSTR DeviceName
)
114 UNICODE_STRING DeviceNameU
;
116 if (DeviceName
== NULL
)
121 while (DeviceName
[Length
])
126 Type
= RtlDetermineDosPathNameType_U(DeviceName
);
134 DeviceNameU
.Length
= DeviceNameU
.MaximumLength
= Length
* sizeof(WCHAR
);
135 DeviceNameU
.Buffer
= DeviceName
;
137 RtlEqualUnicodeString(&DeviceNameU
, (PUNICODE_STRING
)&_condev
, TRUE
))
142 /* name can end with ':' */
143 if (Length
&& DeviceName
[Length
- 1 ] == L
':')
148 /* there can be spaces or points at the end of name */
149 wc
= DeviceName
+ Length
- 1;
150 while (Length
&& (*wc
== L
'.' || *wc
== L
' '))
156 /* let's find a beginning of name */
157 wc
= DeviceName
+ Length
- 1;
158 while (wc
> DeviceName
&& !IS_PATH_SEPARATOR(*(wc
- 1)))
162 Offset
= wc
- DeviceName
;
164 DeviceNameU
.Length
= DeviceNameU
.MaximumLength
= 3 * sizeof(WCHAR
);
165 DeviceNameU
.Buffer
= wc
;
167 /* check for LPTx or COMx */
168 if (Length
== 4 && wc
[3] >= L
'0' && wc
[3] <= L
'9')
175 if (RtlEqualUnicodeString(&DeviceNameU
, (PUNICODE_STRING
)&_lpt
, TRUE
) ||
176 RtlEqualUnicodeString(&DeviceNameU
, (PUNICODE_STRING
)&_com
, TRUE
))
178 return ((Offset
* 2) << 16 ) | 8;
183 /* check for PRN,AUX,NUL or CON */
185 (RtlEqualUnicodeString(&DeviceNameU
, (PUNICODE_STRING
)&_prn
, TRUE
) ||
186 RtlEqualUnicodeString(&DeviceNameU
, (PUNICODE_STRING
)&_aux
, TRUE
) ||
187 RtlEqualUnicodeString(&DeviceNameU
, (PUNICODE_STRING
)&_nul
, TRUE
) ||
188 RtlEqualUnicodeString(&DeviceNameU
, (PUNICODE_STRING
)&_con
, TRUE
)))
190 return ((Offset
* 2) << 16) | 6;
201 RtlGetCurrentDirectory_U(ULONG MaximumLength
,
207 DPRINT ("RtlGetCurrentDirectory %lu %p\n", MaximumLength
, Buffer
);
211 cd
= (PCURDIR
)&(NtCurrentPeb ()->ProcessParameters
->CurrentDirectoryName
);
212 Length
= cd
->DosPath
.Length
/ sizeof(WCHAR
);
213 if (cd
->DosPath
.Buffer
[Length
- 1] == L
'\\' &&
214 cd
->DosPath
.Buffer
[Length
- 2] != L
':')
217 DPRINT ("cd->DosPath.Buffer %S Length %d\n",
218 cd
->DosPath
.Buffer
, Length
);
220 if (MaximumLength
/ sizeof(WCHAR
) > Length
)
224 Length
* sizeof(WCHAR
));
232 RtlReleasePebLock ();
234 DPRINT ("CurrentDirectory %S\n", Buffer
);
236 return (Length
* sizeof(WCHAR
));
244 RtlSetCurrentDirectory_U(PUNICODE_STRING name
)
247 UNICODE_STRING envvar
;
248 OBJECT_ATTRIBUTES Attr
;
249 IO_STATUS_BLOCK iosb
;
253 HANDLE handle
= NULL
;
256 PFILE_NAME_INFORMATION filenameinfo
;
257 ULONG backslashcount
= 0;
261 DPRINT ("RtlSetCurrentDirectory %wZ\n", name
);
263 RtlAcquirePebLock ();
264 cd
= (PCURDIR
)&NtCurrentPeb ()->ProcessParameters
->CurrentDirectoryName
;
266 size
= cd
->DosPath
.MaximumLength
;
267 buf
= RtlAllocateHeap (RtlGetProcessHeap(),
272 RtlReleasePebLock ();
273 return STATUS_NO_MEMORY
;
276 size
= RtlGetFullPathName_U (name
->Buffer
, size
, buf
, 0);
279 RtlFreeHeap (RtlGetProcessHeap (),
282 RtlReleasePebLock ();
283 return STATUS_OBJECT_NAME_INVALID
;
286 if (!RtlDosPathNameToNtPathName_U (buf
, &full
, 0, 0))
288 RtlFreeHeap (RtlGetProcessHeap (),
291 RtlFreeHeap (RtlGetProcessHeap (),
294 RtlReleasePebLock ();
295 return STATUS_OBJECT_NAME_INVALID
;
298 InitializeObjectAttributes (&Attr
,
300 OBJ_CASE_INSENSITIVE
| OBJ_INHERIT
,
304 Status
= NtOpenFile (&handle
,
305 SYNCHRONIZE
| FILE_TRAVERSE
,
308 FILE_SHARE_READ
| FILE_SHARE_WRITE
,
309 FILE_DIRECTORY_FILE
| FILE_SYNCHRONOUS_IO_NONALERT
);
310 if (!NT_SUCCESS(Status
))
312 RtlFreeHeap (RtlGetProcessHeap (),
315 RtlFreeHeap (RtlGetProcessHeap (),
318 RtlReleasePebLock ();
322 filenameinfo
= RtlAllocateHeap(RtlGetProcessHeap(),
324 MAX_PATH
*sizeof(WCHAR
)+sizeof(ULONG
));
326 Status
= NtQueryInformationFile(handle
,
329 MAX_PATH
*sizeof(WCHAR
)+sizeof(ULONG
),
330 FileNameInformation
);
331 if (!NT_SUCCESS(Status
))
333 RtlFreeHeap(RtlGetProcessHeap(),
336 RtlFreeHeap(RtlGetProcessHeap(),
339 RtlFreeHeap(RtlGetProcessHeap(),
346 /* If it's just "\", we need special handling */
347 if (filenameinfo
->FileNameLength
> sizeof(WCHAR
))
349 wcs
= buf
+ size
/ sizeof(WCHAR
) - 1;
354 size
-= sizeof(WCHAR
);
358 Index
< filenameinfo
->FileNameLength
/ sizeof(WCHAR
);
361 if (filenameinfo
->FileName
[Index
] == '\\') backslashcount
++;
364 DPRINT("%d \n",backslashcount
);
365 for (;backslashcount
;wcs
--)
367 if (*wcs
=='\\') backslashcount
--;
371 RtlCopyMemory(wcs
, filenameinfo
->FileName
, filenameinfo
->FileNameLength
);
372 wcs
[filenameinfo
->FileNameLength
/ sizeof(WCHAR
)] = 0;
374 size
= (wcs
- buf
) * sizeof(WCHAR
) + filenameinfo
->FileNameLength
;
377 RtlFreeHeap (RtlGetProcessHeap (),
381 /* append backslash if missing */
382 wcs
= buf
+ size
/ sizeof(WCHAR
) - 1;
387 size
+= sizeof(WCHAR
);
390 memmove(cd
->DosPath
.Buffer
,
392 size
+ sizeof(WCHAR
));
393 cd
->DosPath
.Length
= size
;
399 if (cd
->DosPath
.Buffer
[1]==':')
401 envvar
.Length
= 2 * swprintf (var
, L
"=%c:", cd
->DosPath
.Buffer
[0]);
402 envvar
.MaximumLength
= 8;
405 RtlSetEnvironmentVariable(NULL
,
410 RtlFreeHeap (RtlGetProcessHeap (),
414 RtlFreeHeap (RtlGetProcessHeap (),
420 return STATUS_SUCCESS
;
425 /******************************************************************
428 * Helper for RtlGetFullPathName_U.
429 * 1) Convert slashes into backslashes
430 * 2) Get rid of duplicate backslashes
431 * 3) Get rid of . and .. components in the path.
433 static inline void collapse_path( WCHAR
*path
, UINT mark
)
437 /* convert every / into a \ */
438 for (p
= path
; *p
; p
++) if (*p
== '/') *p
= '\\';
440 /* collapse duplicate backslashes */
441 next
= path
+ max( 1, mark
);
442 for (p
= next
; *p
; p
++) if (*p
!= '\\' || next
[-1] != '\\') *next
++ = *p
;
452 case '\\': /* .\ component */
454 memmove( p
, next
, (wcslen(next
) + 1) * sizeof(WCHAR
) );
456 case 0: /* final . */
457 if (p
> path
+ mark
) p
--;
461 if (p
[2] == '\\') /* ..\ component */
467 while (p
> path
+ mark
&& p
[-1] != '\\') p
--;
469 memmove( p
, next
, (wcslen(next
) + 1) * sizeof(WCHAR
) );
472 else if (!p
[2]) /* final .. */
477 while (p
> path
+ mark
&& p
[-1] != '\\') p
--;
478 if (p
> path
+ mark
) p
--;
486 /* skip to the next component */
487 while (*p
&& *p
!= '\\') p
++;
491 /* remove trailing spaces and dots (yes, Windows really does that, don't ask) */
492 while (p
> path
+ mark
&& (p
[-1] == ' ' || p
[-1] == '.')) p
--;
498 /******************************************************************
501 * Skip the \\share\dir\ part of a file name. Helper for RtlGetFullPathName_U.
503 static const WCHAR
*skip_unc_prefix( const WCHAR
*ptr
)
506 while (*ptr
&& !IS_PATH_SEPARATOR(*ptr
)) ptr
++; /* share name */
507 while (IS_PATH_SEPARATOR(*ptr
)) ptr
++;
508 while (*ptr
&& !IS_PATH_SEPARATOR(*ptr
)) ptr
++; /* dir name */
509 while (IS_PATH_SEPARATOR(*ptr
)) ptr
++;
514 /******************************************************************
515 * get_full_path_helper
517 * Helper for RtlGetFullPathName_U
518 * Note: name and buffer are allowed to point to the same memory spot
520 static ULONG
get_full_path_helper(
525 ULONG reqsize
= 0, mark
= 0, dep
= 0, deplen
;
526 DOS_PATHNAME_TYPE type
;
527 LPWSTR ins_str
= NULL
;
529 const UNICODE_STRING
* cd
;
532 /* return error if name only consists of spaces */
533 for (ptr
= name
; *ptr
; ptr
++) if (*ptr
!= ' ') break;
538 cd
= &((PCURDIR
)&NtCurrentTeb()->Peb
->ProcessParameters
->CurrentDirectoryName
)->DosPath
;
540 switch (type
= RtlDetermineDosPathNameType_U(name
))
542 case UNC_PATH
: /* \\foo */
543 ptr
= skip_unc_prefix( name
);
547 case DEVICE_PATH
: /* \\.\foo */
551 case ABSOLUTE_DRIVE_PATH
: /* c:\foo */
552 reqsize
= sizeof(WCHAR
);
553 tmp
[0] = towupper(name
[0]);
559 case RELATIVE_DRIVE_PATH
: /* c:foo */
561 if (towupper(name
[0]) != towupper(cd
->Buffer
[0]) || cd
->Buffer
[1] != ':')
563 UNICODE_STRING var
, val
;
569 var
.Length
= 3 * sizeof(WCHAR
);
570 var
.MaximumLength
= 4 * sizeof(WCHAR
);
573 val
.MaximumLength
= size
;
574 val
.Buffer
= RtlAllocateHeap(RtlGetProcessHeap(), 0, size
);
576 switch (RtlQueryEnvironmentVariable_U(NULL
, &var
, &val
))
579 /* FIXME: Win2k seems to check that the environment variable actually points
580 * to an existing directory. If not, root of the drive is used
581 * (this seems also to be the only spot in RtlGetFullPathName that the
582 * existence of a part of a path is checked)
585 case STATUS_BUFFER_TOO_SMALL
:
586 reqsize
= val
.Length
+ sizeof(WCHAR
); /* append trailing '\\' */
587 val
.Buffer
[val
.Length
/ sizeof(WCHAR
)] = '\\';
588 ins_str
= val
.Buffer
;
590 case STATUS_VARIABLE_NOT_FOUND
:
591 reqsize
= 3 * sizeof(WCHAR
);
598 DPRINT1("Unsupported status code\n");
606 case RELATIVE_PATH
: /* foo */
607 reqsize
= cd
->Length
;
608 ins_str
= cd
->Buffer
;
609 if (cd
->Buffer
[1] != ':')
611 ptr
= skip_unc_prefix( cd
->Buffer
);
612 mark
= ptr
- cd
->Buffer
;
617 case ABSOLUTE_PATH
: /* \xxx */
619 if (name
[0] == '/') /* may be a Unix path */
621 const WCHAR
*ptr
= name
;
622 int drive
= find_drive_root( &ptr
);
625 reqsize
= 3 * sizeof(WCHAR
);
626 tmp
[0] = 'A' + drive
;
636 if (cd
->Buffer
[1] == ':')
638 reqsize
= 2 * sizeof(WCHAR
);
639 tmp
[0] = cd
->Buffer
[0];
646 ptr
= skip_unc_prefix( cd
->Buffer
);
647 reqsize
= (ptr
- cd
->Buffer
) * sizeof(WCHAR
);
648 mark
= reqsize
/ sizeof(WCHAR
);
649 ins_str
= cd
->Buffer
;
653 case UNC_DOT_PATH
: /* \\. */
654 reqsize
= 4 * sizeof(WCHAR
);
669 deplen
= wcslen(name
+ dep
) * sizeof(WCHAR
);
670 if (reqsize
+ deplen
+ sizeof(WCHAR
) > size
)
672 /* not enough space, return need size (including terminating '\0') */
673 reqsize
+= deplen
+ sizeof(WCHAR
);
677 memmove(buffer
+ reqsize
/ sizeof(WCHAR
), name
+ dep
, deplen
+ sizeof(WCHAR
));
678 if (reqsize
) memcpy(buffer
, ins_str
, reqsize
);
681 if (ins_str
&& ins_str
!= tmp
&& ins_str
!= cd
->Buffer
)
682 RtlFreeHeap(RtlGetProcessHeap(), 0, ins_str
);
684 collapse_path( buffer
, mark
);
685 reqsize
= wcslen(buffer
) * sizeof(WCHAR
);
693 /******************************************************************
694 * RtlGetFullPathName_U (NTDLL.@)
696 * Returns the number of bytes written to buffer (not including the
697 * terminating NULL) if the function succeeds, or the required number of bytes
698 * (including the terminating NULL) if the buffer is too small.
700 * file_part will point to the filename part inside buffer (except if we use
701 * DOS device name, in which case file_in_buf is NULL)
705 DWORD STDCALL
RtlGetFullPathName_U(
715 DPRINT("RtlGetFullPathName_U(%S %lu %p %p)\n", name
, size
, buffer
, file_part
);
717 if (!name
|| !*name
) return 0;
719 if (file_part
) *file_part
= NULL
;
721 /* check for DOS device name */
722 dosdev
= RtlIsDosDeviceName_U((WCHAR
*)name
);
725 DWORD offset
= HIWORD(dosdev
) / sizeof(WCHAR
); /* get it in WCHARs, not bytes */
726 DWORD sz
= LOWORD(dosdev
); /* in bytes */
728 if (8 + sz
+ 2 > size
) return sz
+ 10;
729 wcscpy(buffer
, DeviceRootW
);
730 memmove(buffer
+ 4, name
+ offset
, sz
);
731 buffer
[4 + sz
/ sizeof(WCHAR
)] = '\0';
732 /* file_part isn't set in this case */
736 reqsize
= get_full_path_helper(name
, buffer
, size
);
737 if (!reqsize
) return 0;
740 LPWSTR tmp
= RtlAllocateHeap(RtlGetProcessHeap(), 0, reqsize
);
741 reqsize
= get_full_path_helper(name
, tmp
, reqsize
);
742 if (reqsize
> size
) /* it may have worked the second time */
744 RtlFreeHeap(RtlGetProcessHeap(), 0, tmp
);
745 return reqsize
+ sizeof(WCHAR
);
747 memcpy( buffer
, tmp
, reqsize
+ sizeof(WCHAR
) );
748 RtlFreeHeap(RtlGetProcessHeap(), 0, tmp
);
752 if (file_part
&& (ptr
= wcsrchr(buffer
, '\\')) != NULL
&& ptr
>= buffer
+ 2 && *++ptr
)
762 RtlDosPathNameToNtPathName_U(PWSTR dosname
,
763 PUNICODE_STRING ntname
,
774 WCHAR fullname
[MAX_PATH
+ 1];
778 RtlAcquirePebLock ();
780 RtlInitUnicodeString (&us
, dosname
);
784 /* check for "\\?\" - allows to use very long filenames ( up to 32k ) */
785 if (Buffer
[0] == L
'\\' && Buffer
[1] == L
'\\' &&
786 Buffer
[2] == L
'?' && Buffer
[3] == L
'\\')
788 // if( f_77F68606( &us, ntname, shortname, nah ) )
790 // RtlReleasePebLock ();
794 RtlReleasePebLock ();
799 Buffer
= RtlAllocateHeap (RtlGetProcessHeap (),
801 sizeof( fullname
) + MAX_PFX_SIZE
);
804 RtlReleasePebLock ();
808 Size
= RtlGetFullPathName_U (dosname
,
812 if (Size
== 0 || Size
> MAX_PATH
* sizeof(WCHAR
))
814 RtlFreeHeap (RtlGetProcessHeap (),
817 RtlReleasePebLock ();
823 memcpy (Buffer
, L
"\\??\\", 4 * sizeof(WCHAR
));
826 Type
= RtlDetermineDosPathNameType_U (fullname
);
830 memcpy (Buffer
+ tmpLength
, L
"UNC\\", 4 * sizeof(WCHAR
));
839 Length
= wcslen(fullname
+ Offset
);
840 memcpy (Buffer
+ tmpLength
, fullname
+ Offset
, (Length
+ 1) * sizeof(WCHAR
));
843 /* set NT filename */
844 ntname
->Length
= Length
* sizeof(WCHAR
);
845 ntname
->MaximumLength
= sizeof(fullname
) + MAX_PFX_SIZE
;
846 ntname
->Buffer
= Buffer
;
848 /* set pointer to file part if possible */
849 if (FilePart
&& *FilePart
)
850 *FilePart
= Buffer
+ Length
- wcslen (*FilePart
);
852 /* Set name and handle structure if possible */
855 memset (nah
, 0, sizeof(CURDIR
));
856 cd
= (PCURDIR
)&(NtCurrentPeb ()->ProcessParameters
->CurrentDirectoryName
);
857 if (Type
== 5 && cd
->Handle
)
859 RtlInitUnicodeString(&us
, fullname
);
860 if (RtlEqualUnicodeString(&us
, &cd
->DosPath
, TRUE
))
862 Length
= ((cd
->DosPath
.Length
/ sizeof(WCHAR
)) - Offset
) + ((Type
== 1) ? 8 : 4);
863 nah
->DosPath
.Buffer
= Buffer
+ Length
;
864 nah
->DosPath
.Length
= ntname
->Length
- (Length
* sizeof(WCHAR
));
865 nah
->DosPath
.MaximumLength
= nah
->DosPath
.Length
;
866 nah
->Handle
= cd
->Handle
;
896 Type
= RtlDetermineDosPathNameType_U (name
);
900 Length
= wcslen (sp
);
901 Length
+= wcslen (name
);
902 if (wcschr (name
, L
'.'))
905 Length
+= wcslen (ext
);
907 full_name
= (WCHAR
*)RtlAllocateHeap (RtlGetProcessHeap (),
909 (Length
+ 1) * sizeof(WCHAR
));
911 if (full_name
!= NULL
)
917 while (*path
&& *path
!= L
';')
921 if (wcs
!= full_name
&& *(wcs
- 1) != L
'\\')
926 if (RtlDoesFileExists_U (full_name
))
928 Length
= RtlGetFullPathName_U (full_name
,
936 RtlFreeHeap (RtlGetProcessHeap (),
941 else if (RtlDoesFileExists_U (name
))
943 Length
= RtlGetFullPathName_U (name
,
957 RtlDoesFileExists_U(IN PWSTR FileName
)
959 UNICODE_STRING NtFileName
;
960 OBJECT_ATTRIBUTES Attr
;
965 /* only used by replacement code */
967 IO_STATUS_BLOCK StatusBlock
;
969 if (!RtlDosPathNameToNtPathName_U (FileName
,
975 /* don't forget to free it! */
976 Buffer
= NtFileName
.Buffer
;
978 if (CurDir
.DosPath
.Length
)
979 NtFileName
= CurDir
.DosPath
;
983 InitializeObjectAttributes (&Attr
,
985 OBJ_CASE_INSENSITIVE
,
989 /* FIXME: not implemented yet */
990 // Status = NtQueryAttributesFile (&Attr, NULL);
992 /* REPLACEMENT start */
993 Status
= NtOpenFile (&FileHandle
,
998 FILE_SYNCHRONOUS_IO_NONALERT
);
999 if (NT_SUCCESS(Status
))
1000 NtClose (FileHandle
);
1001 /* REPLACEMENT end */
1003 RtlFreeHeap (RtlGetProcessHeap (),
1007 if (NT_SUCCESS(Status
) ||
1008 Status
== STATUS_SHARING_VIOLATION
||
1009 Status
== STATUS_ACCESS_DENIED
)
1016 RtlpEnsureBufferSize(ULONG Unknown1
, ULONG Unknown2
, ULONG Unknown3
)
1018 DPRINT1("RtlpEnsureBufferSize: stub\n");
1019 return STATUS_NOT_IMPLEMENTED
;
1023 RtlNtPathNameToDosPathName(ULONG Unknown1
, ULONG Unknown2
, ULONG Unknown3
, ULONG Unknown4
)
1025 DPRINT1("RtlNtPathNameToDosPathName: stub\n");
1026 return STATUS_NOT_IMPLEMENTED
;