[fastfat_new]
[reactos.git] / reactos / drivers / filesystems / fastfat_new / fcb.c
1 /*
2 * PROJECT: ReactOS FAT file system driver
3 * LICENSE: GPL - See COPYING in the top level directory
4 * FILE: drivers/filesystems/fastfat/fcb.c
5 * PURPOSE: FCB manipulation routines.
6 * PROGRAMMERS: Aleksey Bragin <aleksey@reactos.org>
7 */
8
9 /* INCLUDES *****************************************************************/
10
11 #define NDEBUG
12 #include "fastfat.h"
13
14 #define TAG_FILENAME 'fBnF'
15
16 /* FUNCTIONS ****************************************************************/
17
18 FSRTL_COMPARISON_RESULT
19 NTAPI
20 FatiCompareNames(PSTRING NameA,
21 PSTRING NameB)
22 {
23 ULONG MinimumLen, i;
24
25 /* Calc the minimum length */
26 MinimumLen = NameA->Length < NameB->Length ? NameA->Length :
27 NameB->Length;
28
29 /* Actually compare them */
30 i = (ULONG)RtlCompareMemory( NameA->Buffer, NameB->Buffer, MinimumLen );
31
32 if (i < MinimumLen)
33 {
34 /* Compare prefixes */
35 if (NameA->Buffer[i] < NameB->Buffer[i])
36 return LessThan;
37 else
38 return GreaterThan;
39 }
40
41 /* Final comparison */
42 if (NameA->Length < NameB->Length)
43 return LessThan;
44 else if (NameA->Length > NameB->Length)
45 return GreaterThan;
46 else
47 return EqualTo;
48 }
49
50 PFCB
51 NTAPI
52 FatFindFcb(PFAT_IRP_CONTEXT IrpContext,
53 PRTL_SPLAY_LINKS *RootNode,
54 PSTRING AnsiName,
55 PBOOLEAN IsDosName)
56 {
57 PFCB_NAME_LINK Node;
58 FSRTL_COMPARISON_RESULT Comparison;
59 PRTL_SPLAY_LINKS Links;
60
61 Links = *RootNode;
62
63 while (Links)
64 {
65 Node = CONTAINING_RECORD(Links, FCB_NAME_LINK, Links);
66
67 /* Compare the prefix */
68 if (*(PUCHAR)Node->Name.Ansi.Buffer != *(PUCHAR)AnsiName->Buffer)
69 {
70 if (*(PUCHAR)Node->Name.Ansi.Buffer < *(PUCHAR)AnsiName->Buffer)
71 Comparison = LessThan;
72 else
73 Comparison = GreaterThan;
74 }
75 else
76 {
77 /* Perform real comparison */
78 Comparison = FatiCompareNames(&Node->Name.Ansi, AnsiName);
79 }
80
81 /* Do they match? */
82 if (Comparison == GreaterThan)
83 {
84 /* No, it's greater, go to the left child */
85 Links = RtlLeftChild(Links);
86 }
87 else if (Comparison == LessThan)
88 {
89 /* No, it's lesser, go to the right child */
90 Links = RtlRightChild(Links);
91 }
92 else
93 {
94 /* Exact match, balance the tree */
95 *RootNode = RtlSplay(Links);
96
97 /* Save type of the name, if needed */
98 if (IsDosName)
99 *IsDosName = Node->IsDosName;
100
101 /* Return the found fcb */
102 return Node->Fcb;
103 }
104 }
105
106 /* Nothing found */
107 return NULL;
108 }
109
110 PFCB
111 NTAPI
112 FatCreateFcb(IN PFAT_IRP_CONTEXT IrpContext,
113 IN PVCB Vcb,
114 IN PFCB ParentDcb,
115 IN FF_FILE *FileHandle)
116 {
117 PFCB Fcb;
118
119 /* Allocate it and zero it */
120 Fcb = ExAllocatePoolWithTag(NonPagedPool, sizeof(FCB), TAG_FCB);
121 RtlZeroMemory(Fcb, sizeof(FCB));
122
123 /* Set node types */
124 Fcb->Header.NodeTypeCode = FAT_NTC_FCB;
125 Fcb->Header.NodeByteSize = sizeof(FCB);
126 Fcb->Condition = FcbGood;
127
128 /* Initialize resources */
129 Fcb->Header.Resource = &Fcb->Resource;
130 ExInitializeResourceLite(Fcb->Header.Resource);
131
132 Fcb->Header.PagingIoResource = &Fcb->PagingIoResource;
133 ExInitializeResourceLite(Fcb->Header.PagingIoResource);
134
135 /* Initialize mutexes */
136 Fcb->Header.FastMutex = &Fcb->HeaderMutex;
137 ExInitializeFastMutex(&Fcb->HeaderMutex);
138 FsRtlSetupAdvancedHeader(&Fcb->Header, &Fcb->HeaderMutex);
139
140 /* Insert into parent's DCB list */
141 InsertTailList(&ParentDcb->Dcb.ParentDcbList, &Fcb->ParentDcbLinks);
142
143 /* Set backlinks */
144 Fcb->ParentFcb = ParentDcb;
145 Fcb->Vcb = Vcb;
146
147 /* Set file handle and sizes */
148 Fcb->Header.FileSize.LowPart = FileHandle->Filesize;
149 Fcb->Header.ValidDataLength.LowPart = FileHandle->Filesize;
150 Fcb->FatHandle = FileHandle;
151
152 /* Set names */
153 FatSetFcbNames(IrpContext, Fcb);
154
155 return Fcb;
156 }
157
158 PCCB
159 NTAPI
160 FatCreateCcb()
161 {
162 PCCB Ccb;
163
164 /* Allocate the CCB and zero it */
165 Ccb = ExAllocatePoolWithTag(NonPagedPool, sizeof(CCB), TAG_CCB);
166 RtlZeroMemory(Ccb, sizeof(CCB));
167
168 /* Set mandatory header */
169 Ccb->NodeTypeCode = FAT_NTC_FCB;
170 Ccb->NodeByteSize = sizeof(CCB);
171
172 return Ccb;
173 }
174
175 VOID
176 NTAPI
177 FatGetFcbUnicodeName(IN PFAT_IRP_CONTEXT IrpContext,
178 IN PFCB Fcb,
179 OUT PUNICODE_STRING LongName)
180 {
181 FF_DIRENT DirEnt;
182 FF_ERROR Err;
183 OEM_STRING ShortName;
184 CHAR ShortNameBuf[13];
185 UCHAR EntryBuffer[32];
186 UCHAR NumLFNs;
187 OEM_STRING LongNameOem;
188 NTSTATUS Status;
189
190 /* We support only files now, not directories */
191 if (Fcb->Header.NodeTypeCode != FAT_NTC_FCB)
192 {
193 UNIMPLEMENTED;
194 ASSERT(FALSE);
195 }
196
197 /* Get the dir entry */
198 Err = FF_GetEntry(Fcb->Vcb->Ioman,
199 Fcb->FatHandle->DirEntry,
200 Fcb->FatHandle->DirCluster,
201 &DirEnt);
202
203 if (Err != FF_ERR_NONE)
204 {
205 DPRINT1("Error %d getting dirent of a file\n", Err);
206 return;
207 }
208
209 /* Read the dirent to fetch the raw short name */
210 FF_FetchEntry(Fcb->Vcb->Ioman,
211 Fcb->FatHandle->DirCluster,
212 Fcb->FatHandle->DirEntry,
213 EntryBuffer);
214 NumLFNs = (UCHAR)(EntryBuffer[0] & ~0x40);
215
216 /* Check if we only have a short name.
217 Convert it to unicode and return if that's the case */
218 if (NumLFNs == 0)
219 {
220 /* Initialize short name string */
221 ShortName.Buffer = ShortNameBuf;
222 ShortName.Length = 0;
223 ShortName.MaximumLength = 12;
224
225 /* Convert raw short name to a proper string */
226 Fati8dot3ToString((PCHAR)EntryBuffer, FALSE, &ShortName);
227
228 /* Convert it to unicode */
229 Status = RtlOemStringToCountedUnicodeString(LongName,
230 &ShortName,
231 FALSE);
232
233 /* Ensure conversion was successful */
234 ASSERT(Status == STATUS_SUCCESS);
235
236 /* Exit */
237 return;
238 }
239
240 /* Convert LFN from OEM to unicode and return */
241 LongNameOem.Buffer = DirEnt.FileName;
242 LongNameOem.MaximumLength = FF_MAX_FILENAME;
243 LongNameOem.Length = strlen(DirEnt.FileName);
244
245 /* Convert it to unicode */
246 Status = RtlOemStringToUnicodeString(LongName, &LongNameOem, FALSE);
247
248 /* Ensure conversion was successful */
249 ASSERT(Status == STATUS_SUCCESS);
250 }
251
252
253 VOID
254 NTAPI
255 FatSetFullNameInFcb(PFCB Fcb,
256 PUNICODE_STRING Name)
257 {
258 PUNICODE_STRING ParentName;
259
260 /* Make sure this FCB's name wasn't already set */
261 ASSERT(Fcb->FullFileName.Buffer == NULL);
262
263 /* First of all, check exact case name */
264 if (Fcb->ExactCaseLongName.Buffer)
265 {
266 ASSERT(Fcb->ExactCaseLongName.Length != 0);
267
268 /* Use exact case name */
269 Name = &Fcb->ExactCaseLongName;
270 }
271
272 /* Treat root dir different */
273 if (FatNodeType(Fcb->ParentFcb) == FAT_NTC_ROOT_DCB)
274 {
275 /* Set lengths */
276 Fcb->FullFileName.MaximumLength = sizeof(WCHAR) + Name->Length;
277 Fcb->FullFileName.Length = Fcb->FullFileName.MaximumLength;
278
279 /* Allocate a buffer */
280 Fcb->FullFileName.Buffer = FsRtlAllocatePoolWithTag(PagedPool,
281 Fcb->FullFileName.Length,
282 TAG_FILENAME);
283
284 /* Prefix with a backslash */
285 Fcb->FullFileName.Buffer[0] = L'\\';
286
287 /* Copy the name here */
288 RtlCopyMemory(&Fcb->FullFileName.Buffer[1],
289 &Name->Buffer[0],
290 Name->Length );
291 }
292 else
293 {
294 ParentName = &Fcb->ParentFcb->FullFileName;
295
296 /* Check if parent's name is set */
297 if (!ParentName->Buffer)
298 return;
299
300 /* Set lengths */
301 Fcb->FullFileName.MaximumLength =
302 ParentName->Length + sizeof(WCHAR) + Name->Length;
303 Fcb->FullFileName.Length = Fcb->FullFileName.MaximumLength;
304
305 /* Allocate a buffer */
306 Fcb->FullFileName.Buffer = FsRtlAllocatePoolWithTag(PagedPool,
307 Fcb->FullFileName.Length,
308 TAG_FILENAME );
309
310 /* Copy parent's name here */
311 RtlCopyMemory(&Fcb->FullFileName.Buffer[0],
312 &ParentName->Buffer[0],
313 ParentName->Length );
314
315 /* Add a backslash */
316 Fcb->FullFileName.Buffer[ParentName->Length / sizeof(WCHAR)] = L'\\';
317
318 /* Copy given name here */
319 RtlCopyMemory(&Fcb->FullFileName.Buffer[(ParentName->Length / sizeof(WCHAR)) + 1],
320 &Name->Buffer[0],
321 Name->Length );
322 }
323 }
324
325 VOID
326 NTAPI
327 FatSetFullFileNameInFcb(IN PFAT_IRP_CONTEXT IrpContext,
328 IN PFCB Fcb)
329 {
330 UNICODE_STRING LongName;
331 PFCB CurFcb = Fcb;
332 PFCB StopFcb;
333 PWCHAR TmpBuffer;
334 ULONG PathLength = 0;
335
336 /* Do nothing if it's already set */
337 if (Fcb->FullFileName.Buffer) return;
338
339 /* Allocate a temporary buffer */
340 LongName.Length = 0;
341 LongName.MaximumLength = FF_MAX_FILENAME * sizeof(WCHAR);
342 LongName.Buffer =
343 FsRtlAllocatePoolWithTag(PagedPool,
344 FF_MAX_FILENAME * sizeof(WCHAR),
345 TAG_FILENAME);
346
347 /* Go through all parents to calculate needed length */
348 while (CurFcb != Fcb->Vcb->RootDcb)
349 {
350 /* Does current FCB have FullFileName set? */
351 if (CurFcb != Fcb &&
352 CurFcb->FullFileName.Buffer)
353 {
354 /* Yes, just use it! */
355 PathLength += CurFcb->FullFileName.Length;
356
357 Fcb->FullFileName.Buffer =
358 FsRtlAllocatePoolWithTag(PagedPool,
359 PathLength,
360 TAG_FILENAME);
361
362 RtlCopyMemory(Fcb->FullFileName.Buffer,
363 CurFcb->FullFileName.Buffer,
364 CurFcb->FullFileName.Length);
365
366 break;
367 }
368
369 /* Sum up length of a current item */
370 PathLength += CurFcb->FileNameLength + sizeof(WCHAR);
371
372 /* Go to the parent */
373 CurFcb = CurFcb->ParentFcb;
374 }
375
376 /* Allocate FullFileName if it wasn't already allocated above */
377 if (!Fcb->FullFileName.Buffer)
378 {
379 Fcb->FullFileName.Buffer =
380 FsRtlAllocatePoolWithTag(PagedPool,
381 PathLength,
382 TAG_FILENAME);
383 }
384
385 StopFcb = CurFcb;
386
387 CurFcb = Fcb;
388 TmpBuffer = Fcb->FullFileName.Buffer + PathLength / sizeof(WCHAR);
389
390 /* Set lengths */
391 Fcb->FullFileName.Length = PathLength;
392 Fcb->FullFileName.MaximumLength = PathLength;
393
394 while (CurFcb != StopFcb)
395 {
396 /* Get its unicode name */
397 FatGetFcbUnicodeName(IrpContext,
398 CurFcb,
399 &LongName);
400
401 /* Copy it */
402 TmpBuffer -= LongName.Length / sizeof(WCHAR);
403 RtlCopyMemory(TmpBuffer, LongName.Buffer, LongName.Length);
404
405 /* Append with a backslash */
406 TmpBuffer -= 1;
407 *TmpBuffer = L'\\';
408
409 /* Go to the parent */
410 CurFcb = CurFcb->ParentFcb;
411 }
412
413 /* Free the temp buffer */
414 ExFreePool(LongName.Buffer);
415 }
416
417
418 VOID
419 NTAPI
420 FatSetFcbNames(IN PFAT_IRP_CONTEXT IrpContext,
421 IN PFCB Fcb)
422 {
423 FF_DIRENT DirEnt;
424 FF_ERROR Err;
425 POEM_STRING ShortName;
426 CHAR ShortNameRaw[13];
427 UCHAR EntryBuffer[32];
428 UCHAR NumLFNs;
429 PUNICODE_STRING UnicodeName;
430 OEM_STRING LongNameOem;
431 NTSTATUS Status;
432
433 /* Get the dir entry */
434 Err = FF_GetEntry(Fcb->Vcb->Ioman,
435 Fcb->FatHandle->DirEntry,
436 Fcb->FatHandle->DirCluster,
437 &DirEnt);
438
439 if (Err != FF_ERR_NONE)
440 {
441 DPRINT1("Error %d getting dirent of a file\n", Err);
442 return;
443 }
444
445 /* Read the dirent to fetch the raw short name */
446 FF_FetchEntry(Fcb->Vcb->Ioman,
447 Fcb->FatHandle->DirCluster,
448 Fcb->FatHandle->DirEntry,
449 EntryBuffer);
450 NumLFNs = (UCHAR)(EntryBuffer[0] & ~0x40);
451 RtlCopyMemory(ShortNameRaw, EntryBuffer, 11);
452
453 /* Initialize short name string */
454 ShortName = &Fcb->ShortName.Name.Ansi;
455 ShortName->Buffer = Fcb->ShortNameBuffer;
456 ShortName->Length = 0;
457 ShortName->MaximumLength = sizeof(Fcb->ShortNameBuffer);
458
459 /* Convert raw short name to a proper string */
460 Fati8dot3ToString(ShortNameRaw, FALSE, ShortName);
461
462 /* Add the short name link */
463 FatInsertName(IrpContext, &Fcb->ParentFcb->Dcb.SplayLinksAnsi, &Fcb->ShortName);
464 Fcb->ShortName.Fcb = Fcb;
465
466 /* Get the long file name (if any) */
467 if (NumLFNs > 0)
468 {
469 /* Prepare the oem string */
470 LongNameOem.Buffer = DirEnt.FileName;
471 LongNameOem.MaximumLength = FF_MAX_FILENAME;
472 LongNameOem.Length = strlen(DirEnt.FileName);
473
474 /* Prepare the unicode string */
475 UnicodeName = &Fcb->LongName.Name.String;
476 UnicodeName->Length = (LongNameOem.Length + 1) * sizeof(WCHAR);
477 UnicodeName->MaximumLength = UnicodeName->Length;
478 UnicodeName->Buffer = FsRtlAllocatePool(PagedPool, UnicodeName->Length);
479
480 /* Convert it to unicode */
481 Status = RtlOemStringToUnicodeString(UnicodeName, &LongNameOem, FALSE);
482 if (!NT_SUCCESS(Status))
483 {
484 ASSERT(FALSE);
485 }
486
487 /* Set its length */
488 Fcb->FileNameLength = UnicodeName->Length;
489
490 /* Save case-preserved copy */
491 Fcb->ExactCaseLongName.Length = UnicodeName->Length;
492 Fcb->ExactCaseLongName.MaximumLength = UnicodeName->Length;
493 Fcb->ExactCaseLongName.Buffer =
494 FsRtlAllocatePoolWithTag(PagedPool, UnicodeName->Length, TAG_FILENAME);
495
496 RtlCopyMemory(Fcb->ExactCaseLongName.Buffer,
497 UnicodeName->Buffer,
498 UnicodeName->Length);
499
500 /* Perform a trick which is done by MS's FASTFAT driver to monocase
501 the filename */
502 RtlDowncaseUnicodeString(UnicodeName, UnicodeName, FALSE);
503 RtlUpcaseUnicodeString(UnicodeName, UnicodeName, FALSE);
504
505 DPRINT("Converted long name: %wZ\n", UnicodeName);
506
507 /* Add the long unicode name link */
508 FatInsertName(IrpContext, &Fcb->ParentFcb->Dcb.SplayLinksUnicode, &Fcb->LongName);
509 Fcb->LongName.Fcb = Fcb;
510
511 /* Indicate that this FCB has a unicode long name */
512 SetFlag(Fcb->State, FCB_STATE_HAS_UNICODE_NAME);
513 }
514 else
515 {
516 /* No LFN, set exact case name to 0 length */
517 Fcb->ExactCaseLongName.Length = 0;
518 Fcb->ExactCaseLongName.MaximumLength = 0;
519
520 /* Set the length based on the short name */
521 Fcb->FileNameLength = RtlOemStringToCountedUnicodeSize(ShortName);
522 }
523
524 /* Mark the fact that names were added to splay trees*/
525 SetFlag(Fcb->State, FCB_STATE_HAS_NAMES);
526 }
527
528 VOID
529 NTAPI
530 Fati8dot3ToString(IN PCHAR FileName,
531 IN BOOLEAN DownCase,
532 OUT POEM_STRING OutString)
533 {
534 ULONG BaseLen, ExtLen;
535 CHAR *cString = OutString->Buffer;
536 ULONG i;
537
538 /* Calc base and ext lens */
539 for (BaseLen = 8; BaseLen > 0; BaseLen--)
540 {
541 if (FileName[BaseLen - 1] != ' ') break;
542 }
543
544 for (ExtLen = 3; ExtLen > 0; ExtLen--)
545 {
546 if (FileName[8 + ExtLen - 1] != ' ') break;
547 }
548
549 /* Process base name */
550 if (BaseLen)
551 {
552 RtlCopyMemory(cString, FileName, BaseLen);
553
554 /* Substitute the e5 thing */
555 if (cString[0] == 0x05) cString[0] = 0xe5;
556
557 /* Downcase if asked to */
558 if (DownCase)
559 {
560 /* Do it manually */
561 for (i = 0; i < BaseLen; i++)
562 {
563 if (cString[i] >= 'A' &&
564 cString[i] <= 'Z')
565 {
566 /* Lowercase it */
567 cString[i] += 'a' - 'A';
568 }
569
570 }
571 }
572 }
573
574 /* Process extension */
575 if (ExtLen)
576 {
577 /* Add the dot */
578 cString[BaseLen] = '.';
579 BaseLen++;
580
581 /* Copy the extension */
582 for (i = 0; i < ExtLen; i++)
583 {
584 cString[BaseLen + i] = FileName[8 + i];
585 }
586
587 /* Lowercase the extension if asked to */
588 if (DownCase)
589 {
590 /* Do it manually */
591 for (i = BaseLen; i < BaseLen + ExtLen; i++)
592 {
593 if (cString[i] >= 'A' &&
594 cString[i] <= 'Z')
595 {
596 /* Lowercase it */
597 cString[i] += 'a' - 'A';
598 }
599 }
600 }
601 }
602
603 /* Set the length */
604 OutString->Length = BaseLen + ExtLen;
605
606 DPRINT("'%s', len %d\n", OutString->Buffer, OutString->Length);
607 }
608
609 VOID
610 NTAPI
611 FatInsertName(IN PFAT_IRP_CONTEXT IrpContext,
612 IN PRTL_SPLAY_LINKS *RootNode,
613 IN PFCB_NAME_LINK Name)
614 {
615 PFCB_NAME_LINK NameLink;
616 FSRTL_COMPARISON_RESULT Comparison;
617
618 /* Initialize the splay links */
619 RtlInitializeSplayLinks(&Name->Links);
620
621 /* Is this the first entry? */
622 if (*RootNode == NULL)
623 {
624 /* Yes, become root and return */
625 *RootNode = &Name->Links;
626 return;
627 }
628
629 /* Get the name link */
630 NameLink = CONTAINING_RECORD(*RootNode, FCB_NAME_LINK, Links);
631 while (TRUE)
632 {
633 /* Compare prefixes */
634 Comparison = FatiCompareNames(&NameLink->Name.Ansi, &Name->Name.Ansi);
635
636 /* Check the bad case first */
637 if (Comparison == EqualTo)
638 {
639 /* Must not happen */
640 ASSERT(FALSE);
641 }
642
643 /* Check comparison result */
644 if (Comparison == GreaterThan)
645 {
646 /* Go to the left child */
647 if (!RtlLeftChild(&NameLink->Links))
648 {
649 /* It's absent, insert here and break */
650 RtlInsertAsLeftChild(&NameLink->Links, &NameLink->Links);
651 break;
652 }
653 else
654 {
655 /* It's present, go inside it */
656 NameLink = CONTAINING_RECORD(RtlLeftChild(&NameLink->Links),
657 FCB_NAME_LINK,
658 Links);
659 }
660 }
661 else
662 {
663 /* Go to the right child */
664 if (!RtlRightChild(&NameLink->Links))
665 {
666 /* It's absent, insert here and break */
667 RtlInsertAsRightChild(&NameLink->Links, &Name->Links);
668 break;
669 }
670 else
671 {
672 /* It's present, go inside it */
673 NameLink = CONTAINING_RECORD(RtlRightChild(&NameLink->Links),
674 FCB_NAME_LINK,
675 Links);
676 }
677 }
678 }
679 }
680
681 VOID
682 NTAPI
683 FatRemoveNames(IN PFAT_IRP_CONTEXT IrpContext,
684 IN PFCB Fcb)
685 {
686 PRTL_SPLAY_LINKS RootNew;
687 PFCB Parent;
688
689 /* Reference the parent for simplicity */
690 Parent = Fcb->ParentFcb;
691
692 /* If this FCB hasn't been added to splay trees - just return */
693 if (!FlagOn( Fcb->State, FCB_STATE_HAS_NAMES ))
694 return;
695
696 /* Delete the short name link */
697 RootNew = RtlDelete(&Fcb->ShortName.Links);
698
699 /* Set the new root */
700 Parent->Dcb.SplayLinksAnsi = RootNew;
701
702 /* Deal with a unicode name if it exists */
703 if (FlagOn( Fcb->State, FCB_STATE_HAS_UNICODE_NAME ))
704 {
705 /* Delete the long unicode name link */
706 RootNew = RtlDelete(&Fcb->LongName.Links);
707
708 /* Set the new root */
709 Parent->Dcb.SplayLinksUnicode = RootNew;
710
711 /* Free the long name string's buffer*/
712 RtlFreeUnicodeString(&Fcb->LongName.Name.String);
713
714 /* Clear the "has unicode name" flag */
715 ClearFlag(Fcb->State, FCB_STATE_HAS_UNICODE_NAME);
716 }
717
718 /* This FCB has no names added to splay trees now */
719 ClearFlag(Fcb->State, FCB_STATE_HAS_NAMES);
720 }
721
722
723 /* EOF */