]> git.proxmox.com Git - mirror_edk2.git/blob - EmbeddedPkg/Ebl/Command.c
Remove dead command and clean up some coding style stuff.
[mirror_edk2.git] / EmbeddedPkg / Ebl / Command.c
1 /** @file
2 Basic commands and command processing infrastructure for EBL
3
4 Copyright (c) 2007, Intel Corporation<BR>
5 Portions copyright (c) 2008-2009, Apple Inc. All rights reserved.
6
7 All rights reserved. This program and the accompanying materials
8 are licensed and made available under the terms and conditions of the BSD License
9 which accompanies this distribution. The full text of the license may be found at
10 http://opensource.org/licenses/bsd-license.php
11
12 THE PROGRAM IS DISTRIBUTED UNDER THE BSD LICENSE ON AN "AS IS" BASIS,
13 WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED.
14
15 **/
16
17 #include "Ebl.h"
18 #include <Protocol/DiskIo.h>
19 #include <Protocol/BlockIo.h>
20
21 UINTN mCmdTableMaxIndex = EBL_MAX_COMMAND_COUNT;
22 UINTN mCmdTableNextFreeIndex = 0;
23 EBL_COMMAND_TABLE *mCmdTable[EBL_MAX_COMMAND_COUNT];
24
25 /**
26 Converts a lowercase Ascii character to upper one
27
28 If Chr is lowercase Ascii character, then converts it to upper one.
29
30 If Value >= 0xA0, then ASSERT().
31 If (Value & 0x0F) >= 0x0A, then ASSERT().
32
33 @param chr one Ascii character
34
35 @return The uppercase value of Ascii character
36
37 **/
38 STATIC
39 CHAR8
40 AsciiToUpper (
41 IN CHAR8 Chr
42 )
43 {
44 return (UINT8) ((Chr >= 'a' && Chr <= 'z') ? Chr - ('a' - 'A') : Chr);
45 }
46
47
48 /**
49 Case insensitve comparison of two Null-terminated Unicode strings with maximum
50 lengths, and returns the difference between the first mismatched Unicode
51 characters.
52 This function compares the Null-terminated Unicode string FirstString to the
53 Null-terminated Unicode string SecondString. At most, Length Unicode
54 characters will be compared. If Length is 0, then 0 is returned. If
55 FirstString is identical to SecondString, then 0 is returned. Otherwise, the
56 value returned is the first mismatched Unicode character in SecondString
57 subtracted from the first mismatched Unicode character in FirstString.
58
59 @param FirstString Pointer to a Null-terminated ASCII string.
60 @param SecondString Pointer to a Null-terminated ASCII string.
61 @param Length Max length to compare.
62
63 @retval 0 FirstString is identical to SecondString using case insensitive
64 comparisons.
65 @retval !=0 FirstString is not identical to SecondString using case
66 insensitive comparisons.
67
68 **/
69 INTN
70 EFIAPI
71 AsciiStrniCmp (
72 IN CONST CHAR8 *FirstString,
73 IN CONST CHAR8 *SecondString,
74 IN UINTN Length
75 )
76 {
77 if (Length == 0) {
78 return 0;
79 }
80
81 while ((AsciiToUpper (*FirstString) != '\0') &&
82 (AsciiToUpper (*FirstString) == AsciiToUpper (*SecondString)) &&
83 (Length > 1)) {
84 FirstString++;
85 SecondString++;
86 Length--;
87 }
88
89 return AsciiToUpper (*FirstString) - AsciiToUpper (*SecondString);
90 }
91
92
93
94 /**
95 Add a command to the mCmdTable. If there is no free space in the command
96 table ASSERT. The mCmdTable is maintained in alphabetical order and the
97 new entry is inserted into its sorted possition.
98
99 @param Entry Commnad Entry to add to the CmdTable
100
101 **/
102 VOID
103 EFIAPI
104 EblAddCommand (
105 IN const EBL_COMMAND_TABLE *Entry
106 )
107 {
108 UINTN Count;
109
110 if (mCmdTableNextFreeIndex == EBL_MAX_COMMAND_COUNT) {
111 //
112 // Ran out of space to store commands. Increase EBL_MAX_COMMAND_COUNT
113 //
114 ASSERT (FALSE);
115 return;
116 }
117
118 //
119 // Add command and Insertion sort array in the process
120 //
121 mCmdTable[mCmdTableNextFreeIndex] = (EBL_COMMAND_TABLE *)Entry;
122 if (mCmdTableNextFreeIndex != 0) {
123 for (Count = mCmdTableNextFreeIndex; Count > 0; Count--) {
124 if (AsciiStriCmp (mCmdTable[Count - 1]->Name, Entry->Name) <= 0) {
125 break;
126 }
127
128 mCmdTable[Count] = mCmdTable[Count - 1];
129 }
130 mCmdTable[Count] = (EBL_COMMAND_TABLE *)Entry;
131 }
132
133 mCmdTableNextFreeIndex++;
134 }
135
136
137 /**
138 Add an set of commands to the command table. Most commonly used on static
139 array of commands.
140
141 @param EntryArray Pointer to array of command entries
142 @param ArrayCount Number of commnad entries to add
143
144 **/
145 VOID
146 EFIAPI
147 EblAddCommands (
148 IN const EBL_COMMAND_TABLE *EntryArray,
149 IN UINTN ArrayCount
150 )
151 {
152 UINTN Index;
153
154 for (Index = 0; Index < ArrayCount; Index++) {
155 EblAddCommand (&EntryArray[Index]);
156 }
157 }
158
159
160 EBL_ADD_COMMAND_PROTOCOL gEblAddCommand = {
161 EblAddCommand,
162 EblAddCommands,
163 EblGetCharKey,
164 EblAnyKeyToContinueQtoQuit
165 };
166
167
168
169 /**
170 Return the best matching command for the passed in command name. The match
171 does not have to be exact, it just needs to be unqiue. This enables commands
172 to be shortend to the smallest set of starting characters that is unique.
173
174 @param CommandName Name of command to search for
175
176 @return NULL CommandName did not match or was not unique
177 Other Pointer to EBL_COMMAND_TABLE entry for CommandName
178
179 **/
180 EBL_COMMAND_TABLE *
181 EblGetCommand (
182 IN CHAR8 *CommandName
183 )
184 {
185 UINTN Index;
186 UINTN BestMatchCount;
187 UINTN Length;
188 EBL_COMMAND_TABLE *Match;
189
190 Length = AsciiStrLen (CommandName);
191 for (Index = 0, BestMatchCount = 0, Match = NULL; Index < mCmdTableNextFreeIndex; Index++) {
192 if (AsciiStriCmp (mCmdTable[Index]->Name, CommandName) == 0) {
193 // match a command exactly
194 return mCmdTable[Index];
195 }
196
197 if (AsciiStrniCmp (CommandName, mCmdTable[Index]->Name, Length) == 0) {
198 // partial match, so keep looking to make sure there is only one partial match
199 BestMatchCount++;
200 Match = mCmdTable[Index];
201 }
202 }
203
204 if (BestMatchCount == 1) {
205 return Match;
206 }
207
208 //
209 // We had no matches or too many matches
210 //
211 return NULL;
212 }
213
214
215
216 /**
217 List out help information on all the commands or print extended information
218 about a specific passed in command.
219
220 Argv[0] - "help"
221 Argv[1] - Command to display help about
222
223 @param Argc Number of command arguments in Argv
224 @param Argv Array of strings that represent the parsed command line.
225 Argv[0] is the comamnd name
226
227 @return EFI_SUCCESS
228
229 **/
230 EFI_STATUS
231 EblHelpCmd (
232 IN UINTN Argc,
233 IN CHAR8 **Argv
234 )
235 {
236 UINTN Index;
237 CHAR8 *Ptr;
238 UINTN CurrentRow;
239
240 if (Argc == 1) {
241 // Print all the commands
242 AsciiPrint ("Embedded Boot Loader (EBL) commands (help command for more info):\n");
243 for (Index = 0; Index < mCmdTableNextFreeIndex; Index++) {
244 EblSetTextColor (EFI_YELLOW);
245 AsciiPrint (" %a", mCmdTable[Index]->Name);
246 EblSetTextColor (0);
247 AsciiPrint ("%a\n", mCmdTable[Index]->HelpSummary);
248 }
249 } else if (Argv[1] != NULL) {
250 // Print specific help
251 for (Index = 0, CurrentRow = 0; Index < mCmdTableNextFreeIndex; Index++) {
252 if (AsciiStriCmp (Argv[1], mCmdTable[Index]->Name) == 0) {
253 Ptr = (mCmdTable[Index]->Help == NULL) ? mCmdTable[Index]->HelpSummary : mCmdTable[Index]->Help;
254 AsciiPrint ("%a%a\n", Argv[1], Ptr);
255 if (EblAnyKeyToContinueQtoQuit (&CurrentRow, FALSE)) {
256 break;
257 }
258 }
259 }
260 }
261
262 return EFI_SUCCESS;
263 }
264
265
266 /**
267 Exit the EBL. If the commnad processor sees EFI_ABORTED return status it will
268 exit the EBL.
269
270 Argv[0] - "exit"
271
272 @param Argc Number of command arguments in Argv
273 @param Argv Array of strings that represent the parsed command line.
274 Argv[0] is the comamnd name
275
276 @return EFI_ABORTED
277
278 **/
279 EFI_STATUS
280 EblExitCmd (
281 IN UINTN Argc,
282 IN CHAR8 **Argv
283 )
284 {
285 EFI_STATUS Status;
286 UINTN MemoryMapSize;
287 EFI_MEMORY_DESCRIPTOR *MemoryMap;
288 UINTN MapKey;
289 UINTN DescriptorSize;
290 UINT32 DescriptorVersion;
291 UINTN Pages;
292
293 if (Argc > 1) {
294 if (AsciiStriCmp (Argv[1], "efi") != 0) {
295 return EFI_ABORTED;
296 }
297 } else if (Argc == 1) {
298 return EFI_ABORTED;
299 }
300
301 MemoryMap = NULL;
302 MemoryMapSize = 0;
303 do {
304 Status = gBS->GetMemoryMap (
305 &MemoryMapSize,
306 MemoryMap,
307 &MapKey,
308 &DescriptorSize,
309 &DescriptorVersion
310 );
311 if (Status == EFI_BUFFER_TOO_SMALL) {
312
313 Pages = EFI_SIZE_TO_PAGES (MemoryMapSize) + 1;
314 MemoryMap = AllocatePages (Pages);
315
316 //
317 // Get System MemoryMap
318 //
319 Status = gBS->GetMemoryMap (
320 &MemoryMapSize,
321 MemoryMap,
322 &MapKey,
323 &DescriptorSize,
324 &DescriptorVersion
325 );
326 // Don't do anything between the GetMemoryMap() and ExitBootServices()
327 if (!EFI_ERROR (Status)) {
328 Status = gBS->ExitBootServices (gImageHandle, MapKey);
329 if (EFI_ERROR (Status)) {
330 FreePages (MemoryMap, Pages);
331 MemoryMap = NULL;
332 MemoryMapSize = 0;
333 }
334 }
335 }
336 } while (EFI_ERROR (Status));
337
338 //
339 // At this point it is very dangerous to do things EFI as most of EFI is now gone.
340 // This command is useful if you are working with a debugger as it will shutdown
341 // DMA and other things that could break a soft resets.
342 //
343 CpuDeadLoop ();
344
345 // Should never get here, but makes the compiler happy
346 return EFI_ABORTED;
347 }
348
349
350 /**
351 Update the screen by decrementing the timeout value.
352 This AsciiPrint has to match the AsciiPrint in
353 EblPauseCmd.
354
355 @param ElaspedTime Current timout value remaining
356
357 **/
358 VOID
359 EFIAPI
360 EblPauseCallback (
361 IN UINTN ElapsedTime
362 )
363 {
364 AsciiPrint ("\b\b\b\b\b\b\b\b\b\b\b\b \b\b%3d seconds", ElapsedTime);
365 }
366
367 /**
368 Pause until a key is pressed and abort the remaining commands on the command
369 line. If no key is pressed continue processing the command line. This command
370 allows the user to stop an operation from happening and return control to the
371 command prompt.
372
373 Argv[0] - "pause"
374 Argv[1] - timeout value is decimal seconds
375
376 @param Argc Number of command arguments in Argv
377 @param Argv Array of strings that represent the parsed command line.
378 Argv[0] is the comamnd name
379
380 @return EFI_SUCCESS Timeout expired with no input
381 @return EFI_TIMEOUT Stop procesing other commands on the same command line
382
383 **/
384 EFI_STATUS
385 EblPauseCmd (
386 IN UINTN Argc,
387 IN CHAR8 **Argv
388 )
389 {
390 EFI_STATUS Status;
391 UINTN Delay;
392 EFI_INPUT_KEY Key;
393
394 Delay = (Argc == 1)? 10 : AsciiStrDecimalToUintn (Argv[1]);
395
396 AsciiPrint ("Hit any key to break. You have %3d seconds", Delay);
397 Status = EblGetCharKey (&Key, Delay, EblPauseCallback);
398 AsciiPrint ("\n");
399
400 // If we timeout then the pause succeded thus return success
401 // If we get a key return timout to stop other commnad on this cmd line
402 return (Status == EFI_SUCCESS) ? EFI_TIMEOUT : EFI_SUCCESS;;
403 }
404
405
406 /**
407 On a debug build issue a software breakpoint to enter the debugger
408
409 Argv[0] - "break"
410
411 @param Argc Number of command arguments in Argv
412 @param Argv Array of strings that represent the parsed command line.
413 Argv[0] is the comamnd name
414
415 @return EFI_SUCCESS
416
417 **/
418 EFI_STATUS
419 EblBreakPointCmd (
420 IN UINTN Argc,
421 IN CHAR8 **Argv
422 )
423 {
424 CpuBreakpoint ();
425 return EFI_SUCCESS;
426 }
427
428
429 /**
430 Reset the system. If no Argument do a Cold reset. If argument use that reset type
431 (W)arm = Warm Reset
432 (S)hutdown = Shutdown Reset
433
434 Argv[0] - "reset"
435 Argv[1] - warm or shutdown reset type
436
437 @param Argc Number of command arguments in Argv
438 @param Argv Array of strings that represent the parsed command line.
439 Argv[0] is the comamnd name
440
441 @return EFI_SUCCESS
442
443 **/
444 EFI_STATUS
445 EblResetCmd (
446 IN UINTN Argc,
447 IN CHAR8 **Argv
448 )
449 {
450 EFI_RESET_TYPE ResetType;
451
452 ResetType = EfiResetCold;
453 if (Argc > 1) {
454 switch (*Argv[1]) {
455 case 'W':
456 case 'w':
457 ResetType = EfiResetWarm;
458 break;
459 case 'S':
460 case 's':
461 ResetType = EfiResetShutdown;
462 }
463 }
464
465 gRT->ResetSystem (ResetType, EFI_SUCCESS, 0, NULL);
466 return EFI_SUCCESS;
467 }
468
469
470 /**
471 Toggle page break global. This turns on and off prompting to Quit or hit any
472 key to continue when a command is about to scroll the screen with its output
473
474 Argv[0] - "page"
475 Argv[1] - on or off
476
477 @param Argc Number of command arguments in Argv
478 @param Argv Array of strings that represent the parsed command line.
479 Argv[0] is the comamnd name
480
481 @return EFI_SUCCESS
482
483 **/
484 EFI_STATUS
485 EblPageCmd (
486 IN UINTN Argc,
487 IN CHAR8 **Argv
488 )
489 {
490 if (Argc <= 1) {
491 // toggle setting
492 gPageBreak = (gPageBreak) ? FALSE : TRUE;
493 } else {
494 // use argv to set the value
495 if ((Argv[1][0] == 'o') || (Argv[1][0] == 'O')) {
496 if ((Argv[1][1] == 'n') || (Argv[1][1] == 'N')) {
497 gPageBreak = TRUE;
498 } else if ((Argv[1][1] == 'f') || (Argv[1][1] == 'F')) {
499 gPageBreak = FALSE;
500 } else {
501 return EFI_INVALID_PARAMETER;
502 }
503 }
504 }
505 return EFI_SUCCESS;
506 }
507
508 EFI_STATUS
509 EblSleepCmd (
510 IN UINTN Argc,
511 IN CHAR8 **Argv
512 )
513 {
514 UINTN Delay;
515
516 Delay = (Argc == 1)? 10 : AsciiStrDecimalToUintn (Argv[1]);
517
518 gBS->Stall (Delay * 1000000);
519
520 return EFI_SUCCESS;
521 }
522
523 CHAR8
524 ConvertToTextLine (
525 IN CHAR8 Character
526 )
527 {
528 if (Character < ' ' || Character > '~') {
529 return '.';
530 } else {
531 return Character;
532 }
533 }
534
535 UINTN
536 GetBytes (
537 IN UINT8 *Address,
538 IN UINTN Bytes
539 )
540 {
541 UINTN Result = 0;
542
543 if (Bytes >= 1) {
544 Result = *Address++;
545 }
546 if (Bytes >= 2) {
547 Result = (Result << 8) + *Address++;
548 }
549 if (Bytes >= 3) {
550 Result = (Result << 8) + *Address++;
551 }
552 return Result;
553 }
554
555 CHAR8 mBlanks[] = " ";
556
557 EFI_STATUS
558 OutputData (
559 IN UINT8 *Address,
560 IN UINTN Length,
561 IN UINTN Width,
562 IN UINTN Offset
563 )
564 {
565 UINT8 *EndAddress;
566 UINTN Line;
567 CHAR8 TextLine[0x11];
568 UINTN CurrentRow = 0;
569 UINTN Bytes;
570 UINTN Spaces = 0;
571 CHAR8 Blanks[80];
572
573 AsciiStrCpy (Blanks, mBlanks);
574 for (EndAddress = Address + Length; Address < EndAddress; Offset += Line) {
575 AsciiPrint ("%08x: ", Offset);
576 for (Line = 0; (Line < 0x10) && (Address < EndAddress);) {
577 Bytes = EndAddress - Address;
578
579 switch (Width) {
580 case 4:
581 if (Bytes >= 4) {
582 AsciiPrint ("%08x ", *((UINT32 *)Address));
583 TextLine[Line++] = ConvertToTextLine(*Address++);
584 TextLine[Line++] = ConvertToTextLine(*Address++);
585 TextLine[Line++] = ConvertToTextLine(*Address++);
586 TextLine[Line++] = ConvertToTextLine(*Address++);
587 } else {
588 AsciiPrint ("%08x ", GetBytes(Address, Bytes));
589 Address += Bytes;
590 Line += Bytes;
591 }
592 break;
593
594 case 2:
595 if (Bytes >= 2) {
596 AsciiPrint ("%04x ", *((UINT16 *)Address));
597 TextLine[Line++] = ConvertToTextLine(*Address++);
598 TextLine[Line++] = ConvertToTextLine(*Address++);
599 } else {
600 AsciiPrint ("%04x ", GetBytes(Address, Bytes));
601 Address += Bytes;
602 Line += Bytes;
603 }
604 break;
605
606 case 1:
607 AsciiPrint ("%02x ", *((UINT8 *)Address));
608 TextLine[Line++] = ConvertToTextLine(*Address++);
609 break;
610
611 default:
612 AsciiPrint ("Width must be 1, 2, or 4!\n");
613 return EFI_INVALID_PARAMETER;
614 }
615 }
616
617 // Pad spaces
618 if (Line < 0x10) {
619 switch (Width) {
620 case 4:
621 Spaces = 9 * ((0x10 - Line)/4);
622 break;
623 case 2:
624 Spaces = 5 * ((0x10 - Line)/2);
625 break;
626 case 1:
627 Spaces = 3 * (0x10 - Line);
628 break;
629 }
630
631 Blanks[Spaces] = '\0';
632
633 AsciiPrint(Blanks);
634
635 Blanks[Spaces] = ' ';
636 }
637
638 TextLine[Line] = 0;
639 AsciiPrint ("|%a|\n", TextLine);
640
641 if (EblAnyKeyToContinueQtoQuit (&CurrentRow, FALSE)) {
642 return EFI_END_OF_FILE;
643 }
644 }
645
646 if (Length % Width != 0) {
647 AsciiPrint ("%08x\n", Offset);
648 }
649
650 return EFI_SUCCESS;
651 }
652
653 #define HEXDUMP_CHUNK 1024
654
655 EFI_STATUS
656 EblHexdumpCmd (
657 IN UINTN Argc,
658 IN CHAR8 **Argv
659 )
660 {
661 EFI_OPEN_FILE *File;
662 VOID *Location;
663 UINTN Size;
664 UINTN Width = 1;
665 UINTN Offset = 0;
666 EFI_STATUS Status;
667 UINTN Chunk = HEXDUMP_CHUNK;
668
669 if ((Argc < 2) || (Argc > 3)) {
670 return EFI_INVALID_PARAMETER;
671 }
672
673 if (Argc == 3) {
674 Width = AsciiStrDecimalToUintn(Argv[2]);
675 }
676
677 if ((Width != 1) && (Width != 2) && (Width != 4)) {
678 return EFI_INVALID_PARAMETER;
679 }
680
681 File = EfiOpen (Argv[1], EFI_FILE_MODE_READ, 0);
682 if (File == NULL) {
683 return EFI_NOT_FOUND;
684 }
685
686 Location = AllocatePool (Chunk);
687 Size = EfiTell(File, NULL);
688
689 for (Offset = 0; Offset + HEXDUMP_CHUNK <= Size; Offset += Chunk) {
690 Chunk = HEXDUMP_CHUNK;
691
692 Status = EfiRead (File, Location, &Chunk);
693 if (EFI_ERROR(Status))
694 {
695 AsciiPrint ("Error reading file content\n");
696 goto Exit;
697 }
698
699 Status = OutputData (Location, Chunk, Width, File->BaseOffset + Offset);
700 if (EFI_ERROR(Status)) {
701 if (Status == EFI_END_OF_FILE) {
702 Status = EFI_SUCCESS;
703 }
704 goto Exit;
705 }
706 }
707
708 // Any left over?
709 if (Offset < Size) {
710 Chunk = Size - Offset;
711 Status = EfiRead (File, Location, &Chunk);
712 if (EFI_ERROR(Status)) {
713 AsciiPrint ("Error reading file content\n");
714 goto Exit;
715 }
716
717 Status = OutputData (Location, Chunk, Width, File->BaseOffset + Offset);
718 if (EFI_ERROR(Status)) {
719 if (Status == EFI_END_OF_FILE) {
720 Status = EFI_SUCCESS;
721 }
722 goto Exit;
723 }
724 }
725
726 Exit:
727 EfiClose (File);
728
729 FreePool (Location);
730
731 return EFI_SUCCESS;
732 }
733
734
735 GLOBAL_REMOVE_IF_UNREFERENCED const EBL_COMMAND_TABLE mCmdTemplate[] =
736 {
737 {
738 "reset",
739 " [type]; Reset system. type = [warm] [shutdown] default is cold reset",
740 NULL,
741 EblResetCmd
742 },
743 {
744 "exit",
745 "; Exit EBL",
746 NULL,
747 EblExitCmd
748 },
749 {
750 "help",
751 " [cmd]; Help on cmd or a list of all commands if cmd is ommited",
752 NULL,
753 EblHelpCmd
754 },
755 {
756 "break",
757 "; Generate debugging breakpoint",
758 NULL,
759 EblBreakPointCmd
760 },
761 {
762 "page",
763 " [on|off]]; toggle promting on command output larger than screen",
764 NULL,
765 EblPageCmd
766 },
767 {
768 "pause",
769 " [sec]; Pause for sec[10] seconds. ",
770 NULL,
771 EblPauseCmd
772 },
773 {
774 "sleep",
775 " [sec]; Sleep for sec[10] seconds. ",
776 NULL,
777 EblSleepCmd
778 },
779 {
780 "hexdump",
781 " filename ; dump a file as hex bytes",
782 NULL,
783 EblHexdumpCmd
784 }
785 };
786
787
788 EFI_HANDLE gExternalCmdHandle = NULL;
789
790 /**
791 Initialize the commands in this in this file
792 **/
793 VOID
794 EblInitializeCmdTable (
795 VOID
796 )
797 {
798
799 EblAddCommands (mCmdTemplate, sizeof (mCmdTemplate)/sizeof (EBL_COMMAND_TABLE));
800
801 gBS->InstallProtocolInterface (
802 &gExternalCmdHandle,
803 &gEfiEblAddCommandProtocolGuid,
804 EFI_NATIVE_INTERFACE,
805 &gEblAddCommand
806 );
807
808 }
809
810
811 VOID
812 EblShutdownExternalCmdTable (
813 VOID
814 )
815 {
816 gBS->UninstallProtocolInterface (gExternalCmdHandle, &gEfiEblAddCommandProtocolGuid, &gEblAddCommand);
817 }
818
819