4c68311766b3c119dfe4031aba8f6869a20c2a03
[reactos.git] / reactos / ntoskrnl / cm / rtlfunc.c
1 /*
2 * COPYRIGHT: See COPYING in the top level directory
3 * PROJECT: ReactOS kernel
4 * FILE: ntoskrnl/cm/rtlfunc.c
5 * PURPOSE: Rtlxxx function for registry access
6 * UPDATE HISTORY:
7 */
8
9 #include <ddk/ntddk.h>
10 #include <roscfg.h>
11 #include <internal/ob.h>
12 #include <limits.h>
13 #include <string.h>
14 #include <internal/pool.h>
15 #include <internal/registry.h>
16
17 #define NDEBUG
18 #include <internal/debug.h>
19
20 #include "cm.h"
21
22 NTSTATUS STDCALL
23 RtlCheckRegistryKey(IN ULONG RelativeTo,
24 IN PWSTR Path)
25 {
26 HANDLE KeyHandle;
27 NTSTATUS Status;
28
29 Status = RtlpGetRegistryHandle(RelativeTo,
30 Path,
31 FALSE,
32 &KeyHandle);
33 if (!NT_SUCCESS(Status))
34 return(Status);
35
36 NtClose(KeyHandle);
37
38 return(STATUS_SUCCESS);
39 }
40
41
42 NTSTATUS STDCALL
43 RtlCreateRegistryKey(IN ULONG RelativeTo,
44 IN PWSTR Path)
45 {
46 HANDLE KeyHandle;
47 NTSTATUS Status;
48
49 Status = RtlpGetRegistryHandle(RelativeTo,
50 Path,
51 TRUE,
52 &KeyHandle);
53 if (!NT_SUCCESS(Status))
54 return(Status);
55
56 NtClose(KeyHandle);
57
58 return(STATUS_SUCCESS);
59 }
60
61
62 NTSTATUS STDCALL
63 RtlDeleteRegistryValue(IN ULONG RelativeTo,
64 IN PWSTR Path,
65 IN PWSTR ValueName)
66 {
67 HANDLE KeyHandle;
68 NTSTATUS Status;
69 UNICODE_STRING Name;
70
71 Status = RtlpGetRegistryHandle(RelativeTo,
72 Path,
73 TRUE,
74 &KeyHandle);
75 if (!NT_SUCCESS(Status))
76 return(Status);
77
78 RtlInitUnicodeString(&Name,
79 ValueName);
80
81 NtDeleteValueKey(KeyHandle,
82 &Name);
83
84 NtClose(KeyHandle);
85
86 return(STATUS_SUCCESS);
87 }
88
89
90 NTSTATUS STDCALL
91 RtlOpenCurrentUser(IN ACCESS_MASK DesiredAccess,
92 OUT PHANDLE KeyHandle)
93 {
94 OBJECT_ATTRIBUTES ObjectAttributes;
95 UNICODE_STRING KeyPath = UNICODE_STRING_INITIALIZER(L"\\Registry\\User\\.Default");
96 NTSTATUS Status;
97
98 Status = RtlFormatCurrentUserKeyPath(&KeyPath);
99 if (NT_SUCCESS(Status))
100 {
101 InitializeObjectAttributes(&ObjectAttributes,
102 &KeyPath,
103 OBJ_CASE_INSENSITIVE,
104 NULL,
105 NULL);
106 Status = NtOpenKey(KeyHandle,
107 DesiredAccess,
108 &ObjectAttributes);
109 RtlFreeUnicodeString(&KeyPath);
110 if (NT_SUCCESS(Status))
111 return(STATUS_SUCCESS);
112 }
113
114 InitializeObjectAttributes(&ObjectAttributes,
115 &KeyPath,
116 OBJ_CASE_INSENSITIVE,
117 NULL,
118 NULL);
119 Status = NtOpenKey(KeyHandle,
120 DesiredAccess,
121 &ObjectAttributes);
122 return(Status);
123 }
124
125
126 NTSTATUS STDCALL
127 RtlQueryRegistryValues(IN ULONG RelativeTo,
128 IN PWSTR Path,
129 IN PRTL_QUERY_REGISTRY_TABLE QueryTable,
130 IN PVOID Context,
131 IN PVOID Environment)
132 {
133 NTSTATUS Status;
134 HANDLE BaseKeyHandle;
135 HANDLE CurrentKeyHandle;
136 PRTL_QUERY_REGISTRY_TABLE QueryEntry;
137 OBJECT_ATTRIBUTES ObjectAttributes;
138 UNICODE_STRING KeyName;
139 PKEY_VALUE_PARTIAL_INFORMATION ValueInfo;
140 PKEY_VALUE_FULL_INFORMATION FullValueInfo;
141 ULONG BufferSize;
142 ULONG ResultSize;
143 ULONG Index;
144 ULONG StringLen;
145 PWSTR StringPtr;
146
147 DPRINT("RtlQueryRegistryValues() called\n");
148
149 Status = RtlpGetRegistryHandle(RelativeTo,
150 Path,
151 FALSE,
152 &BaseKeyHandle);
153 if (!NT_SUCCESS(Status))
154 {
155 DPRINT("RtlpGetRegistryHandle() failed with status %x\n", Status);
156 return(Status);
157 }
158
159 CurrentKeyHandle = BaseKeyHandle;
160 QueryEntry = QueryTable;
161 while ((QueryEntry->QueryRoutine != NULL) ||
162 (QueryEntry->Name != NULL))
163 {
164 /* TODO: (from RobD)
165
166 packet.sys has this code which calls this (and fails here) with:
167
168 RtlZeroMemory(ParamTable, sizeof(ParamTable));
169 //
170 // change to the linkage key
171 //
172 ParamTable[0].QueryRoutine = NULL; // NOTE: QueryRoutine is set to NULL
173 ParamTable[0].Flags = RTL_QUERY_REGISTRY_SUBKEY;
174 ParamTable[0].Name = L"Linkage";
175 //
176 // Get the name of the mac driver we should bind to
177 //
178 ParamTable[1].QueryRoutine = PacketQueryRegistryRoutine;
179 ParamTable[1].Flags = RTL_QUERY_REGISTRY_REQUIRED | RTL_QUERY_REGISTRY_NOEXPAND;
180 ParamTable[1].Name = L"Bind";
181 ParamTable[1].EntryContext = (PVOID)MacDriverName;
182 ParamTable[1].DefaultType = REG_MULTI_SZ;
183
184 Status = RtlQueryRegistryValues(
185 IN ULONG RelativeTo = RTL_REGISTRY_ABSOLUTE,
186 IN PWSTR Path = Path,
187 IN PRTL_QUERY_REGISTRY_TABLE QueryTable = ParamTable,
188 IN PVOID Context = NULL,
189 IN PVOID Environment = NULL);
190
191 */
192 //CSH: Was:
193 //if ((QueryEntry->QueryRoutine == NULL) &&
194 // ((QueryEntry->Flags & (RTL_QUERY_REGISTRY_SUBKEY | RTL_QUERY_REGISTRY_DIRECT)) != 0))
195 // Which is more correct?
196 if ((QueryEntry->QueryRoutine == NULL) &&
197 ((QueryEntry->Flags & RTL_QUERY_REGISTRY_SUBKEY) != 0))
198 {
199 DPRINT("Bad parameters\n");
200 Status = STATUS_INVALID_PARAMETER;
201 break;
202 }
203
204 DPRINT("Name: %S\n", QueryEntry->Name);
205
206 if (((QueryEntry->Flags & (RTL_QUERY_REGISTRY_SUBKEY | RTL_QUERY_REGISTRY_TOPKEY)) != 0) &&
207 (BaseKeyHandle != CurrentKeyHandle))
208 {
209 NtClose(CurrentKeyHandle);
210 CurrentKeyHandle = BaseKeyHandle;
211 }
212
213 if (QueryEntry->Flags & RTL_QUERY_REGISTRY_SUBKEY)
214 {
215 DPRINT("Open new subkey: %S\n", QueryEntry->Name);
216
217 RtlInitUnicodeString(&KeyName,
218 QueryEntry->Name);
219 InitializeObjectAttributes(&ObjectAttributes,
220 &KeyName,
221 OBJ_CASE_INSENSITIVE,
222 BaseKeyHandle,
223 NULL);
224 Status = NtOpenKey(&CurrentKeyHandle,
225 KEY_ALL_ACCESS,
226 &ObjectAttributes);
227 if (!NT_SUCCESS(Status))
228 break;
229 }
230 else if (QueryEntry->Flags & RTL_QUERY_REGISTRY_DIRECT)
231 {
232 DPRINT("Query value directly: %S\n", QueryEntry->Name);
233
234 RtlInitUnicodeString(&KeyName,
235 QueryEntry->Name);
236
237 BufferSize = sizeof(KEY_VALUE_PARTIAL_INFORMATION) + 4096;
238 ValueInfo = ExAllocatePool(PagedPool, BufferSize);
239 if (ValueInfo == NULL)
240 {
241 Status = STATUS_NO_MEMORY;
242 break;
243 }
244
245 Status = ZwQueryValueKey(CurrentKeyHandle,
246 &KeyName,
247 KeyValuePartialInformation,
248 ValueInfo,
249 BufferSize,
250 &ResultSize);
251 if (!NT_SUCCESS(Status))
252 {
253 if (QueryEntry->Flags & RTL_QUERY_REGISTRY_REQUIRED)
254 {
255 ExFreePool(ValueInfo);
256 Status = STATUS_OBJECT_NAME_NOT_FOUND;
257 goto ByeBye;
258 }
259
260 if (QueryEntry->DefaultType == REG_SZ)
261 {
262 PUNICODE_STRING ValueString;
263 PUNICODE_STRING SourceString;
264
265 SourceString = (PUNICODE_STRING)QueryEntry->DefaultData;
266 ValueString = (PUNICODE_STRING)QueryEntry->EntryContext;
267 if (ValueString->Buffer == 0)
268 {
269 ValueString->Length = SourceString->Length;
270 ValueString->MaximumLength = SourceString->MaximumLength;
271 ValueString->Buffer = ExAllocatePool(PagedPool,
272 ValueString->MaximumLength);
273 if (!ValueString->Buffer)
274 break;
275 ValueString->Buffer[0] = 0;
276 memcpy(ValueString->Buffer,
277 SourceString->Buffer,
278 SourceString->MaximumLength);
279 }
280 else
281 {
282 ValueString->Length = RtlMin(SourceString->Length,
283 ValueString->MaximumLength - sizeof(WCHAR));
284 memcpy(ValueString->Buffer,
285 SourceString->Buffer,
286 ValueString->Length);
287 ((PWSTR)ValueString->Buffer)[ValueString->Length / sizeof(WCHAR)] = 0;
288 }
289 }
290 else
291 {
292 memcpy(QueryEntry->EntryContext,
293 QueryEntry->DefaultData,
294 QueryEntry->DefaultLength);
295 }
296 Status = STATUS_SUCCESS;
297 }
298 else
299 {
300 if (ValueInfo->Type == REG_SZ ||
301 ValueInfo->Type == REG_MULTI_SZ ||
302 ValueInfo->Type == REG_EXPAND_SZ)
303 {
304 PUNICODE_STRING ValueString;
305
306 ValueString = (PUNICODE_STRING)QueryEntry->EntryContext;
307 if (ValueString->Buffer == 0)
308 {
309 RtlInitUnicodeString(ValueString,
310 NULL);
311 ValueString->MaximumLength = ValueInfo->DataLength + sizeof(WCHAR); //256 * sizeof(WCHAR);
312 ValueString->Buffer = ExAllocatePool(PagedPool,
313 ValueString->MaximumLength);
314 if (!ValueString->Buffer)
315 break;
316 ValueString->Buffer[0] = 0;
317 }
318 ValueString->Length = RtlMin(ValueInfo->DataLength,
319 ValueString->MaximumLength - sizeof(WCHAR));
320 memcpy(ValueString->Buffer,
321 ValueInfo->Data,
322 ValueString->Length);
323 ((PWSTR)ValueString->Buffer)[ValueString->Length / sizeof(WCHAR)] = 0;
324 }
325 else
326 {
327 memcpy(QueryEntry->EntryContext,
328 ValueInfo->Data,
329 ValueInfo->DataLength);
330 }
331 }
332
333 if (QueryEntry->Flags & RTL_QUERY_REGISTRY_DELETE)
334 {
335 DPRINT("FIXME: Delete value: %S\n", QueryEntry->Name);
336
337 }
338
339 ExFreePool(ValueInfo);
340 }
341 else
342 {
343 DPRINT("Query value via query routine: %S\n", QueryEntry->Name);
344
345 if (QueryEntry->Name != NULL)
346 {
347 DPRINT("Callback\n");
348
349 RtlInitUnicodeString(&KeyName,
350 QueryEntry->Name);
351
352 BufferSize = sizeof(KEY_VALUE_PARTIAL_INFORMATION) + 4096;
353 ValueInfo = ExAllocatePool(PagedPool,
354 BufferSize);
355 if (ValueInfo == NULL)
356 {
357 Status = STATUS_NO_MEMORY;
358 break;
359 }
360
361 Status = NtQueryValueKey(CurrentKeyHandle,
362 &KeyName,
363 KeyValuePartialInformation,
364 ValueInfo,
365 BufferSize,
366 &ResultSize);
367 if (!NT_SUCCESS(Status))
368 {
369 Status = QueryEntry->QueryRoutine(QueryEntry->Name,
370 QueryEntry->DefaultType,
371 QueryEntry->DefaultData,
372 QueryEntry->DefaultLength,
373 Context,
374 QueryEntry->EntryContext);
375 }
376 else if ((ValueInfo->Type == REG_MULTI_SZ) &&
377 !(QueryEntry->Flags & RTL_QUERY_REGISTRY_NOEXPAND))
378 {
379 DPRINT("Expand REG_MULTI_SZ type\n");
380 StringPtr = (PWSTR)ValueInfo->Data;
381 while (*StringPtr != 0)
382 {
383 StringLen = (wcslen(StringPtr) + 1) * sizeof(WCHAR);
384 Status = QueryEntry->QueryRoutine(QueryEntry->Name,
385 REG_SZ,
386 (PVOID)StringPtr,
387 StringLen,
388 Context,
389 QueryEntry->EntryContext);
390 if(!NT_SUCCESS(Status))
391 break;
392 StringPtr = (PWSTR)((PUCHAR)StringPtr + StringLen);
393 }
394 }
395 else
396 {
397 Status = QueryEntry->QueryRoutine(QueryEntry->Name,
398 ValueInfo->Type,
399 ValueInfo->Data,
400 ValueInfo->DataLength,
401 Context,
402 QueryEntry->EntryContext);
403 }
404
405 if (QueryEntry->Flags & RTL_QUERY_REGISTRY_DELETE)
406 {
407 DPRINT("FIXME: Delete value: %S\n", QueryEntry->Name);
408
409 }
410
411 ExFreePool(ValueInfo);
412
413 if (!NT_SUCCESS(Status))
414 break;
415 }
416 else if (QueryEntry->Flags & RTL_QUERY_REGISTRY_NOVALUE)
417 {
418 DPRINT("Simple callback\n");
419 Status = QueryEntry->QueryRoutine(NULL,
420 REG_NONE,
421 NULL,
422 0,
423 Context,
424 QueryEntry->EntryContext);
425 if (!NT_SUCCESS(Status))
426 break;
427 }
428 else
429 {
430 DPRINT("Enumerate values\n");
431
432 BufferSize = sizeof(KEY_VALUE_FULL_INFORMATION) + 4096;
433 FullValueInfo = ExAllocatePool(PagedPool,
434 BufferSize);
435 if (FullValueInfo == NULL)
436 {
437 Status = STATUS_NO_MEMORY;
438 break;
439 }
440
441 Index = 0;
442 while (TRUE)
443 {
444 Status = NtEnumerateValueKey(CurrentKeyHandle,
445 Index,
446 KeyValueFullInformation,
447 FullValueInfo,
448 BufferSize,
449 &ResultSize);
450 if (!NT_SUCCESS(Status))
451 {
452 if ((Status == STATUS_NO_MORE_ENTRIES) &&
453 (Index == 0) &&
454 (QueryEntry->Flags & RTL_QUERY_REGISTRY_REQUIRED))
455 {
456 Status = STATUS_OBJECT_NAME_NOT_FOUND;
457 }
458 else if (Status == STATUS_NO_MORE_ENTRIES)
459 {
460 Status = STATUS_SUCCESS;
461 }
462 break;
463 }
464
465 if ((FullValueInfo->Type == REG_MULTI_SZ) &&
466 !(QueryEntry->Flags & RTL_QUERY_REGISTRY_NOEXPAND))
467 {
468 DPRINT("Expand REG_MULTI_SZ type\n");
469 StringPtr = (PWSTR)((PVOID)FullValueInfo + FullValueInfo->DataOffset);
470 while (*StringPtr != 0)
471 {
472 StringLen = (wcslen(StringPtr) + 1) * sizeof(WCHAR);
473 Status = QueryEntry->QueryRoutine(QueryEntry->Name,
474 REG_SZ,
475 (PVOID)StringPtr,
476 StringLen,
477 Context,
478 QueryEntry->EntryContext);
479 if(!NT_SUCCESS(Status))
480 break;
481 StringPtr = (PWSTR)((PUCHAR)StringPtr + StringLen);
482 }
483 }
484 else
485 {
486 Status = QueryEntry->QueryRoutine(FullValueInfo->Name,
487 FullValueInfo->Type,
488 (PVOID)FullValueInfo + FullValueInfo->DataOffset,
489 FullValueInfo->DataLength,
490 Context,
491 QueryEntry->EntryContext);
492 }
493
494 if (!NT_SUCCESS(Status))
495 break;
496
497 /* FIXME: How will these be deleted? */
498
499 Index++;
500 }
501
502 ExFreePool(FullValueInfo);
503
504 if (!NT_SUCCESS(Status))
505 break;
506 }
507 }
508
509 QueryEntry++;
510 }
511
512 ByeBye:
513
514 if (CurrentKeyHandle != BaseKeyHandle)
515 NtClose(CurrentKeyHandle);
516
517 NtClose(BaseKeyHandle);
518
519 return(Status);
520 }
521
522
523 NTSTATUS STDCALL
524 RtlWriteRegistryValue(IN ULONG RelativeTo,
525 IN PWSTR Path,
526 IN PWSTR ValueName,
527 IN ULONG ValueType,
528 IN PVOID ValueData,
529 IN ULONG ValueLength)
530 {
531 HANDLE KeyHandle;
532 NTSTATUS Status;
533 UNICODE_STRING Name;
534
535 Status = RtlpGetRegistryHandle(RelativeTo,
536 Path,
537 TRUE,
538 &KeyHandle);
539 if (!NT_SUCCESS(Status))
540 return(Status);
541
542 RtlInitUnicodeString(&Name,
543 ValueName);
544
545 NtSetValueKey(KeyHandle,
546 &Name,
547 0,
548 ValueType,
549 ValueData,
550 ValueLength);
551
552 NtClose(KeyHandle);
553
554 return(STATUS_SUCCESS);
555 }
556
557
558 NTSTATUS STDCALL
559 RtlFormatCurrentUserKeyPath(IN OUT PUNICODE_STRING KeyPath)
560 {
561 /* FIXME: !!! */
562 RtlCreateUnicodeString(KeyPath,
563 L"\\Registry\\User\\.Default");
564
565 return(STATUS_SUCCESS);
566 }
567
568 /* ------------------------------------------ Private Implementation */
569
570
571 NTSTATUS
572 RtlpGetRegistryHandle(ULONG RelativeTo,
573 PWSTR Path,
574 BOOLEAN Create,
575 PHANDLE KeyHandle)
576 {
577 UNICODE_STRING KeyName;
578 WCHAR KeyBuffer[MAX_PATH];
579 OBJECT_ATTRIBUTES ObjectAttributes;
580 NTSTATUS Status;
581
582 if (RelativeTo & RTL_REGISTRY_HANDLE)
583 {
584 Status = NtDuplicateObject(PsGetCurrentProcessId(),
585 (HANDLE)Path,
586 PsGetCurrentProcessId(),
587 KeyHandle,
588 0,
589 FALSE,
590 DUPLICATE_SAME_ACCESS);
591 return(Status);
592 }
593
594 if (RelativeTo & RTL_REGISTRY_OPTIONAL)
595 RelativeTo &= ~RTL_REGISTRY_OPTIONAL;
596
597 if (RelativeTo >= RTL_REGISTRY_MAXIMUM)
598 return STATUS_INVALID_PARAMETER;
599
600 KeyName.Length = 0;
601 KeyName.MaximumLength = MAX_PATH;
602 KeyName.Buffer = KeyBuffer;
603 KeyBuffer[0] = 0;
604
605 switch (RelativeTo)
606 {
607 case RTL_REGISTRY_SERVICES:
608 RtlAppendUnicodeToString(&KeyName,
609 L"\\Registry\\Machine\\System\\CurrentControlSet\\Services\\");
610 break;
611
612 case RTL_REGISTRY_CONTROL:
613 RtlAppendUnicodeToString(&KeyName,
614 L"\\Registry\\Machine\\System\\CurrentControlSet\\Control\\");
615 break;
616
617 case RTL_REGISTRY_WINDOWS_NT:
618 RtlAppendUnicodeToString(&KeyName,
619 L"\\Registry\\Machine\\Software\\Microsoft\\Windows NT\\CurrentVersion\\");
620 break;
621
622 case RTL_REGISTRY_DEVICEMAP:
623 RtlAppendUnicodeToString(&KeyName,
624 L"\\Registry\\Machine\\Hardware\\DeviceMap\\");
625 break;
626
627 case RTL_REGISTRY_USER:
628 Status = RtlFormatCurrentUserKeyPath(&KeyName);
629 if (!NT_SUCCESS(Status))
630 return(Status);
631 break;
632
633 /* ReactOS specific */
634 case RTL_REGISTRY_ENUM:
635 RtlAppendUnicodeToString(&KeyName,
636 L"\\Registry\\Machine\\System\\CurrentControlSet\\Enum\\");
637 break;
638 }
639
640 if (Path[0] == L'\\' && RelativeTo != RTL_REGISTRY_ABSOLUTE)
641 {
642 Path++;
643 }
644 RtlAppendUnicodeToString(&KeyName,
645 Path);
646
647 DPRINT("KeyName '%wZ'\n", &KeyName);
648
649 InitializeObjectAttributes(&ObjectAttributes,
650 &KeyName,
651 OBJ_CASE_INSENSITIVE | OBJ_OPENIF,
652 NULL,
653 NULL);
654
655 if (Create == TRUE)
656 {
657 Status = NtCreateKey(KeyHandle,
658 KEY_ALL_ACCESS,
659 &ObjectAttributes,
660 0,
661 NULL,
662 0,
663 NULL);
664 }
665 else
666 {
667 Status = NtOpenKey(KeyHandle,
668 KEY_ALL_ACCESS,
669 &ObjectAttributes);
670 }
671
672 return(Status);
673 }
674
675
676 NTSTATUS
677 RtlpCreateRegistryKeyPath(PWSTR Path)
678 {
679 OBJECT_ATTRIBUTES ObjectAttributes;
680 WCHAR KeyBuffer[MAX_PATH];
681 UNICODE_STRING KeyName;
682 HANDLE KeyHandle;
683 NTSTATUS Status;
684 PWCHAR Current;
685 PWCHAR Next;
686
687 if (_wcsnicmp(Path, L"\\Registry\\", 10) != 0)
688 {
689 return(STATUS_INVALID_PARAMETER);
690 }
691
692 wcsncpy(KeyBuffer, Path, MAX_PATH-1);
693 RtlInitUnicodeString(&KeyName, KeyBuffer);
694
695 /* Skip \\Registry\\ */
696 Current = KeyName.Buffer;
697 Current = wcschr(Current, '\\') + 1;
698 Current = wcschr(Current, '\\') + 1;
699
700 do {
701 Next = wcschr(Current, '\\');
702 if (Next == NULL)
703 {
704 /* The end */
705 }
706 else
707 {
708 *Next = 0;
709 }
710
711 InitializeObjectAttributes(
712 &ObjectAttributes,
713 &KeyName,
714 OBJ_CASE_INSENSITIVE,
715 NULL,
716 NULL);
717
718 DPRINT("Create '%S'\n", KeyName.Buffer);
719
720 Status = NtCreateKey(
721 &KeyHandle,
722 KEY_ALL_ACCESS,
723 &ObjectAttributes,
724 0,
725 NULL,
726 0,
727 NULL);
728 if (!NT_SUCCESS(Status))
729 {
730 DPRINT("NtCreateKey() failed with status %x\n", Status);
731 return Status;
732 }
733
734 NtClose(KeyHandle);
735
736 if (Next != NULL)
737 {
738 *Next = L'\\';
739 }
740
741 Current = Next + 1;
742 } while (Next != NULL);
743
744 return STATUS_SUCCESS;
745 }
746
747 /* EOF */