95811fb14b9c53c47382d18b5e8128473c2611f0
[reactos.git] / reactos / dll / win32 / samsrv / database.c
1 /*
2 * PROJECT: Local Security Authority Server DLL
3 * LICENSE: GPL - See COPYING in the top level directory
4 * FILE: dll/win32/samsrv/database.c
5 * PURPOSE: SAM object database
6 * COPYRIGHT: Copyright 2012 Eric Kohl
7 */
8
9 /* INCLUDES ****************************************************************/
10
11 #include "samsrv.h"
12
13 WINE_DEFAULT_DEBUG_CHANNEL(samsrv);
14
15
16 /* GLOBALS *****************************************************************/
17
18 static HANDLE SamKeyHandle = NULL;
19
20
21 /* FUNCTIONS ***************************************************************/
22
23 static NTSTATUS
24 SampOpenSamKey(VOID)
25 {
26 OBJECT_ATTRIBUTES ObjectAttributes;
27 UNICODE_STRING KeyName;
28 NTSTATUS Status;
29
30 RtlInitUnicodeString(&KeyName,
31 L"\\Registry\\Machine\\SAM");
32
33 InitializeObjectAttributes(&ObjectAttributes,
34 &KeyName,
35 OBJ_CASE_INSENSITIVE,
36 NULL,
37 NULL);
38
39 Status = RtlpNtOpenKey(&SamKeyHandle,
40 KEY_READ | KEY_CREATE_SUB_KEY | KEY_ENUMERATE_SUB_KEYS,
41 &ObjectAttributes,
42 0);
43
44 return Status;
45 }
46
47
48 NTSTATUS
49 SampInitDatabase(VOID)
50 {
51 NTSTATUS Status;
52
53 TRACE("SampInitDatabase()\n");
54
55 Status = SampOpenSamKey();
56 if (!NT_SUCCESS(Status))
57 {
58 ERR("Failed to open the SAM key (Status: 0x%08lx)\n", Status);
59 return Status;
60 }
61
62 #if 0
63 if (!LsapIsDatabaseInstalled())
64 {
65 Status = LsapCreateDatabaseKeys();
66 if (!NT_SUCCESS(Status))
67 {
68 ERR("Failed to create the LSA database keys (Status: 0x%08lx)\n", Status);
69 return Status;
70 }
71
72 Status = LsapCreateDatabaseObjects();
73 if (!NT_SUCCESS(Status))
74 {
75 ERR("Failed to create the LSA database objects (Status: 0x%08lx)\n", Status);
76 return Status;
77 }
78 }
79 else
80 {
81 Status = LsapUpdateDatabase();
82 if (!NT_SUCCESS(Status))
83 {
84 ERR("Failed to update the LSA database (Status: 0x%08lx)\n", Status);
85 return Status;
86 }
87 }
88 #endif
89
90 TRACE("SampInitDatabase() done\n");
91
92 return STATUS_SUCCESS;
93 }
94
95
96 NTSTATUS
97 SampCreateDbObject(IN PSAM_DB_OBJECT ParentObject,
98 IN LPWSTR ContainerName,
99 IN LPWSTR ObjectName,
100 IN SAM_DB_OBJECT_TYPE ObjectType,
101 IN ACCESS_MASK DesiredAccess,
102 OUT PSAM_DB_OBJECT *DbObject)
103 {
104 PSAM_DB_OBJECT NewObject;
105 OBJECT_ATTRIBUTES ObjectAttributes;
106 UNICODE_STRING KeyName;
107 HANDLE ParentKeyHandle;
108 HANDLE ContainerKeyHandle = NULL;
109 HANDLE ObjectKeyHandle = NULL;
110 HANDLE MembersKeyHandle = NULL;
111 NTSTATUS Status;
112
113 if (DbObject == NULL)
114 return STATUS_INVALID_PARAMETER;
115
116 if (ParentObject == NULL)
117 ParentKeyHandle = SamKeyHandle;
118 else
119 ParentKeyHandle = ParentObject->KeyHandle;
120
121 if (ContainerName != NULL)
122 {
123 /* Open the container key */
124 RtlInitUnicodeString(&KeyName,
125 ContainerName);
126
127 InitializeObjectAttributes(&ObjectAttributes,
128 &KeyName,
129 OBJ_CASE_INSENSITIVE,
130 ParentKeyHandle,
131 NULL);
132
133 Status = NtOpenKey(&ContainerKeyHandle,
134 KEY_ALL_ACCESS,
135 &ObjectAttributes);
136 if (!NT_SUCCESS(Status))
137 {
138 return Status;
139 }
140
141 /* Open the object key */
142 RtlInitUnicodeString(&KeyName,
143 ObjectName);
144
145 InitializeObjectAttributes(&ObjectAttributes,
146 &KeyName,
147 OBJ_CASE_INSENSITIVE,
148 ContainerKeyHandle,
149 NULL);
150
151 Status = NtCreateKey(&ObjectKeyHandle,
152 KEY_ALL_ACCESS,
153 &ObjectAttributes,
154 0,
155 NULL,
156 0,
157 NULL);
158
159 if ((ObjectType == SamDbAliasObject) ||
160 (ObjectType == SamDbGroupObject))
161 {
162 /* Open the object key */
163 RtlInitUnicodeString(&KeyName,
164 L"Members");
165
166 InitializeObjectAttributes(&ObjectAttributes,
167 &KeyName,
168 OBJ_CASE_INSENSITIVE | OBJ_OPENIF,
169 ContainerKeyHandle,
170 NULL);
171
172 Status = NtCreateKey(&MembersKeyHandle,
173 KEY_ALL_ACCESS,
174 &ObjectAttributes,
175 0,
176 NULL,
177 0,
178 NULL);
179 }
180
181 NtClose(ContainerKeyHandle);
182
183 if (!NT_SUCCESS(Status))
184 {
185 return Status;
186 }
187 }
188 else
189 {
190 RtlInitUnicodeString(&KeyName,
191 ObjectName);
192
193 InitializeObjectAttributes(&ObjectAttributes,
194 &KeyName,
195 OBJ_CASE_INSENSITIVE,
196 ParentKeyHandle,
197 NULL);
198
199 Status = NtCreateKey(&ObjectKeyHandle,
200 KEY_ALL_ACCESS,
201 &ObjectAttributes,
202 0,
203 NULL,
204 0,
205 NULL);
206 if (!NT_SUCCESS(Status))
207 {
208 return Status;
209 }
210 }
211
212 NewObject = RtlAllocateHeap(RtlGetProcessHeap(),
213 0,
214 sizeof(SAM_DB_OBJECT));
215 if (NewObject == NULL)
216 {
217 if (MembersKeyHandle != NULL)
218 NtClose(MembersKeyHandle);
219 NtClose(ObjectKeyHandle);
220 return STATUS_NO_MEMORY;
221 }
222
223 NewObject->Name = RtlAllocateHeap(RtlGetProcessHeap(),
224 0,
225 (wcslen(ObjectName) + 1) * sizeof(WCHAR));
226 if (NewObject == NULL)
227 {
228 if (MembersKeyHandle != NULL)
229 NtClose(MembersKeyHandle);
230 NtClose(ObjectKeyHandle);
231 RtlFreeHeap(RtlGetProcessHeap(), 0, NewObject);
232 return STATUS_NO_MEMORY;
233 }
234
235 wcscpy(NewObject->Name, ObjectName);
236
237 NewObject->Signature = SAMP_DB_SIGNATURE;
238 NewObject->RefCount = 1;
239 NewObject->ObjectType = ObjectType;
240 NewObject->Access = DesiredAccess;
241 NewObject->KeyHandle = ObjectKeyHandle;
242 NewObject->MembersKeyHandle = MembersKeyHandle;
243 NewObject->ParentObject = ParentObject;
244
245 if (ParentObject != NULL)
246 ParentObject->RefCount++;
247
248 *DbObject = NewObject;
249
250 return STATUS_SUCCESS;
251 }
252
253
254 NTSTATUS
255 SampOpenDbObject(IN PSAM_DB_OBJECT ParentObject,
256 IN LPWSTR ContainerName,
257 IN LPWSTR ObjectName,
258 IN SAM_DB_OBJECT_TYPE ObjectType,
259 IN ACCESS_MASK DesiredAccess,
260 OUT PSAM_DB_OBJECT *DbObject)
261 {
262 PSAM_DB_OBJECT NewObject;
263 OBJECT_ATTRIBUTES ObjectAttributes;
264 UNICODE_STRING KeyName;
265 HANDLE ParentKeyHandle;
266 HANDLE ContainerKeyHandle = NULL;
267 HANDLE ObjectKeyHandle = NULL;
268 HANDLE MembersKeyHandle = NULL;
269 NTSTATUS Status;
270
271 if (DbObject == NULL)
272 return STATUS_INVALID_PARAMETER;
273
274 if (ParentObject == NULL)
275 ParentKeyHandle = SamKeyHandle;
276 else
277 ParentKeyHandle = ParentObject->KeyHandle;
278
279 if (ContainerName != NULL)
280 {
281 /* Open the container key */
282 RtlInitUnicodeString(&KeyName,
283 ContainerName);
284
285 InitializeObjectAttributes(&ObjectAttributes,
286 &KeyName,
287 OBJ_CASE_INSENSITIVE,
288 ParentKeyHandle,
289 NULL);
290
291 Status = NtOpenKey(&ContainerKeyHandle,
292 KEY_ALL_ACCESS,
293 &ObjectAttributes);
294 if (!NT_SUCCESS(Status))
295 {
296 return Status;
297 }
298
299 /* Open the object key */
300 RtlInitUnicodeString(&KeyName,
301 ObjectName);
302
303 InitializeObjectAttributes(&ObjectAttributes,
304 &KeyName,
305 OBJ_CASE_INSENSITIVE,
306 ContainerKeyHandle,
307 NULL);
308
309 Status = NtOpenKey(&ObjectKeyHandle,
310 KEY_ALL_ACCESS,
311 &ObjectAttributes);
312
313 if ((ObjectType == SamDbAliasObject) ||
314 (ObjectType == SamDbGroupObject))
315 {
316 /* Open the object key */
317 RtlInitUnicodeString(&KeyName,
318 L"Members");
319
320 InitializeObjectAttributes(&ObjectAttributes,
321 &KeyName,
322 OBJ_CASE_INSENSITIVE | OBJ_OPENIF,
323 ContainerKeyHandle,
324 NULL);
325
326 Status = NtCreateKey(&MembersKeyHandle,
327 KEY_ALL_ACCESS,
328 &ObjectAttributes,
329 0,
330 NULL,
331 0,
332 NULL);
333 }
334
335 NtClose(ContainerKeyHandle);
336
337 if (!NT_SUCCESS(Status))
338 {
339 return Status;
340 }
341 }
342 else
343 {
344 /* Open the object key */
345 RtlInitUnicodeString(&KeyName,
346 ObjectName);
347
348 InitializeObjectAttributes(&ObjectAttributes,
349 &KeyName,
350 OBJ_CASE_INSENSITIVE,
351 ParentKeyHandle,
352 NULL);
353
354 Status = NtOpenKey(&ObjectKeyHandle,
355 KEY_ALL_ACCESS,
356 &ObjectAttributes);
357 if (!NT_SUCCESS(Status))
358 {
359 return Status;
360 }
361 }
362
363 NewObject = RtlAllocateHeap(RtlGetProcessHeap(),
364 0,
365 sizeof(SAM_DB_OBJECT));
366 if (NewObject == NULL)
367 {
368 if (MembersKeyHandle != NULL)
369 NtClose(MembersKeyHandle);
370 NtClose(ObjectKeyHandle);
371 return STATUS_NO_MEMORY;
372 }
373
374 NewObject->Name = RtlAllocateHeap(RtlGetProcessHeap(),
375 0,
376 (wcslen(ObjectName) + 1) * sizeof(WCHAR));
377 if (NewObject == NULL)
378 {
379 if (MembersKeyHandle != NULL)
380 NtClose(MembersKeyHandle);
381 NtClose(ObjectKeyHandle);
382 RtlFreeHeap(RtlGetProcessHeap(), 0, NewObject);
383 return STATUS_NO_MEMORY;
384 }
385
386 wcscpy(NewObject->Name, ObjectName);
387 NewObject->Signature = SAMP_DB_SIGNATURE;
388 NewObject->RefCount = 1;
389 NewObject->ObjectType = ObjectType;
390 NewObject->Access = DesiredAccess;
391 NewObject->KeyHandle = ObjectKeyHandle;
392 NewObject->MembersKeyHandle = MembersKeyHandle;
393 NewObject->ParentObject = ParentObject;
394
395 if (ParentObject != NULL)
396 ParentObject->RefCount++;
397
398 *DbObject = NewObject;
399
400 return STATUS_SUCCESS;
401 }
402
403
404 NTSTATUS
405 SampValidateDbObject(SAMPR_HANDLE Handle,
406 SAM_DB_OBJECT_TYPE ObjectType,
407 ACCESS_MASK DesiredAccess,
408 PSAM_DB_OBJECT *DbObject)
409 {
410 PSAM_DB_OBJECT LocalObject = (PSAM_DB_OBJECT)Handle;
411 BOOLEAN bValid = FALSE;
412
413 _SEH2_TRY
414 {
415 if (LocalObject->Signature == SAMP_DB_SIGNATURE)
416 {
417 if ((ObjectType == SamDbIgnoreObject) ||
418 (LocalObject->ObjectType == ObjectType))
419 bValid = TRUE;
420 }
421 }
422 _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
423 {
424 bValid = FALSE;
425 }
426 _SEH2_END;
427
428 if (bValid == FALSE)
429 return STATUS_INVALID_HANDLE;
430
431 if (DesiredAccess != 0)
432 {
433 /* Check for granted access rights */
434 if ((LocalObject->Access & DesiredAccess) != DesiredAccess)
435 {
436 ERR("SampValidateDbObject access check failed %08lx %08lx\n",
437 LocalObject->Access, DesiredAccess);
438 return STATUS_ACCESS_DENIED;
439 }
440 }
441
442 if (DbObject != NULL)
443 *DbObject = LocalObject;
444
445 return STATUS_SUCCESS;
446 }
447
448
449 NTSTATUS
450 SampCloseDbObject(PSAM_DB_OBJECT DbObject)
451 {
452 PSAM_DB_OBJECT ParentObject = NULL;
453 NTSTATUS Status = STATUS_SUCCESS;
454
455 DbObject->RefCount--;
456
457 if (DbObject->RefCount > 0)
458 return STATUS_SUCCESS;
459
460 if (DbObject->KeyHandle != NULL)
461 NtClose(DbObject->KeyHandle);
462
463 if (DbObject->MembersKeyHandle != NULL)
464 NtClose(DbObject->MembersKeyHandle);
465
466 if (DbObject->ParentObject != NULL)
467 ParentObject = DbObject->ParentObject;
468
469 if (DbObject->Name != NULL)
470 RtlFreeHeap(RtlGetProcessHeap(), 0, DbObject->Name);
471
472 RtlFreeHeap(RtlGetProcessHeap(), 0, DbObject);
473
474 if (ParentObject != NULL)
475 {
476 ParentObject->RefCount--;
477
478 if (ParentObject->RefCount == 0)
479 Status = SampCloseDbObject(ParentObject);
480 }
481
482 return Status;
483 }
484
485
486 NTSTATUS
487 SampSetAccountNameInDomain(IN PSAM_DB_OBJECT DomainObject,
488 IN LPCWSTR lpContainerName,
489 IN LPCWSTR lpAccountName,
490 IN ULONG ulRelativeId)
491 {
492 OBJECT_ATTRIBUTES ObjectAttributes;
493 UNICODE_STRING KeyName;
494 UNICODE_STRING ValueName;
495 HANDLE ContainerKeyHandle = NULL;
496 HANDLE NamesKeyHandle = NULL;
497 NTSTATUS Status;
498
499 TRACE("SampSetAccountNameInDomain()\n");
500
501 /* Open the container key */
502 RtlInitUnicodeString(&KeyName, lpContainerName);
503
504 InitializeObjectAttributes(&ObjectAttributes,
505 &KeyName,
506 OBJ_CASE_INSENSITIVE,
507 DomainObject->KeyHandle,
508 NULL);
509
510 Status = NtOpenKey(&ContainerKeyHandle,
511 KEY_ALL_ACCESS,
512 &ObjectAttributes);
513 if (!NT_SUCCESS(Status))
514 return Status;
515
516 /* Open the 'Names' key */
517 RtlInitUnicodeString(&KeyName, L"Names");
518
519 InitializeObjectAttributes(&ObjectAttributes,
520 &KeyName,
521 OBJ_CASE_INSENSITIVE,
522 ContainerKeyHandle,
523 NULL);
524
525 Status = NtOpenKey(&NamesKeyHandle,
526 KEY_ALL_ACCESS,
527 &ObjectAttributes);
528 if (!NT_SUCCESS(Status))
529 goto done;
530
531 /* Set the alias value */
532 RtlInitUnicodeString(&ValueName, lpAccountName);
533
534 Status = NtSetValueKey(NamesKeyHandle,
535 &ValueName,
536 0,
537 REG_DWORD,
538 (LPVOID)&ulRelativeId,
539 sizeof(ULONG));
540
541 done:
542 if (NamesKeyHandle)
543 NtClose(NamesKeyHandle);
544
545 if (ContainerKeyHandle)
546 NtClose(ContainerKeyHandle);
547
548 return Status;
549 }
550
551
552 NTSTATUS
553 SampCheckAccountNameInDomain(IN PSAM_DB_OBJECT DomainObject,
554 IN LPWSTR lpAccountName)
555 {
556 HANDLE AccountKey;
557 HANDLE NamesKey;
558 NTSTATUS Status;
559
560 TRACE("SampCheckAccountNameInDomain()\n");
561
562 Status = SampRegOpenKey(DomainObject->KeyHandle,
563 L"Aliases",
564 KEY_READ,
565 &AccountKey);
566 if (NT_SUCCESS(Status))
567 {
568 Status = SampRegOpenKey(AccountKey,
569 L"Names",
570 KEY_READ,
571 &NamesKey);
572 if (NT_SUCCESS(Status))
573 {
574 Status = SampRegQueryValue(NamesKey,
575 lpAccountName,
576 NULL,
577 NULL,
578 NULL);
579 if (Status == STATUS_SUCCESS)
580 {
581 SampRegCloseKey(NamesKey);
582 Status = STATUS_ALIAS_EXISTS;
583 }
584 else if (Status == STATUS_OBJECT_NAME_NOT_FOUND)
585 Status = STATUS_SUCCESS;
586 }
587
588 SampRegCloseKey(AccountKey);
589 }
590
591 if (!NT_SUCCESS(Status))
592 {
593 TRACE("Checking for alias account failed (Status 0x%08lx)\n", Status);
594 return Status;
595 }
596
597 Status = SampRegOpenKey(DomainObject->KeyHandle,
598 L"Groups",
599 KEY_READ,
600 &AccountKey);
601 if (NT_SUCCESS(Status))
602 {
603 Status = SampRegOpenKey(AccountKey,
604 L"Names",
605 KEY_READ,
606 &NamesKey);
607 if (NT_SUCCESS(Status))
608 {
609 Status = SampRegQueryValue(NamesKey,
610 lpAccountName,
611 NULL,
612 NULL,
613 NULL);
614 if (Status == STATUS_SUCCESS)
615 {
616 SampRegCloseKey(NamesKey);
617 Status = STATUS_ALIAS_EXISTS;
618 }
619 else if (Status == STATUS_OBJECT_NAME_NOT_FOUND)
620 Status = STATUS_SUCCESS;
621 }
622
623 SampRegCloseKey(AccountKey);
624 }
625
626 if (!NT_SUCCESS(Status))
627 {
628 TRACE("Checking for group account failed (Status 0x%08lx)\n", Status);
629 return Status;
630 }
631
632 Status = SampRegOpenKey(DomainObject->KeyHandle,
633 L"Users",
634 KEY_READ,
635 &AccountKey);
636 if (NT_SUCCESS(Status))
637 {
638 Status = SampRegOpenKey(AccountKey,
639 L"Names",
640 KEY_READ,
641 &NamesKey);
642 if (NT_SUCCESS(Status))
643 {
644 Status = SampRegQueryValue(NamesKey,
645 lpAccountName,
646 NULL,
647 NULL,
648 NULL);
649 if (Status == STATUS_SUCCESS)
650 {
651 SampRegCloseKey(NamesKey);
652 Status = STATUS_ALIAS_EXISTS;
653 }
654 else if (Status == STATUS_OBJECT_NAME_NOT_FOUND)
655 Status = STATUS_SUCCESS;
656 }
657
658 SampRegCloseKey(AccountKey);
659 }
660
661 if (!NT_SUCCESS(Status))
662 {
663 TRACE("Checking for user account failed (Status 0x%08lx)\n", Status);
664 }
665
666 return Status;
667 }
668
669
670 NTSTATUS
671 SampSetObjectAttribute(PSAM_DB_OBJECT DbObject,
672 LPWSTR AttributeName,
673 ULONG AttributeType,
674 LPVOID AttributeData,
675 ULONG AttributeSize)
676 {
677 UNICODE_STRING ValueName;
678
679 RtlInitUnicodeString(&ValueName,
680 AttributeName);
681
682 return ZwSetValueKey(DbObject->KeyHandle,
683 &ValueName,
684 0,
685 AttributeType,
686 AttributeData,
687 AttributeSize);
688 }
689
690
691 NTSTATUS
692 SampGetObjectAttribute(PSAM_DB_OBJECT DbObject,
693 LPWSTR AttributeName,
694 PULONG AttributeType,
695 LPVOID AttributeData,
696 PULONG AttributeSize)
697 {
698 PKEY_VALUE_PARTIAL_INFORMATION ValueInfo;
699 UNICODE_STRING ValueName;
700 ULONG BufferLength = 0;
701 NTSTATUS Status;
702
703 RtlInitUnicodeString(&ValueName,
704 AttributeName);
705
706 if (AttributeSize != NULL)
707 BufferLength = *AttributeSize;
708
709 BufferLength += FIELD_OFFSET(KEY_VALUE_PARTIAL_INFORMATION, Data);
710
711 /* Allocate memory for the value */
712 ValueInfo = RtlAllocateHeap(RtlGetProcessHeap(), 0, BufferLength);
713 if (ValueInfo == NULL)
714 return STATUS_NO_MEMORY;
715
716 /* Query the value */
717 Status = ZwQueryValueKey(DbObject->KeyHandle,
718 &ValueName,
719 KeyValuePartialInformation,
720 ValueInfo,
721 BufferLength,
722 &BufferLength);
723 if ((NT_SUCCESS(Status)) || (Status == STATUS_BUFFER_OVERFLOW))
724 {
725 if (AttributeType != NULL)
726 *AttributeType = ValueInfo->Type;
727
728 if (AttributeSize != NULL)
729 *AttributeSize = ValueInfo->DataLength;
730 }
731
732 /* Check if the caller wanted data back, and we got it */
733 if ((NT_SUCCESS(Status)) && (AttributeData != NULL))
734 {
735 /* Copy it */
736 RtlMoveMemory(AttributeData,
737 ValueInfo->Data,
738 ValueInfo->DataLength);
739 }
740
741 /* Free the memory and return status */
742 RtlFreeHeap(RtlGetProcessHeap(), 0, ValueInfo);
743
744 return Status;
745 }
746
747
748 NTSTATUS
749 SampGetObjectAttributeString(PSAM_DB_OBJECT DbObject,
750 LPWSTR AttributeName,
751 RPC_UNICODE_STRING *String)
752 {
753 ULONG Length = 0;
754 NTSTATUS Status;
755
756 Status = SampGetObjectAttribute(DbObject,
757 AttributeName,
758 NULL,
759 NULL,
760 &Length);
761 if (!NT_SUCCESS(Status) && Status != STATUS_BUFFER_OVERFLOW)
762 {
763 TRACE("Status 0x%08lx\n", Status);
764 goto done;
765 }
766
767 String->Length = (USHORT)(Length - sizeof(WCHAR));
768 String->MaximumLength = (USHORT)Length;
769 String->Buffer = midl_user_allocate(Length);
770 if (String->Buffer == NULL)
771 {
772 Status = STATUS_INSUFFICIENT_RESOURCES;
773 goto done;
774 }
775
776 TRACE("Length: %lu\n", Length);
777 Status = SampGetObjectAttribute(DbObject,
778 AttributeName,
779 NULL,
780 (PVOID)String->Buffer,
781 &Length);
782 if (!NT_SUCCESS(Status))
783 {
784 TRACE("Status 0x%08lx\n", Status);
785 goto done;
786 }
787
788 done:
789 if (!NT_SUCCESS(Status))
790 {
791 if (String->Buffer != NULL)
792 {
793 midl_user_free(String->Buffer);
794 String->Buffer = NULL;
795 }
796 }
797
798 return Status;
799 }
800
801 /* EOF */
802