Implemented the import of default registry settings from .inf files.
[reactos.git] / reactos / subsys / system / usetup / registry.c
1 /*
2 * ReactOS kernel
3 * Copyright (C) 2003 ReactOS Team
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18 */
19 /*
20 * COPYRIGHT: See COPYING in the top level directory
21 * PROJECT: ReactOS text-mode setup
22 * FILE: subsys/system/usetup/registry.c
23 * PURPOSE: Registry creation functions
24 * PROGRAMMER: Eric Kohl
25 */
26
27 /* INCLUDES *****************************************************************/
28
29 #include <ddk/ntddk.h>
30 #include <ntdll/rtl.h>
31
32 #include "usetup.h"
33 #include "registry.h"
34 #include "infcache.h"
35
36
37
38 #define FLG_ADDREG_DELREG_BIT 0x00008000
39 #define FLG_ADDREG_BINVALUETYPE 0x00000001
40 #define FLG_ADDREG_NOCLOBBER 0x00000002
41 #define FLG_ADDREG_DELVAL 0x00000004
42 #define FLG_ADDREG_APPEND 0x00000008
43 #define FLG_ADDREG_KEYONLY 0x00000010
44 #define FLG_ADDREG_OVERWRITEONLY 0x00000020
45 #define FLG_ADDREG_64BITKEY 0x00001000
46 #define FLG_ADDREG_KEYONLY_COMMON 0x00002000
47 #define FLG_ADDREG_32BITKEY 0x00004000
48 #define FLG_ADDREG_TYPE_SZ 0x00000000
49 #define FLG_ADDREG_TYPE_MULTI_SZ 0x00010000
50 #define FLG_ADDREG_TYPE_EXPAND_SZ 0x00020000
51 #define FLG_ADDREG_TYPE_BINARY (0x00000000 | FLG_ADDREG_BINVALUETYPE)
52 #define FLG_ADDREG_TYPE_DWORD (0x00010000 | FLG_ADDREG_BINVALUETYPE)
53 #define FLG_ADDREG_TYPE_NONE (0x00020000 | FLG_ADDREG_BINVALUETYPE)
54 #define FLG_ADDREG_TYPE_MASK (0xFFFF0000 | FLG_ADDREG_BINVALUETYPE)
55
56 #define FLG_DELREG_VALUE (0x00000000)
57 #define FLG_DELREG_TYPE_MASK FLG_ADDREG_TYPE_MASK
58 #define FLG_DELREG_TYPE_SZ FLG_ADDREG_TYPE_SZ
59 #define FLG_DELREG_TYPE_MULTI_SZ FLG_ADDREG_TYPE_MULTI_SZ
60 #define FLG_DELREG_TYPE_EXPAND_SZ FLG_ADDREG_TYPE_EXPAND_SZ
61 #define FLG_DELREG_TYPE_BINARY FLG_ADDREG_TYPE_BINARY
62 #define FLG_DELREG_TYPE_DWORD FLG_ADDREG_TYPE_DWORD
63 #define FLG_DELREG_TYPE_NONE FLG_ADDREG_TYPE_NONE
64 #define FLG_DELREG_64BITKEY FLG_ADDREG_64BITKEY
65 #define FLG_DELREG_KEYONLY_COMMON FLG_ADDREG_KEYONLY_COMMON
66 #define FLG_DELREG_32BITKEY FLG_ADDREG_32BITKEY
67 #define FLG_DELREG_OPERATION_MASK (0x000000FE)
68 #define FLG_DELREG_MULTI_SZ_DELSTRING (FLG_DELREG_TYPE_MULTI_SZ | FLG_ADDREG_DELREG_BIT | 0x00000002)
69
70
71 /* FUNCTIONS ****************************************************************/
72
73
74 static BOOLEAN
75 GetRootKey (PWCHAR Name)
76 {
77 if (!_wcsicmp (Name, L"HKCR"))
78 {
79 wcscpy (Name, L"\\Registry\\Machine\\SOFTWARE\\Classes\\");
80 return TRUE;
81 }
82
83 if (!_wcsicmp (Name, L"HKCU"))
84 {
85 wcscpy (Name, L"\\Registry\\User\\.DEFAULT\\");
86 return TRUE;
87 }
88
89 if (!_wcsicmp (Name, L"HKLM"))
90 {
91 wcscpy (Name, L"\\Registry\\Machine\\");
92 return TRUE;
93 }
94
95 if (!_wcsicmp (Name, L"HKU"))
96 {
97 wcscpy (Name, L"\\Registry\\User\\");
98 return TRUE;
99 }
100
101 #if 0
102 if (!_wcsicmp (Name, L"HKR"))
103 return FALSE;
104 #endif
105
106 return FALSE;
107 }
108
109
110 /***********************************************************************
111 * append_multi_sz_value
112 *
113 * Append a multisz string to a multisz registry value.
114 */
115 #if 0
116 static void
117 append_multi_sz_value (HANDLE hkey,
118 const WCHAR *value,
119 const WCHAR *strings,
120 DWORD str_size )
121 {
122 DWORD size, type, total;
123 WCHAR *buffer, *p;
124
125 if (RegQueryValueExW( hkey, value, NULL, &type, NULL, &size )) return;
126 if (type != REG_MULTI_SZ) return;
127
128 if (!(buffer = HeapAlloc( GetProcessHeap(), 0, (size + str_size) * sizeof(WCHAR) ))) return;
129 if (RegQueryValueExW( hkey, value, NULL, NULL, (BYTE *)buffer, &size )) goto done;
130
131 /* compare each string against all the existing ones */
132 total = size;
133 while (*strings)
134 {
135 int len = strlenW(strings) + 1;
136
137 for (p = buffer; *p; p += strlenW(p) + 1)
138 if (!strcmpiW( p, strings )) break;
139
140 if (!*p) /* not found, need to append it */
141 {
142 memcpy( p, strings, len * sizeof(WCHAR) );
143 p[len] = 0;
144 total += len;
145 }
146 strings += len;
147 }
148 if (total != size)
149 {
150 TRACE( "setting value %s to %s\n", debugstr_w(value), debugstr_w(buffer) );
151 RegSetValueExW( hkey, value, 0, REG_MULTI_SZ, (BYTE *)buffer, total );
152 }
153 done:
154 HeapFree( GetProcessHeap(), 0, buffer );
155 }
156 #endif
157
158 /***********************************************************************
159 * delete_multi_sz_value
160 *
161 * Remove a string from a multisz registry value.
162 */
163 #if 0
164 static void delete_multi_sz_value( HKEY hkey, const WCHAR *value, const WCHAR *string )
165 {
166 DWORD size, type;
167 WCHAR *buffer, *src, *dst;
168
169 if (RegQueryValueExW( hkey, value, NULL, &type, NULL, &size )) return;
170 if (type != REG_MULTI_SZ) return;
171 /* allocate double the size, one for value before and one for after */
172 if (!(buffer = HeapAlloc( GetProcessHeap(), 0, size * 2 * sizeof(WCHAR) ))) return;
173 if (RegQueryValueExW( hkey, value, NULL, NULL, (BYTE *)buffer, &size )) goto done;
174 src = buffer;
175 dst = buffer + size;
176 while (*src)
177 {
178 int len = strlenW(src) + 1;
179 if (strcmpiW( src, string ))
180 {
181 memcpy( dst, src, len * sizeof(WCHAR) );
182 dst += len;
183 }
184 src += len;
185 }
186 *dst++ = 0;
187 if (dst != buffer + 2*size) /* did we remove something? */
188 {
189 TRACE( "setting value %s to %s\n", debugstr_w(value), debugstr_w(buffer + size) );
190 RegSetValueExW( hkey, value, 0, REG_MULTI_SZ,
191 (BYTE *)(buffer + size), dst - (buffer + size) );
192 }
193 done:
194 HeapFree( GetProcessHeap(), 0, buffer );
195 }
196 #endif
197
198 /***********************************************************************
199 * do_reg_operation
200 *
201 * Perform an add/delete registry operation depending on the flags.
202 */
203 static BOOLEAN
204 do_reg_operation(HANDLE KeyHandle,
205 PUNICODE_STRING ValueName,
206 PINFCONTEXT Context,
207 ULONG Flags)
208 {
209 WCHAR EmptyStr = (WCHAR)0;
210 ULONG Type;
211 ULONG Size;
212
213 if (Flags & (FLG_ADDREG_DELREG_BIT | FLG_ADDREG_DELVAL)) /* deletion */
214 {
215 #if 0
216 if (ValueName && !(flags & FLG_DELREG_KEYONLY_COMMON))
217 {
218 if ((Flags & FLG_DELREG_MULTI_SZ_DELSTRING) == FLG_DELREG_MULTI_SZ_DELSTRING)
219 {
220 WCHAR *str;
221
222 if (!SetupGetStringFieldW( context, 5, NULL, 0, &size ) || !size) return TRUE;
223 if (!(str = HeapAlloc( GetProcessHeap(), 0, size * sizeof(WCHAR) ))) return FALSE;
224 SetupGetStringFieldW( context, 5, str, size, NULL );
225 delete_multi_sz_value( hkey, value, str );
226 HeapFree( GetProcessHeap(), 0, str );
227 }
228 else
229 {
230 RegDeleteValueW( hkey, value );
231 }
232 }
233 else
234 {
235 RegDeleteKeyW( hkey, NULL );
236 }
237 #endif
238 return TRUE;
239 }
240
241 if (Flags & (FLG_ADDREG_KEYONLY | FLG_ADDREG_KEYONLY_COMMON))
242 return TRUE;
243
244 #if 0
245 if (Flags & (FLG_ADDREG_NOCLOBBER | FLG_ADDREG_OVERWRITEONLY))
246 {
247 BOOL exists = !RegQueryValueExW( hkey, value, NULL, NULL, NULL, NULL );
248 if (exists && (flags & FLG_ADDREG_NOCLOBBER))
249 return TRUE;
250 if (!exists & (flags & FLG_ADDREG_OVERWRITEONLY))
251 return TRUE;
252 }
253 #endif
254
255 switch (Flags & FLG_ADDREG_TYPE_MASK)
256 {
257 case FLG_ADDREG_TYPE_SZ:
258 Type = REG_SZ;
259 break;
260
261 case FLG_ADDREG_TYPE_MULTI_SZ:
262 Type = REG_MULTI_SZ;
263 break;
264
265 case FLG_ADDREG_TYPE_EXPAND_SZ:
266 Type = REG_EXPAND_SZ;
267 break;
268
269 case FLG_ADDREG_TYPE_BINARY:
270 Type = REG_BINARY;
271 break;
272
273 case FLG_ADDREG_TYPE_DWORD:
274 Type = REG_DWORD;
275 break;
276
277 case FLG_ADDREG_TYPE_NONE:
278 Type = REG_NONE;
279 break;
280
281 default:
282 Type = Flags >> 16;
283 break;
284 }
285
286 if (!(Flags & FLG_ADDREG_BINVALUETYPE) ||
287 (Type == REG_DWORD && InfGetFieldCount (Context) == 5))
288 {
289 PWCHAR Str = NULL;
290
291 if (Type == REG_MULTI_SZ)
292 {
293 if (!InfGetMultiSzField (Context, 5, NULL, 0, &Size))
294 Size = 0;
295
296 if (Size)
297 {
298 Str = RtlAllocateHeap (ProcessHeap, 0, Size * sizeof(WCHAR));
299 if (Str == NULL)
300 return FALSE;
301
302 InfGetMultiSzField (Context, 5, Str, Size, NULL);
303 }
304
305 if (Flags & FLG_ADDREG_APPEND)
306 {
307 if (Str == NULL)
308 return TRUE;
309
310 // append_multi_sz_value( hkey, value, str, size );
311
312 RtlFreeHeap (ProcessHeap, 0, Str);
313 return TRUE;
314 }
315 /* else fall through to normal string handling */
316 }
317 else
318 {
319 if (!InfGetStringField (Context, 5, NULL, 0, &Size))
320 Size = 0;
321
322 if (Size)
323 {
324 Str = RtlAllocateHeap (ProcessHeap, 0, Size * sizeof(WCHAR));
325 if (Str == NULL)
326 return FALSE;
327
328 InfGetStringField (Context, 5, Str, Size, NULL);
329 }
330 }
331
332 if (Type == REG_DWORD)
333 {
334 ULONG dw = Str ? wcstol (Str, NULL, 16) : 0;
335
336 DPRINT1("setting dword %wZ to %lx\n", &ValueName, dw);
337
338 NtSetValueKey (KeyHandle,
339 ValueName,
340 0,
341 Type,
342 (PVOID)&dw,
343 (ULONG)sizeof(dw));
344 }
345 else
346 {
347 DPRINT1("setting value %wZ to %S\n", ValueName, Str);
348 if (Str)
349 {
350 NtSetValueKey (KeyHandle,
351 ValueName,
352 0,
353 Type,
354 (PVOID)Str,
355 Size * sizeof(WCHAR));
356 }
357 else
358 {
359 NtSetValueKey (KeyHandle,
360 ValueName,
361 0,
362 Type,
363 (PVOID)&EmptyStr,
364 sizeof(WCHAR));
365 }
366 }
367 RtlFreeHeap (ProcessHeap, 0, Str);
368 }
369 else /* get the binary data */
370 {
371 PUCHAR Data = NULL;
372
373 if (!InfGetBinaryField (Context, 5, NULL, 0, &Size))
374 Size = 0;
375
376 if (Size)
377 {
378 Data = RtlAllocateHeap (ProcessHeap, 0, Size);
379 if (Data == NULL)
380 return FALSE;
381
382 DPRINT1("setting binary data %wZ len %lu\n", ValueName, Size);
383 InfGetBinaryField (Context, 5, Data, Size, NULL);
384 }
385
386 NtSetValueKey (KeyHandle,
387 ValueName,
388 0,
389 Type,
390 (PVOID)Data,
391 Size);
392
393 RtlFreeHeap (ProcessHeap, 0, Data);
394
395 return TRUE;
396 }
397
398 return TRUE;
399 }
400
401
402 NTSTATUS
403 CreateNestedKey (PHANDLE KeyHandle,
404 ACCESS_MASK DesiredAccess,
405 POBJECT_ATTRIBUTES ObjectAttributes)
406 {
407 OBJECT_ATTRIBUTES LocalObjectAttributes;
408 UNICODE_STRING LocalKeyName;
409 ULONG Disposition;
410 NTSTATUS Status;
411 ULONG FullNameLength;
412 ULONG Length;
413 PWCHAR Ptr;
414 HANDLE LocalKeyHandle;
415
416 Status = NtCreateKey (KeyHandle,
417 KEY_ALL_ACCESS,
418 ObjectAttributes,
419 0,
420 NULL,
421 0,
422 &Disposition);
423 DPRINT("NtCreateKey(%wZ) called (Status %lx)\n", ObjectAttributes->ObjectName, Status);
424 if (Status != STATUS_OBJECT_NAME_NOT_FOUND)
425 return Status;
426
427 /* Copy object attributes */
428 RtlCopyMemory (&LocalObjectAttributes,
429 ObjectAttributes,
430 sizeof(OBJECT_ATTRIBUTES));
431 RtlCreateUnicodeString (&LocalKeyName,
432 ObjectAttributes->ObjectName->Buffer);
433 LocalObjectAttributes.ObjectName = &LocalKeyName;
434 FullNameLength = LocalKeyName.Length / sizeof(WCHAR);
435
436 /* Remove the last part of the key name and try to create the key again. */
437 while (Status == STATUS_OBJECT_NAME_NOT_FOUND)
438 {
439 Ptr = wcsrchr (LocalKeyName.Buffer, '\\');
440 if (Ptr == NULL || Ptr == LocalKeyName.Buffer)
441 {
442 Status = STATUS_UNSUCCESSFUL;
443 break;
444 }
445 *Ptr = (WCHAR)0;
446 LocalKeyName.Length = wcslen (LocalKeyName.Buffer) * sizeof(WCHAR);
447
448 Status = NtCreateKey (&LocalKeyHandle,
449 KEY_ALL_ACCESS,
450 &LocalObjectAttributes,
451 0,
452 NULL,
453 0,
454 &Disposition);
455 DPRINT("NtCreateKey(%wZ) called (Status %lx)\n", &LocalKeyName, Status);
456 }
457
458 if (!NT_SUCCESS(Status))
459 {
460 RtlFreeUnicodeString (&LocalKeyName);
461 return Status;
462 }
463
464 /* Add removed parts of the key name and create them too. */
465 Length = wcslen (LocalKeyName.Buffer);
466 while (TRUE)
467 {
468 if (Length == FullNameLength)
469 {
470 Status == STATUS_SUCCESS;
471 *KeyHandle = LocalKeyHandle;
472 break;
473 }
474 NtClose (LocalKeyHandle);
475
476 LocalKeyName.Buffer[Length] = L'\\';
477 Length = wcslen (LocalKeyName.Buffer);
478 LocalKeyName.Length = Length * sizeof(WCHAR);
479
480 Status = NtCreateKey (&LocalKeyHandle,
481 KEY_ALL_ACCESS,
482 &LocalObjectAttributes,
483 0,
484 NULL,
485 0,
486 &Disposition);
487 DPRINT("NtCreateKey(%wZ) called (Status %lx)\n", &LocalKeyName, Status);
488 if (!NT_SUCCESS(Status))
489 break;
490 }
491
492 RtlFreeUnicodeString (&LocalKeyName);
493
494 return Status;
495 }
496
497
498 /***********************************************************************
499 * registry_callback
500 *
501 * Called once for each AddReg and DelReg entry in a given section.
502 */
503 static BOOLEAN
504 registry_callback (HINF hInf, PCWSTR Section, BOOLEAN Delete)
505 {
506 OBJECT_ATTRIBUTES ObjectAttributes;
507 WCHAR Buffer[MAX_INF_STRING_LENGTH];
508 UNICODE_STRING Name;
509 UNICODE_STRING Value;
510 PUNICODE_STRING ValuePtr;
511 NTSTATUS Status;
512 ULONG Flags;
513 ULONG Length;
514
515 INFCONTEXT Context;
516 HANDLE KeyHandle;
517 BOOLEAN Ok;
518
519
520 Ok = InfFindFirstLine (hInf, Section, NULL, &Context);
521
522 for (;Ok; Ok = InfFindNextLine (&Context, &Context))
523 {
524 /* get root */
525 if (!InfGetStringField (&Context, 1, Buffer, MAX_INF_STRING_LENGTH, NULL))
526 continue;
527 if (!GetRootKey (Buffer))
528 continue;
529
530 /* get key */
531 Length = wcslen (Buffer);
532 if (!InfGetStringField (&Context, 2, Buffer + Length, MAX_INF_STRING_LENGTH - Length, NULL))
533 *Buffer = 0;
534
535 DPRINT1("KeyName: <%S>\n", Buffer);
536
537 /* get flags */
538 if (!InfGetIntField (&Context, 4, (PLONG)&Flags))
539 Flags = 0;
540
541 DPRINT1("Flags: %lx\n", Flags);
542
543 if (!Delete)
544 {
545 if (Flags & FLG_ADDREG_DELREG_BIT)
546 continue; /* ignore this entry */
547 }
548 else
549 {
550 if (!Flags)
551 Flags = FLG_ADDREG_DELREG_BIT;
552 else if (!(Flags & FLG_ADDREG_DELREG_BIT))
553 continue; /* ignore this entry */
554 }
555
556 RtlInitUnicodeString (&Name,
557 Buffer);
558
559 InitializeObjectAttributes (&ObjectAttributes,
560 &Name,
561 OBJ_CASE_INSENSITIVE,
562 NULL,
563 NULL);
564
565 if (Delete || (Flags & FLG_ADDREG_OVERWRITEONLY))
566 {
567 Status = NtOpenKey (&KeyHandle,
568 KEY_ALL_ACCESS,
569 &ObjectAttributes);
570 if (!NT_SUCCESS(Status))
571 {
572 DPRINT1("NtOpenKey(%wZ) failed (Status %lx)\n", &Name, Status);
573 continue; /* ignore if it doesn't exist */
574 }
575 }
576 else
577 {
578 Status = CreateNestedKey (&KeyHandle,
579 KEY_ALL_ACCESS,
580 &ObjectAttributes);
581 if (!NT_SUCCESS(Status))
582 {
583 DPRINT1("CreateNestedKey(%wZ) failed (Status %lx)\n", &Name, Status);
584 continue;
585 }
586 }
587
588 /* get value name */
589 if (InfGetStringField (&Context, 3, Buffer, MAX_INF_STRING_LENGTH, NULL))
590 {
591 RtlInitUnicodeString (&Value,
592 Buffer);
593 ValuePtr = &Value;
594 }
595 else
596 {
597 ValuePtr = NULL;
598 }
599
600 /* and now do it */
601 if (!do_reg_operation (KeyHandle, ValuePtr, &Context, Flags))
602 {
603 NtClose (KeyHandle);
604 return FALSE;
605 }
606
607 NtClose (KeyHandle);
608 }
609
610 return TRUE;
611 }
612
613
614 BOOLEAN
615 ImportRegistryData(PWSTR Filename)
616 {
617 WCHAR FileNameBuffer[MAX_PATH];
618 UNICODE_STRING FileName;
619 HINF hInf;
620 NTSTATUS Status;
621 ULONG ErrorLine;
622
623 /* Load inf file from install media. */
624 wcscpy(FileNameBuffer, SourceRootPath.Buffer);
625 wcscat(FileNameBuffer, L"\\install\\");
626 wcscat(FileNameBuffer, Filename);
627
628 RtlInitUnicodeString(&FileName,
629 FileNameBuffer);
630
631 Status = InfOpenFile(&hInf,
632 &FileName,
633 &ErrorLine);
634 if (!NT_SUCCESS(Status))
635 {
636 DPRINT1("InfOpenFile() failed (Status %lx)\n", Status);
637 return FALSE;
638 }
639
640 if (!registry_callback (hInf, L"AddReg", FALSE))
641 {
642 DPRINT1("registry_callback() failed\n");
643 }
644
645 InfCloseFile (hInf);
646
647 return TRUE;
648 }
649
650
651 NTSTATUS
652 SetupUpdateRegistry(VOID)
653 {
654 OBJECT_ATTRIBUTES ObjectAttributes;
655 UNICODE_STRING KeyName;
656 UNICODE_STRING ValueName;
657 HANDLE KeyHandle;
658 NTSTATUS Status;
659
660 RtlInitUnicodeStringFromLiteral(&KeyName,
661 L"\\Registry\\Machine\\SYSTEM\\CurrentControlSet\\Control");
662 InitializeObjectAttributes(&ObjectAttributes,
663 &KeyName,
664 OBJ_CASE_INSENSITIVE,
665 NULL,
666 NULL);
667 Status = NtCreateKey(&KeyHandle,
668 KEY_ALL_ACCESS,
669 &ObjectAttributes,
670 0,
671 NULL,
672 REG_OPTION_NON_VOLATILE,
673 NULL);
674 if (!NT_SUCCESS(Status))
675 {
676 DPRINT1("NtCreateKey() failed (Status %lx)\n", Status);
677 }
678
679 NtClose(KeyHandle);
680
681
682 #if 0
683 /* Create '\Registry\Machine\System\Setup' key */
684 RtlInitUnicodeStringFromLiteral(&KeyName,
685 L"\\Registry\\Machine\\SYSTEM\\Setup");
686 InitializeObjectAttributes(&ObjectAttributes,
687 &KeyName,
688 OBJ_CASE_INSENSITIVE,
689 NULL,
690 NULL);
691 Status = NtCreateKey(&KeyHandle,
692 KEY_ALL_ACCESS,
693 &ObjectAttributes,
694 0,
695 NULL,
696 REG_OPTION_NON_VOLATILE,
697 NULL);
698 if (!NT_SUCCESS(Status))
699 {
700 DPRINT1("NtCreateKey() failed (Status %lx)\n", Status);
701 }
702
703 /* FIXME: Create value 'SetupType' */
704
705 /* FIXME: Create value 'SystemSetupInProgress' */
706
707
708 NtClose(KeyHandle);
709 #endif
710
711 SetStatusText(" Importing hivesys.inf...");
712
713 if (!ImportRegistryData (L"hivesys.inf"))
714 {
715 DPRINT1("ImportRegistryData (\"hivesys.inf\") failed\n");
716 }
717
718 SetStatusText(" Done...");
719
720 return STATUS_SUCCESS;
721 }
722
723 /* EOF */