e2026b8cd9c3b271c4904ceb176bbd0d22839f5d
[reactos.git] / drivers / filesystems / fastfat / dirwr.c
1 /*
2 * COPYRIGHT: See COPYING in the top level directory
3 * PROJECT: ReactOS kernel
4 * FILE: drivers/filesystems/fastfat/dirwr.c
5 * PURPOSE: VFAT Filesystem : write in directory
6 * PROGRAMMER: Rex Jolliff (rex@lvcablemodem.com)
7 * Herve Poussineau (reactos@poussine.freesurf.fr)
8 * Pierre Schweitzer (pierre@reactos.org)
9 *
10 */
11
12 /* INCLUDES *****************************************************************/
13
14 #include "vfat.h"
15
16 #define NDEBUG
17 #include <debug.h>
18
19 #ifdef KDBG
20 extern UNICODE_STRING DebugFile;
21 #endif
22
23 NTSTATUS
24 vfatFCBInitializeCacheFromVolume(
25 PVCB vcb,
26 PVFATFCB fcb)
27 {
28 PFILE_OBJECT fileObject;
29 PVFATCCB newCCB;
30 NTSTATUS status;
31 BOOLEAN Acquired;
32
33 /* Don't re-initialize if already done */
34 if (BooleanFlagOn(fcb->Flags, FCB_CACHE_INITIALIZED))
35 {
36 return STATUS_SUCCESS;
37 }
38
39 ASSERT(vfatFCBIsDirectory(fcb));
40
41 Acquired = FALSE;
42 if (!ExIsResourceAcquiredExclusive(&vcb->DirResource))
43 {
44 ExAcquireResourceExclusiveLite(&vcb->DirResource, TRUE);
45 Acquired = TRUE;
46 }
47
48 fileObject = IoCreateStreamFileObject (NULL, vcb->StorageDevice);
49
50 #ifdef KDBG
51 if (DebugFile.Buffer != NULL && FsRtlIsNameInExpression(&DebugFile, &fcb->LongNameU, FALSE, NULL))
52 {
53 DPRINT1("Attaching %p to %p (%d)\n", fcb, fileObject, fcb->RefCount);
54 }
55 #endif
56
57 newCCB = ExAllocateFromNPagedLookasideList(&VfatGlobalData->CcbLookasideList);
58 if (newCCB == NULL)
59 {
60 ObDereferenceObject(fileObject);
61 return STATUS_INSUFFICIENT_RESOURCES;
62 }
63 RtlZeroMemory(newCCB, sizeof (VFATCCB));
64
65 fileObject->SectionObjectPointer = &fcb->SectionObjectPointers;
66 fileObject->FsContext = fcb;
67 fileObject->FsContext2 = newCCB;
68 fileObject->Vpb = vcb->IoVPB;
69 fcb->FileObject = fileObject;
70
71 _SEH2_TRY
72 {
73 CcInitializeCacheMap(fileObject,
74 (PCC_FILE_SIZES)(&fcb->RFCB.AllocationSize),
75 TRUE,
76 &VfatGlobalData->CacheMgrCallbacks,
77 fcb);
78 }
79 _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
80 {
81 status = _SEH2_GetExceptionCode();
82 fcb->FileObject = NULL;
83 ExFreeToNPagedLookasideList(&VfatGlobalData->CcbLookasideList, newCCB);
84 ObDereferenceObject(fileObject);
85 return status;
86 }
87 _SEH2_END;
88
89 vfatGrabFCB(vcb, fcb);
90 SetFlag(fcb->Flags, FCB_CACHE_INITIALIZED);
91
92 if (Acquired)
93 {
94 ExReleaseResourceLite(&vcb->DirResource);
95 }
96
97 return STATUS_SUCCESS;
98 }
99
100 /*
101 * update an existing FAT entry
102 */
103 NTSTATUS
104 VfatUpdateEntry(
105 IN PDEVICE_EXTENSION DeviceExt,
106 IN PVFATFCB pFcb)
107 {
108 PVOID Context;
109 PDIR_ENTRY PinEntry;
110 LARGE_INTEGER Offset;
111 ULONG SizeDirEntry;
112 ULONG dirIndex;
113 NTSTATUS Status;
114
115 ASSERT(pFcb);
116
117 if (vfatVolumeIsFatX(DeviceExt))
118 {
119 SizeDirEntry = sizeof(FATX_DIR_ENTRY);
120 dirIndex = pFcb->startIndex;
121 }
122 else
123 {
124 SizeDirEntry = sizeof(FAT_DIR_ENTRY);
125 dirIndex = pFcb->dirIndex;
126 }
127
128 DPRINT("updEntry dirIndex %u, PathName \'%wZ\'\n", dirIndex, &pFcb->PathNameU);
129
130 if (vfatFCBIsRoot(pFcb) || BooleanFlagOn(pFcb->Flags, FCB_IS_FAT | FCB_IS_VOLUME))
131 {
132 return STATUS_SUCCESS;
133 }
134
135 ASSERT(pFcb->parentFcb);
136
137 Status = vfatFCBInitializeCacheFromVolume(DeviceExt, pFcb->parentFcb);
138 if (!NT_SUCCESS(Status))
139 {
140 return Status;
141 }
142
143 Offset.u.HighPart = 0;
144 Offset.u.LowPart = dirIndex * SizeDirEntry;
145 _SEH2_TRY
146 {
147 CcPinRead(pFcb->parentFcb->FileObject, &Offset, SizeDirEntry, PIN_WAIT, &Context, (PVOID*)&PinEntry);
148 }
149 _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
150 {
151 DPRINT1("Failed write to \'%wZ\'.\n", &pFcb->parentFcb->PathNameU);
152 _SEH2_YIELD(return _SEH2_GetExceptionCode());
153 }
154 _SEH2_END;
155
156 pFcb->Flags &= ~FCB_IS_DIRTY;
157 RtlCopyMemory(PinEntry, &pFcb->entry, SizeDirEntry);
158 CcSetDirtyPinnedData(Context, NULL);
159 CcUnpinData(Context);
160 return STATUS_SUCCESS;
161 }
162
163 /*
164 * rename an existing FAT entry
165 */
166 NTSTATUS
167 vfatRenameEntry(
168 IN PDEVICE_EXTENSION DeviceExt,
169 IN PVFATFCB pFcb,
170 IN PUNICODE_STRING FileName,
171 IN BOOLEAN CaseChangeOnly)
172 {
173 OEM_STRING NameA;
174 ULONG StartIndex;
175 PVOID Context = NULL;
176 LARGE_INTEGER Offset;
177 PFATX_DIR_ENTRY pDirEntry;
178 NTSTATUS Status;
179
180 DPRINT("vfatRenameEntry(%p, %p, %wZ, %d)\n", DeviceExt, pFcb, FileName, CaseChangeOnly);
181
182 Status = vfatFCBInitializeCacheFromVolume(DeviceExt, pFcb->parentFcb);
183 if (!NT_SUCCESS(Status))
184 {
185 return Status;
186 }
187
188 if (vfatVolumeIsFatX(DeviceExt))
189 {
190 VFAT_DIRENTRY_CONTEXT DirContext;
191
192 /* Open associated dir entry */
193 StartIndex = pFcb->startIndex;
194 Offset.u.HighPart = 0;
195 Offset.u.LowPart = (StartIndex * sizeof(FATX_DIR_ENTRY) / PAGE_SIZE) * PAGE_SIZE;
196 _SEH2_TRY
197 {
198 CcPinRead(pFcb->parentFcb->FileObject, &Offset, PAGE_SIZE, PIN_WAIT, &Context, (PVOID*)&pDirEntry);
199 }
200 _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
201 {
202 DPRINT1("CcPinRead(Offset %x:%x, Length %d) failed\n", Offset.u.HighPart, Offset.u.LowPart, PAGE_SIZE);
203 _SEH2_YIELD(return _SEH2_GetExceptionCode());
204 }
205 _SEH2_END;
206
207 pDirEntry = &pDirEntry[StartIndex % (PAGE_SIZE / sizeof(FATX_DIR_ENTRY))];
208
209 /* Set file name */
210 NameA.Buffer = (PCHAR)pDirEntry->Filename;
211 NameA.Length = 0;
212 NameA.MaximumLength = 42;
213 RtlUnicodeStringToOemString(&NameA, FileName, FALSE);
214 pDirEntry->FilenameLength = (unsigned char)NameA.Length;
215
216 /* Update FCB */
217 DirContext.DeviceExt = DeviceExt;
218 DirContext.ShortNameU.Length = 0;
219 DirContext.ShortNameU.MaximumLength = 0;
220 DirContext.ShortNameU.Buffer = NULL;
221 DirContext.LongNameU = *FileName;
222 DirContext.DirEntry.FatX = *pDirEntry;
223
224 CcSetDirtyPinnedData(Context, NULL);
225 CcUnpinData(Context);
226
227 Status = vfatUpdateFCB(DeviceExt, pFcb, &DirContext, pFcb->parentFcb);
228 if (NT_SUCCESS(Status))
229 {
230 CcFlushCache(&pFcb->parentFcb->SectionObjectPointers, NULL, 0, NULL);
231 }
232
233 return Status;
234 }
235 else
236 {
237 /* This we cannot handle properly, move file - would likely need love */
238 return VfatMoveEntry(DeviceExt, pFcb, FileName, pFcb->parentFcb);
239 }
240 }
241
242 /*
243 * try to find contiguous entries frees in directory,
244 * extend a directory if is necessary
245 */
246 BOOLEAN
247 vfatFindDirSpace(
248 IN PDEVICE_EXTENSION DeviceExt,
249 IN PVFATFCB pDirFcb,
250 IN ULONG nbSlots,
251 OUT PULONG start)
252 {
253 LARGE_INTEGER FileOffset;
254 ULONG i, count, size, nbFree = 0;
255 PDIR_ENTRY pFatEntry = NULL;
256 PVOID Context = NULL;
257 NTSTATUS Status;
258 ULONG SizeDirEntry;
259 BOOLEAN IsFatX = vfatVolumeIsFatX(DeviceExt);
260 FileOffset.QuadPart = 0;
261
262 if (IsFatX)
263 SizeDirEntry = sizeof(FATX_DIR_ENTRY);
264 else
265 SizeDirEntry = sizeof(FAT_DIR_ENTRY);
266
267 Status = vfatFCBInitializeCacheFromVolume(DeviceExt, pDirFcb);
268 if (!NT_SUCCESS(Status))
269 {
270 return Status;
271 }
272
273 count = pDirFcb->RFCB.FileSize.u.LowPart / SizeDirEntry;
274 size = DeviceExt->FatInfo.BytesPerCluster / SizeDirEntry;
275 for (i = 0; i < count; i++, pFatEntry = (PDIR_ENTRY)((ULONG_PTR)pFatEntry + SizeDirEntry))
276 {
277 if (Context == NULL || (i % size) == 0)
278 {
279 if (Context)
280 {
281 CcUnpinData(Context);
282 }
283 _SEH2_TRY
284 {
285 CcPinRead(pDirFcb->FileObject, &FileOffset, DeviceExt->FatInfo.BytesPerCluster, PIN_WAIT, &Context, (PVOID*)&pFatEntry);
286 }
287 _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
288 {
289 _SEH2_YIELD(return FALSE);
290 }
291 _SEH2_END;
292
293 FileOffset.u.LowPart += DeviceExt->FatInfo.BytesPerCluster;
294 }
295 if (ENTRY_END(IsFatX, pFatEntry))
296 {
297 break;
298 }
299 if (ENTRY_DELETED(IsFatX, pFatEntry))
300 {
301 nbFree++;
302 }
303 else
304 {
305 nbFree = 0;
306 }
307 if (nbFree == nbSlots)
308 {
309 break;
310 }
311 }
312 if (Context)
313 {
314 CcUnpinData(Context);
315 Context = NULL;
316 }
317 if (nbFree == nbSlots)
318 {
319 /* found enough contiguous free slots */
320 *start = i - nbSlots + 1;
321 }
322 else
323 {
324 *start = i - nbFree;
325 if (*start + nbSlots > count)
326 {
327 LARGE_INTEGER AllocationSize;
328 /* extend the directory */
329 if (vfatFCBIsRoot(pDirFcb) && DeviceExt->FatInfo.FatType != FAT32)
330 {
331 /* We can't extend a root directory on a FAT12/FAT16/FATX partition */
332 return FALSE;
333 }
334 AllocationSize.QuadPart = pDirFcb->RFCB.FileSize.u.LowPart + DeviceExt->FatInfo.BytesPerCluster;
335 Status = VfatSetAllocationSizeInformation(pDirFcb->FileObject, pDirFcb,
336 DeviceExt, &AllocationSize);
337 if (!NT_SUCCESS(Status))
338 {
339 return FALSE;
340 }
341 /* clear the new dir cluster */
342 FileOffset.u.LowPart = (ULONG)(pDirFcb->RFCB.FileSize.QuadPart -
343 DeviceExt->FatInfo.BytesPerCluster);
344 _SEH2_TRY
345 {
346 CcPinRead(pDirFcb->FileObject, &FileOffset, DeviceExt->FatInfo.BytesPerCluster, PIN_WAIT, &Context, (PVOID*)&pFatEntry);
347 }
348 _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
349 {
350 _SEH2_YIELD(return FALSE);
351 }
352 _SEH2_END;
353
354 if (IsFatX)
355 memset(pFatEntry, 0xff, DeviceExt->FatInfo.BytesPerCluster);
356 else
357 RtlZeroMemory(pFatEntry, DeviceExt->FatInfo.BytesPerCluster);
358 }
359 else if (*start + nbSlots < count)
360 {
361 /* clear the entry after the last new entry */
362 FileOffset.u.LowPart = (*start + nbSlots) * SizeDirEntry;
363 _SEH2_TRY
364 {
365 CcPinRead(pDirFcb->FileObject, &FileOffset, SizeDirEntry, PIN_WAIT, &Context, (PVOID*)&pFatEntry);
366 }
367 _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
368 {
369 _SEH2_YIELD(return FALSE);
370 }
371 _SEH2_END;
372
373 if (IsFatX)
374 memset(pFatEntry, 0xff, SizeDirEntry);
375 else
376 RtlZeroMemory(pFatEntry, SizeDirEntry);
377 }
378 if (Context)
379 {
380 CcSetDirtyPinnedData(Context, NULL);
381 CcUnpinData(Context);
382 }
383 }
384 DPRINT("nbSlots %u nbFree %u, entry number %u\n", nbSlots, nbFree, *start);
385 return TRUE;
386 }
387
388 /*
389 create a new FAT entry
390 */
391 static NTSTATUS
392 FATAddEntry(
393 IN PDEVICE_EXTENSION DeviceExt,
394 IN PUNICODE_STRING NameU,
395 IN PVFATFCB* Fcb,
396 IN PVFATFCB ParentFcb,
397 IN ULONG RequestedOptions,
398 IN UCHAR ReqAttr,
399 IN PVFAT_MOVE_CONTEXT MoveContext)
400 {
401 PVOID Context = NULL;
402 PFAT_DIR_ENTRY pFatEntry;
403 slot *pSlots;
404 USHORT nbSlots = 0, j;
405 PUCHAR Buffer;
406 BOOLEAN needTilde = FALSE, needLong = FALSE;
407 BOOLEAN BaseAllLower, BaseAllUpper;
408 BOOLEAN ExtensionAllLower, ExtensionAllUpper;
409 BOOLEAN InExtension;
410 BOOLEAN IsDirectory;
411 WCHAR c;
412 ULONG CurrentCluster;
413 LARGE_INTEGER SystemTime, FileOffset;
414 NTSTATUS Status = STATUS_SUCCESS;
415 ULONG size;
416 long i;
417
418 OEM_STRING NameA;
419 CHAR aName[13];
420 BOOLEAN IsNameLegal;
421 BOOLEAN SpacesFound;
422
423 VFAT_DIRENTRY_CONTEXT DirContext;
424 WCHAR LongNameBuffer[LONGNAME_MAX_LENGTH + 1];
425 WCHAR ShortNameBuffer[13];
426
427 DPRINT("addEntry: Name='%wZ', Dir='%wZ'\n", NameU, &ParentFcb->PathNameU);
428
429 DirContext.LongNameU = *NameU;
430 IsDirectory = BooleanFlagOn(RequestedOptions, FILE_DIRECTORY_FILE);
431
432 /* nb of entry needed for long name+normal entry */
433 nbSlots = (DirContext.LongNameU.Length / sizeof(WCHAR) + 12) / 13 + 1;
434 DPRINT("NameLen= %u, nbSlots =%u\n", DirContext.LongNameU.Length / sizeof(WCHAR), nbSlots);
435 Buffer = ExAllocatePoolWithTag(NonPagedPool, (nbSlots - 1) * sizeof(FAT_DIR_ENTRY), TAG_VFAT);
436 if (Buffer == NULL)
437 {
438 return STATUS_INSUFFICIENT_RESOURCES;
439 }
440 RtlZeroMemory(Buffer, (nbSlots - 1) * sizeof(FAT_DIR_ENTRY));
441 pSlots = (slot *) Buffer;
442
443 NameA.Buffer = aName;
444 NameA.Length = 0;
445 NameA.MaximumLength = sizeof(aName);
446
447 DirContext.DeviceExt = DeviceExt;
448 DirContext.ShortNameU.Buffer = ShortNameBuffer;
449 DirContext.ShortNameU.Length = 0;
450 DirContext.ShortNameU.MaximumLength = sizeof(ShortNameBuffer);
451
452 RtlZeroMemory(&DirContext.DirEntry.Fat, sizeof(FAT_DIR_ENTRY));
453
454 IsNameLegal = RtlIsNameLegalDOS8Dot3(&DirContext.LongNameU, &NameA, &SpacesFound);
455
456 if (!IsNameLegal || SpacesFound)
457 {
458 GENERATE_NAME_CONTEXT NameContext;
459 VFAT_DIRENTRY_CONTEXT SearchContext;
460 WCHAR ShortSearchName[13];
461 needTilde = TRUE;
462 needLong = TRUE;
463 RtlZeroMemory(&NameContext, sizeof(GENERATE_NAME_CONTEXT));
464 SearchContext.DeviceExt = DeviceExt;
465 SearchContext.LongNameU.Buffer = LongNameBuffer;
466 SearchContext.LongNameU.MaximumLength = sizeof(LongNameBuffer);
467 SearchContext.ShortNameU.Buffer = ShortSearchName;
468 SearchContext.ShortNameU.MaximumLength = sizeof(ShortSearchName);
469
470 for (i = 0; i < 100; i++)
471 {
472 RtlGenerate8dot3Name(&DirContext.LongNameU, FALSE, &NameContext, &DirContext.ShortNameU);
473 DirContext.ShortNameU.Buffer[DirContext.ShortNameU.Length / sizeof(WCHAR)] = 0;
474 SearchContext.DirIndex = 0;
475 Status = FindFile(DeviceExt, ParentFcb, &DirContext.ShortNameU, &SearchContext, TRUE);
476 if (!NT_SUCCESS(Status))
477 {
478 break;
479 }
480 else if (MoveContext)
481 {
482 ASSERT(*Fcb);
483 if (strncmp((char *)SearchContext.DirEntry.Fat.ShortName, (char *)(*Fcb)->entry.Fat.ShortName, 11) == 0)
484 {
485 if (MoveContext->InPlace)
486 {
487 ASSERT(SearchContext.DirEntry.Fat.FileSize == MoveContext->FileSize);
488 break;
489 }
490 }
491 }
492 }
493 if (i == 100) /* FIXME : what to do after this ? */
494 {
495 ExFreePoolWithTag(Buffer, TAG_VFAT);
496 return STATUS_UNSUCCESSFUL;
497 }
498 IsNameLegal = RtlIsNameLegalDOS8Dot3(&DirContext.ShortNameU, &NameA, &SpacesFound);
499 }
500 else
501 {
502 BaseAllLower = BaseAllUpper = TRUE;
503 ExtensionAllLower = ExtensionAllUpper = TRUE;
504 InExtension = FALSE;
505 for (i = 0; i < DirContext.LongNameU.Length / sizeof(WCHAR); i++)
506 {
507 c = DirContext.LongNameU.Buffer[i];
508 if (c >= L'A' && c <= L'Z')
509 {
510 if (InExtension)
511 ExtensionAllLower = FALSE;
512 else
513 BaseAllLower = FALSE;
514 }
515 else if (c >= L'a' && c <= L'z')
516 {
517 if (InExtension)
518 ExtensionAllUpper = FALSE;
519 else
520 BaseAllUpper = FALSE;
521 }
522 else if (c > 0x7f)
523 {
524 needLong = TRUE;
525 break;
526 }
527
528 if (c == L'.')
529 {
530 InExtension = TRUE;
531 }
532 }
533
534 if ((!BaseAllLower && !BaseAllUpper) ||
535 (!ExtensionAllLower && !ExtensionAllUpper))
536 {
537 needLong = TRUE;
538 }
539
540 RtlUpcaseUnicodeString(&DirContext.ShortNameU, &DirContext.LongNameU, FALSE);
541 DirContext.ShortNameU.Buffer[DirContext.ShortNameU.Length / sizeof(WCHAR)] = 0;
542 }
543 aName[NameA.Length] = 0;
544 DPRINT("'%s', '%wZ', needTilde=%u, needLong=%u\n",
545 aName, &DirContext.LongNameU, needTilde, needLong);
546 memset(DirContext.DirEntry.Fat.ShortName, ' ', 11);
547 for (i = 0; i < 8 && aName[i] && aName[i] != '.'; i++)
548 {
549 DirContext.DirEntry.Fat.Filename[i] = aName[i];
550 }
551 if (aName[i] == '.')
552 {
553 i++;
554 for (j = 0; j < 3 && aName[i]; j++, i++)
555 {
556 DirContext.DirEntry.Fat.Ext[j] = aName[i];
557 }
558 }
559 if (DirContext.DirEntry.Fat.Filename[0] == 0xe5)
560 {
561 DirContext.DirEntry.Fat.Filename[0] = 0x05;
562 }
563
564 if (needLong)
565 {
566 RtlCopyMemory(LongNameBuffer, DirContext.LongNameU.Buffer, DirContext.LongNameU.Length);
567 DirContext.LongNameU.Buffer = LongNameBuffer;
568 DirContext.LongNameU.MaximumLength = sizeof(LongNameBuffer);
569 DirContext.LongNameU.Buffer[DirContext.LongNameU.Length / sizeof(WCHAR)] = 0;
570 memset(DirContext.LongNameU.Buffer + DirContext.LongNameU.Length / sizeof(WCHAR) + 1, 0xff,
571 DirContext.LongNameU.MaximumLength - DirContext.LongNameU.Length - sizeof(WCHAR));
572 }
573 else
574 {
575 nbSlots = 1;
576 if (BaseAllLower && !BaseAllUpper)
577 {
578 DirContext.DirEntry.Fat.lCase |= VFAT_CASE_LOWER_BASE;
579 }
580 if (ExtensionAllLower && !ExtensionAllUpper)
581 {
582 DirContext.DirEntry.Fat.lCase |= VFAT_CASE_LOWER_EXT;
583 }
584 }
585
586 DPRINT ("dos name=%11.11s\n", DirContext.DirEntry.Fat.Filename);
587
588 /* set attributes */
589 DirContext.DirEntry.Fat.Attrib = ReqAttr;
590 if (IsDirectory)
591 {
592 DirContext.DirEntry.Fat.Attrib |= FILE_ATTRIBUTE_DIRECTORY;
593 }
594 /* set dates and times */
595 KeQuerySystemTime(&SystemTime);
596 FsdSystemTimeToDosDateTime(DeviceExt, &SystemTime, &DirContext.DirEntry.Fat.CreationDate,
597 &DirContext.DirEntry.Fat.CreationTime);
598 DirContext.DirEntry.Fat.UpdateDate = DirContext.DirEntry.Fat.CreationDate;
599 DirContext.DirEntry.Fat.UpdateTime = DirContext.DirEntry.Fat.CreationTime;
600 DirContext.DirEntry.Fat.AccessDate = DirContext.DirEntry.Fat.CreationDate;
601 /* If it's moving, preserve creation time and file size */
602 if (MoveContext != NULL)
603 {
604 DirContext.DirEntry.Fat.CreationDate = MoveContext->CreationDate;
605 DirContext.DirEntry.Fat.CreationTime = MoveContext->CreationTime;
606 DirContext.DirEntry.Fat.FileSize = MoveContext->FileSize;
607 }
608
609 if (needLong)
610 {
611 /* calculate checksum for 8.3 name */
612 for (pSlots[0].alias_checksum = 0, i = 0; i < 11; i++)
613 {
614 pSlots[0].alias_checksum = (((pSlots[0].alias_checksum & 1) << 7
615 | ((pSlots[0].alias_checksum & 0xfe) >> 1))
616 + DirContext.DirEntry.Fat.ShortName[i]);
617 }
618 /* construct slots and entry */
619 for (i = nbSlots - 2; i >= 0; i--)
620 {
621 DPRINT("construct slot %d\n", i);
622 pSlots[i].attr = 0xf;
623 if (i)
624 {
625 pSlots[i].id = (unsigned char)(nbSlots - i - 1);
626 }
627 else
628 {
629 pSlots[i].id = (unsigned char)(nbSlots - i - 1 + 0x40);
630 }
631 pSlots[i].alias_checksum = pSlots[0].alias_checksum;
632 RtlCopyMemory(pSlots[i].name0_4, DirContext.LongNameU.Buffer + (nbSlots - i - 2) * 13, 10);
633 RtlCopyMemory(pSlots[i].name5_10, DirContext.LongNameU.Buffer + (nbSlots - i - 2) * 13 + 5, 12);
634 RtlCopyMemory(pSlots[i].name11_12, DirContext.LongNameU.Buffer + (nbSlots - i - 2) * 13 + 11, 4);
635 }
636 }
637 /* try to find nbSlots contiguous entries frees in directory */
638 if (!vfatFindDirSpace(DeviceExt, ParentFcb, nbSlots, &DirContext.StartIndex))
639 {
640 ExFreePoolWithTag(Buffer, TAG_VFAT);
641 return STATUS_DISK_FULL;
642 }
643 DirContext.DirIndex = DirContext.StartIndex + nbSlots - 1;
644 if (IsDirectory)
645 {
646 /* If we aren't moving, use next */
647 if (MoveContext == NULL)
648 {
649 CurrentCluster = 0;
650 Status = NextCluster(DeviceExt, 0, &CurrentCluster, TRUE);
651 if (CurrentCluster == 0xffffffff || !NT_SUCCESS(Status))
652 {
653 ExFreePoolWithTag(Buffer, TAG_VFAT);
654 if (!NT_SUCCESS(Status))
655 {
656 return Status;
657 }
658 return STATUS_DISK_FULL;
659 }
660 }
661 else
662 {
663 CurrentCluster = MoveContext->FirstCluster;
664 }
665
666 if (DeviceExt->FatInfo.FatType == FAT32)
667 {
668 DirContext.DirEntry.Fat.FirstClusterHigh = (unsigned short)(CurrentCluster >> 16);
669 }
670 DirContext.DirEntry.Fat.FirstCluster = (unsigned short)CurrentCluster;
671 }
672 else if (MoveContext != NULL)
673 {
674 CurrentCluster = MoveContext->FirstCluster;
675
676 if (DeviceExt->FatInfo.FatType == FAT32)
677 {
678 DirContext.DirEntry.Fat.FirstClusterHigh = (unsigned short)(CurrentCluster >> 16);
679 }
680 DirContext.DirEntry.Fat.FirstCluster = (unsigned short)CurrentCluster;
681 }
682
683 /* No need to init cache here, vfatFindDirSpace() will have done it for us */
684
685 i = DeviceExt->FatInfo.BytesPerCluster / sizeof(FAT_DIR_ENTRY);
686 FileOffset.u.HighPart = 0;
687 FileOffset.u.LowPart = DirContext.StartIndex * sizeof(FAT_DIR_ENTRY);
688 if (DirContext.StartIndex / i == DirContext.DirIndex / i)
689 {
690 /* one cluster */
691 _SEH2_TRY
692 {
693 CcPinRead(ParentFcb->FileObject, &FileOffset, nbSlots * sizeof(FAT_DIR_ENTRY), PIN_WAIT, &Context, (PVOID*)&pFatEntry);
694 }
695 _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
696 {
697 ExFreePoolWithTag(Buffer, TAG_VFAT);
698 _SEH2_YIELD(return _SEH2_GetExceptionCode());
699 }
700 _SEH2_END;
701
702 if (nbSlots > 1)
703 {
704 RtlCopyMemory(pFatEntry, Buffer, (nbSlots - 1) * sizeof(FAT_DIR_ENTRY));
705 }
706 RtlCopyMemory(pFatEntry + (nbSlots - 1), &DirContext.DirEntry.Fat, sizeof(FAT_DIR_ENTRY));
707 }
708 else
709 {
710 /* two clusters */
711 size = DeviceExt->FatInfo.BytesPerCluster -
712 (DirContext.StartIndex * sizeof(FAT_DIR_ENTRY)) % DeviceExt->FatInfo.BytesPerCluster;
713 i = size / sizeof(FAT_DIR_ENTRY);
714 _SEH2_TRY
715 {
716 CcPinRead(ParentFcb->FileObject, &FileOffset, size, PIN_WAIT, &Context, (PVOID*)&pFatEntry);
717 }
718 _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
719 {
720 ExFreePoolWithTag(Buffer, TAG_VFAT);
721 _SEH2_YIELD(return _SEH2_GetExceptionCode());
722 }
723 _SEH2_END;
724 RtlCopyMemory(pFatEntry, Buffer, size);
725 CcSetDirtyPinnedData(Context, NULL);
726 CcUnpinData(Context);
727 FileOffset.u.LowPart += size;
728 _SEH2_TRY
729 {
730 CcPinRead(ParentFcb->FileObject, &FileOffset, nbSlots * sizeof(FAT_DIR_ENTRY) - size, PIN_WAIT, &Context, (PVOID*)&pFatEntry);
731 }
732 _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
733 {
734 ExFreePoolWithTag(Buffer, TAG_VFAT);
735 _SEH2_YIELD(return _SEH2_GetExceptionCode());
736 }
737 _SEH2_END;
738 if (nbSlots - 1 > i)
739 {
740 RtlCopyMemory(pFatEntry, (PVOID)(Buffer + size), (nbSlots - 1 - i) * sizeof(FAT_DIR_ENTRY));
741 }
742 RtlCopyMemory(pFatEntry + nbSlots - 1 - i, &DirContext.DirEntry.Fat, sizeof(FAT_DIR_ENTRY));
743 }
744 CcSetDirtyPinnedData(Context, NULL);
745 CcUnpinData(Context);
746
747 if (MoveContext != NULL)
748 {
749 /* We're modifying an existing FCB - likely rename/move */
750 Status = vfatUpdateFCB(DeviceExt, *Fcb, &DirContext, ParentFcb);
751 }
752 else
753 {
754 Status = vfatMakeFCBFromDirEntry(DeviceExt, ParentFcb, &DirContext, Fcb);
755 }
756 if (!NT_SUCCESS(Status))
757 {
758 ExFreePoolWithTag(Buffer, TAG_VFAT);
759 return Status;
760 }
761
762 DPRINT("new : entry=%11.11s\n", (*Fcb)->entry.Fat.Filename);
763 DPRINT("new : entry=%11.11s\n", DirContext.DirEntry.Fat.Filename);
764
765 if (IsDirectory)
766 {
767 Status = vfatFCBInitializeCacheFromVolume(DeviceExt, (*Fcb));
768 if (!NT_SUCCESS(Status))
769 {
770 return Status;
771 }
772
773 FileOffset.QuadPart = 0;
774 _SEH2_TRY
775 {
776 CcPinRead((*Fcb)->FileObject, &FileOffset, DeviceExt->FatInfo.BytesPerCluster, PIN_WAIT, &Context, (PVOID*)&pFatEntry);
777 }
778 _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
779 {
780 ExFreePoolWithTag(Buffer, TAG_VFAT);
781 _SEH2_YIELD(return _SEH2_GetExceptionCode());
782 }
783 _SEH2_END;
784 /* clear the new directory cluster if not moving */
785 if (MoveContext == NULL)
786 {
787 RtlZeroMemory(pFatEntry, DeviceExt->FatInfo.BytesPerCluster);
788 /* create '.' and '..' */
789 RtlCopyMemory(&pFatEntry[0].Attrib, &DirContext.DirEntry.Fat.Attrib, sizeof(FAT_DIR_ENTRY) - 11);
790 RtlCopyMemory(pFatEntry[0].ShortName, ". ", 11);
791 RtlCopyMemory(&pFatEntry[1].Attrib, &DirContext.DirEntry.Fat.Attrib, sizeof(FAT_DIR_ENTRY) - 11);
792 RtlCopyMemory(pFatEntry[1].ShortName, ".. ", 11);
793 }
794
795 pFatEntry[1].FirstCluster = ParentFcb->entry.Fat.FirstCluster;
796 pFatEntry[1].FirstClusterHigh = ParentFcb->entry.Fat.FirstClusterHigh;
797 if (vfatFCBIsRoot(ParentFcb))
798 {
799 pFatEntry[1].FirstCluster = 0;
800 pFatEntry[1].FirstClusterHigh = 0;
801 }
802 CcSetDirtyPinnedData(Context, NULL);
803 CcUnpinData(Context);
804 }
805 ExFreePoolWithTag(Buffer, TAG_VFAT);
806 DPRINT("addentry ok\n");
807 return STATUS_SUCCESS;
808 }
809
810 /*
811 create a new FAT entry
812 */
813 static NTSTATUS
814 FATXAddEntry(
815 IN PDEVICE_EXTENSION DeviceExt,
816 IN PUNICODE_STRING NameU,
817 IN PVFATFCB* Fcb,
818 IN PVFATFCB ParentFcb,
819 IN ULONG RequestedOptions,
820 IN UCHAR ReqAttr,
821 IN PVFAT_MOVE_CONTEXT MoveContext)
822 {
823 PVOID Context = NULL;
824 LARGE_INTEGER SystemTime, FileOffset;
825 OEM_STRING NameA;
826 VFAT_DIRENTRY_CONTEXT DirContext;
827 PFATX_DIR_ENTRY pFatXDirEntry;
828 ULONG Index;
829
830 DPRINT("addEntry: Name='%wZ', Dir='%wZ'\n", NameU, &ParentFcb->PathNameU);
831
832 DirContext.LongNameU = *NameU;
833
834 if (DirContext.LongNameU.Length / sizeof(WCHAR) > 42)
835 {
836 /* name too long */
837 return STATUS_NAME_TOO_LONG;
838 }
839
840 /* try to find 1 entry free in directory */
841 if (!vfatFindDirSpace(DeviceExt, ParentFcb, 1, &DirContext.StartIndex))
842 {
843 return STATUS_DISK_FULL;
844 }
845 Index = DirContext.DirIndex = DirContext.StartIndex;
846 if (!vfatFCBIsRoot(ParentFcb))
847 {
848 DirContext.DirIndex += 2;
849 DirContext.StartIndex += 2;
850 }
851
852 DirContext.ShortNameU.Buffer = 0;
853 DirContext.ShortNameU.Length = 0;
854 DirContext.ShortNameU.MaximumLength = 0;
855 DirContext.DeviceExt = DeviceExt;
856 RtlZeroMemory(&DirContext.DirEntry.FatX, sizeof(FATX_DIR_ENTRY));
857 memset(DirContext.DirEntry.FatX.Filename, 0xff, 42);
858 /* Use cluster, if moving */
859 if (MoveContext != NULL)
860 {
861 DirContext.DirEntry.FatX.FirstCluster = MoveContext->FirstCluster;
862 }
863 else
864 {
865 DirContext.DirEntry.FatX.FirstCluster = 0;
866 }
867 DirContext.DirEntry.FatX.FileSize = 0;
868
869 /* set file name */
870 NameA.Buffer = (PCHAR)DirContext.DirEntry.FatX.Filename;
871 NameA.Length = 0;
872 NameA.MaximumLength = 42;
873 RtlUnicodeStringToOemString(&NameA, &DirContext.LongNameU, FALSE);
874 DirContext.DirEntry.FatX.FilenameLength = (unsigned char)NameA.Length;
875
876 /* set attributes */
877 DirContext.DirEntry.FatX.Attrib = ReqAttr;
878 if (BooleanFlagOn(RequestedOptions, FILE_DIRECTORY_FILE))
879 {
880 DirContext.DirEntry.FatX.Attrib |= FILE_ATTRIBUTE_DIRECTORY;
881 }
882
883 /* set dates and times */
884 KeQuerySystemTime(&SystemTime);
885 FsdSystemTimeToDosDateTime(DeviceExt, &SystemTime, &DirContext.DirEntry.FatX.CreationDate,
886 &DirContext.DirEntry.FatX.CreationTime);
887 DirContext.DirEntry.FatX.UpdateDate = DirContext.DirEntry.FatX.CreationDate;
888 DirContext.DirEntry.FatX.UpdateTime = DirContext.DirEntry.FatX.CreationTime;
889 DirContext.DirEntry.FatX.AccessDate = DirContext.DirEntry.FatX.CreationDate;
890 DirContext.DirEntry.FatX.AccessTime = DirContext.DirEntry.FatX.CreationTime;
891 /* If it's moving, preserve creation time and file size */
892 if (MoveContext != NULL)
893 {
894 DirContext.DirEntry.FatX.CreationDate = MoveContext->CreationDate;
895 DirContext.DirEntry.FatX.CreationTime = MoveContext->CreationTime;
896 DirContext.DirEntry.FatX.FileSize = MoveContext->FileSize;
897 }
898
899 /* No need to init cache here, vfatFindDirSpace() will have done it for us */
900
901 /* add entry into parent directory */
902 FileOffset.u.HighPart = 0;
903 FileOffset.u.LowPart = Index * sizeof(FATX_DIR_ENTRY);
904 _SEH2_TRY
905 {
906 CcPinRead(ParentFcb->FileObject, &FileOffset, sizeof(FATX_DIR_ENTRY), PIN_WAIT, &Context, (PVOID*)&pFatXDirEntry);
907 }
908 _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
909 {
910 _SEH2_YIELD(return _SEH2_GetExceptionCode());
911 }
912 _SEH2_END;
913 RtlCopyMemory(pFatXDirEntry, &DirContext.DirEntry.FatX, sizeof(FATX_DIR_ENTRY));
914 CcSetDirtyPinnedData(Context, NULL);
915 CcUnpinData(Context);
916
917 if (MoveContext != NULL)
918 {
919 /* We're modifying an existing FCB - likely rename/move */
920 /* FIXME: check status */
921 vfatUpdateFCB(DeviceExt, *Fcb, &DirContext, ParentFcb);
922 }
923 else
924 {
925 /* FIXME: check status */
926 vfatMakeFCBFromDirEntry(DeviceExt, ParentFcb, &DirContext, Fcb);
927 }
928
929 DPRINT("addentry ok\n");
930 return STATUS_SUCCESS;
931 }
932
933 /*
934 * deleting an existing FAT entry
935 */
936 static NTSTATUS
937 FATDelEntry(
938 IN PDEVICE_EXTENSION DeviceExt,
939 IN PVFATFCB pFcb,
940 OUT PVFAT_MOVE_CONTEXT MoveContext)
941 {
942 ULONG CurrentCluster = 0, NextCluster, i;
943 PVOID Context = NULL;
944 LARGE_INTEGER Offset;
945 PFAT_DIR_ENTRY pDirEntry = NULL;
946 NTSTATUS Status;
947
948 ASSERT(pFcb);
949 ASSERT(pFcb->parentFcb);
950
951 Status = vfatFCBInitializeCacheFromVolume(DeviceExt, pFcb->parentFcb);
952 if (!NT_SUCCESS(Status))
953 {
954 return Status;
955 }
956
957 DPRINT("delEntry PathName \'%wZ\'\n", &pFcb->PathNameU);
958 DPRINT("delete entry: %u to %u\n", pFcb->startIndex, pFcb->dirIndex);
959 Offset.u.HighPart = 0;
960 for (i = pFcb->startIndex; i <= pFcb->dirIndex; i++)
961 {
962 if (Context == NULL || ((i * sizeof(FAT_DIR_ENTRY)) % PAGE_SIZE) == 0)
963 {
964 if (Context)
965 {
966 CcSetDirtyPinnedData(Context, NULL);
967 CcUnpinData(Context);
968 }
969 Offset.u.LowPart = (i * sizeof(FAT_DIR_ENTRY) / PAGE_SIZE) * PAGE_SIZE;
970 _SEH2_TRY
971 {
972 CcPinRead(pFcb->parentFcb->FileObject, &Offset, PAGE_SIZE, PIN_WAIT, &Context, (PVOID*)&pDirEntry);
973 }
974 _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
975 {
976 _SEH2_YIELD(return _SEH2_GetExceptionCode());
977 }
978 _SEH2_END;
979 }
980 pDirEntry[i % (PAGE_SIZE / sizeof(FAT_DIR_ENTRY))].Filename[0] = 0xe5;
981 if (i == pFcb->dirIndex)
982 {
983 CurrentCluster =
984 vfatDirEntryGetFirstCluster(DeviceExt,
985 (PDIR_ENTRY)&pDirEntry[i % (PAGE_SIZE / sizeof(FAT_DIR_ENTRY))]);
986 }
987 }
988
989 /* In case of moving, save properties */
990 if (MoveContext != NULL)
991 {
992 pDirEntry = &pDirEntry[pFcb->dirIndex % (PAGE_SIZE / sizeof(FAT_DIR_ENTRY))];
993 MoveContext->FirstCluster = CurrentCluster;
994 MoveContext->FileSize = pDirEntry->FileSize;
995 MoveContext->CreationTime = pDirEntry->CreationTime;
996 MoveContext->CreationDate = pDirEntry->CreationDate;
997 }
998
999 if (Context)
1000 {
1001 CcSetDirtyPinnedData(Context, NULL);
1002 CcUnpinData(Context);
1003 }
1004
1005 /* In case of moving, don't delete data */
1006 if (MoveContext == NULL)
1007 {
1008 while (CurrentCluster && CurrentCluster != 0xffffffff)
1009 {
1010 GetNextCluster(DeviceExt, CurrentCluster, &NextCluster);
1011 /* FIXME: check status */
1012 WriteCluster(DeviceExt, CurrentCluster, 0);
1013 CurrentCluster = NextCluster;
1014 }
1015 }
1016
1017 return STATUS_SUCCESS;
1018 }
1019
1020 /*
1021 * deleting an existing FAT entry
1022 */
1023 static NTSTATUS
1024 FATXDelEntry(
1025 IN PDEVICE_EXTENSION DeviceExt,
1026 IN PVFATFCB pFcb,
1027 OUT PVFAT_MOVE_CONTEXT MoveContext)
1028 {
1029 ULONG CurrentCluster = 0, NextCluster;
1030 PVOID Context = NULL;
1031 LARGE_INTEGER Offset;
1032 PFATX_DIR_ENTRY pDirEntry;
1033 ULONG StartIndex;
1034 NTSTATUS Status;
1035
1036 ASSERT(pFcb);
1037 ASSERT(pFcb->parentFcb);
1038 ASSERT(vfatVolumeIsFatX(DeviceExt));
1039
1040 StartIndex = pFcb->startIndex;
1041
1042 Status = vfatFCBInitializeCacheFromVolume(DeviceExt, pFcb->parentFcb);
1043 if (!NT_SUCCESS(Status))
1044 {
1045 return Status;
1046 }
1047
1048 DPRINT("delEntry PathName \'%wZ\'\n", &pFcb->PathNameU);
1049 DPRINT("delete entry: %u\n", StartIndex);
1050 Offset.u.HighPart = 0;
1051 Offset.u.LowPart = (StartIndex * sizeof(FATX_DIR_ENTRY) / PAGE_SIZE) * PAGE_SIZE;
1052 _SEH2_TRY
1053 {
1054 CcPinRead(pFcb->parentFcb->FileObject, &Offset, PAGE_SIZE, PIN_WAIT, &Context, (PVOID*)&pDirEntry);
1055 }
1056 _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
1057 {
1058 DPRINT1("CcPinRead(Offset %x:%x, Length %d) failed\n", Offset.u.HighPart, Offset.u.LowPart, PAGE_SIZE);
1059 _SEH2_YIELD(return _SEH2_GetExceptionCode());
1060 }
1061 _SEH2_END;
1062 pDirEntry = &pDirEntry[StartIndex % (PAGE_SIZE / sizeof(FATX_DIR_ENTRY))];
1063 pDirEntry->FilenameLength = 0xe5;
1064 CurrentCluster = vfatDirEntryGetFirstCluster(DeviceExt,
1065 (PDIR_ENTRY)pDirEntry);
1066
1067 /* In case of moving, save properties */
1068 if (MoveContext != NULL)
1069 {
1070 MoveContext->FirstCluster = CurrentCluster;
1071 MoveContext->FileSize = pDirEntry->FileSize;
1072 MoveContext->CreationTime = pDirEntry->CreationTime;
1073 MoveContext->CreationDate = pDirEntry->CreationDate;
1074 }
1075
1076 CcSetDirtyPinnedData(Context, NULL);
1077 CcUnpinData(Context);
1078
1079 /* In case of moving, don't delete data */
1080 if (MoveContext == NULL)
1081 {
1082 while (CurrentCluster && CurrentCluster != 0xffffffff)
1083 {
1084 GetNextCluster(DeviceExt, CurrentCluster, &NextCluster);
1085 /* FIXME: check status */
1086 WriteCluster(DeviceExt, CurrentCluster, 0);
1087 CurrentCluster = NextCluster;
1088 }
1089 }
1090
1091 return STATUS_SUCCESS;
1092 }
1093
1094 /*
1095 * move an existing FAT entry
1096 */
1097 NTSTATUS
1098 VfatMoveEntry(
1099 IN PDEVICE_EXTENSION DeviceExt,
1100 IN PVFATFCB pFcb,
1101 IN PUNICODE_STRING FileName,
1102 IN PVFATFCB ParentFcb)
1103 {
1104 NTSTATUS Status;
1105 PVFATFCB OldParent;
1106 VFAT_MOVE_CONTEXT MoveContext;
1107
1108 DPRINT("VfatMoveEntry(%p, %p, %wZ, %p)\n", DeviceExt, pFcb, FileName, ParentFcb);
1109
1110 /* Delete old entry while keeping data */
1111 Status = VfatDelEntry(DeviceExt, pFcb, &MoveContext);
1112 if (!NT_SUCCESS(Status))
1113 {
1114 return Status;
1115 }
1116
1117 OldParent = pFcb->parentFcb;
1118 CcFlushCache(&OldParent->SectionObjectPointers, NULL, 0, NULL);
1119 MoveContext.InPlace = (OldParent == ParentFcb);
1120
1121 /* Add our new entry with our cluster */
1122 Status = VfatAddEntry(DeviceExt,
1123 FileName,
1124 &pFcb,
1125 ParentFcb,
1126 (vfatFCBIsDirectory(pFcb) ? FILE_DIRECTORY_FILE : 0),
1127 *pFcb->Attributes,
1128 &MoveContext);
1129
1130 CcFlushCache(&pFcb->parentFcb->SectionObjectPointers, NULL, 0, NULL);
1131
1132 return Status;
1133 }
1134
1135 extern BOOLEAN FATXIsDirectoryEmpty(PDEVICE_EXTENSION DeviceExt, PVFATFCB Fcb);
1136 extern BOOLEAN FATIsDirectoryEmpty(PDEVICE_EXTENSION DeviceExt, PVFATFCB Fcb);
1137 extern NTSTATUS FATGetNextDirEntry(PVOID *pContext, PVOID *pPage, PVFATFCB pDirFcb, PVFAT_DIRENTRY_CONTEXT DirContext, BOOLEAN First);
1138 extern NTSTATUS FATXGetNextDirEntry(PVOID *pContext, PVOID *pPage, PVFATFCB pDirFcb, PVFAT_DIRENTRY_CONTEXT DirContext, BOOLEAN First);
1139
1140 VFAT_DISPATCH FatXDispatch = {
1141 FATXIsDirectoryEmpty, // .IsDirectoryEmpty
1142 FATXAddEntry, // .AddEntry
1143 FATXDelEntry, // .DelEntry
1144 FATXGetNextDirEntry, // .GetNextDirEntry
1145 };
1146
1147 VFAT_DISPATCH FatDispatch = {
1148 FATIsDirectoryEmpty, // .IsDirectoryEmpty
1149 FATAddEntry, // .AddEntry
1150 FATDelEntry, // .DelEntry
1151 FATGetNextDirEntry, // .GetNextDirEntry
1152 };
1153
1154 /* EOF */