RtlCreateUnicodeString->RtlpCreateUnicodeString
[reactos.git] / reactos / ntoskrnl / cm / regobj.c
1 /* $Id$
2 *
3 * COPYRIGHT: See COPYING in the top level directory
4 * PROJECT: ReactOS kernel
5 * FILE: ntoskrnl/cm/regobj.c
6 * PURPOSE: Registry object manipulation routines.
7 *
8 * PROGRAMMERS: No programmer listed.
9 */
10
11 #include <ntoskrnl.h>
12 #define NDEBUG
13 #include <internal/debug.h>
14
15 #include "cm.h"
16
17
18 static NTSTATUS
19 CmiGetLinkTarget(PREGISTRY_HIVE RegistryHive,
20 PKEY_CELL KeyCell,
21 PUNICODE_STRING TargetPath);
22
23 /* FUNCTONS *****************************************************************/
24
25 NTSTATUS STDCALL
26 CmiObjectParse(PVOID ParsedObject,
27 PVOID *NextObject,
28 PUNICODE_STRING FullPath,
29 PWSTR *Path,
30 ULONG Attributes)
31 {
32 BLOCK_OFFSET BlockOffset;
33 PKEY_OBJECT FoundObject;
34 PKEY_OBJECT ParsedKey;
35 PKEY_CELL SubKeyCell;
36 NTSTATUS Status;
37 PWSTR StartPtr;
38 PWSTR EndPtr;
39 ULONG Length;
40 UNICODE_STRING LinkPath;
41 UNICODE_STRING TargetPath;
42 UNICODE_STRING KeyName;
43
44 ParsedKey = ParsedObject;
45
46 VERIFY_KEY_OBJECT(ParsedKey);
47
48 *NextObject = NULL;
49
50 if ((*Path) == NULL)
51 {
52 DPRINT("*Path is NULL\n");
53 return STATUS_UNSUCCESSFUL;
54 }
55
56 DPRINT("Path '%S'\n", *Path);
57
58 /* Extract relevant path name */
59 StartPtr = *Path;
60 if (*StartPtr == L'\\')
61 StartPtr++;
62
63 EndPtr = wcschr(StartPtr, L'\\');
64 if (EndPtr != NULL)
65 Length = ((PCHAR)EndPtr - (PCHAR)StartPtr) / sizeof(WCHAR);
66 else
67 Length = wcslen(StartPtr);
68
69
70 KeyName.Length = Length * sizeof(WCHAR);
71 KeyName.MaximumLength = KeyName.Length + sizeof(WCHAR);
72 KeyName.Buffer = ExAllocatePool(NonPagedPool,
73 KeyName.MaximumLength);
74 RtlCopyMemory(KeyName.Buffer,
75 StartPtr,
76 KeyName.Length);
77 KeyName.Buffer[KeyName.Length / sizeof(WCHAR)] = 0;
78
79
80 FoundObject = CmiScanKeyList(ParsedKey,
81 &KeyName,
82 Attributes);
83 if (FoundObject == NULL)
84 {
85 Status = CmiScanForSubKey(ParsedKey->RegistryHive,
86 ParsedKey->KeyCell,
87 &SubKeyCell,
88 &BlockOffset,
89 &KeyName,
90 0,
91 Attributes);
92 if (!NT_SUCCESS(Status) || (SubKeyCell == NULL))
93 {
94 RtlFreeUnicodeString(&KeyName);
95 return(STATUS_UNSUCCESSFUL);
96 }
97
98 if ((SubKeyCell->Flags & REG_KEY_LINK_CELL) &&
99 !((Attributes & OBJ_OPENLINK) && (EndPtr == NULL)))
100 {
101 RtlInitUnicodeString(&LinkPath, NULL);
102 Status = CmiGetLinkTarget(ParsedKey->RegistryHive,
103 SubKeyCell,
104 &LinkPath);
105 if (NT_SUCCESS(Status))
106 {
107 DPRINT("LinkPath '%wZ'\n", &LinkPath);
108
109 /* build new FullPath for reparsing */
110 TargetPath.MaximumLength = LinkPath.MaximumLength;
111 if (EndPtr != NULL)
112 {
113 TargetPath.MaximumLength += (wcslen(EndPtr) * sizeof(WCHAR));
114 }
115 TargetPath.Length = TargetPath.MaximumLength - sizeof(WCHAR);
116 TargetPath.Buffer = ExAllocatePool(NonPagedPool,
117 TargetPath.MaximumLength);
118 wcscpy(TargetPath.Buffer, LinkPath.Buffer);
119 if (EndPtr != NULL)
120 {
121 wcscat(TargetPath.Buffer, EndPtr);
122 }
123
124 RtlFreeUnicodeString(FullPath);
125 RtlFreeUnicodeString(&LinkPath);
126 FullPath->Length = TargetPath.Length;
127 FullPath->MaximumLength = TargetPath.MaximumLength;
128 FullPath->Buffer = TargetPath.Buffer;
129
130 DPRINT("FullPath '%wZ'\n", FullPath);
131
132 /* reinitialize Path for reparsing */
133 *Path = FullPath->Buffer;
134
135 *NextObject = NULL;
136
137 RtlFreeUnicodeString(&KeyName);
138 return(STATUS_REPARSE);
139 }
140 }
141
142 /* Create new key object and put into linked list */
143 DPRINT("CmiObjectParse: %s\n", Path);
144 Status = ObCreateObject(KernelMode,
145 CmiKeyType,
146 NULL,
147 KernelMode,
148 NULL,
149 sizeof(KEY_OBJECT),
150 0,
151 0,
152 (PVOID*)&FoundObject);
153 if (!NT_SUCCESS(Status))
154 {
155 RtlFreeUnicodeString(&KeyName);
156 return(Status);
157 }
158
159 FoundObject->Flags = 0;
160 FoundObject->KeyCell = SubKeyCell;
161 FoundObject->KeyCellOffset = BlockOffset;
162 FoundObject->RegistryHive = ParsedKey->RegistryHive;
163 RtlpCreateUnicodeString(&FoundObject->Name,
164 KeyName.Buffer, NonPagedPool);
165 CmiAddKeyToList(ParsedKey, FoundObject);
166 DPRINT("Created object 0x%x\n", FoundObject);
167 }
168 else
169 {
170 if ((FoundObject->KeyCell->Flags & REG_KEY_LINK_CELL) &&
171 !((Attributes & OBJ_OPENLINK) && (EndPtr == NULL)))
172 {
173 DPRINT("Found link\n");
174
175 RtlInitUnicodeString(&LinkPath, NULL);
176 Status = CmiGetLinkTarget(FoundObject->RegistryHive,
177 FoundObject->KeyCell,
178 &LinkPath);
179 if (NT_SUCCESS(Status))
180 {
181 DPRINT("LinkPath '%wZ'\n", &LinkPath);
182
183 /* build new FullPath for reparsing */
184 TargetPath.MaximumLength = LinkPath.MaximumLength;
185 if (EndPtr != NULL)
186 {
187 TargetPath.MaximumLength += (wcslen(EndPtr) * sizeof(WCHAR));
188 }
189 TargetPath.Length = TargetPath.MaximumLength - sizeof(WCHAR);
190 TargetPath.Buffer = ExAllocatePool(NonPagedPool,
191 TargetPath.MaximumLength);
192 wcscpy(TargetPath.Buffer, LinkPath.Buffer);
193 if (EndPtr != NULL)
194 {
195 wcscat(TargetPath.Buffer, EndPtr);
196 }
197
198 RtlFreeUnicodeString(FullPath);
199 RtlFreeUnicodeString(&LinkPath);
200 FullPath->Length = TargetPath.Length;
201 FullPath->MaximumLength = TargetPath.MaximumLength;
202 FullPath->Buffer = TargetPath.Buffer;
203
204 DPRINT("FullPath '%wZ'\n", FullPath);
205
206 /* reinitialize Path for reparsing */
207 *Path = FullPath->Buffer;
208
209 *NextObject = NULL;
210
211 RtlFreeUnicodeString(&KeyName);
212 return(STATUS_REPARSE);
213 }
214 }
215
216 ObReferenceObjectByPointer(FoundObject,
217 STANDARD_RIGHTS_REQUIRED,
218 NULL,
219 UserMode);
220 }
221
222 DPRINT("CmiObjectParse: %s\n", FoundObject->Name);
223
224 *Path = EndPtr;
225
226 VERIFY_KEY_OBJECT(FoundObject);
227
228 *NextObject = FoundObject;
229
230 RtlFreeUnicodeString(&KeyName);
231
232 return(STATUS_SUCCESS);
233 }
234
235
236 NTSTATUS STDCALL
237 CmiObjectCreate(PVOID ObjectBody,
238 PVOID Parent,
239 PWSTR RemainingPath,
240 POBJECT_ATTRIBUTES ObjectAttributes)
241 {
242 PKEY_OBJECT KeyObject = ObjectBody;
243 PWSTR Start;
244
245 KeyObject->ParentKey = Parent;
246 if (RemainingPath)
247 {
248 Start = RemainingPath;
249 if(*Start == L'\\')
250 Start++;
251 RtlpCreateUnicodeString(&KeyObject->Name,
252 Start, NonPagedPool);
253 }
254 else
255 {
256 RtlInitUnicodeString(&KeyObject->Name,
257 NULL);
258 }
259
260 return STATUS_SUCCESS;
261 }
262
263
264 VOID STDCALL
265 CmiObjectDelete(PVOID DeletedObject)
266 {
267 PKEY_OBJECT ParentKeyObject;
268 PKEY_OBJECT KeyObject;
269
270 DPRINT("Delete key object (%p)\n", DeletedObject);
271
272 KeyObject = (PKEY_OBJECT) DeletedObject;
273 ParentKeyObject = KeyObject->ParentKey;
274
275 ObReferenceObject (ParentKeyObject);
276
277 if (!NT_SUCCESS(CmiRemoveKeyFromList(KeyObject)))
278 {
279 DPRINT1("Key not found in parent list ???\n");
280 }
281
282 RtlFreeUnicodeString(&KeyObject->Name);
283
284 if (KeyObject->Flags & KO_MARKED_FOR_DELETE)
285 {
286 DPRINT("delete really key\n");
287
288 CmiRemoveSubKey(KeyObject->RegistryHive,
289 ParentKeyObject,
290 KeyObject);
291
292 KeQuerySystemTime (&ParentKeyObject->KeyCell->LastWriteTime);
293 CmiMarkBlockDirty (ParentKeyObject->RegistryHive,
294 ParentKeyObject->KeyCellOffset);
295
296 if (!IsNoFileHive (KeyObject->RegistryHive) ||
297 !IsNoFileHive (ParentKeyObject->RegistryHive))
298 {
299 CmiSyncHives ();
300 }
301 }
302
303 ObDereferenceObject (ParentKeyObject);
304
305 if (KeyObject->NumberOfSubKeys)
306 {
307 KEBUGCHECK(REGISTRY_ERROR);
308 }
309
310 if (KeyObject->SizeOfSubKeys)
311 {
312 ExFreePool(KeyObject->SubKeys);
313 }
314 }
315
316
317 static NTSTATUS
318 CmiQuerySecurityDescriptor(PKEY_OBJECT KeyObject,
319 SECURITY_INFORMATION SecurityInformation,
320 PSECURITY_DESCRIPTOR SecurityDescriptor,
321 PULONG BufferLength)
322 {
323 ULONG_PTR Current;
324 ULONG SidSize;
325 ULONG SdSize;
326 NTSTATUS Status;
327
328 DPRINT("CmiQuerySecurityDescriptor() called\n");
329
330 /*
331 * FIXME:
332 * This is a big hack!!
333 * We need to retrieve the security descriptor from the keys security cell!
334 */
335
336 if (SecurityInformation == 0)
337 {
338 return STATUS_ACCESS_DENIED;
339 }
340
341 SidSize = RtlLengthSid(SeWorldSid);
342 SdSize = sizeof(SECURITY_DESCRIPTOR) + (2 * SidSize);
343
344 if (*BufferLength < SdSize)
345 {
346 *BufferLength = SdSize;
347 return STATUS_BUFFER_TOO_SMALL;
348 }
349
350 *BufferLength = SdSize;
351
352 Status = RtlCreateSecurityDescriptor(SecurityDescriptor,
353 SECURITY_DESCRIPTOR_REVISION);
354 if (!NT_SUCCESS(Status))
355 {
356 return Status;
357 }
358
359 SecurityDescriptor->Control |= SE_SELF_RELATIVE;
360 Current = (ULONG_PTR)SecurityDescriptor + sizeof(SECURITY_DESCRIPTOR);
361
362 if (SecurityInformation & OWNER_SECURITY_INFORMATION)
363 {
364 RtlCopyMemory((PVOID)Current,
365 SeWorldSid,
366 SidSize);
367 SecurityDescriptor->Owner = (PSID)((ULONG_PTR)Current - (ULONG_PTR)SecurityDescriptor);
368 Current += SidSize;
369 }
370
371 if (SecurityInformation & GROUP_SECURITY_INFORMATION)
372 {
373 RtlCopyMemory((PVOID)Current,
374 SeWorldSid,
375 SidSize);
376 SecurityDescriptor->Group = (PSID)((ULONG_PTR)Current - (ULONG_PTR)SecurityDescriptor);
377 Current += SidSize;
378 }
379
380 if (SecurityInformation & DACL_SECURITY_INFORMATION)
381 {
382 SecurityDescriptor->Control |= SE_DACL_PRESENT;
383 }
384
385 if (SecurityInformation & SACL_SECURITY_INFORMATION)
386 {
387 SecurityDescriptor->Control |= SE_SACL_PRESENT;
388 }
389
390 return STATUS_SUCCESS;
391 }
392
393
394 static NTSTATUS
395 CmiAssignSecurityDescriptor(PKEY_OBJECT KeyObject,
396 PSECURITY_DESCRIPTOR SecurityDescriptor)
397 {
398 #if 0
399 PREGISTRY_HIVE Hive;
400
401 DPRINT1("CmiAssignSecurityDescriptor() callled\n");
402
403 DPRINT1("KeyObject %p\n", KeyObject);
404 DPRINT1("KeyObject->RegistryHive %p\n", KeyObject->RegistryHive);
405
406 Hive = KeyObject->RegistryHive;
407 if (Hive == NULL)
408 {
409 DPRINT1("Create new root security cell\n");
410 return STATUS_SUCCESS;
411 }
412
413 if (Hive->RootSecurityCell == NULL)
414 {
415 DPRINT1("Create new root security cell\n");
416
417 }
418 else
419 {
420 DPRINT1("Search for security cell\n");
421
422 }
423 #endif
424
425 return STATUS_SUCCESS;
426 }
427
428
429 NTSTATUS STDCALL
430 CmiObjectSecurity(PVOID ObjectBody,
431 SECURITY_OPERATION_CODE OperationCode,
432 SECURITY_INFORMATION SecurityInformation,
433 PSECURITY_DESCRIPTOR SecurityDescriptor,
434 PULONG BufferLength)
435 {
436 DPRINT("CmiObjectSecurity() called\n");
437
438 switch (OperationCode)
439 {
440 case SetSecurityDescriptor:
441 DPRINT("Set security descriptor\n");
442 return STATUS_SUCCESS;
443
444 case QuerySecurityDescriptor:
445 DPRINT("Query security descriptor\n");
446 return CmiQuerySecurityDescriptor((PKEY_OBJECT)ObjectBody,
447 SecurityInformation,
448 SecurityDescriptor,
449 BufferLength);
450
451 case DeleteSecurityDescriptor:
452 DPRINT("Delete security descriptor\n");
453 return STATUS_SUCCESS;
454
455 case AssignSecurityDescriptor:
456 DPRINT("Assign security descriptor\n");
457 return CmiAssignSecurityDescriptor((PKEY_OBJECT)ObjectBody,
458 SecurityDescriptor);
459 }
460
461 return STATUS_UNSUCCESSFUL;
462 }
463
464
465 NTSTATUS STDCALL
466 CmiObjectQueryName (PVOID ObjectBody,
467 POBJECT_NAME_INFORMATION ObjectNameInfo,
468 ULONG Length,
469 PULONG ReturnLength)
470 {
471 POBJECT_NAME_INFORMATION LocalInfo;
472 PKEY_OBJECT KeyObject;
473 ULONG LocalReturnLength;
474 NTSTATUS Status;
475
476 DPRINT ("CmiObjectQueryName() called\n");
477
478 KeyObject = (PKEY_OBJECT)ObjectBody;
479
480 LocalInfo = ExAllocatePool (NonPagedPool,
481 sizeof(OBJECT_NAME_INFORMATION) +
482 MAX_PATH * sizeof(WCHAR));
483 if (LocalInfo == NULL)
484 return STATUS_INSUFFICIENT_RESOURCES;
485
486 if (KeyObject->ParentKey != KeyObject)
487 {
488 Status = ObQueryNameString (KeyObject->ParentKey,
489 LocalInfo,
490 MAX_PATH * sizeof(WCHAR),
491 &LocalReturnLength);
492 }
493 else
494 {
495 /* KeyObject is the root key */
496 Status = ObQueryNameString (BODY_TO_HEADER(KeyObject)->Parent,
497 LocalInfo,
498 MAX_PATH * sizeof(WCHAR),
499 &LocalReturnLength);
500 }
501
502 if (!NT_SUCCESS (Status))
503 {
504 ExFreePool (LocalInfo);
505 return Status;
506 }
507 DPRINT ("Parent path: %wZ\n", &LocalInfo->Name);
508
509 Status = RtlAppendUnicodeStringToString (&ObjectNameInfo->Name,
510 &LocalInfo->Name);
511 ExFreePool (LocalInfo);
512 if (!NT_SUCCESS (Status))
513 return Status;
514
515 Status = RtlAppendUnicodeToString (&ObjectNameInfo->Name,
516 L"\\");
517 if (!NT_SUCCESS (Status))
518 return Status;
519
520 Status = RtlAppendUnicodeStringToString (&ObjectNameInfo->Name,
521 &KeyObject->Name);
522 if (NT_SUCCESS (Status))
523 {
524 DPRINT ("Total path: %wZ\n", &ObjectNameInfo->Name);
525 }
526
527 return Status;
528 }
529
530
531 VOID
532 CmiAddKeyToList(PKEY_OBJECT ParentKey,
533 PKEY_OBJECT NewKey)
534 {
535 KIRQL OldIrql;
536
537 DPRINT("ParentKey %.08x\n", ParentKey);
538
539 KeAcquireSpinLock(&CmiKeyListLock, &OldIrql);
540
541 if (ParentKey->SizeOfSubKeys <= ParentKey->NumberOfSubKeys)
542 {
543 PKEY_OBJECT *tmpSubKeys = ExAllocatePool(NonPagedPool,
544 (ParentKey->NumberOfSubKeys + 1) * sizeof(ULONG));
545
546 if (ParentKey->NumberOfSubKeys > 0)
547 {
548 RtlCopyMemory (tmpSubKeys,
549 ParentKey->SubKeys,
550 ParentKey->NumberOfSubKeys * sizeof(ULONG));
551 }
552
553 if (ParentKey->SubKeys)
554 ExFreePool(ParentKey->SubKeys);
555
556 ParentKey->SubKeys = tmpSubKeys;
557 ParentKey->SizeOfSubKeys = ParentKey->NumberOfSubKeys + 1;
558 }
559
560 /* FIXME: Please maintain the list in alphabetic order */
561 /* to allow a dichotomic search */
562 ParentKey->SubKeys[ParentKey->NumberOfSubKeys++] = NewKey;
563
564 DPRINT("Reference parent key: 0x%x\n", ParentKey);
565
566 ObReferenceObjectByPointer(ParentKey,
567 STANDARD_RIGHTS_REQUIRED,
568 NULL,
569 UserMode);
570 NewKey->ParentKey = ParentKey;
571 KeReleaseSpinLock(&CmiKeyListLock, OldIrql);
572 }
573
574
575 NTSTATUS
576 CmiRemoveKeyFromList(PKEY_OBJECT KeyToRemove)
577 {
578 PKEY_OBJECT ParentKey;
579 KIRQL OldIrql;
580 DWORD Index;
581
582 ParentKey = KeyToRemove->ParentKey;
583 KeAcquireSpinLock(&CmiKeyListLock, &OldIrql);
584 /* FIXME: If list maintained in alphabetic order, use dichotomic search */
585 for (Index = 0; Index < ParentKey->NumberOfSubKeys; Index++)
586 {
587 if (ParentKey->SubKeys[Index] == KeyToRemove)
588 {
589 if (Index < ParentKey->NumberOfSubKeys-1)
590 RtlMoveMemory(&ParentKey->SubKeys[Index],
591 &ParentKey->SubKeys[Index + 1],
592 (ParentKey->NumberOfSubKeys - Index - 1) * sizeof(PKEY_OBJECT));
593 ParentKey->NumberOfSubKeys--;
594 KeReleaseSpinLock(&CmiKeyListLock, OldIrql);
595
596 DPRINT("Dereference parent key: 0x%x\n", ParentKey);
597
598 ObDereferenceObject(ParentKey);
599 return STATUS_SUCCESS;
600 }
601 }
602 KeReleaseSpinLock(&CmiKeyListLock, OldIrql);
603
604 return STATUS_UNSUCCESSFUL;
605 }
606
607
608 PKEY_OBJECT
609 CmiScanKeyList(PKEY_OBJECT Parent,
610 PUNICODE_STRING KeyName,
611 ULONG Attributes)
612 {
613 PKEY_OBJECT CurKey;
614 KIRQL OldIrql;
615 ULONG Index;
616
617 DPRINT("Scanning key list for: %wZ (Parent: %wZ)\n",
618 KeyName, &Parent->Name);
619
620 KeAcquireSpinLock(&CmiKeyListLock, &OldIrql);
621 /* FIXME: if list maintained in alphabetic order, use dichotomic search */
622 for (Index=0; Index < Parent->NumberOfSubKeys; Index++)
623 {
624 CurKey = Parent->SubKeys[Index];
625 if (Attributes & OBJ_CASE_INSENSITIVE)
626 {
627 if ((KeyName->Length == CurKey->Name.Length)
628 && (_wcsicmp(KeyName->Buffer, CurKey->Name.Buffer) == 0))
629 {
630 KeReleaseSpinLock(&CmiKeyListLock, OldIrql);
631 return CurKey;
632 }
633 }
634 else
635 {
636 if ((KeyName->Length == CurKey->Name.Length)
637 && (wcscmp(KeyName->Buffer, CurKey->Name.Buffer) == 0))
638 {
639 KeReleaseSpinLock(&CmiKeyListLock, OldIrql);
640 return CurKey;
641 }
642 }
643 }
644 KeReleaseSpinLock(&CmiKeyListLock, OldIrql);
645
646 return NULL;
647 }
648
649
650 static NTSTATUS
651 CmiGetLinkTarget(PREGISTRY_HIVE RegistryHive,
652 PKEY_CELL KeyCell,
653 PUNICODE_STRING TargetPath)
654 {
655 UNICODE_STRING LinkName = ROS_STRING_INITIALIZER(L"SymbolicLinkValue");
656 PVALUE_CELL ValueCell;
657 PDATA_CELL DataCell;
658 NTSTATUS Status;
659
660 DPRINT("CmiGetLinkTarget() called\n");
661
662 /* Get Value block of interest */
663 Status = CmiScanKeyForValue(RegistryHive,
664 KeyCell,
665 &LinkName,
666 &ValueCell,
667 NULL);
668 if (!NT_SUCCESS(Status))
669 {
670 DPRINT1("CmiScanKeyForValue() failed (Status %lx)\n", Status);
671 return(Status);
672 }
673
674 if (ValueCell->DataType != REG_LINK)
675 {
676 DPRINT1("Type != REG_LINK\n!");
677 return(STATUS_UNSUCCESSFUL);
678 }
679
680 if (TargetPath->Buffer == NULL && TargetPath->MaximumLength == 0)
681 {
682 TargetPath->Length = 0;
683 TargetPath->MaximumLength = ValueCell->DataSize + sizeof(WCHAR);
684 TargetPath->Buffer = ExAllocatePool(NonPagedPool,
685 TargetPath->MaximumLength);
686 }
687
688 TargetPath->Length = min(TargetPath->MaximumLength - sizeof(WCHAR),
689 (ULONG) ValueCell->DataSize);
690
691 if (ValueCell->DataSize > 0)
692 {
693 DataCell = CmiGetCell (RegistryHive, ValueCell->DataOffset, NULL);
694 RtlCopyMemory(TargetPath->Buffer,
695 DataCell->Data,
696 TargetPath->Length);
697 TargetPath->Buffer[TargetPath->Length / sizeof(WCHAR)] = 0;
698 }
699 else
700 {
701 RtlCopyMemory(TargetPath->Buffer,
702 &ValueCell->DataOffset,
703 TargetPath->Length);
704 TargetPath->Buffer[TargetPath->Length / sizeof(WCHAR)] = 0;
705 }
706
707 DPRINT("TargetPath '%wZ'\n", TargetPath);
708
709 return(STATUS_SUCCESS);
710 }
711
712 /* EOF */