/*
 * PROJECT:     VFAT Filesystem
 * LICENSE:     GPL-2.0-or-later (https://spdx.org/licenses/GPL-2.0-or-later)
 * PURPOSE:     Routines to manipulate directory entries
 * COPYRIGHT:   Copyright 1998 Jason Filby <jasonfilby@yahoo.com>
 *              Copyright 2001 Rex Jolliff <rex@lvcablemodem.com>
 *              Copyright 2004-2022 Hervé Poussineau <hpoussin@reactos.org>
 */

/*  -------------------------------------------------------  INCLUDES  */

#include "vfat.h"

#define NDEBUG
#include <debug.h>

ULONG
vfatDirEntryGetFirstCluster(
    PDEVICE_EXTENSION pDeviceExt,
    PDIR_ENTRY pFatDirEntry)
{
    ULONG cluster;

    if (pDeviceExt->FatInfo.FatType == FAT32)
    {
        cluster = pFatDirEntry->Fat.FirstCluster |
                 (pFatDirEntry->Fat.FirstClusterHigh << 16);
    }
    else if (vfatVolumeIsFatX(pDeviceExt))
    {
        cluster = pFatDirEntry->FatX.FirstCluster;
    }
    else
    {
        cluster = pFatDirEntry->Fat.FirstCluster;
    }

    return  cluster;
}

BOOLEAN
FATIsDirectoryEmpty(
    PDEVICE_EXTENSION DeviceExt,
    PVFATFCB Fcb)
{
    LARGE_INTEGER FileOffset;
    PVOID Context = NULL;
    PFAT_DIR_ENTRY FatDirEntry;
    ULONG Index, MaxIndex;
    NTSTATUS Status;

    if (vfatFCBIsRoot(Fcb))
    {
        Index = 0;
    }
    else
    {
        Index = 2;
    }

    FileOffset.QuadPart = 0;
    MaxIndex = Fcb->RFCB.FileSize.u.LowPart / sizeof(FAT_DIR_ENTRY);

    Status = vfatFCBInitializeCacheFromVolume(DeviceExt, Fcb);
    if (!NT_SUCCESS(Status))
    {
        return FALSE;
    }

    while (Index < MaxIndex)
    {
        if (Context == NULL || (Index % FAT_ENTRIES_PER_PAGE) == 0)
        {
            if (Context != NULL)
            {
                CcUnpinData(Context);
            }

            _SEH2_TRY
            {
                CcMapData(Fcb->FileObject, &FileOffset, sizeof(FAT_DIR_ENTRY), MAP_WAIT, &Context, (PVOID*)&FatDirEntry);
            }
            _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
            {
                _SEH2_YIELD(return TRUE);
            }
            _SEH2_END;

            FatDirEntry += Index % FAT_ENTRIES_PER_PAGE;
            FileOffset.QuadPart += PAGE_SIZE;
        }

        if (FAT_ENTRY_END(FatDirEntry))
        {
            CcUnpinData(Context);
            return TRUE;
        }

        if (!FAT_ENTRY_DELETED(FatDirEntry))
        {
            CcUnpinData(Context);
            return FALSE;
        }

        Index++;
        FatDirEntry++;
    }

    if (Context)
    {
        CcUnpinData(Context);
    }

    return TRUE;
}

BOOLEAN
FATXIsDirectoryEmpty(
    PDEVICE_EXTENSION DeviceExt,
    PVFATFCB Fcb)
{
    LARGE_INTEGER FileOffset;
    PVOID Context = NULL;
    PFATX_DIR_ENTRY FatXDirEntry;
    ULONG Index = 0, MaxIndex;
    NTSTATUS Status;

    FileOffset.QuadPart = 0;
    MaxIndex = Fcb->RFCB.FileSize.u.LowPart / sizeof(FATX_DIR_ENTRY);

    Status = vfatFCBInitializeCacheFromVolume(DeviceExt, Fcb);
    if (!NT_SUCCESS(Status))
    {
        return FALSE;
    }

    while (Index < MaxIndex)
    {
        if (Context == NULL || (Index % FATX_ENTRIES_PER_PAGE) == 0)
        {
            if (Context != NULL)
            {
                CcUnpinData(Context);
            }

            _SEH2_TRY
            {
                CcMapData(Fcb->FileObject, &FileOffset, sizeof(FATX_DIR_ENTRY), MAP_WAIT, &Context, (PVOID*)&FatXDirEntry);
            }
            _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
            {
                _SEH2_YIELD(return TRUE);
            }
            _SEH2_END;

            FatXDirEntry += Index % FATX_ENTRIES_PER_PAGE;
            FileOffset.QuadPart += PAGE_SIZE;
        }

        if (FATX_ENTRY_END(FatXDirEntry))
        {
            CcUnpinData(Context);
            return TRUE;
        }

        if (!FATX_ENTRY_DELETED(FatXDirEntry))
        {
            CcUnpinData(Context);
            return FALSE;
        }

        Index++;
        FatXDirEntry++;
    }

    if (Context)
    {
        CcUnpinData(Context);
    }

    return TRUE;
}

NTSTATUS
FATGetNextDirEntry(
    PVOID *pContext,
    PVOID *pPage,
    IN PVFATFCB pDirFcb,
    PVFAT_DIRENTRY_CONTEXT DirContext,
    BOOLEAN First)
{
    ULONG dirMap;
    PWCHAR pName;
    LARGE_INTEGER FileOffset;
    PFAT_DIR_ENTRY fatDirEntry;
    slot * longNameEntry;
    ULONG index;

    UCHAR CheckSum, shortCheckSum;
    USHORT i;
    BOOLEAN Valid = TRUE;
    BOOLEAN Back = FALSE;

    NTSTATUS Status;

    DirContext->LongNameU.Length = 0;
    DirContext->LongNameU.Buffer[0] = UNICODE_NULL;

    FileOffset.u.HighPart = 0;
    FileOffset.u.LowPart = ROUND_DOWN(DirContext->DirIndex * sizeof(FAT_DIR_ENTRY), PAGE_SIZE);

    Status = vfatFCBInitializeCacheFromVolume(DirContext->DeviceExt, pDirFcb);
    if (!NT_SUCCESS(Status))
    {
        return Status;
    }

    if (*pContext == NULL || (DirContext->DirIndex % FAT_ENTRIES_PER_PAGE) == 0)
    {
        if (*pContext != NULL)
        {
            CcUnpinData(*pContext);
        }

        if (FileOffset.u.LowPart >= pDirFcb->RFCB.FileSize.u.LowPart)
        {
            *pContext = NULL;
            return STATUS_NO_MORE_ENTRIES;
        }

        _SEH2_TRY
        {
            CcMapData(pDirFcb->FileObject, &FileOffset, PAGE_SIZE, MAP_WAIT, pContext, pPage);
        }
        _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
        {
            *pContext = NULL;
            _SEH2_YIELD(return STATUS_NO_MORE_ENTRIES);
        }
        _SEH2_END;
    }

    fatDirEntry = (PFAT_DIR_ENTRY)(*pPage) + DirContext->DirIndex % FAT_ENTRIES_PER_PAGE;
    longNameEntry = (slot*) fatDirEntry;
    dirMap = 0;

    if (First)
    {
        /* This is the first call to vfatGetNextDirEntry. Possible the start index points
         * into a long name or points to a short name with an assigned long name.
         * We must go back to the real start of the entry */
        while (DirContext->DirIndex > 0 &&
               !FAT_ENTRY_END(fatDirEntry) &&
               !FAT_ENTRY_DELETED(fatDirEntry) &&
               ((!FAT_ENTRY_LONG(fatDirEntry) && !Back) ||
               (FAT_ENTRY_LONG(fatDirEntry) && !(longNameEntry->id & 0x40))))
        {
            DirContext->DirIndex--;
            Back = TRUE;

            if ((DirContext->DirIndex % FAT_ENTRIES_PER_PAGE) == FAT_ENTRIES_PER_PAGE - 1)
            {
                CcUnpinData(*pContext);
                FileOffset.u.LowPart -= PAGE_SIZE;

                if (FileOffset.u.LowPart >= pDirFcb->RFCB.FileSize.u.LowPart)
                {
                    *pContext = NULL;
                    return STATUS_NO_MORE_ENTRIES;
                }

                _SEH2_TRY
                {
                    CcMapData(pDirFcb->FileObject, &FileOffset, PAGE_SIZE, MAP_WAIT, pContext, pPage);
                }
                _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
                {
                    *pContext = NULL;
                    _SEH2_YIELD(return STATUS_NO_MORE_ENTRIES);
                }
                _SEH2_END;

                fatDirEntry = (PFAT_DIR_ENTRY)(*pPage) + DirContext->DirIndex % FAT_ENTRIES_PER_PAGE;
                longNameEntry = (slot*) fatDirEntry;
            }
            else
            {
                fatDirEntry--;
                longNameEntry--;
            }
        }

        if (Back && !FAT_ENTRY_END(fatDirEntry) &&
           (FAT_ENTRY_DELETED(fatDirEntry) || !FAT_ENTRY_LONG(fatDirEntry)))
        {
            DirContext->DirIndex++;

            if ((DirContext->DirIndex % FAT_ENTRIES_PER_PAGE) == 0)
            {
                CcUnpinData(*pContext);
                FileOffset.u.LowPart += PAGE_SIZE;

                if (FileOffset.u.LowPart >= pDirFcb->RFCB.FileSize.u.LowPart)
                {
                    *pContext = NULL;
                    return STATUS_NO_MORE_ENTRIES;
                }

                _SEH2_TRY
                {
                    CcMapData(pDirFcb->FileObject, &FileOffset, PAGE_SIZE, MAP_WAIT, pContext, pPage);
                }
                _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
                {
                    *pContext = NULL;
                    _SEH2_YIELD(return STATUS_NO_MORE_ENTRIES);
                }
                _SEH2_END;

                fatDirEntry = (PFAT_DIR_ENTRY)*pPage;
                longNameEntry = (slot*) *pPage;
            }
            else
            {
                fatDirEntry++;
                longNameEntry++;
            }
        }
    }

    DirContext->StartIndex = DirContext->DirIndex;
    CheckSum = 0;

    while (TRUE)
    {
        if (FAT_ENTRY_END(fatDirEntry))
        {
            CcUnpinData(*pContext);
            *pContext = NULL;
            return STATUS_NO_MORE_ENTRIES;
        }

        if (FAT_ENTRY_DELETED(fatDirEntry))
        {
            dirMap = 0;
            DirContext->LongNameU.Buffer[0] = 0;
            DirContext->StartIndex = DirContext->DirIndex + 1;
        }
        else
        {
            if (FAT_ENTRY_LONG(fatDirEntry))
            {
                if (dirMap == 0)
                {
                    DPRINT ("  long name entry found at %u\n", DirContext->DirIndex);
                    RtlZeroMemory(DirContext->LongNameU.Buffer, DirContext->LongNameU.MaximumLength);
                    CheckSum = longNameEntry->alias_checksum;
                    Valid = TRUE;
                }

                DPRINT("  name chunk1:[%.*S] chunk2:[%.*S] chunk3:[%.*S]\n",
                    5, longNameEntry->name0_4,
                    6, longNameEntry->name5_10,
                    2, longNameEntry->name11_12);

                index = longNameEntry->id & 0x3f; // Note: it can be 0 for corrupted FS

                /* Make sure index is valid and we have enough space in buffer
                  (we count one char for \0) */
                if (index > 0 &&
                    index * 13 < DirContext->LongNameU.MaximumLength / sizeof(WCHAR))
                {
                    index--; // make index 0 based
                    dirMap |= 1 << index;

                    pName = DirContext->LongNameU.Buffer + index * 13;
                    RtlCopyMemory(pName, longNameEntry->name0_4, 5 * sizeof(WCHAR));
                    RtlCopyMemory(pName + 5, longNameEntry->name5_10, 6 * sizeof(WCHAR));
                    RtlCopyMemory(pName + 11, longNameEntry->name11_12, 2 * sizeof(WCHAR));

                    if (longNameEntry->id & 0x40)
                    {
                        /* It's last LFN entry. Terminate filename with \0 */
                        pName[13] = UNICODE_NULL;
                    }
                }
                else
                    DPRINT1("Long name entry has invalid index: %x!\n", longNameEntry->id);

                DPRINT ("  longName: [%S]\n", DirContext->LongNameU.Buffer);

                if (CheckSum != longNameEntry->alias_checksum)
                {
                     DPRINT1("Found wrong alias checksum in long name entry (first %x, current %x, %S)\n",
                             CheckSum, longNameEntry->alias_checksum, DirContext->LongNameU.Buffer);
                Valid = FALSE;
                }
            }
            else
            {
                shortCheckSum = 0;
                for (i = 0; i < 11; i++)
                {
                    shortCheckSum = (((shortCheckSum & 1) << 7)
                                  | ((shortCheckSum & 0xfe) >> 1))
                                  + fatDirEntry->ShortName[i];
                }

                if (shortCheckSum != CheckSum && DirContext->LongNameU.Buffer[0])
                {
                    DPRINT1("Checksum from long and short name is not equal (short: %x, long: %x, %S)\n",
                        shortCheckSum, CheckSum, DirContext->LongNameU.Buffer);
                    DirContext->LongNameU.Buffer[0] = 0;
                }

                if (Valid == FALSE)
                {
                    DirContext->LongNameU.Buffer[0] = 0;
                }

            RtlCopyMemory (&DirContext->DirEntry.Fat, fatDirEntry, sizeof (FAT_DIR_ENTRY));
            break;
            }
        }

        DirContext->DirIndex++;

        if ((DirContext->DirIndex % FAT_ENTRIES_PER_PAGE) == 0)
        {
            CcUnpinData(*pContext);
            FileOffset.u.LowPart += PAGE_SIZE;

            if (FileOffset.u.LowPart >= pDirFcb->RFCB.FileSize.u.LowPart)
            {
                *pContext = NULL;
                return STATUS_NO_MORE_ENTRIES;
            }

            _SEH2_TRY
            {
                CcMapData(pDirFcb->FileObject, &FileOffset, PAGE_SIZE, MAP_WAIT, pContext, pPage);
            }
            _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
            {
                *pContext = NULL;
                _SEH2_YIELD(return STATUS_NO_MORE_ENTRIES);
            }
            _SEH2_END;

            fatDirEntry = (PFAT_DIR_ENTRY)*pPage;
            longNameEntry = (slot*) *pPage;
        }
        else
        {
            fatDirEntry++;
            longNameEntry++;
        }
    }

    /* Make sure filename is NULL terminate and calculate length */
    DirContext->LongNameU.Buffer[DirContext->LongNameU.MaximumLength / sizeof(WCHAR) - 1]
        = UNICODE_NULL;
    DirContext->LongNameU.Length = (USHORT)(wcslen(DirContext->LongNameU.Buffer) * sizeof(WCHAR));

    /* Init short name */
    vfat8Dot3ToString(&DirContext->DirEntry.Fat, &DirContext->ShortNameU);

    /* If we found no LFN, use short name as long */
    if (DirContext->LongNameU.Length == 0)
        RtlCopyUnicodeString(&DirContext->LongNameU, &DirContext->ShortNameU);

    return STATUS_SUCCESS;
}

NTSTATUS
FATXGetNextDirEntry(
    PVOID *pContext,
    PVOID *pPage,
    IN PVFATFCB pDirFcb,
    PVFAT_DIRENTRY_CONTEXT DirContext,
    BOOLEAN First)
{
    LARGE_INTEGER FileOffset;
    PFATX_DIR_ENTRY fatxDirEntry;
    OEM_STRING StringO;
    ULONG DirIndex = DirContext->DirIndex;
    NTSTATUS Status;

    FileOffset.u.HighPart = 0;

    UNREFERENCED_PARAMETER(First);

    if (!vfatFCBIsRoot(pDirFcb))
    {
        /* need to add . and .. entries */
        switch (DirContext->DirIndex)
        {
            case 0: /* entry . */
                wcscpy(DirContext->LongNameU.Buffer, L".");
                DirContext->LongNameU.Length = sizeof(WCHAR);
                DirContext->ShortNameU = DirContext->LongNameU;
                RtlCopyMemory(&DirContext->DirEntry.FatX, &pDirFcb->entry.FatX, sizeof(FATX_DIR_ENTRY));
                DirContext->DirEntry.FatX.Filename[0] = '.';
                DirContext->DirEntry.FatX.FilenameLength = 1;
                DirContext->StartIndex = 0;
                return STATUS_SUCCESS;

            case 1: /* entry .. */
                wcscpy(DirContext->LongNameU.Buffer, L"..");
                DirContext->LongNameU.Length = 2 * sizeof(WCHAR);
                DirContext->ShortNameU = DirContext->LongNameU;
                RtlCopyMemory(&DirContext->DirEntry.FatX, &pDirFcb->entry.FatX, sizeof(FATX_DIR_ENTRY));
                DirContext->DirEntry.FatX.Filename[0] = DirContext->DirEntry.FatX.Filename[1] = '.';
                DirContext->DirEntry.FatX.FilenameLength = 2;
                DirContext->StartIndex = 1;
                return STATUS_SUCCESS;

            default:
                DirIndex -= 2;
        }
    }

    Status = vfatFCBInitializeCacheFromVolume(DirContext->DeviceExt, pDirFcb);
    if (!NT_SUCCESS(Status))
    {
        return Status;
    }

    if (*pContext == NULL || (DirIndex % FATX_ENTRIES_PER_PAGE) == 0)
    {
        if (*pContext != NULL)
        {
            CcUnpinData(*pContext);
        }
        FileOffset.u.LowPart = ROUND_DOWN(DirIndex * sizeof(FATX_DIR_ENTRY), PAGE_SIZE);
        if (FileOffset.u.LowPart >= pDirFcb->RFCB.FileSize.u.LowPart)
        {
            *pContext = NULL;
            return STATUS_NO_MORE_ENTRIES;
        }

        _SEH2_TRY
        {
            CcMapData(pDirFcb->FileObject, &FileOffset, PAGE_SIZE, MAP_WAIT, pContext, pPage);
        }
        _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
        {
            *pContext = NULL;
            _SEH2_YIELD(return STATUS_NO_MORE_ENTRIES);
        }
        _SEH2_END;
    }

    fatxDirEntry = (PFATX_DIR_ENTRY)(*pPage) + DirIndex % FATX_ENTRIES_PER_PAGE;

    DirContext->StartIndex = DirContext->DirIndex;

    while (TRUE)
    {
        if (FATX_ENTRY_END(fatxDirEntry))
        {
            CcUnpinData(*pContext);
            *pContext = NULL;
            return STATUS_NO_MORE_ENTRIES;
        }

        if (!FATX_ENTRY_DELETED(fatxDirEntry))
        {
            RtlCopyMemory(&DirContext->DirEntry.FatX, fatxDirEntry, sizeof(FATX_DIR_ENTRY));
            break;
        }
        DirContext->DirIndex++;
        DirContext->StartIndex++;
        DirIndex++;
        if ((DirIndex % FATX_ENTRIES_PER_PAGE) == 0)
        {
            CcUnpinData(*pContext);
            FileOffset.u.LowPart += PAGE_SIZE;
            if (FileOffset.u.LowPart >= pDirFcb->RFCB.FileSize.u.LowPart)
            {
                *pContext = NULL;
                return STATUS_NO_MORE_ENTRIES;
            }

            _SEH2_TRY
            {
                CcMapData(pDirFcb->FileObject, &FileOffset, PAGE_SIZE, MAP_WAIT, pContext, pPage);
            }
            _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
            {
                *pContext = NULL;
                _SEH2_YIELD(return STATUS_NO_MORE_ENTRIES);
            }
            _SEH2_END;

            fatxDirEntry = (PFATX_DIR_ENTRY)*pPage;
        }
        else
        {
            fatxDirEntry++;
        }
    }
    StringO.Buffer = (PCHAR)fatxDirEntry->Filename;
    StringO.Length = StringO.MaximumLength = fatxDirEntry->FilenameLength;
    RtlOemStringToUnicodeString(&DirContext->LongNameU, &StringO, FALSE);
    DirContext->ShortNameU = DirContext->LongNameU;
    return STATUS_SUCCESS;
}
